diff --git a/internal/testdisk/partition.go b/internal/testdisk/partition.go index 0e41f67c58..d887a9bc4b 100644 --- a/internal/testdisk/partition.go +++ b/internal/testdisk/partition.go @@ -398,6 +398,11 @@ func MakeFakePartitionTable(mntPoints ...string) *disk.PartitionTable { } swap.GenUUID(rng) payload = swap + case "raw": + payload = &disk.Raw{ + SourcePipeline: "build", + SourcePath: "/usr/lib/modules/5.0/aboot.img", + } default: payload = &disk.Filesystem{ Type: "ext4", diff --git a/pkg/bib/osinfo/osinfo.go b/pkg/bib/osinfo/osinfo.go index cba8fca04b..930d38aa0c 100644 --- a/pkg/bib/osinfo/osinfo.go +++ b/pkg/bib/osinfo/osinfo.go @@ -26,11 +26,17 @@ type OSRelease struct { IDLike []string } +type KernelInfo struct { + Version string + HasAbootImg bool +} + type Info struct { OSRelease OSRelease UEFIVendor string SELinuxPolicy string ImageCustomization *blueprint.Customizations + KernelInfo *KernelInfo } func validateOSRelease(osrelease map[string]string) error { @@ -119,6 +125,39 @@ func readImageCustomization(root string) (*blueprint.Customizations, error) { return config.Customizations, nil } +func readKernelInfo(root string) (*KernelInfo, error) { + modulesDir := path.Join(root, "usr/lib/modules") + entries, err := os.ReadDir(modulesDir) + if err != nil { + return nil, err + } + + for _, e := range entries { + if !e.IsDir() { + continue + } + + // A kernel dir is valid if there is a vmlinuz in it. + // bootc checks that there is only one such dir, so we + // pick the first here + kernelDir := path.Join(modulesDir, e.Name()) + kernelPath := path.Join(kernelDir, "vmlinuz") + _, err := os.Stat(kernelPath) + if err == nil { + + abootPath := path.Join(kernelDir, "aboot.img") + _, err := os.Stat(abootPath) + hasAbootImg := err == nil + return &KernelInfo{ + Version: e.Name(), + HasAbootImg: hasAbootImg, + }, nil + } + } + + return nil, fmt.Errorf("no valid kernel modules directory") +} + func Load(root string) (*Info, error) { osrelease, err := distro.ReadOSReleaseFromTree(root) if err != nil { @@ -138,6 +177,11 @@ func Load(root string) (*Info, error) { return nil, err } + kernelInfo, err := readKernelInfo(root) + if err != nil { + logrus.Debugf("cannot read kernel info: %v", err) + } + selinuxPolicy, err := readSelinuxPolicy(root) if err != nil { logrus.Debugf("cannot read selinux policy: %v, setting it to none", err) @@ -161,5 +205,6 @@ func Load(root string) (*Info, error) { UEFIVendor: vendor, SELinuxPolicy: selinuxPolicy, ImageCustomization: customization, + KernelInfo: kernelInfo, }, nil } diff --git a/pkg/bib/osinfo/osinfo_test.go b/pkg/bib/osinfo/osinfo_test.go index af48dc73fd..e2effdcd9d 100644 --- a/pkg/bib/osinfo/osinfo_test.go +++ b/pkg/bib/osinfo/osinfo_test.go @@ -168,3 +168,47 @@ func TestLoadInfo(t *testing.T) { }) } } + +func TestLoadInfoKernel(t *testing.T) { + type testCase struct { + desc string + dirs []string + files []string + expected *KernelInfo + } + + cases := []testCase{ + // Incorrect kernel trees + {"nodir", []string{}, []string{"not-a-dir"}, nil}, + {"novmlinuz", []string{"6.15.9-201.fc42.x86_64"}, []string{}, nil}, + {"novmlinuz2", []string{"6.15.9-201.fc42.x86_64", "6.14.11-300.fc42.x86_64"}, []string{"not-a-dir"}, nil}, + {"novmlinuz3", []string{"6.15.9-201.fc42.x86_64", "6.14.11-300.fc42.x86_64"}, []string{"6.15.9-201.fc42.x86_64/not-vmlinuz"}, nil}, + // Correct kernel trees + {"noaboot", []string{"6.15.9-201.fc42.x86_64"}, []string{"6.15.9-201.fc42.x86_64/vmlinuz"}, &KernelInfo{"6.15.9-201.fc42.x86_64", false}}, + {"aboot", []string{"6.15.9-201.fc42.x86_64"}, []string{"6.15.9-201.fc42.x86_64/vmlinuz", "6.15.9-201.fc42.x86_64/aboot.img"}, &KernelInfo{"6.15.9-201.fc42.x86_64", true}}, + {"severaldirs", []string{"6.15.9-201.fc42.x86_64", "6.14.11-300.fc42.x86_64"}, []string{"6.14.11-300.fc42.x86_64/vmlinuz"}, &KernelInfo{"6.14.11-300.fc42.x86_64", false}}, + } + for _, c := range cases { + t.Run(c.desc, func(t *testing.T) { + root := t.TempDir() + baseDir := path.Join(root, "usr/lib/modules") + require.NoError(t, os.MkdirAll(baseDir, 0755)) + for _, dir := range c.dirs { + dirPath := path.Join(baseDir, dir) + require.NoError(t, os.MkdirAll(dirPath, 0755)) + } + for _, file := range c.files { + filePath := path.Join(baseDir, file) + require.NoError(t, os.WriteFile(filePath, nil, 0644)) + } + info, err := readKernelInfo(root) + if c.expected == nil { + require.Error(t, err) + assert.Nil(t, info) + } else { + require.NoError(t, err) + assert.Equal(t, info, c.expected) + } + }) + } +} diff --git a/pkg/disk/raw.go b/pkg/disk/raw.go new file mode 100644 index 0000000000..19976b3b1f --- /dev/null +++ b/pkg/disk/raw.go @@ -0,0 +1,31 @@ +package disk + +import ( + "reflect" +) + +// Raw defines the payload for a raw partition. It's similar to a +// [Filesystem] but with fewer fields. It is a [PayloadEntity]. +type Raw struct { + SourcePipeline string + SourcePath string +} + +func init() { + payloadEntityMap["raw"] = reflect.TypeOf(Raw{}) +} + +func (s *Raw) EntityName() string { + return "raw" +} + +func (s *Raw) Clone() Entity { + if s == nil { + return nil + } + + return &Raw{ + SourcePipeline: s.SourcePipeline, + SourcePath: s.SourcePath, + } +} diff --git a/pkg/disk/raw_test.go b/pkg/disk/raw_test.go new file mode 100644 index 0000000000..1b90632146 --- /dev/null +++ b/pkg/disk/raw_test.go @@ -0,0 +1,11 @@ +package disk_test + +import ( + "testing" + + "github.com/osbuild/images/pkg/disk" +) + +func TestImplementsInterfacesCompileTimeCheckRaw(t *testing.T) { + var _ = disk.PayloadEntity(&disk.Raw{}) +} diff --git a/pkg/osbuild/bootupd_stage.go b/pkg/osbuild/bootupd_stage.go index 3afa264a9a..535e0e2c18 100644 --- a/pkg/osbuild/bootupd_stage.go +++ b/pkg/osbuild/bootupd_stage.go @@ -118,13 +118,13 @@ func genMountsForBootupd(source string, pt *disk.PartitionTable) ([]Mount, error } mount.Source = lv.Name mounts = append(mounts, *mount) - case *disk.Swap: + case *disk.Swap, *disk.Raw: // nothing to do default: return nil, fmt.Errorf("expected LV payload %+[1]v to be mountable or swap, got %[1]T", lv.Payload) } } - case *disk.Swap: + case *disk.Swap, *disk.Raw: // nothing to do default: return nil, fmt.Errorf("type %T not supported by bootupd handling yet", part.Payload) diff --git a/pkg/osbuild/device.go b/pkg/osbuild/device.go index 8dbdb7729c..bd625a04a9 100644 --- a/pkg/osbuild/device.go +++ b/pkg/osbuild/device.go @@ -164,6 +164,8 @@ func deviceName(p disk.Entity) string { return "btrfs-" + payload.UUID[:4] case *disk.Swap: return "swap-" + payload.UUID[:4] + case *disk.Raw: + return "raw-" + pathEscape(payload.SourcePath) } panic(fmt.Sprintf("unsupported device type in deviceName: '%T'", p)) } diff --git a/pkg/osbuild/mkfs_stage.go b/pkg/osbuild/mkfs_stage.go index 61eae63125..44d9f00c6f 100644 --- a/pkg/osbuild/mkfs_stage.go +++ b/pkg/osbuild/mkfs_stage.go @@ -2,6 +2,7 @@ package osbuild import ( "fmt" + "path/filepath" "slices" "strings" @@ -9,6 +10,21 @@ import ( "github.com/osbuild/images/pkg/disk" ) +// Helper to create the `devices` option for the stage with the right +// name such that the last device is the target. +func getDevicesForFsStage(path []disk.Entity, filename string) map[string]Device { + stageDevices, lastName := getDevices(path, filename, true) + + // The last device in the chain must be named "device", + // because that's the device that mkfs and write-device stages + // run on. See the stage schemas for reference. + lastDevice := stageDevices[lastName] + delete(stageDevices, lastName) + stageDevices["device"] = lastDevice + + return stageDevices +} + // GenFsStages generates a list of stages that create the filesystem and other // related entities. Specifically, it creates stages for: // - org.osbuild.mkfs.*: for all filesystems and btrfs volumes @@ -20,15 +36,7 @@ func GenFsStages(pt *disk.PartitionTable, filename string) []*Stage { genStage := func(ent disk.Entity, path []disk.Entity) error { switch e := ent.(type) { case *disk.Filesystem: - // TODO: extract last device renaming into helper - stageDevices, lastName := getDevices(path, filename, true) - - // The last device in the chain must be named "device", because that's - // the device that mkfs stages run on. See the stage schemas for - // reference. - lastDevice := stageDevices[lastName] - delete(stageDevices, lastName) - stageDevices["device"] = lastDevice + stageDevices := getDevicesForFsStage(path, filename) switch e.GetFSType() { case "xfs": @@ -57,14 +65,7 @@ func GenFsStages(pt *disk.PartitionTable, filename string) []*Stage { panic(fmt.Sprintf("unknown fs type: %s", e.GetFSType())) } case *disk.Btrfs: - stageDevices, lastName := getDevices(path, filename, true) - - // The last device in the chain must be named "device", because that's - // the device that mkfs stages run on. See the stage schemas for - // reference. - lastDevice := stageDevices[lastName] - delete(stageDevices, lastName) - stageDevices["device"] = lastDevice + stageDevices := getDevicesForFsStage(path, filename) options := &MkfsBtrfsStageOptions{ UUID: e.UUID, @@ -84,21 +85,22 @@ func GenFsStages(pt *disk.PartitionTable, filename string) []*Stage { mount := *NewBtrfsMount("volume", "device", "/", "", "") stages = append(stages, NewBtrfsSubVol(&BtrfsSubVolOptions{subvolumes}, &stageDevices, &[]Mount{mount})) case *disk.Swap: - // TODO: extract last device renaming into helper - stageDevices, lastName := getDevices(path, filename, true) - - // The last device in the chain must be named "device", because that's - // the device that the mkswap stage runs on. See the stage schema - // for reference. - lastDevice := stageDevices[lastName] - delete(stageDevices, lastName) - stageDevices["device"] = lastDevice + stageDevices := getDevicesForFsStage(path, filename) options := &MkswapStageOptions{ UUID: e.UUID, Label: e.Label, } stages = append(stages, NewMkswapStage(options, stageDevices)) + case *disk.Raw: + stageDevices := getDevicesForFsStage(path, filename) + + inputName := "tree" + options := &WriteDeviceStageOptions{ + From: fmt.Sprintf("input://%s", filepath.Join(inputName, e.SourcePath)), + } + inputs := NewPipelineTreeInputs(inputName, e.SourcePipeline) + stages = append(stages, NewWriteDeviceStage(options, inputs, stageDevices)) } return nil } diff --git a/pkg/osbuild/mkfs_stages_test.go b/pkg/osbuild/mkfs_stages_test.go index 0235a3e262..34e3e84ee2 100644 --- a/pkg/osbuild/mkfs_stages_test.go +++ b/pkg/osbuild/mkfs_stages_test.go @@ -363,6 +363,78 @@ func TestGenFsStagesLVM(t *testing.T) { }, stages) } +func TestGenFsStagesRaw(t *testing.T) { + pt := testdisk.MakeFakePartitionTable("/", "/boot", "/boot/efi", "raw") + stages := GenFsStages(pt, "file.img") + assert.Equal(t, []*Stage{ + { + Type: "org.osbuild.mkfs.ext4", + Options: &MkfsExt4StageOptions{ + UUID: disk.RootPartitionUUID, + }, + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Size: testdisk.FakePartitionSize / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, + { + Type: "org.osbuild.mkfs.ext4", + Options: &MkfsExt4StageOptions{ + UUID: disk.DataPartitionUUID, + }, + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Size: testdisk.FakePartitionSize / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, + { + Type: "org.osbuild.mkfs.fat", + Options: &MkfsFATStageOptions{ + VolID: strings.ReplaceAll(disk.EFIFilesystemUUID, "-", ""), + }, + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Size: testdisk.FakePartitionSize / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, + { + Type: "org.osbuild.write-device", + Options: &WriteDeviceStageOptions{ + From: "input://tree/usr/lib/modules/5.0/aboot.img", + }, + Inputs: NewPipelineTreeInputs("tree", "build"), + Devices: map[string]Device{ + "device": { + Type: "org.osbuild.loopback", + Options: &LoopbackDeviceOptions{ + Filename: "file.img", + Size: testdisk.FakePartitionSize / disk.DefaultSectorSize, + Lock: true, + }, + }, + }, + }, + }, stages) +} + func TestGenFsStagesUnhappy(t *testing.T) { pt := &disk.PartitionTable{ Type: disk.PT_GPT, diff --git a/pkg/osbuild/write_device_stage.go b/pkg/osbuild/write_device_stage.go new file mode 100644 index 0000000000..6ac9d27ef1 --- /dev/null +++ b/pkg/osbuild/write_device_stage.go @@ -0,0 +1,16 @@ +package osbuild + +type WriteDeviceStageOptions struct { + From string `json:"from"` +} + +func (WriteDeviceStageOptions) isStageOptions() {} + +func NewWriteDeviceStage(options *WriteDeviceStageOptions, inputs Inputs, devices map[string]Device) *Stage { + return &Stage{ + Type: "org.osbuild.write-device", + Options: options, + Inputs: inputs, + Devices: devices, + } +}