diff --git a/cmd/ebs-bootstrap.go b/cmd/ebs-bootstrap.go index e97cb0e..680e45a 100644 --- a/cmd/ebs-bootstrap.go +++ b/cmd/ebs-bootstrap.go @@ -66,6 +66,7 @@ func main() { layer.NewCreateVolumeGroupLayer(lb), layer.NewCreateLogicalVolumeLayer(lb), layer.NewActivateLogicalVolumeLayer(lb), + layer.NewResizeLogicalVolumeLayer(lb), } checkError(le.Execute(lvmLayers)) diff --git a/configs/ubuntu.yml b/configs/ubuntu.yml index 3ef9896..e305eb2 100644 --- a/configs/ubuntu.yml +++ b/configs/ubuntu.yml @@ -1,5 +1,4 @@ defaults: - lvmConsumption: 100 resizeFs: true resizeThreshold: 99 devices: @@ -10,6 +9,7 @@ devices: user: ubuntu group: ubuntu permissions: 755 + lvmConsumption: 100 /dev/vdc: fs: xfs lvm: ifmx-var @@ -17,3 +17,4 @@ devices: user: ubuntu group: ubuntu permissions: 755 + lvmConsumption: 30 diff --git a/internal/action/lvm.go b/internal/action/lvm.go index 58c20d0..7859208 100644 --- a/internal/action/lvm.go +++ b/internal/action/lvm.go @@ -88,25 +88,25 @@ func (a *CreateVolumeGroupAction) Success() string { } type CreateLogicalVolumeAction struct { - name string - freeSpacePercent int - volumeGroup string - mode model.Mode - lvmService service.LvmService + name string + volumeGroupPercent int + volumeGroup string + mode model.Mode + lvmService service.LvmService } -func NewCreateLogicalVolumeAction(name string, freeSpacePercent int, volumeGroup string, ls service.LvmService) *CreateLogicalVolumeAction { +func NewCreateLogicalVolumeAction(name string, volumeGroupPercent int, volumeGroup string, ls service.LvmService) *CreateLogicalVolumeAction { return &CreateLogicalVolumeAction{ - name: name, - freeSpacePercent: freeSpacePercent, - volumeGroup: volumeGroup, - mode: model.Empty, - lvmService: ls, + name: name, + volumeGroupPercent: volumeGroupPercent, + volumeGroup: volumeGroup, + mode: model.Empty, + lvmService: ls, } } func (a *CreateLogicalVolumeAction) Execute() error { - return a.lvmService.CreateLogicalVolume(a.name, a.volumeGroup, a.freeSpacePercent) + return a.lvmService.CreateLogicalVolume(a.name, a.volumeGroup, a.volumeGroupPercent) } func (a *CreateLogicalVolumeAction) GetMode() model.Mode { @@ -119,15 +119,15 @@ func (a *CreateLogicalVolumeAction) SetMode(mode model.Mode) Action { } func (a *CreateLogicalVolumeAction) Prompt() string { - return fmt.Sprintf("Would you like to create logical volume %s that consumes %d%% free space of volume group %s", a.name, a.freeSpacePercent, a.volumeGroup) + return fmt.Sprintf("Would you like to create logical volume %s that consumes %d%% of the space of volume group %s", a.name, a.volumeGroupPercent, a.volumeGroup) } func (a *CreateLogicalVolumeAction) Refuse() string { - return fmt.Sprintf("Refused to create logical volume %s that consumes %d%% free space of volume group %s", a.name, a.freeSpacePercent, a.volumeGroup) + return fmt.Sprintf("Refused to create logical volume %s that consumes %d%% of the space of volume group %s", a.name, a.volumeGroupPercent, a.volumeGroup) } func (a *CreateLogicalVolumeAction) Success() string { - return fmt.Sprintf("Successfully created logical volume %s that consumes %d%% free space of volume group %s", a.name, a.freeSpacePercent, a.volumeGroup) + return fmt.Sprintf("Successfully created logical volume %s that consumes %d%% of the space of volume group %s", a.name, a.volumeGroupPercent, a.volumeGroup) } type ActivateLogicalVolumeAction struct { @@ -209,3 +209,46 @@ func (a *ResizePhysicalVolumeAction) Refuse() string { func (a *ResizePhysicalVolumeAction) Success() string { return fmt.Sprintf("Successfully resized physical volume %s", a.name) } + +type ResizeLogicalVolumeAction struct { + name string + volumeGroupPercent int + volumeGroup string + mode model.Mode + lvmService service.LvmService +} + +func NewResizeLogicalVolumeAction(name string, volumeGroupPercent int, volumeGroup string, ls service.LvmService) *ResizeLogicalVolumeAction { + return &ResizeLogicalVolumeAction{ + name: name, + volumeGroupPercent: volumeGroupPercent, + volumeGroup: volumeGroup, + mode: model.Empty, + lvmService: ls, + } +} + +func (a *ResizeLogicalVolumeAction) Execute() error { + return a.lvmService.ResizeLogicalVolume(a.name, a.volumeGroup, a.volumeGroupPercent) +} + +func (a *ResizeLogicalVolumeAction) GetMode() model.Mode { + return a.mode +} + +func (a *ResizeLogicalVolumeAction) SetMode(mode model.Mode) Action { + a.mode = mode + return a +} + +func (a *ResizeLogicalVolumeAction) Prompt() string { + return fmt.Sprintf("Would you like to resize logical volume %s to consume %d%% of the space of volume group %s", a.name, a.volumeGroupPercent, a.volumeGroup) +} + +func (a *ResizeLogicalVolumeAction) Refuse() string { + return fmt.Sprintf("Refused to resize logical volume %s to consume %d%% of the space of volume group %s", a.name, a.volumeGroupPercent, a.volumeGroup) +} + +func (a *ResizeLogicalVolumeAction) Success() string { + return fmt.Sprintf("Successfully resized logical volume %s to consume %d%% of the space of volume group %s", a.name, a.volumeGroupPercent, a.volumeGroup) +} diff --git a/internal/backend/device.go b/internal/backend/device.go index 726b80b..ea05ea9 100644 --- a/internal/backend/device.go +++ b/internal/backend/device.go @@ -117,10 +117,12 @@ func (db *LinuxDeviceBackend) Umount(bd *model.BlockDevice) action.Action { } func (db *LinuxDeviceBackend) From(config *config.Config) error { - // Clear representation of devices + // We populate a temporary map and then assign it to the backend + // after all objects have been successfully added. This avoids a partial + // state in the event of failure during one of intermediate steps. db.blockDevices = nil - blockDevices := map[string]*model.BlockDevice{} + for name := range config.Devices { d, err := db.deviceService.GetBlockDevice(name) if err != nil { diff --git a/internal/backend/file.go b/internal/backend/file.go index eaf5d79..2a0e68a 100644 --- a/internal/backend/file.go +++ b/internal/backend/file.go @@ -89,10 +89,9 @@ func (lfb *LinuxFileBackend) IsMount(p string) bool { } func (lfb *LinuxFileBackend) From(config *config.Config) error { - // Clear representation of files lfb.files = nil - files := map[string]*model.File{} + for _, cd := range config.Devices { if len(cd.MountPoint) == 0 { continue diff --git a/internal/backend/lvm.go b/internal/backend/lvm.go index 73eb292..6145bf0 100644 --- a/internal/backend/lvm.go +++ b/internal/backend/lvm.go @@ -13,14 +13,16 @@ import ( type LvmBackend interface { CreatePhysicalVolume(name string) action.Action CreateVolumeGroup(name string, physicalVolume string) action.Action - CreateLogicalVolume(name string, volumeGroup string, freeSpacePercent int) action.Action + CreateLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) action.Action ActivateLogicalVolume(name string, volumeGroup string) action.Action GetVolumeGroups(name string) []*model.VolumeGroup GetLogicalVolume(name string, volumeGroup string) (*model.LogicalVolume, error) - SearchLogicalVolumes(volumeGroup string) []*model.LogicalVolume - SearchVolumeGroup(physicalVolume string) *model.VolumeGroup - ShouldResizePhysicalVolume(name string, threshold float64) bool + SearchLogicalVolumes(volumeGroup string) ([]*model.LogicalVolume, error) + SearchVolumeGroup(physicalVolume string) (*model.VolumeGroup, error) + ShouldResizePhysicalVolume(name string, threshold float64) (bool, error) ResizePhysicalVolume(name string) action.Action + ShouldResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent int, tolerance float64) (bool, error) + ResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) action.Action From(config *config.Config) error } @@ -38,68 +40,72 @@ func NewLinuxLvmBackend(ls service.LvmService) *LinuxLvmBackend { func (lb *LinuxLvmBackend) GetVolumeGroups(name string) []*model.VolumeGroup { vgs := []*model.VolumeGroup{} - node, err := lb.lvmGraph.GetVolumeGroup(name) + vgn, err := lb.lvmGraph.GetVolumeGroup(name) if err != nil { return vgs } - pvn := lb.lvmGraph.GetParents(node, datastructures.PhysicalVolume) + pvn := lb.lvmGraph.GetParents(vgn, model.PhysicalVolumeKind) for _, pv := range pvn { vgs = append(vgs, &model.VolumeGroup{ - Name: node.Name, + Name: vgn.Name, PhysicalVolume: pv.Name, + State: vgn.State, + Size: vgn.Size, }) } return vgs } func (lb *LinuxLvmBackend) GetLogicalVolume(name string, volumeGroup string) (*model.LogicalVolume, error) { - node, err := lb.lvmGraph.GetLogicalVolume(name, volumeGroup) + lvn, err := lb.lvmGraph.GetLogicalVolume(name, volumeGroup) if err != nil { return nil, err } - vgs := lb.lvmGraph.GetParents(node, datastructures.VolumeGroup) + vgs := lb.lvmGraph.GetParents(lvn, model.VolumeGroupKind) if len(vgs) == 0 { - return nil, fmt.Errorf("🔴 %s: Logical volume has no volume group", node.Name) + return nil, fmt.Errorf("🔴 %s: Logical volume has no volume group", lvn.Name) } return &model.LogicalVolume{ - Name: node.Name, + Name: lvn.Name, VolumeGroup: vgs[0].Name, - State: model.LogicalVolumeState(node.State), + State: lvn.State, + Size: lvn.Size, }, nil } -func (lb *LinuxLvmBackend) SearchLogicalVolumes(volumeGroup string) []*model.LogicalVolume { +func (lb *LinuxLvmBackend) SearchLogicalVolumes(volumeGroup string) ([]*model.LogicalVolume, error) { lvs := []*model.LogicalVolume{} - node, err := lb.lvmGraph.GetVolumeGroup(volumeGroup) + vgn, err := lb.lvmGraph.GetVolumeGroup(volumeGroup) if err != nil { - return lvs + return nil, err } - lvn := lb.lvmGraph.GetChildren(node, datastructures.LogicalVolume) - for _, lv := range lvn { + lvns := lb.lvmGraph.GetChildren(vgn, model.LogicalVolumeKind) + for _, lvn := range lvns { lvs = append(lvs, &model.LogicalVolume{ - Name: lv.Name, - VolumeGroup: node.Name, - State: model.LogicalVolumeState(lv.State), - Size: lv.Size, + Name: lvn.Name, + VolumeGroup: vgn.Name, + State: lvn.State, + Size: lvn.Size, }) } - return lvs + return lvs, nil } -func (lb *LinuxLvmBackend) SearchVolumeGroup(physicalVolume string) *model.VolumeGroup { - node, err := lb.lvmGraph.GetPhysicalVolume(physicalVolume) +func (lb *LinuxLvmBackend) SearchVolumeGroup(physicalVolume string) (*model.VolumeGroup, error) { + pvn, err := lb.lvmGraph.GetPhysicalVolume(physicalVolume) if err != nil { - return nil + return nil, err } - vgn := lb.lvmGraph.GetChildren(node, datastructures.VolumeGroup) + vgn := lb.lvmGraph.GetChildren(pvn, model.VolumeGroupKind) if len(vgn) == 0 { - return nil + return nil, fmt.Errorf("🔴 %s: Physical volume has no volume group", physicalVolume) } return &model.VolumeGroup{ Name: vgn[0].Name, - PhysicalVolume: node.Name, + PhysicalVolume: pvn.Name, + State: vgn[0].State, Size: vgn[0].Size, - } + }, nil } func (lb *LinuxLvmBackend) CreatePhysicalVolume(name string) action.Action { @@ -110,58 +116,100 @@ func (lb *LinuxLvmBackend) CreateVolumeGroup(name string, physicalVolume string) return action.NewCreateVolumeGroupAction(name, physicalVolume, lb.lvmService) } -func (lb *LinuxLvmBackend) CreateLogicalVolume(name string, volumeGroup string, freeSpacePercent int) action.Action { - return action.NewCreateLogicalVolumeAction(name, freeSpacePercent, volumeGroup, lb.lvmService) +func (lb *LinuxLvmBackend) CreateLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) action.Action { + return action.NewCreateLogicalVolumeAction(name, volumeGroupPercent, volumeGroup, lb.lvmService) } func (lb *LinuxLvmBackend) ActivateLogicalVolume(name string, volumeGroup string) action.Action { return action.NewActivateLogicalVolumeAction(name, volumeGroup, lb.lvmService) } -func (lb *LinuxLvmBackend) ShouldResizePhysicalVolume(name string, threshold float64) bool { - node, err := lb.lvmGraph.GetPhysicalVolume(name) +func (lb *LinuxLvmBackend) ShouldResizePhysicalVolume(name string, threshold float64) (bool, error) { + pvn, err := lb.lvmGraph.GetPhysicalVolume(name) if err != nil { - return false + return false, nil } - dvn := lb.lvmGraph.GetParents(node, datastructures.Device) - if len(dvn) == 0 { - return false + dn := lb.lvmGraph.GetParents(pvn, model.DeviceKind) + if len(dn) == 0 { + return false, nil } - return (float64(node.Size) / float64(dvn[0].Size) * 100) < threshold + return (float64(pvn.Size) / float64(dn[0].Size) * 100) < threshold, nil } func (lb *LinuxLvmBackend) ResizePhysicalVolume(name string) action.Action { return action.NewResizePhysicalVolumeAction(name, lb.lvmService) } +func (lb *LinuxLvmBackend) ShouldResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent int, tolerance float64) (bool, error) { + left := float64(volumeGroupPercent) - tolerance + right := float64(volumeGroupPercent) + tolerance + lvn, err := lb.lvmGraph.GetLogicalVolume(name, volumeGroup) + if err != nil { + return false, err + } + vgn := lb.lvmGraph.GetParents(lvn, model.VolumeGroupKind) + if len(vgn) == 0 { + return false, fmt.Errorf("🔴 %s: Logical volume has no volume group", name) + } + usedPerecent := (float64(lvn.Size) / float64(vgn[0].Size)) * 100 + if usedPerecent > right { + return false, fmt.Errorf("🔴 %s: Logical volume %s is using %.0f%% of volume group %s, which exceeds the expected usage of %d%%", volumeGroup, name, usedPerecent, volumeGroup, volumeGroupPercent) + } + return usedPerecent < left, nil +} + +func (lb *LinuxLvmBackend) ResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) action.Action { + return action.NewResizeLogicalVolumeAction(name, volumeGroupPercent, volumeGroup, lb.lvmService) +} + func (db *LinuxLvmBackend) From(config *config.Config) error { + // We populate a temporary lvmGraph and then assign it to the backend + // after all objects have been successfully added. This avoids a partial + // state in the event of failure during one of intermediate steps. + db.lvmGraph = nil + lvmGraph := datastructures.NewLvmGraph() + ds, err := db.lvmService.GetDevices() if err != nil { return err } for _, d := range ds { - db.lvmGraph.AddDevice(d.Name, d.Size) + err := lvmGraph.AddDevice(d.Name, d.Size) + if err != nil { + return err + } } pvs, err := db.lvmService.GetPhysicalVolumes() if err != nil { return err } for _, pv := range pvs { - db.lvmGraph.AddPhysicalVolume(pv.Name, pv.Size) + err := lvmGraph.AddPhysicalVolume(pv.Name, pv.Size) + if err != nil { + return err + } } vgs, err := db.lvmService.GetVolumeGroups() if err != nil { return err } for _, vg := range vgs { - db.lvmGraph.AddVolumeGroup(vg.Name, vg.PhysicalVolume, vg.Size) + err := lvmGraph.AddVolumeGroup(vg.Name, vg.PhysicalVolume, vg.Size) + if err != nil { + return err + } } 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), lv.Size) + err := lvmGraph.AddLogicalVolume(lv.Name, lv.VolumeGroup, lv.State, lv.Size) + if err != nil { + return err + } } + + db.lvmGraph = lvmGraph return nil } diff --git a/internal/backend/metrics.go b/internal/backend/metrics.go index 61de7cc..6243bb5 100644 --- a/internal/backend/metrics.go +++ b/internal/backend/metrics.go @@ -44,10 +44,9 @@ func (dmb *LinuxDeviceMetricsBackend) GetBlockDeviceMetrics(name string) (*model } func (dmb *LinuxDeviceMetricsBackend) From(config *config.Config) error { - // Clear representation of metrics dmb.blockDeviceMetrics = nil - blockDeviceMetrics := map[string]*model.BlockDeviceMetrics{} + for name := range config.Devices { bd, err := dmb.deviceService.GetBlockDevice(name) if err != nil { diff --git a/internal/backend/owner.go b/internal/backend/owner.go index 319017c..055a3d9 100644 --- a/internal/backend/owner.go +++ b/internal/backend/owner.go @@ -53,12 +53,11 @@ func (lfb *LinuxOwnerBackend) GetGroup(group string) (*model.Group, error) { } func (lfb *LinuxOwnerBackend) From(config *config.Config) error { - // Clear representation of users and groups lfb.users = nil lfb.groups = nil - users := map[string]*model.User{} groups := map[string]*model.Group{} + for _, cd := range config.Devices { if len(cd.User) > 0 { o, err := lfb.ownerService.GetUser(cd.User) diff --git a/internal/datastructures/lvm_graph.go b/internal/datastructures/lvm_graph.go index cbf8c9c..8f90bcc 100644 --- a/internal/datastructures/lvm_graph.go +++ b/internal/datastructures/lvm_graph.go @@ -2,32 +2,14 @@ package datastructures import ( "fmt" -) - -type LvmNodeState int32 -type LvmNodeCategory int32 - -const ( - DeviceActive LvmNodeState = 0b0000001 - PhysicalVolumeActive LvmNodeState = 0b0000010 - VolumeGroupInactive LvmNodeState = 0b0000100 - VolumeGroupActive LvmNodeState = 0b0001100 - LogicalVolumeInactive LvmNodeState = 0b0010000 - LogicalVolumeActive LvmNodeState = 0b0110000 - LogicalVolumeUnsupported LvmNodeState = 0b1110000 -) -const ( - Device LvmNodeCategory = 0b0000001 - PhysicalVolume LvmNodeCategory = 0b0000010 - VolumeGroup LvmNodeCategory = 0b0000100 - LogicalVolume LvmNodeCategory = 0b0010000 + "github.com/reecetech/ebs-bootstrap/internal/model" ) type LvmNode struct { id string Name string - State LvmNodeState + State model.LvmState Size uint64 children []*LvmNode parents []*LvmNode @@ -37,7 +19,7 @@ func NewDevice(name string, size uint64) *LvmNode { return &LvmNode{ id: fmt.Sprintf("device:%s", name), Name: name, - State: DeviceActive, + State: model.DeviceActive, Size: size, children: []*LvmNode{}, parents: []*LvmNode{}, @@ -48,7 +30,7 @@ func NewPhysicalVolume(name string, size uint64) *LvmNode { return &LvmNode{ id: fmt.Sprintf("pv:%s", name), Name: name, - State: PhysicalVolumeActive, + State: model.PhysicalVolumeActive, Size: size, children: []*LvmNode{}, parents: []*LvmNode{}, @@ -59,14 +41,14 @@ func NewVolumeGroup(name string, size uint64) *LvmNode { return &LvmNode{ id: fmt.Sprintf("vg:%s", name), Name: name, - State: VolumeGroupInactive, + State: model.VolumeGroupInactive, Size: size, children: []*LvmNode{}, parents: []*LvmNode{}, } } -func NewLogicalVolume(name string, vg string, State LvmNodeState, size uint64) *LvmNode { +func NewLogicalVolume(name string, vg string, State model.LvmState, size uint64) *LvmNode { return &LvmNode{ id: fmt.Sprintf("lv:%s:vg:%s", name, vg), Name: name, @@ -78,7 +60,7 @@ func NewLogicalVolume(name string, vg string, State LvmNodeState, size uint64) * } type LvmGraph struct { - nodes map[string]*LvmNode // A map that stores all the nodes by their Id + nodes map[string]*LvmNode } func NewLvmGraph() *LvmGraph { @@ -92,7 +74,7 @@ func (lg *LvmGraph) AddDevice(name string, size uint64) error { _, found := lg.nodes[bd.id] if found { - return fmt.Errorf("block device %s already exists", name) + return fmt.Errorf("🔴 %s: Device already exists", name) } lg.nodes[bd.id] = bd @@ -104,117 +86,123 @@ func (lg *LvmGraph) AddPhysicalVolume(name string, size uint64) error { _, found := lg.nodes[pv.id] if found { - return fmt.Errorf("physical volume %s already exists", name) + return fmt.Errorf("🔴 %s: Physical volume already exists", name) } - bdId := fmt.Sprintf("device:%s", name) - bdn, found := lg.nodes[bdId] - if !found { - return fmt.Errorf("block device %s does not exist", name) + dn, err := lg.GetDevice(name) + if err != nil { + return err } lg.nodes[pv.id] = pv - bdn.children = append(bdn.children, pv) - pv.parents = append(pv.parents, bdn) + dn.children = append(dn.children, pv) + pv.parents = append(pv.parents, dn) return nil } func (lg *LvmGraph) AddVolumeGroup(name string, pv string, size uint64) error { id := fmt.Sprintf("vg:%s", name) - vg, found := lg.nodes[id] + vgn, found := lg.nodes[id] if !found { - vg = NewVolumeGroup(name, size) + vgn = NewVolumeGroup(name, size) } - pvId := fmt.Sprintf("pv:%s", pv) - pvn, found := lg.nodes[pvId] - if !found { - return fmt.Errorf("physical volume %s does not exist", pv) + pvn, err := lg.GetPhysicalVolume(pv) + if err != nil { + return err } if len(pvn.children) > 0 { - return fmt.Errorf("%s is already assigned to volume group %s", pv, pvn.children[0].Name) + return fmt.Errorf("🔴 %s: Physical volume is already assigned to volume group %s", pv, pvn.children[0].Name) } - lg.nodes[vg.id] = vg - pvn.children = append(pvn.children, vg) - vg.parents = append(vg.parents, pvn) + lg.nodes[vgn.id] = vgn + pvn.children = append(pvn.children, vgn) + vgn.parents = append(vgn.parents, pvn) return nil } -func (lg *LvmGraph) AddLogicalVolume(name string, vg string, state LvmNodeState, size uint64) error { - lv := NewLogicalVolume(name, vg, state, size) +func (lg *LvmGraph) AddLogicalVolume(name string, vg string, state model.LvmState, size uint64) error { + lvn := NewLogicalVolume(name, vg, state, size) - _, found := lg.nodes[lv.id] + _, found := lg.nodes[lvn.id] if found { - return fmt.Errorf("logical volume %s already exists", name) + return fmt.Errorf("🔴 %s/%s: Logical volume already exists", name, vg) } - vgId := fmt.Sprintf("vg:%s", vg) - vgn, found := lg.nodes[vgId] - if !found { - return fmt.Errorf("volume group %s does not exist", vg) + vgn, err := lg.GetVolumeGroup(vg) + if err != nil { + return err } - lg.nodes[lv.id] = lv - vgn.children = append(vgn.children, lv) - lv.parents = append(lv.parents, vgn) + lg.nodes[lvn.id] = lvn + vgn.children = append(vgn.children, lvn) + lvn.parents = append(lvn.parents, vgn) // If at least one of the logical volumes are active, the // volume group is considered active for _, lvn := range vgn.children { - if lvn.State == LogicalVolumeActive { - vgn.State = VolumeGroupActive + if lvn.State == model.LogicalVolumeActive { + vgn.State = model.VolumeGroupActive break } } return nil } +func (lg *LvmGraph) GetDevice(name string) (*LvmNode, error) { + id := fmt.Sprintf("device:%s", name) + dn, found := lg.nodes[id] + if !found { + return nil, fmt.Errorf("🔴 %s: Block device does not exist", name) + } + return dn, nil +} + func (lg *LvmGraph) GetPhysicalVolume(name string) (*LvmNode, error) { id := fmt.Sprintf("pv:%s", name) - node, found := lg.nodes[id] + pvn, found := lg.nodes[id] if !found { - return nil, fmt.Errorf("physical volume %s does not exist", name) + return nil, fmt.Errorf("🔴 %s: Physical volume does not exist", name) } - return node, nil + return pvn, nil } func (lg *LvmGraph) GetVolumeGroup(name string) (*LvmNode, error) { id := fmt.Sprintf("vg:%s", name) - node, found := lg.nodes[id] + vgn, found := lg.nodes[id] if !found { - return nil, fmt.Errorf("volume group %s does not exist", name) + return nil, fmt.Errorf("🔴 %s: Volume group does not exist", name) } - return node, nil + return vgn, nil } func (lg *LvmGraph) GetLogicalVolume(name string, vg string) (*LvmNode, error) { id := fmt.Sprintf("lv:%s:vg:%s", name, vg) - node, found := lg.nodes[id] + lvn, found := lg.nodes[id] if !found { - return nil, fmt.Errorf("logical volume %s/%s does not exist", vg, name) + return nil, fmt.Errorf("🔴 %s/%s: Logical volume does not exist", vg, name) } - return node, nil + return lvn, nil } -func (lg *LvmGraph) GetParents(node *LvmNode, state LvmNodeCategory) []*LvmNode { +func (lg *LvmGraph) GetParents(node *LvmNode, kind model.LvmKind) []*LvmNode { parents := []*LvmNode{} for _, p := range node.parents { - // Bitmasking to check if the parent nodes is of the desired category - if int32(p.State)&int32(state) > 0 { + // Bitmasking to check if the parent nodes is of the desired kind + if int32(p.State)&int32(kind) > 0 { parents = append(parents, p) } } return parents } -func (lg *LvmGraph) GetChildren(node *LvmNode, state LvmNodeCategory) []*LvmNode { +func (lg *LvmGraph) GetChildren(node *LvmNode, kind model.LvmKind) []*LvmNode { children := []*LvmNode{} for _, c := range node.children { - // Bitmasking to check if children nodes is of the desired category - if int32(c.State)&int32(state) > 0 { + // Bitmasking to check if children nodes is of the desired kind + if int32(c.State)&int32(kind) > 0 { children = append(children, c) } } diff --git a/internal/layer/lv.go b/internal/layer/lv.go index 52331a3..787055a 100644 --- a/internal/layer/lv.go +++ b/internal/layer/lv.go @@ -25,7 +25,10 @@ func (cvgl *CreateLogicalVolumeLayer) Modify(c *config.Config) ([]action.Action, continue } - lvs := cvgl.lvmBackend.SearchLogicalVolumes(cd.Lvm) + lvs, err := cvgl.lvmBackend.SearchLogicalVolumes(cd.Lvm) + if err != nil { + return nil, err + } if len(lvs) == 1 { if lvs[0].Name == cd.Lvm { continue @@ -48,7 +51,10 @@ func (cvgl *CreateLogicalVolumeLayer) Validate(c *config.Config) error { if len(cd.Lvm) == 0 { continue } - lvs := cvgl.lvmBackend.SearchLogicalVolumes(cd.Lvm) + lvs, err := cvgl.lvmBackend.SearchLogicalVolumes(cd.Lvm) + if err != nil { + return err + } if len(lvs) == 1 { if lvs[0].Name == cd.Lvm { continue diff --git a/internal/layer/lv_activate.go b/internal/layer/lv_activate.go index fb5fffb..256c4c8 100644 --- a/internal/layer/lv_activate.go +++ b/internal/layer/lv_activate.go @@ -31,11 +31,11 @@ func (cvgl *ActivateLogicalVolumeLayer) Modify(c *config.Config) ([]action.Actio return nil, err } - if lv.State == model.Active { + if lv.State == model.LogicalVolumeActive { continue } - if lv.State == model.Unsupported { + if lv.State == model.LogicalVolumeUnsupported { return nil, fmt.Errorf("🔴 %s: Can not activate a logical volume in an unsupported state", lv.Name) } diff --git a/internal/layer/lv_resize.go b/internal/layer/lv_resize.go new file mode 100644 index 0000000..3303369 --- /dev/null +++ b/internal/layer/lv_resize.go @@ -0,0 +1,99 @@ +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 % tolerance to expect the logical volume size to be within + // ------------------------------------------------------- + // If the (logical volume / volume group size) * 100 is less than + // (lvmConsumption% - tolerance%) then we perform a resize operation + // ------------------------------------------------------- + // If the (logical volume / volume group size) * 100 is greater than + // (lvmConsumption% + tolerance%) then the user is attempting a downsize + // operation. We outright deny this as downsizing can be a destructive + // operation + // ------------------------------------------------------- + // Why implement a tolernace-based policy for resizing? + // - When creating a Logical Volume, `ebs-bootstrap` issues a command like + // `lvcreate -l 20%VG -n lv_name vg_name` + // - When we calculate how much percentage of the volume group has been + // consumed by the logical volume, the value would look like 20.0052096... + // - A tolerance establishes a window of acceptable values for avoiding a + // resizing operation + ResizeTolerance = float64(0.1) +) + +type ResizeLogicalVolumeLayer struct { + lvmBackend backend.LvmBackend +} + +func NewResizeLogicalVolumeLayer(lb backend.LvmBackend) *ResizeLogicalVolumeLayer { + return &ResizeLogicalVolumeLayer{ + lvmBackend: lb, + } +} + +func (rpvl *ResizeLogicalVolumeLayer) 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 + } + shouldResize, err := rpvl.lvmBackend.ShouldResizeLogicalVolume(cd.Lvm, cd.Lvm, c.GetLvmConsumption(name), ResizeTolerance) + if err != nil { + return nil, err + } + if !shouldResize { + continue + } + mode := c.GetMode(name) + a := rpvl.lvmBackend.ResizeLogicalVolume(cd.Lvm, cd.Lvm, c.GetLvmConsumption(name)) + actions = append(actions, a.SetMode(mode)) + } + return actions, nil +} + +func (rpvl *ResizeLogicalVolumeLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + if len(cd.Lvm) == 0 { + continue + } + if !c.GetResizeFs(name) { + continue + } + shouldResize, err := rpvl.lvmBackend.ShouldResizeLogicalVolume(cd.Lvm, cd.Lvm, c.GetLvmConsumption(name), ResizeTolerance) + if err != nil { + return err + } + if shouldResize { + return fmt.Errorf("🔴 %s: Failed resize validation checks. Logical volume %s still needs to be resized", name, name) + } + } + return nil +} + +func (rpvl *ResizeLogicalVolumeLayer) Warning() string { + return DisabledWarning +} + +func (rpvl *ResizeLogicalVolumeLayer) From(c *config.Config) error { + return rpvl.lvmBackend.From(c) +} + +func (rpvl *ResizeLogicalVolumeLayer) 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/layer/pv_resize.go b/internal/layer/pv_resize.go index 4c9b8e9..7bf9c04 100644 --- a/internal/layer/pv_resize.go +++ b/internal/layer/pv_resize.go @@ -47,7 +47,11 @@ func (rpvl *ResizePhysicalVolumeLayer) Modify(c *config.Config) ([]action.Action if !c.GetResizeFs(name) { continue } - if !rpvl.lvmBackend.ShouldResizePhysicalVolume(name, ResizeThreshold) { + shouldResize, err := rpvl.lvmBackend.ShouldResizePhysicalVolume(name, ResizeThreshold) + if err != nil { + return nil, err + } + if !shouldResize { continue } mode := c.GetMode(name) @@ -65,8 +69,12 @@ func (rpvl *ResizePhysicalVolumeLayer) Validate(c *config.Config) error { 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) + shouldResize, err := rpvl.lvmBackend.ShouldResizePhysicalVolume(name, ResizeThreshold) + if err != nil { + return err + } + if shouldResize { + return fmt.Errorf("🔴 %s: Failed resize validation checks. Physical volume %s still needs to be resized", name, name) } } return nil diff --git a/internal/layer/vg.go b/internal/layer/vg.go index ca51758..2c6789b 100644 --- a/internal/layer/vg.go +++ b/internal/layer/vg.go @@ -24,7 +24,10 @@ func (cvgl *CreateVolumeGroupLayer) Modify(c *config.Config) ([]action.Action, e if len(cd.Lvm) == 0 { continue } - vg := cvgl.lvmBackend.SearchVolumeGroup(name) + vg, err := cvgl.lvmBackend.SearchVolumeGroup(name) + if err != nil { + return nil, err + } if vg != nil && vg.Name != cd.Lvm { return nil, fmt.Errorf("🔴 %s: Physical volume %s already has volume group %s associated", name, name, vg.Name) } diff --git a/internal/model/lvm.go b/internal/model/lvm.go index 90cea52..98afbb8 100644 --- a/internal/model/lvm.go +++ b/internal/model/lvm.go @@ -1,6 +1,24 @@ package model -import "github.com/reecetech/ebs-bootstrap/internal/datastructures" +type LvmState int32 +type LvmKind int32 + +const ( + DeviceActive LvmState = 0b0000001 + PhysicalVolumeActive LvmState = 0b0000010 + VolumeGroupInactive LvmState = 0b0000100 + VolumeGroupActive LvmState = 0b0001100 + LogicalVolumeInactive LvmState = 0b0010000 + LogicalVolumeActive LvmState = 0b0110000 + LogicalVolumeUnsupported LvmState = 0b1110000 +) + +const ( + DeviceKind LvmKind = 0b0000001 + PhysicalVolumeKind LvmKind = 0b0000010 + VolumeGroupKind LvmKind = 0b0000100 + LogicalVolumeKind LvmKind = 0b0010000 +) type Device struct { Name string @@ -16,19 +34,12 @@ type VolumeGroup struct { Name string PhysicalVolume string Size uint64 + State LvmState } -type LogicalVolumeState int32 - -const ( - Inactive LogicalVolumeState = LogicalVolumeState(datastructures.LogicalVolumeInactive) - Active LogicalVolumeState = LogicalVolumeState(datastructures.LogicalVolumeActive) - Unsupported LogicalVolumeState = LogicalVolumeState(datastructures.LogicalVolumeUnsupported) -) - type LogicalVolume struct { Name string VolumeGroup string - State LogicalVolumeState + State LvmState Size uint64 } diff --git a/internal/service/lvm.go b/internal/service/lvm.go index ffcb362..c5c1fd0 100644 --- a/internal/service/lvm.go +++ b/internal/service/lvm.go @@ -16,9 +16,10 @@ type LvmService interface { GetLogicalVolumes() ([]*model.LogicalVolume, error) CreatePhysicalVolume(name string) error CreateVolumeGroup(name string, physicalVolume string) error - CreateLogicalVolume(name string, volumeGroup string, freeSpacePercent int) error + CreateLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) error ActivateLogicalVolume(name string, volumeGroup string) error ResizePhysicalVolume(name string) error + ResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) error } type LinuxLvmService struct { @@ -132,6 +133,7 @@ func (ls *LinuxLvmService) GetVolumeGroups() ([]*model.VolumeGroup, error) { vgs[i] = &model.VolumeGroup{ Name: vg.Name, PhysicalVolume: vg.PhysicalVolume, + State: model.VolumeGroupInactive, Size: size, } } @@ -151,19 +153,23 @@ func (ls *LinuxLvmService) GetLogicalVolumes() ([]*model.LogicalVolume, error) { } lvs := make([]*model.LogicalVolume, len(lr.Report[0].LogicalVolume)) for i, lv := range lr.Report[0].LogicalVolume { - var state model.LogicalVolumeState + // Get Logical Volume State + var state model.LvmState switch lv.Attributes[4] { case 'a': - state = model.Active + state = model.LogicalVolumeActive case '-': - state = model.Inactive + state = model.LogicalVolumeInactive default: - state = model.Unsupported + state = model.LogicalVolumeUnsupported } + + // Get Logical Volume Size 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, @@ -186,9 +192,9 @@ func (ls *LinuxLvmService) CreateVolumeGroup(name string, physicalVolume string) return err } -func (ls *LinuxLvmService) CreateLogicalVolume(name string, volumeGroup string, freeSpacePercent int) error { +func (ls *LinuxLvmService) CreateLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) error { r := ls.runnerFactory.Select(utils.LvCreate) - _, err := r.Command("-l", fmt.Sprintf("%d%%FREE", freeSpacePercent), "-n", name, volumeGroup) + _, err := r.Command("-l", fmt.Sprintf("%d%%VG", volumeGroupPercent), "-n", name, volumeGroup) return err } @@ -203,3 +209,9 @@ func (ls *LinuxLvmService) ResizePhysicalVolume(name string) error { _, err := r.Command(name) return err } + +func (ls *LinuxLvmService) ResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) error { + r := ls.runnerFactory.Select(utils.LvExtend) + _, err := r.Command("-l", fmt.Sprintf("%d%%VG", volumeGroupPercent), fmt.Sprintf("%s/%s", volumeGroup, name)) + return err +} diff --git a/internal/utils/exec.go b/internal/utils/exec.go index 97895fe..e949d38 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -30,6 +30,7 @@ const ( Lvs Binary = "lvs" LvCreate Binary = "lvcreate" LvChange Binary = "lvchange" + LvExtend Binary = "lvextend" ) type RunnerFactory interface {