diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 5b85b0c5fad7..6f41d05f856c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1612,6 +1612,7 @@ ./services/x11/xserver.nix ./system/activation/activatable-system.nix ./system/activation/activation-script.nix + ./system/activation/pre-switch-check.nix ./system/activation/specialisation.nix ./system/activation/switchable-system.nix ./system/activation/bootspec.nix diff --git a/nixos/modules/system/activation/pre-switch-check.nix b/nixos/modules/system/activation/pre-switch-check.nix new file mode 100644 index 000000000000..2cbd539a74c8 --- /dev/null +++ b/nixos/modules/system/activation/pre-switch-check.nix @@ -0,0 +1,44 @@ +{ lib, pkgs, ... }: +let + preSwitchCheckScript = + set: + lib.concatLines ( + lib.mapAttrsToList (name: text: '' + # pre-switch check ${name} + ( + ${text} + ) + if [[ $? != 0 ]]; then + echo "Pre-switch check '${name}' failed" + exit 1 + fi + '') set + ); +in +{ + options.system.preSwitchChecks = lib.mkOption { + default = { }; + example = lib.literalExpression '' + { failsEveryTime = + ''' + false + '''; + } + ''; + + description = '' + A set of shell script fragments that are executed before the switch to a + new NixOS system configuration. A failure in any of these fragments will + cause the switch to fail and exit early. + ''; + + type = lib.types.attrsOf lib.types.str; + + apply = + set: + set + // { + script = pkgs.writeShellScript "pre-switch-checks" (preSwitchCheckScript set); + }; + }; +} diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl index 4beca4f0a42a..774e77131f5d 100755 --- a/nixos/modules/system/activation/switch-to-configuration.pl +++ b/nixos/modules/system/activation/switch-to-configuration.pl @@ -78,10 +78,11 @@ if ("@localeArchive@" ne "") { $ENV{LOCALE_ARCHIVE} = "@localeArchive@"; } -if (!defined($action) || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) { +if (!defined($action) || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate" && $action ne "check")) { print STDERR <<"EOF"; -Usage: $0 [switch|boot|test|dry-activate] +Usage: $0 [check|switch|boot|test|dry-activate] +check: run pre-switch checks and exit switch: make the configuration the boot default and activate now boot: make the configuration the boot default test: activate the configuration, but don\'t make it the boot default @@ -101,6 +102,17 @@ open(my $stc_lock, '>>', '/run/nixos/switch-to-configuration.lock') or die "Coul flock($stc_lock, LOCK_EX) or die "Could not acquire lock - $!"; openlog("nixos", "", LOG_USER); +# run pre-switch checks +if (($ENV{"NIXOS_NO_CHECK"} // "") ne "1") { + chomp(my $pre_switch_checks = <<'EOFCHECKS'); +@preSwitchCheck@ +EOFCHECKS + system("$pre_switch_checks $out") == 0 or exit 1; + if ($action eq "check") { + exit 0; + } +} + # Install or update the bootloader. if ($action eq "switch" || $action eq "boot") { chomp(my $install_boot_loader = <<'EOFBOOTLOADER'); diff --git a/nixos/modules/system/activation/switchable-system.nix b/nixos/modules/system/activation/switchable-system.nix index d1326a18e5fe..b4f153f7755e 100644 --- a/nixos/modules/system/activation/switchable-system.nix +++ b/nixos/modules/system/activation/switchable-system.nix @@ -61,6 +61,7 @@ in --subst-var-by coreutils "${pkgs.coreutils}" \ --subst-var-by distroId ${lib.escapeShellArg config.system.nixos.distroId} \ --subst-var-by installBootLoader ${lib.escapeShellArg config.system.build.installBootLoader} \ + --subst-var-by preSwitchCheck ${lib.escapeShellArg config.system.preSwitchChecks.script} \ --subst-var-by localeArchive "${config.i18n.glibcLocales}/lib/locale/locale-archive" \ --subst-var-by perl "${perlWrapped}" \ --subst-var-by shell "${pkgs.bash}/bin/sh" \ @@ -93,6 +94,7 @@ in --set TOPLEVEL ''${!toplevelVar} \ --set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \ --set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \ + --set PRE_SWITCH_CHECK ${lib.escapeShellArg config.system.preSwitchChecks.script} \ --set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \ --set SYSTEMD ${config.systemd.package} ) diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix index 6abbd4b673c0..47868a6862ff 100644 --- a/nixos/modules/system/activation/top-level.nix +++ b/nixos/modules/system/activation/top-level.nix @@ -342,6 +342,7 @@ in perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]); # End if legacy environment variables + preSwitchCheck = config.system.preSwitchChecks.script; # Not actually used in the builder. `passedChecks` is just here to create # the build dependencies. Checks are similar to build dependencies in the diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix index a55155579b4b..c48a3963a79e 100644 --- a/nixos/tests/switch-test.nix +++ b/nixos/tests/switch-test.nix @@ -612,6 +612,10 @@ in { other = { system.switch.enable = true; users.mutableUsers = true; + specialisation.failingCheck.configuration.system.preSwitchChecks.failEveryTime = '' + echo this will fail + false + ''; }; }; @@ -684,6 +688,11 @@ in { boot_loader_text = "Warning: do not know how to make this configuration bootable; please enable a boot loader." + with subtest("pre-switch checks"): + machine.succeed("${stderrRunner} ${otherSystem}/bin/switch-to-configuration check") + out = switch_to_specialisation("${otherSystem}", "failingCheck", action="check", fail=True) + assert_contains(out, "this will fail") + with subtest("actions"): # boot action out = switch_to_specialisation("${machine}", "simpleService", action="boot") diff --git a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs index 61932cb55591..0c4ccbec89fc 100644 --- a/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs +++ b/pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs @@ -79,6 +79,7 @@ const DRY_RELOAD_BY_ACTIVATION_LIST_FILE: &str = "/run/nixos/dry-activation-relo #[derive(Debug, Clone, PartialEq)] enum Action { Switch, + Check, Boot, Test, DryActivate, @@ -93,6 +94,7 @@ impl std::str::FromStr for Action { "boot" => Self::Boot, "test" => Self::Test, "dry-activate" => Self::DryActivate, + "check" => Self::Check, _ => bail!("invalid action {s}"), }) } @@ -105,6 +107,7 @@ impl Into<&'static str> for &Action { Action::Boot => "boot", Action::Test => "test", Action::DryActivate => "dry-activate", + Action::Check => "check", } } } @@ -129,6 +132,28 @@ fn parse_os_release() -> Result> { })) } +fn do_pre_switch_check(command: &str, toplevel: &Path) -> Result<()> { + let mut cmd_split = command.split_whitespace(); + let Some(argv0) = cmd_split.next() else { + bail!("missing first argument in install bootloader commands"); + }; + + match std::process::Command::new(argv0) + .args(cmd_split.collect::>()) + .arg(toplevel) + .spawn() + .map(|mut child| child.wait()) + { + Ok(Ok(status)) if status.success() => {} + _ => { + eprintln!("Pre-switch checks failed"); + die() + } + } + + Ok(()) +} + fn do_install_bootloader(command: &str, toplevel: &Path) -> Result<()> { let mut cmd_split = command.split_whitespace(); let Some(argv0) = cmd_split.next() else { @@ -939,7 +964,8 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> { fn usage(argv0: &str) -> ! { eprintln!( - r#"Usage: {} [switch|boot|test|dry-activate] + r#"Usage: {} [check|switch|boot|test|dry-activate] +check: run pre-switch checks and exit switch: make the configuration the boot default and activate now boot: make the configuration the boot default test: activate the configuration, but don't make it the boot default @@ -955,6 +981,7 @@ fn do_system_switch(action: Action) -> anyhow::Result<()> { let out = PathBuf::from(required_env("OUT")?); let toplevel = PathBuf::from(required_env("TOPLEVEL")?); let distro_id = required_env("DISTRO_ID")?; + let pre_switch_check = required_env("PRE_SWITCH_CHECK")?; let install_bootloader = required_env("INSTALL_BOOTLOADER")?; let locale_archive = required_env("LOCALE_ARCHIVE")?; let new_systemd = PathBuf::from(required_env("SYSTEMD")?); @@ -1013,6 +1040,18 @@ fn do_system_switch(action: Action) -> anyhow::Result<()> { bail!("Failed to initialize logger"); } + if std::env::var("NIXOS_NO_CHECK") + .as_deref() + .unwrap_or_default() + != "1" + { + do_pre_switch_check(&pre_switch_check, &toplevel)?; + } + + if *action == Action::Check { + return Ok(()); + } + // Install or update the bootloader. if matches!(action, Action::Switch | Action::Boot) { do_install_bootloader(&install_bootloader, &toplevel)?;