bin/pvl.hosts-lldp
changeset 399 aadf76a05ec1
parent 398 de275bf6db70
child 400 41dd2a867e0a
equal deleted inserted replaced
398:de275bf6db70 399:aadf76a05ec1
     1 #!/usr/bin/env python
       
     2 
       
     3 """
       
     4     Requirements:
       
     5         pydot
       
     6 """
       
     7 
       
     8 import pvl.args
       
     9 import pvl.hosts
       
    10 from pvl.invoke import merge
       
    11 from pvl.snmp import snmp, lldp, vlan, bridge
       
    12 
       
    13 import logging; log = logging.getLogger('pvl.hosts-lldp')
       
    14 import optparse
       
    15 
       
    16 def hosts_snmp (options, hosts) :
       
    17     """
       
    18         Discover SNMP-supporting hosts.
       
    19 
       
    20         Yields Host, snmpdata
       
    21     """
       
    22 
       
    23     for host in hosts :
       
    24         host_snmp = host.extensions.get('snmp')
       
    25 
       
    26         if not host_snmp :
       
    27             log.debug("%s: skip non-snmp host", host)
       
    28             continue
       
    29 
       
    30         elif host.down :
       
    31             log.debug("%s: skip down host", host)
       
    32             continue
       
    33 
       
    34         else :
       
    35             log.debug("%s: %s", host, host_snmp)
       
    36 
       
    37         yield host, host_snmp
       
    38 
       
    39 def hosts_lldp (options, hosts) :
       
    40     """
       
    41         Discover LLDP-supporting hosts.
       
    42 
       
    43         Yields Host, LLDPAgent
       
    44     """
       
    45 
       
    46     for host, host_snmp in hosts_snmp(options, hosts) :
       
    47         agent = lldp.LLDPAgent.apply(options, host.fqdn(), community=host_snmp.get('community'))
       
    48 
       
    49         try :
       
    50             local = agent.local
       
    51         except snmp.SNMPError as ex :
       
    52             log.warning("%s: %s", host, ex)
       
    53             continue
       
    54         
       
    55         if not local :
       
    56             log.info("%s: no lldp support", host)
       
    57             continue
       
    58 
       
    59         log.info("%s: %s", host, local)
       
    60 
       
    61         if local['sys_name'] != host.host :
       
    62             log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name'])
       
    63 
       
    64         yield host, agent
       
    65 
       
    66 def hosts_vlan (options, hosts) :
       
    67     """
       
    68         Discover VLAN-supporting hosts.
       
    69 
       
    70         Yields Host, VLANAgent
       
    71     """
       
    72 
       
    73     for host, host_snmp in hosts_snmp(options, hosts) :
       
    74         agent = vlan.VLANAgent.apply(options, host.fqdn(), community=host_snmp.get('community'))
       
    75 
       
    76         try :
       
    77             count = agent.count
       
    78         except snmp.SNMPError as ex :
       
    79             log.warning("%s: %s", host, ex)
       
    80             continue
       
    81 
       
    82         log.info("%s: %s", host, count)
       
    83 
       
    84         yield host, agent
       
    85 
       
    86 def hosts_bridge (options, hosts) :
       
    87     """
       
    88         Discover Bridge-supporting hosts.
       
    89 
       
    90         Yields Host, BridgeAgent
       
    91     """
       
    92 
       
    93     for host, host_snmp in hosts_snmp(options, hosts) :
       
    94         agent = bridge.BridgeAgent.apply(options, host.fqdn(), community=host_snmp.get('community'))
       
    95 
       
    96         try :
       
    97             agent.ping()
       
    98         except snmp.SNMPError as ex :
       
    99             log.warning("%s: %s", host, ex)
       
   100             continue
       
   101 
       
   102         log.info("%s", host)
       
   103 
       
   104         yield host, agent
       
   105 
       
   106 
       
   107 def apply_hosts_lldp (options, hosts) :
       
   108     """
       
   109         Query host LLDP info.
       
   110     """
       
   111     
       
   112     _hosts_lldp = list(hosts_lldp(options, hosts))
       
   113     hosts_by_chassis = { }
       
   114     
       
   115     # first pass to discover hosts
       
   116     for host, agent in _hosts_lldp :
       
   117         chassis = agent.local['chassis']
       
   118         log.info("%s: %s", host, chassis)
       
   119 
       
   120         hosts_by_chassis[chassis] = host
       
   121     
       
   122     # second pass to discver links
       
   123     for host, agent in _hosts_lldp :
       
   124         try :
       
   125             remotes = list(agent.remotes())
       
   126         except snmp.SNMPError as ex :
       
   127             log.warn("%s: broken lldp remotes: %s", host, ex)
       
   128             continue
       
   129 
       
   130         for port, remote in remotes :
       
   131             port = agent.port(port)
       
   132 
       
   133             remote_chassis = remote['chassis']
       
   134             remote_host = hosts_by_chassis.get(remote_chassis)
       
   135             
       
   136             log.info("%s: %s: %s (%s)", host, port, remote, remote_host)
       
   137 
       
   138             yield host, merge(agent.local, port), remote, remote_host
       
   139 
       
   140 import collections
       
   141 
       
   142 def apply_hosts_vlan (options, hosts) :
       
   143     """
       
   144         Query host VLAN ports.
       
   145 
       
   146         Yields host, { port: (untagged, [tagged]) }
       
   147     """
       
   148 
       
   149     _hosts_vlan = list(hosts_vlan(options, hosts))
       
   150 
       
   151     for host, agent in _hosts_vlan :
       
   152         # only one untagged vlan / port
       
   153         vlan_untagged = { }
       
   154             
       
   155         # multiple taggd vlans / port
       
   156         vlan_tagged = collections.defaultdict(set)
       
   157 
       
   158         for vlan, (tagged, untagged) in agent.vlan_ports() :
       
   159             log.info("%s: %s: %s + %s", host, vlan, tagged, untagged)
       
   160             
       
   161             for port in tagged :
       
   162                 vlan_tagged[port].add(vlan)
       
   163             
       
   164             for port in untagged :
       
   165                 if port in vlan_untagged :
       
   166                     log.warning("%s: duplicate untagged vlan %s for port %s on vlan %s", host, vlan, port, vlan_untagged[port])
       
   167 
       
   168                 vlan_untagged[port] = vlan
       
   169             
       
   170         # pack into {port: (untagged, [tagged]) }
       
   171         yield host, dict(
       
   172             (
       
   173                     port, (vlan_untagged.get(port), tuple(vlan_tagged[port]))
       
   174             ) for port in set(vlan_untagged) | set(vlan_tagged)
       
   175         )
       
   176 
       
   177 def apply_hosts_bridge (options, hosts) :
       
   178     """
       
   179         Query host bridge tables.
       
   180 
       
   181         Yields host, { port: (macs) }
       
   182     """
       
   183 
       
   184     for host, agent in hosts_bridge(options, hosts) :
       
   185         ports = collections.defaultdict(list)
       
   186 
       
   187         try :
       
   188             vlan_fdb_ports = list(agent.vlan_fdb_ports())
       
   189         except snmp.SNMPError as ex :
       
   190             log.warn("%s: broken dot1q fdb: %s", host, ex)
       
   191             continue
       
   192 
       
   193         if vlan_fdb_ports :
       
   194             log.info("%s: have dot1q ports", host)
       
   195 
       
   196             for ether, port, vlan in agent.vlan_fdb_ports() :
       
   197                 if not port :
       
   198                     # XXX: unknown?
       
   199                     continue
       
   200 
       
   201                 ports[(port, vlan)].append(ether)
       
   202         else :
       
   203             try :
       
   204                 fdb_ports = list(agent.fdb_ports())
       
   205             except snmp.SNMPError as ex :
       
   206                 log.warn("%s: broken dot1q fdb: %s", host, ex)
       
   207                 continue
       
   208             
       
   209             # fallback to dot1d fdb
       
   210             log.info("%s: fallback to dot1d", host)
       
   211 
       
   212             for ether, port in agent.fdb_ports() :
       
   213                 if not port :
       
   214                     # XXX: unknown?
       
   215                     continue
       
   216 
       
   217                 ports[(port, None)].append(ether)
       
   218 
       
   219         yield host, ports
       
   220 
       
   221 COLOR_VLANS = {
       
   222     1:      'grey',         # pvl-lan
       
   223     2:      'blue',         # pvl-lan2
       
   224     3:      'red',          # pvl-san
       
   225     4:      'green',        # pvl-veturi
       
   226     7:      'orange',       # pvl-ranssi
       
   227     8:      'yellow',       # pvl-mgmt
       
   228     10:     'brown',        # pvl-public
       
   229     100:    'navyblue',     # pvl-test
       
   230     103:    'red4',         # pvl-test-san
       
   231     104:    'red2',         # pvl-ganeti
       
   232     192:    'purple',       # paivola-services
       
   233     255:    'magenta',      # pvl-sonera
       
   234 }
       
   235 
       
   236 def apply_graph (options, items, vlans={}) :
       
   237     import pydot
       
   238 
       
   239     dot = pydot.Dot(graph_name='lldp_hosts', graph_type='digraph',
       
   240             # XXX: breaks multi-edges?
       
   241             #splines     = 'true',
       
   242 
       
   243             sep             = '+25,25',
       
   244             overlap         = 'scalexy',
       
   245 
       
   246             # only applies to loops
       
   247             nodesep     = 0.5,
       
   248     )
       
   249     dot.set_edge_defaults(
       
   250             labeldistance   = 3.0,
       
   251             penwidth        = 2.0,
       
   252     )
       
   253 
       
   254     nodes = { }
       
   255     edges = { }
       
   256     vlan_colors = { } # { vlan: color }
       
   257 
       
   258     for host, local, remote, remote_host in items :
       
   259         # src
       
   260         src_name = str(host)
       
   261         src_label = '"{host.location}"'.format(host=host)
       
   262 
       
   263         if src_name in nodes :
       
   264             src = nodes[src_name]
       
   265         else :
       
   266             src = nodes[src_name] = pydot.Node(src_name,
       
   267                     label       = src_label,
       
   268                     fontsize    = 18,
       
   269             )
       
   270             dot.add_node(src)
       
   271         
       
   272         # dst
       
   273         if remote_host :
       
   274             dst_name = str(remote_host)
       
   275             dst_label = '"{host.location}"'.format(host=remote_host)
       
   276         else :
       
   277             dst_name = remote['chassis'].replace(':', '-')
       
   278 
       
   279             # XXX: pydot is not smart enough to quote this
       
   280             if remote['sys_name'] :
       
   281                 dst_label = '"{remote[chassis]} ({remote[sys_name]})"'.format(remote=remote)
       
   282             else :
       
   283                 dst_label = '"{remote[chassis]}"'.format(remote=remote)
       
   284         
       
   285         if dst_name in nodes :
       
   286             dst = nodes[dst_name]
       
   287         else :
       
   288             dst = nodes[dst_name] = pydot.Node(dst_name,
       
   289                     label       = dst_label,
       
   290                     fontsize    = 18,
       
   291             )
       
   292             dot.add_node(dst)
       
   293 
       
   294         # edges
       
   295         headlabel = '"{remote[port]}"'.format(remote=remote)
       
   296         taillabel = '"{local[port]}"'.format(local=local)
       
   297         fillcolor = 'black'
       
   298         color = 'black'
       
   299         untag = tagged = None
       
   300 
       
   301         # vlans?
       
   302         if vlans and host in vlans and local['port_id'] in vlans[host] :
       
   303             untag, tagged = vlans[host][local['port_id']]
       
   304             
       
   305             log.debug("%s#%s: %s+%s", host, local['port_id'], untag, tagged)
       
   306             
       
   307             colors = []
       
   308             for tag in sorted(tagged) :
       
   309                 if tag in COLOR_VLANS :
       
   310                     colors.append(COLOR_VLANS[tag])
       
   311                 elif tag in vlan_colors :
       
   312                     colors.append(vlan_colors[tag])
       
   313                 else :
       
   314                     color = '/paired12/{count}'.format(count=1+len(vlan_colors))
       
   315                     
       
   316                     log.info("%s#%s: chosing new vlan %s color %s", host, local['port_id'], tag, color)
       
   317 
       
   318                     vlan_colors[tag] = color
       
   319                     colors.append(color)
       
   320 
       
   321             if not untag :
       
   322                 pass
       
   323             elif untag in COLOR_VLANS :
       
   324                 fillcolor = COLOR_VLANS[untag]
       
   325             elif untag in vlan_colors :
       
   326                 fillcolor = vlan_colors[untag]
       
   327             else :
       
   328                 color = '/paired12/{count}'.format(count=1+len(vlan_colors))
       
   329 
       
   330                 log.warn("%s#%s: chosing new vlan %s color %s", host, local['port_id'], untag, color)
       
   331                     
       
   332                 fillcolor = vlan_colors[tag] = color
       
   333             
       
   334             # first color overrides fillcolor for heads
       
   335             if colors and untag :
       
   336                 color = ':'.join([fillcolor] + colors)
       
   337             elif colors :
       
   338                 color = ':'.join(colors)
       
   339             elif fillcolor :
       
   340                 color = fillcolor
       
   341 
       
   342         elif vlans :
       
   343             # XXX: this happens when LLDP gives us the LACP ports but the VLANS are on the TRK port
       
   344             log.warn("%s#%s: unknown port for vlans: %s", host, local['port_id'], vlans.get(host))
       
   345 
       
   346         # edge
       
   347         if (src_name, local['port'], dst_name, remote['port']) in edges :
       
   348             log.warning("%s:%s <- %s:%s: duplicate", src_name, local['port'], dst_name, remote['port'])
       
   349         
       
   350         elif (dst_name, remote['port'], src_name, local['port']) in edges :
       
   351             log.info("%s <-> %s", src_name, dst_name)
       
   352             edge = edges[(dst_name, remote['port'], src_name, local['port'])]
       
   353 
       
   354             if edge.get('headlabel') != taillabel :
       
   355                 log.warn("%s -> %s: local port mismatch: %s vs %s", src_name, dst_name, local['port'], edge.get('headlabel'))
       
   356             
       
   357             if edge.get('taillabel') != headlabel :
       
   358                 log.warn("%s -> %s: remote port mismatch: %s vs %s", src_name, dst_name, remote['port'], edge.get('taillabel'))
       
   359 
       
   360             if edge.get('fillcolor') != fillcolor :
       
   361                 log.warn("%s#%s -> %s#%s: remote untag mismatch: %s vs %s", src_name, local['port'], dst_name, remote['port'], fillcolor, edge.get('fillcolor'))
       
   362 
       
   363             if edge.get('color') != '"' + color + '"' :
       
   364                 log.warn("%s#%s -> %s#%s: remote tagged mismatch: %s vs %s", src_name, local['port'], dst_name, remote['port'], color, edge.get('color'))
       
   365 
       
   366             # mark as bidirectional
       
   367             edges[(src_name, local['port'], dst_name, remote['port'])] = edge
       
   368             
       
   369             edge.set('dir', 'both' if untag else 'none')
       
   370 
       
   371             # set second color for tail
       
   372             if untag :
       
   373                 edge.set('color', '"{headcolor}:{tailcolor}{tagcolors}"'.format(
       
   374                     headcolor   = edge.get('fillcolor'),
       
   375                     tailcolor   = fillcolor,
       
   376                     tagcolors   = ':' + ':'.join(colors) if colors else '',
       
   377                 ))
       
   378 
       
   379         else :
       
   380             edge = edges[(src_name, local['port'], dst_name, remote['port'])] = pydot.Edge(src, dst,
       
   381                     dir         = 'forward' if untag else 'none',
       
   382                     headlabel   = headlabel,
       
   383                     taillabel   = taillabel,
       
   384                     color       = '"{color}"'.format(color=color),
       
   385                     fillcolor   = fillcolor,
       
   386             )
       
   387 
       
   388             dot.add_edge(edge)
       
   389 
       
   390     if options.graph_dot :
       
   391         dot.write(options.graph_dot)
       
   392 
       
   393 def main (argv) :
       
   394     """
       
   395         SNMP polling.
       
   396     """
       
   397 
       
   398     parser = optparse.OptionParser(main.__doc__)
       
   399     parser.add_option_group(pvl.args.parser(parser))
       
   400     parser.add_option_group(pvl.hosts.optparser(parser))
       
   401     parser.add_option_group(pvl.snmp.snmp.options(parser))
       
   402 
       
   403     parser.add_option('--graph-dot',
       
   404             help="Output .dot graph data")
       
   405 
       
   406     parser.add_option('--no-vlans', action='store_const', dest='vlans', const=False,
       
   407             help="Do not color VLANs")
       
   408 
       
   409     # input
       
   410     options, args = parser.parse_args(argv[1:])
       
   411     pvl.args.apply(options)
       
   412 
       
   413     hosts = pvl.hosts.apply(options, args)
       
   414     
       
   415     # lookup host-port-vlan mappings
       
   416     if options.vlans is False :
       
   417         vlans = None
       
   418     else :
       
   419         vlans = dict(apply_hosts_vlan(options, hosts))
       
   420 
       
   421     # discover node/port graph
       
   422     items = apply_hosts_lldp(options, hosts)
       
   423 
       
   424     # discover edge nodes
       
   425     leafs = apply_hosts_bridge(options, hosts)
       
   426 
       
   427     # print
       
   428     if options.graph_dot :
       
   429         apply_graph(options, items, vlans)
       
   430     else :
       
   431         for host, local, remote, remote_host in items :
       
   432             if remote_host :
       
   433                 print "{host:30} {host.location:>30} {local[port]:>25} <-> {remote[port]:<25} {remote_host.location:>30} # {remote[chassis]} ({remote_host})".format(host=host, local=local, remote=remote, remote_host=remote_host)
       
   434             else :
       
   435                 print "{host:30} {host.location:>30} {local[port]:>25} --> {remote[port]:<25} {empty:30} # {remote[chassis]} ({remote[sys_name]})".format(host=host, local=local, remote=remote, empty='')
       
   436        
       
   437         for host, ports in vlans.iteritems() :
       
   438             for port, (untag, tagged) in ports.iteritems() :
       
   439                 print "{host:30} {host.location:>30} {port:25} {untag}{tagged}".format(host=host, port=port,
       
   440                         untag       = '({untag}) '.format(untag=untag) if untag else '',
       
   441                         tagged      = ' '.join('<{tag}>'.format(tag=tag) for tag in tagged),
       
   442                 )
       
   443 
       
   444         for host, ports in leafs :
       
   445             for (port, vlan), ethers in ports.iteritems() :
       
   446                 print "{host:30} {host.location:>30} {port:25} <-- {vlan} # {ethers}".format(
       
   447                         host        = host,
       
   448                         port        = port,
       
   449                         vlan        = '<{vlan}>'.format(vlan=vlan) if vlan else '',
       
   450                         ethers      = ' '.join(ethers),
       
   451                 )
       
   452 
       
   453 if __name__ == '__main__':
       
   454     pvl.args.main(main)