From 3de6c4786c1b7261b47930b92175cb06701e644c Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 12 Jan 2024 17:28:19 +0100 Subject: [PATCH] osbuild: add new bootupd stage Add support for the new `org.osbuild.bootupd` stage that got added in https://github.com/osbuild/osbuild/pull/1519 --- pkg/osbuild/bootupd_stage.go | 77 ++++++++++++++++ pkg/osbuild/bootupd_stage_test.go | 145 ++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 pkg/osbuild/bootupd_stage.go create mode 100644 pkg/osbuild/bootupd_stage_test.go diff --git a/pkg/osbuild/bootupd_stage.go b/pkg/osbuild/bootupd_stage.go new file mode 100644 index 0000000000..74c6c88201 --- /dev/null +++ b/pkg/osbuild/bootupd_stage.go @@ -0,0 +1,77 @@ +package osbuild + +import ( + "fmt" + "sort" +) + +type BootupdStageOptionsBios struct { + Device string `json:"device"` + Partition int `json:"partition,omitempty"` +} + +type BootupdStageOptions struct { + Deployment *OSTreeDeployment `json:"deployment,omitempty"` + StaticConfigs bool `json:"static-configs"` + Bios *BootupdStageOptionsBios `json:"bios,omitempty"` +} + +func (BootupdStageOptions) isStageOptions() {} + +func (opts *BootupdStageOptions) validate(devices map[string]Device) error { + if opts.Bios != nil && opts.Bios.Device != "" { + if _, ok := devices[opts.Bios.Device]; !ok { + var devnames []string + for devname := range devices { + devnames = append(devnames, devname) + } + sort.Strings(devnames) + return fmt.Errorf("cannot find expected device %q for bootupd bios option in %v", opts.Bios.Device, devnames) + } + } + return nil +} + +// validateBootupdMounts ensures that all required mounts for the bootup +// stage are generated. Right now the stage requires root, boot and boot/efi +// to find all the bootloader configs +func validateBootupdMounts(mounts []Mount) error { + requiredMounts := map[string]bool{ + "/": true, + "/boot": true, + "/boot/efi": true, + } + for _, mnt := range mounts { + if _, ok := requiredMounts[mnt.Target]; ok { + delete(requiredMounts, mnt.Target) + } + } + if len(requiredMounts) != 0 { + var missingMounts []string + for mnt := range requiredMounts { + missingMounts = append(missingMounts, mnt) + } + sort.Strings(missingMounts) + return fmt.Errorf("required mounts for bootupd stage %v missing", missingMounts) + } + return nil +} + +// NewBootupdStage creates a new stage for the org.osbuild.bootupd stage. It +// requires a mount setup of "/", "/boot" and "/boot/efi" right now so that +// bootupd can find and install all required bootloader bits. +func NewBootupdStage(opts *BootupdStageOptions, devices *Devices, mounts *Mounts) (*Stage, error) { + if err := validateBootupdMounts(*mounts); err != nil { + return nil, err + } + if err := opts.validate(*devices); err != nil { + return nil, err + } + + return &Stage{ + Type: "org.osbuild.bootupd", + Options: opts, + Devices: *devices, + Mounts: *mounts, + }, nil +} diff --git a/pkg/osbuild/bootupd_stage_test.go b/pkg/osbuild/bootupd_stage_test.go new file mode 100644 index 0000000000..7164724fe0 --- /dev/null +++ b/pkg/osbuild/bootupd_stage_test.go @@ -0,0 +1,145 @@ +package osbuild_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/osbuild/images/pkg/osbuild" +) + +func makeOsbuildMounts(targets ...string) osbuild.Mounts { + var mnts []osbuild.Mount + for _, target := range targets { + mnts = append(mnts, osbuild.Mount{ + Type: "org.osbuild.ext4", + Name: "mnt-" + target, + Source: "dev-" + target, + Target: target, + }) + } + return mnts +} + +func makeOsbuildDevices(devnames ...string) osbuild.Devices { + devices := make(map[string]osbuild.Device) + for _, devname := range devnames { + devices[devname] = osbuild.Device{ + Type: "orgosbuild.loopback", + } + } + return devices +} + +func TestBootupdStageNewHappy(t *testing.T) { + opts := &osbuild.BootupdStageOptions{ + StaticConfigs: true, + } + devices := makeOsbuildDevices("dev-/", "dev-/boot", "dev-/boot/efi") + mounts := makeOsbuildMounts("/", "/boot", "/boot/efi") + + expectedStage := &osbuild.Stage{ + Type: "org.osbuild.bootupd", + Options: opts, + Devices: devices, + Mounts: mounts, + } + stage, err := osbuild.NewBootupdStage(opts, &devices, &mounts) + require.Nil(t, err) + assert.Equal(t, stage, expectedStage) +} + +func TestBootupdStageMissingMounts(t *testing.T) { + opts := &osbuild.BootupdStageOptions{ + StaticConfigs: true, + } + devices := makeOsbuildDevices("dev-/") + mounts := makeOsbuildMounts("/") + + stage, err := osbuild.NewBootupdStage(opts, &devices, &mounts) + assert.ErrorContains(t, err, "required mounts for bootupd stage [/boot /boot/efi] missing") + require.Nil(t, stage) +} + +func TestBootupdStageMissingDevice(t *testing.T) { + opts := &osbuild.BootupdStageOptions{ + Bios: &osbuild.BootupdStageOptionsBios{ + Device: "disk", + }, + } + devices := makeOsbuildDevices("dev-/", "dev-/boot", "dev-/boot/efi") + mounts := makeOsbuildMounts("/", "/boot", "/boot/efi") + + stage, err := osbuild.NewBootupdStage(opts, &devices, &mounts) + assert.ErrorContains(t, err, `cannot find expected device "disk" for bootupd bios option in [dev-/ dev-/boot dev-/boot/efi]`) + require.Nil(t, stage) +} + +func TestBootupdStageJsonHappy(t *testing.T) { + opts := &osbuild.BootupdStageOptions{ + Deployment: &osbuild.OSTreeDeployment{ + OSName: "default", + Ref: "ostree/1/1/0", + }, + StaticConfigs: true, + Bios: &osbuild.BootupdStageOptionsBios{ + Device: "disk", + }, + } + devices := makeOsbuildDevices("disk", "dev-/", "dev-/boot", "dev-/boot/efi") + mounts := makeOsbuildMounts("/", "/boot", "/boot/efi") + + stage, err := osbuild.NewBootupdStage(opts, &devices, &mounts) + require.Nil(t, err) + stageJson, err := json.MarshalIndent(stage, "", " ") + require.Nil(t, err) + assert.Equal(t, string(stageJson), `{ + "type": "org.osbuild.bootupd", + "options": { + "deployment": { + "osname": "default", + "ref": "ostree/1/1/0" + }, + "static-configs": true, + "bios": { + "device": "disk" + } + }, + "devices": { + "dev-/": { + "type": "orgosbuild.loopback" + }, + "dev-/boot": { + "type": "orgosbuild.loopback" + }, + "dev-/boot/efi": { + "type": "orgosbuild.loopback" + }, + "disk": { + "type": "orgosbuild.loopback" + } + }, + "mounts": [ + { + "name": "mnt-/", + "type": "org.osbuild.ext4", + "source": "dev-/", + "target": "/" + }, + { + "name": "mnt-/boot", + "type": "org.osbuild.ext4", + "source": "dev-/boot", + "target": "/boot" + }, + { + "name": "mnt-/boot/efi", + "type": "org.osbuild.ext4", + "source": "dev-/boot/efi", + "target": "/boot/efi" + } + ] +}`) +}