diff --git a/internal/osbuild2/lvm2_create_stage.go b/internal/osbuild2/lvm2_create_stage.go new file mode 100644 index 000000000..f328c3349 --- /dev/null +++ b/internal/osbuild2/lvm2_create_stage.go @@ -0,0 +1,51 @@ +package osbuild2 + +import ( + "fmt" + "regexp" +) + +const lvmVolNameRegex = "^[a-zA-Z0-9+_.][a-zA-Z0-9+_.-]*$" + +// Create LVM2 physical volumes, volume groups, and logical volumes + +type LVM2CreateStageOptions struct { + Volumes []LogicalVolume `json:"volumes"` +} + +func (LVM2CreateStageOptions) isStageOptions() {} + +func (o LVM2CreateStageOptions) validate() error { + if len(o.Volumes) == 0 { + return fmt.Errorf("at least one volume is required") + } + + nameRegex := regexp.MustCompile(lvmVolNameRegex) + for _, volume := range o.Volumes { + fmt.Printf("testing volume %q\n", volume.Name) + if !nameRegex.MatchString(volume.Name) { + return fmt.Errorf("volume name %q doesn't conform to schema (%s)", volume.Name, nameRegex.String()) + } else { + fmt.Printf("volume.Name %q is ok\n", volume.Name) + } + } + return nil +} + +type LogicalVolume struct { + Name string `json:"name"` + + Size string `json:"size"` +} + +func NewLVM2CreateStage(options *LVM2CreateStageOptions, device *Device) *Stage { + if err := options.validate(); err != nil { + panic(err) + } + + return &Stage{ + Type: "org.osbuild.lvm2.create", + Options: options, + Devices: Devices{"device": *device}, + } +} diff --git a/internal/osbuild2/lvm2_create_stage_test.go b/internal/osbuild2/lvm2_create_stage_test.go new file mode 100644 index 000000000..0ede0f3c9 --- /dev/null +++ b/internal/osbuild2/lvm2_create_stage_test.go @@ -0,0 +1,60 @@ +package osbuild2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewLVM2CreateStageValidation(t *testing.T) { + assert := assert.New(t) + + okOptions := LVM2CreateStageOptions{ + Volumes: []LogicalVolume{ + { + Name: "a_volume_name", + Size: "", + }, + { + Name: "good-volume.name", + Size: "10G", + }, + { + Name: "99-luft+volumes", + Size: "10737418240", + }, + { + Name: "++", + Size: "1337", + }, + { + Name: "_", + Size: "0", + }, + }, + } + assert.NoError(okOptions.validate()) + + badVolumes := []LogicalVolume{ + { + Name: "!bad-bad-volume-name", + Size: "1337", + }, + { + Name: "even worse", + }, + { + Name: "-", + }, + } + + for _, vol := range badVolumes { + options := LVM2CreateStageOptions{ + Volumes: []LogicalVolume{vol}, + } + assert.Error(options.validate(), vol.Name) + } + + empty := LVM2CreateStageOptions{} + assert.Error(empty.validate()) +} diff --git a/internal/osbuild2/lvm2_lv_device.go b/internal/osbuild2/lvm2_lv_device.go new file mode 100644 index 000000000..ebddd17bc --- /dev/null +++ b/internal/osbuild2/lvm2_lv_device.go @@ -0,0 +1,17 @@ +package osbuild2 + +// Provide access to LVM2 Logical Volume (LV) + +type LVM2LVDeviceOptions struct { + // Logical volume to activate + Volume string `json:"volume"` +} + +func (LVM2LVDeviceOptions) isDeviceOptions() {} + +func NewLVM2LVDevice(options *LoopbackDeviceOptions) *Device { + return &Device{ + Type: "org.osbuild.lvm2.lv", + Options: options, + } +} diff --git a/internal/osbuild2/lvm2_lv_device_test.go b/internal/osbuild2/lvm2_lv_device_test.go new file mode 100644 index 000000000..d0a454639 --- /dev/null +++ b/internal/osbuild2/lvm2_lv_device_test.go @@ -0,0 +1 @@ +package osbuild2 diff --git a/internal/osbuild2/lvm2_metadata_stage.go b/internal/osbuild2/lvm2_metadata_stage.go new file mode 100644 index 000000000..b43fd8f9a --- /dev/null +++ b/internal/osbuild2/lvm2_metadata_stage.go @@ -0,0 +1,46 @@ +package osbuild2 + +import ( + "fmt" + "regexp" + "strconv" +) + +// Set LVM2 Volume Group metadata + +type LVM2MetadataStageOptions struct { + CreationHost string `json:"creation_host,omitempty"` + + // Creation time (uint64 represented as string) + CreationTime string `json:"creation_time,omitempty"` + + Description string `json:"description,omitempty"` + + VGName string `json:"vg_name"` +} + +func (LVM2MetadataStageOptions) isStageOptions() {} + +func (o LVM2MetadataStageOptions) validate() error { + nameRegex := regexp.MustCompile(lvmVolNameRegex) + if !nameRegex.MatchString(o.VGName) { + return fmt.Errorf("volume group name %q doesn't conform to schema (%s)", o.VGName, nameRegex.String()) + } + + if _, err := strconv.ParseUint(o.CreationTime, 10, 64); err != nil { + return fmt.Errorf("invalid volume creation time: %s", o.CreationTime) + } + return nil +} + +func NewLVM2MetadataStage(options *LVM2MetadataStageOptions, device *Device) *Stage { + if err := options.validate(); err != nil { + panic(err) + } + + return &Stage{ + Type: "org.osbuild.lvm2.metadata", + Options: options, + Devices: Devices{"device": *device}, + } +} diff --git a/internal/osbuild2/lvm2_metadata_stage_test.go b/internal/osbuild2/lvm2_metadata_stage_test.go new file mode 100644 index 000000000..f83e55699 --- /dev/null +++ b/internal/osbuild2/lvm2_metadata_stage_test.go @@ -0,0 +1,55 @@ +package osbuild2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewLVM2MetadataStageValidation(t *testing.T) { + assert := assert.New(t) + + okOptions := []LVM2MetadataStageOptions{ + { + VGName: "a_volume_name", + CreationTime: "0", + }, + { + VGName: "good-volume.name", + CreationTime: "1629282647", + }, + { + VGName: "99-luft+volumes", + CreationTime: "2147483648", + }, + { + VGName: "++", + CreationTime: "42", + }, + { + VGName: "_", + CreationTime: "4294967297", + }, + } + for _, o := range okOptions { + assert.NoError(o.validate(), o) + } + + badOptions := []LVM2MetadataStageOptions{ + { + VGName: "ok-name-bad-time", + CreationTime: "-10", + }, + { + VGName: "!bad-name", + CreationTime: "1629282647", + }, + { + VGName: "worse.time", + CreationTime: "TIME", + }, + } + for _, o := range badOptions { + assert.Error(o.validate(), o) + } +}