aboutsummaryrefslogtreecommitdiff
path: root/scripts/testing/net-scenarios
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-11 06:01:17 -0500
committerCraig Jennings <c@cjennings.net>2026-07-11 06:01:17 -0500
commita364c1ee7f2ebfd5d55535d8239fd76aba475a6f (patch)
tree08dbb5d36caf2ee0f86379f563da8f829204768a /scripts/testing/net-scenarios
parent48e7f3e7689abc2d97cd2e7ffc552b5e54a2fb18 (diff)
downloadarchsetup-a364c1ee7f2ebfd5d55535d8239fd76aba475a6f.tar.gz
archsetup-a364c1ee7f2ebfd5d55535d8239fd76aba475a6f.zip
test(net): add the doctor control-plane repair scenarios and runner
net Phase 1's three privileged fixes (unmask-nm, disable-rival, chmod-keyfile) can't be verified against fakes past the point where real sudo and a running NetworkManager matter, and the broken states they repair are too dangerous to stage on a daily driver. I added the live-verification harness they need. Three scenario files encode the break/fix/assert for each: mask NetworkManager, enable a rival dhcpcd, and chmod a profile keyfile to 0644, then run the real net doctor --fix and assert the repair. Each also names the read-only diagnose verdict it expects, so a run that breaks catches whether the fault is in the diagnosis or the fix. run-net-scenarios.sh rsyncs the net and panelkit trees into a target VM over ssh and drives the scenarios there. The target is a booted VM, not a container: NetworkManager needs a real kernel and network stack to actually run, so an nspawn container can only show the filesystem-level half (the mask symlink, the keyfile mode) and not "NM is active". A disposable VM you revert is the right environment. The scenario break/fix/assert logic is the reviewed part. The ssh transport plumbing hasn't been exercised against a live target yet, so it's marked first-draft and may need a shakeout on the first real run. The by-hand equivalent already lives in the manual-testing checklist.
Diffstat (limited to 'scripts/testing/net-scenarios')
-rw-r--r--scripts/testing/net-scenarios/10-nm-masked.sh29
-rw-r--r--scripts/testing/net-scenarios/20-rival-manager.sh27
-rw-r--r--scripts/testing/net-scenarios/30-keyfile-perms.sh30
3 files changed, 86 insertions, 0 deletions
diff --git a/scripts/testing/net-scenarios/10-nm-masked.sh b/scripts/testing/net-scenarios/10-nm-masked.sh
new file mode 100644
index 0000000..d3b382b
--- /dev/null
+++ b/scripts/testing/net-scenarios/10-nm-masked.sh
@@ -0,0 +1,29 @@
+# shellcheck shell=bash disable=SC2034 # sourced by run-net-scenarios.sh
+# Scenario: a masked NetworkManager is unmasked and started.
+#
+# Masking NetworkManager makes the generic "isn't running" verdict useless —
+# systemctl can't start a masked unit — so the doctor must name the mask and
+# unmask first. Break by masking; the fix's chain should unmask then start.
+SCENARIO_DESC="masked NetworkManager is unmasked and started"
+SCENARIO_GROUP="control-plane"
+
+scenario_break() {
+ nexec "systemctl mask NetworkManager"
+}
+
+# The verdict the read-only diagnose should produce before any fix runs.
+scenario_diagnose_expect() {
+ ndoctor_json ".outcome" | grep -qx fixable \
+ && ndoctor_json '.report.control_plane.nm_state' | grep -qx masked
+}
+
+scenario_fix() {
+ nfix # net doctor --fix
+}
+
+scenario_assert() {
+ # Unmasked (the state flip, testable anywhere) AND active (needs a real
+ # NetworkManager, so a container without netlink can only show the unmask).
+ ! nexec "systemctl is-enabled NetworkManager | grep -qx masked" \
+ && nexec "systemctl is-active NetworkManager >/dev/null"
+}
diff --git a/scripts/testing/net-scenarios/20-rival-manager.sh b/scripts/testing/net-scenarios/20-rival-manager.sh
new file mode 100644
index 0000000..0d7836b
--- /dev/null
+++ b/scripts/testing/net-scenarios/20-rival-manager.sh
@@ -0,0 +1,27 @@
+# shellcheck shell=bash disable=SC2034 # sourced by run-net-scenarios.sh
+# Scenario: a rival network manager active alongside NetworkManager is disabled.
+#
+# dhcpcd (or systemd-networkd / iwd) running next to NM fights it for the link.
+# Break by enabling dhcpcd; the fix disables it, and the chain resets to
+# reconnect. Requires dhcpcd installed in the target.
+SCENARIO_DESC="an active rival manager (dhcpcd) is disabled"
+SCENARIO_GROUP="control-plane"
+
+scenario_break() {
+ nexec "systemctl enable --now dhcpcd"
+}
+
+scenario_diagnose_expect() {
+ # The verdict fires only when the link is also failing; in a fresh target
+ # with no configured uplink that holds, so the rival should be named.
+ ndoctor_json ".report.control_plane.rivals | index(\"dhcpcd\")" \
+ | grep -vqx null
+}
+
+scenario_fix() {
+ nfix
+}
+
+scenario_assert() {
+ ! nexec "systemctl is-active dhcpcd >/dev/null 2>&1"
+}
diff --git a/scripts/testing/net-scenarios/30-keyfile-perms.sh b/scripts/testing/net-scenarios/30-keyfile-perms.sh
new file mode 100644
index 0000000..8149f87
--- /dev/null
+++ b/scripts/testing/net-scenarios/30-keyfile-perms.sh
@@ -0,0 +1,30 @@
+# shellcheck shell=bash disable=SC2034 # sourced by run-net-scenarios.sh
+# Scenario: a profile keyfile with unsafe permissions is set back to 0600 root.
+#
+# NetworkManager silently ignores a world-readable keyfile, so the profile
+# never activates. Break by chmod 0644 on an existing profile's keyfile; the
+# fix chmods it back to 0600 root. The doctor must run as root to read the
+# keyfile dir, so this scenario runs the doctor with sudo. Requires at least
+# one saved profile in the target (NET_SCENARIO_PROFILE names it).
+SCENARIO_DESC="an unsafe-perms profile keyfile is restored to 0600 root"
+SCENARIO_GROUP="control-plane"
+
+_keyfile() {
+ echo "/etc/NetworkManager/system-connections/${NET_SCENARIO_PROFILE:?set NET_SCENARIO_PROFILE to a saved profile name}.nmconnection"
+}
+
+scenario_break() {
+ nexec "chmod 0644 '$(_keyfile)'"
+}
+
+scenario_diagnose_expect() {
+ ndoctor_json ".report.control_plane.keyfile.perms_ok" | grep -qx false
+}
+
+scenario_fix() {
+ nfix
+}
+
+scenario_assert() {
+ nexec "[ \"\$(stat -c '%a %U' '$(_keyfile)')\" = '600 root' ]"
+}