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/xfs.nix
./services/x11/xserver.nix
./system/activation/activatable-system.nix
./system/activation/activation-script.nix
./system/activation/specialisation.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`.
'';
};
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(RegularExpressions::ProhibitEscapedMetacharacters)
# System closure path to switch to
# Location of activation scripts
my $out = "@out@";
# System closure path to switch to
my $toplevel = "@toplevel@";
# Path to the directory containing systemd tools of the old system
my $cur_systemd = abs_path("/run/current-system/sw/bin");
# 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');
@installBootLoader@
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.
@ -110,7 +112,7 @@ if ($action eq "boot") {
# 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 $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) {
print STDERR <<'EOF';
@ -477,7 +479,7 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
$units_to_stop->{$socket} = 1;
# Only restart sockets that actually
# exist in new configuration:
if (-e "$out/etc/systemd/system/$socket") {
if (-e "$toplevel/etc/systemd/system/$socket") {
$units_to_start->{$socket} = 1;
if ($units_to_start eq $units_to_restart) {
record_unit($restart_list_file, $socket);
@ -539,13 +541,13 @@ while (my ($unit, $state) = each(%{$active_cur})) {
my $base_unit = $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.
if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$base_unit = "$1\@.$2";
$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;
@ -626,7 +628,7 @@ sub path_to_unit_name {
# we generated units for all mounts; then we could unify this with the
# unit checking code above.
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})) {
my $cur = $cur_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_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_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;
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") // "")) {
my $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.
if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$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;
@ -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(@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") // "")) {
my $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.
if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
$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;
@ -857,7 +859,7 @@ if (scalar(keys(%units_to_reload)) > 0) {
for my $unit (keys(%units_to_reload)) {
if (!unit_is_active($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))) {
$units_to_start{$unit} = 1;
record_unit($start_list_file, $unit);
@ -940,9 +942,9 @@ if (scalar(@failed) > 0) {
}
if ($res == 0) {
syslog(LOG_NOTICE, "finished switching to system configuration $out");
syslog(LOG_NOTICE, "finished switching to system configuration $toplevel");
} 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);

View file

@ -36,13 +36,6 @@ let
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 ''
cp ${config.system.build.bootStage2} $out/prepare-root
substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
@ -63,19 +56,6 @@ let
echo -n "$nixosLabel" > $out/nixos-version
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}
cp "$extraDependenciesPath" "$out/extra-dependencies"
@ -93,7 +73,7 @@ let
# symlinks to the various parts of the built configuration (the
# kernel, systemd units, init scripts, etc.) as well as a script
# `switch-to-configuration' that activates the configuration and
# makes it bootable.
# makes it bootable. See `activatable-system.nix`.
baseSystem = pkgs.stdenvNoCC.mkDerivation ({
name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
preferLocalBuild = true;
@ -101,22 +81,12 @@ let
passAsFile = [ "extraDependencies" ];
buildCommand = systemBuilder;
inherit (pkgs) coreutils;
systemd = config.systemd.package;
shell = "${pkgs.bash}/bin/sh";
su = "${pkgs.shadow.su}/bin/su";
utillinux = pkgs.util-linux;
kernelParams = config.boot.kernelParams;
installBootLoader = config.system.build.installBootLoader;
activationScript = config.system.activationScripts.script;
dryActivationScript = config.system.dryActivationScript;
nixosLabel = config.system.nixos.label;
inherit (config.system) extraDependencies;
# Needed by switch-to-configuration.
perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp ]);
} // config.system.systemBuilderArgs);
# Handle assertions and warnings
@ -178,26 +148,6 @@ in
};
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 {
type = types.package;
readOnly = true;
@ -380,6 +330,16 @@ in
'';
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
# the build dependencies. Checks are similar to build dependencies in the
# 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 = {
imports = [ simpleService.configuration ];
systemd.services.test.description = "Test unit";
@ -482,9 +495,9 @@ in {
};
testScript = { nodes, ... }: let
originalSystem = nodes.machine.config.system.build.toplevel;
otherSystem = nodes.other.config.system.build.toplevel;
machine = nodes.machine.config.system.build.toplevel;
originalSystem = nodes.machine.system.build.toplevel;
otherSystem = nodes.other.system.build.toplevel;
machine = nodes.machine.system.build.toplevel;
# Ensures failures pass through using pipefail, otherwise failing to
# switch-to-configuration is hidden by the success of `tee`.
@ -497,11 +510,15 @@ in {
in /* python */ ''
def switch_to_specialisation(system, name, action="test", fail=False):
if name == "":
stc = f"{system}/bin/switch-to-configuration"
switcher = f"{system}/bin/switch-to-configuration"
else:
stc = f"{system}/specialisation/{name}/bin/switch-to-configuration"
out = machine.fail(f"{stc} {action} 2>&1") if fail \
else machine.succeed(f"{stc} {action} 2>&1")
switcher = f"{system}/specialisation/{name}/bin/switch-to-configuration"
return run_switch(switcher, action, fail)
# 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
return out
@ -639,6 +656,22 @@ in {
assert_lacks(out, "the following new units were started:")
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
out = switch_to_specialisation("${machine}", "unitWithBackslash")
assert_contains(out, "stopping the following units: test.service\n")