Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advanced partitioning customizations (COMPOSER-2355) #926

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f39037a
blueprint: partitioning customization
achilleas-k Aug 27, 2024
805ece4
disk: NewCustomPartitionTable() function
achilleas-k Sep 3, 2024
68fb75a
disk: basic test structure for NewCustomPartitionTable()
achilleas-k Sep 9, 2024
548e871
disk: more testing for NewCustomPartitionTable()
achilleas-k Sep 9, 2024
ae2d48b
disk: move handling of the /boot partition
achilleas-k Sep 9, 2024
e0ba877
pkg/{distro,platform}: move BootMode to platform
achilleas-k Sep 6, 2024
8b5653d
disk: add bios and esp partitions based on boot mode
achilleas-k Sep 6, 2024
80ce76d
disk: add root partition / lv / subvol if missing
achilleas-k Sep 6, 2024
766911f
disk: create root partition for empty customizations
achilleas-k Sep 9, 2024
7e9ca23
blueprint: include minsize in partitioning customization
achilleas-k Sep 9, 2024
f88e6e2
disk: add defaultType to NewCustomPartitionTable()
achilleas-k Sep 9, 2024
397efb0
disk: use defaultType when missing fs type customization
achilleas-k Sep 9, 2024
9b75803
disk: define FSType enum for all supported types
achilleas-k Sep 9, 2024
4a689db
disk: relayout the partition table in NewCustomPartitionTable()
achilleas-k Sep 9, 2024
51c44a5
disk: CustomPartitionTableOptions struct
achilleas-k Sep 9, 2024
986b64c
disk: add PartitionTableType to CustomPartitionTableOptions
achilleas-k Sep 10, 2024
d7b9dd4
disk: add "gpt", "dos", and "" types to tests
achilleas-k Sep 10, 2024
24b83ca
disk: generate UUIDs in NewCustomPartitionTable()
achilleas-k Sep 10, 2024
d844a8a
disk: add RequiredSizes to CustomPartitionTableOptions
achilleas-k Sep 12, 2024
4e7a7ee
blueprint: remove GetFilesystemsMinSize()
achilleas-k Sep 11, 2024
2a0bc1b
blueprint: add getter for PartitioningCustomization
achilleas-k Sep 11, 2024
9a5f8dd
blueprint: add CheckPartitioningPolicy()
achilleas-k Sep 11, 2024
92f089b
distro/fedora: pass all customizations to getPartitionTable()
achilleas-k Sep 11, 2024
5da5eae
distro/fedora: enable the new partitioning functionality
achilleas-k Sep 11, 2024
bcfb63a
distro/fedora: add minimum directory sizes to image types
achilleas-k Sep 18, 2024
d50bf94
distro/fedora: check partitioning customizations
achilleas-k Sep 11, 2024
84d94e0
blueprint: validate partitioning customizations
achilleas-k Sep 11, 2024
d9fe555
disk: refactor LVM Logical Volume name generation
achilleas-k Sep 12, 2024
541f95f
disk: use vg.CreateLogicalVolume() in NewCustomPartitionTable()
achilleas-k Sep 11, 2024
c7c3486
disk: generate volume group names when unspecified
achilleas-k Sep 12, 2024
837ce9b
test: add partitioning build configs
achilleas-k Sep 11, 2024
9451486
blueprint: add extra validation for mountpoints and fstypes
achilleas-k Sep 19, 2024
01b0ab7
disk: private resolver for CustomPartitionTableOptions.DefaultFSType
achilleas-k Sep 19, 2024
5bf3416
disk: split NewCustomPartitionTable() into smaller functions
achilleas-k Sep 19, 2024
f6fd14a
disk: more custom partition table tests
achilleas-k Sep 19, 2024
0266934
disk: add error tests for NewCustomPartitionTable()
achilleas-k Sep 19, 2024
6b1f037
disk: reword invalid pt type error message
achilleas-k Sep 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions pkg/blueprint/customizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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"`
Partitioning *PartitioningCustomization `json:"partitioning,omitempty" toml:"partitioning,omitempty"`
InstallationDevice string `json:"installation_device,omitempty" toml:"installation_device,omitempty"`
FDO *FDOCustomization `json:"fdo,omitempty" toml:"fdo,omitempty"`
OpenSCAP *OpenSCAPCustomization `json:"openscap,omitempty" toml:"openscap,omitempty"`
Expand Down Expand Up @@ -309,20 +310,11 @@ func (c *Customizations) GetFilesystems() []FilesystemCustomization {
return c.Filesystem
}

func (c *Customizations) GetFilesystemsMinSize() uint64 {
func (c *Customizations) GetPartitioning() *PartitioningCustomization {
if c == nil {
return 0
}
var agg uint64
for _, m := range c.Filesystem {
agg += m.MinSize
}
// This ensures that file system customization `size` is a multiple of
// sector size (512)
if agg%512 != 0 {
agg = (agg/512 + 1) * 512
return nil
}
return agg
return c.Partitioning
}

func (c *Customizations) GetInstallationDevice() string {
Expand Down
42 changes: 0 additions & 42 deletions pkg/blueprint/customizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,48 +318,6 @@ func TestGetFilesystems(t *testing.T) {
assert.ElementsMatch(t, expectedFilesystems, retFilesystems)
}

func TestGetFilesystemsMinSize(t *testing.T) {
expectedFilesystems := []FilesystemCustomization{
{
MinSize: 1024,
Mountpoint: "/",
},
{
MinSize: 4096,
Mountpoint: "/var",
},
}

TestCustomizations := Customizations{
Filesystem: expectedFilesystems,
}

retFilesystemsSize := TestCustomizations.GetFilesystemsMinSize()

assert.EqualValues(t, uint64(5120), retFilesystemsSize)
}

func TestGetFilesystemsMinSizeNonSectorSize(t *testing.T) {
expectedFilesystems := []FilesystemCustomization{
{
MinSize: 1025,
Mountpoint: "/",
},
{
MinSize: 4097,
Mountpoint: "/var",
},
}

TestCustomizations := Customizations{
Filesystem: expectedFilesystems,
}

retFilesystemsSize := TestCustomizations.GetFilesystemsMinSize()

assert.EqualValues(t, uint64(5632), retFilesystemsSize)
}

func TestGetOpenSCAPConfig(t *testing.T) {
expectedOscap := OpenSCAPCustomization{
DataStream: "test-data-stream.xml",
Expand Down
281 changes: 280 additions & 1 deletion pkg/blueprint/filesystem_customizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,215 @@ import (
"encoding/json"
"errors"
"fmt"
"path/filepath"
"slices"
"strings"

"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/pkg/pathpolicy"
)

type PartitioningCustomization struct {
MinSize uint64 `json:"minsize,omitempty" toml:"minsize,omitempty"`
Plain *PlainFilesystemCustomization `json:"plain,omitempty" toml:"plain,omitempty"`
LVM *LVMCustomization `json:"lvm,omitempty" toml:"lvm,omitempty"`
Btrfs *BtrfsCustomization `json:"btrfs,omitempty" toml:"btrfs,omitempty"`
}

type PlainFilesystemCustomization struct {
Filesystems []FilesystemCustomization `json:"filesystems,omitempty" toml:"filesystems,omitempty"`
}

type FilesystemCustomization struct {
Mountpoint string `json:"mountpoint,omitempty" toml:"mountpoint,omitempty"`
Mountpoint string `json:"mountpoint" toml:"mountpoint"`
MinSize uint64 `json:"minsize,omitempty" toml:"minsize,omitempty"`
Label string `json:"label,omitempty" toml:"label,omitempty"`
Type string `json:"type,omitempty" toml:"type,omitempty"`
}

type LVMCustomization struct {
VolumeGroups []VGCustomization `json:"volume_groups,omitempty" toml:"volume_groups,omitempty"`
}

type VGCustomization struct {
// Volume group name
Name string `json:"name" toml:"name"`
// Size of the partition that contains the volume group
MinSize uint64 `json:"minsize" toml:"minsize"`
LogicalVolumes []LVCustomization `json:"logical_volumes,omitempty" toml:"logical_volumes,omitempty"`
}

type LVCustomization struct {
// Logical volume name
Name string `json:"name,omitempty" toml:"name,omitempty"`
FilesystemCustomization
}

type BtrfsCustomization struct {
Volumes []BtrfsVolumeCustomization
}

type BtrfsVolumeCustomization struct {
// Size of the btrfs partition/volume.
MinSize uint64 `json:"minsize" toml:"minsize"`
ondrejbudai marked this conversation as resolved.
Show resolved Hide resolved
Subvolumes []BtrfsSubvolumeCustomization
}

type BtrfsSubvolumeCustomization struct {
Name string `json:"name" toml:"name"`
Mountpoint string `json:"mountpoint" toml:"mountpoint"`
}

func validateMountpoint(path string) error {
if path == "" {
return fmt.Errorf("mountpoint is empty")
}

if !strings.HasPrefix(path, "/") {
return fmt.Errorf("mountpoint %q is not an absolute path", path)
}

if cleanPath := filepath.Clean(path); path != cleanPath {
return fmt.Errorf("mountpoint %q is not a canonical path (did you mean %q?)", path, cleanPath)
}

return nil
}

func validateFilesystemType(path, fstype string) error {
// Check that the fs type is valid for the mountpoint.
// Empty strings are allowed for fstype to set the type automatically based
// on the distro defaults.
badfsMsg := "unsupported filesystem type for %q: %s"
switch path {
case "/boot":
switch fstype {
case "xfs", "ext4", "":
default:
return fmt.Errorf(badfsMsg, path, fstype)
}
case "/boot/efi":
switch fstype {
case "vfat", "":
default:
return fmt.Errorf(badfsMsg, path, fstype)
}
}
return nil
}

// Validate checks for customization combinations that are generally not
// supported or can create conflicts, regardless of specific distro or image
// type policies.
func (p *PartitioningCustomization) Validate() error {
if p == nil {
return nil
}

if p.Btrfs != nil && p.LVM != nil {
return fmt.Errorf("btrfs and lvm partitioning cannot be combined")
}

if p.Btrfs != nil && len(p.Btrfs.Volumes) > 1 {
return fmt.Errorf("multiple btrfs volumes are not yet supported")
}

if p.LVM != nil && len(p.LVM.VolumeGroups) > 1 {
return fmt.Errorf("multiple LVM volume groups are not yet supported")
}

// iterate through everything and look for:
// - invalid mountpoints (global)
// - duplicate mountpoints (global)
// - duplicate volume group and logical volume names (lvm)
// - duplicate subvolume names (btrfs)
// - empty subvolume names (btrfs)
// - invalid filesystem for mountpoint (e.g. /boot)
// - special mountpoints on lvm or btrfs
// - btrfs as filesystem on plain

plainOnlyMountpoints := []string{
"/boot",
"/boot/efi", // not allowed by our global policies, but that might change
}

mountpoints := make(map[string]bool)
if p.Plain != nil {
for _, fs := range p.Plain.Filesystems {
if err := validateMountpoint(fs.Mountpoint); err != nil {
return fmt.Errorf("invalid plain filesystem customization: %w", err)
}
if mountpoints[fs.Mountpoint] {
return fmt.Errorf("duplicate mountpoint %q in partitioning customizations", fs.Mountpoint)
}
if err := validateFilesystemType(fs.Mountpoint, fs.Type); err != nil {
return fmt.Errorf("invalid plain filesystem customization: %w", err)
}
if fs.Type == "btrfs" {
return fmt.Errorf("btrfs filesystem defined under plain partitioning customization: please use the \"btrfs\" customization to define btrfs volumes and subvolumes")
}

mountpoints[fs.Mountpoint] = true
}
}

if p.LVM != nil {
vgnames := make(map[string]bool)
for _, vg := range p.LVM.VolumeGroups { // there can be only one VG currently, but keep the check for when we change the rule
if vg.Name != "" && vgnames[vg.Name] { // VGs with no name get autogenerated names
return fmt.Errorf("duplicate volume group name %q in partitioning customizations", vg.Name)
}
vgnames[vg.Name] = true
lvnames := make(map[string]bool)
for _, lv := range vg.LogicalVolumes {
if lv.Name != "" && lvnames[lv.Name] { // LVs with no name get autogenerated names
return fmt.Errorf("duplicate lvm logical volume name %q in volume group %q in partitioning customizations", lv.Name, vg.Name)
}
lvnames[lv.Name] = true

if err := validateMountpoint(lv.Mountpoint); err != nil {
return fmt.Errorf("invalid logical volume customization: %w", err)
}
if mountpoints[lv.Mountpoint] {
return fmt.Errorf("duplicate mountpoint %q in partitioning customizations", lv.Mountpoint)
}
mountpoints[lv.Mountpoint] = true

if slices.Contains(plainOnlyMountpoints, lv.Mountpoint) {
return fmt.Errorf("invalid mountpoint %q for logical volume", lv.Mountpoint)
}
}

}
}

if p.Btrfs != nil {
for _, vol := range p.Btrfs.Volumes {
subvolnames := make(map[string]bool)
for _, subvol := range vol.Subvolumes {
if subvol.Name == "" {
return fmt.Errorf("btrfs subvolume with empty name in partitioning customizations")
}
if subvolnames[subvol.Name] {
return fmt.Errorf("duplicate btrfs subvolume name %q in partitioning customizations", subvol.Name)
}
subvolnames[subvol.Name] = true

if err := validateMountpoint(subvol.Mountpoint); err != nil {
return fmt.Errorf("invalid btrfs subvolume customization: %w", err)
}
if mountpoints[subvol.Mountpoint] {
return fmt.Errorf("duplicate mountpoint %q in partitioning customizations", subvol.Mountpoint)
}
if slices.Contains(plainOnlyMountpoints, subvol.Mountpoint) {
return fmt.Errorf("invalid mountpoint %q for btrfs subvolume", subvol.Mountpoint)
}
mountpoints[subvol.Mountpoint] = true
}
}
}

return nil
}

func (fsc *FilesystemCustomization) UnmarshalTOML(data interface{}) error {
Expand All @@ -24,6 +225,24 @@ func (fsc *FilesystemCustomization) UnmarshalTOML(data interface{}) error {
return fmt.Errorf("TOML unmarshal: mountpoint must be string, got %v of type %T", d["mountpoint"], d["mountpoint"])
}

switch d["type"].(type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we don't test this right now? It's fine if we don't, I would love to have a test for this and also one that checks that the json strings are what we expect but I don't want this PR to become bigger, I can offer to do it as a followup :)

Copy link
Member Author

@achilleas-k achilleas-k Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to existing tests.

case nil:
// empty allowed
case string:
fsc.Type = d["type"].(string)
default:
return fmt.Errorf("TOML unmarshal: type must be string, got %v of type %T", d["type"], d["type"])
}

switch d["label"].(type) {
case nil:
// empty allowed
case string:
fsc.Label = d["label"].(string)
default:
return fmt.Errorf("TOML unmarshal: label must be string, got %v of type %T", d["label"], d["label"])
}

switch d["minsize"].(type) {
case int64:
fsc.MinSize = uint64(d["minsize"].(int64))
Expand Down Expand Up @@ -54,6 +273,24 @@ func (fsc *FilesystemCustomization) UnmarshalJSON(data []byte) error {
return fmt.Errorf("JSON unmarshal: mountpoint must be string, got %v of type %T", d["mountpoint"], d["mountpoint"])
}

switch d["type"].(type) {
case nil:
// empty allowed
case string:
fsc.Type = d["type"].(string)
default:
return fmt.Errorf("JSON unmarshal: type must be string, got %v of type %T", d["type"], d["type"])
}

switch d["label"].(type) {
case nil:
// empty allowed
case string:
fsc.Label = d["label"].(string)
default:
return fmt.Errorf("JSON unmarshal: label must be string, got %v of type %T", d["label"], d["label"])
}

// The JSON specification only mentions float64 and Go defaults to it: https://go.dev/blog/json
switch d["minsize"].(type) {
case float64:
Expand Down Expand Up @@ -86,3 +323,45 @@ func CheckMountpointsPolicy(mountpoints []FilesystemCustomization, mountpointAll

return nil
}

// CheckMountpointsPolicy checks if the mountpoints are allowed by the policy
func CheckPartitioningPolicy(partitioning *PartitioningCustomization, mountpointAllowList *pathpolicy.PathPolicies) error {
if partitioning == nil {
return nil
}

// collect all mountpoints
mountpoints := make([]string, 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(super nitpick) this could be just var mountpoints []string but probably just personal preference :)

if partitioning.Plain != nil {
for _, part := range partitioning.Plain.Filesystems {
mountpoints = append(mountpoints, part.Mountpoint)
}
}
if partitioning.LVM != nil {
for _, vg := range partitioning.LVM.VolumeGroups {
for _, lv := range vg.LogicalVolumes {
mountpoints = append(mountpoints, lv.Mountpoint)
}
}
}
if partitioning.Btrfs != nil {
for _, vol := range partitioning.Btrfs.Volumes {
for _, subvol := range vol.Subvolumes {
mountpoints = append(mountpoints, subvol.Mountpoint)
}
}
}

var errs []error
for _, mp := range mountpoints {
if err := mountpointAllowList.Check(mp); err != nil {
errs = append(errs, err)
}
}

if len(errs) > 0 {
return fmt.Errorf("The following errors occurred while setting up custom mountpoints:\n%w", errors.Join(errs...))
}

return nil
}
Loading
Loading