diff --git a/actions/rootio/v1/Dockerfile b/actions/rootio/v1/Dockerfile index 7f34ed4..1515ca1 100644 --- a/actions/rootio/v1/Dockerfile +++ b/actions/rootio/v1/Dockerfile @@ -34,11 +34,16 @@ WORKDIR /dosfstools/ RUN ./autogen.sh; ./configure RUN make LDFLAGS="--static" +# build lvm2 as static +FROM alpine as lvm +RUN apk update && apk add lvm2-static=2.03.21-r3 + # Build final image FROM scratch COPY --from=mke2fs /e2fsprogs-1.45.6/misc/mke2fs.static /sbin/mke2fs COPY --from=swap util-linux/swapon /sbin/swapon COPY --from=swap util-linux/mkswap /sbin/mkswap COPY --from=fattools dosfstools/src/mkfs.fat /sbin/mkfs.fat +COPY --from=lvm /usr/sbin/lvm.static /sbin/lvm COPY --from=rootio /go/src/github.com/thebsdbox/rootio/rootio . ENTRYPOINT ["/rootio"] diff --git a/actions/rootio/v1/cmd/rootio.go b/actions/rootio/v1/cmd/rootio.go index 6252911..4835a88 100644 --- a/actions/rootio/v1/cmd/rootio.go +++ b/actions/rootio/v1/cmd/rootio.go @@ -114,6 +114,16 @@ var rootioPartition = &cobra.Command{ log.Error(err) } } + + if len(metadata.Instance.Storage.VolumeGroups) > 0 { + log.Infoln("Creating Volume Groups") + } + + for _, vg := range metadata.Instance.Storage.VolumeGroups { + if err := storage.CreateVolumeGroup(vg); err != nil { + log.Error(err) + } + } }, } diff --git a/actions/rootio/v1/pkg/lvm/lvm.go b/actions/rootio/v1/pkg/lvm/lvm.go new file mode 100644 index 0000000..2dc236a --- /dev/null +++ b/actions/rootio/v1/pkg/lvm/lvm.go @@ -0,0 +1,189 @@ +package lvm + +import ( + "fmt" + "os" + "os/exec" + "regexp" + "strings" + + log "github.com/sirupsen/logrus" +) + +var lvNameRegexp = regexp.MustCompile("^[A-Za-z0-9_+.][A-Za-z0-9_+.-]*$") +var vgNameRegexp = regexp.MustCompile("^[A-Za-z0-9_+.][A-Za-z0-9_+.-]*$") +var tagRegexp = regexp.MustCompile("^[A-Za-z0-9_+.][A-Za-z0-9_+.-]*$") + +type VolumeGroup struct { + name string +} + +// CreatePhysicalVolume creates a physical volume of the given device. +func CreatePhysicalVolume(dev string) error { + if err := run("lvm", "pvcreate", dev); err != nil { + return fmt.Errorf("lvm: CreatePhysicalVolume: %v", err) + } + return nil +} + +// PVScan runs the `pvscan --cache ` command. It scans for the +// device at `dev` and adds it to the LVM metadata cache if `lvmetad` +// is running. If `dev` is an empty string, it scans all devices. +func PVScan(dev string) error { + args := []string{"pvscan", "--cache"} + if dev != "" { + args = append(args, dev) + } + return run("lvm", args...) +} + +// VGScan runs the `vgscan --cache ` command. It scans for the +// volume group and adds it to the LVM metadata cache if `lvmetad` +// is running. If `name` is an empty string, it scans all volume groups. +func VGScan(name string) error { + args := []string{"vgscan", "--cache"} + if name != "" { + args = append(args, name) + } + return run("lvm", args...) +} + +// ValidateVolumeGroupName validates a volume group name. A valid volume group +// name can consist of a limited range of characters only. The allowed +// characters are [A-Za-z0-9_+.-]. +func ValidateVolumeGroupName(name string) error { + if !vgNameRegexp.MatchString(name) { + return fmt.Errorf("lvm: Volume group name %q contains invalid character, valid set includes: [A-Za-z0-9_+.-]", name) + } + return nil +} + +// ValidateTag validates a tag. LVM tags are strings of up to 1024 +// characters. LVM tags cannot start with a hyphen. A valid tag can consist of +// a limited range of characters only. The allowed characters are +// [A-Za-z0-9_+.-]. As of the Red Hat Enterprise Linux 6.1 release, the list of +// allowed characters was extended, and tags can contain the /, =, !, :, #, and +// & characters. +// See https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/logical_volume_manager_administration/lvm_tags +func ValidateTag(tag string) error { + if len(tag) > 1024 { + return fmt.Errorf("lvm: Tag %q is too long, maximum length is 1024 characters", tag) + } + if !tagRegexp.MatchString(tag) { + return fmt.Errorf("lvm: Tag %q contains invalid character, valid set includes: [A-Za-z0-9_+.-]", tag) + } + return nil +} + +// CreateVolumeGroup creates a new volume group. +func CreateVolumeGroup(name string, pvs []string, tags []string) (*VolumeGroup, error) { + args := []string{"vgcreate"} + + if err := ValidateVolumeGroupName(name); err != nil { + return nil, err + } + + for _, tag := range tags { + if tag != "" { + if err := ValidateTag(tag); err != nil { + return nil, err + } + args = append(args, "--add-tag="+tag) + } + } + + args = append(args, name) + for _, pv := range pvs { + args = append(args, pv) + } + + if err := run("lvm", args...); err != nil { + return nil, err + } + + if err := PVScan(""); err != nil { + log.Warnf("error during pvscan: %s", err.Error()) + } + + if err := VGScan(""); err != nil { + log.Warnf("error during vgscan: %s", err.Error()) + } + return &VolumeGroup{name}, nil +} + +// ValidateLogicalVolumeName validates a volume group name. A valid volume +// group name can consist of a limited range of characters only. The allowed +// characters are [A-Za-z0-9_+.-]. +func ValidateLogicalVolumeName(name string) error { + if !lvNameRegexp.MatchString(name) { + return fmt.Errorf("lvm: Logical volume name %q contains invalid character, valid set includes: [A-Za-z0-9_+.-]", name) + } + + return nil +} + +// CreateLogicalVolume creates a logical volume of the given device +// and size. +// +// The actual size may be larger than asked for as the smallest +// increment is the size of an extent on the volume group in question. +// +// If sizeInBytes is zero the entire available space is allocated. +// +// Additional optional config items can be specified using CreateLogicalVolumeOpt +func (vg *VolumeGroup) CreateLogicalVolume(name string, sizeInBytes uint64, tags []string, opts []string) error { + if err := ValidateLogicalVolumeName(name); err != nil { + return err + } + + // Validate the tag. + args := []string{"lvcreate"} + for _, tag := range tags { + if tag != "" { + if err := ValidateTag(tag); err != nil { + return err + } + args = append(args, "--add-tag="+tag) + } + } + + if sizeInBytes == 0 { + args = append(args, "-l", "100%FREE") + } else { + args = append(args, fmt.Sprintf("--size=%db", sizeInBytes)) + } + + args = append(args, "--name="+name) + args = append(args, vg.name) + args = append(args, opts...) + + if err := run("lvm", args...); err != nil { + if isInsufficientSpace(err) { + return fmt.Errorf("lvm: not enough free space") + } + if isInsufficientDevices(err) { + return fmt.Errorf("lvm: not enough underlying devices") + } + return err + } + return nil +} + +func run(cmd string, extraArgs ...string) error { + var args []string + args = append(args, extraArgs...) + c := exec.Command(cmd, args...) + c.Stdout, c.Stderr = os.Stdout, os.Stderr + + return c.Run() +} + +// isInsufficientSpace returns true if the error is due to insufficient space +func isInsufficientSpace(err error) bool { + return strings.Contains(strings.ToLower(err.Error()), "insufficient free space") +} + +// isInsufficientDevices returns true if the error is due to insufficient underlying devices +func isInsufficientDevices(err error) bool { + return strings.Contains(err.Error(), "Insufficient suitable allocatable extents for logical volume") +} diff --git a/actions/rootio/v1/pkg/storage/lvm.go b/actions/rootio/v1/pkg/storage/lvm.go new file mode 100644 index 0000000..78e9a74 --- /dev/null +++ b/actions/rootio/v1/pkg/storage/lvm.go @@ -0,0 +1,29 @@ +package storage + +import ( + "fmt" + + "github.com/tinkerbell/hub/actions/rootio/v1/pkg/lvm" + "github.com/tinkerbell/hub/actions/rootio/v1/pkg/types.go" +) + +func CreateVolumeGroup(volumeGroup types.VolumeGroup) error { + for _, p := range volumeGroup.PhysicalVolumes { + if err := lvm.CreatePhysicalVolume(p); err != nil { + return fmt.Errorf("failed to create physical volume %s: %v", p, err) + } + } + + vg, err := lvm.CreateVolumeGroup(volumeGroup.Name, volumeGroup.PhysicalVolumes, volumeGroup.Tags) + if err != nil { + return fmt.Errorf("failed to create volume group %s: %v", volumeGroup.Name, err) + } + + for _, lv := range volumeGroup.LogicalVolumes { + if err := vg.CreateLogicalVolume(lv.Name, lv.Size, lv.Tags, lv.Opts); err != nil { + return fmt.Errorf("failed to create logical volume %s: %v", lv.Name, err) + } + } + + return nil +} diff --git a/actions/rootio/v1/pkg/storage/partition.go b/actions/rootio/v1/pkg/storage/partition.go index 85a931b..340daf2 100644 --- a/actions/rootio/v1/pkg/storage/partition.go +++ b/actions/rootio/v1/pkg/storage/partition.go @@ -90,7 +90,7 @@ func Partition(d types.Disk) error { End: sectorEnd, } - sectorStart += sectorEnd + sectorStart = sectorEnd + 1 switch d.Partitions[x].Label { case "SWAP": diff --git a/actions/rootio/v1/pkg/types.go/metadata.go b/actions/rootio/v1/pkg/types.go/metadata.go index 1f6d37f..f46e467 100644 --- a/actions/rootio/v1/pkg/types.go/metadata.go +++ b/actions/rootio/v1/pkg/types.go/metadata.go @@ -32,8 +32,9 @@ type Instance struct { Version string `json:"version"` } `json:"operating_system_version"` Storage struct { - Disks []Disk `json:"disks"` - Filesystems []Filesystem `json:"filesystems"` + Disks []Disk `json:"disks"` + Filesystems []Filesystem `json:"filesystems"` + VolumeGroups []VolumeGroup `json:"volume_groups"` } `json:"storage"` } @@ -63,6 +64,22 @@ type Partitions struct { Size uint64 `json:"size"` } +// VolumeGroup defines the configuration of a volume group +type VolumeGroup struct { + Name string `json:"name"` + PhysicalVolumes []string `json:"physical_volumes"` + LogicalVolumes []LogicalVolume `json:"logical_volumes"` + Tags []string `json:"tags"` +} + +// LogicalVolume defines the configuration of a logical volume. +type LogicalVolume struct { + Name string `json:"name"` + Size uint64 `json:"size"` + Tags []string `json:"tags"` + Opts []string `json:"opts"` +} + // RetrieveData retrieves metadata from Hegel. func RetrieveData() (*Metadata, error) { metadataURL := os.Getenv("MIRROR_HOST")