Files
infix/test/infamy/topology.py
T
Tobias Waldekranz da39855cec test: infamy: Allow tests to refine the topology matching process
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.
2025-02-12 23:22:09 +01:00

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)