From af4325aae9c7cd09b405474bae92ef5b1dbfa3b1 Mon Sep 17 00:00:00 2001 From: lasith-kg Date: Sat, 18 May 2024 12:16:48 +0000 Subject: [PATCH] (feat): Add Volume Group management support --- LVM.md | 13 ++ cmd/ebs-bootstrap.go | 2 + internal/action/lvm.go | 41 ++++++ internal/backend/lvm.go | 53 ++++++- internal/data_structures/lvm_graph.go | 190 ++++++++++++++++++++++++++ internal/layer/layer.go | 1 - internal/layer/vg.go | 67 +++++++++ internal/model/lvm.go | 20 +-- internal/service/lvm.go | 63 +++++++++ internal/utils/exec.go | 3 + 10 files changed, 434 insertions(+), 19 deletions(-) create mode 100644 internal/data_structures/lvm_graph.go create mode 100644 internal/layer/vg.go diff --git a/LVM.md b/LVM.md index d2381d1..cd1f82d 100644 --- a/LVM.md +++ b/LVM.md @@ -38,4 +38,17 @@ $ sudo pvs -o pv_name,pv_size,dev_size --units B --nosuffix PV PSize DevSize /dev/vda3 31079792640B 31082938368B /dev/vdb 10733223936B 10737418240B +``` + +# LVM Backend + +# Volume Group Names are unique system-wide +# Logical Group Names are unique within the scope of the Volume Group +``` +# Graph Based Data Structure +"device:xvdb" -> "pv:xvdb" +"pv:xvdb" -> "vg:ifmx-etc" +"pv:xvdc" -> "vg:ifmx-etc" +"vg:ifmx-etc" -> "lv:ifmx-etc:pv:ifmx-etc" +"vg:ifmx-etc" -> "lv:ifmx-etc-2:pv:ifmx-etc" ``` \ No newline at end of file diff --git a/cmd/ebs-bootstrap.go b/cmd/ebs-bootstrap.go index 23c1013..9ec2efb 100644 --- a/cmd/ebs-bootstrap.go +++ b/cmd/ebs-bootstrap.go @@ -53,6 +53,7 @@ func main() { // LVM Layers lvmLayers := []layer.Layer{ layer.NewCreatePhysicalVolumeLayer(db, lb), + layer.NewCreateVolumeGroupLayer(lb), } checkError(le.Execute(lvmLayers)) @@ -89,6 +90,7 @@ func main() { layer.NewChangePermissionsLayer(fb), } checkError(le.Execute(layers)) + log.Println("🟢 Passed all validation checks") } func checkError(err error) { diff --git a/internal/action/lvm.go b/internal/action/lvm.go index e7a67b7..b13d500 100644 --- a/internal/action/lvm.go +++ b/internal/action/lvm.go @@ -45,3 +45,44 @@ func (a *CreatePhysicalVolumeAction) Refuse() string { func (a *CreatePhysicalVolumeAction) Success() string { return fmt.Sprintf("Successfully created physical volume %s", a.name) } + +type CreateVolumeGroupAction struct { + name string + physicalVolume string + mode model.Mode + lvmService service.LvmService +} + +func NewCreateVolumeGroupAction(name string, physicalVolume string, ls service.LvmService) *CreateVolumeGroupAction { + return &CreateVolumeGroupAction{ + name: name, + physicalVolume: physicalVolume, + mode: model.Empty, + lvmService: ls, + } +} + +func (a *CreateVolumeGroupAction) Execute() error { + return a.lvmService.CreateVolumeGroup(a.name, a.physicalVolume) +} + +func (a *CreateVolumeGroupAction) GetMode() model.Mode { + return a.mode +} + +func (a *CreateVolumeGroupAction) SetMode(mode model.Mode) Action { + a.mode = mode + return a +} + +func (a *CreateVolumeGroupAction) Prompt() string { + return fmt.Sprintf("Would you like to create volume group %s on physical volume %s", a.name, a.physicalVolume) +} + +func (a *CreateVolumeGroupAction) Refuse() string { + return fmt.Sprintf("Refused to create volume group %s on physical volume %s", a.name, a.physicalVolume) +} + +func (a *CreateVolumeGroupAction) Success() string { + return fmt.Sprintf("Successfully created volume group %s on physical volume %s", a.name, a.physicalVolume) +} diff --git a/internal/backend/lvm.go b/internal/backend/lvm.go index 6945dbf..b8193fd 100644 --- a/internal/backend/lvm.go +++ b/internal/backend/lvm.go @@ -3,28 +3,79 @@ package backend import ( "github.com/reecetech/ebs-bootstrap/internal/action" "github.com/reecetech/ebs-bootstrap/internal/config" + datastructures "github.com/reecetech/ebs-bootstrap/internal/data_structures" + "github.com/reecetech/ebs-bootstrap/internal/model" "github.com/reecetech/ebs-bootstrap/internal/service" ) type LvmBackend interface { CreatePhysicalVolume(name string) action.Action + CreateVolumeGroup(name string, physicalVolume string) action.Action + GetVolumeGroup(name string) (*model.VolumeGroup, error) + GetPhysicalVolumes(vg *model.VolumeGroup) ([]*model.PhysicalVolume, error) + From(config *config.Config) error } type LinuxLvmBackend struct { + lvmGraph *datastructures.LvmGraph lvmService service.LvmService } func NewLinuxLvmBackend(ls service.LvmService) *LinuxLvmBackend { return &LinuxLvmBackend{ + lvmGraph: datastructures.NewLvmGraph(), lvmService: ls, } } +func (lb *LinuxLvmBackend) GetVolumeGroup(name string) (*model.VolumeGroup, error) { + node, err := lb.lvmGraph.GetVolumeGroup(name) + if err != nil { + return nil, err + } + return &model.VolumeGroup{ + Name: node.Name, + }, nil +} + +func (lb *LinuxLvmBackend) GetPhysicalVolumes(vg *model.VolumeGroup) ([]*model.PhysicalVolume, error) { + vgn, err := lb.lvmGraph.GetVolumeGroup(vg.Name) + if err != nil { + return nil, err + } + 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, + } + } + return physicalVolumes, nil +} + func (lb *LinuxLvmBackend) CreatePhysicalVolume(name string) action.Action { return action.NewCreatePhysicalVolumeAction(name, lb.lvmService) } -// TODO: Establish Graph Network +func (lb *LinuxLvmBackend) CreateVolumeGroup(name string, physicalVolume string) action.Action { + return action.NewCreateVolumeGroupAction(name, physicalVolume, lb.lvmService) +} + func (db *LinuxLvmBackend) From(config *config.Config) error { + pvs, err := db.lvmService.GetPhysicalVolumes() + if err != nil { + return err + } + for _, pv := range pvs { + db.lvmGraph.AddBlockDevice(pv.Name) + db.lvmGraph.AddPhysicalVolume(pv.Name) + } + vgs, err := db.lvmService.GetVolumeGroups() + if err != nil { + return err + } + for _, vg := range vgs { + db.lvmGraph.AddVolumeGroup(vg.Name, vg.PhysicalVolume) + } return nil } diff --git a/internal/data_structures/lvm_graph.go b/internal/data_structures/lvm_graph.go new file mode 100644 index 0000000..607cac9 --- /dev/null +++ b/internal/data_structures/lvm_graph.go @@ -0,0 +1,190 @@ +package datastructures + +import ( + "fmt" +) + +type LvmNodeType int + +const ( + BlockDevice LvmNodeType = 0 + PhysicalVolume LvmNodeType = 1 + VolumeGroup LvmNodeType = 2 + LogicalVolume LvmNodeType = 3 +) + +type LvmNode struct { + id string + Name string + Active bool + nodeType LvmNodeType + children []*LvmNode + parents []*LvmNode +} + +func NewBlockDevice(name string) *LvmNode { + return &LvmNode{ + id: fmt.Sprintf("device:%s", name), + Name: name, + Active: true, + nodeType: BlockDevice, + children: []*LvmNode{}, + parents: []*LvmNode{}, + } +} + +func NewPhysicalVolume(name string) *LvmNode { + return &LvmNode{ + id: fmt.Sprintf("pv:%s", name), + Name: name, + Active: true, + nodeType: PhysicalVolume, + children: []*LvmNode{}, + parents: []*LvmNode{}, + } +} + +func NewVolumeGroup(name string) *LvmNode { + return &LvmNode{ + 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 { + return &LvmNode{ + id: fmt.Sprintf("lv:%s:vg:%s", name, vg), + Name: name, + Active: active, + nodeType: LogicalVolume, + children: []*LvmNode{}, + parents: []*LvmNode{}, + } +} + +type LvmGraph struct { + nodes map[string]*LvmNode // A map that stores all the nodes by their Id +} + +func NewLvmGraph() *LvmGraph { + return &LvmGraph{ + nodes: map[string]*LvmNode{}, + } +} + +func (lg *LvmGraph) AddBlockDevice(name string) error { + bd := NewBlockDevice(name) + + _, found := lg.nodes[bd.id] + if found { + return fmt.Errorf("block device %s already exists", name) + } + + lg.nodes[bd.id] = bd + return nil +} + +func (lg *LvmGraph) AddPhysicalVolume(name string) error { + pv := NewPhysicalVolume(name) + + _, found := lg.nodes[pv.id] + if found { + return fmt.Errorf("physical volume %s 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) + } + + lg.nodes[pv.id] = pv + bdn.children = append(bdn.children, pv) + pv.parents = append(pv.parents, bdn) + return nil +} + +func (lg *LvmGraph) AddVolumeGroup(name string, pv string) error { + id := fmt.Sprintf("vg:%s", name) + + vg, found := lg.nodes[id] + if !found { + vg = NewVolumeGroup(name) + } + + pvId := fmt.Sprintf("pv:%s", pv) + pvn, found := lg.nodes[pvId] + if !found { + return fmt.Errorf("physical volume %s does not exist", pv) + } + + if len(pvn.children) > 0 { + return fmt.Errorf("%s 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) + return nil +} + +func (lg *LvmGraph) AddLogicalVolume(name string, vg string, active bool) error { + lv := NewLogicalVolume(name, vg, active) + + _, found := lg.nodes[lv.id] + if found { + return fmt.Errorf("logical volume %s already exists", name) + } + + vgId := fmt.Sprintf("vg:%s", vg) + vgn, found := lg.nodes[vgId] + if !found { + return fmt.Errorf("volume group %s does not exist", vg) + } + + lg.nodes[lv.id] = lv + vgn.children = append(vgn.children, lv) + lv.parents = append(lv.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.Active { + vgn.Active = true + break + } + } + return nil +} + +func (lg *LvmGraph) GetVolumeGroup(name string) (*LvmNode, error) { + id := fmt.Sprintf("vg:%s", name) + node, found := lg.nodes[id] + if !found { + return nil, fmt.Errorf("volume group %s does not exist", name) + } + return node, 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] + if !found { + return nil, fmt.Errorf("logical volume %s/%s does not exist", vg, name) + } + return node, nil +} + +func (lg *LvmGraph) GetParents(node *LvmNode, nodeType LvmNodeType) []*LvmNode { + parents := []*LvmNode{} + for _, p := range node.parents { + if p.nodeType == nodeType { + parents = append(parents, p) + } + } + return parents +} diff --git a/internal/layer/layer.go b/internal/layer/layer.go index ccff3d5..38702ba 100644 --- a/internal/layer/layer.go +++ b/internal/layer/layer.go @@ -96,7 +96,6 @@ func (le *ExponentialBackoffLayerExecutor) Execute(layers []Layer) error { return err } } - log.Println("🟢 Passed all validation checks") return nil } diff --git a/internal/layer/vg.go b/internal/layer/vg.go new file mode 100644 index 0000000..5948365 --- /dev/null +++ b/internal/layer/vg.go @@ -0,0 +1,67 @@ +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 CreateVolumeGroupLayer struct { + lvmBackend backend.LvmBackend +} + +func NewCreateVolumeGroupLayer(lb backend.LvmBackend) *CreateVolumeGroupLayer { + return &CreateVolumeGroupLayer{ + lvmBackend: lb, + } +} + +func (cvgl *CreateVolumeGroupLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, cd := range c.Devices { + if len(cd.Lvm) == 0 { + continue + } + + vg, err := cvgl.lvmBackend.GetVolumeGroup(cd.Lvm) + if err != nil { + mode := c.GetMode(name) + a := cvgl.lvmBackend.CreateVolumeGroup(cd.Lvm, name) + actions = append(actions, a.SetMode(mode)) + continue + } + + pvs, err := cvgl.lvmBackend.GetPhysicalVolumes(vg) + if err != nil { + return nil, err + } + + if len(pvs) > 1 { + return nil, fmt.Errorf("🔴 %s: Cannot manage a volume group %s with more than one physical volumes associated", name, cd.Lvm) + } + } + return actions, nil +} + +func (cvgl *CreateVolumeGroupLayer) Validate(c *config.Config) error { + for name, cd := range c.Devices { + 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) + } + } + return nil +} + +func (cvgl *CreateVolumeGroupLayer) Warning() string { + return DisabledWarning +} + +func (cvgl *CreateVolumeGroupLayer) From(c *config.Config) error { + return cvgl.lvmBackend.From(c) +} diff --git a/internal/model/lvm.go b/internal/model/lvm.go index 4fe4243..a8596d3 100644 --- a/internal/model/lvm.go +++ b/internal/model/lvm.go @@ -1,24 +1,10 @@ package model -type LvmDevice struct { - Name string - Child *PhysicalVolume -} - type PhysicalVolume struct { - Name string - Parent *LvmDevice - Child *VolumeGroup + Name string } type VolumeGroup struct { - Name string - Parent []*PhysicalVolume - Children []*LogicalVolume -} - -type LogicalVolume struct { - Name string - Parent *VolumeGroup - Active bool + Name string + PhysicalVolume string } diff --git a/internal/service/lvm.go b/internal/service/lvm.go index 58e82df..c0ba481 100644 --- a/internal/service/lvm.go +++ b/internal/service/lvm.go @@ -1,11 +1,18 @@ package service import ( + "encoding/json" + "fmt" + + "github.com/reecetech/ebs-bootstrap/internal/model" "github.com/reecetech/ebs-bootstrap/internal/utils" ) type LvmService interface { + GetPhysicalVolumes() ([]*model.PhysicalVolume, error) + GetVolumeGroups() ([]*model.VolumeGroup, error) CreatePhysicalVolume(name string) error + CreateVolumeGroup(name string, physicalVolume string) error } type LinuxLvmService struct { @@ -20,14 +27,70 @@ type PvsResponse struct { } `json:"report"` } +type VgsResponse struct { + Report []struct { + VolumeGroup []struct { + Name string `json:"vg_name"` + PhysicalVolume string `json:"pv_name"` + } `json:"vg"` + } `json:"report"` +} + func NewLinuxLvmService(rf utils.RunnerFactory) *LinuxLvmService { return &LinuxLvmService{ runnerFactory: rf, } } +func (ls *LinuxLvmService) GetPhysicalVolumes() ([]*model.PhysicalVolume, error) { + r := ls.runnerFactory.Select(utils.Pvs) + output, err := r.Command("-o", "pv_name", "--reportformat", "json") + 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.PhysicalVolume, len(pr.Report[0].PhysicalVolume)) + for i, pv := range pr.Report[0].PhysicalVolume { + pvs[i] = &model.PhysicalVolume{ + Name: pv.Name, + } + } + return pvs, nil +} + +func (ls *LinuxLvmService) GetVolumeGroups() ([]*model.VolumeGroup, error) { + r := ls.runnerFactory.Select(utils.Vgs) + output, err := r.Command("-o", "vg_name,pv_name", "--reportformat", "json") + if err != nil { + return nil, err + } + vr := &VgsResponse{} + err = json.Unmarshal([]byte(output), vr) + if err != nil { + return nil, fmt.Errorf("🔴 Failed to decode vgs response: %v", err) + } + vgs := make([]*model.VolumeGroup, len(vr.Report[0].VolumeGroup)) + for i, vg := range vr.Report[0].VolumeGroup { + vgs[i] = &model.VolumeGroup{ + Name: vg.Name, + PhysicalVolume: vg.PhysicalVolume, + } + } + return vgs, nil +} + func (ls *LinuxLvmService) CreatePhysicalVolume(name string) error { r := ls.runnerFactory.Select(utils.PvCreate) _, err := r.Command(name) return err } + +func (ls *LinuxLvmService) CreateVolumeGroup(name string, physicalVolume string) error { + r := ls.runnerFactory.Select(utils.VgCreate) + _, err := r.Command(name, physicalVolume) + return err +} diff --git a/internal/utils/exec.go b/internal/utils/exec.go index e5b3982..50fc22b 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -22,7 +22,10 @@ const ( XfsInfo Binary = "xfs_info" Resize2fs Binary = "resize2fs" XfsGrowfs Binary = "xfs_growfs" + Pvs Binary = "pvs" PvCreate Binary = "pvcreate" + Vgs Binary = "vgs" + VgCreate Binary = "vgcreate" ) type RunnerFactory interface {