Skip to content

Commit

Permalink
Merge pull request #14 from reecetech/feature/resize-refactor
Browse files Browse the repository at this point in the history
(feat): Refactor Resize Capabilities
  • Loading branch information
lasith-kg authored May 24, 2024
2 parents 42e720b + 5336268 commit 85c521d
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 435 deletions.
1 change: 0 additions & 1 deletion cmd/ebs-bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func main() {
validators := []config.Validator{
config.NewFileSystemValidator(),
config.NewModeValidator(),
config.NewResizeThresholdValidator(),
config.NewMountPointValidator(),
config.NewMountOptionsValidator(),
config.NewOwnerValidator(uos),
Expand Down
5 changes: 2 additions & 3 deletions configs/ubuntu.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defaults:
resizeFs: true
resizeThreshold: 99
resize: true
devices:
/dev/vdb:
fs: xfs
Expand All @@ -17,4 +16,4 @@ devices:
user: ubuntu
group: ubuntu
permissions: 755
lvmConsumption: 30
lvmConsumption: 100
8 changes: 4 additions & 4 deletions internal/action/lvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ func (a *CreateVolumeGroupAction) Success() string {

type CreateLogicalVolumeAction struct {
name string
volumeGroupPercent int
volumeGroupPercent uint64
volumeGroup string
mode model.Mode
lvmService service.LvmService
}

func NewCreateLogicalVolumeAction(name string, volumeGroupPercent int, volumeGroup string, ls service.LvmService) *CreateLogicalVolumeAction {
func NewCreateLogicalVolumeAction(name string, volumeGroupPercent uint64, volumeGroup string, ls service.LvmService) *CreateLogicalVolumeAction {
return &CreateLogicalVolumeAction{
name: name,
volumeGroupPercent: volumeGroupPercent,
Expand Down Expand Up @@ -212,13 +212,13 @@ func (a *ResizePhysicalVolumeAction) Success() string {

type ResizeLogicalVolumeAction struct {
name string
volumeGroupPercent int
volumeGroupPercent uint64
volumeGroup string
mode model.Mode
lvmService service.LvmService
}

func NewResizeLogicalVolumeAction(name string, volumeGroupPercent int, volumeGroup string, ls service.LvmService) *ResizeLogicalVolumeAction {
func NewResizeLogicalVolumeAction(name string, volumeGroupPercent uint64, volumeGroup string, ls service.LvmService) *ResizeLogicalVolumeAction {
return &ResizeLogicalVolumeAction{
name: name,
volumeGroupPercent: volumeGroupPercent,
Expand Down
60 changes: 49 additions & 11 deletions internal/backend/lvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,57 @@ import (
"github.com/reecetech/ebs-bootstrap/internal/service"
)

const (
// The % tolerance to expect the logical volume size to be within
// -------------------------------------------------------
// If the (logical volume size / 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
LogicalVolumeResizeTolerance = float64(0.1)
// The % threshold at which to resize a physical volume
// -------------------------------------------------------
// If the (physical volume size / device size) * 100 falls
// under this threshold then we perform a resize operation
// -------------------------------------------------------
// The smallest gp3 EBS volume you can create is 1GiB (1073741824 bytes).
// The default size of the extent of a PV is 4 MiB (4194304 bytes).
// Typically, the first extent of a PV is reserved for metadata. This
// produces a PV of size 1069547520 bytes (Usage=99.6093%). We ensure
// that we set the resize threshold to 99.6% to ensure that a 1 GiB EBS
// volume won't be always resized
// -------------------------------------------------------
// Why not just look for a difference of 4194304 bytes?
// - The size of the extent can be changed by the user
// - Therefore we may not always see a difference of 4194304 bytes between
// the block device and physical volume size
PhysicalVolumeResizeThreshold = float64(99.6)
)

type LvmBackend interface {
CreatePhysicalVolume(name string) action.Action
CreateVolumeGroup(name string, physicalVolume string) action.Action
CreateLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) action.Action
CreateLogicalVolume(name string, volumeGroup string, volumeGroupPercent uint64) 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, error)
SearchVolumeGroup(physicalVolume string) (*model.VolumeGroup, error)
ShouldResizePhysicalVolume(name string, threshold float64) (bool, error)
ShouldResizePhysicalVolume(name string) (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
ShouldResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent uint64) (bool, error)
ResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent uint64) action.Action
From(config *config.Config) error
}

Expand Down Expand Up @@ -116,15 +154,15 @@ func (lb *LinuxLvmBackend) CreateVolumeGroup(name string, physicalVolume string)
return action.NewCreateVolumeGroupAction(name, physicalVolume, lb.lvmService)
}

func (lb *LinuxLvmBackend) CreateLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) action.Action {
func (lb *LinuxLvmBackend) CreateLogicalVolume(name string, volumeGroup string, volumeGroupPercent uint64) 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, error) {
func (lb *LinuxLvmBackend) ShouldResizePhysicalVolume(name string) (bool, error) {
pvn, err := lb.lvmGraph.GetPhysicalVolume(name)
if err != nil {
return false, nil
Expand All @@ -133,16 +171,16 @@ func (lb *LinuxLvmBackend) ShouldResizePhysicalVolume(name string, threshold flo
if len(dn) == 0 {
return false, nil
}
return (float64(pvn.Size) / float64(dn[0].Size) * 100) < threshold, nil
return (float64(pvn.Size) / float64(dn[0].Size) * 100) < PhysicalVolumeResizeThreshold, 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
func (lb *LinuxLvmBackend) ShouldResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent uint64) (bool, error) {
left := float64(volumeGroupPercent) - LogicalVolumeResizeTolerance
right := float64(volumeGroupPercent) + LogicalVolumeResizeTolerance
lvn, err := lb.lvmGraph.GetLogicalVolume(name, volumeGroup)
if err != nil {
return false, err
Expand All @@ -158,7 +196,7 @@ func (lb *LinuxLvmBackend) ShouldResizeLogicalVolume(name string, volumeGroup st
return usedPerecent < left, nil
}

func (lb *LinuxLvmBackend) ResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent int) action.Action {
func (lb *LinuxLvmBackend) ResizeLogicalVolume(name string, volumeGroup string, volumeGroupPercent uint64) action.Action {
return action.NewResizeLogicalVolumeAction(name, volumeGroupPercent, volumeGroup, lb.lvmService)
}

Expand Down
28 changes: 28 additions & 0 deletions internal/backend/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,32 @@ import (
"github.com/reecetech/ebs-bootstrap/internal/service"
)

const (
// The % threshold at which to resize a file system
// -------------------------------------------------------
// If the (file system size / device size) * 100 falls
// under this threshold then we perform a resize operation
// -------------------------------------------------------
// Why is the threshold not set to 100%?
// - A completely extended file system may be size that is
// slightly less than that of the underlying block device
// - This is likely due to reserved sections that store
// file system metadata
// - Therefore we set the threshold to 99.999% to avoid
// unnecessary resize operations
// Why is the threshold set to 99.999%?
// - The largest EBS volume you can provision is a 64 TiB
// io2 Block Express volume.
// - EBS volumes can only be specified in increments of 1 GiB
// - 64 TiB = 65536 GiB | 99.999 % * 65536 = 65535.34 GiB
// - Therefore, a resize threshold of 99.999% ensures a resize
// operation from 65535 GiB to 65536 GiB, since 65535 < 65535.34
FileSystemResizeThreshold = float64(99.999)
)

type DeviceMetricsBackend interface {
GetBlockDeviceMetrics(name string) (*model.BlockDeviceMetrics, error)
ShouldResize(bdm *model.BlockDeviceMetrics) bool
From(config *config.Config) error
}

Expand Down Expand Up @@ -43,6 +67,10 @@ func (dmb *LinuxDeviceMetricsBackend) GetBlockDeviceMetrics(name string) (*model
return metrics, nil
}

func (dmb *LinuxDeviceMetricsBackend) ShouldResize(bdm *model.BlockDeviceMetrics) bool {
return (float64(bdm.FileSystemSize) / float64(bdm.BlockDeviceSize) * 100) < FileSystemResizeThreshold
}

func (dmb *LinuxDeviceMetricsBackend) From(config *config.Config) error {
dmb.blockDeviceMetrics = nil
blockDeviceMetrics := map[string]*model.BlockDeviceMetrics{}
Expand Down
36 changes: 36 additions & 0 deletions internal/backend/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,42 @@ func TestGetBlockDeviceMetrics(t *testing.T) {
}
}

func TestLinuxDeviceMetricsBackendShouldResize(t *testing.T) {
subtests := []struct {
Name string
BlockDeviceMetrics *model.BlockDeviceMetrics
ExpectedOutput bool
}{
// FileSystemThreshold = 99.999%
// 999989 / 1000000 → 99.9989% < 99.999% → true
{
Name: "Should Resize",
BlockDeviceMetrics: &model.BlockDeviceMetrics{
FileSystemSize: 999989,
BlockDeviceSize: 1000000,
},
ExpectedOutput: true,
},
// FileSystemThreshold = 99.999%
// 999990 / 100000 → 99.999% < 99.999% → false
{
Name: "Should Not Resize",
BlockDeviceMetrics: &model.BlockDeviceMetrics{
FileSystemSize: 999990,
BlockDeviceSize: 1000000,
},
ExpectedOutput: false,
},
}
for _, subtest := range subtests {
t.Run(subtest.Name, func(t *testing.T) {
dmb := NewMockLinuxDeviceMetricsBackend(nil)
shouldResize := dmb.ShouldResize(subtest.BlockDeviceMetrics)
utils.CheckOutput("dmb.ShouldResize()", t, subtest.ExpectedOutput, shouldResize)
})
}
}

func TestLinuxDeviceMetricsBackendFrom(t *testing.T) {
fssf := service.NewLinuxFileSystemServiceFactory(nil)

Expand Down
53 changes: 18 additions & 35 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ const (
)

type Flag struct {
Config string
Mode string
Remount bool
MountOptions string
ResizeFs bool
ResizeThreshold float64
LvmConsumption int
Config string
Mode string
Remount bool
MountOptions string
Resize bool
LvmConsumption uint64
}

type Device struct {
Expand All @@ -38,12 +37,11 @@ type Device struct {
}

type Options struct {
Mode model.Mode `yaml:"mode"`
Remount bool `yaml:"remount"`
MountOptions model.MountOptions `yaml:"mountOptions"`
ResizeFs bool `yaml:"resizeFs"`
ResizeThreshold float64 `yaml:"resizeThreshold"`
LvmConsumption int `yaml:"lvmConsumption"`
Mode model.Mode `yaml:"mode"`
Remount bool `yaml:"remount"`
MountOptions model.MountOptions `yaml:"mountOptions"`
Resize bool `yaml:"resize"`
LvmConsumption uint64 `yaml:"lvmConsumption"`
}

// We don't export "overrides" as this is an attribute that is used
Expand Down Expand Up @@ -100,9 +98,8 @@ func parseFlags(program string, args []string) (*Flag, error) {
flags.StringVar(&f.Mode, "mode", "", "override for mode")
flags.BoolVar(&f.Remount, "remount", false, "override for remount")
flags.StringVar(&f.MountOptions, "mount-options", "", "override for mount options")
flags.BoolVar(&f.ResizeFs, "resize-fs", false, "override for resize filesystem")
flags.Float64Var(&f.ResizeThreshold, "resize-threshold", 0, "override for resize threshold")
flags.IntVar(&f.LvmConsumption, "lvm-consumption", 0, "override for lvm consumption")
flags.BoolVar(&f.Resize, "resize", false, "override for resize filesystem")
flags.Uint64Var(&f.LvmConsumption, "lvm-consumption", 0, "override for lvm consumption")

// Actually parse the flag
err := flags.Parse(args)
Expand All @@ -117,8 +114,8 @@ func (c *Config) setOverrides(f *Flag) *Config {
c.overrides.Mode = model.Mode(f.Mode)
c.overrides.Remount = f.Remount
c.overrides.MountOptions = model.MountOptions(f.MountOptions)
c.overrides.ResizeFs = f.ResizeFs
c.overrides.ResizeThreshold = f.ResizeThreshold
c.overrides.Resize = f.Resize
c.overrides.LvmConsumption = f.LvmConsumption
return c
}

Expand Down Expand Up @@ -164,29 +161,15 @@ func (c *Config) GetMountOptions(name string) model.MountOptions {
return DefaultMountOptions
}

func (c *Config) GetResizeFs(name string) bool {
func (c *Config) GetResize(name string) bool {
cd, found := c.Devices[name]
if !found {
return false
}
return c.overrides.ResizeFs || c.Defaults.ResizeFs || cd.ResizeFs
return c.overrides.Resize || c.Defaults.Resize || cd.Resize
}

func (c *Config) GetResizeThreshold(name string) float64 {
cd, found := c.Devices[name]
if !found {
return 0
}
if c.overrides.ResizeThreshold > 0 {
return c.overrides.ResizeThreshold
}
if cd.ResizeThreshold > 0 {
return cd.ResizeThreshold
}
return c.Defaults.ResizeThreshold
}

func (c *Config) GetLvmConsumption(name string) int {
func (c *Config) GetLvmConsumption(name string) uint64 {
cd, found := c.Devices[name]
if !found {
return DefaultLvmConsumption
Expand Down
Loading

0 comments on commit 85c521d

Please sign in to comment.