blueprint: add local DiskCustomization
Add DiskCustomization and all its children to the internal blueprint. Add the conversion to the images counterpart to the Convert() function.
This commit is contained in:
parent
da9a911dea
commit
68dd8699ed
3 changed files with 395 additions and 0 deletions
|
|
@ -313,6 +313,44 @@ func Convert(bp Blueprint) iblueprint.Blueprint {
|
|||
}
|
||||
customizations.Filesystem = ifs
|
||||
}
|
||||
if disk := c.Disk; disk != nil {
|
||||
idisk := &iblueprint.DiskCustomization{
|
||||
MinSize: disk.MinSize,
|
||||
Partitions: make([]iblueprint.PartitionCustomization, len(disk.Partitions)),
|
||||
}
|
||||
for idx, part := range disk.Partitions {
|
||||
ipart := iblueprint.PartitionCustomization{
|
||||
Type: part.Type,
|
||||
MinSize: part.MinSize,
|
||||
BtrfsVolumeCustomization: iblueprint.BtrfsVolumeCustomization{},
|
||||
VGCustomization: iblueprint.VGCustomization{
|
||||
Name: part.VGCustomization.Name,
|
||||
},
|
||||
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization(part.FilesystemTypedCustomization),
|
||||
}
|
||||
|
||||
if len(part.LogicalVolumes) > 0 {
|
||||
ipart.LogicalVolumes = make([]iblueprint.LVCustomization, len(part.LogicalVolumes))
|
||||
for lvidx, lv := range part.LogicalVolumes {
|
||||
ipart.LogicalVolumes[lvidx] = iblueprint.LVCustomization{
|
||||
Name: lv.Name,
|
||||
MinSize: lv.MinSize,
|
||||
FilesystemTypedCustomization: iblueprint.FilesystemTypedCustomization(lv.FilesystemTypedCustomization),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(part.Subvolumes) > 0 {
|
||||
ipart.Subvolumes = make([]iblueprint.BtrfsSubvolumeCustomization, len(part.Subvolumes))
|
||||
for svidx, sv := range part.Subvolumes {
|
||||
ipart.Subvolumes[svidx] = iblueprint.BtrfsSubvolumeCustomization(sv)
|
||||
}
|
||||
}
|
||||
|
||||
idisk.Partitions[idx] = ipart
|
||||
}
|
||||
customizations.Disk = idisk
|
||||
}
|
||||
if tz := c.Timezone; tz != nil {
|
||||
itz := iblueprint.TimezoneCustomization(*tz)
|
||||
customizations.Timezone = &itz
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type Customizations struct {
|
|||
Firewall *FirewallCustomization `json:"firewall,omitempty" toml:"firewall,omitempty"`
|
||||
Services *ServicesCustomization `json:"services,omitempty" toml:"services,omitempty"`
|
||||
Filesystem []FilesystemCustomization `json:"filesystem,omitempty" toml:"filesystem,omitempty"`
|
||||
Disk *DiskCustomization `json:"disk,omitempty" toml:"disk,omitempty"`
|
||||
InstallationDevice string `json:"installation_device,omitempty" toml:"installation_device,omitempty"`
|
||||
PartitioningMode string `json:"partitioning_mode,omitempty" toml:"partitioning_mode,omitempty"`
|
||||
FDO *FDOCustomization `json:"fdo,omitempty" toml:"fdo,omitempty"`
|
||||
|
|
|
|||
356
internal/blueprint/disk_customizations.go
Normal file
356
internal/blueprint/disk_customizations.go
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
package blueprint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/osbuild/images/pkg/datasizes"
|
||||
)
|
||||
|
||||
type DiskCustomization struct {
|
||||
MinSize uint64
|
||||
Partitions []PartitionCustomization
|
||||
}
|
||||
|
||||
type diskCustomizationMarshaler struct {
|
||||
MinSize datasizes.Size `json:"minsize,omitempty" toml:"minsize,omitempty"`
|
||||
Partitions []PartitionCustomization `json:"partitions,omitempty" toml:"partitions,omitempty"`
|
||||
}
|
||||
|
||||
func (dc *DiskCustomization) UnmarshalJSON(data []byte) error {
|
||||
var dcm diskCustomizationMarshaler
|
||||
if err := json.Unmarshal(data, &dcm); err != nil {
|
||||
return err
|
||||
}
|
||||
dc.MinSize = dcm.MinSize.Uint64()
|
||||
dc.Partitions = dcm.Partitions
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *DiskCustomization) UnmarshalTOML(data any) error {
|
||||
return unmarshalTOMLviaJSON(dc, data)
|
||||
}
|
||||
|
||||
// PartitionCustomization defines a single partition on a disk. The Type
|
||||
// defines the kind of "payload" for the partition: plain, lvm, or btrfs.
|
||||
// - plain: the payload will be a filesystem on a partition (e.g. xfs, ext4).
|
||||
// See [FilesystemTypedCustomization] for extra fields.
|
||||
// - lvm: the payload will be an LVM volume group. See [VGCustomization] for
|
||||
// extra fields
|
||||
// - btrfs: the payload will be a btrfs volume. See
|
||||
// [BtrfsVolumeCustomization] for extra fields.
|
||||
type PartitionCustomization struct {
|
||||
// The type of payload for the partition (optional, defaults to "plain").
|
||||
Type string `json:"type" toml:"type"`
|
||||
|
||||
// Minimum size of the partition that contains the filesystem (for "plain"
|
||||
// filesystem), volume group ("lvm"), or btrfs volume ("btrfs"). The final
|
||||
// size of the partition will be larger than the minsize if the sum of the
|
||||
// contained volumes (logical volumes or subvolumes) is larger. In
|
||||
// addition, certain mountpoints have required minimum sizes. See
|
||||
// https://osbuild.org/docs/user-guide/partitioning for more details.
|
||||
// (optional, defaults depend on payload and mountpoints).
|
||||
MinSize uint64 `json:"minsize" toml:"minsize"`
|
||||
|
||||
BtrfsVolumeCustomization
|
||||
|
||||
VGCustomization
|
||||
|
||||
FilesystemTypedCustomization
|
||||
}
|
||||
|
||||
// A filesystem on a plain partition or LVM logical volume.
|
||||
// Note the differences from [FilesystemCustomization]:
|
||||
// - Adds a label.
|
||||
// - Adds a filesystem type (fs_type).
|
||||
// - Does not define a size. The size is defined by its container: a
|
||||
// partition ([PartitionCustomization]) or LVM logical volume
|
||||
// ([LVCustomization]).
|
||||
//
|
||||
// Setting the FSType to "swap" creates a swap area (and the Mountpoint must be
|
||||
// empty).
|
||||
type FilesystemTypedCustomization struct {
|
||||
Mountpoint string `json:"mountpoint,omitempty" toml:"mountpoint,omitempty"`
|
||||
Label string `json:"label,omitempty" toml:"label,omitempty"`
|
||||
FSType string `json:"fs_type,omitempty" toml:"fs_type,omitempty"`
|
||||
}
|
||||
|
||||
// An LVM volume group with one or more logical volumes.
|
||||
type VGCustomization struct {
|
||||
// Volume group name (optional, default will be automatically generated).
|
||||
Name string `json:"name,omitempty" toml:"name,omitempty"`
|
||||
LogicalVolumes []LVCustomization `json:"logical_volumes,omitempty" toml:"logical_volumes,omitempty"`
|
||||
}
|
||||
|
||||
type LVCustomization struct {
|
||||
// Logical volume name
|
||||
Name string `json:"name,omitempty" toml:"name,omitempty"`
|
||||
|
||||
// Minimum size of the logical volume
|
||||
MinSize uint64 `json:"minsize,omitempty" toml:"minsize,omitempty"`
|
||||
|
||||
FilesystemTypedCustomization
|
||||
}
|
||||
|
||||
// Custom JSON unmarshaller for LVCustomization for handling the conversion of
|
||||
// data sizes (minsize) expressed as strings to uint64.
|
||||
func (lv *LVCustomization) UnmarshalJSON(data []byte) error {
|
||||
var lvAnySize struct {
|
||||
Name string `json:"name,omitempty" toml:"name,omitempty"`
|
||||
MinSize any `json:"minsize,omitempty" toml:"minsize,omitempty"`
|
||||
FilesystemTypedCustomization
|
||||
}
|
||||
if err := json.Unmarshal(data, &lvAnySize); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lv.Name = lvAnySize.Name
|
||||
lv.FilesystemTypedCustomization = lvAnySize.FilesystemTypedCustomization
|
||||
|
||||
if lvAnySize.MinSize == nil {
|
||||
return fmt.Errorf("minsize is required")
|
||||
}
|
||||
size, err := decodeSize(lvAnySize.MinSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lv.MinSize = size
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A btrfs volume consisting of one or more subvolumes.
|
||||
type BtrfsVolumeCustomization struct {
|
||||
Subvolumes []BtrfsSubvolumeCustomization `json:"subvolumes,omitempty" toml:"subvolumes,omitempty"`
|
||||
}
|
||||
|
||||
type BtrfsSubvolumeCustomization struct {
|
||||
// The name of the subvolume, which defines the location (path) on the
|
||||
// root volume (required).
|
||||
// See https://btrfs.readthedocs.io/en/latest/Subvolumes.html
|
||||
Name string `json:"name" toml:"name"`
|
||||
|
||||
// Mountpoint for the subvolume.
|
||||
Mountpoint string `json:"mountpoint" toml:"mountpoint"`
|
||||
}
|
||||
|
||||
// Custom JSON unmarshaller that first reads the value of the "type" field and
|
||||
// then deserialises the whole object into a struct that only contains the
|
||||
// fields valid for that partition type. This ensures that no fields are set
|
||||
// for the substructure of a different type than the one defined in the "type"
|
||||
// fields.
|
||||
func (v *PartitionCustomization) UnmarshalJSON(data []byte) error {
|
||||
errPrefix := "JSON unmarshal:"
|
||||
var typeSniffer struct {
|
||||
Type string `json:"type"`
|
||||
MinSize any `json:"minsize"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &typeSniffer); err != nil {
|
||||
return fmt.Errorf("%s %w", errPrefix, err)
|
||||
}
|
||||
|
||||
partType := "plain"
|
||||
if typeSniffer.Type != "" {
|
||||
partType = typeSniffer.Type
|
||||
}
|
||||
|
||||
switch partType {
|
||||
case "plain":
|
||||
if err := decodePlain(v, data); err != nil {
|
||||
return fmt.Errorf("%s %w", errPrefix, err)
|
||||
}
|
||||
case "btrfs":
|
||||
if err := decodeBtrfs(v, data); err != nil {
|
||||
return fmt.Errorf("%s %w", errPrefix, err)
|
||||
}
|
||||
case "lvm":
|
||||
if err := decodeLVM(v, data); err != nil {
|
||||
return fmt.Errorf("%s %w", errPrefix, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%s unknown partition type: %s", errPrefix, partType)
|
||||
}
|
||||
|
||||
v.Type = partType
|
||||
|
||||
if typeSniffer.MinSize == nil {
|
||||
return fmt.Errorf("minsize is required")
|
||||
}
|
||||
|
||||
minsize, err := decodeSize(typeSniffer.MinSize)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s error decoding minsize for partition: %w", errPrefix, err)
|
||||
}
|
||||
v.MinSize = minsize
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodePlain decodes the data into a struct that only embeds the
|
||||
// FilesystemCustomization with DisallowUnknownFields. This ensures that when
|
||||
// the type is "plain", none of the fields for btrfs or lvm are used.
|
||||
func decodePlain(v *PartitionCustomization, data []byte) error {
|
||||
var plain struct {
|
||||
// Type and minsize are handled by the caller. These are added here to
|
||||
// satisfy "DisallowUnknownFields" when decoding.
|
||||
Type string `json:"type"`
|
||||
MinSize any `json:"minsize"`
|
||||
FilesystemTypedCustomization
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(&plain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding partition with type \"plain\": %w", err)
|
||||
}
|
||||
|
||||
v.FilesystemTypedCustomization = plain.FilesystemTypedCustomization
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeBtrfs decodes the data into a struct that only embeds the
|
||||
// BtrfsVolumeCustomization with DisallowUnknownFields. This ensures that when
|
||||
// the type is btrfs, none of the fields for plain or lvm are used.
|
||||
func decodeBtrfs(v *PartitionCustomization, data []byte) error {
|
||||
var btrfs struct {
|
||||
// Type and minsize are handled by the caller. These are added here to
|
||||
// satisfy "DisallowUnknownFields" when decoding.
|
||||
Type string `json:"type"`
|
||||
MinSize any `json:"minsize"`
|
||||
BtrfsVolumeCustomization
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
err := decoder.Decode(&btrfs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding partition with type \"btrfs\": %w", err)
|
||||
}
|
||||
|
||||
v.BtrfsVolumeCustomization = btrfs.BtrfsVolumeCustomization
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeLVM decodes the data into a struct that only embeds the
|
||||
// VGCustomization with DisallowUnknownFields. This ensures that when the type
|
||||
// is lvm, none of the fields for plain or btrfs are used.
|
||||
func decodeLVM(v *PartitionCustomization, data []byte) error {
|
||||
var vg struct {
|
||||
// Type and minsize are handled by the caller. These are added here to
|
||||
// satisfy "DisallowUnknownFields" when decoding.
|
||||
Type string `json:"type"`
|
||||
MinSize any `json:"minsize"`
|
||||
VGCustomization
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&vg); err != nil {
|
||||
return fmt.Errorf("error decoding partition with type \"lvm\": %w", err)
|
||||
}
|
||||
|
||||
v.VGCustomization = vg.VGCustomization
|
||||
return nil
|
||||
}
|
||||
|
||||
// Custom TOML unmarshaller that first reads the value of the "type" field and
|
||||
// then deserialises the whole object into a struct that only contains the
|
||||
// fields valid for that partition type. This ensures that no fields are set
|
||||
// for the substructure of a different type than the one defined in the "type"
|
||||
// fields.
|
||||
func (v *PartitionCustomization) UnmarshalTOML(data any) error {
|
||||
errPrefix := "TOML unmarshal:"
|
||||
d, ok := data.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s customizations.partition is not an object", errPrefix)
|
||||
}
|
||||
|
||||
partType := "plain"
|
||||
if typeField, ok := d["type"]; ok {
|
||||
typeStr, ok := typeField.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s type must be a string, got \"%v\" of type %T", errPrefix, typeField, typeField)
|
||||
}
|
||||
partType = typeStr
|
||||
}
|
||||
|
||||
// serialise the data to JSON and reuse the subobject decoders
|
||||
dataJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s error while decoding partition customization: %w", errPrefix, err)
|
||||
}
|
||||
switch partType {
|
||||
case "plain":
|
||||
if err := decodePlain(v, dataJSON); err != nil {
|
||||
return fmt.Errorf("%s %w", errPrefix, err)
|
||||
}
|
||||
case "btrfs":
|
||||
if err := decodeBtrfs(v, dataJSON); err != nil {
|
||||
return fmt.Errorf("%s %w", errPrefix, err)
|
||||
}
|
||||
case "lvm":
|
||||
if err := decodeLVM(v, dataJSON); err != nil {
|
||||
return fmt.Errorf("%s %w", errPrefix, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%s unknown partition type: %s", errPrefix, partType)
|
||||
}
|
||||
|
||||
v.Type = partType
|
||||
|
||||
minsizeField, ok := d["minsize"]
|
||||
if !ok {
|
||||
return fmt.Errorf("minsize is required")
|
||||
}
|
||||
minsize, err := decodeSize(minsizeField)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s error decoding minsize for partition: %w", errPrefix, err)
|
||||
}
|
||||
v.MinSize = minsize
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalTOMLviaJSON(u json.Unmarshaler, data any) error {
|
||||
// This is the most efficient way to reuse code when unmarshaling
|
||||
// structs in toml, it leaks json errors which is a bit sad but
|
||||
// because the toml unmarshaler gives us not "[]byte" but an
|
||||
// already pre-processed "any" we cannot just unmarshal into our
|
||||
// "fooMarshaling" struct and reuse the result so we resort to
|
||||
// this workaround (but toml will go away long term anyway).
|
||||
dataJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshaling TOML data %v: %w", data, err)
|
||||
}
|
||||
if err := u.UnmarshalJSON(dataJSON); err != nil {
|
||||
return fmt.Errorf("error decoding TOML %v: %w", data, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeSize takes an integer or string representing a data size (with a data
|
||||
// suffix) and returns the uint64 representation.
|
||||
func decodeSize(size any) (uint64, error) {
|
||||
switch s := size.(type) {
|
||||
case string:
|
||||
return datasizes.Parse(s)
|
||||
case int64:
|
||||
if s < 0 {
|
||||
return 0, fmt.Errorf("cannot be negative")
|
||||
}
|
||||
return uint64(s), nil
|
||||
case float64:
|
||||
if s < 0 {
|
||||
return 0, fmt.Errorf("cannot be negative")
|
||||
}
|
||||
// TODO: emit warning of possible truncation?
|
||||
return uint64(s), nil
|
||||
case uint64:
|
||||
return s, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("failed to convert value \"%v\" to number", size)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue