mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-11 20:25:32 +03:00
Merge pull request #237040 from roberth/flexible-activation
nixos/system: Support pre-activated images
This commit is contained in:
commit
3fd4ac8e82
6 changed files with 183 additions and 74 deletions
|
@ -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
|
||||
|
|
92
nixos/modules/system/activation/activatable-system.nix
Normal file
92
nixos/modules/system/activation/activatable-system.nix
Normal 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}
|
||||
'';
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue