# HG changeset patch # User Tero Marttila # Date 1395070305 -7200 # Node ID caa3dbbdbe8326f8cde1f9f2cdc3ff90fc2192a3 # Parent 87b49aa52b3d140623017481663fecaedcb6bf8c split upt pvl.hosts-lldp into pvl.snmp.snmp and pvl.snmp.lldp, match remote hosts, better formatting diff -r 87b49aa52b3d -r caa3dbbdbe83 bin/pvl.hosts-lldp --- 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) diff -r 87b49aa52b3d -r caa3dbbdbe83 pvl/snmp/snmp.py --- /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() +