mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-02 13:53:01 +02:00
114 lines
4.3 KiB
Python
Executable File
114 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Get operational
|
|
|
|
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
|
|
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 operational_interfaces(dut):
|
|
oper = dut.get_data("/ietf-interfaces:interfaces")["interfaces"]["interface"]
|
|
return {i["name"] for i in oper}
|
|
|
|
|
|
def interfaces_match(name, dut, want):
|
|
"""True once operational interfaces equal the configured set.
|
|
|
|
A preceding test (e.g. a container use_case) may leave veth endpoints
|
|
that are still being torn down when this test starts, so operational
|
|
momentarily carries interfaces that are not in the configuration.
|
|
Rather than asserting on that transient state, poll until the kernel --
|
|
and thus operational -- has converged on the configured set.
|
|
"""
|
|
have = operational_interfaces(dut)
|
|
if have != want:
|
|
print(f"{name}: operational {sorted(have)} != configured {sorted(want)}, waiting...")
|
|
return False
|
|
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 features_absent(name, dut):
|
|
"""True once every unconfigured feature subtree is gone.
|
|
|
|
Like the interface set, these subtrees can momentarily linger: a
|
|
preceding test (containers, NTP, a routing protocol) leaves state that
|
|
yangerd only prunes once the underlying daemon/config is torn down.
|
|
Poll until operational has converged rather than asserting on the
|
|
transient overlap.
|
|
"""
|
|
pending = [xpath for xpath in ABSENT if not absent(dut, xpath)]
|
|
if pending:
|
|
print(f"{name}: feature data still present {pending}, waiting...")
|
|
return False
|
|
return True
|
|
|
|
|
|
with infamy.Test() as test:
|
|
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("Verify operational interfaces match the test-config"):
|
|
def check(name, dut):
|
|
want = configured_interfaces(dut)
|
|
until(lambda: interfaces_match(name, dut, want), attempts=60)
|
|
|
|
parallel(*(lambda n=name, d=dut: check(n, d) for name, dut in duts.items()))
|
|
|
|
with test.step("Verify unconfigured feature subtrees are absent"):
|
|
def check_absent(name, dut):
|
|
# NTP is pruned by a periodic collector with a 60s poll
|
|
# interval, so a stale subtree can take a full cycle to clear;
|
|
# wait comfortably past one interval rather than racing it.
|
|
until(lambda: features_absent(name, dut), attempts=120)
|
|
|
|
parallel(*(lambda n=name, d=dut: check_absent(n, d)
|
|
for name, dut in duts.items()))
|
|
|
|
test.succeed()
|