diff --git a/internal/distro/rhel85/pipelines.go b/internal/distro/rhel85/pipelines.go index 0de36367d..714f3820b 100644 --- a/internal/distro/rhel85/pipelines.go +++ b/internal/distro/rhel85/pipelines.go @@ -3,9 +3,12 @@ package rhel85 import ( "fmt" "math/rand" + "strings" "github.com/osbuild/osbuild-composer/internal/blueprint" + "github.com/osbuild/osbuild-composer/internal/disk" "github.com/osbuild/osbuild-composer/internal/distro" + "github.com/osbuild/osbuild-composer/internal/osbuild2" osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2" "github.com/osbuild/osbuild-composer/internal/rpmmd" ) @@ -48,6 +51,13 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti treePipeline.AddStage(osbuild.NewGRUB2Stage(grub2StageOptions(&partitionTable, t.kernelOptions, customizations.GetKernel(), packageSetSpecs["packages"], t.arch.uefi, t.arch.legacy))) pipelines = append(pipelines, *treePipeline) + diskfile := "disk.img" + imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch.legacy) + pipelines = append(pipelines, *imagePipeline) + + qemuPipeline := qemuPipeline(imagePipeline.Name, diskfile, t.filename) + pipelines = append(pipelines, *qemuPipeline) + return pipelines, nil } @@ -363,7 +373,7 @@ func tarStage(source, filename string) *osbuild.Stage { tree := new(osbuild.TarStageInput) tree.Type = "org.osbuild.tree" tree.Origin = "org.osbuild.pipeline" - tree.References = []string{fmt.Sprintf("name:%s", source)} + tree.References = []string{"name:" + source} return osbuild.NewTarStage(&osbuild.TarStageOptions{Filename: filename}, &osbuild.TarStageInputs{Tree: tree}) } @@ -485,3 +495,98 @@ func bootISOPipeline(filename string, arch string) *osbuild.Pipeline { return p } + +func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk.PartitionTable, platform string) *osbuild.Pipeline { + p := new(osbuild.Pipeline) + p.Name = "image" + p.Build = "name:build" + + loopback := osbuild.NewLoopbackDevice(&osbuild.LoopbackDeviceOptions{Filename: outputFilename}) + p.AddStage(osbuild.NewTruncateStage(&osbuild.TruncateStageOptions{Filename: outputFilename, Size: fmt.Sprintf("%d", pt.Size)})) + sfOptions, sfDevices := sfdiskStageOptions(pt, loopback) + p.AddStage(osbuild.NewSfdiskStage(sfOptions, sfDevices)) + + for _, stage := range mkfsStages(pt, loopback) { + p.AddStage(stage) + } + + inputName := "root-tree" + copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback) + copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName) + p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts)) + + p.AddStage(osbuild.NewGrub2InstStage(grub2InstStageOptions(outputFilename, pt, 2, platform))) + + return p +} + +// mkfsStages generates a list of org.osbuild.mkfs.* stages based on a +// partition table description for a single device node +func mkfsStages(pt *disk.PartitionTable, device *osbuild.Device) []*osbuild2.Stage { + stages := make([]*osbuild2.Stage, 0, len(pt.Partitions)) + + // assume loopback device for simplicity since it's the only one currently supported + // panic if the conversion fails + devOptions, ok := device.Options.(*osbuild.LoopbackDeviceOptions) + if !ok { + panic("mkfsStages: failed to convert device options to loopback options") + } + + for _, p := range pt.Partitions { + if p.Filesystem == nil { + // no filesystem for partition (e.g., BIOS boot) + continue + } + var stage *osbuild.Stage + stageDevice := osbuild.NewLoopbackDevice( + &osbuild.LoopbackDeviceOptions{ + Filename: devOptions.Filename, + Start: p.Start, + Size: p.Size, + }, + ) + switch p.Filesystem.Type { + case "xfs": + options := &osbuild.MkfsXfsStageOptions{ + UUID: p.Filesystem.UUID, + Label: p.Filesystem.Label, + } + devices := &osbuild.MkfsXfsStageDevices{Device: *stageDevice} + stage = osbuild.NewMkfsXfsStage(options, devices) + case "vfat": + options := &osbuild.MkfsFATStageOptions{ + VolID: strings.Replace(p.Filesystem.UUID, "-", "", -1), + } + devices := &osbuild.MkfsFATStageDevices{Device: *stageDevice} + stage = osbuild.NewMkfsFATStage(options, devices) + case "btrfs": + options := &osbuild.MkfsBtrfsStageOptions{ + UUID: p.Filesystem.UUID, + Label: p.Filesystem.Label, + } + devices := &osbuild.MkfsBtrfsStageDevices{Device: *stageDevice} + stage = osbuild.NewMkfsBtrfsStage(options, devices) + case "ext4": + options := &osbuild.MkfsExt4StageOptions{ + UUID: p.Filesystem.UUID, + Label: p.Filesystem.Label, + } + devices := &osbuild.MkfsExt4StageDevices{Device: *stageDevice} + stage = osbuild.NewMkfsExt4Stage(options, devices) + default: + panic("unknown fs type " + p.Type) + } + stages = append(stages, stage) + } + return stages +} + +func qemuPipeline(inputPipelineName string, inputFilename, outputFilename string) *osbuild.Pipeline { + p := new(osbuild.Pipeline) + p.Name = "qcow2" + p.Build = "name:build" + + qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename), qemuStageInputs(inputPipelineName, inputFilename)) + p.AddStage(qemuStage) + return p +} diff --git a/internal/distro/rhel85/stage_inputs.go b/internal/distro/rhel85/stage_inputs.go index 8ef119ead..e5cc310cb 100644 --- a/internal/distro/rhel85/stage_inputs.go +++ b/internal/distro/rhel85/stage_inputs.go @@ -49,3 +49,26 @@ func xorrisofsStageInputs() *osbuild.XorrisofsStageInputs { input.References = osbuild.XorrisofsStageReferences{"name:bootiso-tree"} return &osbuild.XorrisofsStageInputs{Tree: input} } + +func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs { + inputName := "root-tree" + treeInput := osbuild.CopyStageInput{} + treeInput.Type = "org.osbuild.tree" + treeInput.Origin = "org.osbuild.pipeline" + treeInput.References = []string{"name:" + inputPipeline} + return &osbuild.CopyStageInputs{inputName: treeInput} +} + +func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs { + stageKey := "name:" + stage + ref := map[string]osbuild.QEMUFile{ + stageKey: { + File: file, + }, + } + input := new(osbuild.QEMUStageInput) + input.Type = "org.osbuild.files" + input.Origin = "org.osbuild.pipeline" + input.References = ref + return &osbuild.QEMUStageInputs{Image: input} +} diff --git a/internal/distro/rhel85/stage_options.go b/internal/distro/rhel85/stage_options.go index a7c9bd374..1d92c51d4 100644 --- a/internal/distro/rhel85/stage_options.go +++ b/internal/distro/rhel85/stage_options.go @@ -350,3 +350,127 @@ func grub2StageOptions(pt *disk.PartitionTable, kernelOptions string, kernel *bl return &stageOptions } + +// sfdiskStageOptions creates the options and devices properties for an +// org.osbuild.sfdisk stage based on a partition table description +func sfdiskStageOptions(pt *disk.PartitionTable, device *osbuild.Device) (*osbuild.SfdiskStageOptions, *osbuild.SfdiskStageDevices) { + stageDevices := &osbuild.SfdiskStageDevices{ + Device: *device, + } + + partitions := make([]osbuild.Partition, len(pt.Partitions)) + for idx, p := range pt.Partitions { + partitions[idx] = osbuild.Partition{ + Bootable: p.Bootable, + Size: p.Size, + Start: p.Start, + Type: p.Type, + UUID: p.UUID, + } + } + stageOptions := &osbuild.SfdiskStageOptions{ + Label: pt.Type, + UUID: pt.UUID, + Partitions: partitions, + } + + return stageOptions, stageDevices +} + +// copyFSTreeOptions creates the options, inputs, devices, and mounts properties +// for an org.osbuild.copy stage for a given source tree using a partition +// table description to define the mounts +func copyFSTreeOptions(inputName, inputPipeline string, pt *disk.PartitionTable, device *osbuild.Device) ( + *osbuild.CopyStageOptions, + *osbuild.CopyStageDevices, + *osbuild.CopyStageMounts, +) { + // assume loopback device for simplicity since it's the only one currently supported + // panic if the conversion fails + devOptions, ok := device.Options.(*osbuild.LoopbackDeviceOptions) + if !ok { + panic("copyStageOptions: failed to convert device options to loopback options") + } + + devices := make(map[string]osbuild.Device, len(pt.Partitions)) + mounts := make(map[string]osbuild.Mount, len(pt.Partitions)) + for _, p := range pt.Partitions { + if p.Filesystem == nil { + // no filesystem for partition (e.g., BIOS boot) + continue + } + name := filepath.Base(p.Filesystem.Mountpoint) + if name == "/" { + name = "root" + } + devices[name] = *osbuild.NewLoopbackDevice( + &osbuild.LoopbackDeviceOptions{ + Filename: devOptions.Filename, + Start: p.Start, + Size: p.Size, + }, + ) + var mount *osbuild.Mount + switch p.Filesystem.Type { + case "xfs": + mount = osbuild.NewXfsMount(name, p.Filesystem.Mountpoint) + case "vfat": + mount = osbuild.NewFATMount(name, p.Filesystem.Mountpoint) + case "ext4": + mount = osbuild.NewExt4Mount(name, p.Filesystem.Mountpoint) + case "btrfs": + mount = osbuild.NewBtrfsMount(name, p.Filesystem.Mountpoint) + default: + panic("unknown fs type " + p.Type) + } + mounts[name] = *mount + } + + stageMounts := osbuild.CopyStageMounts(mounts) + stageDevices := osbuild.CopyStageDevices(devices) + + options := osbuild.CopyStageOptions{ + Paths: []osbuild.CopyStagePath{ + { + From: fmt.Sprintf("input://%s/", inputName), + To: "mount://root/", + }, + }, + } + + return &options, &stageDevices, &stageMounts +} + +func grub2InstStageOptions(filename string, pt *disk.PartitionTable, platform string) *osbuild.Grub2InstStageOptions { + bootPartIndex := findBootPartition(pt) + core := osbuild.CoreMkImage{ + Type: "mkimage", + PartLabel: pt.Type, + Filesystem: pt.Partitions[bootPartIndex].Filesystem.Type, + } + + prefix := osbuild.PrefixPartition{ + Type: "partition", + PartLabel: pt.Type, + Number: bootPartIndex, + Path: "/boot/grub2", + } + + return &osbuild.Grub2InstStageOptions{ + Filename: filename, + Platform: platform, + Location: pt.Partitions[0].Start, + Core: core, + Prefix: prefix, + } +} + +func qemuStageOptions(filename string) *osbuild.QEMUStageOptions { + return &osbuild.QEMUStageOptions{ + Filename: filename, + Format: osbuild.Qcow2Options{ + Type: "qcow2", + Compat: "0.10", + }, + } +} diff --git a/internal/osbuild2/qemu_stage_test.go b/internal/osbuild2/qemu_stage_test.go new file mode 100644 index 000000000..31579d40a --- /dev/null +++ b/internal/osbuild2/qemu_stage_test.go @@ -0,0 +1,48 @@ +package osbuild2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewQemukStage(t *testing.T) { + + formatOptionsList := []QEMUFormatOptions{ + Qcow2Options{ + Type: "qcow2", + Compat: "0.10", + }, + VPCOptions{ + Type: "vpc", + }, + VMDKOptions{ + Type: "vmdk", + }, + } + + input := new(QEMUStageInput) + input.Type = "org.osbuild.files" + input.Origin = "org.osbuild.pipeline" + input.References = map[string]QEMUFile{ + "name:stage": { + File: "img.raw", + }, + } + inputs := QEMUStageInputs{Image: input} + + for _, format := range formatOptionsList { + options := QEMUStageOptions{ + Filename: "img.out", + Format: format, + } + expectedStage := &Stage{ + Type: "org.osbuild.qemu", + Options: &options, + Inputs: &inputs, + } + + actualStage := NewQEMUStage(&options, &inputs) + assert.Equal(t, expectedStage, actualStage) + } +} diff --git a/internal/osbuild2/sfdisk_stage_test.go b/internal/osbuild2/sfdisk_stage_test.go new file mode 100644 index 000000000..4a0a3c0d7 --- /dev/null +++ b/internal/osbuild2/sfdisk_stage_test.go @@ -0,0 +1,37 @@ +package osbuild2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewSfdiskStage(t *testing.T) { + + partition := Partition{ + Bootable: true, + Name: "root", + Size: 2097152, + Start: 0, + Type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + UUID: "68B2905B-DF3E-4FB3-80FA-49D1E773AA33", + } + + options := SfdiskStageOptions{ + Label: "gpt", + UUID: "D209C89E-EA5E-4FBD-B161-B461CCE297E0", + Partitions: []Partition{partition}, + } + + device := NewLoopbackDevice(&LoopbackDeviceOptions{Filename: "disk.raw"}) + devices := SfdiskStageDevices{*device} + + expectedStage := &Stage{ + Type: "org.osbuild.sfdisk", + Options: &options, + Devices: &devices, + } + + actualStage := NewSfdiskStage(&options, &devices) + assert.Equal(t, expectedStage, actualStage) +}