From e85fc3b48c56b49fc210a9b15d2a3d89632c0d61 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Tue, 6 Jul 2021 12:38:34 +0200 Subject: [PATCH] osbuild2: new stages and osbuild features Stages: - org.osbuild.copy - org.osbuild.truncate - org.osbuild.sfdisk - org.osbuild.qemu - org.osbuild.mkfs.btrfs - org.osbuild.mkfs.ext4 - org.osbuild.mkfs.fat - org.osbuild.mkfs.xfs - org.osbuild.grub2.inst Stages can now have devices and mounts in addition to options and inputs. Devices: - org.osbuild.loopback Mounts: - org.osbuild.btrfs - org.osbuild.ext4 - org.osbuild.fat - org.osbuild.xfs --- internal/osbuild2/btrfs_mount.go | 9 +++ internal/osbuild2/copy_stage.go | 46 +++++++++++++ internal/osbuild2/device.go | 14 ++++ internal/osbuild2/ext4_mount.go | 9 +++ internal/osbuild2/fat_mount.go | 9 +++ internal/osbuild2/grub2_inst_stage.go | 94 +++++++++++++++++++++++++++ internal/osbuild2/loopback_device.go | 26 ++++++++ internal/osbuild2/mkfs_btrfs_stage.go | 22 +++++++ internal/osbuild2/mkfs_ext4_stage.go | 22 +++++++ internal/osbuild2/mkfs_fat_stage.go | 23 +++++++ internal/osbuild2/mkfs_xfs_stage.go | 22 +++++++ internal/osbuild2/mount.go | 16 +++++ internal/osbuild2/qemu_stage.go | 85 ++++++++++++++++++++++++ internal/osbuild2/sfdisk_stage.go | 51 +++++++++++++++ internal/osbuild2/stage.go | 33 ++++++++++ internal/osbuild2/truncate_stage.go | 20 ++++++ internal/osbuild2/xfs_mount.go | 9 +++ 17 files changed, 510 insertions(+) create mode 100644 internal/osbuild2/btrfs_mount.go create mode 100644 internal/osbuild2/copy_stage.go create mode 100644 internal/osbuild2/device.go create mode 100644 internal/osbuild2/ext4_mount.go create mode 100644 internal/osbuild2/fat_mount.go create mode 100644 internal/osbuild2/grub2_inst_stage.go create mode 100644 internal/osbuild2/loopback_device.go create mode 100644 internal/osbuild2/mkfs_btrfs_stage.go create mode 100644 internal/osbuild2/mkfs_ext4_stage.go create mode 100644 internal/osbuild2/mkfs_fat_stage.go create mode 100644 internal/osbuild2/mkfs_xfs_stage.go create mode 100644 internal/osbuild2/mount.go create mode 100644 internal/osbuild2/qemu_stage.go create mode 100644 internal/osbuild2/sfdisk_stage.go create mode 100644 internal/osbuild2/truncate_stage.go create mode 100644 internal/osbuild2/xfs_mount.go diff --git a/internal/osbuild2/btrfs_mount.go b/internal/osbuild2/btrfs_mount.go new file mode 100644 index 000000000..194c5b3a7 --- /dev/null +++ b/internal/osbuild2/btrfs_mount.go @@ -0,0 +1,9 @@ +package osbuild2 + +func NewBtrfsMount(source, target string) *Mount { + return &Mount{ + Type: "org.osbuild.btrfs", + Source: source, + Target: target, + } +} diff --git a/internal/osbuild2/copy_stage.go b/internal/osbuild2/copy_stage.go new file mode 100644 index 000000000..2eec631e3 --- /dev/null +++ b/internal/osbuild2/copy_stage.go @@ -0,0 +1,46 @@ +package osbuild2 + +// Stage to copy items from inputs to mount points or the tree. Multiple items +// can be copied. The source and destination is a URL. + +type CopyStageOptions struct { + Paths []CopyStagePath `json:"paths"` +} + +type CopyStagePath struct { + From string `json:"from"` + To string `json:"to"` +} + +func (CopyStageOptions) isStageOptions() {} + +type CopyStageInputs map[string]CopyStageInput + +type CopyStageInput struct { + inputCommon + References CopyStageReferences `json:"references"` +} + +func (CopyStageInputs) isStageInputs() {} + +type CopyStageReferences []string + +func (CopyStageReferences) isReferences() {} + +type CopyStageDevices map[string]Device + +func (CopyStageDevices) isStageDevices() {} + +type CopyStageMounts map[string]Mount + +func (CopyStageMounts) isStageMounts() {} + +func NewCopyStage(options *CopyStageOptions, inputs *CopyStageInputs, devices *CopyStageDevices, mounts *CopyStageMounts) *Stage { + return &Stage{ + Type: "org.osbuild.copy", + Options: options, + Inputs: inputs, + Devices: devices, + Mounts: mounts, + } +} diff --git a/internal/osbuild2/device.go b/internal/osbuild2/device.go new file mode 100644 index 000000000..495537c4a --- /dev/null +++ b/internal/osbuild2/device.go @@ -0,0 +1,14 @@ +package osbuild2 + +type Devices interface { + isStageDevices() +} + +type Device struct { + Type string `json:"type"` + Options DeviceOptions `json:"options"` +} + +type DeviceOptions interface { + isDeviceOptions() +} diff --git a/internal/osbuild2/ext4_mount.go b/internal/osbuild2/ext4_mount.go new file mode 100644 index 000000000..817dc11d9 --- /dev/null +++ b/internal/osbuild2/ext4_mount.go @@ -0,0 +1,9 @@ +package osbuild2 + +func NewExt4Mount(source, target string) *Mount { + return &Mount{ + Type: "org.osbuild.ext4", + Source: source, + Target: target, + } +} diff --git a/internal/osbuild2/fat_mount.go b/internal/osbuild2/fat_mount.go new file mode 100644 index 000000000..c5b2e779b --- /dev/null +++ b/internal/osbuild2/fat_mount.go @@ -0,0 +1,9 @@ +package osbuild2 + +func NewFATMount(source, target string) *Mount { + return &Mount{ + Type: "org.osbuild.fat", + Source: source, + Target: target, + } +} diff --git a/internal/osbuild2/grub2_inst_stage.go b/internal/osbuild2/grub2_inst_stage.go new file mode 100644 index 000000000..8019e26e9 --- /dev/null +++ b/internal/osbuild2/grub2_inst_stage.go @@ -0,0 +1,94 @@ +package osbuild2 + +import ( + "encoding/json" + "fmt" +) + +// Install the grub2 boot loader for non-UEFI systems or hybrid boot + +type Grub2InstStageOptions struct { + // Filename of the disk image + Filename string `json:"filename"` + + // Platform of the target system + Platform string `json:"platform"` + + Location uint64 `json:"location,omitempty"` + + // How to obtain the GRUB core image + Core CoreMkImage `json:"core"` + + // Location of grub config + Prefix PrefixPartition `json:"prefix"` + + // Sector size (in bytes) + SectorSize *uint64 `json:"sector-size,omitempty"` +} + +func (Grub2InstStageOptions) isStageOptions() {} + +// Generate the core image via grub-mkimage +type CoreMkImage struct { + Type string `json:"type"` + + PartLabel string `json:"partlabel"` + + Filesystem string `json:"filesystem"` +} + +// Grub2 config on a specific partition, e.g. (,gpt3)/boot +type PrefixPartition struct { + Type string `json:"type"` + + PartLabel string `json:"partlabel"` + + // The partition number, starting at zero + Number uint `json:"number"` + + // Location of the grub config inside the partition + Path string `json:"path"` +} + +func NewGrub2InstStage(options *Grub2InstStageOptions) *Stage { + return &Stage{ + Type: "org.osbuild.grub2.inst", + Options: options, + } +} + +// alias for custom marshaller +type grub2instStageOptions Grub2InstStageOptions + +func (options Grub2InstStageOptions) MarshalJSON() ([]byte, error) { + g2options := grub2instStageOptions(options) + + valueIn := func(v string, options []string) bool { + for _, o := range options { + if v == o { + return true + } + } + return false + } + + // verify enum values + if g2options.Core.Type != "mkimage" { + return nil, fmt.Errorf("org.osbuild.grub2.inst: invalid value %q for core.type", g2options.Core.Type) + } + if !valueIn(g2options.Core.PartLabel, []string{"gpt", "dos"}) { + return nil, fmt.Errorf("org.osbuild.grub2.inst: invalid value %q for core.partlabel", g2options.Core.PartLabel) + } + if !valueIn(g2options.Core.Filesystem, []string{"ext4", "xfs", "btrfs"}) { + return nil, fmt.Errorf("org.osbuild.grub2.inst: invalid value %q for core.filesystem", g2options.Core.Filesystem) + } + + if g2options.Prefix.Type != "partition" { + return nil, fmt.Errorf("org.osbuild.grub2.inst: invalid value %q for prefix.type", g2options.Prefix.Type) + } + if !valueIn(g2options.Prefix.PartLabel, []string{"gpt", "dos"}) { + return nil, fmt.Errorf("org.osbuild.grub2.inst: invalid value %q for core.partlabel", g2options.Core.PartLabel) + } + + return json.Marshal(g2options) +} diff --git a/internal/osbuild2/loopback_device.go b/internal/osbuild2/loopback_device.go new file mode 100644 index 000000000..1a1142a3d --- /dev/null +++ b/internal/osbuild2/loopback_device.go @@ -0,0 +1,26 @@ +package osbuild2 + +// Expose a file (or part of it) as a device node + +type LoopbackDeviceOptions struct { + // File to associate with the loopback device + Filename string `json:"filename"` + + // Start of the data segment + Start uint64 `json:"start,omitempty"` + + // Size limit of the data segment (in sectors) + Size uint64 `json:"size,omitempty"` + + // Sector size (in bytes) + SectorSize *uint64 `json:"sector-size,omitempty"` +} + +func (LoopbackDeviceOptions) isDeviceOptions() {} + +func NewLoopbackDevice(options *LoopbackDeviceOptions) *Device { + return &Device{ + Type: "org.osbuild.loopback", + Options: options, + } +} diff --git a/internal/osbuild2/mkfs_btrfs_stage.go b/internal/osbuild2/mkfs_btrfs_stage.go new file mode 100644 index 000000000..38f1002f6 --- /dev/null +++ b/internal/osbuild2/mkfs_btrfs_stage.go @@ -0,0 +1,22 @@ +package osbuild2 + +type MkfsBtrfsStageOptions struct { + UUID string `json:"uuid"` + Label string `json:"label,omitempty"` +} + +func (MkfsBtrfsStageOptions) isStageOptions() {} + +type MkfsBtrfsStageDevices struct { + Device Device `json:"device"` +} + +func (MkfsBtrfsStageDevices) isStageDevices() {} + +func NewMkfsBtrfsStage(options *MkfsBtrfsStageOptions, devices *MkfsBtrfsStageDevices) *Stage { + return &Stage{ + Type: "org.osbuild.mkfs.btrfs", + Options: options, + Devices: devices, + } +} diff --git a/internal/osbuild2/mkfs_ext4_stage.go b/internal/osbuild2/mkfs_ext4_stage.go new file mode 100644 index 000000000..0472cf113 --- /dev/null +++ b/internal/osbuild2/mkfs_ext4_stage.go @@ -0,0 +1,22 @@ +package osbuild2 + +type MkfsExt4StageOptions struct { + UUID string `json:"uuid"` + Label string `json:"label,omitempty"` +} + +func (MkfsExt4StageOptions) isStageOptions() {} + +type MkfsExt4StageDevices struct { + Device Device `json:"device"` +} + +func (MkfsExt4StageDevices) isStageDevices() {} + +func NewMkfsExt4Stage(options *MkfsExt4StageOptions, devices *MkfsExt4StageDevices) *Stage { + return &Stage{ + Type: "org.osbuild.mkfs.ext4", + Options: options, + Devices: devices, + } +} diff --git a/internal/osbuild2/mkfs_fat_stage.go b/internal/osbuild2/mkfs_fat_stage.go new file mode 100644 index 000000000..b7556ba67 --- /dev/null +++ b/internal/osbuild2/mkfs_fat_stage.go @@ -0,0 +1,23 @@ +package osbuild2 + +type MkfsFATStageOptions struct { + VolID string `json:"volid"` + Label string `json:"label,omitempty"` + FATSize *int `json:"fat-size,omitempty"` +} + +func (MkfsFATStageOptions) isStageOptions() {} + +type MkfsFATStageDevices struct { + Device Device `json:"device"` +} + +func (MkfsFATStageDevices) isStageDevices() {} + +func NewMkfsFATStage(options *MkfsFATStageOptions, devices *MkfsFATStageDevices) *Stage { + return &Stage{ + Type: "org.osbuild.mkfs.fat", + Options: options, + Devices: devices, + } +} diff --git a/internal/osbuild2/mkfs_xfs_stage.go b/internal/osbuild2/mkfs_xfs_stage.go new file mode 100644 index 000000000..81f5c6a5a --- /dev/null +++ b/internal/osbuild2/mkfs_xfs_stage.go @@ -0,0 +1,22 @@ +package osbuild2 + +type MkfsXfsStageOptions struct { + UUID string `json:"uuid"` + Label string `json:"label,omitempty"` +} + +func (MkfsXfsStageOptions) isStageOptions() {} + +type MkfsXfsStageDevices struct { + Device Device `json:"device"` +} + +func (MkfsXfsStageDevices) isStageDevices() {} + +func NewMkfsXfsStage(options *MkfsXfsStageOptions, devices *MkfsXfsStageDevices) *Stage { + return &Stage{ + Type: "org.osbuild.mkfs.xfs", + Options: options, + Devices: devices, + } +} diff --git a/internal/osbuild2/mount.go b/internal/osbuild2/mount.go new file mode 100644 index 000000000..806753e68 --- /dev/null +++ b/internal/osbuild2/mount.go @@ -0,0 +1,16 @@ +package osbuild2 + +type Mounts interface { + isStageMounts() +} + +type Mount struct { + Type string `json:"type"` + Source string `json:"source"` + Target string `json:"target"` + Options *MountOptions `json:"options,omitempty"` +} + +type MountOptions interface { + isMountOptions() +} diff --git a/internal/osbuild2/qemu_stage.go b/internal/osbuild2/qemu_stage.go new file mode 100644 index 000000000..16ac5e10e --- /dev/null +++ b/internal/osbuild2/qemu_stage.go @@ -0,0 +1,85 @@ +package osbuild2 + +import ( + "encoding/json" + "fmt" +) + +// Convert a disk image to a different format. +// +// Some formats support format-specific options: +// qcow2: The compatibility version can be specified via 'compat' + +type QEMUStageOptions struct { + // Filename for resulting image + Filename string `json:"filename"` + + // Image format and options + Format QEMUFormatOptions `json:"format"` +} + +type QEMUFormatOptions interface { + isQEMUFormatOptions() +} + +type Qcow2Options struct { + // The type of the format must be 'qcow2' + Type string `json:"type"` + + // The qcow2-compatibility-version to use + Compat string `json:"compat"` +} + +func (Qcow2Options) isQEMUFormatOptions() {} + +func (QEMUStageOptions) isStageOptions() {} + +type QEMUStageInputs struct { + Image *QEMUStageInput `json:"image"` +} + +func (QEMUStageInputs) isStageInputs() {} + +type QEMUStageInput struct { + inputCommon + References QEMUStageReferences `json:"references"` +} + +func (QEMUStageInput) isStageInput() {} + +type QEMUStageReferences map[string]QEMUFile + +func (QEMUStageReferences) isReferences() {} + +type QEMUFile struct { + Metadata FileMetadata `json:"metadata,omitempty"` + File string `json:"file,omitempty"` +} + +type FileMetadata map[string]interface{} + +// NewQEMUStage creates a new QEMU Stage object. +func NewQEMUStage(options *QEMUStageOptions, inputs *QEMUStageInputs) *Stage { + return &Stage{ + Type: "org.osbuild.qemu", + Options: options, + Inputs: inputs, + } +} + +// alias for custom marshaller +type qemuStageOptions QEMUStageOptions + +// Custom marshaller for validating +func (options QEMUStageOptions) MarshalJSON() ([]byte, error) { + switch o := options.Format.(type) { + case Qcow2Options: + if o.Type != "qcow2" { + return nil, fmt.Errorf("invalid format type %q for qcow2 options", o.Type) + } + default: + return nil, fmt.Errorf("unknown format options in QEMU stage: %#v", options.Format) + } + + return json.Marshal(qemuStageOptions(options)) +} diff --git a/internal/osbuild2/sfdisk_stage.go b/internal/osbuild2/sfdisk_stage.go new file mode 100644 index 000000000..c27995f82 --- /dev/null +++ b/internal/osbuild2/sfdisk_stage.go @@ -0,0 +1,51 @@ +package osbuild2 + +// Partition a target using sfdisk(8) + +type SfdiskStageOptions struct { + // The type of the partition table + Label string `json:"label"` + + // UUID for the disk image's partition table + UUID string `json:"uuid"` + + // Partition layout + Partitions []Partition `json:"partitions,omitempty"` +} + +func (SfdiskStageOptions) isStageOptions() {} + +// Description of a partition +type Partition struct { + // Mark the partition as bootable (dos) + Bootable bool `json:"bootable,omitempty"` + + // The partition name (GPT) + Name string `json:"name,omitempty"` + + // The size of the partition + Size uint64 `json:"size,omitempty"` + + // The start offset of the partition + Start uint64 `json:"start,omitempty"` + + // The partition type (UUID or identifier) + Type string `json:"type,omitempty"` + + // UUID of the partition (GPT) + UUID string `json:"uuid,omitempty"` +} + +type SfdiskStageDevices struct { + Device Device `json:"device"` +} + +func (SfdiskStageDevices) isStageDevices() {} + +func NewSfdiskStage(options *SfdiskStageOptions, devices *SfdiskStageDevices) *Stage { + return &Stage{ + Type: "org.osbuild.sfdisk", + Options: options, + Devices: devices, + } +} diff --git a/internal/osbuild2/stage.go b/internal/osbuild2/stage.go index 454baffb1..5fa2522f1 100644 --- a/internal/osbuild2/stage.go +++ b/internal/osbuild2/stage.go @@ -14,6 +14,8 @@ type Stage struct { Inputs Inputs `json:"inputs,omitempty"` Options StageOptions `json:"options,omitempty"` + Devices Devices `json:"devices,omitempty"` + Mounts Mounts `json:"mounts,omitempty"` } // Collection of Inputs for a Stage @@ -55,6 +57,8 @@ type rawStage struct { Type string `json:"type"` Options json.RawMessage `json:"options"` Inputs json.RawMessage `json:"inputs"` + Devices json.RawMessage `json:"devices"` + Mounts json.RawMessage `json:"mounts"` } // UnmarshalJSON unmarshals JSON into a Stage object. Each type of stage has @@ -66,6 +70,8 @@ func (stage *Stage) UnmarshalJSON(data []byte) error { } var options StageOptions var inputs Inputs + var devices Devices + var mounts Mounts switch rawStage.Type { case "org.osbuild.authselect": options = new(AuthselectStageOptions) @@ -129,6 +135,31 @@ func (stage *Stage) UnmarshalJSON(data []byte) error { options = new(OSTreeInitStageOptions) case "org.osbuild.ostree.preptree": options = new(OSTreePrepTreeStageOptions) + case "org.osbuild.truncate": + options = new(TruncateStageOptions) + case "org.osbuild.sfdisk": + options = new(SfdiskStageOptions) + devices = new(SfdiskStageDevices) + case "org.osbuild.copy": + options = new(CopyStageOptions) + inputs = new(CopyStageInputs) + devices = new(CopyStageDevices) + mounts = new(CopyStageMounts) + case "org.osbuild.mkfs.btrfs": + options = new(MkfsBtrfsStageOptions) + devices = new(MkfsBtrfsStageDevices) + case "org.osbuild.mkfs.ext4": + options = new(MkfsExt4StageOptions) + devices = new(MkfsExt4StageDevices) + case "org.osbuild.mkfs.fat": + options = new(MkfsFATStageOptions) + devices = new(MkfsFATStageDevices) + case "org.osbuild.mkfs.xfs": + options = new(MkfsXfsStageOptions) + devices = new(MkfsXfsStageDevices) + case "org.osbuild.qemu": + options = new(QEMUStageOptions) + inputs = new(QEMUStageInputs) default: return fmt.Errorf("unexpected stage type: %s", rawStage.Type) } @@ -144,6 +175,8 @@ func (stage *Stage) UnmarshalJSON(data []byte) error { stage.Type = rawStage.Type stage.Options = options stage.Inputs = inputs + stage.Devices = devices + stage.Mounts = mounts return nil } diff --git a/internal/osbuild2/truncate_stage.go b/internal/osbuild2/truncate_stage.go new file mode 100644 index 000000000..65c61d2a4 --- /dev/null +++ b/internal/osbuild2/truncate_stage.go @@ -0,0 +1,20 @@ +package osbuild2 + +// Create, shrink, or extend a file + +type TruncateStageOptions struct { + // Image filename + Filename string `json:"filename"` + + // Desired size + Size string `json:"size"` +} + +func (TruncateStageOptions) isStageOptions() {} + +func NewTruncateStage(options *TruncateStageOptions) *Stage { + return &Stage{ + Type: "org.osbuild.truncate", + Options: options, + } +} diff --git a/internal/osbuild2/xfs_mount.go b/internal/osbuild2/xfs_mount.go new file mode 100644 index 000000000..4be933d55 --- /dev/null +++ b/internal/osbuild2/xfs_mount.go @@ -0,0 +1,9 @@ +package osbuild2 + +func NewXfsMount(source, target string) *Mount { + return &Mount{ + Type: "org.osbuild.xfs", + Source: source, + Target: target, + } +}