diff --git a/cmd/limactl/disk.go b/cmd/limactl/disk.go index f940383829c..491d3f19c70 100644 --- a/cmd/limactl/disk.go +++ b/cmd/limactl/disk.go @@ -26,7 +26,10 @@ func newDiskCommand() *cobra.Command { $ limactl disk ls Delete a disk: - $ limactl disk delete DISK`, + $ limactl disk delete DISK + + Resize a disk: + $ limactl disk resize DISK --size SIZE`, SilenceUsage: true, SilenceErrors: true, } @@ -35,6 +38,7 @@ func newDiskCommand() *cobra.Command { newDiskListCommand(), newDiskDeleteCommand(), newDiskUnlockCommand(), + newDiskResizeCommand(), ) return diskCommand } @@ -171,7 +175,7 @@ func diskListAction(cmd *cobra.Command, args []string) error { } w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) - fmt.Fprintln(w, "NAME\tSIZE\tDIR\tIN-USE-BY") + fmt.Fprintln(w, "NAME\tSIZE\tFORMAT\tDIR\tIN-USE-BY") if len(disks) == 0 { logrus.Warn("No disk found. Run `limactl disk create DISK --size SIZE` to create a disk.") @@ -183,7 +187,7 @@ func diskListAction(cmd *cobra.Command, args []string) error { logrus.WithError(err).Errorf("disk %q does not exist?", diskName) continue } - fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", disk.Name, units.BytesSize(float64(disk.Size)), disk.Dir, disk.Instance) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", disk.Name, units.BytesSize(float64(disk.Size)), disk.Format, disk.Dir, disk.Instance) } return w.Flush() @@ -331,3 +335,58 @@ func diskUnlockAction(_ *cobra.Command, args []string) error { } return nil } + +func newDiskResizeCommand() *cobra.Command { + diskResizeCommand := &cobra.Command{ + Use: "resize DISK", + Example: ` +Resize a disk: +$ limactl disk resize DISK --size SIZE`, + Short: "Resize existing Lima disk", + Args: WrapArgsError(cobra.ExactArgs(1)), + RunE: diskResizeAction, + } + diskResizeCommand.Flags().String("size", "", "Disk size") + _ = diskResizeCommand.MarkFlagRequired("size") + return diskResizeCommand +} + +func diskResizeAction(cmd *cobra.Command, args []string) error { + size, err := cmd.Flags().GetString("size") + if err != nil { + return err + } + + diskSize, err := units.RAMInBytes(size) + if err != nil { + return err + } + + diskName := args[0] + disk, err := store.InspectDisk(diskName) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("disk %q does not exists", diskName) + } + return err + } + + // Shrinking can cause a disk failure + if diskSize < disk.Size { + return fmt.Errorf("specified size %q is less than the current disk size %q. Disk shrinking is currently unavailable", units.BytesSize(float64(diskSize)), units.BytesSize(float64(disk.Size))) + } + + if disk.Instance != "" { + inst, err := store.Inspect(disk.Instance) + if err == nil { + if inst.Status == store.StatusRunning { + return fmt.Errorf("cannot resize disk %q used by running instance %q. Please stop the VM instance", diskName, disk.Instance) + } + } + } + if err := qemu.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize)); err != nil { + return fmt.Errorf("failed to resize disk %q: %w", diskName, err) + } + logrus.Infof("Resized disk %q (%q)", diskName, disk.Dir) + return nil +} diff --git a/hack/test-templates.sh b/hack/test-templates.sh index e1e411dd80f..0b601837252 100755 --- a/hack/test-templates.sh +++ b/hack/test-templates.sh @@ -301,6 +301,11 @@ if [[ -n ${CHECKS["restart"]} ]]; then limactl stop "$NAME" sleep 3 + if [[ -n ${CHECKS["disk"]} ]]; then + INFO "Resize disk and verify that partition and fs size are increased" + limactl disk resize data --size 11G + fi + export ftp_proxy=my.proxy:8021 INFO "Restarting \"$NAME\"" limactl start "$NAME" @@ -325,6 +330,10 @@ if [[ -n ${CHECKS["restart"]} ]]; then ERROR "Disk does not persist across restarts" exit 1 fi + if ! limactl shell "$NAME" sh -c 'df -h /mnt/lima-data/ --output=size | grep -q 11G'; then + ERROR "Disk FS does not resized after restart" + exit 1 + fi fi fi diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-disks.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-disks.sh index d31750bf692..d92c2c4502a 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-disks.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/05-lima-disks.sh @@ -30,4 +30,17 @@ for i in $(seq 0 $((LIMA_CIDATA_DISKS - 1))); do mkdir -p "/mnt/lima-${DISK_NAME}" mount -t $FORMAT_FSTYPE "/dev/${DEVICE_NAME}1" "/mnt/lima-${DISK_NAME}" + if command -v growpart >/dev/null 2>&1 && command -v resize2fs >/dev/null 2>&1; then + growpart "/dev/${DEVICE_NAME}" 1 || true + # Only resize when filesystem is in a healthy state + if command -v "fsck.$FORMAT_FSTYPE" -f -p "/dev/disk/by-label/lima-${DISK_NAME}"; then + if [[ $FORMAT_FSTYPE == "ext2" || $FORMAT_FSTYPE == "ext3" || $FORMAT_FSTYPE == "ext4" ]]; then + resize2fs "/dev/disk/by-label/lima-${DISK_NAME}" || true + elif [ "$FORMAT_FSTYPE" == "xfs" ]; then + xfs_growfs "/dev/disk/by-label/lima-${DISK_NAME}" || true + else + echo >&2 "WARNING: unknown fs '$FORMAT_FSTYPE'. FS will not be grew up automatically" + fi + fi + fi done diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go index c6f68c223d8..173cf19d05b 100644 --- a/pkg/qemu/qemu.go +++ b/pkg/qemu/qemu.go @@ -137,6 +137,17 @@ func CreateDataDisk(dir, format string, size int) error { return nil } +func ResizeDataDisk(dir, format string, size int) error { + dataDisk := filepath.Join(dir, filenames.DataDisk) + + args := []string{"resize", "-f", format, dataDisk, strconv.Itoa(size)} + cmd := exec.Command("qemu-img", args...) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) + } + return nil +} + func newQmpClient(cfg Config) (*qmp.SocketMonitor, error) { qmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock) qmpClient, err := qmp.NewSocketMonitor("unix", qmpSock, 5*time.Second) diff --git a/pkg/store/disk.go b/pkg/store/disk.go index 8a8ee5c2056..d065be59864 100644 --- a/pkg/store/disk.go +++ b/pkg/store/disk.go @@ -15,6 +15,7 @@ import ( type Disk struct { Name string `json:"name"` Size int64 `json:"size"` + Format string `json:"format"` Dir string `json:"dir"` Instance string `json:"instance"` InstanceDir string `json:"instanceDir"` @@ -37,7 +38,7 @@ func InspectDisk(diskName string) (*Disk, error) { return nil, err } - disk.Size, err = inspectDiskSize(dataDisk) + disk.Size, disk.Format, err = inspectDisk(dataDisk) if err != nil { return nil, err } @@ -57,32 +58,33 @@ func InspectDisk(diskName string) (*Disk, error) { return disk, nil } -// inspectDiskSize attempts to inspect the disk size by itself, -// and falls back to inspectDiskSizeWithQemuImg on an error. -func inspectDiskSize(fName string) (int64, error) { +// inspectDisk attempts to inspect the disk size and format by itself, +// and falls back to inspectDiskWithQemuImg on an error. +func inspectDisk(fName string) (int64, string, error) { f, err := os.Open(fName) if err != nil { - return inspectDiskSizeWithQemuImg(fName) + return inspectDiskWithQemuImg(fName) } defer f.Close() img, err := qcow2reader.Open(f) if err != nil { - return inspectDiskSizeWithQemuImg(fName) + return inspectDiskWithQemuImg(fName) } sz := img.Size() if sz < 0 { - return inspectDiskSizeWithQemuImg(fName) + return inspectDiskWithQemuImg(fName) } - return sz, nil + + return sz, string(img.Type()), nil } -// inspectDiskSizeWithQemuImg invokes `qemu-img` binary to inspect the disk size. -func inspectDiskSizeWithQemuImg(fName string) (int64, error) { +// inspectDiskSizeWithQemuImg invokes `qemu-img` binary to inspect the disk size and format. +func inspectDiskWithQemuImg(fName string) (int64, string, error) { info, err := imgutil.GetInfo(fName) if err != nil { - return -1, err + return -1, "", err } - return info.VSize, nil + return info.VSize, info.Format, nil } func (d *Disk) Lock(instanceDir string) error {