197 lines
7.6 KiB
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"
|
|
)
|
|
'';
|
|
}
|