From d333c66f9cfaba49bc2c4262f7fad5ac8f997739 Mon Sep 17 00:00:00 2001 From: Richard Alpe Date: Tue, 23 Jun 2026 09:33:46 +0200 Subject: [PATCH 1/2] test: add generic boot-from-factory-config test Verify that the device's factory-default configuration boots cleanly and that the device remains usable afterwards -- i.e. it does not fall back to the fail-secure failure-config. The test exercises the device's own first-boot bootstrap path: it clears the startup-config and reboots with the test-mode startup override set, so confd initialises running from the factory-config (its first-boot path). That mirrors a factory-fresh / factory-reset device and avoids applying a full config swap over the live management session -- confd cannot replace the whole running datastore from within an RPC without timing out, and a factory config may reconfigure the very transport we are connected over. The test is image-generic, using whatever factory-config the running image was built with, so it covers both the stock Infix factory config and any spin factory config. A single-node (1x1) topology is used on purpose: a factory config is not written with a lab full of peers in mind, so booting it across a multi-node topology could trigger broadcast storms or similar. NETCONF only, as it is the one management transport present in every factory config. Signed-off-by: Richard Alpe --- test/case/system/all.yaml | 3 ++ test/case/system/factory_config/Readme.adoc | 34 ++++++++++++ test/case/system/factory_config/test.py | 57 ++++++++++++++++++++ test/case/system/factory_config/topology.dot | 1 + test/case/system/factory_config/topology.svg | 34 ++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 test/case/system/factory_config/Readme.adoc create mode 100755 test/case/system/factory_config/test.py create mode 120000 test/case/system/factory_config/topology.dot create mode 100644 test/case/system/factory_config/topology.svg diff --git a/test/case/system/all.yaml b/test/case/system/all.yaml index f31dfe46c..f77645f6a 100644 --- a/test/case/system/all.yaml +++ b/test/case/system/all.yaml @@ -25,3 +25,6 @@ - name: Schedule Reboot case: schedule_reboot/test.py + +- name: Boot From Factory Config + case: factory_config/test.py diff --git a/test/case/system/factory_config/Readme.adoc b/test/case/system/factory_config/Readme.adoc new file mode 100644 index 000000000..d6689e35f --- /dev/null +++ b/test/case/system/factory_config/Readme.adoc @@ -0,0 +1,34 @@ +=== Boot From Factory Config + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/factory_config] + +==== Description + +Verify that the device's factory-default configuration boots cleanly and +that the device remains usable afterwards -- i.e. it does not fall back to +the fail-secure failure-config. + +This exercises the device's own first-boot bootstrap path: with no +startup-config present, confd initialises running from the factory-config. +That is exactly what a factory-fresh (or factory-reset) device does, and +it avoids applying a full config swap over the live management session. + +The test is image-generic: it uses whatever factory-config the running +image was built with, so it covers both the stock Infix factory config and +any spin factory config. + +A single-node topology is used on purpose: a factory config is not written +with a lab full of peers in mind, so applying it across a multi-node +topology could trigger broadcast storms or similar. + +==== Topology + +image::topology.svg[Boot From Factory Config topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Determine factory-config hostname +. Clear startup-config so the device boots from factory +. Reboot onto the factory config +. Verify device is usable and not in failure-config diff --git a/test/case/system/factory_config/test.py b/test/case/system/factory_config/test.py new file mode 100755 index 000000000..98830eb6e --- /dev/null +++ b/test/case/system/factory_config/test.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Boot From Factory Config + +Verify that the device boots cleanly from its factory-config and stays +usable, i.e. it does not fall back to the fail-secure failure-config. +Clearing the startup-config makes confd bootstrap running from the +factory-config on the next boot, as on a factory-fresh device. +""" + +import json + +import infamy +from infamy.util import wait_boot + +STARTUP = "/cfg/startup-config.cfg" +FACTORY = "/etc/factory-config.cfg" + + +def factory_hostname(tgtssh): + """Read the hostname the factory-config will boot with.""" + cfg = json.loads(tgtssh.runsh(f"cat {FACTORY}").stdout) + return cfg.get("ietf-system:system", {}).get("hostname") + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + + with test.step("Determine factory-config hostname"): + expected = factory_hostname(tgtssh) + assert expected, "Could not read hostname from factory-config" + print(f"Factory config hostname is {expected!r}") + + with test.step("Clear startup-config so the device boots from factory"): + # No startup-config on the startup boot path -> confd bootstraps + # running from the factory-config. + tgtssh.runsh(f"rm -f {STARTUP}") + target.startup_override() + + with test.step("Reboot onto the factory config"): + target.reboot() + if not wait_boot(target, env): + test.fail("Device did not boot from factory config") + + with test.step("Verify device is usable and not in failure-config"): + target = env.attach("target", "mgmt", test_reset=False) + + # A failed bootstrap reverts to failure-config, which has a + # different hostname; matching the factory hostname proves we + # booted on the factory config, not the fail-secure fallback. + running = target.get_config_dict("/ietf-system:system") + assert running.get("system", {}).get("hostname") == expected, \ + "Device did not boot on the factory config (failure-config fallback?)" + + test.succeed() diff --git a/test/case/system/factory_config/topology.dot b/test/case/system/factory_config/topology.dot new file mode 120000 index 000000000..02b788692 --- /dev/null +++ b/test/case/system/factory_config/topology.dot @@ -0,0 +1 @@ +../../../infamy/topologies/1x1.dot \ No newline at end of file diff --git a/test/case/system/factory_config/topology.svg b/test/case/system/factory_config/topology.svg new file mode 100644 index 000000000..1e10fc28e --- /dev/null +++ b/test/case/system/factory_config/topology.svg @@ -0,0 +1,34 @@ + + + + + + +1x1 + + + +host + +host + +mgmt + + + +target + +mgmt + +target + + + +host:mgmt--target:mgmt + + + + From e233dbc6dadf83376963cf261800597781ca2ec8 Mon Sep 17 00:00:00 2001 From: Richard Alpe Date: Tue, 30 Jun 2026 17:19:07 +0200 Subject: [PATCH 2/2] test: reboot at end of factory config test to restore Reboot at end of factory config test to restore test config. Signed-off-by: Richard Alpe --- test/case/system/factory_config/test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/case/system/factory_config/test.py b/test/case/system/factory_config/test.py index 98830eb6e..8d5a7ac3f 100755 --- a/test/case/system/factory_config/test.py +++ b/test/case/system/factory_config/test.py @@ -21,6 +21,13 @@ def factory_hostname(tgtssh): cfg = json.loads(tgtssh.runsh(f"cat {FACTORY}").stdout) return cfg.get("ietf-system:system", {}).get("hostname") +def cleanup(env): + """Restore the rig to the clean per-test baseline for the next test.""" + print("Restoring device to clean test baseline") + target = env.attach("target", "mgmt", "netconf") + target.reboot() + if not wait_boot(target, env): + test.fail("Device did not come back while restoring baseline") with infamy.Test() as test: with test.step("Set up topology and attach to target DUT"): @@ -43,6 +50,7 @@ def factory_hostname(tgtssh): target.reboot() if not wait_boot(target, env): test.fail("Device did not boot from factory config") + test.push_test_cleanup(lambda: cleanup(env)) with test.step("Verify device is usable and not in failure-config"): target = env.attach("target", "mgmt", test_reset=False)