nixos/activation: Add pre-switch checks

Add an option for shell script fragments that are ran before switching
to a new NixOS system configuration (pre installation of bootloader or
system activation). Also add a new subcommand for
switch-to-configuration called "check" that will cause the program to
always exit after checks are ran.
This commit is contained in:
Jared Baur 2023-06-06 16:37:46 -07:00
parent b0d941edb4
commit 6e192c4489
No known key found for this signature in database
7 changed files with 111 additions and 3 deletions

View File

@ -1612,6 +1612,7 @@
./services/x11/xserver.nix ./services/x11/xserver.nix
./system/activation/activatable-system.nix ./system/activation/activatable-system.nix
./system/activation/activation-script.nix ./system/activation/activation-script.nix
./system/activation/pre-switch-check.nix
./system/activation/specialisation.nix ./system/activation/specialisation.nix
./system/activation/switchable-system.nix ./system/activation/switchable-system.nix
./system/activation/bootspec.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@"; $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"; 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 switch: make the configuration the boot default and activate now
boot: make the configuration the boot default boot: make the configuration the boot default
test: activate the configuration, but don\'t make it 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 - $!"; flock($stc_lock, LOCK_EX) or die "Could not acquire lock - $!";
openlog("nixos", "", LOG_USER); 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. # Install or update the bootloader.
if ($action eq "switch" || $action eq "boot") { if ($action eq "switch" || $action eq "boot") {
chomp(my $install_boot_loader = <<'EOFBOOTLOADER'); chomp(my $install_boot_loader = <<'EOFBOOTLOADER');

View File

@ -61,6 +61,7 @@ in
--subst-var-by coreutils "${pkgs.coreutils}" \ --subst-var-by coreutils "${pkgs.coreutils}" \
--subst-var-by distroId ${lib.escapeShellArg config.system.nixos.distroId} \ --subst-var-by distroId ${lib.escapeShellArg config.system.nixos.distroId} \
--subst-var-by installBootLoader ${lib.escapeShellArg config.system.build.installBootLoader} \ --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 localeArchive "${config.i18n.glibcLocales}/lib/locale/locale-archive" \
--subst-var-by perl "${perlWrapped}" \ --subst-var-by perl "${perlWrapped}" \
--subst-var-by shell "${pkgs.bash}/bin/sh" \ --subst-var-by shell "${pkgs.bash}/bin/sh" \
@ -93,6 +94,7 @@ in
--set TOPLEVEL ''${!toplevelVar} \ --set TOPLEVEL ''${!toplevelVar} \
--set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \ --set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \
--set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \ --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 LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \
--set SYSTEMD ${config.systemd.package} --set SYSTEMD ${config.systemd.package}
) )

View File

@ -342,6 +342,7 @@ in
perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]); perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
# End if legacy environment variables # End if legacy environment variables
preSwitchCheck = config.system.preSwitchChecks.script;
# Not actually used in the builder. `passedChecks` is just here to create # Not actually used in the builder. `passedChecks` is just here to create
# the build dependencies. Checks are similar to build dependencies in the # the build dependencies. Checks are similar to build dependencies in the

View File

@ -612,6 +612,10 @@ in {
other = { other = {
system.switch.enable = true; system.switch.enable = true;
users.mutableUsers = 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." 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"): with subtest("actions"):
# boot action # boot action
out = switch_to_specialisation("${machine}", "simpleService", action="boot") 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)] #[derive(Debug, Clone, PartialEq)]
enum Action { enum Action {
Switch, Switch,
Check,
Boot, Boot,
Test, Test,
DryActivate, DryActivate,
@ -93,6 +94,7 @@ impl std::str::FromStr for Action {
"boot" => Self::Boot, "boot" => Self::Boot,
"test" => Self::Test, "test" => Self::Test,
"dry-activate" => Self::DryActivate, "dry-activate" => Self::DryActivate,
"check" => Self::Check,
_ => bail!("invalid action {s}"), _ => bail!("invalid action {s}"),
}) })
} }
@ -105,6 +107,7 @@ impl Into<&'static str> for &Action {
Action::Boot => "boot", Action::Boot => "boot",
Action::Test => "test", Action::Test => "test",
Action::DryActivate => "dry-activate", 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<()> { fn do_install_bootloader(command: &str, toplevel: &Path) -> Result<()> {
let mut cmd_split = command.split_whitespace(); let mut cmd_split = command.split_whitespace();
let Some(argv0) = cmd_split.next() else { let Some(argv0) = cmd_split.next() else {
@ -939,7 +964,8 @@ fn do_user_switch(parent_exe: String) -> anyhow::Result<()> {
fn usage(argv0: &str) -> ! { fn usage(argv0: &str) -> ! {
eprintln!( 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 switch: make the configuration the boot default and activate now
boot: make the configuration the boot default boot: make the configuration the boot default
test: activate the configuration, but don't make it 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 out = PathBuf::from(required_env("OUT")?);
let toplevel = PathBuf::from(required_env("TOPLEVEL")?); let toplevel = PathBuf::from(required_env("TOPLEVEL")?);
let distro_id = required_env("DISTRO_ID")?; let distro_id = required_env("DISTRO_ID")?;
let pre_switch_check = required_env("PRE_SWITCH_CHECK")?;
let install_bootloader = required_env("INSTALL_BOOTLOADER")?; let install_bootloader = required_env("INSTALL_BOOTLOADER")?;
let locale_archive = required_env("LOCALE_ARCHIVE")?; let locale_archive = required_env("LOCALE_ARCHIVE")?;
let new_systemd = PathBuf::from(required_env("SYSTEMD")?); 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"); 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. // Install or update the bootloader.
if matches!(action, Action::Switch | Action::Boot) { if matches!(action, Action::Switch | Action::Boot) {
do_install_bootloader(&install_bootloader, &toplevel)?; do_install_bootloader(&install_bootloader, &toplevel)?;