From aa8e11a58be931e78991bc314b58574e82cf97c3 Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Wed, 14 Jun 2023 16:51:13 +0200 Subject: [PATCH] test/infamy: Create an infamy specific topology object Create topology objects that are tailored to represent topologies on the expected format. This let's us make convenient accessors for things like the controller node, infix devices, getting the ports used to reach a certain device from another one, etc. --- test/case/ietf_interfaces/vlan_ping.py | 4 +- test/infamy/env.py | 64 ++---------- test/infamy/topology.py | 139 +++++++++++++++++++------ 3 files changed, 119 insertions(+), 88 deletions(-) diff --git a/test/case/ietf_interfaces/vlan_ping.py b/test/case/ietf_interfaces/vlan_ping.py index 555f47f5..635a4877 100755 --- a/test/case/ietf_interfaces/vlan_ping.py +++ b/test/case/ietf_interfaces/vlan_ping.py @@ -8,7 +8,7 @@ with infamy.Test() as test: target = env.attach("target", "mgmt") with test.step("Configure VLAN 10 on target:data with IP 10.0.0.2"): - _, tport = env.xlate("target", "data") + _, tport = env.ltop.xlate("target", "data") target.put_config_dict("ietf-interfaces", { "interfaces": { @@ -45,7 +45,7 @@ with infamy.Test() as test: }) with test.step("Ping 10.0.0.2 from VLAN 10 on host:data with IP 10.0.0.1"): - _, hport = env.xlate("host", "data") + _, hport = env.ltop.xlate("host", "data") with infamy.IsolatedMacVlan(hport) as ns: pingtest = ns.runsh(""" diff --git a/test/infamy/env.py b/test/infamy/env.py index 2605d769..90e5d990 100644 --- a/test/infamy/env.py +++ b/test/infamy/env.py @@ -4,7 +4,6 @@ import os import pydot import shlex import sys -import time from . import neigh, netconf, tap, topology @@ -26,72 +25,29 @@ class Env(object): self.args = ArgumentParser(ltop).parse_args(argv) pdot = pydot.graph_from_dot_file(self.args.ptop[0])[0] - self.ptop = networkx.nx_pydot.read_dot(self.args.ptop[0]) + self.ptop = topology.Topology(pdot) if self.args.ltop: - self.ltop = networkx.nx_pydot.read_dot(self.args.ltop) ldot = pydot.graph_from_dot_file(self.args.ltop)[0] - mapping = topology.find_mapping(pdot, ldot) - if not mapping: + self.ltop = topology.Topology(ldot) + if not self.ltop.map_to(self.ptop): raise tap.TestSkip() - self.mapping = {} - for (log, phy) in mapping.items(): - if ":" in log: - lnode, lport = log.split(":") - pnode, pport = phy.split(":") - - self._map_node(lnode, pnode) - self._map_port((lnode, lport), (pnode, pport)) - else: - self._map_node(log, phy) - - def _map_node(self, lnode, pnode): - if lnode in self.mapping: - assert(self.mapping[lnode][None] == pnode) - else: - self.mapping[lnode] = { None: pnode } - - def _map_port(self, log, phy): - (lnode, lport) = log - (pnode, pport) = phy - - if lport in self.mapping[lnode]: - assert(self.mapping[lnode][lport] == pport) - else: - self.mapping[lnode][lport] = pport - - def xlate(self, lnode, lport=None): - 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 attach(self, node, port): - if self.mapping: - mapping = self.mapping[node] - node, port = mapping[None], mapping[port] + if self.ltop: + mapping = self.ltop.mapping[node] + node, port = self.ltop.xlate(node, port) else: mapping = None - hostport = list(self.ptop.neighbors(f"{node}:{port}"))[0] - hnode, hport = hostport.split(":") + ctrl = self.ptop.get_ctrl() + _, cport = self.ptop.get_path(ctrl, (node, port))[0] - print(f"Probing {node} on port {hport} for IPv6LL mgmt address ...") - mgmtip = neigh.ll6ping(hport) + print(f"Probing {node} on port {cport} for IPv6LL mgmt address ...") + mgmtip = neigh.ll6ping(cport) if not mgmtip: raise Exception(f"Failed, cannot find mgmt IP for {node}") - time.sleep(10) return netconf.Device( location=netconf.Location(mgmtip), mapping=mapping, diff --git a/test/infamy/topology.py b/test/infamy/topology.py index 1814af06..e6c42ad9 100644 --- a/test/infamy/topology.py +++ b/test/infamy/topology.py @@ -6,51 +6,122 @@ def qstrip(text): return text[1:-1] return text -def match_kind(n1, n2): - return qstrip(n1["kind"]) == qstrip(n2["kind"]) +def match_kind(n1attrs, n2attrs): + return n1attrs.get("kind") == n2attrs.get("kind") -def find_mapping(phy, log, node_match=match_kind): - def annotate(nxg, dotg): - for e in list(nxg.edges): +class Topology: + def __init__(self, dotg): + self.dotg = dotg + edges = { tuple(e.get_source().split(":")): { tuple(e.get_destination().split(":")): { "weight": 1 } } \ + for e in self.dotg.get_edges() \ + } + self.g = nx.Graph(edges, weight=1) + for e in list(self.g.edges): s, d = e - nxg.nodes[s]["kind"] = "port" - nxg.nodes[d]["kind"] = "port" + self.g.nodes[s]["kind"] = "port" + self.g.nodes[d]["kind"] = "port" - sn, sp = s.split(":") - dn, dp = d.split(":") + sn, sp = s + dn, dp = d try: - sk = dotg.get_node(sn)[0].get_attributes()["kind"] + sk = qstrip(self.dotg.get_node(sn)[0].get_attributes()["kind"]) except: raise ValueError("\"{}\"'s kind is not known".format(sn)) try: - dk = dotg.get_node(dn)[0].get_attributes()["kind"] + dk = qstrip(self.dotg.get_node(dn)[0].get_attributes()["kind"]) except: raise ValueError("\"{}\"'s kind is not known".format(dn)) - nxg.add_node(sn, kind=sk) - nxg.add_edge(sn, s) - nxg.add_node(dn, kind=dk) - nxg.add_edge(dn, d) + self.g.add_node(sn, kind=sk) + self.g.add_edge(sn, s, weight=0) + self.g.add_node(dn, kind=dk) + self.g.add_edge(dn, d, weight=0) - phyedges = [(e.get_source(), e.get_destination()) for e in phy.get_edges()] - logedges = [(e.get_source(), e.get_destination()) for e in log.get_edges()] + def map_to(self, phy, node_match=match_kind): + def _map_node(lnode, pnode): + if lnode in self.mapping: + assert(self.mapping[lnode][None] == pnode) + else: + self.mapping[lnode] = { None: pnode } - phyedges.sort() - logedges.sort() - nxphy = nx.Graph(phyedges) - nxlog = nx.Graph(logedges) - annotate(nxphy, phy) - annotate(nxlog, log) + def _map_port(log, phy): + (lnode, lport) = log + (pnode, pport) = phy - nxmap = isomorphism.GraphMatcher(nxphy, nxlog, node_match=node_match) - if nxmap.subgraph_is_isomorphic(): - return { v: k for (k, v) in nxmap.mapping.items() } + if lport in self.mapping[lnode]: + assert(self.mapping[lnode][lport] == pport) + else: + self.mapping[lnode][lport] = pport - return None + nxmap = isomorphism.GraphMatcher(phy.g, self.g, node_match=node_match) + if not nxmap.subgraph_is_isomorphic(): + return False -# This let's us call this script like so... + self.phy = phy + self.mapping = {} + for (phy, log) in nxmap.mapping.items(): + if isinstance(log, tuple): + lnode, lport = log + pnode, pport = phy + + _map_node(lnode, pnode) + _map_port((lnode, lport), (pnode, pport)) + else: + _map_node(log, phy) + + 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_ports(self, node): + ports = self.get_nodes(lambda name, _: name.startswith(f"{node}:")) + return { p.removeprefix(f"{node}:") for p in ports } + + def get_path(self, src, dst): + path = nx.shortest_path(self.g, src, dst) + return path[1:-1] if path else None + + def get_paths(self, src, dst): + paths = nx.all_shortest_paths(self.g, src, dst) + if not paths: + return None + + return map(lambda path: path[1:-1], paths) + + def get_ctrl(self): + ns = self.get_nodes(lambda _, attrs: attrs.get("kind") == "controller") + assert(len(ns) == 1) + return ns[0] + + def get_infixen(self): + return self.get_nodes(lambda _, attrs: attrs.get("kind") == "infix") + + +# Support calling this script like so... # # python3 topology.py # @@ -61,8 +132,12 @@ if __name__ == "__main__": import pydot import sys - pdot = pydot.graph_from_dot_file(sys.argv[1])[0] - ldot = pydot.graph_from_dot_file(sys.argv[2])[0] - mapping = find_mapping(pdot, ldot) + 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)) + print(json.dumps(tuple(log.get_paths("host", "target")))) + sys.exit(0) - print(json.dumps(mapping)) + print("{}") + sys.exit(1)