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