diff --git a/cmd/ebs-bootstrap.go b/cmd/ebs-bootstrap.go index 65149dc..cb03eba 100644 --- a/cmd/ebs-bootstrap.go +++ b/cmd/ebs-bootstrap.go @@ -21,6 +21,7 @@ func main() { lds := service.NewLinuxDeviceService(rc) uos := service.NewUnixOwnerService() ans := service.NewAwsNitroNVMeService() + fssf := service.NewLinuxFileSystemServiceFactory(rc) // Warnings warnings(uos) @@ -30,9 +31,10 @@ func main() { checkError(err) // Service + Config Consumers - db := backend.NewLinuxDeviceBackend(lds) + db := backend.NewLinuxDeviceBackend(lds, fssf) fb := backend.NewLinuxFileBackend(ufs) ub := backend.NewLinuxOwnerBackend(uos) + drb := backend.NewLinuxDeviceResizeBackend(lds, fssf) ae := action.NewActionExecutor(rc, c) // Modify Config @@ -59,6 +61,7 @@ func main() { layer.NewUnmountDeviceLayer(db, fb), layer.NewCreateDirectoryLayer(db, fb), layer.NewMountDeviceLayer(db, fb), + layer.NewResizeDeviceLayer(drb), layer.NewChangeOwnerLayer(ub, fb), layer.NewChangePermissionsLayer(fb), }) diff --git a/internal/action/action.go b/internal/action/action.go index dc8f074..a9f15ef 100644 --- a/internal/action/action.go +++ b/internal/action/action.go @@ -28,8 +28,7 @@ const ( ) type Action interface { - Execute(rc *utils.RunnerCache) error - Preflight(rc *utils.RunnerCache) error + Execute() error GetDeviceName() string GetDelay() time.Duration Success() string @@ -56,10 +55,6 @@ func (ae *ActionExecutor) ExecuteAction(action Action) error { if err != nil { return err } - err = action.Preflight(ae.runnerCache) - if err != nil { - return err - } switch mode { case config.Prompt: if !ae.ShouldProceed(action) { @@ -75,7 +70,7 @@ func (ae *ActionExecutor) executeAction(action Action) error { if w := action.Warning(); w != DisabledWarning { log.Printf("🟠 %s: %s", action.GetDeviceName(), w) } - if err := action.Execute(ae.runnerCache); err != nil { + if err := action.Execute(); err != nil { return err } log.Printf("⭐ %s: %s", action.GetDeviceName(), action.Success()) diff --git a/internal/action/file.go b/internal/action/file.go index 3fedf8c..8e1e0a7 100644 --- a/internal/action/file.go +++ b/internal/action/file.go @@ -25,14 +25,10 @@ func NewCreateDirectoryAction(dn string, p string) *CreateDirectoryAction { } } -func (a *CreateDirectoryAction) Execute(rc *utils.RunnerCache) error { +func (a *CreateDirectoryAction) Execute() error { return os.MkdirAll(a.path, DefaultDirectoryPermissions) } -func (a *CreateDirectoryAction) Preflight(rc *utils.RunnerCache) error { - return nil -} - func (a *CreateDirectoryAction) GetDeviceName() string { return a.deviceName } @@ -73,14 +69,10 @@ func NewChangeOwnerAction(dn string, p string, uid int, gid int) *ChangeOwnerAct } } -func (a *ChangeOwnerAction) Execute(rc *utils.RunnerCache) error { +func (a *ChangeOwnerAction) Execute() error { return os.Chown(a.path, a.uid, a.gid) } -func (a *ChangeOwnerAction) Preflight(rc *utils.RunnerCache) error { - return nil -} - func (a *ChangeOwnerAction) GetDeviceName() string { return a.deviceName } @@ -119,7 +111,7 @@ func NewChangePermissions(dn string, p string, perms model.Permissions) *ChangeP } } -func (a *ChangePermissionsAction) Execute(rc *utils.RunnerCache) error { +func (a *ChangePermissionsAction) Execute() error { mode, err := a.perms.ToFileMode() if err != nil { return err diff --git a/internal/action/format.go b/internal/action/format.go index 7f763d1..3cd6394 100644 --- a/internal/action/format.go +++ b/internal/action/format.go @@ -4,99 +4,45 @@ import ( "fmt" "time" - "github.com/reecetech/ebs-bootstrap/internal/utils" + "github.com/reecetech/ebs-bootstrap/internal/service" ) -type FormatExt4Action struct { - deviceName string +type FormatDeviceAction struct { + deviceName string + fileSystemService service.FileSystemService } -func NewFormatExt4Action(dn string) *FormatExt4Action { - return &FormatExt4Action{ - deviceName: dn, +func NewFormatDeviceAction(dn string, fileSystemService service.FileSystemService) *FormatDeviceAction { + return &FormatDeviceAction{ + deviceName: dn, + fileSystemService: fileSystemService, } } -func (a *FormatExt4Action) Execute(rc *utils.RunnerCache) error { - r := rc.GetRunner(utils.MkfsExt4) - _, err := r.Command(a.deviceName) - if err != nil { - return err - } - return nil -} - -func (a *FormatExt4Action) Preflight(rc *utils.RunnerCache) error { - return nil +func (a *FormatDeviceAction) Execute() error { + return a.fileSystemService.Format(a.deviceName) } -func (a *FormatExt4Action) GetDeviceName() string { +func (a *FormatDeviceAction) GetDeviceName() string { return a.deviceName } -func (a *FormatExt4Action) GetDelay() time.Duration { +func (a *FormatDeviceAction) GetDelay() time.Duration { return DeviceActionDelay } -func (a *FormatExt4Action) Prompt() string { - return fmt.Sprintf("Would you like to format %s to ext4", a.deviceName) -} - -func (a *FormatExt4Action) Refuse() string { - return "Refused to format to ext4" -} - -func (a *FormatExt4Action) Success() string { - return "Successfully formated to ext4" -} - -func (a *FormatExt4Action) Warning() string { - return FormatActionWarning -} - -type FormatXfsAction struct { - deviceName string -} - -func NewFormatXfsAction(dn string) *FormatXfsAction { - return &FormatXfsAction{ - deviceName: dn, - } -} - -func (a *FormatXfsAction) Execute(rc *utils.RunnerCache) error { - r := rc.GetRunner(utils.MkfsXfs) - _, err := r.Command(a.deviceName) - if err != nil { - return err - } - return nil -} - -func (a *FormatXfsAction) Preflight(rc *utils.RunnerCache) error { - return nil -} - -func (a *FormatXfsAction) GetDeviceName() string { - return a.deviceName -} - -func (a *FormatXfsAction) Prompt() string { - return fmt.Sprintf("Would you like to format %s to XFS?", a.deviceName) -} - -func (a *FormatXfsAction) GetDelay() time.Duration { - return DeviceActionDelay +func (a *FormatDeviceAction) Prompt() string { + return fmt.Sprintf("Would you like to format %s to %s", a.deviceName, a.fileSystemService.GetFileSystem()) } -func (a *FormatXfsAction) Refuse() string { - return "Refused to format to XFS" +func (a *FormatDeviceAction) Refuse() string { + return fmt.Sprintf("Refused to format to %s", a.fileSystemService.GetFileSystem()) } -func (a *FormatXfsAction) Success() string { - return "Successfully formated to XFS" +func (a *FormatDeviceAction) Success() string { + return fmt.Sprintf("Successfully formated to %s", a.fileSystemService.GetFileSystem()) } -func (a *FormatXfsAction) Warning() string { +func (a *FormatDeviceAction) Warning() string { return FormatActionWarning } diff --git a/internal/action/label.go b/internal/action/label.go index 8e60549..7c3f3ce 100644 --- a/internal/action/label.go +++ b/internal/action/label.go @@ -4,109 +4,47 @@ import ( "fmt" "time" - "github.com/reecetech/ebs-bootstrap/internal/utils" + "github.com/reecetech/ebs-bootstrap/internal/service" ) -type LabelExt4Action struct { - deviceName string - label string +type LabelDeviceAction struct { + deviceName string + label string + fileSystemService service.FileSystemService } -func NewLabelExt4Action(dn string, label string) *LabelExt4Action { - return &LabelExt4Action{ - deviceName: dn, - label: label, +func NewLabelDeviceAction(dn string, label string, fileSystemService service.FileSystemService) *LabelDeviceAction { + return &LabelDeviceAction{ + deviceName: dn, + label: label, + fileSystemService: fileSystemService, } } -func (a *LabelExt4Action) Execute(rc *utils.RunnerCache) error { - r := rc.GetRunner(utils.E2Label) - _, err := r.Command(a.deviceName, a.label) - if err != nil { - return err - } - return nil -} - -func (a *LabelExt4Action) Preflight(rc *utils.RunnerCache) error { - if len(a.label) > 16 { - return fmt.Errorf("🔴 %s: Label cannot exceed 16 characters for the ext4 file system", a.label) - } - return nil -} - -func (a *LabelExt4Action) GetDeviceName() string { - return a.deviceName -} - -func (a *LabelExt4Action) GetDelay() time.Duration { - return DeviceActionDelay -} - -func (a *LabelExt4Action) Prompt() string { - return fmt.Sprintf("Would you like to label device %s to %s", a.deviceName, a.label) -} - -func (a *LabelExt4Action) Refuse() string { - return fmt.Sprintf("Refused to label to %s", a.label) -} - -func (a *LabelExt4Action) Success() string { - return fmt.Sprintf("Successfully labelled to %s", a.label) -} - -func (a *LabelExt4Action) Warning() string { - return DisabledWarning -} - -type LabelXfsAction struct { - deviceName string - label string -} - -func NewLabelXfsAction(dn string, label string) *LabelXfsAction { - return &LabelXfsAction{ - deviceName: dn, - label: label, - } -} - -func (a *LabelXfsAction) Execute(rc *utils.RunnerCache) error { - r := rc.GetRunner(utils.XfsAdmin) - _, err := r.Command("-L", a.label, a.deviceName) - if err != nil { - return err - } - return nil -} - -func (a *LabelXfsAction) Preflight(rc *utils.RunnerCache) error { - if len(a.label) > 12 { - return fmt.Errorf("🔴 %s: Label cannot exceed 12 characters for the XFS file system", a.label) - } - return nil +func (a *LabelDeviceAction) Execute() error { + return a.fileSystemService.Label(a.deviceName, a.label) } -func (a *LabelXfsAction) GetDeviceName() string { +func (a *LabelDeviceAction) GetDeviceName() string { return a.deviceName } -func (a *LabelXfsAction) GetDelay() time.Duration { +func (a *LabelDeviceAction) GetDelay() time.Duration { return DeviceActionDelay } -func (a *LabelXfsAction) Prompt() string { +func (a *LabelDeviceAction) Prompt() string { return fmt.Sprintf("Would you like to label device %s to %s", a.deviceName, a.label) } -func (a *LabelXfsAction) Refuse() string { +func (a *LabelDeviceAction) Refuse() string { return fmt.Sprintf("Refused to label to %s", a.label) } -func (a *LabelXfsAction) Success() string { +func (a *LabelDeviceAction) Success() string { return fmt.Sprintf("Successfully labelled to %s", a.label) } -func (a *LabelXfsAction) Warning() string { +func (a *LabelDeviceAction) Warning() string { return DisabledWarning } diff --git a/internal/action/mount.go b/internal/action/mount.go index f2b0706..d5d98a3 100644 --- a/internal/action/mount.go +++ b/internal/action/mount.go @@ -5,36 +5,30 @@ import ( "time" "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/service" "github.com/reecetech/ebs-bootstrap/internal/utils" ) type MountDeviceAction struct { - source string - target string - fileSystem model.FileSystem - options model.MountOptions + source string + target string + fileSystem model.FileSystem + options model.MountOptions + deviceService service.DeviceService } -func NewMountDeviceAction(source string, target string, fs model.FileSystem, options model.MountOptions) *MountDeviceAction { +func NewMountDeviceAction(source string, target string, fileSystem model.FileSystem, options model.MountOptions, deviceService service.DeviceService) *MountDeviceAction { return &MountDeviceAction{ - source: source, - target: target, - fileSystem: fs, - options: options.Remount(false), + source: source, + target: target, + fileSystem: fileSystem, + options: options, + deviceService: deviceService, } } -func (a *MountDeviceAction) Execute(rc *utils.RunnerCache) error { - r := rc.GetRunner(utils.Mount) - _, err := r.Command(a.source, "-t", string(a.fileSystem), "-o", string(a.options), a.target) - if err != nil { - return err - } - return nil -} - -func (a *MountDeviceAction) Preflight(rc *utils.RunnerCache) error { - return nil +func (a *MountDeviceAction) Execute() error { + return a.deviceService.Mount(a.source, a.target, a.fileSystem, a.options) } func (a *MountDeviceAction) GetDeviceName() string { @@ -61,78 +55,22 @@ func (a *MountDeviceAction) Warning() string { return DisabledWarning } -type RemountDeviceAction struct { - source string - target string - fileSystem model.FileSystem - options model.MountOptions -} - -func NewRemountDeviceAction(source string, target string, fs model.FileSystem, options model.MountOptions) *RemountDeviceAction { - return &RemountDeviceAction{ - source: source, - target: target, - fileSystem: fs, - options: options.Remount(true), - } -} - -func (a *RemountDeviceAction) Execute(rc *utils.RunnerCache) error { - r := rc.GetRunner(utils.Mount) - _, err := r.Command(a.source, "-t", string(a.fileSystem), "-o", string(a.options), a.target) - if err != nil { - return err - } - return nil -} - -func (a *RemountDeviceAction) Preflight(rc *utils.RunnerCache) error { - return nil -} - -func (a *RemountDeviceAction) GetDeviceName() string { - return a.source -} - -func (a *RemountDeviceAction) GetDelay() time.Duration { - return DeviceActionDelay -} - -func (a *RemountDeviceAction) Prompt() string { - return fmt.Sprintf("Would you like to remount %s to %s (%s)", a.source, a.target, a.options) -} - -func (a *RemountDeviceAction) Refuse() string { - return fmt.Sprintf("Refused to remount %s to %s (%s)", a.source, a.target, a.options) -} - -func (a *RemountDeviceAction) Success() string { - return fmt.Sprintf("Successfully remounted %s to %s (%s)", a.source, a.target, a.options) -} - -func (a *RemountDeviceAction) Warning() string { - return DisabledWarning -} - type UnmountDeviceAction struct { - source string - target string + source string + target string + deviceService service.DeviceService } -func NewUnmountDeviceAction(source string, target string) *UnmountDeviceAction { +func NewUnmountDeviceAction(source string, target string, deviceService service.DeviceService) *UnmountDeviceAction { return &UnmountDeviceAction{ - source: source, - target: target, + source: source, + target: target, + deviceService: deviceService, } } -func (a *UnmountDeviceAction) Execute(rc *utils.RunnerCache) error { - r := rc.GetRunner(utils.Umount) - _, err := r.Command(a.target) - if err != nil { - return err - } - return nil +func (a *UnmountDeviceAction) Execute() error { + return a.deviceService.Umount(a.source, a.target) } func (a *UnmountDeviceAction) Preflight(rc *utils.RunnerCache) error { diff --git a/internal/action/resize.go b/internal/action/resize.go new file mode 100644 index 0000000..a9c8784 --- /dev/null +++ b/internal/action/resize.go @@ -0,0 +1,50 @@ +package action + +import ( + "fmt" + "time" + + "github.com/reecetech/ebs-bootstrap/internal/service" +) + +type ResizeDeviceAction struct { + deviceName string + target string + fileSystemService service.FileSystemService +} + +func NewResizeDeviceAction(dn string, target string, fileSystemService service.FileSystemService) *ResizeDeviceAction { + return &ResizeDeviceAction{ + deviceName: dn, + target: target, + fileSystemService: fileSystemService, + } +} + +func (a *ResizeDeviceAction) Execute() error { + return a.fileSystemService.Resize(a.target) +} + +func (a *ResizeDeviceAction) GetDeviceName() string { + return a.deviceName +} + +func (a *ResizeDeviceAction) GetDelay() time.Duration { + return DeviceActionDelay +} + +func (a *ResizeDeviceAction) Prompt() string { + return fmt.Sprintf("Would you like to resize the %s file system of %s", a.fileSystemService.GetFileSystem(), a.deviceName) +} + +func (a *ResizeDeviceAction) Refuse() string { + return fmt.Sprintf("Refused to resize the %s file system of %s", a.fileSystemService.GetFileSystem(), a.deviceName) +} + +func (a *ResizeDeviceAction) Success() string { + return fmt.Sprintf("Successfully resized the %s file system of %s", a.fileSystemService.GetFileSystem(), a.deviceName) +} + +func (a *ResizeDeviceAction) Warning() string { + return "" +} diff --git a/internal/backend/device.go b/internal/backend/device.go index 9b988b4..cfcbf41 100644 --- a/internal/backend/device.go +++ b/internal/backend/device.go @@ -20,14 +20,16 @@ type DeviceBackend interface { } type LinuxDeviceBackend struct { - blockDevices map[string]*model.BlockDevice - deviceService service.DeviceService + blockDevices map[string]*model.BlockDevice + deviceService service.DeviceService + fileSystemServiceFactory service.FileSystemServiceFactory } -func NewLinuxDeviceBackend(ds service.DeviceService) *LinuxDeviceBackend { +func NewLinuxDeviceBackend(ds service.DeviceService, fssf service.FileSystemServiceFactory) *LinuxDeviceBackend { return &LinuxDeviceBackend{ - blockDevices: map[string]*model.BlockDevice{}, - deviceService: ds, + blockDevices: map[string]*model.BlockDevice{}, + deviceService: ds, + fileSystemServiceFactory: fssf, } } @@ -40,37 +42,38 @@ func (db *LinuxDeviceBackend) GetBlockDevice(device string) (*model.BlockDevice, } func (db *LinuxDeviceBackend) Label(bd *model.BlockDevice, label string) (action.Action, error) { - switch bd.FileSystem { - case model.Ext4: - return action.NewLabelExt4Action(bd.Name, label), nil - case model.Xfs: - return action.NewLabelXfsAction(bd.Name, label), nil - default: - return nil, fmt.Errorf("🔴 %s: Can not label a device with no file system", bd.Name) + fs, err := db.fileSystemServiceFactory.Select(bd.FileSystem) + if err != nil { + return nil, err } + return action.NewLabelDeviceAction( + bd.Name, + label, + fs, + ), nil } -func (db *LinuxDeviceBackend) Format(bd *model.BlockDevice, fs model.FileSystem) (action.Action, error) { - switch fs { - case model.Ext4: - return action.NewFormatExt4Action(bd.Name), nil - case model.Xfs: - return action.NewFormatXfsAction(bd.Name), nil - default: - return nil, fmt.Errorf("🔴 %s: Can not erase the file system of a device", bd.Name) +func (db *LinuxDeviceBackend) Format(bd *model.BlockDevice, fileSystem model.FileSystem) (action.Action, error) { + fs, err := db.fileSystemServiceFactory.Select(fileSystem) + if err != nil { + return nil, err } + return action.NewFormatDeviceAction( + bd.Name, + fs, + ), nil } func (db *LinuxDeviceBackend) Mount(bd *model.BlockDevice, target string, options model.MountOptions) action.Action { - return action.NewMountDeviceAction(bd.Name, target, bd.FileSystem, options) + return action.NewMountDeviceAction(bd.Name, target, bd.FileSystem, options, db.deviceService) } func (db *LinuxDeviceBackend) Remount(bd *model.BlockDevice, target string, options model.MountOptions) action.Action { - return action.NewRemountDeviceAction(bd.Name, target, bd.FileSystem, options) + return db.Mount(bd, target, options.Remount(true)) } func (db *LinuxDeviceBackend) Umount(bd *model.BlockDevice) action.Action { - return action.NewUnmountDeviceAction(bd.Name, bd.MountPoint) + return action.NewUnmountDeviceAction(bd.Name, bd.MountPoint, db.deviceService) } func (db *LinuxDeviceBackend) From(config *config.Config) error { diff --git a/internal/backend/resize.go b/internal/backend/resize.go new file mode 100644 index 0000000..8908ac9 --- /dev/null +++ b/internal/backend/resize.go @@ -0,0 +1,110 @@ +package backend + +import ( + "fmt" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/config" + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/service" +) + +type BlockDeviceMetrics struct { + FileSystemSize int + BlockDeviceSize int +} + +type DeviceResizeBackend interface { + GetBlockDevice(device string) (*model.BlockDevice, error) + GetBlockDeviceMetrics(name string) (*BlockDeviceMetrics, error) + Resize(bd *model.BlockDevice) (action.Action, error) + From(config *config.Config) error +} + +type LinuxDeviceResizeBackend struct { + blockDevices map[string]*model.BlockDevice + blockDeviceMetrics map[string]*BlockDeviceMetrics + deviceService service.DeviceService + fileSystemServiceFactory service.FileSystemServiceFactory +} + +func NewLinuxDeviceResizeBackend(ds service.DeviceService, fssf service.FileSystemServiceFactory) *LinuxDeviceResizeBackend { + return &LinuxDeviceResizeBackend{ + blockDevices: map[string]*model.BlockDevice{}, + blockDeviceMetrics: map[string]*BlockDeviceMetrics{}, + deviceService: ds, + fileSystemServiceFactory: fssf, + } +} + +func (db *LinuxDeviceResizeBackend) GetBlockDevice(device string) (*model.BlockDevice, error) { + blockDevice, exists := db.blockDevices[device] + if !exists { + return nil, fmt.Errorf("🔴 %s: Could not find block device", device) + } + return blockDevice, nil +} + +func (dmb *LinuxDeviceResizeBackend) GetBlockDeviceMetrics(name string) (*BlockDeviceMetrics, error) { + metrics, exists := dmb.blockDeviceMetrics[name] + if !exists { + return nil, fmt.Errorf("🔴 %s: Could not find block device metrics", name) + } + return metrics, nil +} + +func (dmb *LinuxDeviceResizeBackend) Resize(bd *model.BlockDevice) (action.Action, error) { + fss, err := dmb.fileSystemServiceFactory.Select(bd.FileSystem) + if err != nil { + return nil, err + } + target := bd.Name + if fss.ResizeRequiresMount() { + if bd.MountPoint == "" { + return nil, fmt.Errorf("🔴 %s: To resize the %s file system, device must be mounted", fss.GetFileSystem(), bd.Name) + } + target = bd.MountPoint + } + return action.NewResizeDeviceAction( + bd.Name, + target, + fss, + ), nil +} + +func (dmb *LinuxDeviceResizeBackend) From(config *config.Config) error { + // Clear in memory representation of metrics and devices + for k := range dmb.blockDeviceMetrics { + delete(dmb.blockDeviceMetrics, k) + } + for k := range dmb.blockDevices { + delete(dmb.blockDevices, k) + } + + for name := range config.Devices { + bd, err := dmb.deviceService.GetBlockDevice(name) + if err != nil { + return err + } + fs, err := dmb.fileSystemServiceFactory.Select(bd.FileSystem) + if err != nil { + return err + } + // Block Device Size + bss, err := dmb.deviceService.GetSize(bd.Name) + if err != nil { + return err + } + // File System Size + fss, err := fs.GetSize(bd.Name) + if err != nil { + return err + } + dmb.blockDevices[bd.Name] = bd + dmb.blockDeviceMetrics[bd.Name] = &BlockDeviceMetrics{ + BlockDeviceSize: bss, + FileSystemSize: fss, + } + } + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go index 8fd212a..78272eb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -39,9 +39,10 @@ func ParseMode(s string) (Mode, error) { } type Flag struct { - Config string - Mode string - Remount bool + Config string + Mode string + Remount bool + ResizeFs bool } type Device struct { @@ -54,16 +55,19 @@ type Device struct { Permissions model.Permissions `yaml:"permissions"` Mode Mode `yaml:"mode"` Remount bool `yaml:"remount"` + ResizeFs bool `yaml:"resizeFs"` } type Defaults struct { - Mode Mode `yaml:"mode"` - Remount bool `yaml:"remount"` + Mode Mode `yaml:"mode"` + Remount bool `yaml:"remount"` + ResizeFs bool `yaml:"resizeFs"` } type Overrides struct { - Mode Mode `yaml:"mode"` - Remount bool `yaml:"remount"` + Mode Mode `yaml:"mode"` + Remount bool `yaml:"remount"` + ResizeFs bool `yaml:"resizeFs"` } type Config struct { @@ -113,6 +117,7 @@ func parseFlags(program string, args []string) (*Flag, error) { flags.StringVar(&flag.Config, "config", "/etc/ebs-bootstrap/config.yml", "path to config file") flags.StringVar(&flag.Mode, "mode", "", "override for mode") flags.BoolVar(&flag.Remount, "remount", false, "override for remount") + flags.BoolVar(&flag.ResizeFs, "resize-fs", false, "override for resize filesystem") // Actually parse the flag err := flags.Parse(args) @@ -147,3 +152,11 @@ func (c *Config) GetRemount(name string) bool { } return c.Overrides.Remount || c.Defaults.Remount || cd.Remount } + +func (c *Config) GetResizeFs(name string) bool { + cd, found := c.Devices[name] + if !found { + return false + } + return c.Overrides.ResizeFs || c.Defaults.ResizeFs || cd.ResizeFs +} diff --git a/internal/config/modifier.go b/internal/config/modifier.go index 551e1ca..70d3a9c 100644 --- a/internal/config/modifier.go +++ b/internal/config/modifier.go @@ -48,6 +48,7 @@ func (om *OverridesModifier) Modify(c *Config) error { } c.Overrides.Mode = mode c.Overrides.Remount = om.flag.Remount + c.Overrides.ResizeFs = om.flag.ResizeFs return nil } diff --git a/internal/layer/resize.go b/internal/layer/resize.go new file mode 100644 index 0000000..069bfed --- /dev/null +++ b/internal/layer/resize.go @@ -0,0 +1,66 @@ +package layer + +import ( + "fmt" + "log" + + "github.com/reecetech/ebs-bootstrap/internal/action" + "github.com/reecetech/ebs-bootstrap/internal/backend" + "github.com/reecetech/ebs-bootstrap/internal/config" +) + +type ResizeDeviceLayer struct { + deviceResizeBackend backend.DeviceResizeBackend +} + +func NewResizeDeviceLayer(dmb backend.DeviceResizeBackend) *ResizeDeviceLayer { + return &ResizeDeviceLayer{ + deviceResizeBackend: dmb, + } +} + +func (fdl *ResizeDeviceLayer) From(c *config.Config) error { + return fdl.deviceResizeBackend.From(c) +} + +func (fdl *ResizeDeviceLayer) Modify(c *config.Config) ([]action.Action, error) { + actions := make([]action.Action, 0) + for name, _ := range c.Devices { + if !c.GetResizeFs(name) { + continue + } + bd, err := fdl.deviceResizeBackend.GetBlockDevice(name) + if err != nil { + return nil, err + } + metrics, err := fdl.deviceResizeBackend.GetBlockDeviceMetrics(name) + if err != nil { + return nil, err + } + if metrics.FileSystemSize < metrics.BlockDeviceSize { + action, err := fdl.deviceResizeBackend.Resize(bd) + if err != nil { + return nil, err + } + actions = append(actions, action) + } + } + return actions, nil +} + +func (fdl *ResizeDeviceLayer) Validate(c *config.Config) error { + for name, _ := range c.Devices { + if !c.GetResizeFs(name) { + continue + } + metrics, err := fdl.deviceResizeBackend.GetBlockDeviceMetrics(name) + if err != nil { + return err + } + if metrics.FileSystemSize < metrics.BlockDeviceSize { + return fmt.Errorf("🔴 %s: Failed to resize file system. File System=%d Block Device=%d (bytes)", name, metrics.FileSystemSize, metrics.BlockDeviceSize) + } + } + log.Println("🟢 Passed file system resize checks") + return nil +} diff --git a/internal/service/device.go b/internal/service/device.go index f462c88..c9cc366 100644 --- a/internal/service/device.go +++ b/internal/service/device.go @@ -3,6 +3,7 @@ package service import ( "encoding/json" "fmt" + "strconv" "github.com/reecetech/ebs-bootstrap/internal/model" "github.com/reecetech/ebs-bootstrap/internal/utils" @@ -11,8 +12,11 @@ import ( // Device Service Interface [START] type DeviceService interface { + GetSize(name string) (int, error) // bytes GetBlockDevices() ([]string, error) GetBlockDevice(name string) (*model.BlockDevice, error) + Mount(source string, target string, fs model.FileSystem, options model.MountOptions) error + Umount(source string, target string) error } // Device Service Interface [END] @@ -39,6 +43,19 @@ func NewLinuxDeviceService(rc *utils.RunnerCache) *LinuxDeviceService { } } +func (du *LinuxDeviceService) GetSize(name string) (int, error) { + r := du.runnerCache.GetRunner(utils.BlockDev) + output, err := r.Command("--getsize64", name) + if err != nil { + return -1, err + } + b, err := strconv.Atoi(output) + if err != nil { + return 0, nil + } + return b, nil +} + func (du *LinuxDeviceService) GetBlockDevices() ([]string, error) { r := du.runnerCache.GetRunner(utils.Lsblk) output, err := r.Command("--nodeps", "-o", "NAME,LABEL,FSTYPE,MOUNTPOINT", "-J") @@ -83,3 +100,21 @@ func (du *LinuxDeviceService) GetBlockDevice(name string) (*model.BlockDevice, e MountPoint: utils.SafeString(lbd.BlockDevices[0].MountPoint), }, nil } + +func (du *LinuxDeviceService) Mount(source string, target string, fs model.FileSystem, options model.MountOptions) error { + r := du.runnerCache.GetRunner(utils.Mount) + _, err := r.Command(source, "-t", string(fs), "-o", options.String(), target) + if err != nil { + return err + } + return nil +} + +func (du *LinuxDeviceService) Umount(source string, target string) error { + r := du.runnerCache.GetRunner(utils.Umount) + _, err := r.Command(target) + if err != nil { + return err + } + return nil +} diff --git a/internal/service/filesystem.go b/internal/service/filesystem.go new file mode 100644 index 0000000..8714cd5 --- /dev/null +++ b/internal/service/filesystem.go @@ -0,0 +1,204 @@ +package service + +import ( + "fmt" + "regexp" + "strconv" + + "github.com/reecetech/ebs-bootstrap/internal/model" + "github.com/reecetech/ebs-bootstrap/internal/utils" +) + +type FileSystemService interface { + GetSize(name string) (int, error) // (bytes) + GetFileSystem() model.FileSystem + Format(name string) error + Label(name string, label string) error + Resize(name string) error + ResizeRequiresMount() bool +} + +type FileSystemServiceFactory interface { + Select(fs model.FileSystem) (FileSystemService, error) +} + +type LinuxFileSystemServiceFactory struct { + services map[model.FileSystem]FileSystemService +} + +func NewLinuxFileSystemServiceFactory(rc *utils.RunnerCache) *LinuxFileSystemServiceFactory { + return &LinuxFileSystemServiceFactory{ + services: map[model.FileSystem]FileSystemService{ + model.Ext4: NewExt4Service(rc), + model.Xfs: NewXfsService(rc), + }, + } +} + +func (fsf *LinuxFileSystemServiceFactory) Select(fs model.FileSystem) (FileSystemService, error) { + fss, exists := fsf.services[fs] + if !exists { + return nil, fmt.Errorf("🔴 An unsupported filesystem was provided") + } + return fss, nil +} + +type Ext4Service struct { + runnerCache *utils.RunnerCache +} + +func NewExt4Service(rc *utils.RunnerCache) *Ext4Service { + return &Ext4Service{runnerCache: rc} +} + +func (es *Ext4Service) GetFileSystem() model.FileSystem { + return model.Ext4 +} + +func (es *Ext4Service) Format(name string) error { + r := es.runnerCache.GetRunner(utils.MkfsExt4) + _, err := r.Command(name) + if err != nil { + return err + } + return nil +} + +func (es *Ext4Service) Label(name string, label string) error { + if len(label) > 16 { + return fmt.Errorf("🔴 %s: Label '%s'cannot exceed 16 characters for the ext4 file system", name, label) + } + r := es.runnerCache.GetRunner(utils.E2Label) + _, err := r.Command(name, label) + if err != nil { + return err + } + return nil +} + +func (es *Ext4Service) Resize(name string) error { + r := es.runnerCache.GetRunner(utils.Resize2fs) + _, err := r.Command(name) + if err != nil { + return err + } + return nil +} + +func (es *Ext4Service) GetSize(name string) (int, error) { + r := es.runnerCache.GetRunner(utils.Tune2fs) + output, err := r.Command("-l", name) + if err != nil { + return 0, err + } + // Regex (Block Size) + rebs := regexp.MustCompile(`Block size:\s+(\d+)`) + // Match (Block Size) + mbs := rebs.FindStringSubmatch(output) + if len(mbs) != 2 { + return 0, fmt.Errorf("🔴 %s: Block size not found tune2fs output", name) + } + // String (Block Size) + sbs := mbs[1] + // Block Size + bs, err := strconv.Atoi(sbs) + if err != nil { + return 0, fmt.Errorf("🔴 Failed to cast block size to integer") + } + + // Regex (Block Count) + rebc := regexp.MustCompile(`Block count:\s+(\d+)`) + // Match (Block Count) + mbc := rebc.FindStringSubmatch(output) + if len(mbs) != 2 { + return 0, fmt.Errorf("🔴 %s: Block count not found tune2fs output", name) + } + // String (Block Count) + sbc := mbc[1] + // Block Count + bc, err := strconv.Atoi(sbc) + if err != nil { + return 0, fmt.Errorf("🔴 Failed to cast block size to integer") + } + return bs * bc, nil +} + +func (es *Ext4Service) ResizeRequiresMount() bool { + return false +} + +type XfsService struct { + runnerCache *utils.RunnerCache +} + +func NewXfsService(rc *utils.RunnerCache) *XfsService { + return &XfsService{runnerCache: rc} +} + +func (es *XfsService) GetFileSystem() model.FileSystem { + return model.Xfs +} + +func (xs *XfsService) Format(name string) error { + r := xs.runnerCache.GetRunner(utils.MkfsXfs) + _, err := r.Command(name) + if err != nil { + return err + } + return nil +} + +func (xs *XfsService) Label(name string, label string) error { + if len(label) > 12 { + return fmt.Errorf("🔴 %s: Label '%s' cannot exceed 12 characters for the XFS file system", name, label) + } + r := xs.runnerCache.GetRunner(utils.XfsAdmin) + _, err := r.Command("-L", label, name) + if err != nil { + return err + } + return nil +} + +func (es *XfsService) Resize(name string) error { + r := es.runnerCache.GetRunner(utils.XfsGrowfs) + _, err := r.Command(name) + if err != nil { + return err + } + return nil +} + +func (xs *XfsService) GetSize(name string) (int, error) { + r := xs.runnerCache.GetRunner(utils.XfsInfo) + output, err := r.Command(name) + if err != nil { + return 0, err + } + // Regex (Data) + red := regexp.MustCompile(`data\s+=\s+bsize=(\d+)\s+blocks=(\d+)`) + // Match (Data) + md := red.FindStringSubmatch(output) + if len(md) != 3 { + return 0, fmt.Errorf("🔴 %s: Block size and block count not found xfs_info output", name) + } + // String (Block Size) + sbs := md[1] + // Block Size + bs, err := strconv.Atoi(sbs) + if err != nil { + return 0, fmt.Errorf("🔴 Failed to cast block size to integer") + } + // String (Block Count) + sbc := md[2] + // Block Count + bc, err := strconv.Atoi(sbc) + if err != nil { + return 0, fmt.Errorf("🔴 Failed to cast block count to integer") + } + return bs * bc, nil +} + +func (es *XfsService) ResizeRequiresMount() bool { + return true +} diff --git a/internal/utils/exec.go b/internal/utils/exec.go index 2fb9c55..965954d 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -9,13 +9,18 @@ import ( type Binary string const ( - Lsblk Binary = "lsblk" - MkfsExt4 Binary = "mkfs.ext4" - E2Label Binary = "e2label" - MkfsXfs Binary = "mkfs.xfs" - XfsAdmin Binary = "xfs_admin" - Mount Binary = "mount" - Umount Binary = "umount" + Lsblk Binary = "lsblk" + MkfsExt4 Binary = "mkfs.ext4" + E2Label Binary = "e2label" + MkfsXfs Binary = "mkfs.xfs" + XfsAdmin Binary = "xfs_admin" + Mount Binary = "mount" + Umount Binary = "umount" + BlockDev Binary = "blockdev" + Tune2fs Binary = "tune2fs" + XfsInfo Binary = "xfs_info" + Resize2fs Binary = "resize2fs" + XfsGrowfs Binary = "xfs_growfs" ) type RunnerCache struct { @@ -24,20 +29,17 @@ type RunnerCache struct { func NewRunnerCache() *RunnerCache { return &RunnerCache{ - cache: map[Binary]Runner{ - Lsblk: NewExecRunner(Lsblk), - MkfsExt4: NewExecRunner(MkfsExt4), - E2Label: NewExecRunner(E2Label), - MkfsXfs: NewExecRunner(MkfsXfs), - XfsAdmin: NewExecRunner(XfsAdmin), - Mount: NewExecRunner(Mount), - Umount: NewExecRunner(Umount), - }, + cache: map[Binary]Runner{}, } } func (rc *RunnerCache) GetRunner(binary Binary) Runner { - return rc.cache[binary] + r, exists := rc.cache[binary] + if !exists { + r = NewExecRunner(binary) + rc.cache[binary] = r + } + return r } type Runner interface {