mirror of
				https://github.com/xcat2/confluent.git
				synced 2025-10-31 19:32:33 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			858 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			858 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # vim: tabstop=4 shiftwidth=4 softtabstop=4
 | |
| 
 | |
| # Copyright 2017-2019 Lenovo
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| 
 | |
| # This is extracting the confluent slp support into
 | |
| # a standalone script, for use in environments that
 | |
| # can't run full confluent and/or would prefer
 | |
| # a utility style approach to SLP
 | |
| 
 | |
| 
 | |
| import os
 | |
| import random
 | |
| import select
 | |
| import socket
 | |
| import struct
 | |
| 
 | |
| _slp_services = set([
 | |
|     'service:management-hardware.IBM:integrated-management-module2',
 | |
|     'service:lenovo-smm',
 | |
|     'service:lenovo-smm2',
 | |
|     'service:ipmi',
 | |
|     'service:lighttpd',
 | |
|     'service:management-hardware.Lenovo:lenovo-xclarity-controller',
 | |
|     'service:management-hardware.IBM:chassis-management-module',
 | |
|     'service:management-hardware.Lenovo:chassis-management-module',
 | |
|     'service:io-device.Lenovo:management-module',
 | |
| ])
 | |
| 
 | |
| # SLP has a lot of ambition that was unfulfilled in practice.
 | |
| # So we have a static footer here to always use 'DEFAULT' scope, no LDAP
 | |
| # predicates, and no authentication for service requests
 | |
| srvreqfooter = b'\x00\x07DEFAULT\x00\x00\x00\x00'
 | |
| # An empty instance of the attribute list extension
 | |
| # which is defined in RFC 3059, used to indicate support for that capability
 | |
| attrlistext = b'\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00'
 | |
| 
 | |
| try:
 | |
|     IPPROTO_IPV6 = socket.IPPROTO_IPV6
 | |
| except AttributeError:
 | |
|     IPPROTO_IPV6 = 41  # Assume Windows value if socket is missing it
 | |
| 
 | |
| 
 | |
| def _parse_slp_header(packet):
 | |
|     packet = bytearray(packet)
 | |
|     if len(packet) < 16 or packet[0] != 2:
 | |
|         # discard packets that are obviously useless
 | |
|         return None
 | |
|     parsed = {
 | |
|         'function': packet[1],
 | |
|     }
 | |
|     (offset, parsed['xid'], langlen) = struct.unpack('!IHH',
 | |
|                                            bytes(b'\x00' + packet[7:14]))
 | |
|     parsed['lang'] = packet[14:14 + langlen].decode('utf-8')
 | |
|     parsed['payload'] = packet[14 + langlen:]
 | |
|     if offset:
 | |
|         parsed['offset'] = 14 + langlen
 | |
|         parsed['extoffset'] = offset
 | |
|     return parsed
 | |
| 
 | |
| 
 | |
| def _pop_url(payload):
 | |
|     urllen = struct.unpack('!H', bytes(payload[3:5]))[0]
 | |
|     url = bytes(payload[5:5+urllen]).decode('utf-8')
 | |
|     if payload[5+urllen] != 0:
 | |
|         raise Exception('Auth blocks unsupported')
 | |
|     payload = payload[5+urllen+1:]
 | |
|     return url, payload
 | |
| 
 | |
| 
 | |
| def _parse_SrvRply(parsed):
 | |
|     """ Modify passed dictionary to have parsed data
 | |
| 
 | |
| 
 | |
|     :param parsed:
 | |
|     :return:
 | |
|     """
 | |
|     payload = parsed['payload']
 | |
|     if len(payload) < 4:
 | |
|         return
 | |
|     ecode, ucount = struct.unpack('!HH', bytes(payload[0:4]))
 | |
|     if ecode:
 | |
|         parsed['errorcode'] = ecode
 | |
|     payload = payload[4:]
 | |
|     parsed['urls'] = []
 | |
|     while ucount:
 | |
|         ucount -= 1
 | |
|         url, payload = _pop_url(payload)
 | |
|         parsed['urls'].append(url)
 | |
| 
 | |
| 
 | |
| def _parse_slp_packet(packet, peer, rsps, xidmap):
 | |
|     parsed = _parse_slp_header(packet)
 | |
|     if not parsed:
 | |
|         return
 | |
|     addr = peer[0]
 | |
|     if '%' in addr:
 | |
|         addr = addr[:addr.index('%')]
 | |
|     mac = None
 | |
|     if addr in neightable:
 | |
|         identifier = neightable[addr]
 | |
|         mac = identifier
 | |
|     else:
 | |
|         identifier = addr
 | |
|     if (identifier, parsed['xid']) in rsps:
 | |
|         # avoid obviously duplicate entries
 | |
|         parsed = rsps[(identifier, parsed['xid'])]
 | |
|     else:
 | |
|         rsps[(identifier, parsed['xid'])] = parsed
 | |
|     if mac and 'hwaddr' not in parsed:
 | |
|         parsed['hwaddr'] = mac
 | |
|     if parsed['xid'] in xidmap:
 | |
|         parsed['services'] = [xidmap[parsed['xid']]]
 | |
|     if 'addresses' in parsed:
 | |
|         if peer not in parsed['addresses']:
 | |
|             parsed['addresses'].append(peer)
 | |
|     else:
 | |
|         parsed['addresses'] = [peer]
 | |
|     if parsed['function'] == 2:  # A service reply
 | |
|         _parse_SrvRply(parsed)
 | |
| 
 | |
| 
 | |
| def _v6mcasthash(srvtype):
 | |
|     # The hash algorithm described by RFC 3111
 | |
|     nums = bytearray(srvtype.encode('utf-8'))
 | |
|     hashval = 0
 | |
|     for i in nums:
 | |
|         hashval *= 33
 | |
|         hashval += i
 | |
|         hashval &= 0xffff  # only need to track the lowest 16 bits
 | |
|     hashval &= 0x3ff
 | |
|     hashval |= 0x1000
 | |
|     return '{0:x}'.format(hashval)
 | |
| 
 | |
| 
 | |
| def _generate_slp_header(payload, multicast, functionid, xid, extoffset=0):
 | |
|     if multicast:
 | |
|         flags = 0x2000
 | |
|     else:
 | |
|         flags = 0
 | |
|     packetlen = len(payload) + 16  # we have a fixed 16 byte header supported
 | |
|     if extoffset:  # if we have an offset, add 16 to account for this function
 | |
|         # generating a 16 byte header
 | |
|         extoffset += 16
 | |
|     if packetlen > 1400:
 | |
|         # For now, we aren't intending to support large SLP transmits
 | |
|         # raise an exception to help identify if such a requirement emerges
 | |
|         raise Exception("TODO: Transmit overflow packets")
 | |
|     # We always do SLP v2, and only v2
 | |
|     header = bytearray([2, functionid])
 | |
|     # SLP uses 24 bit packed integers, so in such places we pack 32 then
 | |
|     # discard the high byte
 | |
|     header.extend(struct.pack('!IH', packetlen, flags)[1:])
 | |
|     # '2' below refers to the length of the language tag
 | |
|     header.extend(struct.pack('!IHH', extoffset, xid, 2)[1:])
 | |
|     # we only do english (in SLP world, it's not like non-english appears...)
 | |
|     header.extend(b'en')
 | |
|     return header
 | |
| 
 | |
| def _generate_attr_request(service, xid):
 | |
|     service = service.encode('utf-8')
 | |
|     payload = bytearray(struct.pack('!HH', 0, len(service)) + service)
 | |
|     payload.extend(srvreqfooter)
 | |
|     header = _generate_slp_header(payload, False, functionid=6, xid=xid)
 | |
|     return header + payload
 | |
| 
 | |
| 
 | |
| 
 | |
| def _generate_request_payload(srvtype, multicast, xid, prlist=''):
 | |
|     prlist = prlist.encode('utf-8')
 | |
|     payload = bytearray(struct.pack('!H', len(prlist)) + prlist)
 | |
|     srvtype = srvtype.encode('utf-8')
 | |
|     payload.extend(struct.pack('!H', len(srvtype)) + srvtype)
 | |
|     payload.extend(srvreqfooter)
 | |
|     extoffset = len(payload)
 | |
|     payload.extend(attrlistext)
 | |
|     header = _generate_slp_header(payload, multicast, functionid=1, xid=xid,
 | |
|                                   extoffset=extoffset)
 | |
|     return header + payload
 | |
| 
 | |
| 
 | |
| def _find_srvtype(net, net4, srvtype, addresses, xid):
 | |
|     """Internal function to find a single service type
 | |
| 
 | |
|     Helper to do singleton requests to srvtype
 | |
| 
 | |
|     :param net: Socket active
 | |
|     :param srvtype: Service type to do now
 | |
|     :param addresses:  Pass through of addresses argument from find_targets
 | |
|     :return:
 | |
|     """
 | |
|     data = _generate_request_payload(srvtype, True, xid)
 | |
|     if addresses is not None:
 | |
|         for addr in addresses:
 | |
|             for saddr in socket.getaddrinfo(addr, 427):
 | |
|                 if saddr[0] == socket.AF_INET:
 | |
|                     net4.sendto(data, saddr[4])
 | |
|                 elif saddr[0] == socket.AF_INET6:
 | |
|                     net.sendto(data, saddr[4])
 | |
|     else:
 | |
|         net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
 | |
|         v6addrs = []
 | |
|         v6hash = _v6mcasthash(srvtype)
 | |
|         # do 'interface local' and 'link local'
 | |
|         # it shouldn't make sense, but some configurations work with interface
 | |
|         # local that do not work with link local
 | |
|         v6addrs.append(('ff01::1:' + v6hash, 427, 0, 0))
 | |
|         v6addrs.append(('ff02::1:' + v6hash, 427, 0, 0))
 | |
|         for idx in list_interface_indexes():
 | |
|             # IPv6 multicast is by index, so lead with that
 | |
|             net.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
 | |
|             for sa in v6addrs:
 | |
|                 try:
 | |
|                     net.sendto(data, sa)
 | |
|                 except socket.error:
 | |
|                     # if we hit an interface without ipv6 multicast,
 | |
|                     # this can cause an error, skip such an interface
 | |
|                     # case in point, 'lo'
 | |
|                     pass
 | |
|         for i4 in list_ips():
 | |
|             if 'broadcast' not in i4:
 | |
|                 continue
 | |
|             addr = i4['addr']
 | |
|             bcast = i4['broadcast']
 | |
|             net4.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
 | |
|                            socket.inet_aton(addr))
 | |
|             try:
 | |
|                 net4.sendto(data, ('239.255.255.253', 427))
 | |
|             except socket.error as se:
 | |
|                 # On occasion, multicasting may be disabled
 | |
|                 # tolerate this scenario and move on
 | |
|                 if se.errno != 101:
 | |
|                     raise
 | |
|             net4.sendto(data, (bcast, 427))
 | |
| 
 | |
| 
 | |
| def _grab_rsps(socks, rsps, interval, xidmap):
 | |
|     r = None
 | |
|     res = select.select(socks, (), (), interval)
 | |
|     if res:
 | |
|         r = res[0]
 | |
|     while r:
 | |
|         for s in r:
 | |
|             (rsp, peer) = s.recvfrom(9000)
 | |
|             refresh_neigh()
 | |
|             _parse_slp_packet(rsp, peer, rsps, xidmap)
 | |
|             res = select.select(socks, (), (), interval)
 | |
|             if not res:
 | |
|                 r = None
 | |
|             else:
 | |
|                 r = res[0]
 | |
| 
 | |
| 
 | |
| 
 | |
| def _parse_attrlist(attrstr):
 | |
|     attribs = {}
 | |
|     previousattrlen = None
 | |
|     attrstr = stringify(attrstr)
 | |
|     while attrstr:
 | |
|         if len(attrstr) == previousattrlen:
 | |
|             raise Exception('Looping in attrstr parsing')
 | |
|         previousattrlen = len(attrstr)
 | |
|         if attrstr[0] == '(':
 | |
|             if ')' not in attrstr:
 | |
|                 attribs['INCOMPLETE'] = True
 | |
|                 return attribs
 | |
|             currattr = attrstr[1:attrstr.index(')')]
 | |
|             if '=' not in currattr:  # Not allegedly kosher, but still..
 | |
|                 attribs[currattr] = None
 | |
|             else:
 | |
|                 attrname, attrval = currattr.split('=', 1)
 | |
|                 attribs[attrname] = []
 | |
|                 for val in attrval.split(','):
 | |
|                     if val[:3] == '\\FF':  # we should make this bytes
 | |
|                         finalval = bytearray([])
 | |
|                         for bnum in attrval[3:].split('\\'):
 | |
|                             if bnum == '':
 | |
|                                 continue
 | |
|                             finalval.append(int(bnum, 16))
 | |
|                         val = finalval
 | |
|                         if 'uuid' in attrname and len(val) == 16:
 | |
|                             lebytes = struct.unpack_from(
 | |
|                                 '<IHH', memoryview(val[:8]))
 | |
|                             bebytes = struct.unpack_from(
 | |
|                                 '>HHI', memoryview(val[8:]))
 | |
|                             val = '{0:08X}-{1:04X}-{2:04X}-{3:04X}-' \
 | |
|                                   '{4:04X}{5:08X}'.format(
 | |
|                                 lebytes[0], lebytes[1], lebytes[2], bebytes[0],
 | |
|                                 bebytes[1], bebytes[2]
 | |
|                             ).lower()
 | |
|                     attribs[attrname].append(val)
 | |
|             attrstr = attrstr[attrstr.index(')'):]
 | |
|         elif attrstr[0] == ','[0]:
 | |
|             attrstr = attrstr[1:]
 | |
|         elif ',' in attrstr:
 | |
|             currattr = attrstr[:attrstr.index(',')]
 | |
|             attribs[currattr] = None
 | |
|             attrstr = attrstr[attrstr.index(','):]
 | |
|         else:
 | |
|             currattr = attrstr
 | |
|             attribs[currattr] = None
 | |
|             attrstr = None
 | |
|     return attribs
 | |
| 
 | |
| 
 | |
| def _parse_attrs(data, parsed, xid=None):
 | |
|     headinfo = _parse_slp_header(data)
 | |
|     if xid is None:
 | |
|         xid = parsed['xid']
 | |
|     if headinfo['function'] != 7 or headinfo['xid'] != xid:
 | |
|         return
 | |
|     payload = headinfo['payload']
 | |
|     if struct.unpack('!H', bytes(payload[:2]))[0] != 0:
 | |
|         return
 | |
|     length = struct.unpack('!H', bytes(payload[2:4]))[0]
 | |
|     attrstr = bytes(payload[4:4+length])
 | |
|     parsed['attributes'] = _parse_attrlist(attrstr)
 | |
| 
 | |
| 
 | |
| def fix_info(info, handler):
 | |
|     if '_attempts' not in info:
 | |
|         info['_attempts'] = 10
 | |
|     if info['_attempts'] == 0:
 | |
|         return
 | |
|     info['_attempts'] -= 1
 | |
|     _add_attributes(info)
 | |
|     handler(info)
 | |
| 
 | |
| def _add_attributes(parsed):
 | |
|     xid = parsed.get('xid', 42)
 | |
|     attrq = _generate_attr_request(parsed['services'][0], xid)
 | |
|     target = None
 | |
|     # prefer reaching out to an fe80 if present, to be highly robust
 | |
|     # in face of network changes
 | |
|     for addr in parsed['addresses']:
 | |
|         if addr[0].startswith('fe80'):
 | |
|             target = addr
 | |
|     # however if no fe80 seen, roll with the first available address
 | |
|     if not target:
 | |
|         target = parsed['addresses'][0]
 | |
|     if len(target) == 4:
 | |
|         net = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
 | |
|     else:
 | |
|         net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 | |
|     try:
 | |
|         net.settimeout(2.0)
 | |
|         net.connect(target)
 | |
|     except socket.error:
 | |
|         return
 | |
|     try:
 | |
|         net.sendall(attrq)
 | |
|         rsp = net.recv(8192)
 | |
|         net.close()
 | |
|         _parse_attrs(rsp, parsed, xid)
 | |
|     except Exception as e:
 | |
|         # this can be a messy area, just degrade the quality of rsp
 | |
|         # in a bad situation
 | |
|         return
 | |
| 
 | |
| 
 | |
| def unicast_scan(address):
 | |
|     pass
 | |
| 
 | |
| def query_srvtypes(target):
 | |
|     """Query the srvtypes advertised by the target
 | |
| 
 | |
|     :param target: A sockaddr tuple (if you get the peer info)
 | |
|     """
 | |
|     payload = b'\x00\x00\xff\xff\x00\x07DEFAULT'
 | |
|     header = _generate_slp_header(payload, False, functionid=9, xid=1)
 | |
|     packet = header + payload
 | |
|     if len(target) == 2:
 | |
|         net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 | |
|     elif len(target) == 4:
 | |
|         net = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
 | |
|     else:
 | |
|         raise Exception('Unrecognized target {0}'.format(repr(target)))
 | |
|     tries = 3
 | |
|     connected = False
 | |
|     while tries and not connected:
 | |
|         tries -= 1
 | |
|         try:
 | |
|             net.settimeout(1.0)
 | |
|             net.connect(target)
 | |
|             connected = True
 | |
|         except socket.error:
 | |
|             pass
 | |
|     if not connected:
 | |
|         return [u'']
 | |
|     net.sendall(packet)
 | |
|     rs = net.recv(8192)
 | |
|     net.close()
 | |
|     parsed = _parse_slp_header(rs)
 | |
|     if parsed:
 | |
|         payload = parsed['payload']
 | |
|         if payload[:2] != '\x00\x00':
 | |
|             return
 | |
|         stypelen = struct.unpack('!H', bytes(payload[2:4]))[0]
 | |
|         stypes = payload[4:4+stypelen].decode('utf-8')
 | |
|         return stypes.split(',')
 | |
| 
 | |
| def rescan(handler):
 | |
|     known_peers = set([])
 | |
|     for scanned in scan():
 | |
|         for addr in scanned['addresses']:
 | |
|             ip = addr[0].partition('%')[0]  # discard scope if present
 | |
|             if ip not in neightable:
 | |
|                 continue
 | |
|             if addr in known_peers:
 | |
|                 break
 | |
|             known_peers.add(addr)
 | |
|         else:
 | |
|             handler(scanned)
 | |
| 
 | |
| 
 | |
| def snoop(handler, protocol=None):
 | |
|     """Watch for SLP activity
 | |
| 
 | |
|     handler will be called with a dictionary of relevant attributes
 | |
| 
 | |
|     :param handler:
 | |
|     :return:
 | |
|     """
 | |
|     try:
 | |
|         active_scan(handler, protocol)
 | |
|     except Exception as e:
 | |
|         raise
 | |
|     net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
 | |
|     net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
 | |
|     slpg = socket.inet_pton(socket.AF_INET6, 'ff01::123')
 | |
|     slpg2 = socket.inet_pton(socket.AF_INET6, 'ff02::123')
 | |
|     for i6idx in list_interface_indexes():
 | |
|         mreq = slpg + struct.pack('=I', i6idx)
 | |
|         net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
 | |
|         mreq = slpg2 + struct.pack('=I', i6idx)
 | |
|         net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
 | |
|     net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 | |
|     net.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 | |
|     net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 | |
|     for i4 in list_ips():
 | |
|         if 'broadcast' not in i4:
 | |
|             continue
 | |
|         slpmcast = socket.inet_aton('239.255.255.253') + \
 | |
|             socket.inet_aton(i4['addr'])
 | |
|         try:
 | |
|             net4.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
 | |
|                             slpmcast)
 | |
|         except socket.error as e:
 | |
|             if e.errno != 98:
 | |
|                 raise
 | |
|             # socket in use can occur when aliased ipv4 are encountered
 | |
|     net.bind(('', 427))
 | |
|     net4.bind(('', 427))
 | |
| 
 | |
|     while True:
 | |
|         try:
 | |
|             newmacs = set([])
 | |
|             r, _, _ = select.select((net, net4), (), (), 60)
 | |
|             # clear known_peers and peerbymacaddress
 | |
|             # to avoid stale info getting in...
 | |
|             # rely upon the select(0.2) to catch rapid fire and aggregate ip
 | |
|             # addresses that come close together
 | |
|             # calling code needs to understand deeper context, as snoop
 | |
|             # will now yield dupe info over time
 | |
|             known_peers = set([])
 | |
|             peerbymacaddress = {}
 | |
|             while r:
 | |
|                 for s in r:
 | |
|                     (rsp, peer) = s.recvfrom(9000)
 | |
|                     ip = peer[0].partition('%')[0]
 | |
|                     if peer in known_peers:
 | |
|                         continue
 | |
|                     if ip not in neightable:
 | |
|                         update_neigh()
 | |
|                     if ip not in neightable:
 | |
|                         continue
 | |
|                     known_peers.add(peer)
 | |
|                     mac = neightable[ip]
 | |
|                     if mac in peerbymacaddress:
 | |
|                         peerbymacaddress[mac]['addresses'].append(peer)
 | |
|                     else:
 | |
|                         q = query_srvtypes(peer)
 | |
|                         if not q or not q[0]:
 | |
|                             # SLP might have started and not ready yet
 | |
|                             # ignore for now
 | |
|                             known_peers.discard(peer)
 | |
|                             continue
 | |
|                         # we want to prioritize the very well known services
 | |
|                         svcs = []
 | |
|                         for svc in q:
 | |
|                             if svc in _slp_services:
 | |
|                                 svcs.insert(0, svc)
 | |
|                             else:
 | |
|                                 svcs.append(svc)
 | |
|                         peerbymacaddress[mac] = {
 | |
|                             'services': svcs,
 | |
|                             'addresses': [peer],
 | |
|                         }
 | |
|                     newmacs.add(mac)
 | |
|                 r, _, _ = select.select((net, net4), (), (), 0.2)
 | |
|             for mac in newmacs:
 | |
|                 peerbymacaddress[mac]['xid'] = 1
 | |
|                 _add_attributes(peerbymacaddress[mac])
 | |
|                 peerbymacaddress[mac]['hwaddr'] = mac
 | |
|                 peerbymacaddress[mac]['protocol'] = protocol
 | |
|                 for srvurl in peerbymacaddress[mac].get('urls', ()):
 | |
|                     if len(srvurl) > 4:
 | |
|                         srvurl = srvurl[:-3]
 | |
|                     if srvurl.endswith('://Athena:'):
 | |
|                         continue
 | |
|                 if 'service:ipmi' in peerbymacaddress[mac]['services']:
 | |
|                     continue
 | |
|                 if 'service:lightttpd' in peerbymacaddress[mac]['services']:
 | |
|                     currinf = peerbymacaddress[mac]
 | |
|                     curratt = currinf.get('attributes', {})
 | |
|                     if curratt.get('System-Manufacturing', [None])[0] == 'Lenovo' and curratt.get('type', [None])[0] == 'LenovoThinkServer':
 | |
|                         peerbymacaddress[mac]['services'] = ['service:lenovo-tsm']
 | |
|                     else:
 | |
|                         continue
 | |
|                 handler(peerbymacaddress[mac])
 | |
|         except Exception as e:
 | |
|             raise
 | |
| 
 | |
| 
 | |
| def active_scan(handler, protocol=None):
 | |
|     known_peers = set([])
 | |
|     for scanned in scan():
 | |
|         for addr in scanned['addresses']:
 | |
|             ip = addr[0].partition('%')[0]  # discard scope if present
 | |
|             if ip not in neightable:
 | |
|                 continue
 | |
|             if addr in known_peers:
 | |
|                 break
 | |
|             known_peers.add(addr)
 | |
|         else:
 | |
|             scanned['protocol'] = protocol
 | |
|             handler(scanned)
 | |
| 
 | |
| 
 | |
| def scan(srvtypes=_slp_services, addresses=None, localonly=False):
 | |
|     """Find targets providing matching requested srvtypes
 | |
| 
 | |
|     This is a generator that will iterate over respondants to the SrvType
 | |
|     requested.
 | |
| 
 | |
|     :param srvtypes: An iterable list of the service types to find
 | |
|     :param addresses: An iterable of addresses/ranges.  Default is to scan
 | |
|                       local network segment using multicast and broadcast.
 | |
|                       Each address can be a single address, hyphen-delimited
 | |
|                       range, or an IP/CIDR indication of a network.
 | |
|     :return: Iterable set of results
 | |
|     """
 | |
|     net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
 | |
|     net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 | |
|     # increase RCVBUF to max, mitigate chance of
 | |
|     # failure due to full buffer.
 | |
|     net.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16777216)
 | |
|     net4.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16777216)
 | |
|     # SLP is very poor at scanning large counts and managing it, so we
 | |
|     # must make the best of it
 | |
|     # Some platforms/config default to IPV6ONLY, we are doing IPv4
 | |
|     # too, so force it
 | |
|     #net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
 | |
|     # we are going to do broadcast, so allow that...
 | |
|     initxid = random.randint(0, 32768)
 | |
|     xididx = 0
 | |
|     xidmap = {}
 | |
|     # First we give fast repsonders of each srvtype individual chances to be
 | |
|     # processed, mitigating volume of response traffic
 | |
|     rsps = {}
 | |
|     for srvtype in srvtypes:
 | |
|         xididx += 1
 | |
|         _find_srvtype(net, net4, srvtype, addresses, initxid + xididx)
 | |
|         xidmap[initxid + xididx] = srvtype
 | |
|         _grab_rsps((net, net4), rsps, 0.1, xidmap)
 | |
|         # now do a more slow check to work to get stragglers,
 | |
|         # but fortunately the above should have taken the brunt of volume, so
 | |
|         # reduced chance of many responses overwhelming receive buffer.
 | |
|     _grab_rsps((net, net4), rsps, 1, xidmap)
 | |
|     # now to analyze and flesh out the responses
 | |
|     for id in rsps:
 | |
|         for srvurl in rsps[id].get('urls', ()):
 | |
|             if len(srvurl) > 4:
 | |
|                 srvurl = srvurl[:-3]
 | |
|             if srvurl.endswith('://Athena:'):
 | |
|                 continue
 | |
|         if 'service:ipmi' in rsps[id]['services']:
 | |
|             continue
 | |
|         if localonly:
 | |
|             for addr in rsps[id]['addresses']:
 | |
|                 if 'fe80' in addr[0]:
 | |
|                     break
 | |
|             else:
 | |
|                 continue
 | |
|         _add_attributes(rsps[id])
 | |
|         if 'service:lighttpd' in rsps[id]['services']:
 | |
|             currinf = rsps[id]
 | |
|             curratt = currinf.get('attributes', {})
 | |
|             if curratt.get('System-Manufacturing', [None])[0] == 'Lenovo' and curratt.get('type', [None])[0] == 'LenovoThinkServer':
 | |
|                currinf['services'] = ['service:lenovo-tsm']
 | |
|                serialnumber = curratt.get('Product-Serial', curratt.get('SerialNumber', None))
 | |
|                if serialnumber:
 | |
|                    curratt['enclosure-serial-number'] = serialnumber
 | |
|                mtm = curratt.get('Machine-Type', curratt.get('Product-Name', None))
 | |
|                if mtm:
 | |
|                    mtm[0] = mtm[0].rstrip()
 | |
|                    curratt['enclosure-machinetype-model'] = mtm
 | |
|             else:
 | |
|                 continue
 | |
|         del rsps[id]['payload']
 | |
|         del rsps[id]['function']
 | |
|         del rsps[id]['xid']
 | |
|         yield rsps[id]
 | |
| 
 | |
| 
 | |
| # vim: tabstop=4 shiftwidth=4 softtabstop=4
 | |
| 
 | |
| # Copyright 2016 Lenovo
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| 
 | |
| # A consolidated manage of neighbor table information management.
 | |
| # Ultimately, this should use AF_NETLINK, but in the interest of time,
 | |
| # use ip neigh for the moment
 | |
| 
 | |
| import subprocess
 | |
| import os
 | |
| 
 | |
| neightable = {}
 | |
| neightime = 0
 | |
| 
 | |
| import re
 | |
| 
 | |
| _validmac = re.compile('..:..:..:..:..:..')
 | |
| 
 | |
| 
 | |
| def update_neigh():
 | |
|     global neightable
 | |
|     global neightime
 | |
|     neightable = {}
 | |
|     if os.name == 'nt':
 | |
|         return
 | |
|     ipn = subprocess.Popen(['ip', 'neigh'], stdin=subprocess.PIPE,
 | |
|                                  stdout=subprocess.PIPE,
 | |
|                                  stderr=subprocess.PIPE)
 | |
|     (neighdata, err) = ipn.communicate()
 | |
|     neighdata = stringify(neighdata)
 | |
|     for entry in neighdata.split('\n'):
 | |
|         entry = entry.split(' ')
 | |
|         if len(entry) < 5 or not entry[4]:
 | |
|             continue
 | |
|         if entry[0] in ('192.168.0.100', '192.168.70.100', '192.168.70.125'):
 | |
|             # Note that these addresses are common static ip addresses
 | |
|             # that are hopelessly ambiguous if there are many
 | |
|             # so ignore such entries and move on
 | |
|             # ideally the system network steers clear of this landmine of
 | |
|             # a subnet, but just in case
 | |
|             continue
 | |
|         if not _validmac.match(entry[4]):
 | |
|             continue
 | |
|         neightable[entry[0]] = entry[4]
 | |
|     neightime = os.times()[4]
 | |
| 
 | |
| 
 | |
| def refresh_neigh():
 | |
|     global neightime
 | |
|     if os.name == 'nt':
 | |
|         return
 | |
|     if os.times()[4] > (neightime + 30):
 | |
|         update_neigh()
 | |
| # vim: tabstop=4 shiftwidth=4 softtabstop=4
 | |
| 
 | |
| # Copyright 2014 IBM Corporation
 | |
| # Copyright 2015-2017 Lenovo
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| 
 | |
| # Various utility functions that do not neatly fit into one category or another
 | |
| import base64
 | |
| import hashlib
 | |
| import netifaces
 | |
| import os
 | |
| import re
 | |
| import socket
 | |
| import ssl
 | |
| import struct
 | |
| 
 | |
| def stringify(instr):
 | |
|     # Normalize unicode and bytes to 'str', correcting for
 | |
|     # current python version
 | |
|     if isinstance(instr, bytes) and not isinstance(instr, str):
 | |
|         return instr.decode('utf-8', errors='replace')
 | |
|     elif not isinstance(instr, bytes) and not isinstance(instr, str):
 | |
|         return instr.encode('utf-8')
 | |
|     return instr
 | |
| 
 | |
| def list_interface_indexes():
 | |
|     # Getting the interface indexes in a portable manner
 | |
|     # would be better, but there's difficulty from a python perspective.
 | |
|     # For now be linux specific
 | |
|     try:
 | |
|         for iface in os.listdir('/sys/class/net/'):
 | |
|             if not os.path.exists('/sys/class/net/{0}/ifindex'.format(iface)):
 | |
|                 continue
 | |
|             ifile = open('/sys/class/net/{0}/ifindex'.format(iface), 'r')
 | |
|             intidx = int(ifile.read())
 | |
|             ifile.close()
 | |
|             yield intidx
 | |
|     except (IOError, OSError):
 | |
|         # Probably situation is non-Linux, just do limited support for
 | |
|         # such platforms until other people come along
 | |
|         for iface in netifaces.interfaces():
 | |
|             addrinfo = netifaces.ifaddresses(iface).get(socket.AF_INET6, [])
 | |
|             for addr in addrinfo:
 | |
|                 v6addr = addr.get('addr', '').partition('%')[2]
 | |
|                 if v6addr:
 | |
|                     yield(int(v6addr))
 | |
|                     break
 | |
|         return
 | |
| 
 | |
| 
 | |
| def list_ips():
 | |
|     # Used for getting addresses to indicate the multicast address
 | |
|     # as well as getting all the broadcast addresses
 | |
|     for iface in netifaces.interfaces():
 | |
|         addrs = netifaces.ifaddresses(iface)
 | |
|         if netifaces.AF_INET in addrs:
 | |
|             for addr in addrs[netifaces.AF_INET]:
 | |
|                 yield addr
 | |
| 
 | |
| def randomstring(length=20):
 | |
|     """Generate a random string of requested length
 | |
| 
 | |
|     :param length: The number of characters to produce, defaults to 20
 | |
|     """
 | |
|     chunksize = length // 4
 | |
|     if length % 4 > 0:
 | |
|         chunksize += 1
 | |
|     strval = base64.urlsafe_b64encode(os.urandom(chunksize * 3))
 | |
|     return stringify(strval[0:length])
 | |
| 
 | |
| 
 | |
| def securerandomnumber(low=0, high=4294967295):
 | |
|     """Return a random number within requested range
 | |
| 
 | |
|     Note that this function will not return smaller than 0 nor larger
 | |
|     than 2^32-1 no matter what is requested.
 | |
|     The python random number facility does not provide characteristics
 | |
|     appropriate for secure rng, go to os.urandom
 | |
| 
 | |
|     :param low: Smallest number to return (defaults to 0)
 | |
|     :param high: largest number to return (defaults to 2^32-1)
 | |
|     """
 | |
|     number = -1
 | |
|     while number < low or number > high:
 | |
|         number = struct.unpack("I", os.urandom(4))[0]
 | |
|     return number
 | |
| 
 | |
| 
 | |
| def monotonic_time():
 | |
|     """Return a monotoc time value
 | |
| 
 | |
|     In scenarios like timeouts and such, monotonic timing is preferred.
 | |
|     """
 | |
|     # for now, just support POSIX systems
 | |
|     return os.times()[4]
 | |
| 
 | |
| 
 | |
| def get_certificate_from_file(certfile):
 | |
|     cert = open(certfile, 'r').read()
 | |
|     inpemcert = False
 | |
|     prunedcert = ''
 | |
|     for line in cert.split('\n'):
 | |
|         if '-----BEGIN CERTIFICATE-----' in line:
 | |
|             inpemcert = True
 | |
|         if inpemcert:
 | |
|             prunedcert += line
 | |
|         if '-----END CERTIFICATE-----' in line:
 | |
|             break
 | |
|     return ssl.PEM_cert_to_DER_cert(prunedcert)
 | |
| 
 | |
| 
 | |
| def get_fingerprint(certificate, algo='sha512'):
 | |
|     if algo == 'sha256':
 | |
|         return 'sha256$' + hashlib.sha256(certificate).hexdigest()
 | |
|     elif algo == 'sha512':
 | |
|         return 'sha512$' + hashlib.sha512(certificate).hexdigest()
 | |
|     raise Exception('Unsupported fingerprint algorithm ' + algo)
 | |
| 
 | |
| 
 | |
| def cert_matches(fingerprint, certificate):
 | |
|     if not fingerprint or not certificate:
 | |
|         return False
 | |
|     algo, _, fp = fingerprint.partition('$')
 | |
|     newfp = None
 | |
|     if algo in ('sha512', 'sha256'):
 | |
|         newfp = get_fingerprint(certificate, algo)
 | |
|     return newfp and fingerprint == newfp
 | |
| 
 | |
| 
 | |
| numregex = re.compile('([0-9]+)')
 | |
| 
 | |
| def naturalize_string(key):
 | |
|     """Analyzes string in a human way to enable natural sort
 | |
| 
 | |
|     :param nodename: The node name to analyze
 | |
|     :returns: A structure that can be consumed by 'sorted'
 | |
|     """
 | |
|     return [int(text) if text.isdigit() else text.lower()
 | |
|             for text in re.split(numregex, key)]
 | |
| 
 | |
| def natural_sort(iterable):
 | |
|     """Return a sort using natural sort if possible
 | |
| 
 | |
|     :param iterable:
 | |
|     :return:
 | |
|     """
 | |
|     try:
 | |
|         return sorted(iterable, key=naturalize_string)
 | |
|     except TypeError:
 | |
|         # The natural sort attempt failed, fallback to ascii sort
 | |
|         return sorted(iterable)
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     print(repr(list(scan(addresses=['10.240.52.189']))))
 |