notes: 2.3.0 -> 2.3.1 (#352950)
[NixPkgs.git] / nixos / tests / systemd-boot.nix
blob812d6088ed4e2b9affa8654b3ab35db708533bf6
2   system ? builtins.currentSystem,
3   config ? { },
4   pkgs ? import ../.. { inherit system config; },
5 }:
7 with import ../lib/testing-python.nix { inherit system pkgs; };
8 with pkgs.lib;
10 let
11   common = {
12     virtualisation.useBootLoader = true;
13     virtualisation.useEFIBoot = true;
14     boot.loader.systemd-boot.enable = true;
15     boot.loader.efi.canTouchEfiVariables = true;
16     environment.systemPackages = [ pkgs.efibootmgr ];
17     system.switch.enable = true;
18   };
20   commonXbootldr =
21     {
22       config,
23       lib,
24       pkgs,
25       ...
26     }:
27     let
28       diskImage = import ../lib/make-disk-image.nix {
29         inherit config lib pkgs;
30         label = "nixos";
31         format = "qcow2";
32         partitionTableType = "efixbootldr";
33         touchEFIVars = true;
34         installBootLoader = true;
35       };
36     in
37     {
38       imports = [ common ];
39       virtualisation.useBootLoader = lib.mkForce false; # Only way to tell qemu-vm not to create the default system image
40       virtualisation.directBoot.enable = false; # But don't direct boot either because we're testing systemd-boot
42       system.build.diskImage = diskImage; # Use custom disk image with an XBOOTLDR partition
43       virtualisation.efi.variables = "${diskImage}/efi-vars.fd";
45       virtualisation.useDefaultFilesystems = false; # Needs custom setup for `diskImage`
46       virtualisation.bootPartition = null;
47       virtualisation.fileSystems = {
48         "/" = {
49           device = "/dev/vda3";
50           fsType = "ext4";
51         };
52         "/boot" = {
53           device = "/dev/vda2";
54           fsType = "vfat";
55           noCheck = true;
56         };
57         "/efi" = {
58           device = "/dev/vda1";
59           fsType = "vfat";
60           noCheck = true;
61         };
62       };
64       boot.loader.systemd-boot.enable = true;
65       boot.loader.efi.efiSysMountPoint = "/efi";
66       boot.loader.systemd-boot.xbootldrMountPoint = "/boot";
67     };
69   customDiskImage = nodes: ''
70     import os
71     import subprocess
72     import tempfile
74     tmp_disk_image = tempfile.NamedTemporaryFile()
76     subprocess.run([
77       "${nodes.machine.virtualisation.qemu.package}/bin/qemu-img",
78       "create",
79       "-f",
80       "qcow2",
81       "-b",
82       "${nodes.machine.system.build.diskImage}/nixos.qcow2",
83       "-F",
84       "qcow2",
85       tmp_disk_image.name,
86     ])
88     # Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
89     os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
90   '';
93   basic = makeTest {
94     name = "systemd-boot";
95     meta.maintainers = with pkgs.lib.maintainers; [
96       danielfullmer
97       julienmalka
98     ];
100     nodes.machine = common;
102     testScript = ''
103       machine.start()
104       machine.wait_for_unit("multi-user.target")
106       machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
107       machine.succeed("grep 'sort-key nixos' /boot/loader/entries/nixos-generation-1.conf")
109       # Ensure we actually booted using systemd-boot
110       # Magic number is the vendor UUID used by systemd-boot.
111       machine.succeed(
112           "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
113       )
115       # "bootctl install" should have created an EFI entry
116       machine.succeed('efibootmgr | grep "Linux Boot Manager"')
117     '';
118   };
120   # Test that systemd-boot works with secure boot
121   secureBoot = makeTest {
122     name = "systemd-boot-secure-boot";
124     nodes.machine = {
125       imports = [ common ];
126       environment.systemPackages = [ pkgs.sbctl ];
127       virtualisation.useSecureBoot = true;
128     };
130     testScript =
131       let
132         efiArch = pkgs.stdenv.hostPlatform.efiArch;
133       in
134       { nodes, ... }:
135       ''
136         machine.start(allow_reboot=True)
137         machine.wait_for_unit("multi-user.target")
139         machine.succeed("sbctl create-keys")
140         machine.succeed("sbctl enroll-keys --yes-this-might-brick-my-machine")
141         machine.succeed('sbctl sign /boot/EFI/systemd/systemd-boot${efiArch}.efi')
142         machine.succeed('sbctl sign /boot/EFI/BOOT/BOOT${toUpper efiArch}.EFI')
143         machine.succeed('sbctl sign /boot/EFI/nixos/*${nodes.machine.system.boot.loader.kernelFile}.efi')
145         machine.reboot()
147         assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status")
148       '';
149   };
151   basicXbootldr = makeTest {
152     name = "systemd-boot-xbootldr";
153     meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];
155     nodes.machine = commonXbootldr;
157     testScript =
158       { nodes, ... }:
159       ''
160         ${customDiskImage nodes}
162         machine.start()
163         machine.wait_for_unit("multi-user.target")
165         machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi")
166         machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
168         # Ensure we actually booted using systemd-boot
169         # Magic number is the vendor UUID used by systemd-boot.
170         machine.succeed(
171             "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
172         )
174         # "bootctl install" should have created an EFI entry
175         machine.succeed('efibootmgr | grep "Linux Boot Manager"')
176       '';
177   };
179   # Check that specialisations create corresponding boot entries.
180   specialisation = makeTest {
181     name = "systemd-boot-specialisation";
182     meta.maintainers = with pkgs.lib.maintainers; [
183       lukegb
184       julienmalka
185     ];
187     nodes.machine =
188       { pkgs, lib, ... }:
189       {
190         imports = [ common ];
191         specialisation.something.configuration = {
192           boot.loader.systemd-boot.sortKey = "something";
194           # Since qemu will dynamically create a devicetree blob when starting
195           # up, it is not straight forward to create an export of that devicetree
196           # blob without knowing before-hand all the flags we would pass to qemu
197           # (we would then be able to use `dumpdtb`). Thus, the following config
198           # will not boot, but it does allow us to assert that the boot entry has
199           # the correct contents.
200           boot.loader.systemd-boot.installDeviceTree = pkgs.stdenv.hostPlatform.isAarch64;
201           hardware.deviceTree.name = "dummy.dtb";
202           hardware.deviceTree.package = lib.mkForce (
203             pkgs.runCommand "dummy-devicetree-package" { } ''
204               mkdir -p $out
205               cp ${pkgs.emptyFile} $out/dummy.dtb
206             ''
207           );
208         };
209       };
211     testScript =
212       { nodes, ... }:
213       ''
214         machine.start()
215         machine.wait_for_unit("multi-user.target")
217         machine.succeed(
218             "test -e /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
219         )
220         machine.succeed(
221             "grep -q 'title NixOS (something)' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
222         )
223         machine.succeed(
224             "grep 'sort-key something' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
225         )
226       ''
227       + pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isAarch64 ''
228         machine.succeed(
229             r"grep 'devicetree /EFI/nixos/[a-z0-9]\{32\}.*dummy' /boot/loader/entries/nixos-generation-1-specialisation-something.conf"
230         )
231       '';
232   };
234   # Boot without having created an EFI entry--instead using default "/EFI/BOOT/BOOTX64.EFI"
235   fallback = makeTest {
236     name = "systemd-boot-fallback";
237     meta.maintainers = with pkgs.lib.maintainers; [
238       danielfullmer
239       julienmalka
240     ];
242     nodes.machine =
243       { pkgs, lib, ... }:
244       {
245         imports = [ common ];
246         boot.loader.efi.canTouchEfiVariables = mkForce false;
247       };
249     testScript = ''
250       machine.start()
251       machine.wait_for_unit("multi-user.target")
253       machine.succeed("test -e /boot/loader/entries/nixos-generation-1.conf")
255       # Ensure we actually booted using systemd-boot
256       # Magic number is the vendor UUID used by systemd-boot.
257       machine.succeed(
258           "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
259       )
261       # "bootctl install" should _not_ have created an EFI entry
262       machine.fail('efibootmgr | grep "Linux Boot Manager"')
263     '';
264   };
266   update = makeTest {
267     name = "systemd-boot-update";
268     meta.maintainers = with pkgs.lib.maintainers; [
269       danielfullmer
270       julienmalka
271     ];
273     nodes.machine = common;
275     testScript = ''
276       machine.succeed("mount -o remount,rw /boot")
278       def switch():
279           # Replace version inside sd-boot with something older. See magic[] string in systemd src/boot/efi/boot.c
280           machine.succeed(
281             """
282             find /boot -iname '*boot*.efi' -print0 | \
283             xargs -0 -I '{}' sed -i 's/#### LoaderInfo: systemd-boot .* ####/#### LoaderInfo: systemd-boot 000.0-1-notnixos ####/' '{}'
284             """
285           )
286           return machine.succeed("/run/current-system/bin/switch-to-configuration boot 2>&1")
288       output = switch()
289       assert "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
290       assert 'to "/boot/EFI/systemd/systemd-bootx64.efi"' in output, "systemd-boot not copied to to /boot/EFI/systemd/systemd-bootx64.efi"
291       assert 'to "/boot/EFI/BOOT/BOOTX64.EFI"' in output, "systemd-boot not copied to to /boot/EFI/BOOT/BOOTX64.EFI"
293       with subtest("Test that updating works with lowercase bootx64.efi"):
294           machine.succeed(
295               # Move to tmp file name first, otherwise mv complains the new location is the same
296               "mv /boot/EFI/BOOT/BOOTX64.EFI /boot/EFI/BOOT/bootx64.efi.new",
297               "mv /boot/EFI/BOOT/bootx64.efi.new /boot/EFI/BOOT/bootx64.efi",
298           )
299           output = switch()
300           assert "updating systemd-boot from 000.0-1-notnixos to " in output, "Couldn't find systemd-boot update message"
301           assert 'to "/boot/EFI/systemd/systemd-bootx64.efi"' in output, "systemd-boot not copied to to /boot/EFI/systemd/systemd-bootx64.efi"
302           assert 'to "/boot/EFI/BOOT/BOOTX64.EFI"' in output, "systemd-boot not copied to to /boot/EFI/BOOT/BOOTX64.EFI"
303     '';
304   };
306   memtest86 =
307     with pkgs.lib;
308     optionalAttrs (meta.availableOn { inherit system; } pkgs.memtest86plus) (makeTest {
309       name = "systemd-boot-memtest86";
310       meta.maintainers = with maintainers; [ julienmalka ];
312       nodes.machine =
313         { pkgs, lib, ... }:
314         {
315           imports = [ common ];
316           boot.loader.systemd-boot.memtest86.enable = true;
317         };
319       testScript = ''
320         machine.succeed("test -e /boot/loader/entries/memtest86.conf")
321         machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
322       '';
323     });
325   netbootxyz = makeTest {
326     name = "systemd-boot-netbootxyz";
327     meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
329     nodes.machine =
330       { pkgs, lib, ... }:
331       {
332         imports = [ common ];
333         boot.loader.systemd-boot.netbootxyz.enable = true;
334       };
336     testScript = ''
337       machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
338       machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
339     '';
340   };
342   edk2-uefi-shell = makeTest {
343     name = "systemd-boot-edk2-uefi-shell";
344     meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ];
346     nodes.machine = { ... }: {
347       imports = [ common ];
348       boot.loader.systemd-boot.edk2-uefi-shell.enable = true;
349     };
351     testScript = ''
352       machine.succeed("test -e /boot/loader/entries/edk2-uefi-shell.conf")
353       machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
354     '';
355   };
357   windows = makeTest {
358     name = "systemd-boot-windows";
359     meta.maintainers = with pkgs.lib.maintainers; [ iFreilicht ];
361     nodes.machine = { ... }: {
362       imports = [ common ];
363       boot.loader.systemd-boot.windows = {
364         "7" = {
365           efiDeviceHandle = "HD0c1";
366           sortKey = "before_all_others";
367         };
368         "Ten".efiDeviceHandle = "FS0";
369         "11" = {
370           title = "Title with-_-punctuation ...?!";
371           efiDeviceHandle = "HD0d4";
372           sortKey = "zzz";
373         };
374       };
375     };
377     testScript = ''
378       machine.succeed("test -e /boot/efi/edk2-uefi-shell/shell.efi")
380       machine.succeed("test -e /boot/loader/entries/windows_7.conf")
381       machine.succeed("test -e /boot/loader/entries/windows_Ten.conf")
382       machine.succeed("test -e /boot/loader/entries/windows_11.conf")
384       machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_7.conf")
385       machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_Ten.conf")
386       machine.succeed("grep 'efi /efi/edk2-uefi-shell/shell.efi' /boot/loader/entries/windows_11.conf")
388       machine.succeed("grep 'HD0c1:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_7.conf")
389       machine.succeed("grep 'FS0:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_Ten.conf")
390       machine.succeed("grep 'HD0d4:EFI\\\\Microsoft\\\\Boot\\\\Bootmgfw.efi' /boot/loader/entries/windows_11.conf")
392       machine.succeed("grep 'sort-key before_all_others' /boot/loader/entries/windows_7.conf")
393       machine.succeed("grep 'sort-key o_windows_Ten' /boot/loader/entries/windows_Ten.conf")
394       machine.succeed("grep 'sort-key zzz' /boot/loader/entries/windows_11.conf")
396       machine.succeed("grep 'title Windows 7' /boot/loader/entries/windows_7.conf")
397       machine.succeed("grep 'title Windows Ten' /boot/loader/entries/windows_Ten.conf")
398       machine.succeed('grep "title Title with-_-punctuation ...?!" /boot/loader/entries/windows_11.conf')
399     '';
400   };
402   memtestSortKey = makeTest {
403     name = "systemd-boot-memtest-sortkey";
404     meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
406     nodes.machine =
407       { pkgs, lib, ... }:
408       {
409         imports = [ common ];
410         boot.loader.systemd-boot.memtest86.enable = true;
411         boot.loader.systemd-boot.memtest86.sortKey = "apple";
412       };
414     testScript = ''
415       machine.succeed("test -e /boot/loader/entries/memtest86.conf")
416       machine.succeed("test -e /boot/efi/memtest86/memtest.efi")
417       machine.succeed("grep 'sort-key apple' /boot/loader/entries/memtest86.conf")
418     '';
419   };
421   entryFilenameXbootldr = makeTest {
422     name = "systemd-boot-entry-filename-xbootldr";
423     meta.maintainers = with pkgs.lib.maintainers; [ sdht0 ];
425     nodes.machine =
426       { pkgs, lib, ... }:
427       {
428         imports = [ commonXbootldr ];
429         boot.loader.systemd-boot.memtest86.enable = true;
430       };
432     testScript =
433       { nodes, ... }:
434       ''
435         ${customDiskImage nodes}
437         machine.start()
438         machine.wait_for_unit("multi-user.target")
440         machine.succeed("test -e /efi/EFI/systemd/systemd-bootx64.efi")
441         machine.succeed("test -e /boot/loader/entries/memtest86.conf")
442         machine.succeed("test -e /boot/EFI/memtest86/memtest.efi")
443       '';
444   };
446   extraEntries = makeTest {
447     name = "systemd-boot-extra-entries";
448     meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
450     nodes.machine =
451       { pkgs, lib, ... }:
452       {
453         imports = [ common ];
454         boot.loader.systemd-boot.extraEntries = {
455           "banana.conf" = ''
456             title banana
457           '';
458         };
459       };
461     testScript = ''
462       machine.succeed("test -e /boot/loader/entries/banana.conf")
463       machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/banana.conf")
464     '';
465   };
467   extraFiles = makeTest {
468     name = "systemd-boot-extra-files";
469     meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
471     nodes.machine =
472       { pkgs, lib, ... }:
473       {
474         imports = [ common ];
475         boot.loader.systemd-boot.extraFiles = {
476           "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
477         };
478       };
480     testScript = ''
481       machine.succeed("test -e /boot/efi/fruits/tomato.efi")
482       machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
483     '';
484   };
486   switch-test = makeTest {
487     name = "systemd-boot-switch-test";
488     meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
490     nodes = {
491       inherit common;
493       machine =
494         { pkgs, nodes, ... }:
495         {
496           imports = [ common ];
497           boot.loader.systemd-boot.extraFiles = {
498             "efi/fruits/tomato.efi" = pkgs.netbootxyz-efi;
499           };
501           # These are configs for different nodes, but we'll use them here in `machine`
502           system.extraDependencies = [
503             nodes.common.system.build.toplevel
504             nodes.with_netbootxyz.system.build.toplevel
505           ];
506         };
508       with_netbootxyz =
509         { pkgs, ... }:
510         {
511           imports = [ common ];
512           boot.loader.systemd-boot.netbootxyz.enable = true;
513         };
514     };
516     testScript =
517       { nodes, ... }:
518       let
519         originalSystem = nodes.machine.system.build.toplevel;
520         baseSystem = nodes.common.system.build.toplevel;
521         finalSystem = nodes.with_netbootxyz.system.build.toplevel;
522       in
523       ''
524         machine.succeed("test -e /boot/efi/fruits/tomato.efi")
525         machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
527         with subtest("remove files when no longer needed"):
528             machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
529             machine.fail("test -e /boot/efi/fruits/tomato.efi")
530             machine.fail("test -d /boot/efi/fruits")
531             machine.succeed("test -d /boot/efi/nixos/.extra-files")
532             machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
533             machine.fail("test -d /boot/efi/nixos/.extra-files/efi/fruits")
535         with subtest("files are added back when needed again"):
536             machine.succeed("${originalSystem}/bin/switch-to-configuration boot")
537             machine.succeed("test -e /boot/efi/fruits/tomato.efi")
538             machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
540         with subtest("simultaneously removing and adding files works"):
541             machine.succeed("${finalSystem}/bin/switch-to-configuration boot")
542             machine.fail("test -e /boot/efi/fruits/tomato.efi")
543             machine.fail("test -e /boot/efi/nixos/.extra-files/efi/fruits/tomato.efi")
544             machine.succeed("test -e /boot/loader/entries/netbootxyz.conf")
545             machine.succeed("test -e /boot/efi/netbootxyz/netboot.xyz.efi")
546             machine.succeed("test -e /boot/efi/nixos/.extra-files/loader/entries/netbootxyz.conf")
547             machine.succeed("test -e /boot/efi/nixos/.extra-files/efi/netbootxyz/netboot.xyz.efi")
548       '';
549   };
551   garbage-collect-entry = makeTest {
552     name = "systemd-boot-garbage-collect-entry";
553     meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
555     nodes = {
556       inherit common;
557       machine =
558         { pkgs, nodes, ... }:
559         {
560           imports = [ common ];
562           # These are configs for different nodes, but we'll use them here in `machine`
563           system.extraDependencies = [
564             nodes.common.system.build.toplevel
565           ];
566         };
567     };
569     testScript =
570       { nodes, ... }:
571       let
572         baseSystem = nodes.common.system.build.toplevel;
573       in
574       ''
575         machine.succeed("nix-env -p /nix/var/nix/profiles/system --set ${baseSystem}")
576         machine.succeed("nix-env -p /nix/var/nix/profiles/system --delete-generations 1")
577         machine.succeed("${baseSystem}/bin/switch-to-configuration boot")
578         machine.fail("test -e /boot/loader/entries/nixos-generation-1.conf")
579         machine.succeed("test -e /boot/loader/entries/nixos-generation-2.conf")
580       '';
581   };
583   no-bootspec = makeTest {
584     name = "systemd-boot-no-bootspec";
585     meta.maintainers = with pkgs.lib.maintainers; [ julienmalka ];
587     nodes.machine = {
588       imports = [ common ];
589       boot.bootspec.enable = false;
590     };
592     testScript = ''
593       machine.start()
594       machine.wait_for_unit("multi-user.target")
595     '';
596   };