From 2dbc7b5983e15efe4e003cc7400222050eb6b122 Mon Sep 17 00:00:00 2001 From: masteryyh Date: Fri, 2 Jun 2023 10:45:28 +0800 Subject: [PATCH] Add configuration for COS_PERSISTENT partition size. Signed-off-by: masteryyh --- pkg/config/config.go | 10 +- pkg/config/constants.go | 7 + pkg/config/cos.go | 38 ++-- pkg/config/cos_test.go | 105 +++------ pkg/console/constant.go | 89 ++++---- pkg/console/install_panels.go | 407 +++++++++++++++++++++++++--------- pkg/console/util.go | 12 +- pkg/console/validator.go | 16 +- pkg/util/common.go | 24 ++ pkg/util/disk.go | 50 +++++ pkg/util/disk_test.go | 84 +++++++ 11 files changed, 590 insertions(+), 252 deletions(-) create mode 100644 pkg/util/disk.go create mode 100644 pkg/util/disk_test.go diff --git a/pkg/config/config.go b/pkg/config/config.go index be994bc2c..056d2f5f5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -30,11 +30,9 @@ const ( ) const ( - SoftMinDiskSizeGiB = 140 - HardMinDiskSizeGiB = 60 - MinCosPartSizeGiB = 25 - NormalCosPartSizeGiB = 50 - MaxPods = 200 + HardMinDiskSizeGiB = 250 + HardMinDataDiskSizeGiB = 50 + MaxPods = 200 ) // refer: https://github.com/harvester/harvester/blob/master/pkg/settings/settings.go @@ -153,6 +151,8 @@ type Install struct { Webhooks []Webhook `json:"webhooks,omitempty"` Addons map[string]Addon `json:"addons,omitempty"` Harvester HarvesterChartValues `json:"harvester,omitempty"` + + PersistentPartitionSize string `json:"persistentPartitionSize,omitempty"` } type Wifi struct { diff --git a/pkg/config/constants.go b/pkg/config/constants.go index 556035746..57a3ec277 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -13,4 +13,11 @@ const ( MgmtBondInterfaceName = "mgmt-bo" RancherdConfigFile = "/etc/rancher/rancherd/config.yaml" + + DefaultCosOemSizeMiB = 50 + DefaultCosStateSizeMiB = 15360 + DefaultCosRecoverySizeMiB = 8192 + + DefaultPersistentPercentageNum = 0.3 + PersistentSizeMinGiB = 150 ) diff --git a/pkg/config/cos.go b/pkg/config/cos.go index e6427272b..ad8c7505e 100644 --- a/pkg/config/cos.go +++ b/pkg/config/cos.go @@ -707,53 +707,49 @@ func genBootstrapResources(config *HarvesterConfig) (map[string]string, error) { return bootstrapConfs, nil } -func calcCosPersistentPartSize(diskSizeGiB uint64) (uint64, error) { - switch { - case diskSizeGiB < HardMinDiskSizeGiB: - return 0, fmt.Errorf("disk too small: %dGB. Minimum %dGB is required", diskSizeGiB, HardMinDiskSizeGiB) - case diskSizeGiB < SoftMinDiskSizeGiB: - d := MinCosPartSizeGiB / float64(SoftMinDiskSizeGiB-HardMinDiskSizeGiB) - partSizeGiB := MinCosPartSizeGiB + float64(diskSizeGiB-HardMinDiskSizeGiB)*d - return uint64(partSizeGiB), nil - default: - partSizeGiB := NormalCosPartSizeGiB + ((diskSizeGiB-100)/100)*10 - if partSizeGiB > 100 { - partSizeGiB = 100 - } - return partSizeGiB, nil +func calcCosPersistentPartSize(diskSizeGiB uint64, partSize string) (uint64, error) { + size, err := util.ParsePartitionSize(util.GiToByte(diskSizeGiB), partSize) + if err != nil { + return 0, err } + return util.ByteToMi(size), nil } -func CreateRootPartitioningLayout(elementalConfig *ElementalConfig, devPath string) (*ElementalConfig, error) { - diskSizeBytes, err := util.GetDiskSizeBytes(devPath) +func CreateRootPartitioningLayout(elementalConfig *ElementalConfig, hvstConfig *HarvesterConfig) (*ElementalConfig, error) { + diskSizeBytes, err := util.GetDiskSizeBytes(hvstConfig.Install.Device) if err != nil { return nil, err } - cosPersistentSizeGiB, err := calcCosPersistentPartSize(diskSizeBytes >> 30) + persistentSize := hvstConfig.Install.PersistentPartitionSize + if persistentSize == "" { + persistentSize = fmt.Sprintf("%dGi", PersistentSizeMinGiB) + } + cosPersistentSizeMiB, err := calcCosPersistentPartSize(util.ByteToGi(diskSizeBytes), persistentSize) if err != nil { return nil, err } + logrus.Infof("Calculated COS_PERSISTENT partition size: %d MiB", cosPersistentSizeMiB) elementalConfig.Install.Partitions = &ElementalDefaultPartition{ OEM: &ElementalPartition{ FilesystemLabel: "COS_OEM", - Size: 50, + Size: DefaultCosOemSizeMiB, FS: "ext4", }, State: &ElementalPartition{ FilesystemLabel: "COS_STATE", - Size: 15360, + Size: DefaultCosStateSizeMiB, FS: "ext4", }, Recovery: &ElementalPartition{ FilesystemLabel: "COS_RECOVERY", - Size: 8192, + Size: DefaultCosRecoverySizeMiB, FS: "ext4", }, Persistent: &ElementalPartition{ FilesystemLabel: "COS_PERSISTENT", - Size: uint(cosPersistentSizeGiB << 10), + Size: uint(cosPersistentSizeMiB), FS: "ext4", }, } diff --git a/pkg/config/cos_test.go b/pkg/config/cos_test.go index d96895ee5..6059f2736 100644 --- a/pkg/config/cos_test.go +++ b/pkg/config/cos_test.go @@ -10,97 +10,54 @@ import ( func TestCalcCosPersistentPartSize(t *testing.T) { testCases := []struct { - name string - input uint64 - output uint64 - expectError bool + diskSize uint64 + partitionSize string + result uint64 + err string }{ { - name: "Disk too small", - input: 50, - output: 0, - expectError: true, + diskSize: 300, + partitionSize: "150Gi", + result: 153600, }, { - name: "Disk meet hard requirement", - input: 60, - output: 25, - expectError: false, + diskSize: 500, + partitionSize: "153600Mi", + result: 153600, }, { - name: "Disk a bit larger than hard requirement: 80G", - input: 80, - output: 31, - expectError: false, + diskSize: 250, + partitionSize: "240Gi", + err: "Partition size is too large. Maximum 176Gi is allowed", }, { - name: "Disk a bit larger than hard requirement: 100G", - input: 100, - output: 37, - expectError: false, + diskSize: 150, + partitionSize: "100Gi", + err: "Disk size is too small. Minimum 250Gi is required", }, { - name: "Disk close to the soft requirement", - input: 139, - output: 49, - expectError: false, + diskSize: 300, + partitionSize: "153600Ki", + err: "Partition size should be ended with 'Mi', 'Gi', and no dot and negative is allowed", }, { - name: "Disk meet soft requirement", - input: SoftMinDiskSizeGiB, - output: 50, - expectError: false, + diskSize: 2000, + partitionSize: "1.5Ti", + err: "Partition size should be ended with 'Mi', 'Gi', and no dot and negative is allowed", }, { - name: "200GiB", - input: 200, - output: 60, - expectError: false, - }, - { - name: "300GiB", - input: 300, - output: 70, - expectError: false, - }, - { - name: "400GiB", - input: 400, - output: 80, - expectError: false, - }, - { - name: "500GiB", - input: 500, - output: 90, - expectError: false, - }, - { - name: "600GiB", - input: 600, - output: 100, - expectError: false, - }, - { - name: "Greater than 600GiB should still get 100", - input: 700, - output: 100, - expectError: false, + diskSize: 500, + partitionSize: "abcd", + err: "Partition size should be ended with 'Mi', 'Gi', and no dot and negative is allowed", }, } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - sizeGiB, err := calcCosPersistentPartSize(testCase.input) - if testCase.expectError { - assert.NotNil(t, err) - } else { - if err != nil { - t.Log(err) - } - assert.Equal(t, sizeGiB, testCase.output) - } - }) + for _, tc := range testCases { + result, err := calcCosPersistentPartSize(tc.diskSize, tc.partitionSize) + assert.Equal(t, tc.result, result) + if err != nil { + assert.EqualError(t, err, tc.err) + } } } diff --git a/pkg/console/constant.go b/pkg/console/constant.go index 472de2170..4db53fcb2 100644 --- a/pkg/console/constant.go +++ b/pkg/console/constant.go @@ -1,50 +1,56 @@ package console const ( - titlePanel = "title" - debugPanel = "debug" - diskPanel = "disk" - dataDiskPanel = "dataDisk" - dataDiskValidatorPanel = "dataDiskValidator" - askForceMBRTitlePanel = "askForceMBRTitle" - askForceMBRPanel = "askForceMBR" - forceMBRNotePanel = "forceMBRNote" - askCreatePanel = "askCreate" - serverURLPanel = "serverUrl" - passwordPanel = "osPassword" - passwordConfirmPanel = "osPasswordConfirm" - sshKeyPanel = "sshKey" - tokenPanel = "token" - proxyPanel = "proxy" - askInterfacePanel = "askInterface" - askVlanIDPanel = "askVlanID" - askBondModePanel = "askBondMode" - bondNotePanel = "bondNote" - askNetworkMethodPanel = "askNetworkMethod" - hostnamePanel = "hostname" - addressPanel = "address" - gatewayPanel = "gateway" - mtuPanel = "mtu" - dnsServersPanel = "dnsServers" - hostnameValidatorPanel = "hostnameValidator" - networkValidatorPanel = "networkValidator" - diskValidatorPanel = "diskValidator" - cloudInitPanel = "cloudInit" - validatorPanel = "validator" - notePanel = "note" - installPanel = "install" - footerPanel = "footer" - spinnerPanel = "spinner" - confirmInstallPanel = "confirmInstall" - confirmUpgradePanel = "confirmUpgrade" - upgradePanel = "upgrade" - askVipMethodPanel = "askVipMethodPanel" - vipPanel = "vipPanel" - vipTextPanel = "vipTextPanel" - ntpServersPanel = "ntpServersPanel" + titlePanel = "title" + debugPanel = "debug" + diskPanel = "disk" + persistentSizePanel = "persistentSize" + dataPersistentSizePanel = "dataPersistentSize" + dataPersistentSizeNotePanel = "dataPersistentSizeNote" + dataDiskPanel = "dataDisk" + dataDiskValidatorPanel = "dataDiskValidator" + askForceMBRTitlePanel = "askForceMBRTitle" + askForceMBRPanel = "askForceMBR" + diskNotePanel = "diskNote" + askCreatePanel = "askCreate" + serverURLPanel = "serverUrl" + passwordPanel = "osPassword" + passwordConfirmPanel = "osPasswordConfirm" + sshKeyPanel = "sshKey" + tokenPanel = "token" + proxyPanel = "proxy" + askInterfacePanel = "askInterface" + askVlanIDPanel = "askVlanID" + askBondModePanel = "askBondMode" + bondNotePanel = "bondNote" + askNetworkMethodPanel = "askNetworkMethod" + hostnamePanel = "hostname" + addressPanel = "address" + gatewayPanel = "gateway" + mtuPanel = "mtu" + dnsServersPanel = "dnsServers" + hostnameValidatorPanel = "hostnameValidator" + networkValidatorPanel = "networkValidator" + diskValidatorPanel = "diskValidator" + cloudInitPanel = "cloudInit" + validatorPanel = "validator" + notePanel = "note" + installPanel = "install" + footerPanel = "footer" + spinnerPanel = "spinner" + confirmInstallPanel = "confirmInstall" + confirmUpgradePanel = "confirmUpgrade" + upgradePanel = "upgrade" + askVipMethodPanel = "askVipMethodPanel" + vipPanel = "vipPanel" + vipTextPanel = "vipTextPanel" + ntpServersPanel = "ntpServersPanel" hostnameTitle = "Configure hostname for this instance" networkTitle = "Configure network" + diskLabel = "Installation disk" + dataDiskLabel = "Data disk" + persistentSizeLabel = "Persistent size" askBondModeLabel = "Bond Mode" askInterfaceLabel = "Management NIC" askVlanIDLabel = "VLAN ID (optional)" @@ -72,6 +78,7 @@ const ( dnsServersNote = "Note: You can use comma to add more DNS servers. Leave blank to use default DNS." bondNote = "Note: Select one or more NICs for the Management NIC.\nUse the default value for the Bond Mode if only one NIC is selected." forceMBRNote = "Note: GPT is used by default. You can use MBR if you encountered compatibility issues." + persistentSizeNote = "Note: persistent partition stores data like system package and container images, not the VM data. \nYou can specify a size like 200Gi or 15360Mi. \nLeave it blank to use the default value." authorizedFile = "/home/rancher/.ssh/authorized_keys" ) diff --git a/pkg/console/install_panels.go b/pkg/console/install_panels.go index 1326205ba..3cd6eb889 100644 --- a/pkg/console/install_panels.go +++ b/pkg/console/install_panels.go @@ -174,17 +174,50 @@ func addFooterPanel(c *Console) error { } func showDiskPage(c *Console) error { + diskOptions, err := getDiskOptions() + if err != nil { + return err + } + showPersistentSizeOption := len(diskOptions) == 1 + if systemIsBIOS() { + if showPersistentSizeOption { + return showNext(c, + persistentSizePanel, + diskNotePanel, + askForceMBRPanel, + askForceMBRTitlePanel, + diskPanel, + ) + } return showNext(c, - forceMBRNotePanel, + diskNotePanel, askForceMBRPanel, askForceMBRTitlePanel, diskPanel, ) } + + if showPersistentSizeOption { + return showNext(c, persistentSizePanel, diskPanel) + } return showNext(c, diskPanel) } +func calculateDefaultPersistentSize(dev string) (string, error) { + bytes, err := util.GetDiskSizeBytes(dev) + if err != nil { + return "", err + } + + defaultBytes := uint64(float64(bytes) * config.DefaultPersistentPercentageNum) + defaultSize := util.ByteToGi(defaultBytes) + if defaultSize < config.PersistentSizeMinGiB { + defaultSize = config.PersistentSizeMinGiB + } + return fmt.Sprintf("%dGi", defaultSize), nil +} + func addDiskPanel(c *Console) error { setLocation := createVerticalLocator(c) diskOpts, err := getDiskOptions() @@ -193,21 +226,58 @@ func addDiskPanel(c *Console) error { } // Select device panel - diskV, err := widgets.NewSelect(c.Gui, diskPanel, "", getDiskOptions) + diskV, err := widgets.NewDropDown(c.Gui, diskPanel, diskLabel, func() ([]widgets.Option, error) { + return diskOpts, nil + }) if err != nil { return err } diskV.PreShow = func() error { - diskV.Value = c.config.Install.Device + if c.config.Install.Device != "" { + diskV.SetData(c.config.Install.Device) + } else { + diskV.SetData(diskOpts[0].Value) + } + _ = c.setContentByName(diskNotePanel, "") return c.setContentByName(titlePanel, "Choose installation target. Device will be formatted") } - setLocation(diskV.Panel, len(diskOpts)+2) + setLocation(diskV.Panel, 3) c.AddElement(diskPanel, diskV) + //Persistent partition size panel + persistentSizeV, err := widgets.NewInput(c.Gui, persistentSizePanel, persistentSizeLabel, false) + if err != nil { + return err + } + persistentSizeV.PreShow = func() error { + c.Cursor = true + + device := c.config.Install.Device + if device == "" { + device = diskOpts[0].Value + } + + // If the user has already set a persistent partition size, use that + if persistentSizeV.Value != "" { + if c.config.Install.PersistentPartitionSize != "" { + persistentSizeV.Value = c.config.Install.PersistentPartitionSize + } else { + defaultValue, err := calculateDefaultPersistentSize(device) + if err != nil { + return err + } + persistentSizeV.Value = defaultValue + } + } + return nil + } + setLocation(persistentSizeV, 3) + c.AddElement(persistentSizePanel, persistentSizeV) + // Asking force MBR title askForceMBRTitleV := widgets.NewPanel(c.Gui, askForceMBRTitlePanel) askForceMBRTitleV.SetContent("Use MBR partitioning scheme") - setLocation(askForceMBRTitleV, 2) + setLocation(askForceMBRTitleV, 3) c.AddElement(askForceMBRTitlePanel, askForceMBRTitleV) // Asking force MBR DropDown @@ -221,19 +291,18 @@ func addDiskPanel(c *Console) error { c.Cursor = true if c.config.ForceMBR { askForceMBRV.SetData("yes") - } else { - askForceMBRV.SetData("no") } + askForceMBRV.SetData("no") return nil } setLocation(askForceMBRV.Panel, 3) c.AddElement(askForceMBRPanel, askForceMBRV) - // Note panel for ForceMBR - forceMBRNoteV := widgets.NewPanel(c.Gui, forceMBRNotePanel) - forceMBRNoteV.Wrap = true - setLocation(forceMBRNoteV, 3) - c.AddElement(forceMBRNotePanel, forceMBRNoteV) + // Note panel for ForceMBR and persistent partition size + diskNoteV := widgets.NewPanel(c.Gui, diskNotePanel) + diskNoteV.Wrap = true + setLocation(diskNoteV, 3) + c.AddElement(diskNotePanel, diskNoteV) // Panel for showing validator message diskValidatorV := widgets.NewPanel(c.Gui, diskValidatorPanel) @@ -253,8 +322,9 @@ func addDiskPanel(c *Console) error { diskPanel, askForceMBRPanel, diskValidatorPanel, - forceMBRNotePanel, + diskNotePanel, askForceMBRTitlePanel, + persistentSizePanel, ) } gotoPrevPage := func(g *gocui.Gui, v *gocui.View) error { @@ -262,21 +332,6 @@ func addDiskPanel(c *Console) error { return showNext(c, askCreatePanel) } gotoNextPage := func(g *gocui.Gui, v *gocui.View) error { - forceMBR, err := askForceMBRV.GetData() - if err != nil { - return err - } - if forceMBR == "yes" { - diskTooLargeForMBR, err := diskExceedsMBRLimit(c.config.Device) - if err != nil { - return err - } - if diskTooLargeForMBR { - return updateValidatorMessage("Disk too large for MBR. Must be less than 2TiB") - } - } - c.config.ForceMBR = (forceMBR == "yes") - closeThisPage() if canChoose, err := canChooseDataDisk(); err != nil { return err @@ -287,34 +342,49 @@ func addDiskPanel(c *Console) error { } } - // Keybindings - diskV.KeyBindings = map[gocui.Key]func(*gocui.Gui, *gocui.View) error{ - gocui.KeyEnter: func(g *gocui.Gui, v *gocui.View) error { - device, err := diskV.GetData() - if err != nil { + showPersistentSize := len(diskOpts) == 1 + diskConfirm := func(g *gocui.Gui, v *gocui.View) error { + device, err := diskV.GetData() + if err != nil { + return err + } + if err := validateDiskSize(device); err != nil { + return updateValidatorMessage(err.Error()) + } + + userInputData.HasWarnedDiskSize = false + if err := updateValidatorMessage(""); err != nil { + return err + } + c.config.Install.Device = device + + if showPersistentSize { + if err := c.setContentByName(diskNotePanel, persistentSizeNote); err != nil { return err } - if err := validateDiskSize(device); err != nil { - return updateValidatorMessage(err.Error()) + if err := updateValidatorMessage(""); err != nil { + return err } + return showNext(c, persistentSizePanel) + } - if err := validateDiskSizeSoft(device); err != nil && !userInputData.HasWarnedDiskSize { - userInputData.HasWarnedDiskSize = true - return updateValidatorMessage(fmt.Sprintf("%s. Press Enter to continue.", err.Error())) + if systemIsBIOS() { + if err := c.setContentByName(diskNotePanel, forceMBRNote); err != nil { + return err } - - c.config.Install.Device = device - - if systemIsBIOS() { - c.setContentByName(forceMBRNotePanel, forceMBRNote) - return showNext(c, askForceMBRPanel) + if err := updateValidatorMessage(""); err != nil { + return err } - return gotoNextPage(g, v) - }, - gocui.KeyArrowDown: func(g *gocui.Gui, v *gocui.View) error { - userInputData.HasWarnedDiskSize = false - return updateValidatorMessage("") - }, + return showNext(c, askForceMBRPanel) + } + + logrus.Infof("Selected installation disk: %s", c.config.Install.Device) + return gotoNextPage(g, v) + } + // Keybindings + diskV.KeyBindings = map[gocui.Key]func(*gocui.Gui, *gocui.View) error{ + gocui.KeyEnter: diskConfirm, + gocui.KeyArrowDown: diskConfirm, gocui.KeyArrowUp: func(g *gocui.Gui, v *gocui.View) error { userInputData.HasWarnedDiskSize = false return updateValidatorMessage("") @@ -322,21 +392,81 @@ func addDiskPanel(c *Console) error { gocui.KeyEsc: gotoPrevPage, } + mbrConfirm := func(g *gocui.Gui, v *gocui.View) error { + forceMBR, err := askForceMBRV.GetData() + if err != nil { + return err + } + if forceMBR == "yes" { + diskTooLargeForMBR, err := diskExceedsMBRLimit(c.config.Device) + if err != nil { + return err + } + if diskTooLargeForMBR { + return updateValidatorMessage("Disk too large for MBR. Must be less than 2TiB") + } + } + + c.config.ForceMBR = forceMBR == "yes" + return gotoNextPage(g, v) + } askForceMBRV.KeyBindings = map[gocui.Key]func(*gocui.Gui, *gocui.View) error{ - gocui.KeyEnter: gotoNextPage, + gocui.KeyEnter: mbrConfirm, gocui.KeyArrowUp: func(g *gocui.Gui, v *gocui.View) error { - //XXX Must close diskPanel first or gocui would crash - c.CloseElement(diskPanel) + if showPersistentSize { + if err := c.setContentByName(diskNotePanel, persistentSizeNote); err != nil { + return err + } + return showNext(c, persistentSizePanel) + } return showNext(c, diskPanel) }, - gocui.KeyArrowDown: gotoNextPage, + gocui.KeyArrowDown: mbrConfirm, + gocui.KeyEsc: gotoPrevPage, + } + + persistentSizeConfirm := func(g *gocui.Gui, v *gocui.View) error { + persistentSize, err := persistentSizeV.GetData() + if err != nil { + return err + } + + diskSize, err := util.GetDiskSizeBytes(c.config.Install.Device) + if err != nil { + return err + } + + if _, err := util.ParsePartitionSize(diskSize, persistentSize); err != nil { + return updateValidatorMessage(err.Error()) + } + + c.config.Install.PersistentPartitionSize = persistentSize + + if systemIsBIOS() { + if err := c.setContentByName(diskNotePanel, forceMBRNote); err != nil { + return err + } + return showNext(c, askForceMBRPanel) + } + return gotoNextPage(g, v) + } + persistentSizeV.KeyBindings = map[gocui.Key]func(*gocui.Gui, *gocui.View) error{ + gocui.KeyEnter: persistentSizeConfirm, + gocui.KeyArrowUp: func(g *gocui.Gui, v *gocui.View) error { + if err := updateValidatorMessage(""); err != nil { + return err + } + + return showNext(c, diskPanel) + }, + gocui.KeyArrowDown: persistentSizeConfirm, gocui.KeyEsc: gotoPrevPage, } return nil } func getDiskOptions() ([]widgets.Option, error) { - output, err := exec.Command("/bin/sh", "-c", `lsblk -r -o NAME,SIZE,TYPE | grep -w disk|cut -d ' ' -f 1,2`).CombinedOutput() + output, err := exec.Command("/bin/sh", "-c", `lsblk -r -o NAME,SIZE,TYPE | grep -w disk | cut -d ' ' -f 1,2`).CombinedOutput() if err != nil { return nil, err } @@ -356,26 +486,10 @@ func getDiskOptions() ([]widgets.Option, error) { } func showDataDiskPage(c *Console) error { - setLocation := createVerticalLocatorWithName(c) - diskOpts, err := getDataDiskOptions(c.config) - if err != nil { - return err - } - panels := []string{dataDiskPanel, dataDiskValidatorPanel} - setLocation(dataDiskPanel, len(diskOpts)+2) - setLocation(dataDiskValidatorPanel, 3) - - if err := showNext(c, panels...); err != nil { - return err - } - if err := c.ShowElement(dataDiskPanel); err != nil { - return err - } - if err := c.setContentByName(titlePanel, "Choose disk for storing VM data"); err != nil { - return err + if c.config.Install.Device == c.config.Install.DataDisk || c.config.Install.DataDisk == "" { + return showNext(c, dataPersistentSizeNotePanel, dataPersistentSizePanel, dataDiskPanel) } - - return nil + return showNext(c, dataDiskPanel) } func getDataDiskOptions(hvstConfig *config.HarvesterConfig) ([]widgets.Option, error) { @@ -396,33 +510,79 @@ func getDataDiskOptions(hvstConfig *config.HarvesterConfig) ([]widgets.Option, e return diskOpts, nil } } - return nil, fmt.Errorf("device '%s' not found in disk options", deviceForOS) + logrus.Warnf("device '%s' not found in disk options", deviceForOS) + return nil, nil } func addDataDiskPanel(c *Console) error { - dataDiskV, err := widgets.NewSelect(c.Gui, dataDiskPanel, "", func() ([]widgets.Option, error) { + setLocation := createVerticalLocator(c) + + dataDiskV, _ := widgets.NewDropDown(c.Gui, dataDiskPanel, dataDiskLabel, func() ([]widgets.Option, error) { return getDataDiskOptions(c.config) }) + + dataDiskV.PreShow = func() error { + if c.config.Install.DataDisk != "" { + dataDiskV.SetData(c.config.Install.DataDisk) + } else { + dataDiskV.SetData(c.config.Install.Device) + } + return c.setContentByName(titlePanel, "Choose disk for storing VM data") + } + setLocation(dataDiskV.Panel, 3) + c.AddElement(dataDiskPanel, dataDiskV) + + persistentSizeV, err := widgets.NewInput(c.Gui, dataPersistentSizePanel, persistentSizeLabel, false) if err != nil { return err } - dataDiskV.PreShow = func() error { - return dataDiskV.SetData(c.config.Install.DataDisk) + persistentSizeV.PreShow = func() error { + c.Cursor = true + + device := c.config.Install.DataDisk + if device == "" { + diskOptions, err := getDataDiskOptions(c.config) + if err != nil { + return err + } + device = diskOptions[0].Value + } + + if persistentSizeV.Value == "" { + if c.config.Install.PersistentPartitionSize != "" { + persistentSizeV.Value = c.config.Install.PersistentPartitionSize + } else { + size, err := calculateDefaultPersistentSize(device) + if err != nil { + return err + } + persistentSizeV.Value = size + } + } + return nil } - c.AddElement(dataDiskPanel, dataDiskV) + setLocation(persistentSizeV, 3) + c.AddElement(dataPersistentSizePanel, persistentSizeV) + + persistentSizeNoteV := widgets.NewPanel(c.Gui, dataPersistentSizeNotePanel) + persistentSizeNoteV.Wrap = true + setLocation(persistentSizeNoteV, 3) + c.AddElement(dataPersistentSizeNotePanel, persistentSizeNoteV) dataDiskValidatorV := widgets.NewPanel(c.Gui, dataDiskValidatorPanel) dataDiskValidatorV.FgColor = gocui.ColorRed dataDiskValidatorV.Wrap = true dataDiskValidatorV.Focus = false updateValidatorMessage := func(msg string) error { + dataDiskValidatorV.Focus = false return c.setContentByName(dataDiskValidatorPanel, msg) } + setLocation(dataDiskValidatorV, 3) c.AddElement(dataDiskValidatorPanel, dataDiskValidatorV) closeThisPage := func() { userInputData.HasWarnedDataDiskSize = false - c.CloseElements(dataDiskPanel, dataDiskValidatorPanel) + c.CloseElements(dataDiskPanel, dataDiskValidatorPanel, dataPersistentSizePanel, dataPersistentSizeNotePanel) } gotoNextPage := func() error { @@ -430,40 +590,87 @@ func addDataDiskPanel(c *Console) error { return showHostnamePage(c) } - gotoPrevPage := func() error { + gotoPrevPage := func(g *gocui.Gui, v *gocui.View) error { closeThisPage() return showDiskPage(c) } - dataDiskV.KeyBindings = map[gocui.Key]func(*gocui.Gui, *gocui.View) error{ - gocui.KeyEnter: func(g *gocui.Gui, v *gocui.View) error { - device, err := dataDiskV.GetData() - if err != nil { - return err - } + dataDiskConfirm := func(_ *gocui.Gui, _ *gocui.View) error { + device, err := dataDiskV.GetData() + if err != nil { + return err + } + + if device != c.config.Install.Device { + c.CloseElements(dataPersistentSizePanel, dataPersistentSizeNotePanel) + } - if err := validateDiskSize(device); err != nil { - return updateValidatorMessage(err.Error()) + if err := validateDataDiskSize(device); err != nil { + return updateValidatorMessage(err.Error()) + } + + if device == c.config.Install.Device { + if err := c.setContentByName(dataPersistentSizeNotePanel, persistentSizeNote); err != nil { + return err } - if err := validateDiskSizeSoft(device); err != nil && !userInputData.HasWarnedDataDiskSize { - userInputData.HasWarnedDataDiskSize = true - return updateValidatorMessage(fmt.Sprintf("%s. Press Enter to continue.", err.Error())) + if err := updateValidatorMessage(""); err != nil { + return err } + return showNext(c, dataPersistentSizePanel) + } - c.config.Install.DataDisk = device - return gotoNextPage() - }, - gocui.KeyArrowDown: func(g *gocui.Gui, v *gocui.View) error { + c.config.Install.DataDisk = device + userInputData.HasWarnedDataDiskSize = false + if err := updateValidatorMessage(""); err != nil { + return err + } + + return gotoNextPage() + } + dataDiskV.KeyBindings = map[gocui.Key]func(*gocui.Gui, *gocui.View) error{ + gocui.KeyEnter: dataDiskConfirm, + gocui.KeyArrowDown: dataDiskConfirm, + gocui.KeyArrowUp: func(g *gocui.Gui, v *gocui.View) error { userInputData.HasWarnedDataDiskSize = false return updateValidatorMessage("") }, + gocui.KeyEsc: gotoPrevPage, + } + + persistentSizeConfirm := func(g *gocui.Gui, v *gocui.View) error { + persistentSize, err := persistentSizeV.GetData() + if err != nil { + return err + } + + diskSize, err := util.GetDiskSizeBytes(c.config.Install.Device) + if err != nil { + return err + } + + if _, err := util.ParsePartitionSize(diskSize, persistentSize); err != nil { + return updateValidatorMessage(err.Error()) + } + + c.config.Install.PersistentPartitionSize = persistentSize + userInputData.HasWarnedDataDiskSize = false + if err := updateValidatorMessage(""); err != nil { + return err + } + + return gotoNextPage() + } + persistentSizeV.KeyBindings = map[gocui.Key]func(*gocui.Gui, *gocui.View) error{ + gocui.KeyEnter: persistentSizeConfirm, gocui.KeyArrowUp: func(g *gocui.Gui, v *gocui.View) error { userInputData.HasWarnedDataDiskSize = false - return updateValidatorMessage("") - }, - gocui.KeyEsc: func(g *gocui.Gui, v *gocui.View) error { - return gotoPrevPage() + if err := updateValidatorMessage(""); err != nil { + return err + } + + return showNext(c, dataDiskPanel) }, + gocui.KeyEsc: gotoPrevPage, } return nil diff --git a/pkg/console/util.go b/pkg/console/util.go index 3d54243d4..91a70566e 100644 --- a/pkg/console/util.go +++ b/pkg/console/util.go @@ -489,7 +489,7 @@ func doInstall(g *gocui.Gui, hvstConfig *config.HarvesterConfig, webhooks Render if hvstConfig.ShouldCreateDataPartitionOnOsDisk() { // Use custom layout (which also creates Longhorn partition) when needed - elementalConfig, err = config.CreateRootPartitioningLayout(elementalConfig, hvstConfig.Install.Device) + elementalConfig, err = config.CreateRootPartitioningLayout(elementalConfig, hvstConfig) if err != nil { return err } @@ -646,20 +646,20 @@ func validateDiskSize(devPath string) error { if err != nil { return err } - if diskSizeBytes>>30 < config.HardMinDiskSizeGiB { - return fmt.Errorf("Disk size too small. Minimum %dGB is required", config.HardMinDiskSizeGiB) + if util.ByteToGi(diskSizeBytes) < config.HardMinDiskSizeGiB { + return fmt.Errorf("Disk size is too small. Minimum %dGi is required", config.HardMinDiskSizeGiB) } return nil } -func validateDiskSizeSoft(devPath string) error { +func validateDataDiskSize(devPath string) error { diskSizeBytes, err := util.GetDiskSizeBytes(devPath) if err != nil { return err } - if diskSizeBytes>>30 < config.SoftMinDiskSizeGiB { - return fmt.Errorf("Disk size is smaller than the recommended size: %dGB", config.SoftMinDiskSizeGiB) + if util.ByteToGi(diskSizeBytes) < config.HardMinDataDiskSizeGiB { + return fmt.Errorf("Disk size is too small. Minimum %dGi is required", config.HardMinDataDiskSizeGiB) } return nil diff --git a/pkg/console/validator.go b/pkg/console/validator.go index 44ba7f298..56a9430cc 100644 --- a/pkg/console/validator.go +++ b/pkg/console/validator.go @@ -95,7 +95,7 @@ func checkInterface(iface config.NetworkInterface) error { return prettyError(ErrMsgInterfaceNotFound, iface.Name) } -func checkDevice(device string) error { +func checkDevice(device string, isDataDisk bool) error { if device == "" { return errors.New(ErrMsgDeviceNotSpecified) } @@ -130,8 +130,14 @@ func checkDevice(device string) error { return prettyError(ErrMsgDeviceNotFound, device) } - if err := validateDiskSize(device); err != nil { - return prettyError(ErrMsgDeviceTooSmall, device) + if isDataDisk { + if err := validateDataDiskSize(device); err != nil { + return prettyError(ErrMsgDeviceTooSmall, device) + } + } else { + if err := validateDiskSize(device); err != nil { + return prettyError(ErrMsgDeviceTooSmall, device) + } } return nil @@ -345,12 +351,12 @@ func (v ConfigValidator) Validate(cfg *config.HarvesterConfig) error { return errors.Errorf("invalid hostname. A lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.'") } - if err := checkDevice(cfg.Install.Device); err != nil { + if err := checkDevice(cfg.Install.Device, false); err != nil { return err } if cfg.Install.DataDisk != "" { - if err := checkDevice(cfg.Install.DataDisk); err != nil { + if err := checkDevice(cfg.Install.DataDisk, true); err != nil { return err } } diff --git a/pkg/util/common.go b/pkg/util/common.go index 04ab584ea..910a7a70a 100644 --- a/pkg/util/common.go +++ b/pkg/util/common.go @@ -1,5 +1,13 @@ package util +import ( + "regexp" +) + +var ( + sizeRegexp = regexp.MustCompile(`^(\d+)(Mi|Gi)$`) +) + func StringSliceContains(sSlice []string, s string) bool { for _, target := range sSlice { if target == s { @@ -17,3 +25,19 @@ func DupStrings(src []string) []string { copy(s, src) return s } + +func ByteToGi(byte uint64) uint64 { + return byte >> 30 +} + +func ByteToMi(byte uint64) uint64 { + return byte >> 20 +} + +func GiToByte(gi uint64) uint64 { + return gi << 30 +} + +func MiToByte(mi uint64) uint64 { + return mi << 20 +} diff --git a/pkg/util/disk.go b/pkg/util/disk.go new file mode 100644 index 000000000..7bb368e65 --- /dev/null +++ b/pkg/util/disk.go @@ -0,0 +1,50 @@ +package util + +import ( + "fmt" + "strconv" +) + +const ( + MinDiskSize = 250 << 30 + MinPersistentSize = 150 << 30 + MiByteMultiplier = 1 << 20 + GiByteMultiplier = 1 << 30 + + // 50Mi for COS_OEM, 15Gi for COS_STATE, 8Gi for COS_RECOVERY, 64Mi for ESP partition, 50Gi for VM data + fixedOccupiedSize = (50 + 15360 + 8192 + 64 + 51200) * MiByteMultiplier +) + +func ParsePartitionSize(diskSizeBytes uint64, partitionSize string) (uint64, error) { + if diskSizeBytes < MinDiskSize { + return 0, fmt.Errorf("Disk size is too small. Minimum %dGi is required", ByteToGi(MinDiskSize)) + } + actualDiskSizeBytes := diskSizeBytes - fixedOccupiedSize + + if !sizeRegexp.MatchString(partitionSize) { + return 0, fmt.Errorf("Partition size should be ended with 'Mi', 'Gi', and no dot and negative is allowed") + } + + size, err := strconv.ParseUint(partitionSize[:len(partitionSize)-2], 10, 64) + if err != nil { + return 0, fmt.Errorf("Failed to parse partition size: %s", partitionSize) + } + + var partitionBytes uint64 + unit := partitionSize[len(partitionSize)-2:] + switch unit { + case "Mi": + partitionBytes = size * MiByteMultiplier + case "Gi": + partitionBytes = size * GiByteMultiplier + } + + if partitionBytes < MinPersistentSize { + return 0, fmt.Errorf("Partition size is too small. Minimum %dGi is required", ByteToGi(MinPersistentSize)) + } + if partitionBytes > actualDiskSizeBytes { + return 0, fmt.Errorf("Partition size is too large. Maximum %dGi is allowed", ByteToGi(actualDiskSizeBytes)) + } + + return partitionBytes, nil +} diff --git a/pkg/util/disk_test.go b/pkg/util/disk_test.go new file mode 100644 index 000000000..c5fa0e8f1 --- /dev/null +++ b/pkg/util/disk_test.go @@ -0,0 +1,84 @@ +package util + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestParsePartitionSize(t *testing.T) { + testCases := []struct { + diskSize uint64 + partitionSize string + result uint64 + err string + }{ + { + diskSize: 300 * GiByteMultiplier, + partitionSize: "150Gi", + result: 150 * GiByteMultiplier, + }, + { + diskSize: 500 * GiByteMultiplier, + partitionSize: "153600Mi", + result: 153600 * MiByteMultiplier, + }, + { + diskSize: 2000 * GiByteMultiplier, + partitionSize: "1999Gi", + err: "Partition size is too large. Maximum 1926Gi is allowed", + }, + { + diskSize: 2000 * GiByteMultiplier, + partitionSize: "0Gi", + err: "Partition size is too small. Minimum 150Gi is required", + }, + { + diskSize: 500 * GiByteMultiplier, + partitionSize: "0Mi", + err: "Partition size is too small. Minimum 150Gi is required", + }, + { + diskSize: 100 * GiByteMultiplier, + partitionSize: "50Gi", + err: "Disk size is too small. Minimum 250Gi is required", + }, + { + diskSize: 249 * GiByteMultiplier, + partitionSize: "50Gi", + err: "Disk size is too small. Minimum 250Gi is required", + }, + { + diskSize: 2000 * GiByteMultiplier, + partitionSize: "abcd", + err: "Partition size should be ended with 'Mi', 'Gi', and no dot and negative is allowed", + }, + { + diskSize: 2000 * GiByteMultiplier, + partitionSize: "1Ti", + err: "Partition size should be ended with 'Mi', 'Gi', and no dot and negative is allowed", + }, + { + diskSize: 2000 * GiByteMultiplier, + partitionSize: "50Ki", + err: "Partition size should be ended with 'Mi', 'Gi', and no dot and negative is allowed", + }, + { + diskSize: 2000 * GiByteMultiplier, + partitionSize: "5.5", + err: "Partition size should be ended with 'Mi', 'Gi', and no dot and negative is allowed", + }, + { + diskSize: 400 * GiByteMultiplier, + partitionSize: "385933Mi", + err: "Partition size is too large. Maximum 326Gi is allowed", + }, + } + + for _, tc := range testCases { + result, err := ParsePartitionSize(tc.diskSize, tc.partitionSize) + assert.Equal(t, tc.result, result) + if err != nil { + assert.EqualError(t, err, tc.err) + } + } +}