:pvl.hosts-lldp: split into pvl.hosts-snmp to gather data, and pvl.hosts-graph to process/graph it
--- /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)
--- 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)
--- /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)
+