diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 029a2543ec93..21dad5f3ced8 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -726,7 +726,13 @@ in greetd-no-shadow = runTest ./greetd-no-shadow.nix; grocy = runTest ./grocy.nix; grow-partition = runTest ./grow-partition.nix; - grub = runTest ./grub.nix; + grub = { + basic = runTest ./grub/basic.nix; + efi = runTest ./grub/efi.nix; + graphical = runTest ./grub/graphical.nix; + hashed-password = runTest ./grub/hashed-password.nix; + mirrored-boots = runTest ./grub/mirrored-boots.nix; + }; gs1200-exporter = runTest ./gs1200-exporter.nix; guacamole-server = runTest ./guacamole-server.nix; guix = handleTest ./guix { }; diff --git a/nixos/tests/grub.nix b/nixos/tests/grub/basic.nix similarity index 100% rename from nixos/tests/grub.nix rename to nixos/tests/grub/basic.nix diff --git a/nixos/tests/grub/efi.nix b/nixos/tests/grub/efi.nix new file mode 100644 index 000000000000..953548bb24bb --- /dev/null +++ b/nixos/tests/grub/efi.nix @@ -0,0 +1,45 @@ +{ lib, ... }: +{ + name = "grub-efi"; + + meta = with lib.maintainers; { + maintainers = [ + tomfitzhenry + rnhmjoj + ]; + }; + + nodes.machine = + { ... }: + { + virtualisation.useBootLoader = true; + virtualisation.useEFIBoot = true; + + boot.loader.grub = { + enable = true; + efiSupport = true; + device = "nodev"; + + # Read GRUB from the serial console so its output can be matched + # deterministically with wait_for_console_text, rather than via OCR. + extraConfig = "serial; terminal_output serial"; + }; + boot.loader.efi.canTouchEfiVariables = true; + }; + + testScript = '' + machine.start() + + with subtest("Enters GRUB"): + machine.wait_for_console_text("GNU GRUB") + + with subtest("Loads kernel"): + machine.wait_for_console_text("Linux version") + + with subtest("Reaches multi-user target"): + machine.wait_for_unit("multi-user.target") + + with subtest("Boots via UEFI"): + machine.succeed("test -d /sys/firmware/efi") + ''; +} diff --git a/nixos/tests/grub/graphical.nix b/nixos/tests/grub/graphical.nix new file mode 100644 index 000000000000..54d25ce3d843 --- /dev/null +++ b/nixos/tests/grub/graphical.nix @@ -0,0 +1,57 @@ +{ lib, ... }: +{ + name = "grub-graphical"; + + meta = with lib.maintainers; { + maintainers = [ + tomfitzhenry + rnhmjoj + ]; + }; + + nodes.machine = + { pkgs, ... }: + let + # GRUB only draws a background image when it is in graphical gfxterm + # mode. Bake a marker string into the splash so the OCR check below can + # only succeed when gfxterm actually rendered it; a silent fallback to + # text mode would show the plain menu without this image (and its text). + # + # The image must be an 8-bit sRGB PNG, otherwise GRUB's png module fails + # to load it and silently falls back to its default (non-graphical) menu. + splash = pkgs.runCommand "grub-gfxterm-splash.png" { nativeBuildInputs = [ pkgs.imagemagick ]; } '' + magick -size 1024x768 xc:'#2d2d2d' \ + -font ${pkgs.dejavu_fonts.minimal}/share/fonts/truetype/DejaVuSans.ttf \ + -gravity south -pointsize 48 -fill white \ + -annotate +0+150 'GFXTERMOK' \ + -depth 8 -type TrueColor PNG24:$out + ''; + in + { + virtualisation.useBootLoader = true; + + # Leave the menu up long enough for OCR to catch it; we boot early with a + # keypress once it has, so this does not slow the test down. + boot.loader.timeout = 30; + boot.loader.grub = { + enable = true; + splashImage = splash; + }; + }; + + enableOCR = true; + + testScript = '' + machine.start() + + with subtest("GRUB renders its menu graphically (gfxterm), showing the splash"): + # The marker text lives inside the background image, so reading it back + # proves GRUB displayed the image rather than falling back to text mode. + machine.wait_for_text("GFXTERMOK") + machine.screenshot("grub_gfxterm") + + with subtest("Machine boots into NixOS from GRUB"): + machine.send_key("ret") + machine.wait_for_unit("multi-user.target") + ''; +} diff --git a/nixos/tests/grub/hashed-password.nix b/nixos/tests/grub/hashed-password.nix new file mode 100644 index 000000000000..191468f8b69a --- /dev/null +++ b/nixos/tests/grub/hashed-password.nix @@ -0,0 +1,77 @@ +{ lib, ... }: +{ + name = "grub-hashed-password"; + + meta = with lib.maintainers; { + maintainers = [ + tomfitzhenry + rnhmjoj + ]; + }; + + nodes.machine = + { pkgs, ... }: + let + mkGrubPbkdf2HashFile = + password: + toString ( + pkgs.runCommandLocal "grub-pbkdf2-hash" { nativeBuildInputs = [ pkgs.grub2 ]; } '' + printf "%s\n%s\n" "${password}" "${password}" | grub-mkpasswd-pbkdf2 | grep -o 'grub\.pbkdf2\.[^[:space:]]*' > $out + '' + ); + in + { + virtualisation.useBootLoader = true; + + boot.loader.timeout = null; + boot.loader.grub = { + enable = true; + users.bob.hashedPasswordFile = mkGrubPbkdf2HashFile "bobsecret"; + + # Read GRUB from the serial console so its output can be matched + # deterministically; OCR would work but is flakier, which matters for + # the multi-step interactive login exercised below. + 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 hashedPassword credentials are rejected"): + grub_login_as("bob", "wrongsecret") + machine.wait_for_console_text("access denied") + + grub_select_all_configurations() + with subtest("Valid hashedPassword credentials are accepted"): + grub_login_as("bob", "bobsecret") + 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") + ''; +} diff --git a/nixos/tests/grub/mirrored-boots.nix b/nixos/tests/grub/mirrored-boots.nix new file mode 100644 index 000000000000..f3059bf675bc --- /dev/null +++ b/nixos/tests/grub/mirrored-boots.nix @@ -0,0 +1,61 @@ +{ lib, ... }: +{ + name = "grub-mirrored-boots"; + + meta = with lib.maintainers; { + maintainers = [ + tomfitzhenry + rnhmjoj + ]; + }; + + nodes.machine = + { lib, ... }: + { + virtualisation.useBootLoader = true; + + boot.loader.timeout = null; + boot.loader.grub = { + enable = true; + device = lib.mkOverride 0 ""; + mirroredBoots = [ + { + path = "/boot1"; + devices = [ "/dev/vda" ]; + } + { + path = "/boot2"; + devices = [ "nodev" ]; + } + ]; + + # Read GRUB from the serial console so its output can be matched + # deterministically with wait_for_console_text, rather than via OCR. + extraConfig = "serial; terminal_output serial"; + }; + }; + + testScript = '' + machine.start() + + # wait for grub screen + machine.wait_for_console_text("GNU GRUB") + + machine.send_chars("\n") # press enter to boot default option + + with subtest("Machine boots correctly"): + machine.wait_for_unit("multi-user.target") + + with subtest("Verify boot path 1 GRUB installation and configuration"): + machine.succeed("test -d /boot1/grub") + machine.succeed("test -f /boot1/grub/grub.cfg") + machine.succeed("test -f /boot1/grub/state") + machine.succeed("grep -q 'menuentry' /boot1/grub/grub.cfg") + + with subtest("Verify boot path 2 GRUB installation and configuration"): + machine.succeed("test -d /boot2/grub") + machine.succeed("test -f /boot2/grub/grub.cfg") + machine.succeed("test -f /boot2/grub/state") + machine.succeed("grep -q 'menuentry' /boot2/grub/grub.cfg") + ''; +}