pvl.hosts-lldp: refactor lldp collection schema
authorTero Marttila <terom@paivola.fi>
Mon, 17 Mar 2014 15:52:42 +0200
changeset 382 ba47a64f61f9
parent 381 6fe465ce6d52
child 383 87b49aa52b3d
pvl.hosts-lldp: refactor lldp collection schema
bin/pvl.hosts-lldp
--- a/bin/pvl.hosts-lldp	Mon Mar 17 15:51:08 2014 +0200
+++ b/bin/pvl.hosts-lldp	Mon Mar 17 15:52:42 2014 +0200
@@ -15,6 +15,7 @@
 
 import pvl.args
 import pvl.hosts
+from pvl.invoke import merge
 
 import logging; log = logging.getLogger('pvl.hosts-snmp')
 import optparse
@@ -39,10 +40,13 @@
     snmp_cmdgen = pysnmp.CommandGenerator()
 
     @classmethod
-    def apply (cls, options, host) :
-        community = options.snmp_community
+    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)
         
@@ -113,28 +117,22 @@
         #    for name, value in response :
         #        yield name.getMibSymbol(), value.prettyPrint()
 
-    def table (self, table) :
+    def table (self, *columns) :
         """
-            Load a simple table with a running index.
-
-            Given [(attr, oid, type)] yields { attr: type(value) }
+            Given [oid] returns { idx: { oid: value } }
         """
 
         data = collections.defaultdict(dict)
 
-        for row in self.walk(*[oid for attr, oid, type in table]) :
+        for row in self.walk(*columns) :
             log.debug("%s", row)
 
-            for (attr, type), (field, value) in zip([(attr, type) for attr, oid, type in table], row) :
+            for column, (field, value) in zip(columns, row) :
                 mib, sym, idx = field.getMibSymbol()
 
-                # convert
-                if type :
-                    value = type(value)
-
-                log.debug("%s::%s.%s: %s: %s", mib, sym, idx, attr, value)
+                log.debug("%s::%s[%s]: %s: %s", mib, sym, ', '.join(str(x) for x in idx), column, value)
                 
-                data[idx][attr] = value
+                data[idx][column] = value
             
         return data.items()
 
@@ -152,23 +150,55 @@
         Query LLDP info from a remote agent.
     """
 
-    LOCAL = (
-            ( 'chassis_id',     pysnmp.MibVariable('LLDP-MIB', 'lldpLocChassisId'),     macaddr ),
-            ( 'sys_name',       pysnmp.MibVariable('LLDP-MIB', 'lldpLocSysName'),       str     ),
-    )
-    
+
+    @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.LOCAL) :
-            return data
+        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]),
+            }
 
-    PORTS = (
-            ( 'port_id',        pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortId'),      str     ),
-    )
+    LLDP_LOC_PORT_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortId')
+    LLDP_LOC_PORT_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortIdSubtype')
 
     @memoized_property
     def ports (self) :
@@ -178,31 +208,89 @@
 
         ports = { }
 
-        for idx, data in self.table(self.PORTS) :
+        for idx, data in self.table(
+                self.LLDP_LOC_PORT_ID,
+                self.LLDP_LOC_PORT_ID_SUBTYPE,
+        ) :
             port, = idx
             
-            ports[int(port)] = data
+            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[port]
+        return self.ports[int(port)]
 
-    REMOTE_DATA = (
-            ( 'chassis_id',     pysnmp.MibVariable('LLDP-MIB', 'lldpRemChassisId'),     macaddr ),
-            ( 'sys_name',       pysnmp.MibVariable('LLDP-MIB', 'lldpRemSysName'),       str     ),
-            ( 'remote_port',    pysnmp.MibVariable('LLDP-MIB', 'lldpRemPortId'),        str     ),
-    )
+    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.REMOTE_DATA) :
+        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), data
+            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]),
+            }
+
+def hosts_lldp (options, hosts) :
+    """
+        Discover LLDP-supporting hosts.
+
+        Yields Host, LLDPAgent
+    """
+
+    for host in hosts :
+        snmp = host.extensions.get('snmp')
+
+        log.debug("%s: %s", host, snmp)
+
+        if not snmp :
+            continue
+
+        lldp = LLDPAgent.apply(options, host.fqdn(), community=snmp.get('community'))
+
+        try :
+            local = lldp.local
+        except SNMPError as ex :
+            log.warning("%s: %s", host, ex)
+            continue
+
+        if local['sys_name'] != host.host :
+            log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name'])
+
+        yield host, lldp
+
+def apply_hosts_lldp (options, hosts) :
+    """
+        Query host LLDP info.
+    """
+
+    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)
+            
+            log.info("%s: %s: %s", host, port, remote)
+
+            yield host, merge(lldp.local, port), remote
 
 def main (argv) :
     """
@@ -211,7 +299,7 @@
 
     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.hosts.optparser(parser))
 
     parser.add_option('--snmp-community',
             help="SNMPv2 read community")
@@ -220,15 +308,15 @@
     pvl.args.apply(options)
 
     # input
-    #hosts = pvl.hosts.apply(options, args)
-
-    for arg in args :
-        lldp = LLDPAgent.apply(options, arg)
-        
-        print arg, "{chassis_id}({sys_name})".format(**lldp.local)
-        
-        for port, remote in lldp.remotes() :
-            print '\t', lldp.port(port), remote
+    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),
+        )
 
 if __name__ == '__main__':
     pvl.args.main(main)