mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 21:13:00 +02:00
To run LAG tests on hardware, we have to make sure that the chosen links are running at the same speed. We do not want to require a specific link speed from the logical topology, as the real requirement is merely that the two (or however many) links are of the _same_ speed, not any _particular_ speed. Rather than having to complicate the topology matcher, let it take care of the common pattern of matching requirements to provided features, then let tests pass custom node/edge matchers for the complicated cases.
177 lines
4.9 KiB
Python
177 lines
4.9 KiB
Python
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_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 <physical> <logical>
|
|
#
|
|
# 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)
|