From ee679b745e955e3d555b49500ae2d09aa3336abb Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 28 Jun 2016 14:21:21 -0400 Subject: [PATCH 1/6] Add a util function for SNMP On the path to instrumenting network switches, first we'll add some framework for SNMP. Given that we are using eventlet and thus we need a patchable SNMP, we employ PySNMP, despite it being a bit peculiar. This commit tucks away the oddness and makes it pretty easy to use for our purposes. --- confluent_server/confluent/snmputil.py | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 confluent_server/confluent/snmputil.py diff --git a/confluent_server/confluent/snmputil.py b/confluent_server/confluent/snmputil.py new file mode 100644 index 00000000..b747c0b7 --- /dev/null +++ b/confluent_server/confluent/snmputil.py @@ -0,0 +1,89 @@ +# 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. + +# This provides a simplified wrapper around snmp implementation roughly +# mapping to the net-snmp commands + +# net-snmp-python was considered as the API is cleaner, but the ability to +# patch pysnmp to have it be eventlet friendly has caused it's selection +# This module simplifies the complex hlapi pysnmp interface + +import confluent.exceptions as exc +import eventlet +from eventlet.support.greendns import getaddrinfo +import socket +snmp = eventlet.import_patched('pysnmp.hlapi') + + +def _get_transport(name): + # Annoyingly, pysnmp does not automatically determine ipv6 v ipv4 + res = getaddrinfo(name, 161, 0, socket.SOCK_DGRAM) + if res[0][0] == socket.AF_INET6: + return snmp.Udp6TransportTarget(res[0][4]) + else: + return snmp.UdpTransportTarget(res[0][4]) + + +def walk(server, oid, secret, username=None, context=None): + """Walk over children of a given OID + + This is roughly equivalent to snmpwalk. It will automatically try to be + an snmpbulkwalk if possible. If username is not given, it is assumed that + the secret is a community string, and v2c is used. If a username given, + it'll assume SHA auth and DES privacy with the secret being the same for + both. + + :param server: The network name/address to target + :param oid: The SNMP object identifier + :param secret: The community string or password + :param username: The username for SNMPv3 + :param context: The SNMPv3 context or index for community string indexing + """ + # SNMP is a complicated mess of things. Will endeavor to shield caller + # from as much as possible, assuming reasonable defaults where possible. + # there may come a time where we add more parameters to override the + # automatic behavior (e.g. DES is weak, so it's a likely candidate to be + # overriden, but some devices only support DES) + tp = _get_transport(server) + ctx = snmp.ContextData(context) + if '::' in oid: + mib, field = oid.split('::') + obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field)) + else: + obj = snmp.ObjectType(snmp.ObjectIdentity(oid)) + eng = snmp.SnmpEngine() + if username is None: + # SNMP v2c + authdata = snmp.CommunityData(secret, mpModel=1) + else: + authdata = snmp.UsmUserData(username, authKey=secret, privKey=secret) + walking = snmp.bulkCmd(eng, authdata, tp, ctx, 0, 10, obj, + lexicographicMode=False) + for rsp in walking: + errstr, errnum, erridx, answers = rsp + if errstr: + raise exc.TargetEndpointUnreachable(str(errstr)) + elif errnum: + raise exc.ConfluentException(errnum.prettyPrint()) + for ans in answers: + yield ans + + +if __name__ == '__main__': + import sys + for kp in walk(sys.argv[1], sys.argv[2], 'public'): + print(str(kp[0])) + print(str(kp[1])) From 8387f0e13e3063e460b19a33e434f1d0c6e30075 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 29 Jun 2016 11:26:46 -0400 Subject: [PATCH 2/6] Implement the next layer of switch discovery Refactor the snmputil to be object oriented to simplify upstream code. Implement a method to generate a mac address to ifName/ifDescr for a given switch. --- .../confluent/networking/__init__.py | 0 .../confluent/networking/macmap.py | 97 ++++++++++++++++++ confluent_server/confluent/snmputil.py | 98 +++++++++++-------- 3 files changed, 153 insertions(+), 42 deletions(-) create mode 100644 confluent_server/confluent/networking/__init__.py create mode 100644 confluent_server/confluent/networking/macmap.py diff --git a/confluent_server/confluent/networking/__init__.py b/confluent_server/confluent/networking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/confluent_server/confluent/networking/macmap.py b/confluent_server/confluent/networking/macmap.py new file mode 100644 index 00000000..fd601f7b --- /dev/null +++ b/confluent_server/confluent/networking/macmap.py @@ -0,0 +1,97 @@ +# 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. + +# This provides the implementation of locating MAC addresses on ethernet +# switches. It is, essentially, a port of 'MacMap.pm' to confluent. +# However, there are enhancements. +# For one, each switch interrogation is handled in an eventlet 'thread' +# For another, MAC addresses are checked in the dictionary on every +# switch return, rather than waiting for all switches to check in +# (which makes it more responsive when there is a missing or bad switch) +# Also, we track the quantity, actual ifName value, and provide a mechanism +# to detect ambiguous result (e.g. if two matches are found, can log an error +# rather than doing the wrong one, complete with the detected ifName value). +# Further, the map shall be available to all facets of the codebase, not just +# the discovery process, so that the cached data maintenance will pay off +# for direct queries + +# this module will provide mac to switch and full 'ifName' label +# This functionality is restricted to the null tenant + +import confluent.exceptions as exc +import confluent.snmputil as snmp + +_macmap = {} + + +def _map_switch(switch, password, user=None): + """Manipulate portions of mac address map relevant to a given switch + """ + + # 1.3.6.1.2.1.17.7.1.2.2.1.2 - mactoindex (qbridge - preferred) + # if not, check for cisco and if cisco, build list of all relevant vlans: + # .1.3.6.1.4.1.9.9.46.1.6.1.1.5 - trunk port vlan map (cisco only) + # .1.3.6.1.4.1.9.9.68.1.2.2.1.2 - access port vlan map (cisco only) + # if cisco, vlan community string indexed or snmpv3 contest for: + # 1.3.6.1.2.1.17.4.3.1.2 - mactoindx (bridge - low-end switches and cisco) + # .1.3.6.1.2.1.17.1.4.1.2 - bridge index to if index map + # no vlan index or context for: + # .1.3.6.1.2.1.31.1.1.1.1 - ifName... but some switches don't do it + # .1.3.6.1.2.1.2.2.1.2 - ifDescr, usually useless, but a + # fallback if ifName is empty + # + haveqbridge = False + mactobridge = {} + conn = snmp.Session(switch, password, user) + for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'): + haveqbridge = True + oid, bridgeport = vb + oid = str(oid).rsplit('.', 6) # if 7, then oid[1] would be vlan id + macaddr = '{0:02x}:{1:02x}:{2:02x}:{3:02x}:{4:02x}:{5:02x}'.format( + *([int(x) for x in oid[-6:]]) + ) + mactobridge[macaddr] = int(bridgeport) + if not haveqbridge: + raise exc.NotImplementedException('TODO: Bridge-MIB without QBRIDGE') + bridgetoifmap = {} + for vb in conn.walk('1.3.6.1.2.1.17.1.4.1.2'): + bridgeport, ifidx = vb + bridgeport = int(str(bridgeport).rsplit('.', 1)[1]) + bridgetoifmap[bridgeport] = int(ifidx) + 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) + localmap = {} + for mac in mactobridge: + localmap[mac] = ifnamemap[bridgetoifmap[mactobridge[mac]]] + print(repr(localmap)) + + +if __name__ == '__main__': + # invoke as switch community + import sys + _map_switch(sys.argv[1], sys.argv[2]) diff --git a/confluent_server/confluent/snmputil.py b/confluent_server/confluent/snmputil.py index b747c0b7..9b5afd0d 100644 --- a/confluent_server/confluent/snmputil.py +++ b/confluent_server/confluent/snmputil.py @@ -37,53 +37,67 @@ def _get_transport(name): return snmp.UdpTransportTarget(res[0][4]) -def walk(server, oid, secret, username=None, context=None): - """Walk over children of a given OID +class Session(object): - This is roughly equivalent to snmpwalk. It will automatically try to be - an snmpbulkwalk if possible. If username is not given, it is assumed that - the secret is a community string, and v2c is used. If a username given, - it'll assume SHA auth and DES privacy with the secret being the same for - both. + def __init__(self, server, secret, username=None, context=None): + """Create a new session to interrogate a switch - :param server: The network name/address to target - :param oid: The SNMP object identifier - :param secret: The community string or password - :param username: The username for SNMPv3 - :param context: The SNMPv3 context or index for community string indexing - """ - # SNMP is a complicated mess of things. Will endeavor to shield caller - # from as much as possible, assuming reasonable defaults where possible. - # there may come a time where we add more parameters to override the - # automatic behavior (e.g. DES is weak, so it's a likely candidate to be - # overriden, but some devices only support DES) - tp = _get_transport(server) - ctx = snmp.ContextData(context) - if '::' in oid: - mib, field = oid.split('::') - obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field)) - else: - obj = snmp.ObjectType(snmp.ObjectIdentity(oid)) - eng = snmp.SnmpEngine() - if username is None: - # SNMP v2c - authdata = snmp.CommunityData(secret, mpModel=1) - else: - authdata = snmp.UsmUserData(username, authKey=secret, privKey=secret) - walking = snmp.bulkCmd(eng, authdata, tp, ctx, 0, 10, obj, - lexicographicMode=False) - for rsp in walking: - errstr, errnum, erridx, answers = rsp - if errstr: - raise exc.TargetEndpointUnreachable(str(errstr)) - elif errnum: - raise exc.ConfluentException(errnum.prettyPrint()) - for ans in answers: - yield ans + If username is not given, it is assumed that + the secret is community string, and v2c is used. If a username given, + it'll assume SHA auth and DES privacy with the secret being the same + for both. + + :param server: The network name/address to target + :param secret: The community string or password + :param username: The username for SNMPv3 + :param context: The SNMPv3 context or index for community indexing + """ + self.server = server + self.context = context + if username is None: + # SNMP v2c + self.authdata = snmp.CommunityData(secret, mpModel=1) + else: + self.authdata = snmp.UsmUserData(username, authKey=secret, + privKey=secret) + self.eng = snmp.SnmpEngine() + + def walk(self, oid): + """Walk over children of a given OID + + This is roughly equivalent to snmpwalk. It will automatically try to + be a snmpbulkwalk if possible. + + :param oid: The SNMP object identifier + """ + # SNMP is a complicated mess of things. Will endeavor to shield caller + # from as much as possible, assuming reasonable defaults when possible. + # there may come a time where we add more parameters to override the + # automatic behavior (e.g. DES is weak, so it's likely to be + # overriden, but some devices only support DES) + tp = _get_transport(self.server) + ctx = snmp.ContextData(self.context) + if '::' in oid: + mib, field = oid.split('::') + obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field)) + else: + obj = snmp.ObjectType(snmp.ObjectIdentity(oid)) + + walking = snmp.bulkCmd(self.eng, self.authdata, tp, ctx, 0, 10, obj, + lexicographicMode=False) + for rsp in walking: + errstr, errnum, erridx, answers = rsp + if errstr: + raise exc.TargetEndpointUnreachable(str(errstr)) + elif errnum: + raise exc.ConfluentException(errnum.prettyPrint()) + for ans in answers: + yield ans if __name__ == '__main__': import sys - for kp in walk(sys.argv[1], sys.argv[2], 'public'): + ts = Session(sys.argv[1], 'public') + for kp in ts.walk(sys.argv[2]): print(str(kp[0])) print(str(kp[1])) From 6b5f437a1c7f5e065d480e857c4d804b25ee44f0 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 29 Jun 2016 11:29:05 -0400 Subject: [PATCH 3/6] Provide a simple global 'log' function As we implement internal processes with automation, provide a hook for code to convey information about situations encountered during background activity. Ultimately, it is intended to hook event forwarders for things like syslog/email/etc --- confluent_server/confluent/config/attributes.py | 13 ++++++++----- confluent_server/confluent/log.py | 8 ++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 0303628b..9a12e218 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -207,16 +207,19 @@ node = { #IBM Flex)''', # 'appliesto': ['system'], # }, -# 'inventory.serialnumber': { +# 'id.serial': { # 'description': 'The manufacturer serial number of node', # }, -# 'inventory.uuid': { +# 'id.uuid': { # 'description': 'The UUID of the node as presented in DMI', # }, -# 'inventory.modelnumber': { +# 'id.modelnumber': { # 'description': 'The manufacturer dictated model number for the node', # }, -# 'inventory.snmpengineid': { +# 'id.modelname': { +# 'description': 'The manufacturer model label for the node', +# }, +# 'id.snmpengineid': { # 'description': 'The SNMP Engine id used by this node', # }, # 'secret.snmpuser': { @@ -235,7 +238,7 @@ node = { # 'description': ('SNMPv1 community string, it is highly recommended to' # 'step up to SNMPv3'), # }, -# 'secret.localadminpassphrase': { +# 'secret.adminpassword': { # 'description': ('The passphrase to apply to local root/administrator ' # 'account. ' # 'If the environment is 100% Linux, the value may be ' diff --git a/confluent_server/confluent/log.py b/confluent_server/confluent/log.py index 9269c359..2c3906be 100644 --- a/confluent_server/confluent/log.py +++ b/confluent_server/confluent/log.py @@ -722,3 +722,11 @@ class Logger(object): def closelog(self): self.handler.close() self.closer = None + +globaleventlog = None + + +def log(logdata=None, ltype=None, event=0, eventdata=None): + if globaleventlog is None: + globaleventlog = Logger('events') + globaleventlog.log(logdata, ltype, event, eventdata) \ No newline at end of file From f539a4e4b643c435a721552cd89896c143ca771b Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 29 Jun 2016 16:32:46 -0400 Subject: [PATCH 4/6] Provide config enabled switch mapping Wire up the singleton switch search function to a function that extracts list of switches and relevant auth data from the config engine. Add attributes to allow indication by hardware management port connection. The OS nics will be added later for in-band discovery, but that's of limited value until PXE support anyway. This time, the update function is a generator that yields as a sign to caller that the mac map has had at least a partial update to be considered. --- .../confluent/config/attributes.py | 60 +++++++++---------- .../confluent/config/configmanager.py | 6 +- .../confluent/networking/macmap.py | 50 ++++++++++++++-- 3 files changed, 78 insertions(+), 38 deletions(-) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 9a12e218..468358b1 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -188,12 +188,24 @@ node = { # 'appliesto': ['vm'], # }, 'hardwaremanagement.manager': { - 'description': 'The management address dedicated to this node', + 'description': 'The management address dedicated to this node. This ' + 'is the address of, for example, the Lenovo IMM.', }, 'hardwaremanagement.method': { 'description': 'The method used to perform operations such as power ' 'control, get sensor data, get inventory, and so on. ' }, + 'hardwaremanagement.switch': { + 'description': 'The switch to which the hardware manager is connected.' + ' Only relevant if using switch based discovery via the' + ' hardware manager (Lenovo IMMs and CMMs). Not ' + 'applicable to Lenovo Flex nodes.' + }, + 'hardwaremanagement.switchport': { + 'description': 'The port of the switch that the hardware manager is ' + 'connected. See documentation of ' + 'hardwaremanagement.switch for more detail.' + }, 'enclosure.manager': { 'description': "The management device for this node's chassis", # 'appliesto': ['system'], @@ -202,6 +214,7 @@ node = { 'description': 'The bay in the enclosure, if any', # 'appliesto': ['system'], }, + # 'enclosure.type': { # 'description': '''The type of enclosure in use (e.g. IBM BladeCenter, #IBM Flex)''', @@ -225,19 +238,19 @@ node = { # 'secret.snmpuser': { # 'description': 'The user to use for SNMPv3 access to this node', # }, -# 'secret.snmppassphrase': { -# 'description': 'The passphrase to use for SNMPv3 access to this node', +# 'secret.snmppassword': { +# 'description': 'The password to use for SNMPv3 access to this node', # }, + 'secret.snmpcommunity': { + 'description': ('SNMPv1 community string, it is highly recommended to' + 'step up to SNMPv3'), + }, # 'secret.snmplocalizedkey': { # 'description': ("SNMPv3 key localized to this node's SNMP Engine id" # 'This can be used in lieu of snmppassphrase to avoid' # 'retaining the passphrase TODO: document procedure' # 'to commit passphrase to localized key'), # }, -# 'secret.snmpcommunity': { -# 'description': ('SNMPv1 community string, it is highly recommended to' -# 'step up to SNMPv3'), -# }, # 'secret.adminpassword': { # 'description': ('The passphrase to apply to local root/administrator ' # 'account. ' @@ -249,35 +262,18 @@ node = { # 'AD') # }, 'secret.ipmikg': { - 'description': 'Optional Integrity key for IPMI communication' + 'description': 'Optional Integrity key for IPMI communication. This ' + 'should generally be ignored, as mutual authentication ' + 'is normally done with the password alone (which is a ' + 'shared secret in IPMI)' }, -# 'secret.ipmiuser': { -# 'description': ('The username to use to log into IPMI device related ' -# 'to the node. For setting username, default ' -# 'behavior is to randomize username, for using ' -# 'username if not set, USERID is assumed'), -# }, -# 'secret.ipmipassphrase': { -# 'description': ('The key to use to authenticate to IPMI device ' -# 'related to the node. For setting passphrase, ' -# 'default behavior is to randomize passphrase and ' -# 'store it here. If going to connect over the ' -# 'network and value is not set, PASSW0RD is attempted') -# }, 'secret.hardwaremanagementuser': { - 'description': ('Username to be set and used by protocols like SSH ' - 'and HTTP where client provides passphrase over the ' - 'network. Given the distinct security models betwen ' - 'this class of protocols and SNMP and IPMI, snmp and ' - 'ipmi utilize dedicated values.'), + 'description': ('The username to use when connecting to the hardware ' + 'manager'), }, 'secret.hardwaremanagementpassword': { - 'description': ('Passphrase to be set and used by protocols like SSH ' - 'and HTTP, where client sends passphrase over the ' - 'network. Given distinct security models between ' - 'this class of protocols, SNMP, and IPMI, SNMP and ' - 'IPMI are given their own settings with distinct ' - 'behaviors'), + 'description': ('Password to use when connecting to the hardware ' + 'manager'), }, 'pubkeys.addpolicy': { 'description': ('Policy to use when encountering unknown public ' diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 6986043b..c5043ab9 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -761,7 +761,9 @@ class ConfigManager(object): decrypt=self.decrypt) return nodeobj - def get_node_attributes(self, nodelist, attributes=()): + def get_node_attributes(self, nodelist, attributes=(), decrypt=None): + if decrypt is None: + decrypt = self.decrypt retdict = {} relattribs = attributes if isinstance(nodelist, str) or isinstance(nodelist, unicode): @@ -783,7 +785,7 @@ class ConfigManager(object): # skipped. The decryption, however, we want to do only on # demand nodeobj[attribute] = _decode_attribute(attribute, cfgnodeobj, - decrypt=self.decrypt) + decrypt=decrypt) retdict[node] = nodeobj return retdict diff --git a/confluent_server/confluent/networking/macmap.py b/confluent_server/confluent/networking/macmap.py index fd601f7b..172d6d6c 100644 --- a/confluent_server/confluent/networking/macmap.py +++ b/confluent_server/confluent/networking/macmap.py @@ -33,11 +33,14 @@ import confluent.exceptions as exc import confluent.snmputil as snmp +import confluent.util as util +import eventlet +from eventlet.greenpool import GreenPool _macmap = {} -def _map_switch(switch, password, user=None): +def _map_switch(args): """Manipulate portions of mac address map relevant to a given switch """ @@ -53,6 +56,8 @@ def _map_switch(switch, password, user=None): # .1.3.6.1.2.1.2.2.1.2 - ifDescr, usually useless, but a # fallback if ifName is empty # + global _macmap + switch, password, user = args haveqbridge = False mactobridge = {} conn = snmp.Session(switch, password, user) @@ -85,10 +90,47 @@ def _map_switch(switch, password, user=None): ifidx, ifname = vb ifidx = int(str(ifidx).rsplit('.', 1)[1]) ifnamemap[ifidx] = str(ifname) - localmap = {} for mac in mactobridge: - localmap[mac] = ifnamemap[bridgetoifmap[mactobridge[mac]]] - print(repr(localmap)) + _macmap[mac] = (switch, ifnamemap[bridgetoifmap[mactobridge[mac]]], + util.monotonic_time()) + + +def update_macmap(configmanager): + """Interrogate switches to build/update mac table + + Begin a rebuild process. This process is a generator that will yield + as each switch interrogation completes, allowing a caller to + recheck the cache as results become possible, rather + than having to wait for the process to complete to interrogate. + """ + if configmanager.tenant is not None: + raise exc.ForbiddenRequest('Network topology not available to tenants') + nodelocations = configmanager.get_node_attributes( + configmanager.list_nodes(), ('hardwaremanagement.switch',)) + switches = set([]) + for node in nodelocations: + cfg = nodelocations[node] + if 'hardwaremanagement.switch' in cfg: + switches.add(cfg['hardwaremanagement.switch']['value']) + switchcfg = configmanager.get_node_attributes( + switches, ('secret.hardwaremanagementuser', + 'secret.hardwaremanagementpassword'), decrypt=True) + switchauth = [] + for switch in switches: + password = 'public' + user = None + if (switch in switchcfg and + 'secret.hardwaremanagementpassword' in switchcfg[switch]): + password = switchcfg[switch]['secret.hardwaremanagementpassword'][ + 'value'] + if 'secret.hardwaremanagementuser' in switchcfg[switch]: + user = switchcfg[switch]['secret.hardwaremanagementuser'][ + 'value'] + switchauth.append((switch, password, user)) + pool = GreenPool() + for res in pool.imap(_map_switch, switchauth): + yield res + print(repr(_macmap)) if __name__ == '__main__': From 9764a0241994c156ec9c10851f33f8d606a4ff89 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 30 Jun 2016 15:54:18 -0400 Subject: [PATCH 5/6] Improve behavior of mac map One, include a number of 'fellow' mac addresses on the same port. Another, allow a mac to appear on multiple ports and have that reflected in the data structure. Also capture errors to trace log rather than hanging up on unexpected cases. --- confluent_server/confluent/log.py | 11 +++++-- .../confluent/networking/macmap.py | 29 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/confluent_server/confluent/log.py b/confluent_server/confluent/log.py index 2c3906be..87b71e3a 100644 --- a/confluent_server/confluent/log.py +++ b/confluent_server/confluent/log.py @@ -724,9 +724,16 @@ class Logger(object): self.closer = None globaleventlog = None - +tracelog = None def log(logdata=None, ltype=None, event=0, eventdata=None): if globaleventlog is None: globaleventlog = Logger('events') - globaleventlog.log(logdata, ltype, event, eventdata) \ No newline at end of file + globaleventlog.log(logdata, ltype, event, eventdata) + +def logtrace(): + global tracelog + if tracelog is None: + tracelog = Logger('trace') + tracelog.log(traceback.format_exc(), ltype=DataTypes.event, + event=Events.stacktrace) \ No newline at end of file diff --git a/confluent_server/confluent/networking/macmap.py b/confluent_server/confluent/networking/macmap.py index 172d6d6c..0f60abf4 100644 --- a/confluent_server/confluent/networking/macmap.py +++ b/confluent_server/confluent/networking/macmap.py @@ -32,6 +32,7 @@ # This functionality is restricted to the null tenant import confluent.exceptions as exc +import confluent.log as log import confluent.snmputil as snmp import confluent.util as util import eventlet @@ -39,8 +40,13 @@ from eventlet.greenpool import GreenPool _macmap = {} - def _map_switch(args): + try: + return _map_switch_backend(args) + except Exception as e: + log.logtrace() + +def _map_switch_backend(args): """Manipulate portions of mac address map relevant to a given switch """ @@ -64,6 +70,8 @@ def _map_switch(args): for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'): haveqbridge = True oid, bridgeport = vb + if not bridgeport: + continue oid = str(oid).rsplit('.', 6) # if 7, then oid[1] would be vlan id macaddr = '{0:02x}:{1:02x}:{2:02x}:{3:02x}:{4:02x}:{5:02x}'.format( *([int(x) for x in oid[-6:]]) @@ -90,9 +98,21 @@ def _map_switch(args): ifidx, ifname = vb ifidx = int(str(ifidx).rsplit('.', 1)[1]) ifnamemap[ifidx] = str(ifname) + maccounts = {} for mac in mactobridge: - _macmap[mac] = (switch, ifnamemap[bridgetoifmap[mactobridge[mac]]], - util.monotonic_time()) + ifname = ifnamemap[bridgetoifmap[mactobridge[mac]]] + if ifname not in maccounts: + maccounts[ifname] = 1 + else: + maccounts[ifname] += 1 + for mac in mactobridge: + # We want to merge it so that when a mac appears in multiple + # places, it is captured. + ifname = ifnamemap[bridgetoifmap[mactobridge[mac]]] + if mac in _macmap: + _macmap[mac].append((switch, ifname, maccounts[ifname])) + else: + _macmap[mac] = [(switch, ifname, maccounts[ifname])] def update_macmap(configmanager): @@ -103,6 +123,9 @@ def update_macmap(configmanager): recheck the cache as results become possible, rather than having to wait for the process to complete to interrogate. """ + global _macmap + # Clear all existing entries + _macmap = {} if configmanager.tenant is not None: raise exc.ForbiddenRequest('Network topology not available to tenants') nodelocations = configmanager.get_node_attributes( From 7a4c9a1fc0bd9e08bc84bffd91c756f45c3f5ce9 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 14 Jul 2016 08:55:50 -0400 Subject: [PATCH 6/6] Add mac map lookup against config to get node This brings things right to the level of xCAT in terms of underlying capability. mac addresses have both an all inclusive list of ports it is found on, and any nodes that it matches. It goes another step further by logging errors when ambiguity is detected (either verbatim config conflict or ambiguous result based on 'namesmatch' and the switch config). --- confluent_server/confluent/log.py | 1 + .../confluent/networking/macmap.py | 95 ++++++++++++++++++- 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/confluent_server/confluent/log.py b/confluent_server/confluent/log.py index 87b71e3a..662294a2 100644 --- a/confluent_server/confluent/log.py +++ b/confluent_server/confluent/log.py @@ -726,6 +726,7 @@ class Logger(object): globaleventlog = None tracelog = None + def log(logdata=None, ltype=None, event=0, eventdata=None): if globaleventlog is None: globaleventlog = Logger('events') diff --git a/confluent_server/confluent/networking/macmap.py b/confluent_server/confluent/networking/macmap.py index 0f60abf4..29344b09 100644 --- a/confluent_server/confluent/networking/macmap.py +++ b/confluent_server/confluent/networking/macmap.py @@ -34,11 +34,58 @@ import confluent.exceptions as exc import confluent.log as log import confluent.snmputil as snmp -import confluent.util as util -import eventlet from eventlet.greenpool import GreenPool +import re _macmap = {} +_macsbyswitch = {} +_nodesbymac = {} +_switchportmap = {} + + +_whitelistnames = ( + # 3com + re.compile(r'^RMON Port (\d+) on unit \d+'), + # Dell + re.compile(r'^Unit \d+ Port (\d+)\Z'), +) + +_blacklistnames = ( + re.compile(r'vl'), + re.compile(r'Nu'), + re.compile(r'RMON'), + re.compile(r'onsole'), + re.compile(r'Stack'), + re.compile(r'Trunk'), + re.compile(r'po\d'), + re.compile(r'XGE'), + re.compile(r'LAG'), + re.compile(r'CPU'), + re.compile(r'Management'), +) + + +def _namesmatch(switchdesc, userdesc): + if switchdesc == userdesc: + return True + try: + portnum = int(userdesc) + except ValueError: + portnum = None + if portnum is not None: + for exp in _whitelistnames: + match = exp.match(switchdesc) + if match: + snum = int(match.groups()[0]) + if snum == portnum: + return True + anymatch = re.search(r'[^0123456789]' + userdesc + r'(\.0)?\Z', switchdesc) + if not anymatch: + return False + for blexp in _blacklistnames: + if blexp.match(switchdesc): + return False + def _map_switch(args): try: @@ -46,6 +93,16 @@ def _map_switch(args): except Exception as e: log.logtrace() + +def _nodelookup(switch, ifname): + """Get a nodename for a given switch and interface name + """ + for portdesc in _switchportmap.get(switch, {}): + if _namesmatch(ifname, portdesc): + return _switchportmap[switch][portdesc] + return None + + def _map_switch_backend(args): """Manipulate portions of mac address map relevant to a given switch """ @@ -105,6 +162,7 @@ def _map_switch_backend(args): maccounts[ifname] = 1 else: maccounts[ifname] += 1 + _macsbyswitch[switch] = {} for mac in mactobridge: # We want to merge it so that when a mac appears in multiple # places, it is captured. @@ -113,6 +171,18 @@ def _map_switch_backend(args): _macmap[mac].append((switch, ifname, maccounts[ifname])) else: _macmap[mac] = [(switch, ifname, maccounts[ifname])] + if ifname in _macsbyswitch[switch]: + _macsbyswitch[switch][ifname].append(mac) + else: + _macsbyswitch[switch][ifname] = [mac] + nodename = _nodelookup(switch, ifname) + if nodename is not None: + if mac in _nodesbymac and _nodesbymac[mac] != nodename: + log.log({'warning': '{0} and {1} described by ambiguous' + ' switch topology values'.format(nodename, + _nodesbymac[mac] + )}) + _nodesbymac[mac] = nodename def update_macmap(configmanager): @@ -124,17 +194,34 @@ def update_macmap(configmanager): than having to wait for the process to complete to interrogate. """ global _macmap + global _nodesbymac + global _switchportmap # Clear all existing entries _macmap = {} + _nodesbymac = {} + _switchportmap = {} if configmanager.tenant is not None: raise exc.ForbiddenRequest('Network topology not available to tenants') nodelocations = configmanager.get_node_attributes( - configmanager.list_nodes(), ('hardwaremanagement.switch',)) + configmanager.list_nodes(), ('hardwaremanagement.switch', + 'hardwaremanagement.switchport')) switches = set([]) for node in nodelocations: cfg = nodelocations[node] if 'hardwaremanagement.switch' in cfg: - switches.add(cfg['hardwaremanagement.switch']['value']) + curswitch = cfg['hardwaremanagement.switch']['value'] + switches.add(curswitch) + if 'hardwaremanagement.switchport' in cfg: + portname = cfg['hardwaremanagement.switchport']['value'] + if curswitch not in _switchportmap: + _switchportmap[curswitch] = {} + if portname in _switchportmap[curswitch]: + log.log({'warning': 'Duplicate switch topology config for ' + '{0} and {1}'.format(node, + _switchportmap[ + curswitch][ + portname])}) + _switchportmap[curswitch][portname] = node switchcfg = configmanager.get_node_attributes( switches, ('secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'), decrypt=True)