# HG changeset patch # User Tero Marttila # Date 1395064362 -7200 # Node ID ba47a64f61f9ae0db4ea54c39a85cc12fa115fdb # Parent 6fe465ce6d527ae5ccb7ed159047f4e694152011 pvl.hosts-lldp: refactor lldp collection schema diff -r 6fe465ce6d52 -r ba47a64f61f9 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)