Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/activation: Add pre-switch checks #236375

Merged
merged 1 commit into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions nixos/modules/system/activation/pre-switch-check.nix
Original file line number Diff line number Diff line change
@@ -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);
};
};
}
16 changes: 14 additions & 2 deletions nixos/modules/system/activation/switch-to-configuration.pl
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@
$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
Expand All @@ -101,6 +102,17 @@
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');
Expand Down
2 changes: 2 additions & 0 deletions nixos/modules/system/activation/switchable-system.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down Expand Up @@ -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}
)
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/system/activation/top-level.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions nixos/tests/switch-test.nix
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,10 @@ in {
other = {
system.switch.enable = true;
users.mutableUsers = true;
specialisation.failingCheck.configuration.system.preSwitchChecks.failEveryTime = ''
echo this will fail
false
'';
};
};

Expand Down Expand Up @@ -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")
Expand Down
41 changes: 40 additions & 1 deletion pkgs/by-name/sw/switch-to-configuration-ng/src/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}"),
})
}
Expand All @@ -105,6 +107,7 @@ impl Into<&'static str> for &Action {
Action::Boot => "boot",
Action::Test => "test",
Action::DryActivate => "dry-activate",
Action::Check => "check",
}
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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")?);
Expand Down Expand Up @@ -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)?;
Expand Down