diff --git a/cmd/ebs-bootstrap.go b/cmd/ebs-bootstrap.go index 9ec2efb..d54c1f1 100644 --- a/cmd/ebs-bootstrap.go +++ b/cmd/ebs-bootstrap.go @@ -54,6 +54,7 @@ func main() { lvmLayers := []layer.Layer{ layer.NewCreatePhysicalVolumeLayer(db, lb), layer.NewCreateVolumeGroupLayer(lb), + layer.NewCreateLogicalVolumeLayer(lb), } checkError(le.Execute(lvmLayers)) diff --git a/configs/ubuntu.yml b/configs/ubuntu.yml index c2d32e7..e09243c 100644 --- a/configs/ubuntu.yml +++ b/configs/ubuntu.yml @@ -1,4 +1,9 @@ +defaults: + lvmConsumption: 80 devices: /dev/vdb: fs: xfs lvm: ifmx-etc + /dev/vdc: + fs: xfs + lvm: ifmx-var \ No newline at end of file diff --git a/internal/action/lvm.go b/internal/action/lvm.go index b13d500..979307c 100644 --- a/internal/action/lvm.go +++ b/internal/action/lvm.go @@ -86,3 +86,46 @@ func (a *CreateVolumeGroupAction) Refuse() string { func (a *CreateVolumeGroupAction) Success() string { return fmt.Sprintf("Successfully created volume group %s on physical volume %s", a.name, a.physicalVolume) } + +type CreateLogicalVolumeAction struct { + name string + freeSpacePercent int + volumeGroup string + mode model.Mode + lvmService service.LvmService +} + +func NewCreateLogicalVolumeAction(name string, freeSpacePercent int, volumeGroup string, ls service.LvmService) *CreateLogicalVolumeAction { + return &CreateLogicalVolumeAction{ + name: name, + freeSpacePercent: freeSpacePercent, + volumeGroup: volumeGroup, + mode: model.Empty, + lvmService: ls, + } +} + +func (a *CreateLogicalVolumeAction) Execute() error { + return a.lvmService.CreateLogicalVolume(a.name, a.volumeGroup, a.freeSpacePercent) +} + +func (a *CreateLogicalVolumeAction) GetMode() model.Mode { + return a.mode +} + +func (a *CreateLogicalVolumeAction) SetMode(mode model.Mode) Action { + a.mode = mode + return a +} + +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) +} + +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) +} + +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) +} diff --git a/internal/backend/lvm.go b/internal/backend/lvm.go index b160cfd..42fd1bf 100644 --- a/internal/backend/lvm.go +++ b/internal/backend/lvm.go @@ -1,8 +1,6 @@ package backend import ( - "fmt" - "github.com/reecetech/ebs-bootstrap/internal/action" "github.com/reecetech/ebs-bootstrap/internal/config" datastructures "github.com/reecetech/ebs-bootstrap/internal/data_structures" @@ -13,10 +11,10 @@ import ( type LvmBackend interface { CreatePhysicalVolume(name string) action.Action CreateVolumeGroup(name string, physicalVolume string) action.Action - GetVolumeGroup(name string) (*model.VolumeGroup, error) - SearchVolumeGroup(pv *model.PhysicalVolume) (*model.VolumeGroup, error) - GetPhysicalVolume(name string) (*model.PhysicalVolume, error) - SearchPhysicalVolumes(vg *model.VolumeGroup) ([]*model.PhysicalVolume, error) + CreateLogicalVolume(name string, volumeGroup string, freeSpacePercent int) action.Action + GetVolumeGroups(name string) []*model.VolumeGroup + SearchLogicalVolumes(volumeGroup string) []*model.LogicalVolume + SearchVolumeGroup(physicalVolume string) *model.VolumeGroup From(config *config.Config) error } @@ -32,54 +30,51 @@ func NewLinuxLvmBackend(ls service.LvmService) *LinuxLvmBackend { } } -func (lb *LinuxLvmBackend) GetVolumeGroup(name string) (*model.VolumeGroup, error) { +func (lb *LinuxLvmBackend) GetVolumeGroups(name string) []*model.VolumeGroup { + vgs := []*model.VolumeGroup{} node, err := lb.lvmGraph.GetVolumeGroup(name) if err != nil { - return nil, err + return vgs } - return &model.VolumeGroup{ - Name: node.Name, - }, nil -} - -func (lb *LinuxLvmBackend) SearchVolumeGroup(pv *model.PhysicalVolume) (*model.VolumeGroup, error) { - pvn, err := lb.lvmGraph.GetPhysicalVolume(pv.Name) - if err != nil { - return nil, err - } - vg := lb.lvmGraph.GetChildren(pvn, datastructures.VolumeGroup) - if len(vg) == 0 { - return nil, nil + pvn := lb.lvmGraph.GetParents(node, datastructures.PhysicalVolume) + for _, pv := range pvn { + vgs = append(vgs, &model.VolumeGroup{ + Name: node.Name, + PhysicalVolume: pv.Name, + }) } - return &model.VolumeGroup{Name: vg[0].Name}, nil + return vgs } -func (lb *LinuxLvmBackend) GetPhysicalVolume(name string) (*model.PhysicalVolume, error) { - node, err := lb.lvmGraph.GetPhysicalVolume(name) +func (lb *LinuxLvmBackend) SearchLogicalVolumes(volumeGroup string) []*model.LogicalVolume { + lvs := []*model.LogicalVolume{} + node, err := lb.lvmGraph.GetVolumeGroup(volumeGroup) if err != nil { - return nil, err + return lvs + } + lvn := lb.lvmGraph.GetChildren(node, datastructures.LogicalVolume) + for _, lv := range lvn { + lvs = append(lvs, &model.LogicalVolume{ + Name: lv.Name, + VolumeGroup: node.Name, + }) } - return &model.PhysicalVolume{ - Name: node.Name, - }, nil + return lvs } -func (lb *LinuxLvmBackend) SearchPhysicalVolumes(vg *model.VolumeGroup) ([]*model.PhysicalVolume, error) { - vgn, err := lb.lvmGraph.GetVolumeGroup(vg.Name) +func (lb *LinuxLvmBackend) SearchVolumeGroup(physicalVolume string) *model.VolumeGroup { + node, err := lb.lvmGraph.GetPhysicalVolume(physicalVolume) if err != nil { - return nil, err + return nil } - pvs := lb.lvmGraph.GetParents(vgn, datastructures.PhysicalVolume) - physicalVolumes := make([]*model.PhysicalVolume, len(pvs)) - for i, pv := range pvs { - physicalVolumes[i] = &model.PhysicalVolume{ - Name: pv.Name, - } + vgn := lb.lvmGraph.GetChildren(node, datastructures.VolumeGroup) + if len(vgn) == 0 { + return nil } - if len(physicalVolumes) == 0 { - return nil, fmt.Errorf("🔴 %s: No physical volumes found", vg.Name) + return &model.VolumeGroup{ + Name: vgn[0].Name, + PhysicalVolume: node.Name, } - return physicalVolumes, nil } func (lb *LinuxLvmBackend) CreatePhysicalVolume(name string) action.Action { @@ -90,6 +85,10 @@ 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 (db *LinuxLvmBackend) From(config *config.Config) error { pvs, err := db.lvmService.GetPhysicalVolumes() if err != nil { @@ -106,5 +105,12 @@ func (db *LinuxLvmBackend) From(config *config.Config) error { for _, vg := range vgs { db.lvmGraph.AddVolumeGroup(vg.Name, vg.PhysicalVolume) } + lvs, err := db.lvmService.GetLogicalVolumes() + if err != nil { + return err + } + for _, lv := range lvs { + db.lvmGraph.AddLogicalVolume(lv.Name, lv.VolumeGroup) + } return nil } diff --git a/internal/data_structures/lvm_graph.go b/internal/data_structures/lvm_graph.go index e351177..ebd0b4c 100644 --- a/internal/data_structures/lvm_graph.go +++ b/internal/data_structures/lvm_graph.go @@ -14,9 +14,9 @@ const ( ) type LvmNode struct { - id string - Name string - Active bool + id string + Name string + // Active bool nodeType LvmNodeType children []*LvmNode parents []*LvmNode @@ -24,9 +24,9 @@ type LvmNode struct { func NewBlockDevice(name string) *LvmNode { return &LvmNode{ - id: fmt.Sprintf("device:%s", name), - Name: name, - Active: true, + id: fmt.Sprintf("device:%s", name), + Name: name, + // Active: true, nodeType: BlockDevice, children: []*LvmNode{}, parents: []*LvmNode{}, @@ -35,9 +35,9 @@ func NewBlockDevice(name string) *LvmNode { func NewPhysicalVolume(name string) *LvmNode { return &LvmNode{ - id: fmt.Sprintf("pv:%s", name), - Name: name, - Active: true, + id: fmt.Sprintf("pv:%s", name), + Name: name, + // Active: true, nodeType: PhysicalVolume, children: []*LvmNode{}, parents: []*LvmNode{}, @@ -46,20 +46,20 @@ func NewPhysicalVolume(name string) *LvmNode { func NewVolumeGroup(name string) *LvmNode { return &LvmNode{ - id: fmt.Sprintf("vg:%s", name), - Name: name, - Active: false, + id: fmt.Sprintf("vg:%s", name), + Name: name, + // Active: false, nodeType: VolumeGroup, children: []*LvmNode{}, parents: []*LvmNode{}, } } -func NewLogicalVolume(name string, vg string, active bool) *LvmNode { +func NewLogicalVolume(name string, vg string) *LvmNode { return &LvmNode{ - id: fmt.Sprintf("lv:%s:vg:%s", name, vg), - Name: name, - Active: active, + id: fmt.Sprintf("lv:%s:vg:%s", name, vg), + Name: name, + // Active: active, nodeType: LogicalVolume, children: []*LvmNode{}, parents: []*LvmNode{}, @@ -132,8 +132,8 @@ func (lg *LvmGraph) AddVolumeGroup(name string, pv string) error { return nil } -func (lg *LvmGraph) AddLogicalVolume(name string, vg string, active bool) error { - lv := NewLogicalVolume(name, vg, active) +func (lg *LvmGraph) AddLogicalVolume(name string, vg string) error { + lv := NewLogicalVolume(name, vg) _, found := lg.nodes[lv.id] if found { @@ -152,12 +152,12 @@ func (lg *LvmGraph) AddLogicalVolume(name string, vg string, active bool) error // If at least one of the logical volumes are active, the // volume group is considered active - for _, lvn := range vgn.children { - if lvn.Active { - vgn.Active = true - break - } - } + // for _, lvn := range vgn.children { + // if lvn.Active { + // vgn.Active = true + // break + // } + // } return nil } diff --git a/internal/layer/lv.go b/internal/layer/lv.go new file mode 100644 index 0000000..cea99cc --- /dev/null +++ b/internal/layer/lv.go @@ -0,0 +1,71 @@ +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" +) + +type CreateLogicalVolumeLayer struct { + lvmBackend backend.LvmBackend +} + +func NewCreateLogicalVolumeLayer(lb backend.LvmBackend) *CreateLogicalVolumeLayer { + return &CreateLogicalVolumeLayer{ + lvmBackend: lb, + } +} + +func (cvgl *CreateLogicalVolumeLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + if len(cd.Lvm) == 0 { + continue + } + + lvs := cvgl.lvmBackend.SearchLogicalVolumes(cd.Lvm) + if len(lvs) == 1 { + if lvs[0].Name == cd.Lvm { + continue + } + return nil, fmt.Errorf("🔴 %s: Volume group %s already has logical volume %s associated", name, cd.Lvm, lvs[0].Name) + } + if len(lvs) > 1 { + return nil, fmt.Errorf("🔴 %s: Cannot manage volume group %s with more than one logical volume associated", name, cd.Lvm) + } + + mode := c.GetMode(name) + a := cvgl.lvmBackend.CreateLogicalVolume(cd.Lvm, cd.Lvm, c.GetLvmConsumption(name)) + actions = append(actions, a.SetMode(mode)) + } + return actions, nil +} + +func (cvgl *CreateLogicalVolumeLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + if len(cd.Lvm) == 0 { + continue + } + lvs := cvgl.lvmBackend.SearchLogicalVolumes(cd.Lvm) + if len(lvs) == 1 { + if lvs[0].Name == cd.Lvm { + continue + } + return fmt.Errorf("🔴 %s: Failed to validate logical volume. Expected=%s, Actual=%s", name, cd.Lvm, lvs[0].Name) + } + if len(lvs) > 1 { + return fmt.Errorf("🔴 %s: ailed to validate logical volume. #(Logical Volume) Expected=%d, Actual=%d", name, 1, len(lvs)) + } + } + return nil +} + +func (cvgl *CreateLogicalVolumeLayer) Warning() string { + return DisabledWarning +} + +func (cvgl *CreateLogicalVolumeLayer) From(c *config.Config) error { + return cvgl.lvmBackend.From(c) +} diff --git a/internal/layer/vg.go b/internal/layer/vg.go index 2a64a8d..499a333 100644 --- a/internal/layer/vg.go +++ b/internal/layer/vg.go @@ -24,39 +24,24 @@ func (cvgl *CreateVolumeGroupLayer) Modify(c *config.Config) ([]action.Action, e if len(cd.Lvm) == 0 { continue } - - vg, _ := cvgl.lvmBackend.GetVolumeGroup(cd.Lvm) - - if vg != nil { - pvs, err := cvgl.lvmBackend.SearchPhysicalVolumes(vg) - if err != nil { - return nil, err - } - if len(pvs) == 1 { - if pvs[0].Name == name { - continue - } - return nil, fmt.Errorf("🔴 %s: Volume group %s belongs to the incorrect physical volume. Expected=%s, Actual=%s", name, vg.Name, name, pvs[0].Name) - } - return nil, fmt.Errorf("🔴 %s: Cannot manage a volume group %s with more than one physical volumes associated", name, vg.Name) - } - - pv, err := cvgl.lvmBackend.GetPhysicalVolume(name) - if err != nil { - return nil, err + vg := cvgl.lvmBackend.SearchVolumeGroup(name) + 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) } - vg, err = cvgl.lvmBackend.SearchVolumeGroup(pv) - if err != nil { - return nil, err + vgs := cvgl.lvmBackend.GetVolumeGroups(cd.Lvm) + if len(vgs) == 1 { + if vgs[0].PhysicalVolume == name { + continue + } + return nil, fmt.Errorf("🔴 %s: Volume group %s does not belong to physical volume %s", name, cd.Lvm, name) } - - if vg != nil { - return nil, fmt.Errorf("🔴 %s: Volume group %s already exists on physical volume %s", name, vg.Name, pv.Name) + if len(vgs) > 1 { + return nil, fmt.Errorf("🔴 %s: Cannot manage volume group %s with more than one physical volumes associated", name, cd.Lvm) } mode := c.GetMode(name) - a := cvgl.lvmBackend.CreateVolumeGroup(cd.Lvm, pv.Name) + a := cvgl.lvmBackend.CreateVolumeGroup(cd.Lvm, name) actions = append(actions, a.SetMode(mode)) } return actions, nil @@ -67,10 +52,17 @@ func (cvgl *CreateVolumeGroupLayer) Validate(c *config.Config) error { if len(cd.Lvm) == 0 { continue } - _, err := cvgl.lvmBackend.GetVolumeGroup(cd.Lvm) - if err != nil { - return fmt.Errorf("🔴 %s: Failed volume group validation checks. Volume group %s does not exist", name, cd.Lvm) + vgs := cvgl.lvmBackend.GetVolumeGroups(cd.Lvm) + if len(vgs) == 1 { + if vgs[0].PhysicalVolume == name { + return nil + } + return fmt.Errorf("🔴 %s: Failed to validate volume group. Expected=%s, Actual=%s", name, name, vgs[0].PhysicalVolume) } + if len(vgs) > 1 { + return fmt.Errorf("🔴 %s: Failed to validate volume group. #(Physical volume) Expected=%d, Actual=%d", name, 1, len(vgs)) + } + } return nil } diff --git a/internal/model/lvm.go b/internal/model/lvm.go index a8596d3..1461c2a 100644 --- a/internal/model/lvm.go +++ b/internal/model/lvm.go @@ -8,3 +8,8 @@ type VolumeGroup struct { Name string PhysicalVolume string } + +type LogicalVolume struct { + Name string + VolumeGroup string +} diff --git a/internal/service/lvm.go b/internal/service/lvm.go index c0ba481..0242e08 100644 --- a/internal/service/lvm.go +++ b/internal/service/lvm.go @@ -11,8 +11,10 @@ import ( type LvmService interface { GetPhysicalVolumes() ([]*model.PhysicalVolume, error) GetVolumeGroups() ([]*model.VolumeGroup, error) + GetLogicalVolumes() ([]*model.LogicalVolume, error) CreatePhysicalVolume(name string) error CreateVolumeGroup(name string, physicalVolume string) error + CreateLogicalVolume(name string, volumeGroup string, freeSpacePercent int) error } type LinuxLvmService struct { @@ -36,6 +38,15 @@ type VgsResponse struct { } `json:"report"` } +type LvsResponse struct { + Report []struct { + LogicalVolume []struct { + Name string `json:"lv_name"` + VolumeGroup string `json:"vg_name"` + } `json:"lv"` + } `json:"report"` +} + func NewLinuxLvmService(rf utils.RunnerFactory) *LinuxLvmService { return &LinuxLvmService{ runnerFactory: rf, @@ -83,6 +94,27 @@ func (ls *LinuxLvmService) GetVolumeGroups() ([]*model.VolumeGroup, error) { return vgs, nil } +func (ls *LinuxLvmService) GetLogicalVolumes() ([]*model.LogicalVolume, error) { + r := ls.runnerFactory.Select(utils.Lvs) + output, err := r.Command("-o", "lv_name,vg_name", "--reportformat", "json") + if err != nil { + return nil, err + } + lr := &LvsResponse{} + err = json.Unmarshal([]byte(output), lr) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to decode lvs response: %v", err) + } + lvs := make([]*model.LogicalVolume, len(lr.Report[0].LogicalVolume)) + for i, lv := range lr.Report[0].LogicalVolume { + lvs[i] = &model.LogicalVolume{ + Name: lv.Name, + VolumeGroup: lv.VolumeGroup, + } + } + return lvs, nil +} + func (ls *LinuxLvmService) CreatePhysicalVolume(name string) error { r := ls.runnerFactory.Select(utils.PvCreate) _, err := r.Command(name) @@ -94,3 +126,9 @@ func (ls *LinuxLvmService) CreateVolumeGroup(name string, physicalVolume string) _, err := r.Command(name, physicalVolume) return err } + +func (ls *LinuxLvmService) CreateLogicalVolume(name string, volumeGroup string, freeSpacePercent int) error { + r := ls.runnerFactory.Select(utils.LvCreate) + _, err := r.Command("-l", fmt.Sprintf("%d%%FREE", freeSpacePercent), "-n", name, volumeGroup) + return err +} diff --git a/internal/utils/exec.go b/internal/utils/exec.go index 50fc22b..645840e 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -26,6 +26,8 @@ const ( PvCreate Binary = "pvcreate" Vgs Binary = "vgs" VgCreate Binary = "vgcreate" + Lvs Binary = "lvs" + LvCreate Binary = "lvcreate" ) type RunnerFactory interface {