Merge pull request #237040 from roberth/flexible-activation

nixos/system: Support pre-activated images
This commit is contained in:
Robert Hensing 2023-07-08 16:06:25 +02:00 committed by GitHub
commit 3fd4ac8e82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 183 additions and 74 deletions

View file

@ -1340,6 +1340,7 @@
./services/x11/xbanish.nix ./services/x11/xbanish.nix
./services/x11/xfs.nix ./services/x11/xfs.nix
./services/x11/xserver.nix ./services/x11/xserver.nix
./system/activation/activatable-system.nix
./system/activation/activation-script.nix ./system/activation/activation-script.nix
./system/activation/specialisation.nix ./system/activation/specialisation.nix
./system/activation/bootspec.nix ./system/activation/bootspec.nix

View file

@ -0,0 +1,92 @@
{ config, lib, pkgs, ... }:
let
inherit (lib)
mkOption
optionalString
types
;
perlWrapped = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
systemBuilderArgs = {
activationScript = config.system.activationScripts.script;
dryActivationScript = config.system.dryActivationScript;
};
systemBuilderCommands = ''
echo "$activationScript" > $out/activate
echo "$dryActivationScript" > $out/dry-activate
substituteInPlace $out/activate --subst-var-by out ''${!toplevelVar}
substituteInPlace $out/dry-activate --subst-var-by out ''${!toplevelVar}
chmod u+x $out/activate $out/dry-activate
unset activationScript dryActivationScript
mkdir $out/bin
substitute ${./switch-to-configuration.pl} $out/bin/switch-to-configuration \
--subst-var out \
--subst-var-by toplevel ''${!toplevelVar} \
--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 localeArchive "${config.i18n.glibcLocales}/lib/locale/locale-archive" \
--subst-var-by perl "${perlWrapped}" \
--subst-var-by shell "${pkgs.bash}/bin/sh" \
--subst-var-by su "${pkgs.shadow.su}/bin/su" \
--subst-var-by systemd "${config.systemd.package}" \
--subst-var-by utillinux "${pkgs.util-linux}" \
;
chmod +x $out/bin/switch-to-configuration
${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
if ! output=$(${perlWrapped}/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
echo "switch-to-configuration syntax is not valid:"
echo "$output"
exit 1
fi
''}
'';
in
{
options = {
system.activatable = mkOption {
type = types.bool;
default = true;
description = ''
Whether to add the activation script to the system profile.
The default, to have the script available all the time, is what we normally
do, but for image based systems, this may not be needed or not be desirable.
'';
};
system.build.separateActivationScript = mkOption {
type = types.package;
description = ''
A separate activation script package that's not part of the system profile.
This is useful for configurations where `system.activatable` is `false`.
Otherwise, you can just use `system.build.toplevel`.
'';
};
};
config = {
system.systemBuilderCommands = lib.mkIf config.system.activatable systemBuilderCommands;
system.systemBuilderArgs = lib.mkIf config.system.activatable
(systemBuilderArgs // {
toplevelVar = "out";
});
system.build.separateActivationScript =
pkgs.runCommand
"separate-activation-script"
(systemBuilderArgs // {
toplevelVar = "toplevel";
toplevel = config.system.build.toplevel;
})
''
mkdir $out
${systemBuilderCommands}
'';
};
}

View file

@ -204,6 +204,27 @@ in
`/usr/bin/env`. `/usr/bin/env`.
''; '';
}; };
system.build.installBootLoader = mkOption {
internal = true;
# "; true" => make the `$out` argument from switch-to-configuration.pl
# go to `true` instead of `echo`, hiding the useless path
# from the log.
default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
description = lib.mdDoc ''
A program that writes a bootloader installation script to the path passed in the first command line argument.
See `nixos/modules/system/activation/switch-to-configuration.pl`.
'';
type = types.unique {
message = ''
Only one bootloader can be enabled at a time. This requirement has not
been checked until NixOS 22.05. Earlier versions defaulted to the last
definition. Change your configuration to enable only one bootloader.
'';
} (types.either types.str types.package);
};
}; };

View file

@ -31,8 +31,10 @@ use Cwd qw(abs_path);
## no critic(ValuesAndExpressions::ProhibitNoisyQuotes, ValuesAndExpressions::ProhibitMagicNumbers, ValuesAndExpressions::ProhibitEmptyQuotes, ValuesAndExpressions::ProhibitInterpolationOfLiterals) ## no critic(ValuesAndExpressions::ProhibitNoisyQuotes, ValuesAndExpressions::ProhibitMagicNumbers, ValuesAndExpressions::ProhibitEmptyQuotes, ValuesAndExpressions::ProhibitInterpolationOfLiterals)
## no critic(RegularExpressions::ProhibitEscapedMetacharacters) ## no critic(RegularExpressions::ProhibitEscapedMetacharacters)
# System closure path to switch to # Location of activation scripts
my $out = "@out@"; my $out = "@out@";
# System closure path to switch to
my $toplevel = "@toplevel@";
# Path to the directory containing systemd tools of the old system # Path to the directory containing systemd tools of the old system
my $cur_systemd = abs_path("/run/current-system/sw/bin"); my $cur_systemd = abs_path("/run/current-system/sw/bin");
# Path to the systemd store path of the new system # Path to the systemd store path of the new system
@ -96,7 +98,7 @@ if ($action eq "switch" || $action eq "boot") {
chomp(my $install_boot_loader = <<'EOFBOOTLOADER'); chomp(my $install_boot_loader = <<'EOFBOOTLOADER');
@installBootLoader@ @installBootLoader@
EOFBOOTLOADER EOFBOOTLOADER
system("$install_boot_loader $out") == 0 or exit 1; system("$install_boot_loader $toplevel") == 0 or exit 1;
} }
# Just in case the new configuration hangs the system, do a sync now. # Just in case the new configuration hangs the system, do a sync now.
@ -110,7 +112,7 @@ if ($action eq "boot") {
# Check if we can activate the new configuration. # Check if we can activate the new configuration.
my $cur_init_interface_version = read_file("/run/current-system/init-interface-version", err_mode => "quiet") // ""; my $cur_init_interface_version = read_file("/run/current-system/init-interface-version", err_mode => "quiet") // "";
my $new_init_interface_version = read_file("$out/init-interface-version"); my $new_init_interface_version = read_file("$toplevel/init-interface-version");
if ($new_init_interface_version ne $cur_init_interface_version) { if ($new_init_interface_version ne $cur_init_interface_version) {
print STDERR <<'EOF'; print STDERR <<'EOF';
@ -477,7 +479,7 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
$units_to_stop->{$socket} = 1; $units_to_stop->{$socket} = 1;
# Only restart sockets that actually # Only restart sockets that actually
# exist in new configuration: # exist in new configuration:
if (-e "$out/etc/systemd/system/$socket") { if (-e "$toplevel/etc/systemd/system/$socket") {
$units_to_start->{$socket} = 1; $units_to_start->{$socket} = 1;
if ($units_to_start eq $units_to_restart) { if ($units_to_start eq $units_to_restart) {
record_unit($restart_list_file, $socket); record_unit($restart_list_file, $socket);
@ -539,13 +541,13 @@ while (my ($unit, $state) = each(%{$active_cur})) {
my $base_unit = $unit; my $base_unit = $unit;
my $cur_unit_file = "/etc/systemd/system/$base_unit"; my $cur_unit_file = "/etc/systemd/system/$base_unit";
my $new_unit_file = "$out/etc/systemd/system/$base_unit"; my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
# Detect template instances. # Detect template instances.
if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) { if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$base_unit = "$1\@.$2"; $base_unit = "$1\@.$2";
$cur_unit_file = "/etc/systemd/system/$base_unit"; $cur_unit_file = "/etc/systemd/system/$base_unit";
$new_unit_file = "$out/etc/systemd/system/$base_unit"; $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
} }
my $base_name = $base_unit; my $base_name = $base_unit;
@ -626,7 +628,7 @@ sub path_to_unit_name {
# we generated units for all mounts; then we could unify this with the # we generated units for all mounts; then we could unify this with the
# unit checking code above. # unit checking code above.
my ($cur_fss, $cur_swaps) = parse_fstab("/etc/fstab"); my ($cur_fss, $cur_swaps) = parse_fstab("/etc/fstab");
my ($new_fss, $new_swaps) = parse_fstab("$out/etc/fstab"); my ($new_fss, $new_swaps) = parse_fstab("$toplevel/etc/fstab");
foreach my $mount_point (keys(%{$cur_fss})) { foreach my $mount_point (keys(%{$cur_fss})) {
my $cur = $cur_fss->{$mount_point}; my $cur = $cur_fss->{$mount_point};
my $new = $new_fss->{$mount_point}; my $new = $new_fss->{$mount_point};
@ -670,7 +672,7 @@ foreach my $device (keys(%{$cur_swaps})) {
my $cur_pid1_path = abs_path("/proc/1/exe") // "/unknown"; my $cur_pid1_path = abs_path("/proc/1/exe") // "/unknown";
my $cur_systemd_system_config = abs_path("/etc/systemd/system.conf") // "/unknown"; my $cur_systemd_system_config = abs_path("/etc/systemd/system.conf") // "/unknown";
my $new_pid1_path = abs_path("$new_systemd/lib/systemd/systemd") or die; my $new_pid1_path = abs_path("$new_systemd/lib/systemd/systemd") or die;
my $new_systemd_system_config = abs_path("$out/etc/systemd/system.conf") // "/unknown"; my $new_systemd_system_config = abs_path("$toplevel/etc/systemd/system.conf") // "/unknown";
my $restart_systemd = $cur_pid1_path ne $new_pid1_path; my $restart_systemd = $cur_pid1_path ne $new_pid1_path;
if ($cur_systemd_system_config ne $new_systemd_system_config) { if ($cur_systemd_system_config ne $new_systemd_system_config) {
@ -709,12 +711,12 @@ if ($action eq "dry-activate") {
foreach (split(/\n/msx, read_file($dry_restart_by_activation_file, err_mode => "quiet") // "")) { foreach (split(/\n/msx, read_file($dry_restart_by_activation_file, err_mode => "quiet") // "")) {
my $unit = $_; my $unit = $_;
my $base_unit = $unit; my $base_unit = $unit;
my $new_unit_file = "$out/etc/systemd/system/$base_unit"; my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
# Detect template instances. # Detect template instances.
if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) { if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$base_unit = "$1\@.$2"; $base_unit = "$1\@.$2";
$new_unit_file = "$out/etc/systemd/system/$base_unit"; $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
} }
my $base_name = $base_unit; my $base_name = $base_unit;
@ -757,7 +759,7 @@ if ($action eq "dry-activate") {
} }
syslog(LOG_NOTICE, "switching to system configuration $out"); syslog(LOG_NOTICE, "switching to system configuration $toplevel");
if (scalar(keys(%units_to_stop)) > 0) { if (scalar(keys(%units_to_stop)) > 0) {
if (scalar(@units_to_stop_filtered)) { if (scalar(@units_to_stop_filtered)) {
@ -781,12 +783,12 @@ system("$out/activate", "$out") == 0 or $res = 2;
foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quiet") // "")) { foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quiet") // "")) {
my $unit = $_; my $unit = $_;
my $base_unit = $unit; my $base_unit = $unit;
my $new_unit_file = "$out/etc/systemd/system/$base_unit"; my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
# Detect template instances. # Detect template instances.
if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) { if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$base_unit = "$1\@.$2"; $base_unit = "$1\@.$2";
$new_unit_file = "$out/etc/systemd/system/$base_unit"; $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
} }
my $base_name = $base_unit; my $base_name = $base_unit;
@ -857,7 +859,7 @@ if (scalar(keys(%units_to_reload)) > 0) {
for my $unit (keys(%units_to_reload)) { for my $unit (keys(%units_to_reload)) {
if (!unit_is_active($unit)) { if (!unit_is_active($unit)) {
# Figure out if we need to start the unit # Figure out if we need to start the unit
my %unit_info = parse_unit("$out/etc/systemd/system/$unit"); my %unit_info = parse_unit("$toplevel/etc/systemd/system/$unit");
if (!(parse_systemd_bool(\%unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%unit_info, "Unit", "X-OnlyManualStart", 0))) { if (!(parse_systemd_bool(\%unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%unit_info, "Unit", "X-OnlyManualStart", 0))) {
$units_to_start{$unit} = 1; $units_to_start{$unit} = 1;
record_unit($start_list_file, $unit); record_unit($start_list_file, $unit);
@ -940,9 +942,9 @@ if (scalar(@failed) > 0) {
} }
if ($res == 0) { if ($res == 0) {
syslog(LOG_NOTICE, "finished switching to system configuration $out"); syslog(LOG_NOTICE, "finished switching to system configuration $toplevel");
} else { } else {
syslog(LOG_ERR, "switching to system configuration $out failed (status $res)"); syslog(LOG_ERR, "switching to system configuration $toplevel failed (status $res)");
} }
exit($res); exit($res);

View file

@ -36,13 +36,6 @@ let
ln -s ${config.hardware.firmware}/lib/firmware $out/firmware ln -s ${config.hardware.firmware}/lib/firmware $out/firmware
''} ''}
echo "$activationScript" > $out/activate
echo "$dryActivationScript" > $out/dry-activate
substituteInPlace $out/activate --subst-var out
substituteInPlace $out/dry-activate --subst-var out
chmod u+x $out/activate $out/dry-activate
unset activationScript dryActivationScript
${if config.boot.initrd.systemd.enable then '' ${if config.boot.initrd.systemd.enable then ''
cp ${config.system.build.bootStage2} $out/prepare-root cp ${config.system.build.bootStage2} $out/prepare-root
substituteInPlace $out/prepare-root --subst-var-by systemConfig $out substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
@ -63,19 +56,6 @@ let
echo -n "$nixosLabel" > $out/nixos-version echo -n "$nixosLabel" > $out/nixos-version
echo -n "${config.boot.kernelPackages.stdenv.hostPlatform.system}" > $out/system echo -n "${config.boot.kernelPackages.stdenv.hostPlatform.system}" > $out/system
mkdir $out/bin
export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive"
export distroId=${config.system.nixos.distroId};
substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration
chmod +x $out/bin/switch-to-configuration
${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
if ! output=$($perl/bin/perl -c $out/bin/switch-to-configuration 2>&1); then
echo "switch-to-configuration syntax is not valid:"
echo "$output"
exit 1
fi
''}
${config.system.systemBuilderCommands} ${config.system.systemBuilderCommands}
cp "$extraDependenciesPath" "$out/extra-dependencies" cp "$extraDependenciesPath" "$out/extra-dependencies"
@ -93,7 +73,7 @@ let
# symlinks to the various parts of the built configuration (the # symlinks to the various parts of the built configuration (the
# kernel, systemd units, init scripts, etc.) as well as a script # kernel, systemd units, init scripts, etc.) as well as a script
# `switch-to-configuration' that activates the configuration and # `switch-to-configuration' that activates the configuration and
# makes it bootable. # makes it bootable. See `activatable-system.nix`.
baseSystem = pkgs.stdenvNoCC.mkDerivation ({ baseSystem = pkgs.stdenvNoCC.mkDerivation ({
name = "nixos-system-${config.system.name}-${config.system.nixos.label}"; name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
preferLocalBuild = true; preferLocalBuild = true;
@ -101,22 +81,12 @@ let
passAsFile = [ "extraDependencies" ]; passAsFile = [ "extraDependencies" ];
buildCommand = systemBuilder; buildCommand = systemBuilder;
inherit (pkgs) coreutils;
systemd = config.systemd.package; systemd = config.systemd.package;
shell = "${pkgs.bash}/bin/sh";
su = "${pkgs.shadow.su}/bin/su";
utillinux = pkgs.util-linux;
kernelParams = config.boot.kernelParams; kernelParams = config.boot.kernelParams;
installBootLoader = config.system.build.installBootLoader;
activationScript = config.system.activationScripts.script;
dryActivationScript = config.system.dryActivationScript;
nixosLabel = config.system.nixos.label; nixosLabel = config.system.nixos.label;
inherit (config.system) extraDependencies; inherit (config.system) extraDependencies;
# Needed by switch-to-configuration.
perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
} // config.system.systemBuilderArgs); } // config.system.systemBuilderArgs);
# Handle assertions and warnings # Handle assertions and warnings
@ -178,26 +148,6 @@ in
}; };
system.build = { system.build = {
installBootLoader = mkOption {
internal = true;
# "; true" => make the `$out` argument from switch-to-configuration.pl
# go to `true` instead of `echo`, hiding the useless path
# from the log.
default = "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
description = lib.mdDoc ''
A program that writes a bootloader installation script to the path passed in the first command line argument.
See `nixos/modules/system/activation/switch-to-configuration.pl`.
'';
type = types.unique {
message = ''
Only one bootloader can be enabled at a time. This requirement has not
been checked until NixOS 22.05. Earlier versions defaulted to the last
definition. Change your configuration to enable only one bootloader.
'';
} (types.either types.str types.package);
};
toplevel = mkOption { toplevel = mkOption {
type = types.package; type = types.package;
readOnly = true; readOnly = true;
@ -380,6 +330,16 @@ in
''; '';
system.systemBuilderArgs = { system.systemBuilderArgs = {
# Legacy environment variables. These were used by the activation script,
# but some other script might still depend on them, although unlikely.
installBootLoader = config.system.build.installBootLoader;
localeArchive = "${config.i18n.glibcLocales}/lib/locale/locale-archive";
distroId = config.system.nixos.distroId;
perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
# End if legacy environment variables
# 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
# sense that if they fail, the system build fails. However, checks do not # sense that if they fail, the system build fails. However, checks do not

View file

@ -70,6 +70,19 @@ in {
}; };
}; };
simpleServiceSeparateActivationScript.configuration = {
system.activatable = false;
systemd.services.test = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/true";
ExecReload = "${pkgs.coreutils}/bin/true";
};
};
};
simpleServiceDifferentDescription.configuration = { simpleServiceDifferentDescription.configuration = {
imports = [ simpleService.configuration ]; imports = [ simpleService.configuration ];
systemd.services.test.description = "Test unit"; systemd.services.test.description = "Test unit";
@ -482,9 +495,9 @@ in {
}; };
testScript = { nodes, ... }: let testScript = { nodes, ... }: let
originalSystem = nodes.machine.config.system.build.toplevel; originalSystem = nodes.machine.system.build.toplevel;
otherSystem = nodes.other.config.system.build.toplevel; otherSystem = nodes.other.system.build.toplevel;
machine = nodes.machine.config.system.build.toplevel; machine = nodes.machine.system.build.toplevel;
# Ensures failures pass through using pipefail, otherwise failing to # Ensures failures pass through using pipefail, otherwise failing to
# switch-to-configuration is hidden by the success of `tee`. # switch-to-configuration is hidden by the success of `tee`.
@ -497,11 +510,15 @@ in {
in /* python */ '' in /* python */ ''
def switch_to_specialisation(system, name, action="test", fail=False): def switch_to_specialisation(system, name, action="test", fail=False):
if name == "": if name == "":
stc = f"{system}/bin/switch-to-configuration" switcher = f"{system}/bin/switch-to-configuration"
else: else:
stc = f"{system}/specialisation/{name}/bin/switch-to-configuration" switcher = f"{system}/specialisation/{name}/bin/switch-to-configuration"
out = machine.fail(f"{stc} {action} 2>&1") if fail \ return run_switch(switcher, action, fail)
else machine.succeed(f"{stc} {action} 2>&1")
# like above but stc = switcher
def run_switch(switcher, action="test", fail=False):
out = machine.fail(f"{switcher} {action} 2>&1") if fail \
else machine.succeed(f"{switcher} {action} 2>&1")
assert_lacks(out, "switch-to-configuration line") # Perl warnings assert_lacks(out, "switch-to-configuration line") # Perl warnings
return out return out
@ -639,6 +656,22 @@ in {
assert_lacks(out, "the following new units were started:") assert_lacks(out, "the following new units were started:")
assert_contains(out, "would start the following units: test.service\n") assert_contains(out, "would start the following units: test.service\n")
out = switch_to_specialisation("${machine}", "", action="test")
# Ensure the service can be started when the activation script isn't in toplevel
# This is a lot like "Start a simple service", except activation-only deps could be gc-ed
out = run_switch("${nodes.machine.specialisation.simpleServiceSeparateActivationScript.configuration.system.build.separateActivationScript}/bin/switch-to-configuration");
assert_lacks(out, "installing dummy bootloader") # test does not install a bootloader
assert_lacks(out, "stopping the following units:")
assert_lacks(out, "NOT restarting the following changed units:")
assert_contains(out, "reloading the following units: dbus.service\n") # huh
assert_lacks(out, "\nrestarting the following units:")
assert_lacks(out, "\nstarting the following units:")
assert_contains(out, "the following new units were started: test.service\n")
machine.succeed("! test -e /run/current-system/activate")
machine.succeed("! test -e /run/current-system/dry-activate")
machine.succeed("! test -e /run/current-system/bin/switch-to-configuration")
# Ensure \ works in unit names # Ensure \ works in unit names
out = switch_to_specialisation("${machine}", "unitWithBackslash") out = switch_to_specialisation("${machine}", "unitWithBackslash")
assert_contains(out, "stopping the following units: test.service\n") assert_contains(out, "stopping the following units: test.service\n")