mirror of
https://github.com/xcat2/confluent.git
synced 2024-11-25 02:52:07 +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']))))
|