From 7e69ff0f45dea7ab8f2fdc0a5c033767fcc0169c Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sun, 1 Feb 2026 13:49:42 +0100 Subject: [PATCH] feat: add LVM on LUKS test --- modules/ruther/tests/lvm.scm | 169 ++++++++++++++++++++++++++- modules/ruthless/bootloader/grub.scm | 2 +- modules/ruthless/image.scm | 135 ++++++++++++++++++++- 3 files changed, 303 insertions(+), 3 deletions(-) diff --git a/modules/ruther/tests/lvm.scm b/modules/ruther/tests/lvm.scm index 91f0a8e4b3e0b434a2024c914011b0f99bbbcf6b..93a2aba09d7140bbbf775de21766581eaf2b043e 100644 --- a/modules/ruther/tests/lvm.scm +++ b/modules/ruther/tests/lvm.scm @@ -2,10 +2,12 @@ ;;; Copyright © 2025 (define-module (ruther tests lvm) + #:use-module (ruthless bootloader grub) #:use-module (gnu bootloader) #:use-module (gnu bootloader grub) #:use-module (gnu build marionette) #:use-module (gnu packages firmware) + #:use-module (gnu packages ocr) #:use-module (gnu packages virtualization) #:use-module (gnu services) #:use-module (gnu services base) @@ -18,7 +20,8 @@ #:use-module (gnu tests base) #:use-module (guix gexp) #:use-module (ruthless image) - #:export (%test-root-lvm)) + #:export (%test-root-lvm + %test-lvm-on-luks)) ;;; ;;; OS definition with root on LVM @@ -111,3 +114,167 @@ (name "lvm-root") (description "Test basic functionality of a Guix System with root on LVM.") (value (run-lvm-root-test)))) + +;;; +;;; OS definition with LVM on LUKS +;;; + +;; The LUKS passphrase and UUID used during image creation. +;; These must match the values in make-lvm-on-luks-disk-initializer. +(define %luks-passphrase "testpassword") +(define %luks-uuid "12345678-1234-1234-1234-123456789abc") + +(define %lvm-on-luks-os + ;; Operating system with root and home on LVM, which sits on a LUKS + ;; encrypted partition. + ;; + ;; Mapped devices dependency chain: + ;; /dev/vda3 (LUKS partition) + ;; -> /dev/mapper/cryptroot (opened LUKS container) + ;; -> vg0 (LVM volume group on cryptroot) + ;; -> /dev/mapper/vg0-root (root LV) + ;; -> /dev/mapper/vg0-home (home LV) + (operating-system + (host-name "crptlvm") ;; NOTE: no y, because that one the OCR does not like. + (timezone "Europe/Berlin") + (locale "en_US.UTF-8") + + (bootloader (bootloader-configuration + (bootloader grub-efi-removable-bootloader) + (targets '("/boot/efi")) + (terminal-outputs '(console)))) + + ;; Note: Do not pass "console=ttyS0" so we can use our passphrase prompt + ;; detection logic in 'enter-luks-passphrase' which uses OCR. + + ;; Mapped devices: first LUKS, then LVM on top + (mapped-devices + (list + ;; LUKS encrypted partition + (mapped-device + (source (uuid %luks-uuid)) + (targets '("cryptroot")) + (type luks-device-mapping)) + ;; LVM volume group on the LUKS container + (mapped-device + (source "vg0") + (targets '("vg0-root" "vg0-home")) + (type lvm-device-mapping)))) + + (file-systems + (cons* (file-system + (device "/dev/mapper/vg0-root") + (mount-point "/") + (type "ext4") + (dependencies mapped-devices)) + (file-system + (device (file-system-label "ESP")) + (mount-point "/boot/efi") + (type "vfat")) + (file-system + (device "/dev/mapper/vg0-home") + (mount-point "/home") + (type "ext4") + (dependencies mapped-devices)) + %base-file-systems)) + + (users (cons (user-account + (name "alice") + (group "users") + (supplementary-groups '("wheel"))) + %base-user-accounts)) + + (services %base-services))) + +;;; +;;; Test execution +;;; + +;; Taken from the GNU Guix channel, with edits. +(define (enter-luks-passphrase marionette) + "Return a gexp to be inserted in the basic system test running on MARIONETTE +to enter the LUKS passphrase." + (let ((ocrad (file-append ocrad "/bin/ocrad"))) + #~(begin + (define (passphrase-prompt? text) + (string-contains (pk 'screen-text text) "Enter pass")) + + (test-assert "enter LUKS passphrase for GRUB" + (begin + + ;; At this point we have no choice but to use OCR to determine + ;; when the passphrase should be entered. + (wait-for-screen-text #$marionette passphrase-prompt? + #:ocr #$ocrad + #:timeout 60) + + (marionette-control (string-append "screendump " #$output + "/post-grub-passphrase.ppm") + #$marionette) + + (marionette-type #$(string-append %luks-passphrase "\n") + #$marionette) + + ;; It's hard to determine what to wait for here. + ;; So just wait long enough. + (sleep 40))) + + (test-assert "enter LUKS passphrase for the initrd" + (begin + ;; XXX: Here we use OCR as well but we could instead use QEMU + ;; '-serial stdio' and run it in an input pipe, + (wait-for-screen-text #$marionette passphrase-prompt? + #:ocr #$ocrad + #:timeout 60) + (marionette-type #$(string-append %luks-passphrase "\n") + #$marionette) + + ;; Take a screenshot for debugging purposes. + (marionette-control (string-append "screendump " #$output + "/post-initrd-passphrase.ppm") + #$marionette)))))) + +(define (run-lvm-on-luks-test) + "Run the basic test suite on an OS with LVM on LUKS." + (define os + (marionette-operating-system + %lvm-on-luks-os + #:imported-modules '((gnu services herd) + (guix combinators)))) + + ;; Use the generic disk image builder with LVM-on-LUKS initializer + (define image + (build-disk-image os + #:disk-initializer (make-lvm-on-luks-disk-initializer + #:luks-passphrase %luks-passphrase + #:luks-uuid %luks-uuid) + #:disk-size (* 4096 1024 1024) ; 4 GiB (need more space for LUKS + LVM) + #:uefi? #t + #:name "lvm-on-luks-image")) + + (define vm-command + #~(list (string-append #$qemu-minimal "/bin/" + #$(qemu-command)) + "-m" "1024" ; More memory for crypto operations + "-bios" #$(file-append ovmf-x86-64 "/share/firmware/ovmf_x64.bin") + "-drive" + (string-append "file=" #$image + ",format=qcow2,if=virtio") + "-snapshot" ; Use snapshot mode since image is in read-only store + "-no-reboot" + #$@(if (file-exists? "/dev/kvm") + '("-enable-kvm") + '()))) + + (run-basic-test os vm-command "lvm-on-luks" + #:initialization enter-luks-passphrase)) + +;;; +;;; System test definition +;;; + +(define %test-lvm-on-luks + (system-test + (name "lvm-on-luks") + (description "Test basic functionality of a Guix System with LVM on LUKS encryption.") + (value (run-lvm-on-luks-test)))) diff --git a/modules/ruthless/bootloader/grub.scm b/modules/ruthless/bootloader/grub.scm index 6b2db1de577e38119a155c7569a1fcf62df269c6..a8724b320f12d795cf17e09a3c5c1187c2cb4d6f 100644 --- a/modules/ruthless/bootloader/grub.scm +++ b/modules/ruthless/bootloader/grub.scm @@ -156,7 +156,7 @@ (lambda (port) (use-modules (ice-9 textual-ports)) ;; Sneek in insmod lvm at beginning of the file - (display "insmod lvm\n" port) + (display "insmod lvm\ninsmod luks\ninsmod luks2\n" port) ;; After, copy the original file. (display (call-with-input-file #$original-grub-cfg get-string-all) port))))) (computed-file "grub.cfg" builder diff --git a/modules/ruthless/image.scm b/modules/ruthless/image.scm index e1d49156931cb26610a90b35fa9dde77ee83d58c..8a07880d71bc7fc3b0f7f7dca40b1a34ebb2129f 100644 --- a/modules/ruthless/image.scm +++ b/modules/ruthless/image.scm @@ -47,7 +47,9 @@ make-lvm-disk-initializer lvm-disk-initializer make-simple-gpt-disk-initializer - simple-gpt-disk-initializer)) + simple-gpt-disk-initializer + make-lvm-on-luks-disk-initializer + lvm-on-luks-disk-initializer)) ;;; ;;; Disk initializer record @@ -446,3 +448,134 @@ The partition layout is: (define simple-gpt-disk-initializer (make-simple-gpt-disk-initializer)) + +(define* (make-lvm-on-luks-disk-initializer #:key + (luks-name "cryptroot") + (luks-uuid "12345678-1234-1234-1234-123456789abc") + (luks-passphrase "testpassword") + (volume-group "vg0") + (root-volume "root") + (home-volume "home") + (root-size "95%FREE") + (esp-size "100MiB") + (esp-label "ESP") + (root-label "root") + (home-label "home")) + "Create a disk-initializer for LVM on LUKS with UEFI boot. + +The partition layout is: + - 1-3 MiB: BIOS boot partition (for legacy GRUB) + - 3-ESP-SIZE: EFI System Partition + - ESP-SIZE-100%: LUKS-encrypted partition containing: + - VOLUME-GROUP/ROOT-VOLUME (ROOT-SIZE of VG) + - VOLUME-GROUP/HOME-VOLUME (remaining space) + +LUKS-UUID is the UUID to assign to the LUKS container (used by mapped-device). +LUKS-PASSPHRASE is the encryption passphrase (for testing only - in production +use a key file or interactive input)." + (disk-initializer + (name 'lvm-on-luks-uefi) + (packages (list lvm2 cryptsetup)) + (setup-proc + #~(begin + (use-modules (guix build utils)) + + ;; Partition: GPT with BIOS boot, ESP, and LUKS partition + (unless (zero? + (system* #$(file-append parted "/sbin/parted") + "-s" "/dev/vdb" + "mklabel" "gpt" + "mkpart" "bios_grub" "1MiB" "3MiB" + "set" "1" "bios_grub" "on" + "mkpart" "ESP" "fat32" "3MiB" #$esp-size + "set" "2" "esp" "on" + "mkpart" "cryptroot" #$esp-size "100%")) + (error "Failed to partition disk")) + + ;; Create ESP filesystem + (unless (zero? + (system* #$(file-append dosfstools "/sbin/mkfs.fat") + "-F" "32" "-n" #$esp-label "/dev/vdb2")) + (error "Failed to create ESP filesystem")) + + ;; Setup LUKS encryption + ;; Use a pipe to provide the passphrase + (unless (zero? + (system (string-append + "echo -n " #$luks-passphrase + " | " #$(file-append cryptsetup "/sbin/cryptsetup") + " luksFormat --uuid=" #$luks-uuid + " --batch-mode /dev/vdb3 -"))) + (error "Failed to create LUKS container")) + + ;; Open LUKS container + (unless (zero? + (system (string-append + "echo -n " #$luks-passphrase + " | " #$(file-append cryptsetup "/sbin/cryptsetup") + " open /dev/vdb3 " #$luks-name " -"))) + (error "Failed to open LUKS container")) + + ;; Setup LVM on the opened LUKS device + (unless (zero? + (system* #$(file-append lvm2 "/sbin/pvcreate") "-ff" "-y" + #$(string-append "/dev/mapper/" luks-name))) + (error "Failed to create LVM physical volume")) + (unless (zero? + (system* #$(file-append lvm2 "/sbin/vgcreate") #$volume-group + #$(string-append "/dev/mapper/" luks-name))) + (error "Failed to create LVM volume group")) + (unless (zero? + (system* #$(file-append lvm2 "/sbin/lvcreate") + "-l" #$root-size "-n" #$root-volume #$volume-group)) + (error "Failed to create LVM root volume")) + (unless (zero? + (system* #$(file-append lvm2 "/sbin/lvcreate") + "-l" "100%FREE" "-n" #$home-volume #$volume-group)) + (error "Failed to create LVM home volume")) + + ;; Format filesystems + (unless (zero? + (system* #$(file-append e2fsprogs "/sbin/mkfs.ext4") + "-L" #$root-label + #$(string-append "/dev/mapper/" volume-group "-" root-volume))) + (error "Failed to format root filesystem")) + (unless (zero? + (system* #$(file-append e2fsprogs "/sbin/mkfs.ext4") + "-L" #$home-label + #$(string-append "/dev/mapper/" volume-group "-" home-volume))) + (error "Failed to format home filesystem")) + + ;; Mount filesystems + (mkdir-p "/mnt") + (unless (zero? + (system* "mount" + #$(string-append "/dev/mapper/" volume-group "-" root-volume) + "/mnt")) + (error "Failed to mount root filesystem")) + (mkdir-p "/mnt/boot/efi") + (unless (zero? (system* "mount" "/dev/vdb2" "/mnt/boot/efi")) + (error "Failed to mount ESP")) + (mkdir-p "/mnt/home") + (unless (zero? + (system* "mount" + #$(string-append "/dev/mapper/" volume-group "-" home-volume) + "/mnt/home")) + (error "Failed to mount home filesystem")))) + + (cleanup-proc + #~(begin + (sync) + (unless (zero? (system* "umount" "/mnt/home")) + (error "Failed to unmount /mnt/home")) + (unless (zero? (system* "umount" "/mnt/boot/efi")) + (error "Failed to unmount /mnt/boot/efi")) + (unless (zero? (system* "umount" "/mnt")) + (error "Failed to unmount /mnt")) + (unless (zero? (system* #$(file-append lvm2 "/sbin/vgchange") "-an" #$volume-group)) + (error "Failed to deactivate LVM volume group")) + (unless (zero? (system* #$(file-append cryptsetup "/sbin/cryptsetup") "close" #$luks-name)) + (error "Failed to close LUKS container")))))) + +(define lvm-on-luks-disk-initializer + (make-lvm-on-luks-disk-initializer))