# HG changeset patch # User Tero Marttila # Date 1395177241 -7200 # Node ID aadf76a05ec1f2ca62937a2ca1223e2d26a47bf5 # Parent de275bf6db7002c665ffe988e7bfb24f7095e0f0 :pvl.hosts-lldp: split into pvl.hosts-snmp to gather data, and pvl.hosts-graph to process/graph it diff -r de275bf6db70 -r aadf76a05ec1 bin/pvl.hosts-graph --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/pvl.hosts-graph Tue Mar 18 23:14:01 2014 +0200 @@ -0,0 +1,309 @@ +#!/usr/bin/env python + +""" + Requirements: + pydot +""" + +import pvl.args +import pvl.hosts + +import collections +import logging; log = logging.getLogger('pvl.hosts-graph') +import optparse + +COLOR_VLANS = { + 1: 'grey', # pvl-lan + 2: 'blue', # pvl-lan2 + 3: 'red', # pvl-san + 4: 'green', # pvl-veturi + 7: 'orange', # pvl-ranssi + 8: 'yellow', # pvl-mgmt + 10: 'brown', # pvl-public + 100: 'navyblue', # pvl-test + 103: 'red4', # pvl-test-san + 104: 'red2', # pvl-ganeti + 192: 'purple', # paivola-services + 255: 'magenta', # pvl-sonera +} + +class ParseError (Exception) : + def __init__ (self, file, line, msg) : + self.file = file + self.line = line + self.msg = msg + + def __str__ (self) : + return "{self.file}:{self.line}: {self.msg}".format(self=self) + +def _load_snmp_data (options, file) : + """ + Load a data dict generated by pvl.hosts-snmp from a file. + + Yields (host, attr, value) + """ + + host = None + attr = None + value = None + + for idx, line in enumerate(file, 1) : + indent = line.count('\t') + line = line.strip() + + if indent == 0 : + host = line + attr = None + value = None + + elif indent == 1 : + if attr and not value : + yield host, attr, None + + attr = tuple((int(a) if a.isdigit() else a) for a in line.split()) + value = None + + elif indent == 2 : + if not attr : + raise ParseError(file, line, "value outside of attr") + + value = line + + yield host, attr, value + +def load_snmp_data (options, file, hosts) : + """ + Load snmp data as dict, from given file path, or stdin. + """ + + if file : + file = open(file) + else : + file = sys.stdin + + root = { } + + for host_name, attr, value in _load_snmp_data(options, file) : + host = hosts[host_name] + + log.info("[%s] %s%s", host, ' '.join(str(a) for a in attr), (': ' + str(value)) if value else '') + + item = root.setdefault(host, { }) + + if value : + end = None + else : + end = attr[-1] + attr = attr[:-1] + + for a in attr[:-1] : + item = item.setdefault(a, {}) + + a = attr[-1] + + if value is None : + item[a] = end + else : + item.setdefault(a, set()).add(value) + + return root + +def apply_graph (options, items, vlans={}) : + import pydot + + dot = pydot.Dot(graph_name='lldp_hosts', graph_type='digraph', + # XXX: breaks multi-edges? + #splines = 'true', + + sep = '+25,25', + overlap = 'scalexy', + + # only applies to loops + nodesep = 0.5, + ) + dot.set_edge_defaults( + labeldistance = 3.0, + penwidth = 2.0, + ) + + nodes = { } + edges = { } + vlan_colors = { } # { vlan: color } + + for host, local, remote, remote_host in items : + # src + src_name = str(host) + src_label = '"{host.location}"'.format(host=host) + + if src_name in nodes : + src = nodes[src_name] + else : + src = nodes[src_name] = pydot.Node(src_name, + label = src_label, + fontsize = 18, + ) + dot.add_node(src) + + # dst + if remote_host : + dst_name = str(remote_host) + dst_label = '"{host.location}"'.format(host=remote_host) + else : + dst_name = remote['chassis'].replace(':', '-') + + # XXX: pydot is not smart enough to quote this + if remote['sys_name'] : + 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] + else : + dst = nodes[dst_name] = pydot.Node(dst_name, + label = dst_label, + fontsize = 18, + ) + dot.add_node(dst) + + # edges + headlabel = '"{remote[port]}"'.format(remote=remote) + taillabel = '"{local[port]}"'.format(local=local) + fillcolor = 'black' + color = 'black' + untag = tagged = None + + # vlans? + if vlans and 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]) + elif tag in vlan_colors : + colors.append(vlan_colors[tag]) + else : + color = '/paired12/{count}'.format(count=1+len(vlan_colors)) + + log.info("%s#%s: chosing new vlan %s color %s", host, local['port_id'], tag, color) + + vlan_colors[tag] = color + colors.append(color) + + if not untag : + pass + elif untag in COLOR_VLANS : + fillcolor = COLOR_VLANS[untag] + elif untag in vlan_colors : + fillcolor = vlan_colors[untag] + else : + color = '/paired12/{count}'.format(count=1+len(vlan_colors)) + + log.warn("%s#%s: chosing new vlan %s color %s", host, local['port_id'], untag, color) + + fillcolor = vlan_colors[tag] = color + + # first color overrides fillcolor for heads + if colors and untag : + color = ':'.join([fillcolor] + colors) + elif colors : + color = ':'.join(colors) + elif fillcolor : + color = fillcolor + + 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)) + + # 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']) + + elif (dst_name, remote['port'], src_name, local['port']) in edges : + log.info("%s <-> %s", src_name, dst_name) + edge = edges[(dst_name, remote['port'], src_name, local['port'])] + + if edge.get('headlabel') != taillabel : + log.warn("%s -> %s: local port mismatch: %s vs %s", src_name, dst_name, local['port'], edge.get('headlabel')) + + 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' 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 = 'forward' if untag else 'none', + headlabel = headlabel, + taillabel = taillabel, + color = '"{color}"'.format(color=color), + fillcolor = fillcolor, + ) + + dot.add_edge(edge) + + if options.graph_dot : + dot.write(options.graph_dot) + +def main (argv) : + """ + Graph network + """ + + parser = optparse.OptionParser(main.__doc__) + parser.add_option_group(pvl.args.parser(parser)) + parser.add_option_group(pvl.hosts.optparser(parser)) + + parser.add_option('--snmp-data', metavar='FILE', default=None, + help="Load snmp data from FILE") + + parser.add_option('--graph-dot', metavar='FILE', + help="Output .dot graph data to file") + + parser.add_option('--graph-vlans', action='store_true', dest='vlans', + help="Graph all VLANs") + + parser.add_option('--no-vlans', action='store_false', dest='vlans', + help="Do not color VLANs") + + # input + options, args = parser.parse_args(argv[1:]) + pvl.args.apply(options) + + # load hosts for correlation + hosts = dict((str(host), host) for host in pvl.hosts.apply(options, args)) + + # load raw snmp data + data = load_snmp_data(options, options.snmp_data, hosts) + + import pprint; pprint.pprint(data) + + # TODO: process data into graph + + # TODO: generate graph + #apply_graph(options, items, vlans if options.vlans else None) + + return 0 + +if __name__ == '__main__': + pvl.args.main(main) diff -r de275bf6db70 -r aadf76a05ec1 bin/pvl.hosts-lldp --- a/bin/pvl.hosts-lldp Tue Mar 18 21:25:35 2014 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,454 +0,0 @@ -#!/usr/bin/env python - -""" - Requirements: - pydot -""" - -import pvl.args -import pvl.hosts -from pvl.invoke import merge -from pvl.snmp import snmp, lldp, vlan, bridge - -import logging; log = logging.getLogger('pvl.hosts-lldp') -import optparse - -def hosts_snmp (options, hosts) : - """ - Discover SNMP-supporting hosts. - - 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 - - elif host.down : - log.debug("%s: skip down host", host) - continue - - 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 : - local = agent.local - except snmp.SNMPError as ex : - log.warning("%s: %s", host, ex) - continue - - if not local : - log.info("%s: no lldp support", host) - continue - - log.info("%s: %s", host, local) - - if local['sys_name'] != host.host : - log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name']) - - 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 hosts_bridge (options, hosts) : - """ - Discover Bridge-supporting hosts. - - Yields Host, BridgeAgent - """ - - for host, host_snmp in hosts_snmp(options, hosts) : - agent = bridge.BridgeAgent.apply(options, host.fqdn(), community=host_snmp.get('community')) - - try : - agent.ping() - except snmp.SNMPError as ex : - log.warning("%s: %s", host, ex) - continue - - log.info("%s", host) - - yield host, agent - - -def apply_hosts_lldp (options, hosts) : - """ - Query host LLDP info. - """ - - _hosts_lldp = list(hosts_lldp(options, hosts)) - hosts_by_chassis = { } - - # first pass to discover hosts - for host, agent in _hosts_lldp : - chassis = agent.local['chassis'] - log.info("%s: %s", host, chassis) - - hosts_by_chassis[chassis] = host - - # second pass to discver links - for host, agent in _hosts_lldp : - try : - remotes = list(agent.remotes()) - except snmp.SNMPError as ex : - log.warn("%s: broken lldp remotes: %s", host, ex) - continue - - for port, remote in remotes : - port = agent.port(port) - - remote_chassis = remote['chassis'] - remote_host = hosts_by_chassis.get(remote_chassis) - - log.info("%s: %s: %s (%s)", host, port, remote, remote_host) - - yield host, merge(agent.local, port), remote, remote_host - -import collections - -def apply_hosts_vlan (options, hosts) : - """ - Query host VLAN ports. - - Yields host, { port: (untagged, [tagged]) } - """ - - _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.vlan_ports() : - 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) - ) - -def apply_hosts_bridge (options, hosts) : - """ - Query host bridge tables. - - Yields host, { port: (macs) } - """ - - for host, agent in hosts_bridge(options, hosts) : - ports = collections.defaultdict(list) - - try : - vlan_fdb_ports = list(agent.vlan_fdb_ports()) - except snmp.SNMPError as ex : - log.warn("%s: broken dot1q fdb: %s", host, ex) - continue - - if vlan_fdb_ports : - log.info("%s: have dot1q ports", host) - - for ether, port, vlan in agent.vlan_fdb_ports() : - if not port : - # XXX: unknown? - continue - - ports[(port, vlan)].append(ether) - else : - try : - fdb_ports = list(agent.fdb_ports()) - except snmp.SNMPError as ex : - log.warn("%s: broken dot1q fdb: %s", host, ex) - continue - - # fallback to dot1d fdb - log.info("%s: fallback to dot1d", host) - - for ether, port in agent.fdb_ports() : - if not port : - # XXX: unknown? - continue - - ports[(port, None)].append(ether) - - yield host, ports - -COLOR_VLANS = { - 1: 'grey', # pvl-lan - 2: 'blue', # pvl-lan2 - 3: 'red', # pvl-san - 4: 'green', # pvl-veturi - 7: 'orange', # pvl-ranssi - 8: 'yellow', # pvl-mgmt - 10: 'brown', # pvl-public - 100: 'navyblue', # pvl-test - 103: 'red4', # pvl-test-san - 104: 'red2', # pvl-ganeti - 192: 'purple', # paivola-services - 255: 'magenta', # pvl-sonera -} - -def apply_graph (options, items, vlans={}) : - import pydot - - dot = pydot.Dot(graph_name='lldp_hosts', graph_type='digraph', - # XXX: breaks multi-edges? - #splines = 'true', - - sep = '+25,25', - overlap = 'scalexy', - - # only applies to loops - nodesep = 0.5, - ) - dot.set_edge_defaults( - labeldistance = 3.0, - penwidth = 2.0, - ) - - nodes = { } - edges = { } - vlan_colors = { } # { vlan: color } - - for host, local, remote, remote_host in items : - # src - src_name = str(host) - src_label = '"{host.location}"'.format(host=host) - - if src_name in nodes : - src = nodes[src_name] - else : - src = nodes[src_name] = pydot.Node(src_name, - label = src_label, - fontsize = 18, - ) - dot.add_node(src) - - # dst - if remote_host : - dst_name = str(remote_host) - dst_label = '"{host.location}"'.format(host=remote_host) - else : - dst_name = remote['chassis'].replace(':', '-') - - # XXX: pydot is not smart enough to quote this - if remote['sys_name'] : - 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] - else : - dst = nodes[dst_name] = pydot.Node(dst_name, - label = dst_label, - fontsize = 18, - ) - dot.add_node(dst) - - # edges - headlabel = '"{remote[port]}"'.format(remote=remote) - taillabel = '"{local[port]}"'.format(local=local) - fillcolor = 'black' - color = 'black' - untag = tagged = None - - # vlans? - if vlans and 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]) - elif tag in vlan_colors : - colors.append(vlan_colors[tag]) - else : - color = '/paired12/{count}'.format(count=1+len(vlan_colors)) - - log.info("%s#%s: chosing new vlan %s color %s", host, local['port_id'], tag, color) - - vlan_colors[tag] = color - colors.append(color) - - if not untag : - pass - elif untag in COLOR_VLANS : - fillcolor = COLOR_VLANS[untag] - elif untag in vlan_colors : - fillcolor = vlan_colors[untag] - else : - color = '/paired12/{count}'.format(count=1+len(vlan_colors)) - - log.warn("%s#%s: chosing new vlan %s color %s", host, local['port_id'], untag, color) - - fillcolor = vlan_colors[tag] = color - - # first color overrides fillcolor for heads - if colors and untag : - color = ':'.join([fillcolor] + colors) - elif colors : - color = ':'.join(colors) - elif fillcolor : - color = fillcolor - - 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)) - - # 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']) - - elif (dst_name, remote['port'], src_name, local['port']) in edges : - log.info("%s <-> %s", src_name, dst_name) - edge = edges[(dst_name, remote['port'], src_name, local['port'])] - - if edge.get('headlabel') != taillabel : - log.warn("%s -> %s: local port mismatch: %s vs %s", src_name, dst_name, local['port'], edge.get('headlabel')) - - 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' 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 = 'forward' if untag else 'none', - headlabel = headlabel, - taillabel = taillabel, - color = '"{color}"'.format(color=color), - fillcolor = fillcolor, - ) - - dot.add_edge(edge) - - if options.graph_dot : - dot.write(options.graph_dot) - -def main (argv) : - """ - SNMP polling. - """ - - parser = optparse.OptionParser(main.__doc__) - parser.add_option_group(pvl.args.parser(parser)) - parser.add_option_group(pvl.hosts.optparser(parser)) - parser.add_option_group(pvl.snmp.snmp.options(parser)) - - parser.add_option('--graph-dot', - help="Output .dot graph data") - - parser.add_option('--no-vlans', action='store_const', dest='vlans', const=False, - help="Do not color VLANs") - - # input - options, args = parser.parse_args(argv[1:]) - pvl.args.apply(options) - - hosts = pvl.hosts.apply(options, args) - - # lookup host-port-vlan mappings - if options.vlans is False : - vlans = None - else : - vlans = dict(apply_hosts_vlan(options, hosts)) - - # discover node/port graph - items = apply_hosts_lldp(options, hosts) - - # discover edge nodes - leafs = apply_hosts_bridge(options, hosts) - - # print - if options.graph_dot : - 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='') - - for host, ports in vlans.iteritems() : - for port, (untag, tagged) in ports.iteritems() : - print "{host:30} {host.location:>30} {port:25} {untag}{tagged}".format(host=host, port=port, - untag = '({untag}) '.format(untag=untag) if untag else '', - tagged = ' '.join('<{tag}>'.format(tag=tag) for tag in tagged), - ) - - for host, ports in leafs : - for (port, vlan), ethers in ports.iteritems() : - print "{host:30} {host.location:>30} {port:25} <-- {vlan} # {ethers}".format( - host = host, - port = port, - vlan = '<{vlan}>'.format(vlan=vlan) if vlan else '', - ethers = ' '.join(ethers), - ) - -if __name__ == '__main__': - pvl.args.main(main) diff -r de275bf6db70 -r aadf76a05ec1 bin/pvl.hosts-snmp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/pvl.hosts-snmp Tue Mar 18 23:14:01 2014 +0200 @@ -0,0 +1,277 @@ +#!/usr/bin/env python + +import pvl.args +import pvl.hosts +from pvl.snmp import snmp, lldp, vlan, bridge + +import collections +import logging; log = logging.getLogger('pvl.hosts-lldp') +import optparse + +def hosts_snmp (options, hosts) : + """ + Discover SNMP-supporting hosts. + + 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 + + elif host.down : + log.debug("%s: skip down host", host) + continue + + 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 : + local = agent.local + except snmp.SNMPError as ex : + log.warning("%s: %s", host, ex) + continue + + if not local : + log.info("%s: no lldp support", host) + continue + + log.info("%s: %s", host, local) + + if local['sys_name'] != host.host : + log.warning("%s: SNMP sys_name mismatch: %s", host, local['sys_name']) + + 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 hosts_bridge (options, hosts) : + """ + Discover Bridge-supporting hosts. + + Yields Host, BridgeAgent + """ + + for host, host_snmp in hosts_snmp(options, hosts) : + agent = bridge.BridgeAgent.apply(options, host.fqdn(), community=host_snmp.get('community')) + + try : + agent.ping() + except snmp.SNMPError as ex : + log.warning("%s: %s", host, ex) + continue + + log.info("%s", host) + + yield host, agent + + +def apply_hosts_lldp (options, hosts) : + """ + Query host LLDP info. + """ + + # second pass to discver links + for host, agent in hosts_lldp(options, hosts) : + try : + remotes = list(agent.remotes()) + except snmp.SNMPError as ex : + log.warn("%s: broken lldp remotes: %s", host, ex) + continue + + for port, remote in remotes : + port = agent.port(port) + + log.info("%s: %s: %s", host, port, remote) + + yield host, agent.local, port, remote + +def apply_hosts_vlan (options, hosts) : + """ + Query host VLAN ports. + + Yields host, { port: (untagged, [tagged]) } + """ + + _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.vlan_ports() : + 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 + + for port in set(vlan_untagged) | set(vlan_tagged) : + yield host, port, vlan_untagged.get(port), tuple(vlan_tagged[port]) + +def apply_hosts_bridge (options, hosts) : + """ + Query host bridge tables. + + Yields host, { port: (macs) } + """ + + for host, agent in hosts_bridge(options, hosts) : + ports = collections.defaultdict(list) + + try : + vlan_fdb_ports = list(agent.vlan_fdb_ports()) + except snmp.SNMPError as ex : + log.warn("%s: broken dot1q fdb: %s", host, ex) + continue + + if vlan_fdb_ports : + log.info("%s: have dot1q ports", host) + + for ether, port, vlan in agent.vlan_fdb_ports() : + if not port : + # XXX: unknown? + continue + + ports[(port, vlan)].append(ether) + else : + try : + fdb_ports = list(agent.fdb_ports()) + except snmp.SNMPError as ex : + log.warn("%s: broken dot1q fdb: %s", host, ex) + continue + + # fallback to dot1d fdb + log.info("%s: fallback to dot1d", host) + + for ether, port in agent.fdb_ports() : + if not port : + # XXX: unknown? + continue + + ports[(port, None)].append(ether) + + for (port, vlan), ethers in ports.iteritems() : + yield host, vlan, port, ethers + +def apply_hosts (options, hosts) : + """ + Gather data on given hosts... + + (host, key, value) + """ + + if options.scan or options.scan_lldp : + # discover node/port graph + for host, local, port, remote in apply_hosts_lldp(options, hosts) : + yield host, ('lldp', ), set((local['chassis'], )) + + yield host, ('port', port['port'], 'lldp', remote['chassis'], 'port', remote['port']), None + + if options.scan or options.scan_vlan : + # discover vlan ports + for host, port, untag, tagged in apply_hosts_vlan(options, hosts) : + if untag : + yield host, ('port', port, 'untagged', untag), None + + if tagged : + yield host, ('port', port, 'tagged'), set(tagged) + + if options.scan or options.scan_bridge : + # discover edge nodes + for host, vlan, port, ethers in apply_hosts_bridge(options, hosts) : + if vlan : + yield host, ('port', port, 'bridge', 'vlan', vlan), set(ethers) + else : + yield host, ('port', port, 'bridge'), set(ethers) + +def main (argv) : + """ + SNMP polling. + """ + + parser = optparse.OptionParser(main.__doc__) + parser.add_option_group(pvl.args.parser(parser)) + parser.add_option_group(pvl.hosts.optparser(parser)) + parser.add_option_group(pvl.snmp.snmp.options(parser)) + + parser.add_option('--scan', action='store_true') + parser.add_option('--scan-lldp', action='store_true') + parser.add_option('--scan-vlan', action='store_true') + parser.add_option('--scan-bridge', action='store_true') + + # input + options, args = parser.parse_args(argv[1:]) + pvl.args.apply(options) + + # gather SNMP data from hosts + hosts = pvl.hosts.apply(options, args) + + data = collections.defaultdict(dict) + + for host, attr, values in apply_hosts(options, hosts) : + log.info("[%s] %s%s", host, ' '.join(str(a) for a in attr), (': ' + ' '.join(str(value) for value in values)) if values else '') + + if values is None : + data[host][attr] = None + else : + # merge + data[host].setdefault(attr, set()).update(values) + + # output + for host, attrs in sorted(data.items()) : + print "{host}".format(host=host) + + for attr, value in sorted(attrs.items()) : + print "\t{attr}".format(attr=' '.join(str(a) for a in attr)) + + if value : + for v in sorted(value) : + print "\t\t{value}".format(value=v) + + return 0 + +if __name__ == '__main__': + pvl.args.main(main) +