--- 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)