import networkx as nx from networkx.algorithms import isomorphism from itertools import permutations import json def _qstrip(text): if text is None: return None if text.startswith("\"") and text.endswith("\""): return text[1:-1] return text def compatible(physical, logical): return logical["requires"].issubset(physical["provides"]) def edge_mappings(les, pes): les = les.values() pes = pes.values() for perm in permutations(pes, len(les)): candidate = tuple(zip(les, perm)) if all(map(lambda pair: compatible(pair[1], pair[0]), candidate)): yield candidate class Topology: def __init__(self, dotg): self.dotg = dotg self.g = nx.MultiGraph() for n in self.dotg.get_nodes(): name = n.get_name() if name in ("node", "edge"): continue repr(n.get_attributes()) attrs = { _qstrip(k): _qstrip(v) for k, v in n.get_attributes().items() if k != "label" } for attr in ("requires", "provides"): attrs[attr] = set(attrs.get(attr, "").split()) self.g.add_node(name, **attrs) for e in self.dotg.get_edges(): sn, sp = e.get_source().split(":") dn, dp = e.get_destination().split(":") attrs = {_qstrip(k): _qstrip(v) for k, v in e.get_attributes().items()} attrs[sn] = sp attrs[dn] = dp for attr in ("requires", "provides"): attrs[attr] = set(attrs.get(attr, "").split()) self.g.add_edge(sn, dn, **attrs) def __repr__(self): if not self.mapping: return "" out = "" for n in self.mapping: out += f"{n + ':':<8} {self.mapping[n][None]}\n" for e in self.mapping[n]: if not e: continue out += f" {e + ':':<8} {self.mapping[n][e]}\n" return out def map_to(self, phy, nodes_compatible=compatible, edge_mappings=edge_mappings): mapper = isomorphism.MultiGraphMatcher(phy.g, self.g, edge_match=lambda pes, les: any(edge_mappings(les, pes)), node_match=nodes_compatible) if not mapper.subgraph_is_monomorphic(): return False # breakpoint() self.phy = phy self.mapping = {} for pn, ln in mapper.mapping.items(): self.mapping.setdefault(ln, { None: pn }) for lsrc, ldst in set(self.g.edges()): psrc = self.mapping[lsrc][None] pdst = self.mapping[ldst][None] les = self.g.get_edge_data(lsrc, ldst) pes = self.phy.g.get_edge_data(psrc, pdst) for le, pe in next(edge_mappings(les, pes)): self.mapping[lsrc][le[lsrc]] = pe[psrc] self.mapping[ldst][le[ldst]] = pe[pdst] return True def xlate(self, lnode, lport=None): assert self.mapping if lnode not in self.mapping: return None nodemap = self.mapping[lnode] if lport not in nodemap: return None if not lport: return nodemap[None] return (nodemap[None], nodemap[lport]) def get_nodes(self, flt): out = [] for name in self.g.nodes: if flt(name, self.g.nodes[name]): out.append(name) return out def get_password(self, node): n = self.dotg.get_node(node) b = n[0] if n else {} password = b.get("password") return qstrip(password) if password is not None else "admin" def get_expected_boot(self, node): n = self.dotg.get_node(node) b = n[0] if n else {} boot = b.get("expected_boot") return _qstrip(boot) def get_link(self, src, dst, flt=lambda _: True): es = self.g.get_edge_data(src, dst) for e in es.values(): if flt(e): return e[src], e[dst] return None def get_mgmt_link(self, src, dst): return self.get_link(src, dst, lambda e: compatible(e, {"requires": {"mgmt"}})) def get_ctrl(self): ns = self.get_nodes(lambda _, attrs: compatible(attrs, {"requires": {"controller"}})) assert len(ns) == 1 return ns[0] def get_infixen(self): return self.get_nodes(lambda _, attrs: compatible(attrs, {"requires": {"infix"}})) def get_attr(self, name, default=None): return _qstrip(self.dotg.get_attributes().get(name, default)) # Support calling this script like so... # # python3 topology.py # # to inspect the graph matcher's results in isolation from the rest of # the system. if __name__ == "__main__": import json import pydot import sys phy = Topology(pydot.graph_from_dot_file(sys.argv[1])[0]) log = Topology(pydot.graph_from_dot_file(sys.argv[2])[0]) if log.map_to(phy): print(json.dumps(log.mapping)) sys.exit(0) print("{}") sys.exit(1)