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..9a36076 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: 20 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/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..de53671 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 } @@ -68,11 +70,11 @@ func (lb *LinuxLvmBackend) GetLogicalVolume(name string, volumeGroup string) (*m }, 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) if err != nil { - return lvs + return nil, err } lvn := lb.lvmGraph.GetChildren(node, datastructures.LogicalVolume) for _, lv := range lvn { @@ -83,23 +85,23 @@ func (lb *LinuxLvmBackend) SearchLogicalVolumes(volumeGroup string) []*model.Log Size: lv.Size, }) } - return lvs + return lvs, nil } -func (lb *LinuxLvmBackend) SearchVolumeGroup(physicalVolume string) *model.VolumeGroup { +func (lb *LinuxLvmBackend) SearchVolumeGroup(physicalVolume string) (*model.VolumeGroup, error) { node, err := lb.lvmGraph.GetPhysicalVolume(physicalVolume) if err != nil { - return nil + return nil, err } vgn := lb.lvmGraph.GetChildren(node, datastructures.VolumeGroup) 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, Size: vgn[0].Size, - } + }, nil } func (lb *LinuxLvmBackend) CreatePhysicalVolume(name string) action.Action { @@ -110,58 +112,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 { +func (lb *LinuxLvmBackend) ShouldResizePhysicalVolume(name string, threshold float64) (bool, error) { node, 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 + return false, nil } - return (float64(node.Size) / float64(dvn[0].Size) * 100) < threshold + return (float64(node.Size) / float64(dvn[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 + node, err := lb.lvmGraph.GetLogicalVolume(name, volumeGroup) + if err != nil { + return false, err + } + vgn := lb.lvmGraph.GetParents(node, datastructures.VolumeGroup) + if len(vgn) == 0 { + return false, fmt.Errorf("🔴 %s: Logical volume has no volume group", name) + } + usedPerecent := (float64(node.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, datastructures.LvmNodeState(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..c1b3cb6 100644 --- a/internal/datastructures/lvm_graph.go +++ b/internal/datastructures/lvm_graph.go @@ -220,3 +220,7 @@ func (lg *LvmGraph) GetChildren(node *LvmNode, state LvmNodeCategory) []*LvmNode } return children } + +func (lg *LvmGraph) Clear() { + lg.nodes = map[string]*LvmNode{} +} 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_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/service/lvm.go b/internal/service/lvm.go index ffcb362..6beae5c 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 { @@ -186,9 +187,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 +204,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 {