diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix index 1b0a62c2e8e7..6abbd4b673c0 100644 --- a/nixos/modules/system/activation/top-level.nix +++ b/nixos/modules/system/activation/top-level.nix @@ -20,6 +20,12 @@ let ''} ln -s ${config.system.build.etc}/etc $out/etc + + ${lib.optionalString config.system.etc.overlay.enable '' + ln -s ${config.system.build.etcMetadataImage} $out/etc-metadata-image + ln -s ${config.system.build.etcBasedir} $out/etc-basedir + ''} + ln -s ${config.system.path} $out/sw ln -s "$systemd" $out/systemd diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index fc2e5ddab159..6f47e6491c88 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -507,12 +507,20 @@ in { in nameValuePair "${n}.automount" (automountToUnit v)) cfg.automounts); - services.initrd-nixos-activation = { - after = [ "initrd-fs.target" ]; + services.initrd-find-nixos-closure = { + description = "Find NixOS closure"; + + unitConfig = { + RequiresMountsFor = "/sysroot/nix/store"; + DefaultDependencies = false; + }; + before = [ "shutdown.target" ]; + conflicts = [ "shutdown.target" ]; requiredBy = [ "initrd.target" ]; - unitConfig.AssertPathExists = "/etc/initrd-release"; - serviceConfig.Type = "oneshot"; - description = "NixOS Activation"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; script = /* bash */ '' set -uo pipefail @@ -542,6 +550,8 @@ in { # Assume the directory containing the init script is the closure. closure="$(dirname "$closure")" + ln --symbolic "$closure" /nixos-closure + # If we are not booting a NixOS closure (e.g. init=/bin/sh), # we don't know what root to prepare so we don't do anything if ! [ -x "/sysroot$(readlink "/sysroot$closure/prepare-root" || echo "$closure/prepare-root")" ]; then @@ -550,12 +560,48 @@ in { exit 0 fi echo 'NEW_INIT=' > /etc/switch-root.conf + ''; + }; + # We need to propagate /run for things like /run/booted-system + # and /run/current-system. + mounts = [ + { + where = "/sysroot/run"; + what = "/run"; + options = "bind"; + unitConfig = { + # See the comment on the mount unit for /run/etc-metadata + DefaultDependencies = false; + }; + requiredBy = [ "initrd-fs.target" ]; + before = [ "initrd-fs.target" ]; + } + ]; - # We need to propagate /run for things like /run/booted-system - # and /run/current-system. - mkdir -p /sysroot/run - mount --bind /run /sysroot/run + services.initrd-nixos-activation = { + requires = [ + config.boot.initrd.systemd.services.initrd-find-nixos-closure.name + ]; + after = [ + "initrd-fs.target" + config.boot.initrd.systemd.services.initrd-find-nixos-closure.name + ]; + requiredBy = [ "initrd.target" ]; + unitConfig = { + AssertPathExists = "/etc/initrd-release"; + RequiresMountsFor = [ + "/sysroot/run" + ]; + }; + serviceConfig.Type = "oneshot"; + description = "NixOS Activation"; + + script = /* bash */ '' + set -uo pipefail + export PATH="/bin:${cfg.package.util-linux}/bin" + + closure="$(realpath /nixos-closure)" # Initialize the system export IN_NIXOS_SYSTEMD_STAGE1=true diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix index 6c6352b0419d..944920e92335 100644 --- a/nixos/modules/system/etc/etc-activation.nix +++ b/nixos/modules/system/etc/etc-activation.nix @@ -1,4 +1,4 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: { @@ -34,12 +34,30 @@ mounts = [ { where = "/run/etc-metadata"; - what = "/sysroot${config.system.build.etcMetadataImage}"; + what = "/etc-metadata-image"; type = "erofs"; options = "loop"; - unitConfig.RequiresMountsFor = [ - "/sysroot/nix/store" + unitConfig = { + # Since this unit depends on the nix store being mounted, it cannot + # be a dependency of local-fs.target, because if it did, we'd have + # local-fs.target ordered after the nix store mount which would cause + # things like network.target to only become active after the nix store + # has been mounted. + # This breaks for instance setups where sshd needs to be up before + # any encrypted disks can be mounted. + DefaultDependencies = false; + RequiresMountsFor = [ + "/sysroot/nix/store" + ]; + }; + requires = [ + config.boot.initrd.systemd.services.initrd-find-etc.name ]; + after = [ + config.boot.initrd.systemd.services.initrd-find-etc.name + ]; + requiredBy = [ "initrd-fs.target" ]; + before = [ "initrd-fs.target" ]; } { where = "/sysroot/etc"; @@ -49,7 +67,7 @@ "relatime" "redirect_dir=on" "metacopy=on" - "lowerdir=/run/etc-metadata::/sysroot${config.system.build.etcBasedir}" + "lowerdir=/run/etc-metadata::/etc-basedir" ] ++ lib.optionals config.system.etc.overlay.mutable [ "rw" "upperdir=/sysroot/.rw-etc/upper" @@ -59,28 +77,77 @@ ]); requiredBy = [ "initrd-fs.target" ]; before = [ "initrd-fs.target" ]; - requires = lib.mkIf config.system.etc.overlay.mutable [ "rw-etc.service" ]; - after = lib.mkIf config.system.etc.overlay.mutable [ "rw-etc.service" ]; - unitConfig.RequiresMountsFor = [ - "/sysroot/nix/store" - "/run/etc-metadata" + requires = [ + config.boot.initrd.systemd.services.initrd-find-etc.name + ] ++ lib.optionals config.system.etc.overlay.mutable [ + config.boot.initrd.systemd.services."rw-etc".name ]; + after = [ + config.boot.initrd.systemd.services.initrd-find-etc.name + ] ++ lib.optionals config.system.etc.overlay.mutable [ + config.boot.initrd.systemd.services."rw-etc".name + ]; + unitConfig = { + RequiresMountsFor = [ + "/sysroot/nix/store" + "/run/etc-metadata" + ]; + DefaultDependencies = false; + }; } ]; - services = lib.mkIf config.system.etc.overlay.mutable { - rw-etc = { - unitConfig = { - DefaultDependencies = false; - RequiresMountsFor = "/sysroot"; + services = lib.mkMerge [ + (lib.mkIf config.system.etc.overlay.mutable { + rw-etc = { + requiredBy = [ "initrd-fs.target" ]; + before = [ "initrd-fs.target" ]; + unitConfig = { + DefaultDependencies = false; + RequiresMountsFor = "/sysroot"; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = '' + /bin/mkdir -p -m 0755 /sysroot/.rw-etc/upper /sysroot/.rw-etc/work + ''; + }; }; - serviceConfig = { - Type = "oneshot"; - ExecStart = '' - /bin/mkdir -p -m 0755 /sysroot/.rw-etc/upper /sysroot/.rw-etc/work + }) + { + initrd-find-etc = { + description = "Find the path to the etc metadata image and based dir"; + requires = [ + config.boot.initrd.systemd.services.initrd-find-nixos-closure.name + ]; + after = [ + config.boot.initrd.systemd.services.initrd-find-nixos-closure.name + ]; + before = [ "shutdown.target" ]; + conflicts = [ "shutdown.target" ]; + requiredBy = [ "initrd.target" ]; + unitConfig = { + DefaultDependencies = false; + RequiresMountsFor = "/sysroot/nix/store"; + }; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + script = /* bash */ '' + set -uo pipefail + + closure="$(realpath /nixos-closure)" + + metadata_image="$(chroot /sysroot ${lib.getExe' pkgs.coreutils "realpath"} "$closure/etc-metadata-image")" + ln -s "/sysroot$metadata_image" /etc-metadata-image + + basedir="$(chroot /sysroot ${lib.getExe' pkgs.coreutils "realpath"} "$closure/etc-basedir")" + ln -s "/sysroot$basedir" /etc-basedir ''; }; - }; - }; + } + ]; }; }) diff --git a/nixos/tests/activation/etc-overlay-immutable.nix b/nixos/tests/activation/etc-overlay-immutable.nix index 6d56db43f0b2..601ac77cbd84 100644 --- a/nixos/tests/activation/etc-overlay-immutable.nix +++ b/nixos/tests/activation/etc-overlay-immutable.nix @@ -15,6 +15,10 @@ boot.kernelPackages = pkgs.linuxPackages_latest; time.timeZone = "Utc"; + # The standard resolvconf service tries to write to /etc and crashes, + # which makes nixos-rebuild exit uncleanly when switching into the new generation + services.resolved.enable = true; + environment.etc = { "mountpoint/.keep".text = "keep"; "filemount".text = "keep"; @@ -26,6 +30,13 @@ }; testScript = '' + with subtest("/run/etc-metadata/ is mounted"): + print(machine.succeed("mountpoint /run/etc-metadata")) + + with subtest("No temporary files leaked into stage 2"): + machine.succeed("[ ! -e /etc-metadata-image ]") + machine.succeed("[ ! -e /etc-basedir ]") + with subtest("/etc is mounted as an overlay"): machine.succeed("findmnt --kernel --type overlay /etc") @@ -50,6 +61,9 @@ with subtest("switching to the same generation"): machine.succeed("/run/current-system/bin/switch-to-configuration test") + with subtest("the initrd didn't get rebuilt"): + machine.succeed("test /run/current-system/initrd -ef /run/current-system/specialisation/new-generation/initrd") + with subtest("switching to a new generation"): machine.fail("stat /etc/newgen") diff --git a/nixos/tests/activation/etc-overlay-mutable.nix b/nixos/tests/activation/etc-overlay-mutable.nix index 8561ff7fd230..fe6165212470 100644 --- a/nixos/tests/activation/etc-overlay-mutable.nix +++ b/nixos/tests/activation/etc-overlay-mutable.nix @@ -18,12 +18,22 @@ }; testScript = '' + with subtest("/run/etc-metadata/ is mounted"): + print(machine.succeed("mountpoint /run/etc-metadata")) + + with subtest("No temporary files leaked into stage 2"): + machine.succeed("[ ! -e /etc-metadata-image ]") + machine.succeed("[ ! -e /etc-basedir ]") + with subtest("/etc is mounted as an overlay"): machine.succeed("findmnt --kernel --type overlay /etc") with subtest("switching to the same generation"): machine.succeed("/run/current-system/bin/switch-to-configuration test") + with subtest("the initrd didn't get rebuilt"): + machine.succeed("test /run/current-system/initrd -ef /run/current-system/specialisation/new-generation/initrd") + with subtest("switching to a new generation"): machine.fail("stat /etc/newgen") machine.succeed("echo -n 'mutable' > /etc/mutable") diff --git a/nixos/tests/systemd-initrd-simple.nix b/nixos/tests/systemd-initrd-simple.nix index 2b7283a82193..b61cb8ddae7b 100644 --- a/nixos/tests/systemd-initrd-simple.nix +++ b/nixos/tests/systemd-initrd-simple.nix @@ -29,6 +29,8 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: { machine.succeed("[ -e /dev/shm ]") # /dev/shm machine.succeed("[ -e /dev/pts/ptmx ]") # /dev/pts machine.succeed("[ -e /run/keys ]") # /run/keys + # /nixos-closure didn't leak into stage-2 + machine.succeed("[ ! -e /nixos-closure ]") with subtest("groups work"): machine.fail("journalctl -b 0 | grep 'systemd-udevd.*Unknown group.*ignoring'")