~ruther/ruthless-guix

7e69ff0f45dea7ab8f2fdc0a5c033767fcc0169c — Rutherther 2 days ago 48d5aa9 main
feat: add LVM on LUKS test
3 files changed, 303 insertions(+), 3 deletions(-)

M modules/ruther/tests/lvm.scm
M modules/ruthless/bootloader/grub.scm
M modules/ruthless/image.scm
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))