terom@380: #!/usr/bin/env python terom@380: terom@380: """ terom@380: Requirements: terom@387: pydot terom@380: """ terom@380: terom@380: import pvl.args terom@380: import pvl.hosts terom@382: from pvl.invoke import merge terom@391: from pvl.snmp import snmp, lldp, vlan terom@380: terom@384: import logging; log = logging.getLogger('pvl.hosts-lldp') terom@384: import optparse terom@382: terom@391: def hosts_snmp (options, hosts) : terom@382: """ terom@391: Discover SNMP-supporting hosts. terom@382: terom@391: Yields Host, snmpdata terom@382: """ terom@382: terom@382: for host in hosts : terom@384: host_snmp = host.extensions.get('snmp') terom@382: terom@384: if not host_snmp : terom@384: log.debug("%s: skip non-snmp host", host) terom@382: continue terom@382: terom@384: elif host.down : terom@384: log.debug("%s: skip down host", host) terom@384: continue terom@384: terom@384: else : terom@384: log.debug("%s: %s", host, host_snmp) terom@384: terom@391: yield host, host_snmp terom@391: terom@391: def hosts_lldp (options, hosts) : terom@391: """ terom@391: Discover LLDP-supporting hosts. terom@391: terom@391: Yields Host, LLDPAgent terom@391: """ terom@391: terom@391: for host, host_snmp in hosts_snmp(options, hosts) : terom@384: agent = lldp.LLDPAgent.apply(options, host.fqdn(), community=host_snmp.get('community')) terom@382: terom@382: try : terom@384: local = agent.local terom@384: except snmp.SNMPError as ex : terom@382: log.warning("%s: %s", host, ex) terom@382: continue terom@382: terom@384: log.info("%s: %s", host, local) terom@384: terom@382: if local['sys_name'] != host.host : terom@382: log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name']) terom@382: terom@384: yield host, agent terom@382: terom@391: def hosts_vlan (options, hosts) : terom@391: """ terom@391: Discover VLAN-supporting hosts. terom@391: terom@391: Yields Host, VLANAgent terom@391: """ terom@391: terom@391: for host, host_snmp in hosts_snmp(options, hosts) : terom@391: agent = vlan.VLANAgent.apply(options, host.fqdn(), community=host_snmp.get('community')) terom@391: terom@391: try : terom@391: count = agent.count terom@391: except snmp.SNMPError as ex : terom@391: log.warning("%s: %s", host, ex) terom@391: continue terom@391: terom@391: log.info("%s: %s", host, count) terom@391: terom@391: yield host, agent terom@391: terom@382: def apply_hosts_lldp (options, hosts) : terom@382: """ terom@382: Query host LLDP info. terom@382: """ terom@384: terom@384: _hosts_lldp = list(hosts_lldp(options, hosts)) terom@384: hosts_by_chassis = { } terom@384: terom@384: # first pass to discover hosts terom@384: for host, agent in _hosts_lldp : terom@384: chassis = agent.local['chassis'] terom@384: log.info("%s: %s", host, chassis) terom@382: terom@384: hosts_by_chassis[chassis] = host terom@384: terom@384: # second pass to discver links terom@384: for host, agent in _hosts_lldp : terom@384: for port, remote in agent.remotes() : terom@384: port = agent.port(port) terom@384: terom@384: remote_chassis = remote['chassis'] terom@384: remote_host = hosts_by_chassis.get(remote_chassis) terom@382: terom@384: log.info("%s: %s: %s (%s)", host, port, remote, remote_host) terom@382: terom@384: yield host, merge(agent.local, port), remote, remote_host terom@380: terom@391: import collections terom@391: terom@391: def apply_hosts_vlan (options, hosts) : terom@391: """ terom@391: Query host VLAN ports. terom@391: """ terom@391: terom@391: _hosts_vlan = list(hosts_vlan(options, hosts)) terom@391: terom@391: for host, agent in _hosts_vlan : terom@391: # only one untagged vlan / port terom@391: vlan_untagged = { } terom@391: terom@391: # multiple taggd vlans / port terom@391: vlan_tagged = collections.defaultdict(set) terom@391: terom@391: for vlan, (tagged, untagged) in agent.vlans() : terom@391: log.info("%s: %s: %s + %s", host, vlan, tagged, untagged) terom@391: terom@391: for port in tagged : terom@391: vlan_tagged[port].add(vlan) terom@391: terom@391: for port in untagged : terom@391: if port in vlan_untagged : terom@391: log.warning("%s: duplicate untagged vlan %s for port %s on vlan %s", host, vlan, port, vlan_untagged[port]) terom@391: terom@391: vlan_untagged[port] = vlan terom@391: terom@391: # pack into {port: (untagged, [tagged]) } terom@391: yield host, dict( terom@391: ( terom@391: port, (vlan_untagged.get(port), tuple(vlan_tagged[port])) terom@391: ) for port in set(vlan_untagged) | set(vlan_tagged) terom@391: ) terom@391: terom@391: COLOR_VLANS = { terom@391: 1: 'grey', # pvl-lan terom@391: 2: 'blue', # pvl-lan2 terom@391: 3: 'red', # pvl-san terom@391: 4: 'yellow', # pvl-veturi terom@391: 7: 'orange', # pvl-ranssi terom@391: 8: 'green', # pvl-mgmt terom@391: 10: 'brown', # pvl-public terom@391: terom@391: 100: 'red', # test terom@391: 102: 'red', # ganeti terom@391: 103: 'red', # test-san terom@391: 104: 'red', # pvl-ganeti terom@391: terom@391: 192: 'green', # paivola-services terom@391: 255: 'purple', # pvl-sonera terom@391: } terom@391: terom@391: def apply_graph (options, items, vlans={}) : terom@387: import pydot terom@387: terom@387: dot = pydot.Dot(graph_name='lldp_hosts', graph_type='digraph', terom@388: # XXX: breaks multi-edges? terom@388: #splines = 'true', terom@387: terom@387: sep = '+25,25', terom@387: overlap = 'scalexy', terom@387: terom@387: # only applies to loops terom@391: nodesep = 0.5, terom@387: ) terom@387: dot.set_edge_defaults( terom@387: labeldistance = 3.0, terom@391: penwidth = 2.0, terom@387: ) terom@387: terom@387: nodes = { } terom@387: edges = { } terom@387: terom@387: for host, local, remote, remote_host in items : terom@387: # src terom@387: src_name = str(host) terom@387: src_label = '"{host.location}"'.format(host=host) terom@387: terom@387: if src_name in nodes : terom@387: src = nodes[src_name] terom@387: else : terom@387: src = nodes[src_name] = pydot.Node(src_name, label=src_label) terom@387: dot.add_node(src) terom@387: terom@387: # dst terom@387: if remote_host : terom@387: dst_name = str(remote_host) terom@387: dst_label = '"{host.location}"'.format(host=remote_host) terom@387: else : terom@387: dst_name = remote['chassis'].replace(':', '-') terom@387: terom@387: # XXX: pydot is not smart enough to quote this terom@387: if remote['sys_name'] : terom@387: dst_label = '"{remote[chassis]} ({remote[sys_name]})"'.format(remote=remote) terom@387: else : terom@387: dst_label = '"{remote[chassis]}"'.format(remote=remote) terom@387: terom@387: if dst_name in nodes : terom@387: dst = nodes[dst_name] terom@387: else : terom@387: dst = nodes[dst_name] = pydot.Node(dst_name, label=dst_label) terom@387: dot.add_node(dst) terom@387: terom@391: # edges terom@387: headlabel = '"{remote[port]}"'.format(remote=remote) terom@387: taillabel = '"{local[port]}"'.format(local=local) terom@391: fillcolor = None terom@391: color = None terom@387: terom@391: # vlans? terom@391: if host in vlans and local['port_id'] in vlans[host] : terom@391: untag, tagged = vlans[host][local['port_id']] terom@391: terom@391: log.debug("%s#%s: %s+%s", host, local['port_id'], untag, tagged) terom@391: terom@391: colors = [] terom@391: for tag in sorted(tagged) : terom@391: if tag in COLOR_VLANS : terom@391: colors.append(COLOR_VLANS[tag]) terom@391: else : terom@391: log.warn("%s#%s: unknown vlan %s", host, local['port_id'], tag) terom@391: colors.append('black') terom@391: terom@391: if untag and untag in COLOR_VLANS : terom@391: fillcolor = COLOR_VLANS[untag] terom@391: elif untag : terom@391: log.warn("%s#%s: no color for vlan %s", host, local['port_id'], untag) terom@391: terom@391: # first color overrides fillcolor for heads terom@391: if colors and fillcolor : terom@391: color = ':'.join([fillcolor] + colors) terom@391: elif colors : terom@391: color = ':'.join(colors) terom@391: elif fillcolor : terom@391: color = fillcolor terom@391: else : terom@391: color = 'black' terom@391: terom@391: if not fillcolor : terom@391: fillcolor = 'black' terom@391: terom@391: elif vlans : terom@391: # XXX: this happens when LLDP gives us the LACP ports but the VLANS are on the TRK port terom@391: log.warn("%s#%s: unknown port for vlans: %s", host, local['port_id'], vlans.get(host)) terom@391: terom@391: untag = tag = None terom@391: terom@391: # edge terom@388: if (src_name, local['port'], dst_name, remote['port']) in edges : terom@388: log.warning("%s:%s <- %s:%s: duplicate", src_name, local['port'], dst_name, remote['port']) terom@387: terom@388: elif (dst_name, remote['port'], src_name, local['port']) in edges : terom@387: log.info("%s <-> %s", src_name, dst_name) terom@388: edge = edges[(dst_name, remote['port'], src_name, local['port'])] terom@387: terom@387: if edge.get('headlabel') != taillabel : terom@387: log.warn("%s -> %s: local port mismatch: %s vs %s", src_name, dst_name, local['port'], edge.get('headlabel')) terom@387: terom@387: if edge.get('taillabel') != headlabel : terom@387: log.warn("%s -> %s: remote port mismatch: %s vs %s", src_name, dst_name, remote['port'], edge.get('taillabel')) terom@387: terom@391: if edge.get('fillcolor') != fillcolor : terom@391: log.warn("%s#%s -> %s#%s: remote untag mismatch: %s vs %s", src_name, local['port'], dst_name, remote['port'], fillcolor, edge.get('fillcolor')) terom@391: terom@391: if edge.get('color') != '"' + color + '"' : terom@391: log.warn("%s#%s -> %s#%s: remote tagged mismatch: %s vs %s", src_name, local['port'], dst_name, remote['port'], color, edge.get('color')) terom@391: terom@387: # mark as bidirectional terom@388: edges[(src_name, local['port'], dst_name, remote['port'])] = edge terom@387: terom@391: edge.set('dir', 'both' if untag else 'none') terom@391: terom@391: # set second color for tail terom@391: if untag : terom@391: edge.set('color', '"{headcolor}:{tailcolor}{tagcolors}"'.format( terom@391: headcolor = edge.get('fillcolor'), terom@391: tailcolor = fillcolor, terom@391: tagcolors = ':' + ':'.join(colors) if colors else '', terom@391: )) terom@387: terom@387: else : terom@388: edge = edges[(src_name, local['port'], dst_name, remote['port'])] = pydot.Edge(src, dst, terom@391: dir = 'forward' if untag else 'none', terom@387: headlabel = headlabel, terom@387: taillabel = taillabel, terom@391: color = '"{color}"'.format(color=color) if color else 'black', terom@391: fillcolor = fillcolor or 'black', terom@387: ) terom@387: terom@387: dot.add_edge(edge) terom@387: terom@387: if options.graph_dot : terom@387: dot.write(options.graph_dot) terom@387: terom@380: def main (argv) : terom@380: """ terom@380: SNMP polling. terom@380: """ terom@380: terom@380: parser = optparse.OptionParser(main.__doc__) terom@380: parser.add_option_group(pvl.args.parser(parser)) terom@382: parser.add_option_group(pvl.hosts.optparser(parser)) terom@384: parser.add_option_group(pvl.snmp.snmp.options(parser)) terom@380: terom@387: parser.add_option('--graph-dot', terom@387: help="Output .dot graph data") terom@380: terom@391: # input terom@380: options, args = parser.parse_args(argv[1:]) terom@380: pvl.args.apply(options) terom@380: terom@382: hosts = pvl.hosts.apply(options, args) terom@382: terom@391: # lookup host-port-vlan mappings terom@391: vlans = dict(apply_hosts_vlan(options, hosts)) terom@391: terom@391: # discover node/port graph terom@387: items = apply_hosts_lldp(options, hosts) terom@387: terom@387: # print terom@387: if options.graph_dot : terom@391: apply_graph(options, items, vlans) terom@387: else : terom@387: for host, local, remote, remote_host in items : terom@387: if remote_host : terom@387: 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) terom@387: else : terom@387: 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='') terom@391: terom@391: terom@380: terom@380: if __name__ == '__main__': terom@380: pvl.args.main(main)