diff mbox series

[bug#41011] gnu: grub: Support for network boot via tftp/nfs.

Message ID C69B738F-3B97-4E49-ABFD-96E8DF9BFD9E@vodafonemail.de
State Accepted
Headers show
Series [bug#41011] gnu: grub: Support for network boot via tftp/nfs. | expand

Checks

Context Check Description
cbaines/applying patch fail View Laminar job

Commit Message

Stefan May 21, 2020, 6:40 p.m. UTC
* gnu/bootloader/grub.scm (grub-efi-net-bootloader): New efi bootloader for
network booting via tftp/nfs, prepared for chain loading.
(make-grub-efi-net-bootloader): New macro to define a customized bootloader
based on 'grub-efi-net-bootloader'.
(install-grub-efi-net): New bootloader installer for tftp.
(grub-root-search): Adding support for tftp root.
(eye-candy): Use 'gfxterm' for all systems if selected via 'terminal-outputs'.
* gnu/system.scm (read-boot-parameters): Prevent devices with ":/" from being
treated as a file system label.
---
 gnu/bootloader/grub.scm | 125 +++++++++++++++++++++++++++++-----------
 gnu/system.scm          |   3 +-
 2 files changed, 94 insertions(+), 34 deletions(-)

Comments

Mathieu Othacehe May 23, 2020, 8:10 a.m. UTC | #1
Hey Stefan,

Thanks for rebasing!

> +    #~(lambda (bootloader target mount-point)
> +        "Install GRUB as e.g. \"bootx64.efi\" or \"bootarm64.efi\" \"into
> +EFI-SUBDIR, which is usually \"efi/boot\" or \"efi/Guix\" below the directory

Is that TARGET or EFI-SUBDIR?

> +TARGET for the system whose root is mounted at MOUNT-POINT."
> +        (let* ((mount-point-list (delete "" (string-split mount-point #\/)))
> +               (target-list (delete "" (string-split target #\/)))
> +               (net-dir
> +                (string-append "/" (string-join (append
> +                                                 mount-point-list
> +                                                 target-list)
> +                                                "/")))

I think you can use something like "(in-vicinity mount-point target)"
to do the same job.

> +          ;; Tell 'grub-install' that there might be a LUKS-encrypted /boot or
> +          ;; root partition.
> +          (setenv "GRUB_ENABLE_CRYPTODISK" "y")
> +          (invoke/quiet (string-append bootloader "/bin/grub-mknetdir")
> +                        (string-append "--net-directory=" net-dir)
> +                        (string-append "--subdir=" subdir))
> +          (false-if-exception
> +            (delete-file efi-bootloader-link))
> +          (symlink #$efi-bootloader
> +                   efi-bootloader-link)
> +          (false-if-exception
> +            (delete-file store-link))
> +          (symlink store
> +                   store-link)))))

What's the purpose of those two symlinks, isn't grub-mknetdir taking
care of all this?

Creating a system test for this may be a bit difficult, but if you could
add a section in the documentation describing how to setup a
'grub-efi-net-bootloader, that would be great!

Thanks,

Mathieu
Stefan May 24, 2020, 12:22 a.m. UTC | #2
Hi Mathieu!

> Am 23.05.2020 um 10:10 schrieb Mathieu Othacehe <othacehe@gnu.org>:
> 
>> +    #~(lambda (bootloader target mount-point)
>> +        "Install GRUB as e.g. \"bootx64.efi\" or \"bootarm64.efi\" \"into
>> +EFI-SUBDIR, which is usually \"efi/boot\" or \"efi/Guix\" below the directory
> 
> Is that TARGET or EFI-SUBDIR?

I really mean with EFI-SUBDIR the argument from the enclosing install-grub-efi-net function, and with TARGET the argument of the lambda function. The prototype of the lambda function is fixed by some other guix machinery, it is not possible to pass an EFI-SUBDIR argument directly.

Take for example this path, the default: "/mnt/boot/efi/boot". Then MOUNT-POINT is "/mnt" (like in 'guix system init config.scm /mnt'), TARGET is "/boot" (which you also have to specify inside (operating-system (bootloader (target "/boot") …) …), and EFI-SUBDIR is the remaining "efi/boot".

In my personal case I need MOUNT-POINT as "/" (well, 'guix system init …' was only necessary once), TARGET as "boot-nfs" (my special setup), and EFI-SUBDIR as "efi/boot" (because the U-Boot is using this path by default).

For a TFFP server this TARGET with /boot (or /boot-nfs in my case) is the directory whose content has to be served. It can’t be /boot/efi (like for the normal grub-efi bootloader), as the TFTP server needs to serve several firmware files and the U-Boot.¹ When the U-Boot takes control, by default it tries to load /efi/boot/bootaa64.efi via TFTP.

It may be possible to configure the U-Boot to look for files below a /efi/Guix directory or any other path, I’m not really sure about this yet. At least /efi/boot is the documented last resort for UEFI systems to look for some “removable madia” to boot over TFTP.

Next GRUB takes control and looks for its grub.cfg and all its other files via TFTP at the path specified during 'grub-mknetdir --subdir=…'. As the TFTP root is /boot (where U-Boot and other stuff may live), this now has to be efi/boot. So you get all GRUB files like /boot/efi/boot/grub.cfg served via TFTP from the directory /boot/efi/boot.

For the usual grub-efi-bootloader this is a bit different. Here the normal path is /mnt/boot/efi/Guix and for the 'grub-install' command these parameters are required: '—-boot-directory /mnt/boot --bootloader-id Guix --efi-directory /boot/efi'. Here the "boot" part to '--boot-directory' is hard coded, as well as "Guix". So the only part you are free to chose is "efi" in (operating-system (bootloader (target "boot/efi") …) …). Additionally the usual grub-efi-bootloader looks for /boot/grub.cfg which thus is not residing inside the efi partition like all its other files.


>> +TARGET for the system whose root is mounted at MOUNT-POINT."
>> +        (let* ((mount-point-list (delete "" (string-split mount-point #\/)))
>> +               (target-list (delete "" (string-split target #\/)))
>> +               (net-dir
>> +                (string-append "/" (string-join (append
>> +                                                 mount-point-list
>> +                                                 target-list)
>> +                                                "/")))
> 
> I think you can use something like "(in-vicinity mount-point target)"
> to do the same job.

MOUNT-POINT may be just "/", TARGET is "/boot". I’m avoiding double slashes, like "//boot", also for all the other paths. In an earlier version I had the “problem” that GRUB searched files with the prefix "//efi/boot" via TFTP. This confused me on one side, but on the other side I feared that another TFTP server might get problems with this.

Avoiding double slashes seems not to be something that in-vicinity is taking care about. And TARGET with its "/boot" looks like an absolute path, making it an improper argument to in-vicinity: “in-vicinity should allow filename to override vicinity when filename is an absolute pathname and vicinity is equal to the value of (user-vicinity). The behavior of in-vicinity when filename is absolute and vicinity is not equal to the value of (user-vicinity) is unspecified.”

A bit further down I need to count the directory levels up to the "/gnu" store to construct a proper link, so I need the result of a string-split anyway. 

So all in all – although it looks a bit complicated – I’d like to keep this.

Hm, but I wasn’t that strict with the EFI-SUBDIR argument. And potentially MOUNT-POINT may be a relative path, may it? Do you know? In that case there is a bug in prepending the "/" to net-dir, turning a relative into an absolute path.

> 
>> +          ;; Tell 'grub-install' that there might be a LUKS-encrypted /boot or
>> +          ;; root partition.
>> +          (setenv "GRUB_ENABLE_CRYPTODISK" "y")
>> +          (invoke/quiet (string-append bootloader "/bin/grub-mknetdir")
>> +                        (string-append "--net-directory=" net-dir)
>> +                        (string-append "--subdir=" subdir))
>> +          (false-if-exception
>> +            (delete-file efi-bootloader-link))
>> +          (symlink #$efi-bootloader
>> +                   efi-bootloader-link)
>> +          (false-if-exception
>> +            (delete-file store-link))
>> +          (symlink store
>> +                   store-link)))))
> 
> What's the purpose of those two symlinks, isn't grub-mknetdir taking
> care of all this?

No, it doesn’t. The final .efi file after calling 'grub-mknetdir' is /boot/efi/boot/arm64-efi/core.efi. I guess when using PXE there is a way to point to this efi file, so GRUB doesn’t care. But without this the U-Boot looks for the standardised /efi/boot/bootaa64.efi file via TFTP. So the first symlink fixes this gap without removing other possibilities.

The second symlink is for GRUB itself. Inside /boot/efi/boot/grub.cfg there are commands to specify the root device, usually via a file system label or a UUID. But in this case the root device specification is just ”set root=(tftp)”. Following lines look up files by e.g. /gnu/store/…-grub-2.04/share/grub/unicode.pf2, which GRUB will then try to access via TFTP. But the root for the TFTP server is /boot from the TARGET argument, and not /. So GRUB trying to access /gnu/store/… results in an access to /boot/gnu/store/…. Therefore the symlink from /boot/gnu to ../gnu is needed. And as TARGET can freely be chosen the number of up-levels needs to be calculated.

Where the usual grub-efi-bootloader is able to find and access the real root device directly, the grub-efi-net-bootloader can only use TFTP. Therefore we have to make the real “root device” accessible via TFPT. I’m not sure if this is wise from a security point of view. But otherwise all files listed in the grub.cfg would need to be copied. This would be an overhead and leads to more complications, when deleting system generations; then obsolete copies need determined and removed again. 

> Creating a system test for this may be a bit difficult, but if you could
> add a section in the documentation describing how to setup a
> 'grub-efi-net-bootloader, that would be great!

I have some difficulties with this – despite not having knowledge with texi yet. So help is very welcome.

You need to make use of an NFS server, serving your / directory. 

You need a TFTP server set up to serve the files of our /boot directory with access to your /gnu store via the /boot/gnu symlink as well. 

You need to setup your DNS server to send some boot-options. 

I use a “DiskStation” with its UI for all this but needed to fiddle around with dnsmasq for the DNS. So this is nothing for a guix documentation. However, this might be even out of scope. But ideally there would be examples of how to achieve all this with guix itself. I have no experience using guix for anything of this.

Then I use all this to boot a Raspberry Pi 3b in aarch64 mode. This requires a none free firmware, additional configuration files for the firmware, the (modified) U-Boot. And there is even more. I compiled the kernel linux (currently from the mainline) with a Raspberry specific defconfig-package as an input for the linux package and an additional function to modify the defconfig on the fly.

There are more patches to follow for at least parts of all this.

But what I actually want to say with this: Even to reproduce my setup, still a lot other stuff is missing. And I have no other computer for tinkering. I can’t try out this patch on a usual x86…64 machine. I can't really tell, if this will work right out of the box for other people on different computers.

Before documenting this for the public, I would wish that at least someone else with access to an x86_64 UEFI system gives it a try. I'm pretty sure it will work.

Besides all the server stuff with NFS, TFTP and DNS, there are only few specialities to know:

(operating-system
  (file-systems (cons (file-system
                        (mount-point "/")
                        (type "nfs")
                        (device ":/your/servers/path/to/guix-root")
                        (options "addr=10.11.12.2,vers=4.1"))
                       %base-file-systems)) 
  (bootloader 
    (target "/boot")
    (bootloader-configuration (bootloader grub-efi-net-bootloader)
    …))
  …)

The IP address of your server needs to be specified via an “addr=” option. To make use of the defaults of the new grub-efi-net-bootloader, the target field has to be "/boot".


Bye

Stefan


¹ Well, actually – as for grub-efi-net-bootloader you are now free to use any path for target – you can use "/boot/efi". But then you need to make this your TFTP server’s root, and you need to use the make-grub-efi-net-bootloader macro with "/boot/efi" as well, and you will end up in having double efi folders as in /boot/efi/efi/boot/grub.cfg etc. So this doesn’t make sense.
diff mbox series

Patch

diff --git a/gnu/bootloader/grub.scm b/gnu/bootloader/grub.scm
index bb40c551a7..2b0ecec279 100644
--- a/gnu/bootloader/grub.scm
+++ b/gnu/bootloader/grub.scm
@@ -23,7 +23,7 @@ 
 
 (define-module (gnu bootloader grub)
   #:use-module (guix records)
-  #:use-module ((guix utils) #:select (%current-system))
+  #:use-module ((guix utils) #:select (%current-system %current-target-system))
   #:use-module (guix gexp)
   #:use-module (gnu artwork)
   #:use-module (gnu bootloader)
@@ -47,6 +47,8 @@ 
 
             grub-bootloader
             grub-efi-bootloader
+            make-grub-efi-net-bootloader
+            grub-efi-net-bootloader
             grub-mkrescue-bootloader
             grub-minimal-bootloader
 
@@ -140,36 +142,20 @@  file with the resolution provided in CONFIG."
 concerned with graphics mode, background images, colors, and all that.
 STORE-DEVICE designates the device holding the store, and STORE-MOUNT-POINT is
 its mount point; these are used to determine where the background image and
-fonts must be searched for.  SYSTEM must be the target system string---e.g.,
-\"x86_64-linux\".  BTRFS-STORE-SUBVOLUME-FILE-NAME is the file name of the
-Btrfs subvolume, to be prepended to any store path, if any."
-  (define setup-gfxterm-body
-    (let ((gfxmode
-           (or (and-let* ((theme (bootloader-configuration-theme config))
-                          (gfxmode (grub-theme-gfxmode theme)))
-                 (string-join gfxmode ";"))
-               "auto")))
-
-      ;; Intel and EFI systems need to be switched into graphics mode, whereas
-      ;; most other modern architectures have no other mode and therefore
-      ;; don't need to be switched.
-
-      ;; XXX: Do we really need to restrict to x86 systems?  We could imitate
-      ;; what the GRUB default configuration does and decide based on whether
-      ;; a user provided 'gfxterm' in the terminal-outputs field of their
-      ;; bootloader-configuration record.
-      (if (string-match "^(x86_64|i[3-6]86)-" system)
-          (format #f "
-  set gfxmode=~a
-  insmod all_video
-  insmod gfxterm~%" gfxmode)
-          "")))
-
+fonts must be searched for.  BTRFS-STORE-SUBVOLUME-FILE-NAME is the file name
+of the Btrfs subvolume, to be prepended to any store path, if any."
   (define (setup-gfxterm config font-file)
     (if (memq 'gfxterm (bootloader-configuration-terminal-outputs config))
-        #~(format #f "if loadfont ~a; then
-  setup_gfxterm
-fi~%" #+font-file)
+        #~(format #f "
+if loadfont ~a; then
+  set gfxmode=~a
+  insmod all_video
+  insmod gfxterm
+fi~%"
+                  #$font-file
+                  #$(string-join
+                      (grub-theme-gfxmode (bootloader-theme config))
+                      ";"))
         ""))
 
   (define (theme-colors type)
@@ -190,8 +176,6 @@  fi~%" #+font-file)
 
   (and image
        #~(format #$port "
-function setup_gfxterm {~a}
-
 # Set 'root' to the partition that contains /gnu/store.
 ~a
 
@@ -206,7 +190,6 @@  else
   set menu_color_normal=cyan/blue
   set menu_color_highlight=white/blue
 fi~%"
-                 #$setup-gfxterm-body
                  #$(grub-root-search store-device font-file)
                  #$(setup-gfxterm config font-file)
                  #$(grub-setup-io config)
@@ -313,6 +296,9 @@  code."
         ((? file-system-label? label)
          (format #f "search --label --set ~a"
                  (file-system-label->string label)))
+        ((? (lambda (device)
+              (and (string? device) (string-contains device ":/"))) nfs-uri)
+         "set root=(tftp)")
         ((or #f (? string?))
          #~(format #f "search --file --set ~a" #$file)))))
 
@@ -454,6 +440,68 @@  fi~%"))))
                       "--bootloader-id=Guix"
                       "--efi-directory" target-esp))))
 
+(define (install-grub-efi-net efi-subdir)
+  "Define a grub-efi bootloader installer for installation in EFI-SUBDIR,
+which is usually \"efi/boot\" or \"efi/Guix\"."
+  (let* ((arch (car (string-split (or (%current-target-system)
+                                      (%current-system))
+                                  #\-)))
+         (efi-bootloader-link (string-append "boot"
+                                             (match arch
+                                               ("i686" "ia32")
+                                               ("x86_64" "x64")
+                                               ("arm" "arm")
+                                               ("armhf" "arm")
+                                               ("aarch64" "aa64")
+                                               ("riscv" "riscv32")
+                                               ("riscv64" "riscv64"))
+                                             ".efi"))
+         (efi-bootloader (string-append (match arch
+                                          ("i686" "i386")
+                                          ("x86_64" "x86_64")
+                                          ("arm" "arm")
+                                          ("armhf" "arm")
+                                          ("aarch64" "arm64")
+                                          ("riscv" "riscv32")
+                                          ("riscv64" "riscv64"))
+                                        "-efi/core.efi")))
+    #~(lambda (bootloader target mount-point)
+        "Install GRUB as e.g. \"bootx64.efi\" or \"bootarm64.efi\" \"into
+EFI-SUBDIR, which is usually \"efi/boot\" or \"efi/Guix\" below the directory
+TARGET for the system whose root is mounted at MOUNT-POINT."
+        (let* ((mount-point-list (delete "" (string-split mount-point #\/)))
+               (target-list (delete "" (string-split target #\/)))
+               (net-dir
+                (string-append "/" (string-join (append
+                                                 mount-point-list
+                                                 target-list)
+                                                "/")))
+               (subdir #$efi-subdir)
+               (efi-bootloader-link
+                (string-append net-dir "/" subdir "/" #$efi-bootloader-link))
+               (store-name (car (delete "" (string-split bootloader #\/))))
+               (store
+                ;; Use target-list to construct a "../gnu" link with a correct
+                ;; number of "../" to the store.
+                (string-join (append (make-list (length target-list) "..")
+                                     (list store-name))
+                             "/"))
+               (store-link (string-append net-dir "/" store-name)))
+          ;; Tell 'grub-install' that there might be a LUKS-encrypted /boot or
+          ;; root partition.
+          (setenv "GRUB_ENABLE_CRYPTODISK" "y")
+          (invoke/quiet (string-append bootloader "/bin/grub-mknetdir")
+                        (string-append "--net-directory=" net-dir)
+                        (string-append "--subdir=" subdir))
+          (false-if-exception
+            (delete-file efi-bootloader-link))
+          (symlink #$efi-bootloader
+                   efi-bootloader-link)
+          (false-if-exception
+            (delete-file store-link))
+          (symlink store
+                   store-link)))))
+
 ^L
 
 ;;;
@@ -483,7 +531,18 @@  fi~%"))))
    (name 'grub-efi)
    (package grub-efi)))
 
-(define* grub-mkrescue-bootloader
+(define-syntax-rule (make-grub-efi-net-bootloader bootloader-name target efi-subdir)
+    (define bootloader-name
+       (bootloader
+          (inherit grub-bootloader)
+          (name (quote bootloader-name))
+          (package grub-efi)
+          (installer (install-grub-efi-net efi-subdir))
+          (configuration-file (string-append target "/" efi-subdir "/grub.cfg")))))
+
+(make-grub-efi-net-bootloader grub-efi-net-bootloader "boot" "efi/boot")
+
+(define grub-mkrescue-bootloader
   (bootloader
    (inherit grub-efi-bootloader)
    (package grub-hybrid)))
diff --git a/gnu/system.scm b/gnu/system.scm
index d929187695..2035235549 100644
--- a/gnu/system.scm
+++ b/gnu/system.scm
@@ -301,7 +301,8 @@  file system labels."
       ((? string? device)
        ;; It used to be that we would not distinguish between labels and
        ;; device names.  Try to infer the right thing here.
-       (if (string-prefix? "/dev/" device)
+       (if (or (string-prefix? "/dev/" device)
+               (string-contains device ":/")) ; nfs
            device
            (file-system-label device)))))