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

add the ability to create lvm in the partition command #101

Merged
merged 3 commits into from Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions actions/rootio/v1/Dockerfile
Expand Up @@ -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"]
10 changes: 10 additions & 0 deletions actions/rootio/v1/cmd/rootio.go
Expand Up @@ -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)
}
}
},
}

Expand Down
189 changes: 189 additions & 0 deletions 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 <dev>` 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 <name>` 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")
}
29 changes: 29 additions & 0 deletions 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
}
2 changes: 1 addition & 1 deletion actions/rootio/v1/pkg/storage/partition.go
Expand Up @@ -90,7 +90,7 @@ func Partition(d types.Disk) error {
End: sectorEnd,
}

sectorStart += sectorEnd
sectorStart = sectorEnd + 1
walkerus marked this conversation as resolved.
Show resolved Hide resolved

switch d.Partitions[x].Label {
case "SWAP":
Expand Down
21 changes: 19 additions & 2 deletions actions/rootio/v1/pkg/types.go/metadata.go
Expand Up @@ -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"`
}

Expand Down Expand Up @@ -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")
Expand Down