package main // The boot-entries script as the mesh writes it, run with sh over a stand-in root (novox/hq ADR 0508 §9 and // §10, issue 1841). The fixtures are the shapes read from machines: a systemd-boot entries directory, a GRUB // menu, a hook that copies every kernel image to the default kernel's name, and the command line a machine // booted with. import ( "os" "os/exec" "path/filepath" "strings" "testing" ) const bootedCmdline = "initrd=\\initramfs-linux.img BOOT_IMAGE=/vmlinuz-linux root=UUID=2f6c1e0a-5c0e-4d52-9a57-1d8a2a7a3c11 rw quiet loglevel=3 initrd=\\intel-ucode.img\n" const defaultEntry = "title Arch Linux\nlinux /vmlinuz-linux\ninitrd /intel-ucode.img\ninitrd /initramfs-linux.img\noptions root=UUID=2f6c1e0a-5c0e-4d52-9a57-1d8a2a7a3c11 rw quiet\n" const loaderConf = "default arch.conf\ntimeout 3\nconsole-mode max\n" // grubMenu is the part of a generated grub.cfg that names kernels, for a machine holding one kernel. const grubMenu = `menuentry 'Arch Linux' --class arch { set root='hd0,gpt2' linux /boot/vmlinuz-linux root=UUID=2f6c1e0a-5c0e-4d52-9a57-1d8a2a7a3c11 rw quiet initrd /boot/initramfs-linux.img } menuentry 'Arch Linux, with fallback initramfs' --class arch { linux /boot/vmlinuz-linux root=UUID=2f6c1e0a-5c0e-4d52-9a57-1d8a2a7a3c11 rw quiet initrd /boot/initramfs-linux-fallback.img } ` // kernelCopyingHook copies every installed kernel image to the default kernel's name on the boot partition. const kernelCopyingHook = `[Trigger] Operation = Install Operation = Upgrade Type = Path Target = usr/lib/modules/*/vmlinuz [Action] Description = Copying the kernel to the default name When = PostTransaction Exec = /usr/bin/sh -c '/usr/bin/cp /usr/lib/modules/$(uname -r)/vmlinuz /boot/vmlinuz-linux' ` // otherHook is a hook of the distribution's shape that has nothing to do with kernel images. const otherHook = `[Trigger] Operation = Upgrade Type = Package Target = pacman-mirrorlist [Action] Description = Updating the mirror list When = PostTransaction Exec = /usr/bin/cp /etc/pacman.d/mirrorlist.pacnew /etc/pacman.d/mirrorlist ` type bootBed struct { script, root, state, bin, mkconfig string need string } func newBootBed(t *testing.T) bootBed { t.Helper() dir := t.TempDir() b := bootBed{script: filepath.Join(dir, "boot-entries"), root: filepath.Join(dir, "root"), state: filepath.Join(dir, "state"), bin: filepath.Join(dir, "bin"), mkconfig: filepath.Join(dir, "mkconfig")} content := manifest(t).resource(t, "boot-entries").text(t) content = strings.ReplaceAll(content, "${dir:state}", b.state) if strings.Contains(content, "${") { t.Fatalf("a substitution the test does not make is left in the script") } for _, d := range []string{b.root + "/boot", b.root + "/proc", b.root + "/etc/pacman.d/hooks", b.state, b.bin} { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } } write(t, b.script, content, 0o755) write(t, b.root+"/proc/cmdline", bootedCmdline, 0o644) write(t, filepath.Join(b.bin, "grub-mkconfig"), "#!/bin/sh\necho \"$@\" >>"+b.mkconfig+"\n", 0o755) return b } func (b bootBed) put(t *testing.T, p, content string) { t.Helper() full := filepath.Join(b.root, p) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } write(t, full, content, 0o644) } func (b bootBed) systemdBoot(t *testing.T) { b.put(t, "sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f", "\x07\x00\x00\x00a\x00r\x00c\x00h\x00.\x00c\x00o\x00n\x00f\x00\x00\x00") b.put(t, "boot/loader/loader.conf", loaderConf) b.put(t, "boot/loader/entries/arch.conf", defaultEntry) b.put(t, "boot/vmlinuz-linux", "k") } func (b bootBed) run(t *testing.T) (string, error) { t.Helper() args := []string{b.script, "run", b.root, b.state} if b.need != "" { args = append(args, b.need) } cmd := exec.Command("sh", args...) cmd.Env = append(os.Environ(), "PATH="+b.bin+string(os.PathListSeparator)+os.Getenv("PATH")) out, err := cmd.CombinedOutput() return string(out), err } func (b bootBed) read(t *testing.T, p string) string { t.Helper() raw, _ := os.ReadFile(filepath.Join(b.root, p)) return string(raw) } func TestASystemdBootMachineGetsTheFallbackEntryFromItsBootedCommandLine(t *testing.T) { b := newBootBed(t) b.systemdBoot(t) b.put(t, "boot/intel-ucode.img", "u") out, err := b.run(t) if err != nil || !strings.Contains(out, "wrote") { t.Fatalf("%v %s", err, out) } want := "title Arch Linux (linux-lts, fallback)\nlinux /vmlinuz-linux-lts\ninitrd /intel-ucode.img\ninitrd /initramfs-linux-lts.img\n" + "options root=UUID=2f6c1e0a-5c0e-4d52-9a57-1d8a2a7a3c11 rw quiet loglevel=3\n" if got := b.read(t, "boot/loader/entries/mesh-linux-lts.conf"); got != want { t.Fatalf("entry:\n%s\nwant:\n%s", got, want) } // The default entry and the loader's own file are as they were. if b.read(t, "boot/loader/entries/arch.conf") != defaultEntry || b.read(t, "boot/loader/loader.conf") != loaderConf { t.Fatalf("the default entry or loader.conf was touched") } if out, err := b.run(t); err != nil || !strings.Contains(out, "as it was") { t.Fatalf("again: %v %s", err, out) } } func TestWithoutMicrocodeThereIsNoMicrocodeLine(t *testing.T) { b := newBootBed(t) b.systemdBoot(t) if out, err := b.run(t); err != nil { t.Fatalf("%v %s", err, out) } if got := b.read(t, "boot/loader/entries/mesh-linux-lts.conf"); strings.Contains(got, "ucode") || !strings.Contains(got, "initrd /initramfs-linux-lts.img") { t.Fatalf("entry: %s", got) } } func TestAGrubMachineIsGeneratedAgainOnlyWhenItsKernelsChanged(t *testing.T) { b := newBootBed(t) b.put(t, "boot/grub/grub.cfg", grubMenu) b.put(t, "boot/vmlinuz-linux", "k") if out, err := b.run(t); err != nil || !strings.Contains(out, "left as it is") { t.Fatalf("same: %v %s", err, out) } if _, err := os.Stat(b.mkconfig); err == nil { t.Fatalf("the menu was generated again with the same kernels") } b.put(t, "boot/vmlinuz-linux-lts", "k") if out, err := b.run(t); err != nil || !strings.Contains(out, "generated again") { t.Fatalf("changed: %v %s", err, out) } raw, _ := os.ReadFile(b.mkconfig) if string(raw) != "-o "+filepath.Join(b.root, "boot/grub/grub.cfg")+"\n" { t.Fatalf("grub-mkconfig was asked %q", raw) } } func TestAMachineWithNoLoaderFailsLoudly(t *testing.T) { b := newBootBed(t) out, err := b.run(t) if err == nil || !strings.Contains(out, "no boot loader found") { t.Fatalf("%v %s", err, out) } } func TestABootPartitionWithoutRoomFailsBeforeAnythingIsWritten(t *testing.T) { b := newBootBed(t) b.systemdBoot(t) b.need = "999999999999" out, err := b.run(t) if err == nil || !strings.Contains(out, "free room there before the kernel is installed") { t.Fatalf("%v %s", err, out) } if b.read(t, "boot/loader/entries/mesh-linux-lts.conf") != "" { t.Fatalf("an entry was written") } // Once the fallback kernel is in place the room is not asked for again. b.put(t, "boot/vmlinuz-linux-lts", "k") if out, err := b.run(t); err != nil { t.Fatalf("%v %s", err, out) } } func TestAHookThatCopiesKernelsToOneFixedNameIsMovedIntoTheStateAndNamed(t *testing.T) { b := newBootBed(t) b.systemdBoot(t) b.put(t, "etc/pacman.d/hooks/60-copy-kernel.hook", kernelCopyingHook) b.put(t, "etc/pacman.d/hooks/70-mirrorlist.hook", otherHook) b.put(t, "etc/pacman.d/hooks/90-mesh-kernel-boot-entries.hook", "[Trigger]\nTarget = usr/lib/modules/*/vmlinuz\n[Action]\nExec = /bin/cp x /boot/y\n") out, err := b.run(t) if err != nil || !strings.Contains(out, "taken over: the pacman hook 60-copy-kernel.hook") { t.Fatalf("%v %s", err, out) } if b.read(t, "etc/pacman.d/hooks/60-copy-kernel.hook") != "" { t.Fatalf("the copying hook is still in the hook directory") } if raw, _ := os.ReadFile(filepath.Join(b.state, "taken-over", "60-copy-kernel.hook")); string(raw) != kernelCopyingHook { t.Fatalf("moved hook: %q", raw) } if b.read(t, "etc/pacman.d/hooks/70-mirrorlist.hook") != otherHook || b.read(t, "etc/pacman.d/hooks/90-mesh-kernel-boot-entries.hook") == "" { t.Fatalf("a hook that copies no kernel was moved") } } func TestTheBootEntriesUnitAndHookAreTheModules(t *testing.T) { m := manifest(t) unit := m.resource(t, "boot-unit").text(t) for _, line := range []string{"Type=oneshot", "RemainAfterExit=yes", "ExecStart=${dir:state}/boot-entries run", "ProtectSystem=strict", "ReadWritePaths=/boot ${dir:state} /etc/pacman.d/hooks", "NoNewPrivileges=yes", "WantedBy=multi-user.target"} { if !strings.Contains(unit, line+"\n") { t.Errorf("the unit lacks %q", line) } } hook := m.resource(t, "boot-hook") text := hook.text(t) if hook["path"] != "/etc/pacman.d/hooks/90-mesh-kernel-boot-entries.hook" || !strings.Contains(text, "Target = usr/lib/modules/*/vmlinuz\n") || !strings.Contains(text, "When = PostTransaction\n") || !strings.Contains(text, "Exec = ${dir:state}/boot-entries run\n") { t.Fatalf("the hook: %v\n%s", hook["path"], text) } apply := m.resource(t, "boot-apply") if apply["unit"] != "mesh-kernel-boot-entries.service" || apply["state"] != "running" || apply["boot"] != "enabled" { t.Fatalf("apply: %v", apply) } } // The fallback kernel and its headers are declared after the boot-entries service, so the entry is placed // before the kernel is installed and every DKMS module is built for it (ADR 0508 §9). func TestTheFallbackKernelAndItsHeadersAreDeclaredAfterTheBootEntriesUnit(t *testing.T) { m := manifest(t) at := map[string]int{} for i, r := range m.Resources { at[r["id"].(string)] = i } for id, pkg := range map[string]string{"linux-lts": "linux-lts", "linux-lts-headers": "linux-lts-headers"} { r := m.resource(t, id) if r["type"] != "package" || r["package"] != pkg || r["absent"] == true { t.Errorf("%s: %v", id, r) } if at[id] < at["boot-apply"] || at[id] < at["boot-hook"] { t.Errorf("%s is declared before the boot-entries unit and hook", id) } } }