Files
nixos/tests/deploy-finalize.nix

197 lines
7.6 KiB
Nix

# Test for modules/server-deploy-finalize.nix.
#
# Covers the decision and scheduling logic with fabricated profile directories,
# since spawning a second booted NixOS toplevel to diff kernels is too heavy for
# a runNixOSTest. We rely on the shellcheck pass baked into writeShellApplication
# to catch syntax regressions in the script itself.
{
lib,
pkgs,
inputs,
...
}:
pkgs.testers.runNixOSTest {
name = "deploy-finalize";
node.specialArgs = {
inherit inputs lib;
username = "testuser";
};
nodes.machine =
{ ... }:
{
imports = [
../modules/server-deploy-finalize.nix
];
services.deployFinalize = {
enable = true;
# Shorter default in the test to make expected-substring assertions
# stable and reinforce that the option is wired through.
delay = 15;
};
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
# Test fixtures: fabricated profile trees whose kernel/initrd/kernel-modules
# symlinks are under test control. `readlink -e` requires the targets to
# exist, so we point at real files in /tmp rather than non-existent paths.
machine.succeed(
"mkdir -p /tmp/profile-same /tmp/profile-changed-kernel "
"/tmp/profile-changed-initrd /tmp/profile-changed-modules "
"/tmp/profile-missing /tmp/fake-targets"
)
machine.succeed(
"touch /tmp/fake-targets/alt-kernel /tmp/fake-targets/alt-initrd "
"/tmp/fake-targets/alt-modules"
)
booted_kernel = machine.succeed("readlink -e /run/booted-system/kernel").strip()
booted_initrd = machine.succeed("readlink -e /run/booted-system/initrd").strip()
booted_modules = machine.succeed("readlink -e /run/booted-system/kernel-modules").strip()
def link_profile(path, kernel, initrd, modules):
machine.succeed(f"ln -sf {kernel} {path}/kernel")
machine.succeed(f"ln -sf {initrd} {path}/initrd")
machine.succeed(f"ln -sf {modules} {path}/kernel-modules")
# profile-same: matches booted exactly should choose `switch`.
link_profile("/tmp/profile-same", booted_kernel, booted_initrd, booted_modules)
machine.succeed("mkdir -p /tmp/profile-same/bin")
machine.succeed(
"ln -sf /run/current-system/bin/switch-to-configuration "
"/tmp/profile-same/bin/switch-to-configuration"
)
# profile-changed-kernel: kernel differs only should choose `reboot`.
link_profile(
"/tmp/profile-changed-kernel",
"/tmp/fake-targets/alt-kernel",
booted_initrd,
booted_modules,
)
# profile-changed-initrd: initrd differs only should choose `reboot`.
link_profile(
"/tmp/profile-changed-initrd",
booted_kernel,
"/tmp/fake-targets/alt-initrd",
booted_modules,
)
# profile-changed-modules: kernel-modules differs only should choose `reboot`.
# Catches the obelisk PR / nixpkgs auto-upgrade case where modules rebuild
# against the same kernel but ABI-incompatible.
link_profile(
"/tmp/profile-changed-modules",
booted_kernel,
booted_initrd,
"/tmp/fake-targets/alt-modules",
)
# profile-missing: no kernel/initrd/kernel-modules should fail closed.
with subtest("dry-run against identical profile selects switch"):
rc, out = machine.execute(
"deploy-finalize --dry-run --profile /tmp/profile-same 2>&1"
)
assert rc == 0, f"rc={rc}\n{out}"
assert "action=switch" in out, out
assert "services only" in out, out
assert "dry-run not scheduling" in out, out
assert "would run: /tmp/profile-same/bin/switch-to-configuration switch" in out, out
assert "would schedule: systemd-run" in out, out
with subtest("dry-run against changed-kernel profile selects reboot"):
rc, out = machine.execute(
"deploy-finalize --dry-run --profile /tmp/profile-changed-kernel 2>&1"
)
assert rc == 0, f"rc={rc}\n{out}"
assert "action=reboot" in out, out
assert "reason=kernel changed" in out, out
assert "systemctl reboot" in out, out
with subtest("dry-run against changed-initrd profile selects reboot"):
rc, out = machine.execute(
"deploy-finalize --dry-run --profile /tmp/profile-changed-initrd 2>&1"
)
assert rc == 0, f"rc={rc}\n{out}"
assert "action=reboot" in out, out
assert "reason=initrd changed" in out, out
with subtest("dry-run against changed-modules profile selects reboot"):
rc, out = machine.execute(
"deploy-finalize --dry-run --profile /tmp/profile-changed-modules 2>&1"
)
assert rc == 0, f"rc={rc}\n{out}"
assert "action=reboot" in out, out
assert "reason=kernel-modules changed" in out, out
with subtest("dry-run against empty profile fails closed with rc=1"):
rc, out = machine.execute(
"deploy-finalize --dry-run --profile /tmp/profile-missing 2>&1"
)
assert rc == 1, f"rc={rc}\n{out}"
assert "missing kernel, initrd, or kernel-modules" in out, out
with subtest("--delay override is reflected in output"):
rc, out = machine.execute(
"deploy-finalize --dry-run --delay 7 --profile /tmp/profile-same 2>&1"
)
assert rc == 0, f"rc={rc}\n{out}"
assert "delay=7s" in out, out
with subtest("configured default delay from module option is used"):
rc, out = machine.execute(
"deploy-finalize --dry-run --profile /tmp/profile-same 2>&1"
)
assert rc == 0, f"rc={rc}\n{out}"
# module option delay=15 in nodes.machine above.
assert "delay=15s" in out, out
with subtest("unknown option rejected with rc=2"):
rc, out = machine.execute("deploy-finalize --bogus 2>&1")
assert rc == 2, f"rc={rc}\n{out}"
assert "unknown option --bogus" in out, out
with subtest("non-dry run arms a transient systemd timer"):
# Long delay so the timer doesn't fire during the test. We stop it
# explicitly afterwards.
rc, out = machine.execute(
"deploy-finalize --delay 3600 --profile /tmp/profile-same 2>&1"
)
assert rc == 0, f"scheduling rc={rc}\n{out}"
# Confirm exactly one transient timer is active.
timers = machine.succeed(
"systemctl list-units --type=timer --no-legend 'deploy-finalize-*.timer' "
"--state=waiting | awk 'NF{print $1}'"
).strip().splitlines()
assert len(timers) == 1, f"expected exactly one pending timer, got {timers}"
assert timers[0].startswith("deploy-finalize-"), timers
with subtest("back-to-back scheduling cancels the previous timer"):
# The previous subtest left one timer armed. Schedule again; the old
# one should be stopped before the new unit name is created.
machine.succeed("sleep 1") # ensure a distinct unit-name timestamp
rc, out = machine.execute(
"deploy-finalize --delay 3600 --profile /tmp/profile-same 2>&1"
)
assert rc == 0, f"second-schedule rc={rc}\n{out}"
timers = machine.succeed(
"systemctl list-units --type=timer --no-legend 'deploy-finalize-*.timer' "
"--state=waiting | awk 'NF{print $1}'"
).strip().splitlines()
assert len(timers) == 1, f"expected only the new timer, got {timers}"
# Clean up so the test's shutdown path is quiet.
machine.succeed(
"systemctl stop 'deploy-finalize-*.timer' 'deploy-finalize-*.service' "
"2>/dev/null || true"
)
'';
}