From e6b66f08a53261cf825817df59d3ccd75ed0eead Mon Sep 17 00:00:00 2001 From: nikstur Date: Sat, 25 Nov 2023 21:31:09 +0100 Subject: [PATCH] nixos/switch-to-configuration: add sysinit-reactivation.target --- ...-happens-during-a-system-switch.chapter.md | 2 +- .../activation/switch-to-configuration.pl | 12 +- nixos/modules/system/boot/systemd.nix | 7 ++ .../modules/system/boot/systemd/tmpfiles.nix | 35 ++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/sysinit-reactivation.nix | 107 ++++++++++++++++++ 6 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 nixos/tests/sysinit-reactivation.nix diff --git a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md index ccadb819e061..5d17a9c98514 100644 --- a/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md +++ b/nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md @@ -37,7 +37,7 @@ of actions is always the same: - Forget about the failed state of units (`systemctl reset-failed`) - Reload systemd (`systemctl daemon-reload`) - Reload systemd user instances (`systemctl --user daemon-reload`) -- Set up tmpfiles (`systemd-tmpfiles --create`) +- Reactivate sysinit (`systemctl restart sysinit-reactivation.target`) - Reload units (`systemctl reload`) - Restart units (`systemctl restart`) - Start units (`systemctl start`) diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl index e2f66a287bc4..ba45231465fb 100755 --- a/nixos/modules/system/activation/switch-to-configuration.pl +++ b/nixos/modules/system/activation/switch-to-configuration.pl @@ -889,9 +889,15 @@ while (my $f = <$list_active_users>) { close($list_active_users) || die("Unable to close the file handle to loginctl"); -# Set the new tmpfiles -print STDERR "setting up tmpfiles\n"; -system("$new_systemd/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3; +# Restart sysinit-reactivation.target. +# This target only exists to restart services ordered before sysinit.target. We +# cannot use X-StopOnReconfiguration to restart sysinit.target because then ALL +# services of the system would be restarted since all normal services have a +# default dependency on sysinit.target. sysinit-reactivation.target ensures +# that services ordered BEFORE sysinit.target get re-started in the correct +# order. Ordering between these services is respected. +print STDERR "restarting sysinit-reactivation.target\n"; +system("$new_systemd/bin/systemctl", "restart", "sysinit-reactivation.target") == 0 or $res = 4; # Before reloading we need to ensure that the units are still active. They may have been # deactivated because one of their requirements got stopped. If they are inactive diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index c3902007906a..46c3f66f02dc 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -569,6 +569,13 @@ in unitConfig.X-StopOnReconfiguration = true; }; + # This target only exists so that services ordered before sysinit.target + # are restarted in the correct order, notably BEFORE the other services, + # when switching configurations. + systemd.targets.sysinit-reactivation = { + description = "Reactivate sysinit units"; + }; + systemd.units = mapAttrs' (n: v: nameValuePair "${n}.path" (pathToUnit n v)) cfg.paths // mapAttrs' (n: v: nameValuePair "${n}.service" (serviceToUnit n v)) cfg.services diff --git a/nixos/modules/system/boot/systemd/tmpfiles.nix b/nixos/modules/system/boot/systemd/tmpfiles.nix index 183e2033ecb0..dae23eddd1e2 100644 --- a/nixos/modules/system/boot/systemd/tmpfiles.nix +++ b/nixos/modules/system/boot/systemd/tmpfiles.nix @@ -150,6 +150,41 @@ in "systemd-tmpfiles-setup.service" ]; + # Allow systemd-tmpfiles to be restarted by switch-to-configuration. This + # service is not pulled into the normal boot process. It only exists for + # switch-to-configuration. + # + # This needs to be a separate unit because it does not execute + # systemd-tmpfiles with `--boot` as that is supposed to only be executed + # once at boot time. + # + # Keep this aligned with the upstream `systemd-tmpfiles-setup.service` unit. + systemd.services."systemd-tmpfiles-resetup" = { + description = "Re-setup tmpfiles on a system that is already running."; + + requiredBy = [ "sysinit-reactivation.target" ]; + after = [ "local-fs.target" "systemd-sysusers.service" "systemd-journald.service" ]; + before = [ "sysinit-reactivation.target" "shutdown.target" ]; + conflicts = [ "shutdown.target" ]; + restartTriggers = [ config.environment.etc."tmpfiles.d".source ]; + + unitConfig.DefaultDependencies = false; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "systemd-tmpfiles --create --remove --exclude-prefix=/dev"; + SuccessExitStatus = "DATAERR CANTCREAT"; + ImportCredential = [ + "tmpfiles.*" + "loging.motd" + "login.issue" + "network.hosts" + "ssh.authorized_keys.root" + ]; + }; + }; + environment.etc = { "tmpfiles.d".source = (pkgs.symlinkJoin { name = "tmpfiles.d"; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 63e957eace88..25ee587e8f7a 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -819,6 +819,7 @@ in { syncthing-init = handleTest ./syncthing-init.nix {}; syncthing-many-devices = handleTest ./syncthing-many-devices.nix {}; syncthing-relay = handleTest ./syncthing-relay.nix {}; + sysinit-reactivation = runTest ./sysinit-reactivation.nix; systemd = handleTest ./systemd.nix {}; systemd-analyze = handleTest ./systemd-analyze.nix {}; systemd-binfmt = handleTestOn ["x86_64-linux"] ./systemd-binfmt.nix {}; diff --git a/nixos/tests/sysinit-reactivation.nix b/nixos/tests/sysinit-reactivation.nix new file mode 100644 index 000000000000..1a0caecb610a --- /dev/null +++ b/nixos/tests/sysinit-reactivation.nix @@ -0,0 +1,107 @@ +# This runs to two scenarios but in one tests: +# - A post-sysinit service needs to be restarted AFTER tmpfiles was restarted. +# - A service needs to be restarted BEFORE tmpfiles is restarted + +{ lib, ... }: + +let + makeGeneration = generation: { + "${generation}".configuration = { + systemd.services.pre-sysinit-before-tmpfiles.environment.USER = + lib.mkForce "${generation}-tmpfiles-user"; + + systemd.services.pre-sysinit-after-tmpfiles.environment = { + NEEDED_PATH = lib.mkForce "/run/${generation}-needed-by-pre-sysinit-after-tmpfiles"; + PATH_TO_CREATE = lib.mkForce "/run/${generation}-needed-by-post-sysinit"; + }; + + systemd.services.post-sysinit.environment = { + NEEDED_PATH = lib.mkForce "/run/${generation}-needed-by-post-sysinit"; + PATH_TO_CREATE = lib.mkForce "/run/${generation}-created-by-post-sysinit"; + }; + + systemd.tmpfiles.settings.test = lib.mkForce { + "/run/${generation}-needed-by-pre-sysinit-after-tmpfiles".f.user = + "${generation}-tmpfiles-user"; + }; + }; + }; +in + +{ + + name = "sysinit-reactivation"; + + meta.maintainers = with lib.maintainers; [ nikstur ]; + + nodes.machine = { config, lib, pkgs, ... }: { + systemd.services.pre-sysinit-before-tmpfiles = { + wantedBy = [ "sysinit.target" ]; + requiredBy = [ "sysinit-reactivation.target" ]; + before = [ "systemd-tmpfiles-setup.service" "systemd-tmpfiles-resetup.service" ]; + unitConfig.DefaultDependencies = false; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + environment.USER = "tmpfiles-user"; + script = "${pkgs.shadow}/bin/useradd $USER"; + }; + + systemd.services.pre-sysinit-after-tmpfiles = { + wantedBy = [ "sysinit.target" ]; + requiredBy = [ "sysinit-reactivation.target" ]; + after = [ "systemd-tmpfiles-setup.service" "systemd-tmpfiles-resetup.service" ]; + unitConfig.DefaultDependencies = false; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + environment = { + NEEDED_PATH = "/run/needed-by-pre-sysinit-after-tmpfiles"; + PATH_TO_CREATE = "/run/needed-by-post-sysinit"; + }; + script = '' + if [[ -e $NEEDED_PATH ]]; then + touch $PATH_TO_CREATE + fi + ''; + }; + + systemd.services.post-sysinit = { + wantedBy = [ "default.target" ]; + serviceConfig.Type = "oneshot"; + serviceConfig.RemainAfterExit = true; + environment = { + NEEDED_PATH = "/run/needed-by-post-sysinit"; + PATH_TO_CREATE = "/run/created-by-post-sysinit"; + }; + script = '' + if [[ -e $NEEDED_PATH ]]; then + touch $PATH_TO_CREATE + fi + ''; + }; + + systemd.tmpfiles.settings.test = { + "/run/needed-by-pre-sysinit-after-tmpfiles".f.user = + "tmpfiles-user"; + }; + + specialisation = lib.mkMerge [ + (makeGeneration "second") + (makeGeneration "third") + ]; + }; + + testScript = { nodes, ... }: '' + def switch(generation): + toplevel = "${nodes.machine.system.build.toplevel}"; + machine.succeed(f"{toplevel}/specialisation/{generation}/bin/switch-to-configuration switch") + + machine.wait_for_unit("default.target") + machine.succeed("test -e /run/created-by-post-sysinit") + + switch("second") + machine.succeed("test -e /run/second-created-by-post-sysinit") + + switch("third") + machine.succeed("test -e /run/third-created-by-post-sysinit") + ''; +}