Skip to content

Commit

Permalink
nixos: Add system.activatable flag for images that are pre-activated
Browse files Browse the repository at this point in the history
  • Loading branch information
roberth committed Jun 28, 2023
1 parent 8966419 commit 772d607
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 64 deletions.
121 changes: 77 additions & 44 deletions nixos/modules/system/activation/activatable-system.nix
Original file line number Diff line number Diff line change
@@ -1,59 +1,92 @@
/*
This module adds the activation script to toplevel, so that any previously
built configuration can be activated again, as long as they're available in
the store, e.g. through the profile's older generations.
Alternate applications of the NixOS modules may omit this module, e.g. to
build images that are pre-activated and omit the activation script and its
dependencies.
*/
{ 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
{
config = {
system.systemBuilderArgs = {
activationScript = config.system.activationScripts.script;
dryActivationScript = config.system.dryActivationScript;
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.systemBuilderCommands = ''
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
mkdir $out/bin
substitute ${./switch-to-configuration.pl} $out/bin/switch-to-configuration \
--subst-var out \
--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
''}
'';
system.build.separateActivationScript =
pkgs.runCommand
"separate-activation-script"
(systemBuilderArgs // {
toplevelVar = "toplevel";
toplevel = config.system.build.toplevel;
})
''
mkdir $out
${systemBuilderCommands}
'';
};
}
34 changes: 18 additions & 16 deletions nixos/modules/system/activation/switch-to-configuration.pl
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
## 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
Expand Down Expand Up @@ -96,7 +98,7 @@
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.
Expand All @@ -110,7 +112,7 @@

# 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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -539,13 +541,13 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
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;
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -670,7 +672,7 @@ sub path_to_unit_name {
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) {
Expand Down Expand Up @@ -709,12 +711,12 @@ sub filter_units {
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;
Expand Down Expand Up @@ -757,7 +759,7 @@ sub filter_units {
}


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)) {
Expand All @@ -781,12 +783,12 @@ sub filter_units {
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;
Expand Down Expand Up @@ -857,7 +859,7 @@ sub filter_units {
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);
Expand Down Expand Up @@ -940,9 +942,9 @@ sub filter_units {
}

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);
41 changes: 37 additions & 4 deletions nixos/tests/switch-test.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 772d607

Please sign in to comment.