134 else : |
134 else : |
135 item[a] = value |
135 item[a] = value |
136 |
136 |
137 return root |
137 return root |
138 |
138 |
139 def search_vlans (snmp) : |
139 def host_vlans (host, host_vlans) : |
140 for host in snmp : |
140 """ |
141 ports = set() |
141 {vlan: { tagged/untagged: [port] } } -> (port, (untag, [tag])). |
142 vlans_untagged = { } |
142 """ |
143 vlans_tagged = collections.defaultdict(set) |
143 |
144 |
144 ports = set() |
145 if 'vlan' not in snmp[host] : |
145 vlans_untagged = { } |
146 continue |
146 vlans_tagged = collections.defaultdict(set) |
147 |
147 |
148 for vlan in snmp[host]['vlan'] : |
148 for vlan, vlan_attrs in host_vlans.iteritems() : |
149 for port in snmp[host]['vlan'][vlan].get('tagged', ()) : |
149 for port in vlan_attrs.get('tagged', ()) : |
150 ports.add(port) |
150 ports.add(port) |
151 vlans_tagged[port].add(vlan) |
151 vlans_tagged[port].add(vlan) |
152 |
152 |
153 for port in snmp[host]['vlan'][vlan].get('untagged', ()) : |
153 for port in vlan_attrs.get('untagged', ()) : |
154 ports.add(port) |
154 ports.add(port) |
155 vlans_untagged[port] = vlan |
155 vlans_untagged[port] = vlan |
156 |
156 |
157 out = { } |
157 for port in ports : |
158 for port in ports : |
158 untag = vlans_untagged.get(port) |
159 untag = vlans_untagged.get(port) |
159 tagged = vlans_tagged.get(port, ()) |
160 tagged = vlans_tagged.get(port, ()) |
160 |
161 |
161 log.debug("%s: %s: untag=%s tag=%s", host, port, untag, tagged) |
162 log.info("%s: %s: untag=%s tag=%s", host, port, untag, tagged) |
162 |
163 |
163 yield port, (untag, tagged) |
164 out[port] = (untag, tagged) |
164 |
165 |
165 def build_graph (snmp, hosts) : |
166 yield host, out |
166 """ |
167 |
167 Combine given snmp data and { host: Host } into |
168 def search_lldp (snmp) : |
168 { node: label } |
169 lldp_hosts = {} |
169 { (remote, remote_port, local_port, local): (local_untag, tagged, remote_untag) } |
170 |
170 """ |
171 # first pass to resolve chassis ID's |
171 |
|
172 nodes = { } |
|
173 links = { } |
|
174 |
|
175 # first scan: lldp hosts |
172 for host, host_attrs in snmp.iteritems() : |
176 for host, host_attrs in snmp.iteritems() : |
173 lldp = host_attrs.get('lldp') |
177 lldp = host_attrs.get('lldp') |
174 |
178 |
|
179 if lldp : |
|
180 lldp_local = lldp['local'] |
|
181 |
|
182 nodes[lldp_local['chassis']] = host.location |
|
183 |
|
184 # second scan: lldp remotes |
|
185 for host, host_attrs in snmp.iteritems() : |
|
186 lldp = host_attrs.get('lldp') |
|
187 |
175 if not lldp : |
188 if not lldp : |
176 continue |
189 continue |
177 |
190 |
178 lldp_local = lldp.get('local') |
191 local = lldp['local']['chassis'] |
179 |
192 |
180 if not lldp_local : |
193 if 'vlan' in host_attrs : |
181 continue |
194 vlans = dict(host_vlans(host, host_attrs['vlan'])) |
182 |
195 else : |
183 lldp_hosts[lldp_local['chassis']] = host |
196 vlans = None |
184 |
197 |
185 # second pass to resolve remote |
198 for port, port_attrs in lldp.get('port', { }).iteritems() : |
186 for host, host_attrs in snmp.iteritems() : |
199 local_port = port_attrs['local']['port'] |
187 lldp = host_attrs.get('lldp') |
200 |
188 |
201 for remote, remote_attrs in port_attrs['remote'].iteritems() : |
189 if not lldp : |
202 if remote not in nodes : |
190 continue |
203 nodes[remote] = remote_attrs['sys_name'] |
191 |
204 |
192 local_attrs = lldp.get('local') |
205 remote_port = remote_attrs['port'] |
193 |
206 |
194 for port, port_attrs in snmp[host]['lldp']['port'].iteritems() : |
207 # local vlans |
195 port_local_attrs = port_attrs['local'] |
208 if vlans : |
196 |
209 port_vlans = vlans.get(port) |
197 for chassis, remote_attrs in port_attrs['remote'].iteritems() : |
210 else : |
198 remote_host = lldp_hosts.get(chassis) |
211 port_vlans = None |
199 |
212 |
200 local = merge(local_attrs, port_local_attrs, port_id=port) |
213 if port_vlans : |
201 remote = merge(remote_attrs, chassis=chassis) |
214 local_untag, local_tagged = port_vlans |
202 |
215 |
203 log.info("%s @ %s %s : %s %s @ %s", host, host.location, local, remote, remote_host.location if remote_host else '', remote_host) |
216 # bidirectional mappings |
204 |
217 forward = (local, local_port, remote_port, remote) |
205 yield host, local, remote, remote_host |
218 reverse = (remote, remote_port, local_port, local) |
206 |
219 |
207 def apply_graph (options, items, vlans={}) : |
220 if reverse not in links : |
208 import pydot |
221 links[forward] = (local_untag, local_tagged, None) |
209 |
222 else : |
210 dot = pydot.Dot(graph_name='lldp_hosts', graph_type='digraph', |
223 remote_untag, remote_tagged, _ = links[reverse] |
|
224 |
|
225 # merge |
|
226 if remote_untag != local_untag : |
|
227 log.warning("%s:%s untag %s <=> %s untag %s:%s", |
|
228 nodes[local], local_port, local_untag, |
|
229 remote_untag, nodes[remote], remote_port |
|
230 ) |
|
231 |
|
232 if remote_tagged != local_tagged : |
|
233 log.warning("%s:%s tagged %s <-> %s tagged %s:%s", |
|
234 nodes[local], local_port, ':'.join(str(x) for x in sorted(local_tagged)), |
|
235 ':'.join(str(x) for x in sorted(remote_tagged)), nodes[remote], remote_port |
|
236 ) |
|
237 |
|
238 links[reverse] = (remote_untag, remote_tagged, local_untag) |
|
239 |
|
240 return nodes, links |
|
241 |
|
242 class GraphVlans (object) : |
|
243 """ |
|
244 Maintain vlan -> dot style/color mappings |
|
245 """ |
|
246 |
|
247 SERIES = 'paired12' |
|
248 NONE = 'black' |
|
249 |
|
250 def __init__ (self, vlans=None) : |
|
251 if vlans : |
|
252 self.vlans = dict(vlans) |
|
253 else : |
|
254 self.vlans = { } |
|
255 |
|
256 def color (self, vlan) : |
|
257 if vlan in self.vlans : |
|
258 return self.vlans[vlan] |
|
259 |
|
260 # alloc |
|
261 color = '/{series}/{index}'.format(series=self.SERIES, index=len(self.vlans) + 1) |
|
262 |
|
263 self.vlans[vlan] = color |
|
264 |
|
265 return color |
|
266 |
|
267 def dot_quote (value) : |
|
268 """ |
|
269 Quote a dot value. |
|
270 """ |
|
271 |
|
272 return '"{value}"'.format(value=value) |
|
273 |
|
274 def dot (*line, **attrs) : |
|
275 """ |
|
276 Build dot-syntax: |
|
277 *line { |
|
278 *line [**attrs]; |
|
279 } |
|
280 """ |
|
281 |
|
282 if line and attrs : |
|
283 return ''.join(('\t', ' '.join(str(x) for x in line), ' [', |
|
284 ', '.join('{name}="{value}"'.format(name=name, value=value) for name, value in attrs.iteritems()), |
|
285 ']', ';')) |
|
286 elif line : |
|
287 return ' '.join(line) + ' {' |
|
288 else : |
|
289 return '}' |
|
290 |
|
291 def build_dot (options, nodes, links, type='digraph', vlans=None) : |
|
292 """ |
|
293 Construct a dot description of the given node/links graph. |
|
294 """ |
|
295 |
|
296 if vlans is True : |
|
297 vlans = { } |
|
298 |
|
299 yield dot(type, 'verkko') |
|
300 |
|
301 # defaults |
|
302 yield dot('graph', |
211 # XXX: breaks multi-edges? |
303 # XXX: breaks multi-edges? |
212 #splines = 'true', |
304 #splines = 'true', |
213 |
305 |
214 sep = '+25,25', |
306 sep = '+25,25', |
215 overlap = 'scalexy', |
307 overlap = 'scalexy', |
216 |
308 |
217 # only applies to loops |
309 # only applies to loops |
218 nodesep = 0.5, |
310 nodesep = 0.5, |
219 ) |
311 ) |
220 dot.set_edge_defaults( |
312 yield dot('edge', |
221 labeldistance = 3.0, |
313 labeldistance = 3.0, |
222 penwidth = 2.0, |
314 penwidth = 2.0, |
223 ) |
315 ) |
224 |
316 yield dot('node', |
225 nodes = { } |
317 fontsize = 18, |
226 edges = { } |
318 ) |
227 vlan_colors = { } # { vlan: color } |
319 |
228 |
320 # nodes |
229 for host, local, remote, remote_host in items : |
321 for node, node_label in nodes.iteritems() : |
230 # src |
322 yield dot(dot_quote(node), label=node_label) |
231 src_name = str(host) |
323 |
232 src_label = '"{host.location}"'.format(host=host) |
324 # links |
233 |
325 for (local, local_port, remote_port, remote), (local_untag, tagged, remote_untag) in links.iteritems() : |
234 if src_name in nodes : |
326 if vlans : |
235 src = nodes[src_name] |
327 head_color = vlans.color(local_untag) if local_untag else None |
236 else : |
328 tail_color = vlans.color(remote_untag) if remote_untag else None |
237 src = nodes[src_name] = pydot.Node(src_name, |
329 line_colors = [vlans.color(tag) for tag in sorted(tagged)] |
238 label = src_label, |
330 else : |
239 fontsize = 18, |
331 head_color = GraphVlans.NONE if local_untag else None |
240 ) |
332 tail_color = GraphVlans.NONE if remote_untag else None |
241 dot.add_node(src) |
333 line_colors = [] |
242 |
334 |
243 # dst |
335 if head_color and tail_color : |
244 if remote_host : |
336 dir = 'both' |
245 dst_name = str(remote_host) |
337 colors = [head_color, tail_color] + line_colors |
246 dst_label = '"{host.location}"'.format(host=remote_host) |
338 elif head_color : |
247 else : |
339 dir = 'forward' |
248 dst_name = remote['chassis'].replace(':', '-') |
340 colors = [head_color] + line_colors |
249 |
341 elif tail_color : |
250 # XXX: pydot is not smart enough to quote this |
342 dir = 'back' |
251 if remote['sys_name'] : |
343 colors = [vlans.NONE, tail_color] + line_colors |
252 dst_label = '"{remote[chassis]} ({remote[sys_name]})"'.format(remote=remote) |
344 else : |
253 else : |
345 dir = 'none' |
254 dst_label = '"{remote[chassis]}"'.format(remote=remote) |
346 colors = line_colors |
255 |
347 |
256 if dst_name in nodes : |
348 yield dot(dot_quote(local), '->', dot_quote(remote), |
257 dst = nodes[dst_name] |
349 taillabel = local_port, |
258 else : |
350 headlabel = remote_port, |
259 dst = nodes[dst_name] = pydot.Node(dst_name, |
351 dir = dir, |
260 label = dst_label, |
352 |
261 fontsize = 18, |
353 fillcolor = 'black', |
262 ) |
354 color = ':'.join(colors), |
263 dot.add_node(dst) |
355 ) |
264 |
356 |
265 # edges |
357 yield dot() |
266 headlabel = '"{remote[port]}"'.format(remote=remote) |
358 |
267 taillabel = '"{local[port]}"'.format(local=local) |
359 def apply_dot (options, file, dot) : |
268 fillcolor = 'black' |
360 """ |
269 color = 'black' |
361 Output dot file for given graphbits |
270 untag = tagged = None |
362 """ |
271 |
363 |
272 # vlans? |
364 for line in dot : |
273 if vlans and host in vlans and local['port_id'] in vlans[host] : |
365 file.write(line + '\n') |
274 untag, tagged = vlans[host][local['port_id']] |
|
275 |
|
276 log.debug("%s#%s: %s+%s", host, local['port_id'], untag, tagged) |
|
277 |
|
278 colors = [] |
|
279 for tag in sorted(tagged) : |
|
280 if tag in COLOR_VLANS : |
|
281 colors.append(COLOR_VLANS[tag]) |
|
282 elif tag in vlan_colors : |
|
283 colors.append(vlan_colors[tag]) |
|
284 else : |
|
285 color = '/paired12/{count}'.format(count=1+len(vlan_colors)) |
|
286 |
|
287 log.info("%s#%s: chosing new vlan %s color %s", host, local['port_id'], tag, color) |
|
288 |
|
289 vlan_colors[tag] = color |
|
290 colors.append(color) |
|
291 |
|
292 if not untag : |
|
293 pass |
|
294 elif untag in COLOR_VLANS : |
|
295 fillcolor = COLOR_VLANS[untag] |
|
296 elif untag in vlan_colors : |
|
297 fillcolor = vlan_colors[untag] |
|
298 else : |
|
299 color = '/paired12/{count}'.format(count=1+len(vlan_colors)) |
|
300 |
|
301 log.warn("%s#%s: chosing new vlan %s color %s", host, local['port_id'], untag, color) |
|
302 |
|
303 fillcolor = vlan_colors[tag] = color |
|
304 |
|
305 # first color overrides fillcolor for heads |
|
306 if colors and untag : |
|
307 color = ':'.join([fillcolor] + colors) |
|
308 elif colors : |
|
309 color = ':'.join(colors) |
|
310 elif fillcolor : |
|
311 color = fillcolor |
|
312 |
|
313 elif vlans : |
|
314 # XXX: this happens when LLDP gives us the LACP ports but the VLANS are on the TRK port |
|
315 log.warn("%s#%s: unknown port for vlans: %s", host, local['port_id'], vlans.get(host)) |
|
316 |
|
317 # edge |
|
318 if (src_name, local['port'], dst_name, remote['port']) in edges : |
|
319 log.warning("%s:%s <- %s:%s: duplicate", src_name, local['port'], dst_name, remote['port']) |
|
320 |
|
321 elif (dst_name, remote['port'], src_name, local['port']) in edges : |
|
322 log.info("%s <-> %s", src_name, dst_name) |
|
323 edge = edges[(dst_name, remote['port'], src_name, local['port'])] |
|
324 |
|
325 if edge.get('headlabel') != taillabel : |
|
326 log.warn("%s -> %s: local port mismatch: %s vs %s", src_name, dst_name, local['port'], edge.get('headlabel')) |
|
327 |
|
328 if edge.get('taillabel') != headlabel : |
|
329 log.warn("%s -> %s: remote port mismatch: %s vs %s", src_name, dst_name, remote['port'], edge.get('taillabel')) |
|
330 |
|
331 if edge.get('fillcolor') != fillcolor : |
|
332 log.warn("%s#%s -> %s#%s: remote untag mismatch: %s vs %s", src_name, local['port'], dst_name, remote['port'], fillcolor, edge.get('fillcolor')) |
|
333 |
|
334 if edge.get('color') != '"' + color + '"' : |
|
335 log.warn("%s#%s -> %s#%s: remote tagged mismatch: %s vs %s", src_name, local['port'], dst_name, remote['port'], color, edge.get('color')) |
|
336 |
|
337 # mark as bidirectional |
|
338 edges[(src_name, local['port'], dst_name, remote['port'])] = edge |
|
339 |
|
340 edge.set('dir', 'both' if untag else 'none') |
|
341 |
|
342 # set second color for tail |
|
343 if untag : |
|
344 edge.set('color', '"{headcolor}:{tailcolor}{tagcolors}"'.format( |
|
345 headcolor = edge.get('fillcolor'), |
|
346 tailcolor = fillcolor, |
|
347 tagcolors = ':' + ':'.join(colors) if colors else '', |
|
348 )) |
|
349 |
|
350 else : |
|
351 edge = edges[(src_name, local['port'], dst_name, remote['port'])] = pydot.Edge(src, dst, |
|
352 dir = 'forward' if untag else 'none', |
|
353 headlabel = headlabel, |
|
354 taillabel = taillabel, |
|
355 color = '"{color}"'.format(color=color), |
|
356 fillcolor = fillcolor, |
|
357 ) |
|
358 |
|
359 dot.add_edge(edge) |
|
360 |
|
361 if options.graph_dot : |
|
362 dot.write(options.graph_dot) |
|
363 |
366 |
364 def main (argv) : |
367 def main (argv) : |
365 """ |
368 """ |
366 Graph network |
369 Graph network |
367 """ |
370 """ |