M modules/ruther/tests/lvm.scm => modules/ruther/tests/lvm.scm +168 -1
@@ 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))))
M modules/ruthless/bootloader/grub.scm => modules/ruthless/bootloader/grub.scm +1 -1
@@ 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
M modules/ruthless/image.scm => modules/ruthless/image.scm +134 -1
@@ 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))