diff --git a/cmd/ebs-bootstrap.go b/cmd/ebs-bootstrap.go index 2270e26..e97cb0e 100644 --- a/cmd/ebs-bootstrap.go +++ b/cmd/ebs-bootstrap.go @@ -62,6 +62,7 @@ func main() { // LVM Layers lvmLayers := []layer.Layer{ layer.NewCreatePhysicalVolumeLayer(db, lb), + layer.NewResizePhysicalVolumeLayer(lb), layer.NewCreateVolumeGroupLayer(lb), layer.NewCreateLogicalVolumeLayer(lb), layer.NewActivateLogicalVolumeLayer(lb), diff --git a/configs/ubuntu.yml b/configs/ubuntu.yml index 2ab6075..3ef9896 100644 --- a/configs/ubuntu.yml +++ b/configs/ubuntu.yml @@ -1,5 +1,7 @@ defaults: lvmConsumption: 100 + resizeFs: true + resizeThreshold: 99 devices: /dev/vdb: fs: xfs diff --git a/internal/action/lvm.go b/internal/action/lvm.go index 5be10a6..58c20d0 100644 --- a/internal/action/lvm.go +++ b/internal/action/lvm.go @@ -170,3 +170,42 @@ func (a *ActivateLogicalVolumeAction) Refuse() string { func (a *ActivateLogicalVolumeAction) Success() string { return fmt.Sprintf("Successfully activated logical volume %s in volume group %s", a.name, a.volumeGroup) } + +type ResizePhysicalVolumeAction struct { + name string + mode model.Mode + lvmService service.LvmService +} + +func NewResizePhysicalVolumeAction(name string, ls service.LvmService) *ResizePhysicalVolumeAction { + return &ResizePhysicalVolumeAction{ + name: name, + mode: model.Empty, + lvmService: ls, + } +} + +func (a *ResizePhysicalVolumeAction) Execute() error { + return a.lvmService.ResizePhysicalVolume(a.name) +} + +func (a *ResizePhysicalVolumeAction) GetMode() model.Mode { + return a.mode +} + +func (a *ResizePhysicalVolumeAction) SetMode(mode model.Mode) Action { + a.mode = mode + return a +} + +func (a *ResizePhysicalVolumeAction) Prompt() string { + return fmt.Sprintf("Would you like to resize physical volume %s", a.name) +} + +func (a *ResizePhysicalVolumeAction) Refuse() string { + return fmt.Sprintf("Refused to resize physical volume %s", a.name) +} + +func (a *ResizePhysicalVolumeAction) Success() string { + return fmt.Sprintf("Successfully resized physical volume %s", a.name) +} diff --git a/internal/backend/lvm.go b/internal/backend/lvm.go index bec98b1..73eb292 100644 --- a/internal/backend/lvm.go +++ b/internal/backend/lvm.go @@ -19,6 +19,8 @@ type LvmBackend interface { GetLogicalVolume(name string, volumeGroup string) (*model.LogicalVolume, error) SearchLogicalVolumes(volumeGroup string) []*model.LogicalVolume SearchVolumeGroup(physicalVolume string) *model.VolumeGroup + ShouldResizePhysicalVolume(name string, threshold float64) bool + ResizePhysicalVolume(name string) action.Action From(config *config.Config) error } @@ -78,6 +80,7 @@ func (lb *LinuxLvmBackend) SearchLogicalVolumes(volumeGroup string) []*model.Log Name: lv.Name, VolumeGroup: node.Name, State: model.LogicalVolumeState(lv.State), + Size: lv.Size, }) } return lvs @@ -95,6 +98,7 @@ func (lb *LinuxLvmBackend) SearchVolumeGroup(physicalVolume string) *model.Volum return &model.VolumeGroup{ Name: vgn[0].Name, PhysicalVolume: node.Name, + Size: vgn[0].Size, } } @@ -114,28 +118,50 @@ func (lb *LinuxLvmBackend) ActivateLogicalVolume(name string, volumeGroup string return action.NewActivateLogicalVolumeAction(name, volumeGroup, lb.lvmService) } +func (lb *LinuxLvmBackend) ShouldResizePhysicalVolume(name string, threshold float64) bool { + node, err := lb.lvmGraph.GetPhysicalVolume(name) + if err != nil { + return false + } + dvn := lb.lvmGraph.GetParents(node, datastructures.Device) + if len(dvn) == 0 { + return false + } + return (float64(node.Size) / float64(dvn[0].Size) * 100) < threshold +} + +func (lb *LinuxLvmBackend) ResizePhysicalVolume(name string) action.Action { + return action.NewResizePhysicalVolumeAction(name, lb.lvmService) +} + func (db *LinuxLvmBackend) From(config *config.Config) error { + ds, err := db.lvmService.GetDevices() + if err != nil { + return err + } + for _, d := range ds { + db.lvmGraph.AddDevice(d.Name, d.Size) + } pvs, err := db.lvmService.GetPhysicalVolumes() if err != nil { return err } for _, pv := range pvs { - db.lvmGraph.AddBlockDevice(pv.Name) - db.lvmGraph.AddPhysicalVolume(pv.Name) + db.lvmGraph.AddPhysicalVolume(pv.Name, pv.Size) } vgs, err := db.lvmService.GetVolumeGroups() if err != nil { return err } for _, vg := range vgs { - db.lvmGraph.AddVolumeGroup(vg.Name, vg.PhysicalVolume) + db.lvmGraph.AddVolumeGroup(vg.Name, vg.PhysicalVolume, vg.Size) } lvs, err := db.lvmService.GetLogicalVolumes() if err != nil { return err } for _, lv := range lvs { - db.lvmGraph.AddLogicalVolume(lv.Name, lv.VolumeGroup, datastructures.LvmNodeState(lv.State)) + db.lvmGraph.AddLogicalVolume(lv.Name, lv.VolumeGroup, datastructures.LvmNodeState(lv.State), lv.Size) } return nil } diff --git a/internal/datastructures/lvm_graph.go b/internal/datastructures/lvm_graph.go index 29bbd89..cbf8c9c 100644 --- a/internal/datastructures/lvm_graph.go +++ b/internal/datastructures/lvm_graph.go @@ -8,7 +8,7 @@ type LvmNodeState int32 type LvmNodeCategory int32 const ( - BlockDeviceActive LvmNodeState = 0b0000001 + DeviceActive LvmNodeState = 0b0000001 PhysicalVolumeActive LvmNodeState = 0b0000010 VolumeGroupInactive LvmNodeState = 0b0000100 VolumeGroupActive LvmNodeState = 0b0001100 @@ -18,7 +18,7 @@ const ( ) const ( - BlockDevice LvmNodeCategory = 0b0000001 + Device LvmNodeCategory = 0b0000001 PhysicalVolume LvmNodeCategory = 0b0000010 VolumeGroup LvmNodeCategory = 0b0000100 LogicalVolume LvmNodeCategory = 0b0010000 @@ -28,45 +28,50 @@ type LvmNode struct { id string Name string State LvmNodeState + Size uint64 children []*LvmNode parents []*LvmNode } -func NewBlockDevice(name string) *LvmNode { +func NewDevice(name string, size uint64) *LvmNode { return &LvmNode{ id: fmt.Sprintf("device:%s", name), Name: name, - State: BlockDeviceActive, + State: DeviceActive, + Size: size, children: []*LvmNode{}, parents: []*LvmNode{}, } } -func NewPhysicalVolume(name string) *LvmNode { +func NewPhysicalVolume(name string, size uint64) *LvmNode { return &LvmNode{ id: fmt.Sprintf("pv:%s", name), Name: name, State: PhysicalVolumeActive, + Size: size, children: []*LvmNode{}, parents: []*LvmNode{}, } } -func NewVolumeGroup(name string) *LvmNode { +func NewVolumeGroup(name string, size uint64) *LvmNode { return &LvmNode{ id: fmt.Sprintf("vg:%s", name), Name: name, State: VolumeGroupInactive, + Size: size, children: []*LvmNode{}, parents: []*LvmNode{}, } } -func NewLogicalVolume(name string, vg string, State LvmNodeState) *LvmNode { +func NewLogicalVolume(name string, vg string, State LvmNodeState, size uint64) *LvmNode { return &LvmNode{ id: fmt.Sprintf("lv:%s:vg:%s", name, vg), Name: name, State: State, + Size: size, children: []*LvmNode{}, parents: []*LvmNode{}, } @@ -82,8 +87,8 @@ func NewLvmGraph() *LvmGraph { } } -func (lg *LvmGraph) AddBlockDevice(name string) error { - bd := NewBlockDevice(name) +func (lg *LvmGraph) AddDevice(name string, size uint64) error { + bd := NewDevice(name, size) _, found := lg.nodes[bd.id] if found { @@ -94,8 +99,8 @@ func (lg *LvmGraph) AddBlockDevice(name string) error { return nil } -func (lg *LvmGraph) AddPhysicalVolume(name string) error { - pv := NewPhysicalVolume(name) +func (lg *LvmGraph) AddPhysicalVolume(name string, size uint64) error { + pv := NewPhysicalVolume(name, size) _, found := lg.nodes[pv.id] if found { @@ -114,12 +119,12 @@ func (lg *LvmGraph) AddPhysicalVolume(name string) error { return nil } -func (lg *LvmGraph) AddVolumeGroup(name string, pv string) error { +func (lg *LvmGraph) AddVolumeGroup(name string, pv string, size uint64) error { id := fmt.Sprintf("vg:%s", name) vg, found := lg.nodes[id] if !found { - vg = NewVolumeGroup(name) + vg = NewVolumeGroup(name, size) } pvId := fmt.Sprintf("pv:%s", pv) @@ -138,8 +143,8 @@ func (lg *LvmGraph) AddVolumeGroup(name string, pv string) error { return nil } -func (lg *LvmGraph) AddLogicalVolume(name string, vg string, state LvmNodeState) error { - lv := NewLogicalVolume(name, vg, state) +func (lg *LvmGraph) AddLogicalVolume(name string, vg string, state LvmNodeState, size uint64) error { + lv := NewLogicalVolume(name, vg, state, size) _, found := lg.nodes[lv.id] if found { diff --git a/internal/layer/pv_resize.go b/internal/layer/pv_resize.go new file mode 100644 index 0000000..4c9b8e9 --- /dev/null +++ b/internal/layer/pv_resize.go @@ -0,0 +1,90 @@ +package layer + +import ( + "fmt" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +const ( + // The % threshold at which to resize a physical volume + // ------------------------------------------------------- + // If the (physical volume / device size) * 100 falls + // under this threshold then we perform a resize operation + // ------------------------------------------------------- + // The smallest gp3 EBS volume you can create is 1GiB (1073741824 bytes). + // The default size of the extent of a PV is 4 MiB (4194304 bytes). + // Typically, the first extent of a PV is reserved for metadata. This + // produces a PV of size 1069547520 bytes (Usage=99.6093%). We ensure + // that we set the resize threshold to 99.6% to ensure that a 1 GiB EBS + // volume won't be always resized + // ------------------------------------------------------- + // Why not just look for a difference of 4194304 bytes? + // - The size of the extent can be changed by the user + // - Therefore we may not always see a difference of 4194304 bytes between + // the block device and physical volume size + ResizeThreshold = float64(99.6) +) + +type ResizePhysicalVolumeLayer struct { + lvmBackend backend.LvmBackend +} + +func NewResizePhysicalVolumeLayer(lb backend.LvmBackend) *ResizePhysicalVolumeLayer { + return &ResizePhysicalVolumeLayer{ + lvmBackend: lb, + } +} + +func (rpvl *ResizePhysicalVolumeLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + if len(cd.Lvm) == 0 { + continue + } + if !c.GetResizeFs(name) { + continue + } + if !rpvl.lvmBackend.ShouldResizePhysicalVolume(name, ResizeThreshold) { + continue + } + mode := c.GetMode(name) + a := rpvl.lvmBackend.ResizePhysicalVolume(name) + actions = append(actions, a.SetMode(mode)) + } + return actions, nil +} + +func (rpvl *ResizePhysicalVolumeLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + if len(cd.Lvm) == 0 { + continue + } + if !c.GetResizeFs(name) { + continue + } + if rpvl.lvmBackend.ShouldResizePhysicalVolume(name, ResizeThreshold) { + return fmt.Errorf("🔴 %s: Failed to resize validation checks. Physical volume %s still needs to be resized", name, name) + } + } + return nil +} + +func (rpvl *ResizePhysicalVolumeLayer) Warning() string { + return DisabledWarning +} + +func (rpvl *ResizePhysicalVolumeLayer) From(c *config.Config) error { + return rpvl.lvmBackend.From(c) +} + +func (rpvl *ResizePhysicalVolumeLayer) ShouldProcess(c *config.Config) bool { + for name, cd := range c.Devices { + if len(cd.Lvm) > 0 && c.GetResizeFs(name) { + return true + } + } + return false +} diff --git a/internal/model/lvm.go b/internal/model/lvm.go index 1feb5de..90cea52 100644 --- a/internal/model/lvm.go +++ b/internal/model/lvm.go @@ -2,13 +2,20 @@ package model import "github.com/reecetech/ebs-bootstrap/internal/datastructures" +type Device struct { + Name string + Size uint64 +} + type PhysicalVolume struct { Name string + Size uint64 } type VolumeGroup struct { Name string PhysicalVolume string + Size uint64 } type LogicalVolumeState int32 @@ -23,4 +30,5 @@ type LogicalVolume struct { Name string VolumeGroup string State LogicalVolumeState + Size uint64 } diff --git a/internal/service/lvm.go b/internal/service/lvm.go index f92540b..ffcb362 100644 --- a/internal/service/lvm.go +++ b/internal/service/lvm.go @@ -3,12 +3,14 @@ package service import ( "encoding/json" "fmt" + "strconv" "github.com/reecetech/ebs-bootstrap/internal/model" "github.com/reecetech/ebs-bootstrap/internal/utils" ) type LvmService interface { + GetDevices() ([]*model.Device, error) GetPhysicalVolumes() ([]*model.PhysicalVolume, error) GetVolumeGroups() ([]*model.VolumeGroup, error) GetLogicalVolumes() ([]*model.LogicalVolume, error) @@ -16,6 +18,7 @@ type LvmService interface { CreateVolumeGroup(name string, physicalVolume string) error CreateLogicalVolume(name string, volumeGroup string, freeSpacePercent int) error ActivateLogicalVolume(name string, volumeGroup string) error + ResizePhysicalVolume(name string) error } type LinuxLvmService struct { @@ -25,7 +28,9 @@ type LinuxLvmService struct { type PvsResponse struct { Report []struct { PhysicalVolume []struct { - Name string `json:"pv_name"` + Name string `json:"pv_name"` + PhysicalVolumeSize string `json:"pv_size"` + DeviceSize string `json:"dev_size"` } `json:"pv"` } `json:"report"` } @@ -35,6 +40,7 @@ type VgsResponse struct { VolumeGroup []struct { Name string `json:"vg_name"` PhysicalVolume string `json:"pv_name"` + Size string `json:"vg_size"` } `json:"vg"` } `json:"report"` } @@ -45,6 +51,7 @@ type LvsResponse struct { Name string `json:"lv_name"` VolumeGroup string `json:"vg_name"` Attributes string `json:"lv_attr"` + Size string `json:"lv_size"` } `json:"lv"` } `json:"report"` } @@ -55,9 +62,34 @@ func NewLinuxLvmService(rf utils.RunnerFactory) *LinuxLvmService { } } +func (ls *LinuxLvmService) GetDevices() ([]*model.Device, error) { + r := ls.runnerFactory.Select(utils.Pvs) + output, err := r.Command("-o", "pv_name,dev_size", "--reportformat", "json", "--units", "b", "--nosuffix") + if err != nil { + return nil, err + } + pr := &PvsResponse{} + err = json.Unmarshal([]byte(output), pr) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to decode pvs response: %v", err) + } + pvs := make([]*model.Device, len(pr.Report[0].PhysicalVolume)) + for i, pv := range pr.Report[0].PhysicalVolume { + size, err := strconv.ParseUint(pv.DeviceSize, 10, 64) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to cast device size to unsigned 64-bit integer") + } + pvs[i] = &model.Device{ + Name: pv.Name, + Size: size, + } + } + return pvs, nil +} + func (ls *LinuxLvmService) GetPhysicalVolumes() ([]*model.PhysicalVolume, error) { r := ls.runnerFactory.Select(utils.Pvs) - output, err := r.Command("-o", "pv_name", "--reportformat", "json") + output, err := r.Command("-o", "pv_name,pv_size", "--reportformat", "json", "--units", "b", "--nosuffix") if err != nil { return nil, err } @@ -68,8 +100,13 @@ func (ls *LinuxLvmService) GetPhysicalVolumes() ([]*model.PhysicalVolume, error) } pvs := make([]*model.PhysicalVolume, len(pr.Report[0].PhysicalVolume)) for i, pv := range pr.Report[0].PhysicalVolume { + size, err := strconv.ParseUint(pv.PhysicalVolumeSize, 10, 64) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to cast physical volume size to unsigned 64-bit integer") + } pvs[i] = &model.PhysicalVolume{ Name: pv.Name, + Size: size, } } return pvs, nil @@ -77,7 +114,7 @@ func (ls *LinuxLvmService) GetPhysicalVolumes() ([]*model.PhysicalVolume, error) func (ls *LinuxLvmService) GetVolumeGroups() ([]*model.VolumeGroup, error) { r := ls.runnerFactory.Select(utils.Vgs) - output, err := r.Command("-o", "vg_name,pv_name", "--reportformat", "json") + output, err := r.Command("-o", "vg_name,pv_name,vg_size", "--reportformat", "json", "--units", "b", "--nosuffix") if err != nil { return nil, err } @@ -88,9 +125,14 @@ func (ls *LinuxLvmService) GetVolumeGroups() ([]*model.VolumeGroup, error) { } vgs := make([]*model.VolumeGroup, len(vr.Report[0].VolumeGroup)) for i, vg := range vr.Report[0].VolumeGroup { + size, err := strconv.ParseUint(vg.Size, 10, 64) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to cast volume group size to unsigned 64-bit integer") + } vgs[i] = &model.VolumeGroup{ Name: vg.Name, PhysicalVolume: vg.PhysicalVolume, + Size: size, } } return vgs, nil @@ -98,7 +140,7 @@ func (ls *LinuxLvmService) GetVolumeGroups() ([]*model.VolumeGroup, error) { func (ls *LinuxLvmService) GetLogicalVolumes() ([]*model.LogicalVolume, error) { r := ls.runnerFactory.Select(utils.Lvs) - output, err := r.Command("-o", "lv_name,vg_name,lv_attr", "--reportformat", "json") + output, err := r.Command("-o", "lv_name,vg_name,lv_attr,lv_size", "--reportformat", "json", "--units", "b", "--nosuffix") if err != nil { return nil, err } @@ -118,10 +160,15 @@ func (ls *LinuxLvmService) GetLogicalVolumes() ([]*model.LogicalVolume, error) { default: state = model.Unsupported } + size, err := strconv.ParseUint(lv.Size, 10, 64) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to cast logical volume size to unsigned 64-bit integer") + } lvs[i] = &model.LogicalVolume{ Name: lv.Name, VolumeGroup: lv.VolumeGroup, State: state, + Size: size, } } return lvs, nil @@ -150,3 +197,9 @@ func (ls *LinuxLvmService) ActivateLogicalVolume(name string, volumeGroup string _, err := r.Command("-ay", fmt.Sprintf("%s/%s", volumeGroup, name)) return err } + +func (ls *LinuxLvmService) ResizePhysicalVolume(name string) error { + r := ls.runnerFactory.Select(utils.PvResize) + _, err := r.Command(name) + return err +} diff --git a/internal/utils/exec.go b/internal/utils/exec.go index 070ecdc..97895fe 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -24,6 +24,7 @@ const ( XfsGrowfs Binary = "xfs_growfs" Pvs Binary = "pvs" PvCreate Binary = "pvcreate" + PvResize Binary = "pvresize" Vgs Binary = "vgs" VgCreate Binary = "vgcreate" Lvs Binary = "lvs"