nixos/systemd-boot-builder: re-instate boot counting
Co-Authored-By: Julien Malka <julien@malka.sh> Co-Authored-By: AkechiShiro <14914796+AkechiShiro@users.noreply.github.com>
This commit is contained in:
co-authored by
Julien Malka
AkechiShiro
parent
ac2410be5d
commit
69ce6b2391
@@ -11,7 +11,8 @@ import sys
|
||||
import tempfile
|
||||
import warnings
|
||||
import json
|
||||
from typing import NamedTuple, Any, Sequence
|
||||
import glob
|
||||
from typing import NamedTuple, Any, Sequence, Type
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
@@ -36,6 +37,8 @@ GRACEFUL = "@graceful@"
|
||||
COPY_EXTRA_FILES = "@copyExtraFiles@"
|
||||
CHECK_MOUNTPOINTS = "@checkMountpoints@"
|
||||
STORE_DIR = "@storeDir@"
|
||||
BOOT_COUNTING_TRIES = "@bootCountingTries@"
|
||||
BOOT_COUNTING = "@bootCounting@" == "True"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -53,6 +56,135 @@ class BootSpec:
|
||||
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: Path
|
||||
initrd: Path
|
||||
devicetree: Path | None
|
||||
kernel_params: str | None
|
||||
machine_id: str | None
|
||||
sort_key: str
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
maybe_devicetree_path = entry_map.get("devicetree")
|
||||
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=Path(entry_map["linux"]),
|
||||
initrd=Path(entry_map["initrd"]),
|
||||
devicetree=Path(maybe_devicetree_path) if maybe_devicetree_path else None,
|
||||
machine_id=entry_map.get("machine-id"),
|
||||
sort_key=entry_map.get("sort_key", "nixos"),
|
||||
kernel_params=entry_map.get("options"),
|
||||
)
|
||||
return disk_entry
|
||||
|
||||
def write(self, sorted_first: str) -> None:
|
||||
# TODO
|
||||
# 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"devicetree /{self.devicetree}"
|
||||
if self.devicetree is not None
|
||||
else None,
|
||||
f"sort-key {default_sort_key if self.default else self.sort_key}",
|
||||
]
|
||||
|
||||
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")
|
||||
|
||||
FILE = None | int
|
||||
@@ -72,7 +204,9 @@ class SystemIdentifier(NamedTuple):
|
||||
|
||||
def copy_if_not_exists(source: Path, dest: Path) -> None:
|
||||
if not dest.exists():
|
||||
tmpfd, tmppath = tempfile.mkstemp(dir=dest.parent, prefix=dest.name, suffix='.tmp.')
|
||||
tmpfd, tmppath = tempfile.mkstemp(
|
||||
dir=dest.parent, prefix=dest.name, suffix=".tmp."
|
||||
)
|
||||
shutil.copyfile(source, tmppath)
|
||||
os.fsync(tmpfd)
|
||||
shutil.move(tmppath, dest)
|
||||
@@ -97,38 +231,14 @@ def system_dir(
|
||||
return d
|
||||
|
||||
|
||||
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:
|
||||
def write_loader_conf(profile: str | None) -> None:
|
||||
tmp = LOADER_CONF.with_suffix(".tmp")
|
||||
with tmp.open("x") as f:
|
||||
f.write(f"timeout {TIMEOUT}\n")
|
||||
f.write(
|
||||
"default %s\n"
|
||||
% generation_conf_filename(profile, generation, specialisation)
|
||||
)
|
||||
if profile:
|
||||
f.write("default nixos-%s-generation-*\n" % profile)
|
||||
else:
|
||||
f.write("default nixos-generation-*\n")
|
||||
if not EDITOR:
|
||||
f.write("editor 0\n")
|
||||
if REBOOT_FOR_BITLOCKER:
|
||||
@@ -139,6 +249,23 @@ def write_loader_conf(
|
||||
os.rename(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)
|
||||
boot_json_path = (system_directory / "boot.json").resolve()
|
||||
@@ -210,6 +337,8 @@ def write_entry(
|
||||
specialisation: str | None,
|
||||
machine_id: str | None,
|
||||
bootspec: BootSpec,
|
||||
entries: list[DiskEntry],
|
||||
sorted_first: str,
|
||||
current: bool,
|
||||
) -> None:
|
||||
if specialisation:
|
||||
@@ -244,37 +373,35 @@ def write_entry(
|
||||
"or renamed a file in `boot.initrd.secrets`",
|
||||
file=sys.stderr,
|
||||
)
|
||||
entry_file = (
|
||||
BOOT_MOUNT_POINT
|
||||
/ "loader/entries"
|
||||
/ generation_conf_filename(profile, generation, specialisation)
|
||||
)
|
||||
tmp_path = entry_file.with_suffix(".tmp")
|
||||
kernel_params = "init=%s " % bootspec.init
|
||||
|
||||
kernel_params = kernel_params + " ".join(bootspec.kernelParams)
|
||||
build_time = int(system_dir(profile, generation, specialisation).stat().st_ctime)
|
||||
build_date = datetime.datetime.fromtimestamp(build_time).strftime("%F")
|
||||
|
||||
with tmp_path.open("w") as f:
|
||||
f.write(
|
||||
BOOT_ENTRY.format(
|
||||
title=title,
|
||||
sort_key=bootspec.sortKey,
|
||||
generation=generation,
|
||||
kernel=f"/{kernel}",
|
||||
initrd=f"/{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(f"devicetree /{devicetree}\n")
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
tmp_path.rename(entry_file)
|
||||
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,
|
||||
devicetree=devicetree,
|
||||
counters=counters,
|
||||
kernel_params=kernel_params,
|
||||
machine_id=machine_id,
|
||||
description=f"Generation {generation} {bootspec.label}, built on {build_date}",
|
||||
sort_key=bootspec.sortKey,
|
||||
default=current,
|
||||
).write(sorted_first)
|
||||
|
||||
|
||||
def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
|
||||
@@ -301,11 +428,9 @@ def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
|
||||
return configurations[-configurationLimit:]
|
||||
|
||||
|
||||
def remove_old_entries(gens: list[SystemIdentifier]) -> None:
|
||||
rex_profile = re.compile(r"^nixos-(.*)-generation-.*\.conf$")
|
||||
rex_generation = re.compile(
|
||||
r"^nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$"
|
||||
)
|
||||
def remove_old_entries(
|
||||
gens: list[SystemIdentifier], disk_entries: list[DiskEntry]
|
||||
) -> None:
|
||||
known_paths = []
|
||||
for gen in gens:
|
||||
bootspec = get_bootspec(gen.profile, gen.generation)
|
||||
@@ -313,20 +438,16 @@ def remove_old_entries(gens: list[SystemIdentifier]) -> None:
|
||||
known_paths.append(copy_from_file(bootspec.initrd, True).name)
|
||||
if bootspec.devicetree is not None:
|
||||
known_paths.append(copy_from_file(bootspec.devicetree, True).name)
|
||||
for path in (BOOT_MOUNT_POINT / "loader/entries").glob(
|
||||
"nixos*-generation-[1-9]*.conf", case_sensitive=False
|
||||
):
|
||||
if rex_profile.match(path.name):
|
||||
prof = rex_profile.sub(r"\1", path.name)
|
||||
else:
|
||||
prof = None
|
||||
try:
|
||||
gen_number = int(rex_generation.sub(r"\1", path.name))
|
||||
except ValueError:
|
||||
continue
|
||||
if (prof, gen_number, None) not in gens:
|
||||
path.unlink()
|
||||
for path in (BOOT_MOUNT_POINT / NIXOS_DIR).iterdir():
|
||||
|
||||
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_name in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/*"):
|
||||
path = Path(path_name)
|
||||
if path.name not in known_paths and not path.is_dir():
|
||||
path.unlink()
|
||||
|
||||
@@ -444,13 +565,37 @@ def install_bootloader(args: argparse.Namespace) -> None:
|
||||
for profile in get_profiles():
|
||||
gens += get_generations(profile)
|
||||
|
||||
remove_old_entries(gens)
|
||||
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
|
||||
|
||||
for gen in gens:
|
||||
try:
|
||||
bootspec = get_bootspec(gen.profile, gen.generation)
|
||||
is_default = Path(bootspec.init).parent == Path(args.default_config)
|
||||
write_entry(*gen, machine_id, bootspec, current=is_default)
|
||||
write_entry(
|
||||
*gen, machine_id, bootspec, entries, sorted_first, current=is_default
|
||||
)
|
||||
for specialisation in bootspec.specialisations.keys():
|
||||
write_entry(
|
||||
gen.profile,
|
||||
@@ -458,10 +603,16 @@ def install_bootloader(args: argparse.Namespace) -> None:
|
||||
specialisation,
|
||||
machine_id,
|
||||
bootspec,
|
||||
current=is_default,
|
||||
entries,
|
||||
sorted_first,
|
||||
current=(
|
||||
is_default
|
||||
and bootspec.specialisations[specialisation].sortKey
|
||||
== bootspec.sortKey
|
||||
),
|
||||
)
|
||||
if is_default:
|
||||
write_loader_conf(*gen)
|
||||
write_loader_conf(gen.profile)
|
||||
except OSError as e:
|
||||
# See https://github.com/NixOS/nixpkgs/issues/114552
|
||||
if e.errno == errno.EINVAL:
|
||||
|
||||
@@ -97,6 +97,9 @@ let
|
||||
'') cfg.extraEntries
|
||||
)}
|
||||
'';
|
||||
|
||||
bootCountingTries = cfg.bootCounting.tries;
|
||||
bootCounting = if cfg.bootCounting.enable then "True" else "False";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -417,6 +420,15 @@ 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;
|
||||
|
||||
|
||||
+253
-99
@@ -14,6 +14,8 @@ let
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
environment.systemPackages = [ pkgs.efibootmgr ];
|
||||
system.switch.enable = true;
|
||||
# Needed for machine-id to be persisted between reboots
|
||||
environment.etc."machine-id".text = "00000000000000000000000000000000";
|
||||
};
|
||||
|
||||
commonXbootldr =
|
||||
@@ -90,8 +92,111 @@ let
|
||||
# Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
|
||||
os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
|
||||
'';
|
||||
# Check that we are booting the default entry and not the generation with largest version number
|
||||
defaultEntry =
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
withBootCounting ? false,
|
||||
...
|
||||
}:
|
||||
runTest {
|
||||
name = "systemd-boot-default-entry" + lib.optionalString withBootCounting "-with-boot-counting";
|
||||
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes = {
|
||||
machine =
|
||||
{ nodes, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
system.extraDependencies = [ nodes.other_machine.system.build.toplevel ];
|
||||
boot.loader.systemd-boot.bootCounting.enable = withBootCounting;
|
||||
};
|
||||
|
||||
other_machine = {
|
||||
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)
|
||||
'';
|
||||
};
|
||||
garbage-collect-entry =
|
||||
{
|
||||
withBootCounting ? false,
|
||||
...
|
||||
}:
|
||||
runTest (
|
||||
{ lib, ... }:
|
||||
{
|
||||
name =
|
||||
"systemd-boot-garbage-collect-entry" + lib.optionalString withBootCounting "-with-boot-counting";
|
||||
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes = {
|
||||
inherit common;
|
||||
machine =
|
||||
{ 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
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
let
|
||||
baseSystem = nodes.common.system.build.toplevel;
|
||||
in
|
||||
''
|
||||
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
|
||||
${lib.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.succeed("test -e /boot/loader/entries/nixos-generation-2.conf")
|
||||
'';
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
inherit defaultEntry;
|
||||
basic = runTest (
|
||||
{ lib, ... }:
|
||||
{
|
||||
@@ -108,7 +213,10 @@ in
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
|
||||
machine.succeed("grep 'sort-key nixos' /boot/loader/entries/nixos-generation-1.conf")
|
||||
out = machine.succeed("cat /boot/loader/entries/nixos-generation-1.conf")
|
||||
print(out)
|
||||
# our sort-key will uses r to sort before nixos
|
||||
machine.succeed("grep 'sort-key nixor-default' /boot/loader/entries/nixos-generation-1.conf")
|
||||
|
||||
# Ensure we actually booted using systemd-boot
|
||||
# Magic number is the vendor UUID used by systemd-boot.
|
||||
@@ -224,33 +332,31 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
''
|
||||
machine.start()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
testScript = ''
|
||||
machine.start()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
machine.succeed(
|
||||
"test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
|
||||
)
|
||||
machine.succeed(
|
||||
"grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
|
||||
)
|
||||
machine.succeed(
|
||||
"grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
|
||||
)
|
||||
''
|
||||
+ pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
|
||||
machine.succeed(
|
||||
r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
|
||||
)
|
||||
'';
|
||||
machine.succeed(
|
||||
"test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
|
||||
)
|
||||
machine.succeed(
|
||||
"grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
|
||||
)
|
||||
machine.succeed(
|
||||
"grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
|
||||
)
|
||||
''
|
||||
+ pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
|
||||
machine.succeed(
|
||||
r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
|
||||
)
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
||||
# Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
|
||||
fallback = runTest (
|
||||
{ pkgs, lib, ... }:
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "systemd-boot-fallback";
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
@@ -259,7 +365,7 @@ in
|
||||
];
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{ lib, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
|
||||
@@ -333,12 +439,10 @@ in
|
||||
name = "systemd-boot-memtest86";
|
||||
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.memtest86.enable = true;
|
||||
};
|
||||
nodes.machine = {
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.memtest86.enable = true;
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.succeed("test -e /boot/loader/entries/memtest86.conf")
|
||||
@@ -353,12 +457,10 @@ in
|
||||
name = "systemd-boot-netbootxyz";
|
||||
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.netbootxyz.enable = true;
|
||||
};
|
||||
nodes.machine = {
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.netbootxyz.enable = true;
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
|
||||
@@ -443,13 +545,11 @@ in
|
||||
name = "systemd-boot-memtest-sortkey";
|
||||
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.memtest86.enable = true;
|
||||
boot.loader.systemd-boot.memtest86.sortKey = "apple";
|
||||
};
|
||||
nodes.machine = {
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.memtest86.enable = true;
|
||||
boot.loader.systemd-boot.memtest86.sortKey = "apple";
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.succeed("test -e /boot/loader/entries/memtest86.conf")
|
||||
@@ -466,12 +566,10 @@ in
|
||||
name = "systemd-boot-entry-filename-xbootldr";
|
||||
meta.maintainers = with lib.maintainers; [ sdht0 ];
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [ commonXbootldr ];
|
||||
boot.loader.systemd-boot.memtest86.enable = true;
|
||||
};
|
||||
nodes.machine = {
|
||||
imports = [ commonXbootldr ];
|
||||
boot.loader.systemd-boot.memtest86.enable = true;
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
@@ -494,16 +592,14 @@ in
|
||||
name = "systemd-boot-extra-entries";
|
||||
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.extraEntries = {
|
||||
"banana.conf" = ''
|
||||
title banana
|
||||
'';
|
||||
};
|
||||
nodes.machine = {
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.extraEntries = {
|
||||
"banana.conf" = ''
|
||||
title banana
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.succeed("test -e /boot/loader/entries/banana.conf")
|
||||
@@ -519,7 +615,7 @@ in
|
||||
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.extraFiles = {
|
||||
@@ -558,12 +654,10 @@ in
|
||||
];
|
||||
};
|
||||
|
||||
with_netbootxyz =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.netbootxyz.enable = true;
|
||||
};
|
||||
with_netbootxyz = {
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.netbootxyz.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
@@ -602,41 +696,6 @@ in
|
||||
}
|
||||
);
|
||||
|
||||
garbage-collect-entry = runTest (
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "systemd-boot-garbage-collect-entry";
|
||||
meta.maintainers = with lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes = {
|
||||
inherit common;
|
||||
machine =
|
||||
{ pkgs, nodes, ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
|
||||
# These are configs for different nodes, but we'll use them here in `machine`
|
||||
system.extraDependencies = [
|
||||
nodes.common.system.build.toplevel
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
let
|
||||
baseSystem = nodes.common.system.build.toplevel;
|
||||
in
|
||||
''
|
||||
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")
|
||||
machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
|
||||
machine.fail("test -e /boot/loader/entries/nixos-generation-1.conf")
|
||||
machine.succeed("test -e /boot/loader/entries/nixos-generation-2.conf")
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
||||
no-bootspec = runTest (
|
||||
{ lib, ... }:
|
||||
{
|
||||
@@ -654,4 +713,99 @@ in
|
||||
'';
|
||||
}
|
||||
);
|
||||
|
||||
bootCounting =
|
||||
let
|
||||
baseConfig = {
|
||||
imports = [ common ];
|
||||
boot.loader.systemd-boot.bootCounting.enable = true;
|
||||
boot.loader.systemd-boot.bootCounting.trials = 2;
|
||||
};
|
||||
in
|
||||
{ pkgs, ... }:
|
||||
runTest {
|
||||
name = "systemd-boot-counting";
|
||||
meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
|
||||
|
||||
nodes = {
|
||||
machine =
|
||||
{ nodes, ... }:
|
||||
{
|
||||
imports = [ baseConfig ];
|
||||
system.extraDependencies = [ nodes.bad_machine.system.build.toplevel ];
|
||||
};
|
||||
|
||||
bad_machine = {
|
||||
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 =
|
||||
{ lib, pkgs }:
|
||||
defaultEntry {
|
||||
inherit lib pkgs;
|
||||
withBootCounting = true;
|
||||
};
|
||||
garbageCollectEntryWithBootCounting = garbage-collect-entry { withBootCounting = true; };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user