From 6d18ba4c394e62b068dc5e2657701a7fc4d6067b Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Mon, 2 Dec 2024 11:25:37 +0100 Subject: [PATCH 1/2] test: infamy: Unify {NET,REST}CONF operational data formatting Replace the existing remove-module-names-pass in the RESTCONF transport -- which only worked for top-level containers -- with libyang's standard `.print_dict()` used by the NETCONF transport implementation. As a bouns, the implementation of `.get_iface()` is now transport agnostic. --- test/infamy/netconf.py | 12 ------------ test/infamy/restconf.py | 26 +++----------------------- test/infamy/transport.py | 12 ++++++++---- 3 files changed, 11 insertions(+), 39 deletions(-) diff --git a/test/infamy/netconf.py b/test/infamy/netconf.py index 0662e394..f2807566 100644 --- a/test/infamy/netconf.py +++ b/test/infamy/netconf.py @@ -15,7 +15,6 @@ import libyang import lxml import netconf_client.connect import netconf_client.ncclient -import infamy.iface as iface from infamy.transport import Transport,infer_put_dict from netconf_client.error import RpcError from . import env @@ -382,17 +381,6 @@ class Device(Transport): with open(outdir+"/"+schema["filename"], "w") as f: f.write(data.schema) - def get_iface(self, name): - """Fetch target dict for iface and extract param from JSON""" - content = self.get_data(iface.get_xpath(name)) - interface = content.get("interfaces", {}).get("interface", None) - - if interface is None: - return None - - # Restconf (rousette) address by id, otherwise (netopper2) by name - return interface[name] - def delete_xpath(self, xpath): # Split out the model and the container from xpath' pattern = r"^/(?P[^:]+):(?P[^/]+)" diff --git a/test/infamy/restconf.py b/test/infamy/restconf.py index aa23e92a..6d440c19 100644 --- a/test/infamy/restconf.py +++ b/test/infamy/restconf.py @@ -321,19 +321,11 @@ class Device(Transport): """Get operational data""" uri = xpath_to_uri(xpath) if xpath is not None else None data = self.get_operational(uri, parse) - if parse is False: - return data - if data is None: - return None + if parse and data: + return data.print_dict() - data = json.loads(data.print_mem("json", with_siblings=True, - pretty=False)) - for k, v in data.items(): - model, container = k.split(":") - break - - return {container: v} + return data def copy(self, source, target): factory = self.get_datastore(source) @@ -372,18 +364,6 @@ class Device(Transport): data = json.loads(data) return data["ietf-system:system-state"]["clock"]["current-datetime"] - def get_iface(self, iface): - """Fetch target dict for iface and extract param from JSON""" - xpath = f"/ietf-interfaces:interfaces/interface={iface}" - content = self.get_data(xpath) - interface = content.get("interfaces", {}).get("interface", None) - if interface is None: - return None - - # RESTCONF (at least rousette) addresses by id, but NETCONF (at - # least netopper2) addresses by name - return interface[0] - def delete_xpath(self, xpath): """Delete XPath from running config""" path = f"/ds/ietf-datastores:running/{xpath_to_uri(xpath)}" diff --git a/test/infamy/transport.py b/test/infamy/transport.py index 3b435384..61daae9d 100644 --- a/test/infamy/transport.py +++ b/test/infamy/transport.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from infamy.neigh import ll6ping +import infamy.iface def infer_put_dict(name, models): if not models.get("ietf-system"): @@ -50,10 +51,13 @@ class Transport(ABC): def call_action(self, xpath): pass - @abstractmethod - def get_iface(self, iface): - """Should be common, but is not due to bug in rousette""" - pass + def get_iface(self, name): + """Fetch target dict for iface and extract param from JSON""" + content = self.get_data(infamy.iface.get_xpath(name)) + interfaces = content.get("interfaces", {}).get("interface", {}) + + # KeyedList does not support `.get()` + return interfaces[name] if name in interfaces else None def get_mgmt_ip(self): """Return managment IP address used for RESTCONF/NETCONF""" From 784b3914674a124b16689116ffca1f3d35cc3c5e Mon Sep 17 00:00:00 2001 From: Tobias Waldekranz Date: Mon, 2 Dec 2024 09:21:09 +0100 Subject: [PATCH 2/2] test: reproducible: Log software versions Make sure that we keep a record of the versions and boot source used by all devices under test when running the full suite - in case we ever suspect that there is a mismatch between the intended test image and the actual one. Since this test now needs to communicate with the devices, make sure that we run the wait test first. --- test/case/all.yaml | 2 +- test/case/meta/reproducible.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/test/case/all.yaml b/test/case/all.yaml index d6ca30f0..114d836c 100644 --- a/test/case/all.yaml +++ b/test/case/all.yaml @@ -1,6 +1,6 @@ --- -- case: meta/reproducible.py - case: meta/wait.py +- case: meta/reproducible.py - name: Misc tests suite: misc/misc.yaml diff --git a/test/case/meta/reproducible.py b/test/case/meta/reproducible.py index 6e2ad565..e6f68515 100755 --- a/test/case/meta/reproducible.py +++ b/test/case/meta/reproducible.py @@ -13,4 +13,27 @@ with infamy.Test() as test: else: print(f"Specify PYTHONHASHSEED={seed} to reproduce this test environment") + with test.step("Discover topology and attach to available DUTs"): + env = infamy.Env(False) + ctrl = env.ptop.get_ctrl() + + duts = {} + for ix in env.ptop.get_infixen(): + cport, ixport = env.ptop.get_mgmt_link(ctrl, ix) + print(f"Attaching to {ix}:{ixport} via {ctrl}:{cport}") + duts[ix] = env.attach(ix, ixport) + + with test.step("Log running software versions"): + for name, tgt in duts.items(): + sys = tgt.get_data("/ietf-system:system-state") + sw = sys["system-state"]["software"] + plt = sys["system-state"]["platform"] + + print(f"{name}:") + for k,v in plt.items(): + print(f" {k:<16s} {v}") + + for k in ("compatible", "booted"): + print(f" {k:<16s} {sw[k]}") + test.succeed()