From 772d6076e825dc999a04245f7dfa3cb19082ec28 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Wed, 28 Jun 2023 14:06:28 +0200 Subject: nixos: Add system.activatable flag for images that are pre-activated --- .../system/activation/activatable-system.nix | 121 +++++++++++++-------- .../system/activation/switch-to-configuration.pl | 34 +++--- nixos/tests/switch-test.nix | 41 ++++++- 3 files changed, 132 insertions(+), 64 deletions(-) diff --git a/nixos/modules/system/activation/activatable-system.nix b/nixos/modules/system/activation/activatable-system.nix index b179fce417e..7f6154794bd 100644 --- a/nixos/modules/system/activation/activatable-system.nix +++ b/nixos/modules/system/activation/activatable-system.nix @@ -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} + ''; }; } diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl index de6e43dd30d..a41e67a2294 100755 --- a/nixos/modules/system/activation/switch-to-configuration.pl +++ b/nixos/modules/system/activation/switch-to-configuration.pl @@ -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); diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix index 8cc4e68e78a..f44dede7fef 100644 --- a/nixos/tests/switch-test.nix +++ b/nixos/tests/switch-test.nix @@ -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"; @@ -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") -- cgit 1.4.1