diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index a40b91d9467ef..40c284fc8dabc 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -18,8 +18,6 @@ - `hardware.display` is a new module implementing workarounds for misbehaving monitors through setting up custom EDID files and forcing kernel/framebuffer modes. -- NixOS now has support for *automatic boot assessment* (see [here](https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/)) for detailed description of the feature) for `systemd-boot` users. Available as [boot.loader.systemd-boot.bootCounting](#opt-boot.loader.systemd-boot.bootCounting.enable). - - A new display-manager `services.displayManager.ly` was added. It is a tui based replacement of sddm and lightdm for window manager users. Users can use it by `services.displayManager.ly.enable` and config it by diff --git a/nixos/modules/system/activation/specialisation.nix b/nixos/modules/system/activation/specialisation.nix index fc348ad94c03a..fdab287802fa5 100644 --- a/nixos/modules/system/activation/specialisation.nix +++ b/nixos/modules/system/activation/specialisation.nix @@ -1,14 +1,10 @@ -{ config, lib, extendModules, noUserModules, ... }: +{ config, lib, pkgs, extendModules, noUserModules, ... }: let inherit (lib) - attrNames concatStringsSep - filter - length mapAttrs mapAttrsToList - match mkOption types ; @@ -77,19 +73,6 @@ in }; config = { - assertions = [( - let - invalidNames = filter (name: match "[[:alnum:]_]+" name == null) (attrNames config.specialisation); - in - { - assertion = length invalidNames == 0; - message = '' - Specialisation names can only contain alphanumeric characters and underscores - Invalid specialisation names: ${concatStringsSep ", " invalidNames} - ''; - } - )]; - system.systemBuilderCommands = '' mkdir $out/specialisation ${concatStringsSep "\n" diff --git a/nixos/modules/system/boot/loader/systemd-boot/boot-counting.md b/nixos/modules/system/boot/loader/systemd-boot/boot-counting.md deleted file mode 100644 index 743584b525915..0000000000000 --- a/nixos/modules/system/boot/loader/systemd-boot/boot-counting.md +++ /dev/null @@ -1,38 +0,0 @@ -# Automatic boot assessment with systemd-boot {#sec-automatic-boot-assessment} - -## Overview {#sec-automatic-boot-assessment-overview} - -Automatic boot assessment (or boot-counting) is a feature of `systemd-boot` that allows for automatically detecting invalid boot entries. -When the feature is active, each boot entry has an associated counter with a user defined number of trials. Whenever `systemd-boot` boots an entry, its counter is decreased by one, ultimately being marked as *bad* if the counter ever reaches zero. However, if an entry is successfully booted, systemd will permanently mark it as *good* and remove the counter altogether. Whenever an entry is marked as *bad*, it is sorted last in the `systemd-boot` menu. -A complete explanation of how that feature works can be found [here](https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/). - -## Enabling the feature {#sec-automatic-boot-assessment-enable} - -The feature can be enabled by toogling the [boot.loader.systemd-boot.bootCounting](#opt-boot.loader.systemd-boot.bootCounting.enable) option. - -## The boot-complete.target unit {#sec-automatic-boot-assessment-boot-complete-target} - -A *successful boot* for an entry is defined in terms of the `boot-complete.target` synchronisation point. It is up to the user to schedule all necessary units for the machine to be considered successfully booted before that synchronisation point. -For example, if you are running `docker` on a machine and you want to be sure that a *good* entry is an entry where docker is started successfully. -A configuration for that NixOS machine could look like that: - -``` -boot.loader.systemd-boot.bootCounting.enable = true; -services.docker.enable = true; - -systemd.services.docker = { - before = [ "boot-complete.target" ]; - wantedBy = [ "boot-complete.target" ]; - unitConfig.FailureAction = "reboot"; -}; -``` - -The systemd service type must be of type `notify` or `oneshot` for systemd to dectect the startup error properly. - -## Interaction with specialisations {#sec-automatic-boot-assessment-specialisations} - -When the boot-counting feature is enabled, `systemd-boot` will still try the boot entries in the same order as they are displayed in the boot menu. This means that the specialisations of a given generation will be tried directly after that generation, but that behavior is customizable with the [boot.loader.systemd-boot.sortKey](#opt-boot.loader.systemd-boot.sortKey) option. - -## Limitations {#sec-automatic-boot-assessment-limitations} - -This feature has to be used wisely to not risk any data integrity issues. Rollbacking into past generations can sometimes be dangerous, for example if some of the services may have undefined behaviors in the presence of unrecognized data migrations from future versions of themselves. diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py index c4324a8eae5bc..6db0ee7a4e475 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -12,9 +12,8 @@ import sys import warnings import json -from typing import NamedTuple, Any, Type +from typing import NamedTuple, Any from dataclasses import dataclass -from pathlib import Path # These values will be replaced with actual values during the package build EFI_SYS_MOUNT_POINT = "@efiSysMountPoint@" @@ -34,8 +33,6 @@ GRACEFUL = "@graceful@" COPY_EXTRA_FILES = "@copyExtraFiles@" CHECK_MOUNTPOINTS = "@checkMountpoints@" -BOOT_COUNTING_TRIES = "@bootCountingTries@" -BOOT_COUNTING = "@bootCounting@" == "True" @dataclass class BootSpec: @@ -51,108 +48,6 @@ class BootSpec: devicetree: str | None = None # noqa: N815 initrdSecrets: str | None = None # noqa: N815 -@dataclass -class Entry: - profile: str | None - generation_number: int - specialisation: str | None - - @classmethod - def from_path(cls: Type["Entry"], path: Path) -> "Entry": - filename = path.name - # Matching nixos-$profile-generation-*.conf - rex_profile = re.compile(r"^nixos-(.*)-generation-.*\.conf$") - # Matching nixos*-generation-$number*.conf - rex_generation = re.compile(r"^nixos.*-generation-([0-9]+).*\.conf$") - # Matching nixos*-generation-$number-specialisation-$specialisation_name*.conf - rex_specialisation = re.compile(r"^nixos.*-generation-([0-9]+)-specialisation-([a-zA-Z0-9_]+).*\.conf$") - profile = rex_profile.sub(r"\1", filename) if rex_profile.match(filename) else None - specialisation = rex_specialisation.sub(r"\2", filename) if rex_specialisation.match(filename) else None - try: - generation_number = int(rex_generation.sub(r"\1", filename)) - except ValueError: - raise - return cls(profile, generation_number, specialisation) - -@dataclass -class DiskEntry: - entry: Entry - default: bool - counters: str | None - title: str | None - description: str | None - kernel: str - initrd: str - kernel_params: str | None - machine_id: str | None - sort_key: str - devicetree: str | None - - @classmethod - def from_path(cls: Type["DiskEntry"], path: Path) -> "DiskEntry": - entry = Entry.from_path(path) - data = path.read_text().splitlines() - if '' in data: - data.remove('') - entry_map = dict(lines.split(' ', 1) for lines in data) - assert "linux" in entry_map - assert "initrd" in entry_map - filename = path.name - # Matching nixos*-generation-*$counters.conf - rex_counters = re.compile(r"^nixos.*-generation-.*(\+\d(-\d)?)\.conf$") - counters = rex_counters.sub(r"\1", filename) if rex_counters.match(filename) else None - disk_entry = cls( - entry=entry, - default=(entry_map.get("sort-key") == "default"), - counters=counters, - title=entry_map.get("title"), - description=entry_map.get("version"), - kernel=entry_map["linux"], - initrd=entry_map["initrd"], - kernel_params=entry_map.get("options"), - machine_id=entry_map.get("machine-id"), - sort_key=entry_map.get("sort_key", "nixos"), - devicetree=entry_map.get("devicetree"), - ) - return disk_entry - - def write(self, sorted_first: str) -> None: - # Compute a sort-key sorted before sorted_first - # This will compute something like: nixos -> nixor-default to make sure we come before other nixos entries, - # while allowing users users can pre-pend their own entries before. - default_sort_key = sorted_first[:-1] + chr(ord(sorted_first[-1])-1) + "-default" - tmp_path = self.path.with_suffix(".tmp") - with tmp_path.open('w') as f: - # We use "sort-key" to sort the default generation first. - # The "default" string is sorted before "non-default" (alphabetically) - boot_entry = [ - f"title {self.title}" if self.title is not None else None, - f"version {self.description}" if self.description is not None else None, - f"linux {self.kernel}", - f"initrd {self.initrd}", - f"options {self.kernel_params}" if self.kernel_params is not None else None, - f"machine-id {self.machine_id}" if self.machine_id is not None else None, - f"sort-key {default_sort_key if self.default else self.sort_key}", - f"devicetree {self.devicetree}" if self.devicetree is not None else None, - ] - - f.write("\n".join(filter(None, boot_entry))) - f.flush() - os.fsync(f.fileno()) - tmp_path.rename(self.path) - - - @property - def path(self) -> Path: - pieces = [ - "nixos", - self.entry.profile or None, - "generation", - str(self.entry.generation_number), - f"specialisation-{self.entry.specialisation}" if self.entry.specialisation else None, - ] - prefix = "-".join(p for p in pieces if p) - return Path(f"{BOOT_MOUNT_POINT}/loader/entries/{prefix}{self.counters if self.counters else ''}.conf") libc = ctypes.CDLL("libc.so.6") @@ -185,13 +80,29 @@ def system_dir(profile: str | None, generation: int, specialisation: str | None) else: return d -def write_loader_conf(profile: str | None) -> None: - with open(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf.tmp", 'w') as f: +BOOT_ENTRY = """title {title} +sort-key {sort_key} +version Generation {generation} {description} +linux {kernel} +initrd {initrd} +options {kernel_params} +""" + +def generation_conf_filename(profile: str | None, generation: int, specialisation: str | None) -> str: + pieces = [ + "nixos", + profile or None, + "generation", + str(generation), + f"specialisation-{specialisation}" if specialisation else None, + ] + return "-".join(p for p in pieces if p) + ".conf" + + +def write_loader_conf(profile: str | None, generation: int, specialisation: str | None) -> None: + with open(f"{LOADER_CONF}.tmp", 'w') as f: f.write(f"timeout {TIMEOUT}\n") - if profile: - f.write("default nixos-%s-generation-*\n" % profile) - else: - f.write("default nixos-generation-*\n") + f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation)) if not EDITOR: f.write("editor 0\n") if REBOOT_FOR_BITLOCKER: @@ -201,19 +112,6 @@ def write_loader_conf(profile: str | None) -> None: os.fsync(f.fileno()) os.rename(f"{LOADER_CONF}.tmp", LOADER_CONF) -def scan_entries() -> list[DiskEntry]: - """ - Scan all entries in $ESP/loader/entries/* - Does not support Type 2 entries as we do not support them for now. - Returns a generator of Entry. - """ - entries = [] - for path in Path(f"{EFI_SYS_MOUNT_POINT}/loader/entries/").glob("nixos*-generation-[1-9]*.conf"): - try: - entries.append(DiskEntry.from_path(path)) - except ValueError: - continue - return entries def get_bootspec(profile: str | None, generation: int) -> BootSpec: system_directory = system_dir(profile, generation, None) @@ -258,14 +156,8 @@ def copy_from_file(file: str, dry_run: bool = False) -> str: copy_if_not_exists(store_file_path, f"{BOOT_MOUNT_POINT}{efi_file_path}") return efi_file_path -def write_entry(profile: str | None, - generation: int, - specialisation: str | None, - machine_id: str, - bootspec: BootSpec, - entries: list[DiskEntry], - sorted_first: str, - current: bool) -> None: +def write_entry(profile: str | None, generation: int, specialisation: str | None, + machine_id: str, bootspec: BootSpec, current: bool) -> None: if specialisation: bootspec = bootspec.specialisations[specialisation] kernel = copy_from_file(bootspec.kernel) @@ -289,33 +181,31 @@ def write_entry(profile: str | None, f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr) print("note: this is normal after having removed " "or renamed a file in `boot.initrd.secrets`", file=sys.stderr) + entry_file = f"{BOOT_MOUNT_POINT}/loader/entries/%s" % ( + generation_conf_filename(profile, generation, specialisation)) + tmp_path = "%s.tmp" % (entry_file) kernel_params = "init=%s " % bootspec.init + kernel_params = kernel_params + " ".join(bootspec.kernelParams) build_time = int(os.path.getctime(system_dir(profile, generation, specialisation))) build_date = datetime.datetime.fromtimestamp(build_time).strftime('%F') - counters = f"+{BOOT_COUNTING_TRIES}" if BOOT_COUNTING else "" - entry = Entry(profile, generation, specialisation) - # We check if the entry we are writing is already on disk - # and we update its "default entry" status - for entry_on_disk in entries: - if entry == entry_on_disk.entry: - entry_on_disk.default = current - entry_on_disk.write(sorted_first) - return - - DiskEntry( - entry=entry, - title=title, - kernel=kernel, - initrd=initrd, - counters=counters, - kernel_params=kernel_params, - machine_id=machine_id, - description=f"Generation {generation} {bootspec.label}, built on {build_date}", - sort_key=bootspec.sortKey, - devicetree=devicetree, - default=current - ).write(sorted_first) + + with open(tmp_path, 'w') as f: + f.write(BOOT_ENTRY.format(title=title, + sort_key=bootspec.sortKey, + generation=generation, + kernel=kernel, + initrd=initrd, + kernel_params=kernel_params, + description=f"{bootspec.label}, built on {build_date}")) + if machine_id is not None: + f.write("machine-id %s\n" % machine_id) + if devicetree is not None: + f.write("devicetree %s\n" % devicetree) + f.flush() + os.fsync(f.fileno()) + os.rename(tmp_path, entry_file) + def get_generations(profile: str | None = None) -> list[SystemIdentifier]: gen_list = run( @@ -343,19 +233,30 @@ def get_generations(profile: str | None = None) -> list[SystemIdentifier]: return configurations[-configurationLimit:] -def remove_old_entries(gens: list[SystemIdentifier], disk_entries: list[DiskEntry]) -> None: +def remove_old_entries(gens: list[SystemIdentifier]) -> None: + rex_profile = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + r"/loader/entries/nixos-(.*)-generation-.*\.conf$") + rex_generation = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + r"/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$") known_paths = [] for gen in gens: bootspec = get_bootspec(gen.profile, gen.generation) known_paths.append(copy_from_file(bootspec.kernel, True)) known_paths.append(copy_from_file(bootspec.initrd, True)) - for disk_entry in disk_entries: - if (disk_entry.entry.profile, disk_entry.entry.generation_number, None) not in gens: - os.unlink(disk_entry.path) - for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/*"): + for path in glob.iglob(f"{BOOT_MOUNT_POINT}/loader/entries/nixos*-generation-[1-9]*.conf"): + if rex_profile.match(path): + prof = rex_profile.sub(r"\1", path) + else: + prof = None + try: + gen_number = int(rex_generation.sub(r"\1", path)) + except ValueError: + continue + if (prof, gen_number, None) not in gens: + os.unlink(path) + for path in glob.iglob(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/*"): if path not in known_paths and not os.path.isdir(path): os.unlink(path) + def cleanup_esp() -> None: for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/loader/entries/nixos*"): os.unlink(path) @@ -374,7 +275,7 @@ def get_profiles() -> list[str]: def install_bootloader(args: argparse.Namespace) -> None: try: with open("/etc/machine-id") as machine_file: - machine_id = machine_file.readlines()[0].strip() + machine_id = machine_file.readlines()[0] except IOError as e: if e.errno != errno.ENOENT: raise @@ -458,32 +359,18 @@ def install_bootloader(args: argparse.Namespace) -> None: gens = get_generations() for profile in get_profiles(): gens += get_generations(profile) - entries = scan_entries() - remove_old_entries(gens, entries) - # Compute the sort-key that will be sorted first. - sorted_first = "" - for gen in gens: - try: - bootspec = get_bootspec(gen.profile, gen.generation) - if bootspec.sortKey < sorted_first or sorted_first == "": - sorted_first = bootspec.sortKey - except OSError as e: - # See https://github.com/NixOS/nixpkgs/issues/114552 - if e.errno == errno.EINVAL: - profile = f"profile '{gen.profile}'" if gen.profile else "default profile" - print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr) - else: - raise e + + remove_old_entries(gens) for gen in gens: try: bootspec = get_bootspec(gen.profile, gen.generation) is_default = os.path.dirname(bootspec.init) == args.default_config - write_entry(*gen, machine_id, bootspec, entries, sorted_first, current=is_default) + write_entry(*gen, machine_id, bootspec, current=is_default) for specialisation in bootspec.specialisations.keys(): - write_entry(gen.profile, gen.generation, specialisation, machine_id, bootspec, entries, sorted_first, current=(is_default and bootspec.specialisations[specialisation].sortKey == bootspec.sortKey)) + write_entry(gen.profile, gen.generation, specialisation, machine_id, bootspec, current=is_default) if is_default: - write_loader_conf(gen.profile) + write_loader_conf(*gen) except OSError as e: # See https://github.com/NixOS/nixpkgs/issues/114552 if e.errno == errno.EINVAL: diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix index bd4dbe96ff3a7..bc32b15e52b2a 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix @@ -80,8 +80,6 @@ let ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n} '') cfg.extraEntries)} ''; - bootCountingTries = cfg.bootCounting.tries; - bootCounting = if cfg.bootCounting.enable then "True" else "False"; }; finalSystemdBootBuilder = pkgs.writeScript "install-systemd-boot.sh" '' @@ -91,10 +89,7 @@ let ''; in { - meta = { - maintainers = with lib.maintainers; [ julienmalka ]; - doc = ./boot-counting.md; - }; + meta.maintainers = with lib.maintainers; [ julienmalka ]; imports = [ (mkRenamedOptionModule [ "boot" "loader" "gummiboot" "enable" ] [ "boot" "loader" "systemd-boot" "enable" ]) @@ -333,15 +328,6 @@ in { ''; }; - bootCounting = { - enable = mkEnableOption "automatic boot assessment"; - tries = mkOption { - default = 3; - type = types.int; - description = "number of tries each entry should start with"; - }; - }; - rebootForBitlocker = mkOption { default = false; diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index 85e9b0a68b46a..9e7387e403795 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -107,10 +107,6 @@ let "systemd-rfkill.service" "systemd-rfkill.socket" - # Boot counting - "boot-complete.target" - ] ++ lib.optional config.boot.loader.systemd-boot.bootCounting.enable "systemd-bless-boot.service" ++ [ - # Hibernate / suspend. "hibernate.target" "suspend.target" diff --git a/nixos/tests/nixos-rebuild-specialisations.nix b/nixos/tests/nixos-rebuild-specialisations.nix index a5b916f7d7e90..9192b8a8a030b 100644 --- a/nixos/tests/nixos-rebuild-specialisations.nix +++ b/nixos/tests/nixos-rebuild-specialisations.nix @@ -71,32 +71,6 @@ import ./make-test-python.nix ({ pkgs, ... }: { } ''; - wrongConfigFile = pkgs.writeText "configuration.nix" '' - { lib, pkgs, ... }: { - imports = [ - ./hardware-configuration.nix - - ]; - - boot.loader.grub = { - enable = true; - device = "/dev/vda"; - forceInstall = true; - }; - - documentation.enable = false; - - environment.systemPackages = [ - (pkgs.writeShellScriptBin "parent" "") - ]; - - specialisation.foo-bar = { - inheritParentConfig = true; - - configuration = { ... }: { }; - }; - } - ''; in '' machine.start() @@ -142,12 +116,5 @@ import ./make-test-python.nix ({ pkgs, ... }: { with subtest("Make sure nonsense command combinations are forbidden"): machine.fail("nixos-rebuild boot --specialisation foo") machine.fail("nixos-rebuild boot -c foo") - - machine.copy_from_host( - "${wrongConfigFile}", - "/etc/nixos/configuration.nix", - ) - with subtest("Make sure that invalid specialisation names are rejected"): - machine.fail("nixos-rebuild switch") ''; }) diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix index a9f42900addd0..64bd72f9f6a27 100644 --- a/nixos/tests/systemd-boot.nix +++ b/nixos/tests/systemd-boot.nix @@ -13,8 +13,6 @@ let boot.loader.systemd-boot.enable = true; boot.loader.efi.canTouchEfiVariables = true; environment.systemPackages = [ pkgs.efibootmgr ]; - # Needed for machine-id to be persisted between reboots - environment.etc."machine-id".text = "00000000000000000000000000000000"; }; commonXbootldr = { config, lib, pkgs, ... }: @@ -83,7 +81,7 @@ let os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name ''; in -rec { +{ basic = makeTest { name = "systemd-boot"; meta.maintainers = with pkgs.lib.maintainers; [ danielfullmer julienmalka ]; @@ -95,8 +93,7 @@ rec { machine.wait_for_unit("multi-user.target") machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") - # our sort-key will uses r to sort before nixos - machine.succeed("grep 'sort-key nixor-default' /boot/loader/entries/nixos-generation-1.conf") + machine.succeed("grep 'sort-key nixos' /boot/loader/entries/nixos-generation-1.conf") # Ensure we actually booted using systemd-boot # Magic number is the vendor UUID used by systemd-boot. @@ -421,15 +418,15 @@ rec { ''; }; - garbage-collect-entry = { withBootCounting ? false, ... }: makeTest { - name = "systemd-boot-garbage-collect-entry" + optionalString withBootCounting "-with-boot-counting"; + garbage-collect-entry = makeTest { + name = "systemd-boot-garbage-collect-entry"; meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; nodes = { inherit common; machine = { pkgs, nodes, ... }: { imports = [ common ]; - boot.loader.systemd-boot.bootCounting.enable = withBootCounting; + # These are configs for different nodes, but we'll use them here in `machine` system.extraDependencies = [ nodes.common.system.build.toplevel @@ -444,12 +441,8 @@ rec { '' machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${baseSystem}") machine.succeed("nix-env -p /nix/var/nix/profiles/system --delete-generations 1") - # At this point generation 1 has already been marked as good so we reintroduce counters artificially - ${optionalString withBootCounting '' - machine.succeed("mv /boot/loader/entries/nixos-generation-1.conf /boot/loader/entries/nixos-generation-1+3.conf") - ''} machine.succeed("${baseSystem}/bin/switch-to-configuration boot") - machine.fail("test -e /boot/loader/entries/nixos-generation-1*") + machine.fail("test -e /boot/loader/entries/nixos-generation-1.conf") machine.succeed("test -e /boot/loader/entries/nixos-generation-2.conf") ''; }; @@ -469,138 +462,4 @@ rec { machine.wait_for_unit("multi-user.target") ''; }; - - # Check that we are booting the default entry and not the generation with largest version number - defaultEntry = { withBootCounting ? false, ... }: makeTest { - name = "systemd-boot-default-entry" + optionalString withBootCounting "-with-boot-counting"; - meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; - - nodes = { - machine = { pkgs, lib, nodes, ... }: { - imports = [ common ]; - system.extraDependencies = [ nodes.other_machine.system.build.toplevel ]; - boot.loader.systemd-boot.bootCounting.enable = withBootCounting; - }; - - other_machine = { pkgs, lib, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.bootCounting.enable = withBootCounting; - environment.systemPackages = [ pkgs.hello ]; - }; - }; - testScript = { nodes, ... }: - let - orig = nodes.machine.system.build.toplevel; - other = nodes.other_machine.system.build.toplevel; - in - '' - orig = "${orig}" - other = "${other}" - - def check_current_system(system_path): - machine.succeed(f'test $(readlink -f /run/current-system) = "{system_path}"') - - check_current_system(orig) - - # Switch to other configuration - machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${other}") - machine.succeed(f"{other}/bin/switch-to-configuration boot") - # Rollback, default entry is now generation 1 - machine.succeed("nix-env -p /nix/var/nix/profiles/system --rollback") - machine.succeed(f"{orig}/bin/switch-to-configuration boot") - machine.shutdown() - machine.start() - machine.wait_for_unit("multi-user.target") - # Check that we booted generation 1 (default) - # even though generation 2 comes first in alphabetical order - check_current_system(orig) - ''; - }; - - - bootCounting = - let - baseConfig = { pkgs, lib, ... }: { - imports = [ common ]; - boot.loader.systemd-boot.bootCounting.enable = true; - boot.loader.systemd-boot.bootCounting.tries = 2; - }; - in - makeTest { - name = "systemd-boot-counting"; - meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ]; - - nodes = { - machine = { pkgs, lib, nodes, ... }: { - imports = [ baseConfig ]; - system.extraDependencies = [ nodes.bad_machine.system.build.toplevel ]; - }; - - bad_machine = { pkgs, lib, ... }: { - imports = [ baseConfig ]; - - systemd.services."failing" = { - script = "exit 1"; - requiredBy = [ "boot-complete.target" ]; - before = [ "boot-complete.target" ]; - serviceConfig.Type = "oneshot"; - }; - }; - }; - testScript = { nodes, ... }: - let - orig = nodes.machine.system.build.toplevel; - bad = nodes.bad_machine.system.build.toplevel; - in - '' - orig = "${orig}" - bad = "${bad}" - - def check_current_system(system_path): - machine.succeed(f'test $(readlink -f /run/current-system) = "{system_path}"') - - # Ensure we booted using an entry with counters enabled - machine.succeed( - "test -e /sys/firmware/efi/efivars/LoaderBootCountPath-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" - ) - - # systemd-bless-boot should have already removed the "+2" suffix from the boot entry - machine.wait_for_unit("systemd-bless-boot.service") - machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") - check_current_system(orig) - - # Switch to bad configuration - machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${bad}") - machine.succeed(f"{bad}/bin/switch-to-configuration boot") - - # Ensure new bootloader entry has initialized counter - machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") - machine.succeed("test -e /boot/loader/entries/nixos-generation-2+2.conf") - machine.shutdown() - - machine.start() - machine.wait_for_unit("multi-user.target") - check_current_system(bad) - machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") - machine.succeed("test -e /boot/loader/entries/nixos-generation-2+1-1.conf") - machine.shutdown() - - machine.start() - machine.wait_for_unit("multi-user.target") - check_current_system(bad) - machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") - machine.succeed("test -e /boot/loader/entries/nixos-generation-2+0-2.conf") - machine.shutdown() - - # Should boot back into original configuration - machine.start() - check_current_system(orig) - machine.wait_for_unit("multi-user.target") - machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf") - machine.succeed("test -e /boot/loader/entries/nixos-generation-2+0-2.conf") - machine.shutdown() - ''; - }; - defaultEntryWithBootCounting = defaultEntry { withBootCounting = true; }; - garbageCollectEntryWithBootCounting = garbage-collect-entry { withBootCounting = true; }; }