From ea2712558711b4a8fa7269678739a0edd929da25 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 26 Sep 2017 16:56:45 -0400 Subject: [PATCH] Begin implementing the 'neighbors' collection Refactor some common function and list interfaces on a switch --- .../confluent/networking/macmap.py | 143 +++++++++++++----- confluent_server/confluent/util.py | 24 +++ 2 files changed, 128 insertions(+), 39 deletions(-) diff --git a/confluent_server/confluent/networking/macmap.py b/confluent_server/confluent/networking/macmap.py index a0b385b6..b364f832 100644 --- a/confluent_server/confluent/networking/macmap.py +++ b/confluent_server/confluent/networking/macmap.py @@ -34,6 +34,7 @@ if __name__ == '__main__': import sys import confluent.config.configmanager as cfm +import confluent.networking.lldp as lldp import confluent.exceptions as exc import confluent.log as log import confluent.messages as msg @@ -48,6 +49,7 @@ _macmap = {} _macsbyswitch = {} _nodesbymac = {} _switchportmap = {} +_neighdata = {} vintage = None @@ -179,20 +181,7 @@ def _map_switch_backend(args): except ValueError: # ifidx might be '', skip in such a case continue - ifnamemap = {} - havenames = False - for vb in conn.walk('1.3.6.1.2.1.31.1.1.1.1'): - ifidx, ifname = vb - if not ifname: - continue - havenames = True - ifidx = int(str(ifidx).rsplit('.', 1)[1]) - ifnamemap[ifidx] = str(ifname) - if not havenames: - for vb in conn.walk( '1.3.6.1.2.1.2.2.1.2'): - ifidx, ifname = vb - ifidx = int(str(ifidx).rsplit('.', 1)[1]) - ifnamemap[ifidx] = str(ifname) + ifnamemap = _get_portnamemap(conn) maccounts = {} bridgetoifvalid = False for mac in mactobridge: @@ -251,6 +240,24 @@ def _map_switch_backend(args): _nodesbymac[mac] = nodename +def _get_portnamemap(conn): + ifnamemap = {} + havenames = False + for vb in conn.walk('1.3.6.1.2.1.31.1.1.1.1'): + ifidx, ifname = vb + if not ifname: + continue + havenames = True + ifidx = int(str(ifidx).rsplit('.', 1)[1]) + ifnamemap[ifidx] = str(ifname) + if not havenames: + for vb in conn.walk('1.3.6.1.2.1.2.2.1.2'): + ifidx, ifname = vb + ifidx = int(str(ifidx).rsplit('.', 1)[1]) + ifnamemap[ifidx] = str(ifname) + return ifnamemap + + def find_node_by_mac(mac, configmanager): now = util.monotonic_time() if vintage and (now - vintage) < 90 and mac in _nodesbymac: @@ -296,6 +303,23 @@ def _finish_update(completions): for _ in completions: pass + +def _list_switches(configmanager): + nodelocations = configmanager.get_node_attributes( + configmanager.list_nodes(), ('net*.switch', 'net*.switchport')) + switches = set([]) + for node in nodelocations: + cfg = nodelocations[node] + for attr in cfg: + if not attr.endswith('.switch') or 'value' not in cfg[attr]: + continue + curswitch = cfg[attr].get('value', None) + if not curswitch: + continue + switches.add(curswitch) + return util.natural_sort(switches) + + def _full_updatemacmap(configmanager): global vintage global _macmap @@ -312,6 +336,7 @@ def _full_updatemacmap(configmanager): if configmanager.tenant is not None: raise exc.ForbiddenRequest( 'Network topology not available to tenants') + # here's a list of switches... need to add nodes that are switches nodelocations = configmanager.get_node_attributes( configmanager.list_nodes(), ('net*.switch', 'net*.switchport')) switches = set([]) @@ -340,30 +365,35 @@ def _full_updatemacmap(configmanager): _switchportmap[curswitch][portname] = None else: _switchportmap[curswitch][portname] = node - switchcfg = configmanager.get_node_attributes( - switches, ('secret.hardwaremanagementuser', 'secret.snmpcommunity', - 'secret.hardwaremanagementpassword'), decrypt=True) - switchauth = [] - for switch in switches: - if not switch: - continue - switchparms = switchcfg.get(switch, {}) - user = None - password = switchparms.get( - 'secret.snmpcommunity', {}).get('value', None) - if not password: - password = switchparms.get( - 'secret.hardwaremanagementpassword', {}).get('value', - 'public') - user = switchparms.get( - 'secret.hardwaremanagementuser', {}).get('value', None) - switchauth.append((switch, password, user)) + switchauth = _get_switchcreds(configmanager, switches) pool = GreenPool() for ans in pool.imap(_map_switch, switchauth): vintage = util.monotonic_time() yield ans +def _get_switchcreds(configmanager, switches): + switchcfg = configmanager.get_node_attributes( + switches, ('secret.hardwaremanagementuser', 'secret.snmpcommunity', + 'secret.hardwaremanagementpassword'), decrypt=True) + switchauth = [] + for switch in switches: + if not switch: + continue + switchparms = switchcfg.get(switch, {}) + user = None + password = switchparms.get( + 'secret.snmpcommunity', {}).get('value', None) + if not password: + password = switchparms.get( + 'secret.hardwaremanagementpassword', {}).get('value', + 'public') + user = switchparms.get( + 'secret.hardwaremanagementuser', {}).get('value', None) + switchauth.append((switch, password, user)) + return switchauth + + def _dump_locations(info, macaddr, nodename=None): yield msg.KeyValueData({'possiblenode': nodename, 'mac': macaddr}) retdata = {} @@ -378,7 +408,7 @@ def _dump_locations(info, macaddr, nodename=None): def handle_api_request(configmanager, inputdata, operation, pathcomponents): if operation == 'retrieve': - return handle_read_api_request(pathcomponents) + return handle_read_api_request(pathcomponents, configmanager) if (operation in ('update', 'create') and pathcomponents == ['networking', 'macs', 'rescan']): if inputdata != {'rescan': 'start'}: @@ -390,15 +420,50 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents): operation, '/'.join(pathcomponents))) -def handle_read_api_request(pathcomponents): +def _list_interfaces(switchname, configmanager): + switchcreds = _get_switchcreds(configmanager, (switchname,)) + switchcreds = switchcreds[0] + conn = snmp.Session(*switchcreds) + ifnames = _get_portnamemap(conn) + return util.natural_sort(ifnames.values()) + + +def _handle_neighbor_query(pathcomponents, configmanager): + switchname = pathcomponents[0] + if len(pathcomponents) == 1: + return [msg.ChildCollection('by-port/')] + if len(pathcomponents) == 2: + # need to list ports for the switchname + return [msg.ChildCollection(x) for x in _list_interfaces(switchname, + configmanager)] + portname = pathcomponents[2] + return [msg.ChildCollection(portname)] + + +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 if len(pathcomponents) == 1: - return [msg.ChildCollection('macs/')] + return [msg.ChildCollection('macs/'), + msg.ChildCollection('neighbors/')] + elif pathcomponents[1] == 'neighbors': + if len(pathcomponents) == 2: + return [msg.ChildCollection('by-switch/')] + elif len(pathcomponents) == 3: + return [msg.ChildCollection(x + '/') + for x in _list_switches(configmanager)] + else: + return _handle_neighbor_query(pathcomponents[3:], configmanager) elif len(pathcomponents) == 2: - return [msg.ChildCollection(x) for x in (# 'by-node/', - 'by-mac/', 'by-switch/', - 'rescan')] + if pathcomponents[-1] == 'macs': + return [msg.ChildCollection(x) for x in (# 'by-node/', + 'by-mac/', 'by-switch/', + 'rescan')] + elif pathcomponents[-1] == 'neighbors': + return [msg.ChildCollection('by-switch/')] + else: + raise exc.NotFoundException( + 'Unknown networking resource {0}'.format(pathcomponents[-1])) if False and pathcomponents[2] == 'by-node': # TODO: should be list of node names, and then under that 'by-mac' if len(pathcomponents) == 3: @@ -416,7 +481,7 @@ def handle_read_api_request(pathcomponents): elif pathcomponents[2] == 'by-switch': if len(pathcomponents) == 3: return [msg.ChildCollection(x + '/') - for x in sorted(list(_switchportmap))] + for x in _list_switches(configmanager)] if len(pathcomponents) == 4: return [msg.ChildCollection('by-port/')] if len(pathcomponents) == 5: diff --git a/confluent_server/confluent/util.py b/confluent_server/confluent/util.py index cb25d911..c02a0e0b 100644 --- a/confluent_server/confluent/util.py +++ b/confluent_server/confluent/util.py @@ -22,6 +22,7 @@ import confluent.log as log import hashlib import netifaces import os +import re import socket import struct @@ -150,3 +151,26 @@ class TLSCertVerifier(object): raise cexc.PubkeyInvalid( 'Mismatched certificate detected', certificate, fingerprint, self.fieldname, 'mismatch') + +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)