From 88b9c53e59430d851cb23b7bfb092913c90ffb58 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Sat, 18 Jul 2020 21:27:51 -0700 Subject: [PATCH] add support for setting arbitrary MIB values... This can be used to set the STP priority for the root switch.. --- README.md | 73 ++++++++++++++++++++----- test_data.py | 7 ++- vlanmang/__init__.py | 126 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 180 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index c7bf91a..c0a55e6 100644 --- a/README.md +++ b/README.md @@ -14,26 +14,69 @@ a list of changes that need to be made to the switches to make them match what is configured. Then the second part, which is implemented as part of the main function, is to apply those changes. + Usage ----- -The vlanmang command will import the Python module named data, aka -`data.py`. The easiest way is if there is a file named data.py in the -current directory, if there is, it will use that. Note that this file is -run as Python code, so it can write files, read files, or any thing else -that a Python program can do. This means that putting untrusted data -from users should never be done unless properly escaped, or handled -appropriately. +The vlanmang command will import the Python module named data, for +example `data.py`. The easiest way is if there is a file named data.py +in the current directory, if there is, it will use that. Note that this +file is run as Python code, so it can write files, read files, or any +thing else that a Python program can do. This means that putting +untrusted data from users should never be done unless properly escaped, +or handled appropriately. The file consists of declarations of how the switches should be configured, and the credentials necessary to verify configuration and make the necessary changes. One slightly unusual part of the tool is that you have to declare ports that you do not care about. This is to help ensure that you have a configuration specified for all the ports you -care about, not just some of them. The common ports you will ignore are -cpu interfaces and extra lag interfaces. You can specify the ports by -the names the switch knows them by (the ifName column in SNMP) for -convience, or they can be specified by their index in ifTable. +care about, not just some of them. Common ports that should be ignored +are the cpu interfaces and any extra lag interfaces. You can specify +the ports by the names the switch knows them by (the ifName column in +SNMP) for convience, or they can be specified by their index in ifTable. + + +MIBs +---- + +I'm sorry that vlanmang has to subject you to PySNMP. It is a terrible +library that has been a complete miseriable experience to work with. If +someone suggests a better library, I will be more than glad to switch to +it, but it HAS to be better, just not a different pile of crap like +PySNMP is. + +The issue with MIBs is that PySNMP does not parse MIB files to figure out +what files have what definitions in them. It REQUIRES that the files +have a specific name. NetSNMP does not have this requirement, and as +such, vendors do not follow PySNMP's specific naming. + +For example, the NetGear MIBs files have SNMPv2-MIB definitions in a file +named v2-mib.my, but PySNMP will ONLY find them if the file is named +SNMPv2-MIB.mib (or some other extension). + +In order to make the `mibdump.py` utility be able to convert these files, +you first need to run this command over your MIB files: +``` +grep DEFINITIONS *.my | awk '{ gsub(":", " "); system( "ln -s " $1 " " $2 ".mib") }' +``` + +This command will find all the definitions in *.my files, and create +a symlink to the file. This then allows you to run the command: +``` +mibdump.py --mib-source=mibdir +``` + +You can specify the `--mib-source` multiple times, e.g. to include the +NetSNMP definitions that are often located in /usr/share/snmp/mibs. + +Note: There are may be errors in the MIB file, like NetGear's +fastpathswitching.my file has a definition for +agentKeepalivePortLastLoopDetectedTime that has a default value that is +too short. It's a 4 byte octet string instead of an 8 byte octet string. +If you modify the MIB files, you will need to rerun the `mibdump.py` +command. + Example ------- @@ -68,6 +111,11 @@ switchvlans = { 't': lag1, }, +mibsettings = [ + (('BRIDGE-MIB', 'dot1dStp', 1, 0), 3), + # Bump this switch's STP priority + (('BRIDGE-MIB', 'dot1dStp', 2, 0), 16384) +] # You can put your passwords in another file for security from passwords import switchvlankey @@ -78,7 +126,8 @@ authdata = dict(username='admin', authKey=key, privKey=key, switch = vlanmang.SwitchConfig('203.0.113.10', authdata, switchvlangs, rng(25,26) + # part of lag1 - [ 'ch%d' % x for x in rng(2,8) ] # ignore the extra lag interfaces + [ 'ch%d' % x for x in rng(2,8) ], # ignore the extra lag interfaces + mibsettings ) ``` diff --git a/test_data.py b/test_data.py index 98e97b2..2f8c725 100644 --- a/test_data.py +++ b/test_data.py @@ -26,4 +26,9 @@ distributionswitch = { }, } -distswitch = vlanmang.SwitchConfig('192.168.0.58', { 'community': 'private' }, distributionswitch, [ 'lag2' ]) +settings = [ + ('somesettingtrue', True), + ('annumberset', 42), + ('nochange', 100), +] +distswitch = vlanmang.SwitchConfig('192.168.0.58', { 'community': 'private' }, distributionswitch, [ 'lag2' ], settings) diff --git a/vlanmang/__init__.py b/vlanmang/__init__.py index 8a1f993..bc9f1f0 100644 --- a/vlanmang/__init__.py +++ b/vlanmang/__init__.py @@ -27,10 +27,14 @@ # from pysnmp.hlapi import * -from pysnmp.proto.rfc1905 import NoSuchInstance +from pysnmp.proto.rfc1905 import NoSuchInstance, NoSuchObject from pysnmp.smi.builder import MibBuilder from pysnmp.smi.view import MibViewController +if False: + from pysnmp import debug + debug.setLogger(debug.Debug('mibbuild')) + import importlib import itertools import mock @@ -50,6 +54,11 @@ __all__ = [ _mbuilder = MibBuilder() _mvc = MibViewController(_mbuilder) +# Doesn't work because internally uses a different MibBuilder(), and +# therefore the class object is different. +#for m, n in {('SNMPv2-TC', 'DisplayString')}: +# locals()[n] = _mbuilder.importSymbols(m, n)[0] + # received packages # pvid: dot1qPvid # @@ -109,11 +118,12 @@ class SwitchConfig(object): any unused lag ports. ''' - def __init__(self, host, authargs, vlanconf, ignports): + def __init__(self, host, authargs, vlanconf, ignports, settings=[]): self._host = host self._authargs = authargs self._vlanconf = vlanconf self._ignports = ignports + self._settings = settings @property def host(self): @@ -131,6 +141,10 @@ class SwitchConfig(object): def ignports(self): return self._ignports + @property + def settings(self): + return self._settings + def getportlist(self, lookupfun): '''Return a set of all the ports indexes in data. This includes, both vlanconf and ignports. Any ports using names @@ -239,6 +253,13 @@ def checkchanges(module): raise ValueError('missing or extra ports found: %s' % repr(ports.symmetric_difference(portlist))) + # compare settings + settings = list(i.settings) + switchsettings = list(switch.getsettings(*(x[0] for x in settings))) + + res.extend((switch, name, 'setsetting', s, tobeval, curval) for (s, tobeval), + curval in zip(settings, switchsettings) if tobeval != curval) + # compare pvid pvidmap = getpvidmapping(i.vlanconf, lufun) switchpvid = switch.getpvid() @@ -389,6 +410,13 @@ class SNMPSwitch(object): if len(varBinds) != len(oids): # pragma: no cover raise ValueError('too many return values') + nsi = [ x for x in varBinds if isinstance(x[1], + (NoSuchInstance, NoSuchObject)) ] + if nsi: + raise ValueError( + 'No such instance/object: %s' % + repr(nsi[0][0].getMibSymbol())) + return varBinds def _get(self, oid): @@ -396,16 +424,19 @@ class SNMPSwitch(object): var = varBinds[0][1] - if isinstance(var, NoSuchInstance): - raise ValueError(repr(var)) - return var def _set(self, oid, value): oid = ObjectIdentity(*oid) oid.resolveWithMib(_mvc) - if isinstance(value, int): + #print(repr(tuple(oid))) + #if tuple(oid) == (1, 3, 6, 1, 4, 1, 4526, 11, 1, 2, 15, 10, 1, 2, 0): + if True: + #print('yes') + #import pdb; pdb.set_trace() + value = oid.getMibNode().getSyntax().clone(value) + elif isinstance(value, int): value = Integer(value) elif isinstance(value, bytes): value = OctetString(value) @@ -448,6 +479,20 @@ class SNMPSwitch(object): for varBind in varBinds: yield varBind + _convfun = dict(DisplayString=str, Integer32=int, Integer=int, Gauge32=int) + + def getsettings(self, *oids): + '''Return the values for the passed in oids. It is best + to pass in as many oids as possible, as a bulk walk get + will be used for effeciency.''' + + #a = list(self._getmany(*oids)) + #print(repr(a)) + return (self._convfun[y.__class__.__name__](y) for x, y in self._getmany(*oids)) + + def setsetting(self, oid, value): + return self._set(oid, value) + def getportmapping(self): '''Return a port name mapping. Keys are the port index and the value is the name from the IF-MIB::ifName entry.''' @@ -778,12 +823,13 @@ class _TestMisc(unittest.TestCase): privProtocol=usmDESPrivProtocol) #@unittest.skip('foo') + @mock.patch('vlanmang.SNMPSwitch.getsettings') @mock.patch('vlanmang.SNMPSwitch.getuntagged') @mock.patch('vlanmang.SNMPSwitch.getegress') @mock.patch('vlanmang.SNMPSwitch.getpvid') @mock.patch('vlanmang.SNMPSwitch.getportmapping') @mock.patch('importlib.import_module') - def test_checkchanges(self, imprt, portmapping, gpvid, gegress, guntagged): + def test_checkchanges(self, imprt, portmapping, gpvid, gegress, guntagged, gsettings): # that import returns the test data imprt.side_effect = itertools.repeat(self._test_data) @@ -825,8 +871,21 @@ class _TestMisc(unittest.TestCase): 283: '00000000111111111110011', } ] + # that the switch's settings provided + settings = [ + ('somesettingtrue', False), + ('annumberset', 6), + ('nochange', 100), + ] + + gsettings.side_effect = itertools.repeat(tuple(x[1] for x in + settings)) + res = checkchanges('data') + # That gsettings was called + gsettings.assert_called_with(*(x[0] for x in settings)) + # Make sure that the first one are all instances of SNMPSwitch # XXX make sure args for them are correct. self.assertTrue(all(isinstance(x[0], SNMPSwitch) for x in res)) @@ -835,15 +894,17 @@ class _TestMisc(unittest.TestCase): self.assertTrue(all(x[1] == 'distswitch' for x in res)) res = [ x[2:] for x in res ] - validres = [ ('setpvid', x, 5, 283) for x in range(1, 9) ] + \ + validres = [ ('setsetting', 'somesettingtrue', True, False), + ('setsetting', 'annumberset', 42, 6) ] + \ + [ ('setpvid', x, 5, 283) for x in range(1, 9) ] + \ [ ('setpvid', 20, 1, 283), - ('setpvid', 21, 1, 283), - ('setpvid', 30, 1, 5), - ('setegress', 1, '0' * 19 + '11' + '0' * 8 + '1', + ('setpvid', 21, 1, 283), + ('setpvid', 30, 1, 5), + ('setegress', 1, '0' * 19 + '11' + '0' * 8 + '1', '1' * 10), - ('setuntagged', 1, '0' * 19 + '11' + '0' * 8 + '1', + ('setuntagged', 1, '0' * 19 + '11' + '0' * 8 + '1', '1' * 10), - ('setegress', 5, '1' * 8 + '0' * 11 + '11' + '0' * 8 + + ('setegress', 5, '1' * 8 + '0' * 11 + '11' + '0' * 8 + '1', '1' * 10), ] @@ -867,6 +928,32 @@ class _TestSNMPSwitch(unittest.TestCase): # and call _getmany w/ the correct arg gm.assert_called_with(arg) + @mock.patch('pysnmp.hlapi.ContextData') + @mock.patch('vlanmang.getCmd') + def test_getmany_nosuchinstance(self, gc, cd): + # that a switch + switch = SNMPSwitch(None, community=None) + + lookup = { x: chr(x) for x in range(1, 10) } + + # when getCmd returns NoSuchInstance + gc.side_effect = [iter([[None, None, None, [ None, NoSuchInstance() ]]])] + + self.assertRaises(ValueError, switch.getsettings, ('IF-MIB', 'ifName')) + + @mock.patch('pysnmp.hlapi.ContextData') + @mock.patch('vlanmang.getCmd') + def test_getmany_nosuchobject(self, gc, cd): + # that a switch + switch = SNMPSwitch(None, community=None) + + lookup = { x: chr(x) for x in range(1, 10) } + + # when getCmd returns NoSuchInstance + gc.side_effect = [iter([[None, None, None, [ None, NoSuchObject() ]]])] + + self.assertRaises(ValueError, switch.getsettings, ('IF-MIB', 'ifName')) + @mock.patch('pysnmp.hlapi.ContextData') @mock.patch('vlanmang.getCmd') def test_getmany(self, gc, cd): @@ -928,6 +1015,19 @@ class _TestSwitch(unittest.TestCase): self.assertEqual(switch.findport('g1'), 1) self.assertEqual(switch.findport('l1'), 14) + def test_settings(self): + switch = self.switch + + vals = [ + ('SNMPv2-MIB', 'sysDescr', 0), + ('SNMPv2-MIB', 'sysServices', 0), + ] + + a, b = switch.getsettings(*vals) + #raise Exception('%s %s' % (repr(a), repr(b))) + self.assertEqual(a, 'GS108Tv2') + self.assertEqual(b, 2) + def test_portnames(self): switch = self.switch