split upt pvl.hosts-lldp into pvl.snmp.snmp and pvl.snmp.lldp, match remote hosts, better formatting
authorTero Marttila <terom@paivola.fi>
Mon, 17 Mar 2014 17:31:45 +0200
changeset 384 caa3dbbdbe83
parent 383 87b49aa52b3d
child 385 01d31fda75ab
split upt pvl.hosts-lldp into pvl.snmp.snmp and pvl.snmp.lldp, match remote hosts, better formatting
bin/pvl.hosts-lldp
pvl/snmp/snmp.py
--- a/bin/pvl.hosts-lldp	Mon Mar 17 17:19:30 2014 +0200
+++ b/bin/pvl.hosts-lldp	Mon Mar 17 17:31:45 2014 +0200
@@ -16,238 +16,10 @@
 import pvl.args
 import pvl.hosts
 from pvl.invoke import merge
-
-import logging; log = logging.getLogger('pvl.hosts-snmp')
-import optparse
-
-from pysnmp.entity.rfc3413.oneliner import cmdgen as pysnmp
-import collections
-
-class SNMPError (Exception) :
-    pass
-
-class SNMPEngineError (SNMPError) :
-    """
-        Internal SNMP Engine error (?)
-    """
-
-class SNMPAgent (object) :
-    """
-        GET SNMP shit from a remote host.
-    """
-
-    SNMP_PORT = 161
-    snmp_cmdgen = pysnmp.CommandGenerator()
-
-    @classmethod
-    def apply (cls, options, host, community=None) :
-        port = cls.SNMP_PORT
-
-        if community is None :
-            community = options.snmp_community
-
-
-        if '@' in host :
-            community, host = host.split('@', 1)
-        
-        if ':' in host :
-            host, port = host.rsplit(':', 1)
-
-        return cls(
-                pysnmp.CommunityData(community),
-                pysnmp.UdpTransportTarget((host, port))
-        )
-
-    def __init__ (self, security, transport) :
-        self.security = security
-        self.transport = transport
-
-    def get (self, *request) :
-        """
-            request = (
-                pysnmp.MibVariable('IF-MIB', 'ifInOctets', 1),
-            )
-        """
-
-        opts = dict(
-                lookupNames     = True,
-                lookupValues    = True,
-        )
-
-        try :
-            error, error_status, error_index, response = self.snmp_cmdgen.getCmd(self.security, self.transport,  *request, **opts)
-        except pysnmp.error.PySnmpError as ex :
-            raise SNMPEngineError(ex)
-
-        if error :
-            raise SNMPEngineError(error)
-        
-        if error_status :
-            raise SNMPError(errorStatus.prettyPrint())
-        
-        return response
-        #for name, value in response :
-        #    yield name.getMibSymbol(), value.prettyPrint()
-
-    def walk (self, *request) :
-        """
-            request = (
-                    pysnmp.MibVariable('IF-MIB', 'ifInOctets'),
-            )
-        """
-
-        opts = dict(
-                lookupNames     = True,
-                lookupValues    = True,
-        )
-
-        try :
-            error, error_status, error_index, responses = self.snmp_cmdgen.nextCmd(self.security, self.transport, *request, **opts)
-        except pysnmp.error.PySnmpError as ex :
-            raise SNMPEngineError(ex)
-
-        if error :
-            raise SNMPEngineError(error)
-        
-        if error_status :
-            raise SNMPError(errorStatus.prettyPrint())
-        
-        return responses
-        #for response in responses:
-        #    for name, value in response :
-        #        yield name.getMibSymbol(), value.prettyPrint()
-
-    def table (self, *columns) :
-        """
-            Given [oid] returns { idx: { oid: value } }
-        """
-
-        data = collections.defaultdict(dict)
-
-        for row in self.walk(*columns) :
-            log.debug("%s", row)
-
-            for column, (field, value) in zip(columns, row) :
-                mib, sym, idx = field.getMibSymbol()
+from pvl.snmp import snmp, lldp
 
-                log.debug("%s::%s[%s]: %s: %s", mib, sym, ', '.join(str(x) for x in idx), column, value)
-                
-                data[idx][column] = value
-            
-        return data.items()
-
-from memoized_property import memoized_property
-
-def macaddr (value) :
-    """
-        Excepts a MAC address from an SNMP OctetString.
-    """
-
-    return ':'.join('{octet:02x}'.format(octet=c) for c in value.asNumbers())
-
-class LLDPAgent (SNMPAgent) :
-    """
-        Query LLDP info from a remote agent.
-    """
-
-
-    @classmethod
-    def _chassis_id (cls, chassis_id, subtype) :
-        log.debug("%s: %r", subtype, chassis_id)
-        
-        # XXX: reference names from LLDP-MIB.py
-        if subtype == 4:
-            return macaddr(chassis_id)
-        else :
-            return chassis_id
- 
-    @classmethod
-    def _port_id (cls, port_id, subtype) :
-        log.debug("%s: %r", subtype, port_id)
-        
-        # XXX: reference names from LLDP-MIB.py
-        if subtype == 5: # interfaceName -> IF-MIB::ifName ?
-            return str(port_id)
-        elif subtype == 3 : # macAddress
-            return macaddr(port_id)
-        elif subtype == 7 : # local
-            return str(port_id) # XXX: integer?
-        else :
-            log.warn("unknown subtype=%d: %r", subtype, port_id)
-
-            return port_id
-       
-    LLDP_LOC_CHASSIS_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpLocChassisId')
-    LLDP_LOC_CHASSIS_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpLocChassisIdSubtype')
-    LLDP_LOC_SYS_NAME = pysnmp.MibVariable('LLDP-MIB', 'lldpLocSysName')
-
-    @memoized_property
-    def local (self) :
-        """
-            Describe the local system.
-        """
-
-        for idx, data in self.table(
-                self.LLDP_LOC_CHASSIS_ID,
-                self.LLDP_LOC_CHASSIS_ID_SUBTYPE,
-                self.LLDP_LOC_SYS_NAME,
-        ) :
-            return {
-                    'chassis':  self._chassis_id(data[self.LLDP_LOC_CHASSIS_ID], data[self.LLDP_LOC_CHASSIS_ID_SUBTYPE]),
-                    'sys_name': str(data[self.LLDP_LOC_SYS_NAME]),
-            }
-
-    LLDP_LOC_PORT_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortId')
-    LLDP_LOC_PORT_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortIdSubtype')
-
-    @memoized_property
-    def ports (self) :
-        """
-            Describe the local ports.
-        """
-
-        ports = { }
-
-        for idx, data in self.table(
-                self.LLDP_LOC_PORT_ID,
-                self.LLDP_LOC_PORT_ID_SUBTYPE,
-        ) :
-            port, = idx
-            
-            ports[int(port)] = {
-                    'port': self._port_id(data[self.LLDP_LOC_PORT_ID], data[self.LLDP_LOC_PORT_ID_SUBTYPE]),
-            }
-
-        return ports
-
-    def port (self, port) :
-        return self.ports[int(port)]
-
-    LLDP_REM_CHASSIS_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpRemChassisId')
-    LLDP_REM_CHASSIS_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpRemChassisIdSubtype')
-    LLDP_REM_SYS_NAME = pysnmp.MibVariable('LLDP-MIB', 'lldpRemSysName')
-    LLDP_REM_PORT_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpRemPortId')
-    LLDP_REM_PORT_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpRemPortIdSubtype')
-
-    def remotes (self) :
-        """
-            Describe remote systems, indexed by local port.
-        """
-
-        for idx, data in self.table(
-                self.LLDP_REM_CHASSIS_ID,
-                self.LLDP_REM_CHASSIS_ID_SUBTYPE,
-                self.LLDP_REM_SYS_NAME,
-                self.LLDP_REM_PORT_ID,
-                self.LLDP_REM_PORT_ID_SUBTYPE,
-        ) :
-            time, port, idx = idx
-
-            yield int(port), {
-                'chassis':  self._chassis_id(data[self.LLDP_REM_CHASSIS_ID], data[self.LLDP_REM_CHASSIS_ID_SUBTYPE]),
-                'sys_name': str(data[self.LLDP_REM_SYS_NAME]),
-                'port':     self._port_id(data[self.LLDP_REM_PORT_ID], data[self.LLDP_REM_PORT_ID_SUBTYPE]),
-            }
+import logging; log = logging.getLogger('pvl.hosts-lldp')
+import optparse
 
 def hosts_lldp (options, hosts) :
     """
@@ -257,40 +29,61 @@
     """
 
     for host in hosts :
-        snmp = host.extensions.get('snmp')
+        host_snmp = host.extensions.get('snmp')
 
-        log.debug("%s: %s", host, snmp)
 
-        if not snmp :
+        if not host_snmp :
+            log.debug("%s: skip non-snmp host", host)
             continue
 
-        lldp = LLDPAgent.apply(options, host.fqdn(), community=snmp.get('community'))
+        elif host.down :
+            log.debug("%s: skip down host", host)
+            continue
+
+        else :
+            log.debug("%s: %s", host, host_snmp)
+
+        agent = lldp.LLDPAgent.apply(options, host.fqdn(), community=host_snmp.get('community'))
 
         try :
-            local = lldp.local
-        except SNMPError as ex :
+            local = agent.local
+        except snmp.SNMPError as ex :
             log.warning("%s: %s", host, ex)
             continue
 
+        log.info("%s: %s", host, local)
+
         if local['sys_name'] != host.host :
             log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name'])
 
-        yield host, lldp
+        yield host, agent
 
 def apply_hosts_lldp (options, hosts) :
     """
         Query host LLDP info.
     """
+    
+    _hosts_lldp = list(hosts_lldp(options, hosts))
+    hosts_by_chassis = { }
+    
+    # first pass to discover hosts
+    for host, agent in _hosts_lldp :
+        chassis = agent.local['chassis']
+        log.info("%s: %s", host, chassis)
 
-    for host, lldp in hosts_lldp(options, hosts) :
-        log.info("%s: %s", host, lldp.local)
-        
-        for port, remote in lldp.remotes() :
-            port = lldp.port(port)
+        hosts_by_chassis[chassis] = host
+    
+    # second pass to discver links
+    for host, agent in _hosts_lldp :
+        for port, remote in agent.remotes() :
+            port = agent.port(port)
+
+            remote_chassis = remote['chassis']
+            remote_host = hosts_by_chassis.get(remote_chassis)
             
-            log.info("%s: %s: %s", host, port, remote)
+            log.info("%s: %s: %s (%s)", host, port, remote, remote_host)
 
-            yield host, merge(lldp.local, port), remote
+            yield host, merge(agent.local, port), remote, remote_host
 
 def main (argv) :
     """
@@ -300,9 +93,8 @@
     parser = optparse.OptionParser(main.__doc__)
     parser.add_option_group(pvl.args.parser(parser))
     parser.add_option_group(pvl.hosts.optparser(parser))
+    parser.add_option_group(pvl.snmp.snmp.options(parser))
 
-    parser.add_option('--snmp-community',
-            help="SNMPv2 read community")
 
     options, args = parser.parse_args(argv[1:])
     pvl.args.apply(options)
@@ -311,12 +103,11 @@
     hosts = pvl.hosts.apply(options, args)
     
     # apply
-    for host, local, remote in apply_hosts_lldp(options, hosts) :
-        print "{host:30} {local:40} <- {remote:40}".format(
-                host    = host,
-                local   = "{local[chassis]:15}[{local[port]}]".format(local=local),
-                remote  = "{remote[chassis]:15}[{remote[port]}]".format(remote=remote),
-        )
+    for host, local, remote, remote_host in apply_hosts_lldp(options, hosts) :
+        if remote_host :
+            print "{host:30} {local[port]:25} <-> {remote[port]:25} {remote_host:30}".format(host=host, local=local, remote=remote, remote_host=remote_host)
+        else :
+            print "{host:30} {local[port]:25} <-- {remote[port]:25} # {remote[chassis]} ({remote[sys_name]})".format(host=host, local=local, remote=remote)
 
 if __name__ == '__main__':
     pvl.args.main(main)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/snmp/snmp.py	Mon Mar 17 17:31:45 2014 +0200
@@ -0,0 +1,133 @@
+"""
+    Requirements:
+        pysnmp
+        pysnmp-mibs
+"""
+
+import logging; log = logging.getLogger('pvl.snmp.snmp')
+import optparse
+
+from pysnmp.entity.rfc3413.oneliner import cmdgen as pysnmp
+import collections
+
+def options (parser) :
+    parser = optparse.OptionGroup(parser, "SNMP Agent")
+
+    parser.add_option('--snmp-community',
+            help="SNMPv2 read community")
+
+    return parser
+
+class SNMPError (Exception) :
+    pass
+
+class SNMPEngineError (SNMPError) :
+    """
+        Internal SNMP Engine error (?)
+    """
+
+class SNMPAgent (object) :
+    """
+        GET SNMP shit from a remote host.
+    """
+
+    SNMP_PORT = 161
+    snmp_cmdgen = pysnmp.CommandGenerator()
+
+    @classmethod
+    def apply (cls, options, host, community=None) :
+        port = cls.SNMP_PORT
+
+        if community is None :
+            community = options.snmp_community
+
+
+        if '@' in host :
+            community, host = host.split('@', 1)
+        
+        if ':' in host :
+            host, port = host.rsplit(':', 1)
+
+        return cls(
+                pysnmp.CommunityData(community),
+                pysnmp.UdpTransportTarget((host, port))
+        )
+
+    def __init__ (self, security, transport) :
+        self.security = security
+        self.transport = transport
+
+    def get (self, *request) :
+        """
+            request = (
+                pysnmp.MibVariable('IF-MIB', 'ifInOctets', 1),
+            )
+        """
+
+        opts = dict(
+                lookupNames     = True,
+                lookupValues    = True,
+        )
+
+        try :
+            error, error_status, error_index, response = self.snmp_cmdgen.getCmd(self.security, self.transport,  *request, **opts)
+        except pysnmp.error.PySnmpError as ex :
+            raise SNMPEngineError(ex)
+
+        if error :
+            raise SNMPEngineError(error)
+        
+        if error_status :
+            raise SNMPError(errorStatus.prettyPrint())
+        
+        return response
+        #for name, value in response :
+        #    yield name.getMibSymbol(), value.prettyPrint()
+
+    def walk (self, *request) :
+        """
+            request = (
+                    pysnmp.MibVariable('IF-MIB', 'ifInOctets'),
+            )
+        """
+
+        opts = dict(
+                lookupNames     = True,
+                lookupValues    = True,
+        )
+
+        try :
+            error, error_status, error_index, responses = self.snmp_cmdgen.nextCmd(self.security, self.transport, *request, **opts)
+        except pysnmp.error.PySnmpError as ex :
+            raise SNMPEngineError(ex)
+
+        if error :
+            raise SNMPEngineError(error)
+        
+        if error_status :
+            raise SNMPError(errorStatus.prettyPrint())
+        
+        return responses
+        #for response in responses:
+        #    for name, value in response :
+        #        yield name.getMibSymbol(), value.prettyPrint()
+
+    def table (self, *columns) :
+        """
+            Given [oid] returns { idx: { oid: value } }
+        """
+
+        data = collections.defaultdict(dict)
+
+        for row in self.walk(*columns) :
+            log.debug("%s", row)
+
+            for column, (field, value) in zip(columns, row) :
+                mib, sym, idx = field.getMibSymbol()
+
+                log.debug("%s::%s[%s]: %s: %s", mib, sym, ', '.join(str(x) for x in idx), column, value)
+                
+                data[idx][column] = value
+            
+        return data.items()
+