From 0c0a450fc27e615068130f0a7b0fcc24152f3b7d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 9 Jan 2018 17:25:31 -0500 Subject: [PATCH] Implement requisite functions to do chain SMM discovery Hook into the neighbor data and lldp to identify SMMs. Still need to provide context to the XCCs based on the chassis-uuid property. --- .../confluent/config/attributes.py | 13 ++-- confluent_server/confluent/discovery/core.py | 72 ++++++++++++++++++- confluent_server/confluent/networking/lldp.py | 19 +++++ .../confluent/networking/macmap.py | 13 +++- confluent_server/confluent/util.py | 12 ++-- 5 files changed, 117 insertions(+), 12 deletions(-) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 59344990..630f42ad 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -279,12 +279,17 @@ node = { 'description': 'The method used to perform operations such as power ' 'control, get sensor data, get inventory, and so on. ' }, - 'enclosure.manager': { - 'description': "The management device for this node's chassis", -# 'appliesto': ['system'], - }, 'enclosure.bay': { 'description': 'The bay in the enclosure, if any', +# 'appliesto': ['system'], + }, + 'enclosure.extends': { + 'description': 'When using an extendable enclosure, this is the node ' + 'representing the manager that is one closer to the ' + 'uplink.', + }, + 'enclosure.manager': { + 'description': "The management device for this node's chassis", # 'appliesto': ['system'], }, diff --git a/confluent_server/confluent/discovery/core.py b/confluent_server/confluent/discovery/core.py index 8f9fae91..e981e7d5 100644 --- a/confluent_server/confluent/discovery/core.py +++ b/confluent_server/confluent/discovery/core.py @@ -75,7 +75,10 @@ import confluent.messages as msg import confluent.networking.macmap as macmap import confluent.noderange as noderange import confluent.util as util +import eventlet import traceback +webclient = eventlet.import_patched('pyghmi.util.webclient') + import eventlet import eventlet.greenpool @@ -579,8 +582,8 @@ def detected(info): if handler.cert_fail_reason == 'unreachable': log.log( { - 'info': '{0} with hwaddr {1} is not reachable at {2}' - ''.format( + 'info': '{0} with hwaddr {1} is not reachable by https ' + 'at address {2}'.format( handler.devname, info['hwaddr'], handler.ipaddr )}) info['addresses'] = [x for x in info.get('addresses', []) if x != handler.ipaddr] @@ -626,6 +629,44 @@ def detected(info): unknown_info[info['hwaddr']] = info + +def b64tohex(b64str): + bd = base64.b64decode(b64str) + return ''.join(['{0:02x}'.format(ord(x)) for x in bd]) + + +def get_chained_smm_name(nodename, cfg, handler, nl): + # first we check to see if directly connected + mycert = handler.https_cert + fprints = macmap.get_node_fingerprints(nodename, cfg) + for fprint in fprints: + if util.cert_matches(fprint, mycert): + # ok we have a direct match, it is this node + return nodename + # ok, unable to get it, need to traverse the chain from the beginning + while nl: + if len(nl) != 1: + raise exc.InvalidArgumentException('Multiple enclosures trying to ' + 'extend a single enclosure') + cd = cfg.get_node_attributes(nodename, 'hardwaremanagement.manager') + smmaddr = cd[nodename]['hardwaremanagement.manager']['value'] + cv = util.TLSCertVerifier( + cfg, nodename, 'pubkeys.tls_hardwaremanager').verify_cert + wc = webclient.SecureHTTPConnection(smmaddr, verifycallback=cv) + neighs = wc.grab_json_response('/scripts/neighdata.json') + for idx in (4, 5): + if 'sha256' not in neighs[idx]: + continue + fprint = 'sha256$' + b64tohex(neighs[idx]['sha256']) + if util.cert_matches(fprint, mycert): + return nl[0] + # advance down the chain by one and try again + nodename = nl[0] + nl = list(cfg.filter_node_attributes( + 'enclosure.extends=' + nodename)) + return None + + def get_nodename(cfg, handler, info): nodename = None maccount = None @@ -646,6 +687,21 @@ def get_nodename(cfg, handler, info): if not nodename: # as a last resort, search switch for info nodename, macinfo = macmap.find_nodeinfo_by_mac(info['hwaddr'], cfg) maccount = macinfo['maccount'] + if nodename: + if handler.devname == 'SMM': + nl = list(cfg.filter_node_attributes( + 'enclosure.extends=' + nodename)) + if not nl: + # we reached the end of the chain without success + return None, None + # We found an SMM, and it's in a chain per configuration + # we need to ask the switch for the fingerprint to see + # if we have a match or not + newnodename = get_chained_smm_name(nodename, cfg, handler, + nl) + if newnodename: + return newnodename, None + if (nodename and not handler.discoverable_by_switch(macinfo['maccount'])): if handler.devname == 'SMM': @@ -693,6 +749,7 @@ def eval_node(cfg, handler, info, nodename, manual=False): if not handler.is_enclosure and nl: # The specified node is an enclosure (has nodes mapped to it), but # what we are talking to is *not* an enclosure + # might be ambiguous, need to match chassis-uuid as well.. if 'enclosure.bay' not in info: unknown_info[info['hwaddr']] = info info['discostatus'] = 'unidentified' @@ -740,6 +797,17 @@ def eval_node(cfg, handler, info, nodename, manual=False): # we can and did accurately discover by switch or in enclosure # but... is this really ok? could be on an upstream port or # erroneously put in the enclosure with no nodes yet + # so first, see if the candidate node is a chain host + if info['maccount']: + # discovery happened through switch + nl = list(cfg.filter_node_attributes( + 'enclosure.extends=' + nodename)) + if nl: + # The candidate nodename is the head of a chain, we must + # validate the smm certificate by the switch + macmap.get_node_fingerprint(nodename, cfg) + util.handler.cert_matches(fprint, handler.https_cert) + return if (info['maccount'] and not handler.discoverable_by_switch(info['maccount'])): errorstr = 'The detected node {0} was detected using switch, ' \ diff --git a/confluent_server/confluent/networking/lldp.py b/confluent_server/confluent/networking/lldp.py index dd937671..096fd9c3 100644 --- a/confluent_server/confluent/networking/lldp.py +++ b/confluent_server/confluent/networking/lldp.py @@ -33,6 +33,7 @@ if __name__ == '__main__': import sys import confluent.config.configmanager as cfm +import base64 import confluent.exceptions as exc import confluent.log as log import confluent.messages as msg @@ -118,6 +119,24 @@ def _lldpdesc_to_ifname(switchid, idx, desc): def _dump_neighbordatum(info): return [msg.KeyValueData(info)] +def b64tohex(b64str): + bd = base64.b64decode(b64str) + return ''.join(['{0:02x}'.format(ord(x)) for x in bd]) + +def get_fingerprint(switch, port, configmanager, portmatch): + update_switch_data(switch, configmanager) + for neigh in _neighbypeerid: + info = _neighbypeerid[neigh] + if neigh == '!!vintage' or info.get('switch', None) != switch: + continue + if 'peersha256fingerprint' not in info: + continue + if info.get('switch', None) != switch: + continue + if portmatch(info.get('port'), port): + return 'sha256$' + b64tohex(info['peersha256fingerprint']) + return None + def _extract_extended_desc(info, source, integritychecked): source = str(source) diff --git a/confluent_server/confluent/networking/macmap.py b/confluent_server/confluent/networking/macmap.py index 69cdddb3..67bca895 100644 --- a/confluent_server/confluent/networking/macmap.py +++ b/confluent_server/confluent/networking/macmap.py @@ -30,7 +30,7 @@ # this module will provide mac to switch and full 'ifName' label # This functionality is restricted to the null tenant -from confluent.networking.lldp import _handle_neighbor_query +from confluent.networking.lldp import _handle_neighbor_query, get_fingerprint from confluent.networking.netutil import get_switchcreds, list_switches, get_portnamemap if __name__ == '__main__': @@ -378,6 +378,17 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents): operation, '/'.join(pathcomponents))) +def get_node_fingerprints(nodename, configmanager): + cfg = configmanager.get_node_attributes(nodename, ['net*.switch', + 'net*.switchport']) + for attrkey in cfg[nodename]: + if attrkey.endswith('switch'): + switch = cfg[nodename][attrkey]['value'] + port = cfg[nodename][attrkey + 'port']['value'] + yield get_fingerprint(switch, port, configmanager, + _namesmatch) + + def handle_read_api_request(pathcomponents, configmanager): # TODO(jjohnson2): discovery core.py api handler design, apply it here # to make this a less tangled mess as it gets extended diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py index c02a0e0b..10069b24 100644 --- a/confluent_server/confluent/util.py +++ b/confluent_server/confluent/util.py @@ -100,9 +100,11 @@ def monotonic_time(): def get_fingerprint(certificate, algo='sha512'): - if algo != 'sha512': - raise Exception("TODO: Non-sha512") - return 'sha512$' + hashlib.sha512(certificate).hexdigest() + 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): @@ -110,8 +112,8 @@ def cert_matches(fingerprint, certificate): return False algo, _, fp = fingerprint.partition('$') newfp = None - if algo == 'sha512': - newfp = get_fingerprint(certificate) + if algo in ('sha512', 'sha256'): + newfp = get_fingerprint(certificate, algo) return newfp and fingerprint == newfp