-
-
Notifications
You must be signed in to change notification settings - Fork 18.5k
nixos/grub: generate BLS entries #95901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
435a72e
7e6d6f7
5e5476e
7b71abe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,10 @@ let | |
| then realGrub.override { efiSupport = cfg.efiSupport; } | ||
| else null; | ||
|
|
||
| bootPath = if cfg.mirroredBoots != [ ] | ||
| then (builtins.head cfg.mirroredBoots).path | ||
| else "/boot"; | ||
|
|
||
| f = x: optionalString (x != null) ("" + x); | ||
|
|
||
| grubConfig = args: | ||
|
|
@@ -756,6 +760,16 @@ in | |
|
|
||
| environment.systemPackages = mkIf (grub != null) [ grub ]; | ||
|
|
||
| # Link /boot under /run/boot-loder-entries to make | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: loder -> laoder
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo: laoder -> loader |
||
| # systemd happy even on non-EFI system | ||
| systemd.mounts = lib.optional (!cfg.efiSupport) { | ||
| what = bootPath; | ||
| where = "/run/boot-loader-entries"; | ||
| type = "none"; | ||
| options = "bind"; | ||
| requiredBy = [ "local-fs.target" ]; | ||
| }; | ||
|
|
||
| boot.loader.grub.extraPrepareConfig = | ||
| concatStrings (mapAttrsToList (n: v: '' | ||
| ${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -99,7 +99,15 @@ sub runCommand { | |
|
|
||
| print STDERR "updating GRUB 2 menu...\n"; | ||
|
|
||
| make_path("$bootPath/grub", { mode => 0700 }); | ||
| # Make GRUB directory | ||
| make_path("$bootPath/grub", { mode => 0700 }); | ||
|
|
||
| # Make BLS entries directory, see addBLSEntry | ||
| make_path("$bootPath/loader/entries", { mode => 0700 }); | ||
| writeFile("$bootPath/loader/entries.srel", "type1"); | ||
|
|
||
| # and a temporary one for new entries | ||
| make_path("$bootPath/loader/entries.tmp", { mode => 0700 }); | ||
|
|
||
| # Discover whether the bootPath is on the same filesystem as / and | ||
| # /nix/store. If not, then all kernels and initrds must be copied to | ||
|
|
@@ -460,6 +468,7 @@ sub copyToKernelsDir { | |
| } | ||
|
|
||
| sub addEntry { | ||
| # Creates a Grub menu entry for a given system | ||
| my ($name, $path, $options, $current) = @_; | ||
| return unless -e "$path/kernel" && -e "$path/initrd"; | ||
|
|
||
|
|
@@ -521,6 +530,58 @@ sub addEntry { | |
| $conf .= "}\n\n"; | ||
| } | ||
|
|
||
| sub addBLSEntry { | ||
| # Creates a Boot Loader Specification[1] entry for a given system. | ||
| # The information contained in the entry mirrors a boot entry in GRUB menu. | ||
| # | ||
| # [1]: https://uapi-group.org/specifications/specs/boot_loader_specification | ||
| my ($prof, $spec, $gen, $link) = @_; | ||
|
|
||
| # collect data from system | ||
| my %bootspec = %{decode_json(readFile("$link/boot.json"))->{"org.nixos.bootspec.v1"}}; | ||
| my $date = strftime("%F", localtime(lstat($link)->mtime)); | ||
| my $kernel = $bootspec{kernel} =~ s@$storePath/@@r =~ s@/@-@r; | ||
| my $initrd = $bootspec{initrd} =~ s@$storePath/@@r =~ s@/@-@r; | ||
| my $kernelParams = readFile("$link/kernel-params"); | ||
| my $machineId = readFile("/etc/machine-id"); | ||
|
|
||
| if ($grubEfi eq "" && !$copyKernels) { | ||
| # workaround for https://github.com/systemd/systemd/issues/35729 | ||
| make_path("$bootPath/kernels", { mode => 0755 }); | ||
| symlink($bootspec{kernel}, "$bootPath/kernels/$kernel"); | ||
| symlink($bootspec{initrd}, "$bootPath/kernels/$initrd"); | ||
| $copied{"$bootPath/kernels/$kernel"} = 1; | ||
| $copied{"$bootPath/kernels/$initrd"} = 1; | ||
| } | ||
|
Comment on lines
+548
to
+555
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The correctness of this is highly suspect. BLS doesn't specify how to interpret files in the tree that are symlinks to an absolute path like this. What root should be the root for resolving those symlinks? Obviously, in the case of running Additionally, why must this not be an EFI setup to take this code path? You can still do
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is working around a bug in systemd, of course it's not "correct" according to the BLS spec.
It's unrelated to
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand the reasoning for this. It's still highly suspect, and should probably not be considered proper BLS. And yes, it does have to do with IMO the feature simply shouldn't work without |
||
|
|
||
| # fill in the entry | ||
| my $extras = join(' ', $prof = $prof ne "system" ? " [$prof] " : "", | ||
| $spec = $spec ne "" ? " ($spec) " : ""); | ||
| my $entry = <<~END; | ||
| title @distroName@$extras | ||
| sort-key nixos | ||
| version Generation $gen $bootspec{label}, built on $date | ||
| linux kernels/$kernel | ||
| initrd kernels/$initrd | ||
| options init=$bootspec{init} $kernelParams | ||
| END | ||
| $entry .= "machine-id $machineId" if defined $machineId; | ||
|
|
||
| # entry file basename | ||
| my $name = join("-", grep { length $_ > 0 } | ||
| "nixos", $prof ne "system" ? $prof : "", | ||
| "generation", $gen, | ||
| $spec ? "specialisation-$spec" : ""); | ||
|
|
||
| # write entry to the temp directory | ||
| writeFile("$bootPath/loader/entries.tmp/$name.conf", $entry); | ||
|
|
||
| # mark the default entry | ||
| if (readlink($link) eq $defaultConfig) { | ||
| writeFile("$bootPath/loader/loader.conf", "default $name.conf"); | ||
| } | ||
|
Comment on lines
+579
to
+582
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, But much more importantly, if another OS owns the |
||
| } | ||
|
|
||
| sub addGeneration { | ||
| my ($name, $nameSuffix, $path, $options, $current) = @_; | ||
|
|
||
|
|
@@ -592,12 +653,18 @@ sub addProfile { | |
| warn "skipping corrupt system profile entry ‘$link’\n"; | ||
| next; | ||
| } | ||
| my $gen = nrFromGen($link); | ||
| my $date = strftime("%F", localtime(lstat($link)->mtime)); | ||
| my $version = | ||
| -e "$link/nixos-version" | ||
| ? readFile("$link/nixos-version") | ||
| : basename((glob(dirname(Cwd::abs_path("$link/kernel")) . "/lib/modules/*"))[0]); | ||
| addGeneration("@distroName@ - Configuration " . nrFromGen($link), " ($date - $version)", $link, $subEntryOptions, 0); | ||
| addGeneration("@distroName@ - Configuration " . $gen, " ($date - $version)", $link, $subEntryOptions, 0); | ||
|
|
||
| addBLSEntry(basename($profile), "", $gen, $link); | ||
| foreach my $spec (glob "$link/specialisation/*") { | ||
| addBLSEntry(basename($profile), $spec, $gen, $spec); | ||
| } | ||
| } | ||
|
|
||
| $conf .= "}\n"; | ||
|
|
@@ -611,6 +678,12 @@ sub addProfile { | |
| addProfile $profile, "@distroName@ - Profile '$name'"; | ||
| } | ||
|
|
||
| # Atomically replace the BLS entries directory | ||
| my $entriesDir = "$bootPath/loader/entries"; | ||
| rename $entriesDir, "$entriesDir.bak" or die "cannot rename $entriesDir to $entriesDir.bak: $!\n"; | ||
| rename "$entriesDir.tmp", $entriesDir or die "cannot rename $entriesDir.tmp to $entriesDir: $!\n"; | ||
| rmtree "$entriesDir.bak" or die "cannot remove $entriesDir.bak: $!\n"; | ||
|
Comment on lines
+681
to
+685
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is similar to the |
||
|
|
||
| # extraPrepareConfig could refer to @bootPath@, which we have to substitute | ||
| $extraPrepareConfig =~ s/\@bootPath\@/$bootPath/g; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,60 +1,123 @@ | ||
| import ./make-test-python.nix ({ lib, ... }: { | ||
| name = "grub"; | ||
| { pkgs, runTest }: | ||
|
|
||
| meta = with lib.maintainers; { | ||
| maintainers = [ rnhmjoj ]; | ||
| }; | ||
| { | ||
| # Basic GRUB setup with BIOS and a password | ||
| basic = runTest { | ||
| name = "grub-basic"; | ||
| meta.maintainers = with pkgs.lib.maintainers; [ rnhmjoj ]; | ||
|
|
||
| nodes.machine = { ... }: { | ||
| virtualisation.useBootLoader = true; | ||
| boot.loader.timeout = null; | ||
| boot.loader.grub = { | ||
| enable = true; | ||
| users.alice.password = "supersecret"; | ||
| # OCR is not accurate enough | ||
| extraConfig = "serial; terminal_output serial"; | ||
| }; | ||
| }; | ||
|
|
||
| testScript = '' | ||
| def grub_login_as(user, password): | ||
| """ | ||
| Enters user and password to log into GRUB | ||
| """ | ||
| machine.wait_for_console_text("Enter username:") | ||
| machine.send_chars(user + "\n") | ||
| machine.wait_for_console_text("Enter password:") | ||
| machine.send_chars(password + "\n") | ||
|
|
||
|
|
||
| def grub_select_all_configurations(): | ||
| """ | ||
| Selects "All configurations" from the GRUB menu | ||
| to trigger a login request. | ||
| """ | ||
| machine.send_monitor_command("sendkey down") | ||
| machine.send_monitor_command("sendkey ret") | ||
|
|
||
|
|
||
| machine.start() | ||
|
|
||
| # wait for grub screen | ||
| machine.wait_for_console_text("GNU GRUB") | ||
|
|
||
| grub_select_all_configurations() | ||
| with subtest("Invalid credentials are rejected"): | ||
| grub_login_as("wronguser", "wrongsecret") | ||
| machine.wait_for_console_text("error: access denied.") | ||
|
|
||
| grub_select_all_configurations() | ||
| with subtest("Valid credentials are accepted"): | ||
| grub_login_as("alice", "supersecret") | ||
| machine.send_chars("\n") # press enter to boot | ||
| machine.wait_for_console_text("Linux version") | ||
|
|
||
| with subtest("Machine boots correctly"): | ||
| machine.wait_for_unit("multi-user.target") | ||
| ''; | ||
| }; | ||
|
|
||
| # Test boot loader entries on EFI | ||
| bls-efi = runTest { | ||
| name = "grub-bls-efi"; | ||
| meta.maintainers = with pkgs.lib.maintainers; [ rnhmjoj ]; | ||
|
|
||
| nodes.machine = { pkgs, ... }: { | ||
| virtualisation.useBootLoader = true; | ||
| virtualisation.useEFIBoot = true; | ||
| boot.loader.efi.canTouchEfiVariables = true; | ||
| boot.loader.grub.enable = true; | ||
| boot.loader.grub.efiSupport = true; | ||
| }; | ||
|
|
||
| testScript = '' | ||
| with subtest("Machine boots correctly"): | ||
| machine.wait_for_unit("multi-user.target") | ||
|
|
||
| with subtest("Boot entries are installed"): | ||
| entries = machine.succeed("bootctl list") | ||
| print(entries) | ||
| error = "NixOS boot entry not found in bootctl list." | ||
| assert "version: Generation 1" in entries, error | ||
|
|
||
| nodes.machine = { ... }: { | ||
| virtualisation.useBootLoader = true; | ||
| with subtest("systemctl kexec can detect the kernel"): | ||
| machine.succeed("systemctl kexec --dry-run") | ||
|
|
||
| boot.loader.timeout = null; | ||
| boot.loader.grub = { | ||
| enable = true; | ||
| users.alice.password = "supersecret"; | ||
| with subtest("systemctl kexec really works"): | ||
| machine.execute("systemctl kexec", check_return=False) | ||
| machine.connected = False | ||
| machine.connect() | ||
| machine.wait_for_unit("multi-user.target") | ||
| ''; | ||
| }; | ||
|
|
||
| # Test boot loader entries on BIOS | ||
| bls-bios = runTest { | ||
| name = "grub-bls-bios"; | ||
| meta.maintainers = with pkgs.lib.maintainers; [ rnhmjoj ]; | ||
|
|
||
| # OCR is not accurate enough | ||
| extraConfig = "serial; terminal_output serial"; | ||
| nodes.machine = { pkgs, ... }: { | ||
| virtualisation.useBootLoader = true; | ||
| boot.loader.grub.enable = true; | ||
| }; | ||
|
|
||
| testScript = '' | ||
| with subtest("Machine boots correctly"): | ||
| machine.wait_for_unit("multi-user.target") | ||
|
|
||
| with subtest("Boot entries are installed"): | ||
| machine.succeed("test -f /boot/loader/entries/nixos-generation-1.conf") | ||
|
|
||
| with subtest("systemctl kexec can detect the kernel"): | ||
| machine.succeed("systemctl kexec --dry-run") | ||
|
|
||
| with subtest("systemctl kexec really works"): | ||
| machine.execute("systemctl kexec", check_return=False) | ||
| machine.connected = False | ||
| machine.connect() | ||
| machine.wait_for_unit("multi-user.target") | ||
| ''; | ||
| }; | ||
|
|
||
| testScript = '' | ||
| def grub_login_as(user, password): | ||
| """ | ||
| Enters user and password to log into GRUB | ||
| """ | ||
| machine.wait_for_console_text("Enter username:") | ||
| machine.send_chars(user + "\n") | ||
| machine.wait_for_console_text("Enter password:") | ||
| machine.send_chars(password + "\n") | ||
|
|
||
|
|
||
| def grub_select_all_configurations(): | ||
| """ | ||
| Selects "All configurations" from the GRUB menu | ||
| to trigger a login request. | ||
| """ | ||
| machine.send_monitor_command("sendkey down") | ||
| machine.send_monitor_command("sendkey ret") | ||
|
|
||
|
|
||
| machine.start() | ||
|
|
||
| # wait for grub screen | ||
| machine.wait_for_console_text("GNU GRUB") | ||
|
|
||
| grub_select_all_configurations() | ||
| with subtest("Invalid credentials are rejected"): | ||
| grub_login_as("wronguser", "wrongsecret") | ||
| machine.wait_for_console_text("error: access denied.") | ||
|
|
||
| grub_select_all_configurations() | ||
| with subtest("Valid credentials are accepted"): | ||
| grub_login_as("alice", "supersecret") | ||
| machine.send_chars("\n") # press enter to boot | ||
| machine.wait_for_console_text("Linux version") | ||
|
|
||
| with subtest("Machine boots correctly"): | ||
| machine.wait_for_unit("multi-user.target") | ||
| ''; | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part confused me for several reasons. I thought there was an error here, but after looking closer, I see there isn't. But I do still have some nitpicks about what confused me.
bootPathvariable name that is already used nearby in this module