[Backport release-24.11] nixos/activation: Add pre-switch checks (#358286)

This commit is contained in:
Connor Baker 2024-11-22 17:43:17 -08:00 committed by GitHub
commit 618c7a6a0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 111 additions and 3 deletions

View File

@ -1614,6 +1614,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

View File

@ -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);
};
};
}

View File

@ -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');

View File

@ -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}
)

View File

@ -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

View File

@ -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")

View File

@ -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<HashMap<String, String>> {
}))
}
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::<Vec<&str>>())
.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)?;