test: operational_all: Harden to catch more

- Test so the value in operational actually is correct
- Move test to last in suite to see that there are no stray
  data in operational (yangerd)
This commit is contained in:
Mattias Walström
2026-06-27 08:41:21 +02:00
parent 39114f89dd
commit b662984f09
5 changed files with 96 additions and 54 deletions
+4 -3
View File
@@ -17,9 +17,6 @@
infamy:
specification: False
- name: "Miscellaneous"
suite: misc/all.yaml
- name: "System"
suite: system/all.yaml
@@ -62,3 +59,7 @@
- name: "Use Case Tests"
suite: use_case/all.yaml
- name: "Miscellaneous"
suite: misc/all.yaml
+18 -6
View File
@@ -4,16 +4,28 @@ ifdef::topdoc[:imagesdir: {topdoc}../../test/case/misc/operational_all]
==== Description
Basic test just to get operational from test-config without errors.
For every Infix device, verify the operational datastore against the
test-config -- the running config is the source of truth, so it works
regardless of how interfaces differ between devices:
==== Topology
1. The operational interfaces are exactly the configured interfaces --
no missing interface, and nothing operational that was not
configured.
2. Features the test-config does not enable (NTP, containers, routing
protocols) emit no operational data.
image::topology.svg[Get Operational topology, align=center, scaledwidth=75%]
Both checks use specific-path GETs, which behave consistently across
NETCONF and RESTCONF. A full-datastore GET is not portable: RESTCONF does
not serve the operational datastore root, and NETCONF's operational
provider errors when asked for an empty subtree.
This test has no logical topology -- it reads state from whatever Infix
DUTs the physical topology provides.
==== Sequence
. Set up topology and attach to target DUT
. Copy test-config to running configuration
. Get all Operational data from 'target', verify there are no errors
. Attach to all Infix DUTs in the topology
. Verify operational interfaces match the test-config
. Verify unconfigured feature subtrees are absent
+74 -11
View File
@@ -1,23 +1,86 @@
#!/usr/bin/env python3
# Test that it is possible to get all operational data
"""
Get operational
Basic test just to get operational from test-config without errors.
For every Infix device, verify the operational datastore against the
test-config -- the running config is the source of truth, so it works
regardless of how interfaces differ between devices:
1. The operational interfaces are exactly the configured interfaces --
no missing interface, and nothing operational that was not
configured.
2. Features the test-config does not enable (NTP, containers, routing
protocols) emit no operational data.
Both checks use specific-path GETs, which behave consistently across
NETCONF and RESTCONF. A full-datastore GET is not portable: RESTCONF does
not serve the operational datastore root, and NETCONF's operational
provider errors when asked for an empty subtree.
This test has no logical topology -- it reads state from whatever Infix
DUTs the physical topology provides.
"""
import infamy
import infamy.iface as iface
from infamy.util import parallel, until
# Feature subtrees that must be absent because the test-config does not
# enable them. /ietf-routing:routing itself is always present (its RIB
# reflects the kernel's connected/local routes), so we target the
# config-gated control-plane-protocols child rather than all of routing.
ABSENT = [
"/ietf-ntp:ntp",
"/infix-containers:containers",
"/ietf-routing:routing/control-plane-protocols",
]
def configured_interfaces(dut):
cfg = dut.get_config_dict("/ietf-interfaces:interfaces")
return {i["name"] for i in cfg["interfaces"]["interface"]}
def verify_interfaces(name, dut, want):
"""Operational interfaces must be exactly the configured set."""
oper = dut.get_data("/ietf-interfaces:interfaces")["interfaces"]["interface"]
have = {i["name"] for i in oper}
assert have == want, \
f"{name}: operational interfaces {sorted(have)} != configured {sorted(want)}"
return True
def absent(dut, xpath):
# RESTCONF returns nothing for an absent subtree; NETCONF's operational
# provider errors when asked for one -- both mean "not present".
try:
return not dut.get_data(xpath)
except Exception:
return True
def verify_absent(name, dut):
for xpath in ABSENT:
assert absent(dut, xpath), \
f"{name}: unexpected operational data at {xpath}"
return True
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
with test.step("Attach to all Infix DUTs in the topology"):
env = infamy.Env(ltop=False)
infixen = env.ptop.get_infixen()
assert infixen, "no Infix devices found in topology"
duts = dict(zip(infixen, parallel(*(lambda n=name: env.attach(n, "mgmt")
for name in infixen))))
with test.step("Copy test-config to running configuration"):
pass
with test.step("Verify operational interfaces match the test-config"):
def check(name, dut):
want = configured_interfaces(dut)
until(lambda: verify_interfaces(name, dut, want))
with test.step("Get all Operational data from 'target', verify there are no errors"):
target.get_data(parse=False)
parallel(*(lambda n=name, d=dut: check(n, d) for name, dut in duts.items()))
with test.step("Verify unconfigured feature subtrees are absent"):
parallel(*(lambda n=name, d=dut: verify_absent(n, d)
for name, dut in duts.items()))
test.succeed()
@@ -1 +0,0 @@
../../../infamy/topologies/1x1.dot
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x1 Pages: 1 -->
<svg width="424pt" height="45pt"
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
<title>1x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB