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..a215aab4b --- /dev/null +++ b/test/case/system/factory_config/Readme.adoc @@ -0,0 +1,37 @@ +=== 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. + +Uses NETCONF only: it is the one management transport present in every +factory config, so booting onto the factory config cannot lock us out. + +==== 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..7b84eb9d4 --- /dev/null +++ b/test/case/system/factory_config/test.py @@ -0,0 +1,67 @@ +#!/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") + + +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"): + env = infamy.Env() + target = env.attach("target", "mgmt", "netconf") + 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") + test.push_test_cleanup(lambda: cleanup(env)) + + with test.step("Verify device is usable and not in failure-config"): + target = env.attach("target", "mgmt", "netconf", 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 + + + +