# HG changeset patch # User Tero Marttila # Date 1395093311 -7200 # Node ID e2c367b1556f132fb84b9d5ee9ada4a2a9a68880 # Parent e72f78b8bce496c20838627ed357b506e752fb71 pvl.hosts-lldp: add VLAN support diff -r e72f78b8bce4 -r e2c367b1556f 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)