pvl.hosts-lldp: add VLAN support
authorTero Marttila <terom@paivola.fi>
Mon, 17 Mar 2014 23:55:11 +0200
changeset 391 e2c367b1556f
parent 390 e72f78b8bce4
child 392 e7a55db74072
pvl.hosts-lldp: add VLAN support
bin/pvl.hosts-lldp
--- a/bin/pvl.hosts-lldp	Mon Mar 17 23:54:51 2014 +0200
+++ b/bin/pvl.hosts-lldp	Mon Mar 17 23:55:11 2014 +0200
@@ -8,22 +8,21 @@
 import pvl.args
 import pvl.hosts
 from pvl.invoke import merge
-from pvl.snmp import snmp, lldp
+from pvl.snmp import snmp, lldp, vlan
 
 import logging; log = logging.getLogger('pvl.hosts-lldp')
 import optparse
 
-def hosts_lldp (options, hosts) :
+def hosts_snmp (options, hosts) :
     """
-        Discover LLDP-supporting hosts.
+        Discover SNMP-supporting hosts.
 
-        Yields Host, LLDPAgent
+        Yields Host, snmpdata
     """
 
     for host in hosts :
         host_snmp = host.extensions.get('snmp')
 
-
         if not host_snmp :
             log.debug("%s: skip non-snmp host", host)
             continue
@@ -35,6 +34,16 @@
         else :
             log.debug("%s: %s", host, host_snmp)
 
+        yield host, host_snmp
+
+def hosts_lldp (options, hosts) :
+    """
+        Discover LLDP-supporting hosts.
+
+        Yields Host, LLDPAgent
+    """
+
+    for host, host_snmp in hosts_snmp(options, hosts) :
         agent = lldp.LLDPAgent.apply(options, host.fqdn(), community=host_snmp.get('community'))
 
         try :
@@ -50,6 +59,26 @@
 
         yield host, agent
 
+def hosts_vlan (options, hosts) :
+    """
+        Discover VLAN-supporting hosts.
+
+        Yields Host, VLANAgent
+    """
+
+    for host, host_snmp in hosts_snmp(options, hosts) :
+        agent = vlan.VLANAgent.apply(options, host.fqdn(), community=host_snmp.get('community'))
+
+        try :
+            count = agent.count
+        except snmp.SNMPError as ex :
+            log.warning("%s: %s", host, ex)
+            continue
+
+        log.info("%s: %s", host, count)
+
+        yield host, agent
+
 def apply_hosts_lldp (options, hosts) :
     """
         Query host LLDP info.
@@ -77,7 +106,60 @@
 
             yield host, merge(agent.local, port), remote, remote_host
 
-def apply_graph (options, items) :
+import collections
+
+def apply_hosts_vlan (options, hosts) :
+    """
+        Query host VLAN ports.
+    """
+
+    _hosts_vlan = list(hosts_vlan(options, hosts))
+
+    for host, agent in _hosts_vlan :
+        # only one untagged vlan / port
+        vlan_untagged = { }
+            
+        # multiple taggd vlans / port
+        vlan_tagged = collections.defaultdict(set)
+
+        for vlan, (tagged, untagged) in agent.vlans() :
+            log.info("%s: %s: %s + %s", host, vlan, tagged, untagged)
+            
+            for port in tagged :
+                vlan_tagged[port].add(vlan)
+            
+            for port in untagged :
+                if port in vlan_untagged :
+                    log.warning("%s: duplicate untagged vlan %s for port %s on vlan %s", host, vlan, port, vlan_untagged[port])
+
+                vlan_untagged[port] = vlan
+            
+        # pack into {port: (untagged, [tagged]) }
+        yield host, dict(
+            (
+                    port, (vlan_untagged.get(port), tuple(vlan_tagged[port]))
+            ) for port in set(vlan_untagged) | set(vlan_tagged)
+        )
+
+COLOR_VLANS = {
+    1:      'grey',         # pvl-lan
+    2:      'blue',         # pvl-lan2
+    3:      'red',          # pvl-san
+    4:      'yellow',       # pvl-veturi
+    7:      'orange',       # pvl-ranssi
+    8:      'green',        # pvl-mgmt
+    10:     'brown',        # pvl-public
+
+    100:    'red',          # test
+    102:    'red',          # ganeti
+    103:    'red',          # test-san
+    104:    'red',          # pvl-ganeti
+
+    192:    'green',        # paivola-services
+    255:    'purple',       # pvl-sonera
+}
+
+def apply_graph (options, items, vlans={}) :
     import pydot
 
     dot = pydot.Dot(graph_name='lldp_hosts', graph_type='digraph',
@@ -88,10 +170,11 @@
             overlap         = 'scalexy',
 
             # only applies to loops
-            #nodesep     = 0.5,
+            nodesep     = 0.5,
     )
     dot.set_edge_defaults(
             labeldistance   = 3.0,
+            penwidth        = 2.0,
     )
 
     nodes = { }
@@ -120,7 +203,6 @@
                 dst_label = '"{remote[chassis]} ({remote[sys_name]})"'.format(remote=remote)
             else :
                 dst_label = '"{remote[chassis]}"'.format(remote=remote)
-
         
         if dst_name in nodes :
             dst = nodes[dst_name]
@@ -128,10 +210,51 @@
             dst = nodes[dst_name] = pydot.Node(dst_name, label=dst_label)
             dot.add_node(dst)
 
-        # edge
+        # edges
         headlabel = '"{remote[port]}"'.format(remote=remote)
         taillabel = '"{local[port]}"'.format(local=local)
+        fillcolor = None
+        color = None
 
+        # vlans?
+        if host in vlans and local['port_id'] in vlans[host] :
+            untag, tagged = vlans[host][local['port_id']]
+            
+            log.debug("%s#%s: %s+%s", host, local['port_id'], untag, tagged)
+            
+            colors = []
+            for tag in sorted(tagged) :
+                if tag in COLOR_VLANS :
+                    colors.append(COLOR_VLANS[tag])
+                else :
+                    log.warn("%s#%s: unknown vlan %s", host, local['port_id'], tag)
+                    colors.append('black')
+
+            if untag and untag in COLOR_VLANS :
+                fillcolor = COLOR_VLANS[untag]
+            elif untag :
+                log.warn("%s#%s: no color for vlan %s", host, local['port_id'], untag)
+            
+            # first color overrides fillcolor for heads
+            if colors and fillcolor :
+                color = ':'.join([fillcolor] + colors)
+            elif colors :
+                color = ':'.join(colors)
+            elif fillcolor :
+                color = fillcolor
+            else :
+                color = 'black'
+
+            if not fillcolor :
+                fillcolor = 'black'
+
+        elif vlans :
+            # XXX: this happens when LLDP gives us the LACP ports but the VLANS are on the TRK port
+            log.warn("%s#%s: unknown port for vlans: %s", host, local['port_id'], vlans.get(host))
+
+            untag = tag = None
+
+        # edge
         if (src_name, local['port'], dst_name, remote['port']) in edges :
             log.warning("%s:%s <- %s:%s: duplicate", src_name, local['port'], dst_name, remote['port'])
         
@@ -145,16 +268,32 @@
             if edge.get('taillabel') != headlabel :
                 log.warn("%s -> %s: remote port mismatch: %s vs %s", src_name, dst_name, remote['port'], edge.get('taillabel'))
 
+            if edge.get('fillcolor') != fillcolor :
+                log.warn("%s#%s -> %s#%s: remote untag mismatch: %s vs %s", src_name, local['port'], dst_name, remote['port'], fillcolor, edge.get('fillcolor'))
+
+            if edge.get('color') != '"' + color + '"' :
+                log.warn("%s#%s -> %s#%s: remote tagged mismatch: %s vs %s", src_name, local['port'], dst_name, remote['port'], color, edge.get('color'))
+
             # mark as bidirectional
             edges[(src_name, local['port'], dst_name, remote['port'])] = edge
             
-            edge.set('dir', 'both')
+            edge.set('dir', 'both' if untag else 'none')
+
+            # set second color for tail
+            if untag :
+                edge.set('color', '"{headcolor}:{tailcolor}{tagcolors}"'.format(
+                    headcolor   = edge.get('fillcolor'),
+                    tailcolor   = fillcolor,
+                    tagcolors   = ':' + ':'.join(colors) if colors else '',
+                ))
 
         else :
             edge = edges[(src_name, local['port'], dst_name, remote['port'])] = pydot.Edge(src, dst,
-                    dir         = 'back',
+                    dir         = 'forward' if untag else 'none',
                     headlabel   = headlabel,
                     taillabel   = taillabel,
+                    color       = '"{color}"'.format(color=color) if color else 'black',
+                    fillcolor   = fillcolor or 'black',
             )
 
             dot.add_edge(edge)
@@ -175,24 +314,29 @@
     parser.add_option('--graph-dot',
             help="Output .dot graph data")
 
+    # input
     options, args = parser.parse_args(argv[1:])
     pvl.args.apply(options)
 
-    # input
     hosts = pvl.hosts.apply(options, args)
     
-    # apply
+    # lookup host-port-vlan mappings
+    vlans = dict(apply_hosts_vlan(options, hosts))
+
+    # discover node/port graph
     items = apply_hosts_lldp(options, hosts)
 
     # print
     if options.graph_dot :
-        apply_graph(options, items)
+        apply_graph(options, items, vlans)
     else :
         for host, local, remote, remote_host in items :
             if remote_host :
                 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)
             else :
                 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='')
+        
+
 
 if __name__ == '__main__':
     pvl.args.main(main)