distro/rhel85: add image and qcow pipelines

Live image pipeline: Creates the live image in a file through a loopback
device.

Stages:
- truncate: create the file to hold the image
- sfdisk: partition the device
- mkfs.fat, mkfs.xfs: create the filesystems
- copy: copy the tree from the previous pipeline (the OS pipeline) into
  the directories where the partitions are mounted
- grub2.inst: install the bootloader

QEMU pipeline: Convert the live image from the previous pipeline to a
qcow2 image.
This commit is contained in:
Achilleas Koutsou 2021-07-06 19:37:41 +02:00 committed by Ondřej Budai
parent 6debb62758
commit dec74dba32
5 changed files with 338 additions and 1 deletions

View file

@ -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
}

View file

@ -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}
}

View file

@ -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",
},
}
}

View file

@ -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)
}
}

View file

@ -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)
}