From 8387f0e13e3063e460b19a33e434f1d0c6e30075 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 29 Jun 2016 11:26:46 -0400 Subject: [PATCH] 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]))