bin/pvl.hosts-lldp
changeset 384 caa3dbbdbe83
parent 382 ba47a64f61f9
child 386 9e1abcf47d27
equal deleted inserted replaced
383:87b49aa52b3d 384:caa3dbbdbe83
    14 """
    14 """
    15 
    15 
    16 import pvl.args
    16 import pvl.args
    17 import pvl.hosts
    17 import pvl.hosts
    18 from pvl.invoke import merge
    18 from pvl.invoke import merge
       
    19 from pvl.snmp import snmp, lldp
    19 
    20 
    20 import logging; log = logging.getLogger('pvl.hosts-snmp')
    21 import logging; log = logging.getLogger('pvl.hosts-lldp')
    21 import optparse
    22 import optparse
    22 
       
    23 from pysnmp.entity.rfc3413.oneliner import cmdgen as pysnmp
       
    24 import collections
       
    25 
       
    26 class SNMPError (Exception) :
       
    27     pass
       
    28 
       
    29 class SNMPEngineError (SNMPError) :
       
    30     """
       
    31         Internal SNMP Engine error (?)
       
    32     """
       
    33 
       
    34 class SNMPAgent (object) :
       
    35     """
       
    36         GET SNMP shit from a remote host.
       
    37     """
       
    38 
       
    39     SNMP_PORT = 161
       
    40     snmp_cmdgen = pysnmp.CommandGenerator()
       
    41 
       
    42     @classmethod
       
    43     def apply (cls, options, host, community=None) :
       
    44         port = cls.SNMP_PORT
       
    45 
       
    46         if community is None :
       
    47             community = options.snmp_community
       
    48 
       
    49 
       
    50         if '@' in host :
       
    51             community, host = host.split('@', 1)
       
    52         
       
    53         if ':' in host :
       
    54             host, port = host.rsplit(':', 1)
       
    55 
       
    56         return cls(
       
    57                 pysnmp.CommunityData(community),
       
    58                 pysnmp.UdpTransportTarget((host, port))
       
    59         )
       
    60 
       
    61     def __init__ (self, security, transport) :
       
    62         self.security = security
       
    63         self.transport = transport
       
    64 
       
    65     def get (self, *request) :
       
    66         """
       
    67             request = (
       
    68                 pysnmp.MibVariable('IF-MIB', 'ifInOctets', 1),
       
    69             )
       
    70         """
       
    71 
       
    72         opts = dict(
       
    73                 lookupNames     = True,
       
    74                 lookupValues    = True,
       
    75         )
       
    76 
       
    77         try :
       
    78             error, error_status, error_index, response = self.snmp_cmdgen.getCmd(self.security, self.transport,  *request, **opts)
       
    79         except pysnmp.error.PySnmpError as ex :
       
    80             raise SNMPEngineError(ex)
       
    81 
       
    82         if error :
       
    83             raise SNMPEngineError(error)
       
    84         
       
    85         if error_status :
       
    86             raise SNMPError(errorStatus.prettyPrint())
       
    87         
       
    88         return response
       
    89         #for name, value in response :
       
    90         #    yield name.getMibSymbol(), value.prettyPrint()
       
    91 
       
    92     def walk (self, *request) :
       
    93         """
       
    94             request = (
       
    95                     pysnmp.MibVariable('IF-MIB', 'ifInOctets'),
       
    96             )
       
    97         """
       
    98 
       
    99         opts = dict(
       
   100                 lookupNames     = True,
       
   101                 lookupValues    = True,
       
   102         )
       
   103 
       
   104         try :
       
   105             error, error_status, error_index, responses = self.snmp_cmdgen.nextCmd(self.security, self.transport, *request, **opts)
       
   106         except pysnmp.error.PySnmpError as ex :
       
   107             raise SNMPEngineError(ex)
       
   108 
       
   109         if error :
       
   110             raise SNMPEngineError(error)
       
   111         
       
   112         if error_status :
       
   113             raise SNMPError(errorStatus.prettyPrint())
       
   114         
       
   115         return responses
       
   116         #for response in responses:
       
   117         #    for name, value in response :
       
   118         #        yield name.getMibSymbol(), value.prettyPrint()
       
   119 
       
   120     def table (self, *columns) :
       
   121         """
       
   122             Given [oid] returns { idx: { oid: value } }
       
   123         """
       
   124 
       
   125         data = collections.defaultdict(dict)
       
   126 
       
   127         for row in self.walk(*columns) :
       
   128             log.debug("%s", row)
       
   129 
       
   130             for column, (field, value) in zip(columns, row) :
       
   131                 mib, sym, idx = field.getMibSymbol()
       
   132 
       
   133                 log.debug("%s::%s[%s]: %s: %s", mib, sym, ', '.join(str(x) for x in idx), column, value)
       
   134                 
       
   135                 data[idx][column] = value
       
   136             
       
   137         return data.items()
       
   138 
       
   139 from memoized_property import memoized_property
       
   140 
       
   141 def macaddr (value) :
       
   142     """
       
   143         Excepts a MAC address from an SNMP OctetString.
       
   144     """
       
   145 
       
   146     return ':'.join('{octet:02x}'.format(octet=c) for c in value.asNumbers())
       
   147 
       
   148 class LLDPAgent (SNMPAgent) :
       
   149     """
       
   150         Query LLDP info from a remote agent.
       
   151     """
       
   152 
       
   153 
       
   154     @classmethod
       
   155     def _chassis_id (cls, chassis_id, subtype) :
       
   156         log.debug("%s: %r", subtype, chassis_id)
       
   157         
       
   158         # XXX: reference names from LLDP-MIB.py
       
   159         if subtype == 4:
       
   160             return macaddr(chassis_id)
       
   161         else :
       
   162             return chassis_id
       
   163  
       
   164     @classmethod
       
   165     def _port_id (cls, port_id, subtype) :
       
   166         log.debug("%s: %r", subtype, port_id)
       
   167         
       
   168         # XXX: reference names from LLDP-MIB.py
       
   169         if subtype == 5: # interfaceName -> IF-MIB::ifName ?
       
   170             return str(port_id)
       
   171         elif subtype == 3 : # macAddress
       
   172             return macaddr(port_id)
       
   173         elif subtype == 7 : # local
       
   174             return str(port_id) # XXX: integer?
       
   175         else :
       
   176             log.warn("unknown subtype=%d: %r", subtype, port_id)
       
   177 
       
   178             return port_id
       
   179        
       
   180     LLDP_LOC_CHASSIS_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpLocChassisId')
       
   181     LLDP_LOC_CHASSIS_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpLocChassisIdSubtype')
       
   182     LLDP_LOC_SYS_NAME = pysnmp.MibVariable('LLDP-MIB', 'lldpLocSysName')
       
   183 
       
   184     @memoized_property
       
   185     def local (self) :
       
   186         """
       
   187             Describe the local system.
       
   188         """
       
   189 
       
   190         for idx, data in self.table(
       
   191                 self.LLDP_LOC_CHASSIS_ID,
       
   192                 self.LLDP_LOC_CHASSIS_ID_SUBTYPE,
       
   193                 self.LLDP_LOC_SYS_NAME,
       
   194         ) :
       
   195             return {
       
   196                     'chassis':  self._chassis_id(data[self.LLDP_LOC_CHASSIS_ID], data[self.LLDP_LOC_CHASSIS_ID_SUBTYPE]),
       
   197                     'sys_name': str(data[self.LLDP_LOC_SYS_NAME]),
       
   198             }
       
   199 
       
   200     LLDP_LOC_PORT_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortId')
       
   201     LLDP_LOC_PORT_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpLocPortIdSubtype')
       
   202 
       
   203     @memoized_property
       
   204     def ports (self) :
       
   205         """
       
   206             Describe the local ports.
       
   207         """
       
   208 
       
   209         ports = { }
       
   210 
       
   211         for idx, data in self.table(
       
   212                 self.LLDP_LOC_PORT_ID,
       
   213                 self.LLDP_LOC_PORT_ID_SUBTYPE,
       
   214         ) :
       
   215             port, = idx
       
   216             
       
   217             ports[int(port)] = {
       
   218                     'port': self._port_id(data[self.LLDP_LOC_PORT_ID], data[self.LLDP_LOC_PORT_ID_SUBTYPE]),
       
   219             }
       
   220 
       
   221         return ports
       
   222 
       
   223     def port (self, port) :
       
   224         return self.ports[int(port)]
       
   225 
       
   226     LLDP_REM_CHASSIS_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpRemChassisId')
       
   227     LLDP_REM_CHASSIS_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpRemChassisIdSubtype')
       
   228     LLDP_REM_SYS_NAME = pysnmp.MibVariable('LLDP-MIB', 'lldpRemSysName')
       
   229     LLDP_REM_PORT_ID = pysnmp.MibVariable('LLDP-MIB', 'lldpRemPortId')
       
   230     LLDP_REM_PORT_ID_SUBTYPE = pysnmp.MibVariable('LLDP-MIB', 'lldpRemPortIdSubtype')
       
   231 
       
   232     def remotes (self) :
       
   233         """
       
   234             Describe remote systems, indexed by local port.
       
   235         """
       
   236 
       
   237         for idx, data in self.table(
       
   238                 self.LLDP_REM_CHASSIS_ID,
       
   239                 self.LLDP_REM_CHASSIS_ID_SUBTYPE,
       
   240                 self.LLDP_REM_SYS_NAME,
       
   241                 self.LLDP_REM_PORT_ID,
       
   242                 self.LLDP_REM_PORT_ID_SUBTYPE,
       
   243         ) :
       
   244             time, port, idx = idx
       
   245 
       
   246             yield int(port), {
       
   247                 'chassis':  self._chassis_id(data[self.LLDP_REM_CHASSIS_ID], data[self.LLDP_REM_CHASSIS_ID_SUBTYPE]),
       
   248                 'sys_name': str(data[self.LLDP_REM_SYS_NAME]),
       
   249                 'port':     self._port_id(data[self.LLDP_REM_PORT_ID], data[self.LLDP_REM_PORT_ID_SUBTYPE]),
       
   250             }
       
   251 
    23 
   252 def hosts_lldp (options, hosts) :
    24 def hosts_lldp (options, hosts) :
   253     """
    25     """
   254         Discover LLDP-supporting hosts.
    26         Discover LLDP-supporting hosts.
   255 
    27 
   256         Yields Host, LLDPAgent
    28         Yields Host, LLDPAgent
   257     """
    29     """
   258 
    30 
   259     for host in hosts :
    31     for host in hosts :
   260         snmp = host.extensions.get('snmp')
    32         host_snmp = host.extensions.get('snmp')
   261 
    33 
   262         log.debug("%s: %s", host, snmp)
       
   263 
    34 
   264         if not snmp :
    35         if not host_snmp :
       
    36             log.debug("%s: skip non-snmp host", host)
   265             continue
    37             continue
   266 
    38 
   267         lldp = LLDPAgent.apply(options, host.fqdn(), community=snmp.get('community'))
    39         elif host.down :
       
    40             log.debug("%s: skip down host", host)
       
    41             continue
       
    42 
       
    43         else :
       
    44             log.debug("%s: %s", host, host_snmp)
       
    45 
       
    46         agent = lldp.LLDPAgent.apply(options, host.fqdn(), community=host_snmp.get('community'))
   268 
    47 
   269         try :
    48         try :
   270             local = lldp.local
    49             local = agent.local
   271         except SNMPError as ex :
    50         except snmp.SNMPError as ex :
   272             log.warning("%s: %s", host, ex)
    51             log.warning("%s: %s", host, ex)
   273             continue
    52             continue
       
    53 
       
    54         log.info("%s: %s", host, local)
   274 
    55 
   275         if local['sys_name'] != host.host :
    56         if local['sys_name'] != host.host :
   276             log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name'])
    57             log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name'])
   277 
    58 
   278         yield host, lldp
    59         yield host, agent
   279 
    60 
   280 def apply_hosts_lldp (options, hosts) :
    61 def apply_hosts_lldp (options, hosts) :
   281     """
    62     """
   282         Query host LLDP info.
    63         Query host LLDP info.
   283     """
    64     """
       
    65     
       
    66     _hosts_lldp = list(hosts_lldp(options, hosts))
       
    67     hosts_by_chassis = { }
       
    68     
       
    69     # first pass to discover hosts
       
    70     for host, agent in _hosts_lldp :
       
    71         chassis = agent.local['chassis']
       
    72         log.info("%s: %s", host, chassis)
   284 
    73 
   285     for host, lldp in hosts_lldp(options, hosts) :
    74         hosts_by_chassis[chassis] = host
   286         log.info("%s: %s", host, lldp.local)
    75     
   287         
    76     # second pass to discver links
   288         for port, remote in lldp.remotes() :
    77     for host, agent in _hosts_lldp :
   289             port = lldp.port(port)
    78         for port, remote in agent.remotes() :
       
    79             port = agent.port(port)
       
    80 
       
    81             remote_chassis = remote['chassis']
       
    82             remote_host = hosts_by_chassis.get(remote_chassis)
   290             
    83             
   291             log.info("%s: %s: %s", host, port, remote)
    84             log.info("%s: %s: %s (%s)", host, port, remote, remote_host)
   292 
    85 
   293             yield host, merge(lldp.local, port), remote
    86             yield host, merge(agent.local, port), remote, remote_host
   294 
    87 
   295 def main (argv) :
    88 def main (argv) :
   296     """
    89     """
   297         SNMP polling.
    90         SNMP polling.
   298     """
    91     """
   299 
    92 
   300     parser = optparse.OptionParser(main.__doc__)
    93     parser = optparse.OptionParser(main.__doc__)
   301     parser.add_option_group(pvl.args.parser(parser))
    94     parser.add_option_group(pvl.args.parser(parser))
   302     parser.add_option_group(pvl.hosts.optparser(parser))
    95     parser.add_option_group(pvl.hosts.optparser(parser))
       
    96     parser.add_option_group(pvl.snmp.snmp.options(parser))
   303 
    97 
   304     parser.add_option('--snmp-community',
       
   305             help="SNMPv2 read community")
       
   306 
    98 
   307     options, args = parser.parse_args(argv[1:])
    99     options, args = parser.parse_args(argv[1:])
   308     pvl.args.apply(options)
   100     pvl.args.apply(options)
   309 
   101 
   310     # input
   102     # input
   311     hosts = pvl.hosts.apply(options, args)
   103     hosts = pvl.hosts.apply(options, args)
   312     
   104     
   313     # apply
   105     # apply
   314     for host, local, remote in apply_hosts_lldp(options, hosts) :
   106     for host, local, remote, remote_host in apply_hosts_lldp(options, hosts) :
   315         print "{host:30} {local:40} <- {remote:40}".format(
   107         if remote_host :
   316                 host    = host,
   108             print "{host:30} {local[port]:25} <-> {remote[port]:25} {remote_host:30}".format(host=host, local=local, remote=remote, remote_host=remote_host)
   317                 local   = "{local[chassis]:15}[{local[port]}]".format(local=local),
   109         else :
   318                 remote  = "{remote[chassis]:15}[{remote[port]}]".format(remote=remote),
   110             print "{host:30} {local[port]:25} <-- {remote[port]:25} # {remote[chassis]} ({remote[sys_name]})".format(host=host, local=local, remote=remote)
   319         )
       
   320 
   111 
   321 if __name__ == '__main__':
   112 if __name__ == '__main__':
   322     pvl.args.main(main)
   113     pvl.args.main(main)