From d78b0a89e61afbb73790c561653acda1d79d6f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Mon, 26 Aug 2024 16:53:47 -0400 Subject: [PATCH 1/5] incusd/storage/lvm: Allow live resize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- .../storage/drivers/driver_lvm_utils.go | 34 +++++++++++++------ .../storage/drivers/driver_lvm_volumes.go | 19 +++++++++-- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/internal/server/storage/drivers/driver_lvm_utils.go b/internal/server/storage/drivers/driver_lvm_utils.go index f573dce1f71..4ae43d56007 100644 --- a/internal/server/storage/drivers/driver_lvm_utils.go +++ b/internal/server/storage/drivers/driver_lvm_utils.go @@ -435,18 +435,14 @@ func (d *lvm) createLogicalVolumeSnapshot(vgName string, srcVol Volume, snapVol // If clustered, we need to acquire exclusive write access for this operation. if d.clustered && !makeThinLv { parent, _, _ := api.GetParentAndSnapshotName(srcVol.Name()) - volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], srcVol.volType, srcVol.contentType, parent) + parentVol := NewVolume(d, d.Name(), srcVol.volType, srcVol.contentType, parent, srcVol.config, srcVol.poolConfig) - if util.PathExists(volDevPath) { - _, err := subprocess.TryRunCommand("lvchange", "--activate", "ey", "--ignoreactivationskip", volDevPath) - if err != nil { - return "", fmt.Errorf("Failed to acquire exclusive lock on LVM logical volume %q: %w", volDevPath, err) - } - - defer func() { - _, _ = subprocess.TryRunCommand("lvchange", "--activate", "sy", "--ignoreactivationskip", volDevPath) - }() + release, err := d.acquireExclusive(parentVol) + if err != nil { + return "", err } + + defer release() } _, err = subprocess.TryRunCommand("lvcreate", args...) @@ -466,6 +462,24 @@ func (d *lvm) createLogicalVolumeSnapshot(vgName string, srcVol Volume, snapVol return targetVolDevPath, nil } +// acquireExclusive switches a volume lock to exclusive mode. +func (d *lvm) acquireExclusive(vol Volume) (func(), error) { + volDevPath := d.lvmDevPath(d.config["lvm.vg_name"], vol.volType, vol.contentType, vol.name) + + if !d.clustered || !util.PathExists(volDevPath) { + return func() {}, nil + } + + _, err := subprocess.TryRunCommand("lvchange", "--activate", "ey", "--ignoreactivationskip", volDevPath) + if err != nil { + return nil, fmt.Errorf("Failed to acquire exclusive lock on LVM logical volume %q: %w", volDevPath, err) + } + + return func() { + _, _ = subprocess.TryRunCommand("lvchange", "--activate", "sy", "--ignoreactivationskip", volDevPath) + }, nil +} + // removeLogicalVolume removes a logical volume. func (d *lvm) removeLogicalVolume(volDevPath string) error { _, err := subprocess.TryRunCommand("lvremove", "-f", volDevPath) diff --git a/internal/server/storage/drivers/driver_lvm_volumes.go b/internal/server/storage/drivers/driver_lvm_volumes.go index 92924e6d07c..fbf9ed624f9 100644 --- a/internal/server/storage/drivers/driver_lvm_volumes.go +++ b/internal/server/storage/drivers/driver_lvm_volumes.go @@ -495,6 +495,14 @@ func (d *lvm) SetVolumeQuota(vol Volume, size string, allowUnsafeResize bool, op return err } } else if sizeBytes > oldSizeBytes { + // Get exclusive mode if active. + release, err := d.acquireExclusive(vol) + if err != nil { + return err + } + + defer release() + // Grow block device first. err = d.resizeLogicalVolume(volDevPath, sizeBytes) if err != nil { @@ -528,12 +536,17 @@ func (d *lvm) SetVolumeQuota(vol Volume, size string, allowUnsafeResize bool, op if sizeBytes < oldSizeBytes { return fmt.Errorf("Block volumes cannot be shrunk: %w", ErrCannotBeShrunk) } + } - if inUse { - return ErrInUse // We don't allow online resizing of block volumes. - } + // Get exclusive mode. + release, err := d.acquireExclusive(vol) + if err != nil { + return err } + defer release() + + // Resize the block device. err = d.resizeLogicalVolume(volDevPath, sizeBytes) if err != nil { return err From 0d8561e95d0f0eac1f4a5c497916f950dc6a6db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Mon, 26 Aug 2024 20:41:38 -0400 Subject: [PATCH 2/5] incusd/storage/zfs: Allow online resize of ZFS block volumes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- internal/server/storage/drivers/driver_zfs_volumes.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/server/storage/drivers/driver_zfs_volumes.go b/internal/server/storage/drivers/driver_zfs_volumes.go index 64f3cbeafa7..0c6d3cf5626 100644 --- a/internal/server/storage/drivers/driver_zfs_volumes.go +++ b/internal/server/storage/drivers/driver_zfs_volumes.go @@ -1792,10 +1792,6 @@ func (d *zfs) SetVolumeQuota(vol Volume, size string, allowUnsafeResize bool, op if sizeBytes < oldVolSizeBytes { return fmt.Errorf("Block volumes cannot be shrunk: %w", ErrCannotBeShrunk) } - - if inUse { - return ErrInUse // We don't allow online resizing of block volumes. - } } err = d.setDatasetProperties(d.dataset(vol, false), fmt.Sprintf("volsize=%d", sizeBytes)) From c13e9298cc6341bdb522b91ea53bbb91e6865eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Mon, 26 Aug 2024 20:41:57 -0400 Subject: [PATCH 3/5] incusd/device/disk: Add callback on resize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- .../server/device/config/device_runconfig.go | 1 + internal/server/device/disk.go | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/internal/server/device/config/device_runconfig.go b/internal/server/device/config/device_runconfig.go index d4cac687d03..b2603f085f9 100644 --- a/internal/server/device/config/device_runconfig.go +++ b/internal/server/device/config/device_runconfig.go @@ -30,6 +30,7 @@ type MountEntryItem struct { PassNo int // Used by fsck(8) to determine the order in which filesystem checks are done at boot time. Defaults to zero (don't fsck) if not present. OwnerShift string // Ownership shifting mode, use constants MountOwnerShiftNone, MountOwnerShiftStatic or MountOwnerShiftDynamic. Limits *DiskLimits // Disk limits. + Size int64 // Expected disk size in bytes. } // RootFSEntryItem represents the root filesystem options for an Instance. diff --git a/internal/server/device/disk.go b/internal/server/device/disk.go index 37661ac299e..7c1870b8e3d 100644 --- a/internal/server/device/disk.go +++ b/internal/server/device/disk.go @@ -1483,6 +1483,26 @@ func (d *disk) Update(oldDevices deviceConfig.Devices, isRunning bool) error { d.logger.Warn("Could not apply quota because disk is in use, deferring until next start") } else if err != nil { return err + } else if d.inst.Type() == instancetype.VM && d.inst.IsRunning() { + // Get the disk size in bytes. + size, err := units.ParseByteSizeString(newRootDiskDeviceSize) + if err != nil { + return err + } + + // Notify to reload disk size. + runConf := deviceConfig.RunConfig{} + runConf.Mounts = []deviceConfig.MountEntryItem{ + { + DevName: d.name, + Size: size, + }, + } + + err = d.inst.DeviceEventHandler(&runConf) + if err != nil { + return err + } } } } From 17fb18ef07b1369f59bc9181e9657f7c6e1ee3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Mon, 26 Aug 2024 20:42:25 -0400 Subject: [PATCH 4/5] incusd/instance/drivers/qmp: Add resize handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- .../server/instance/drivers/qmp/commands.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internal/server/instance/drivers/qmp/commands.go b/internal/server/instance/drivers/qmp/commands.go index 53a79903797..34173c5d6d5 100644 --- a/internal/server/instance/drivers/qmp/commands.go +++ b/internal/server/instance/drivers/qmp/commands.go @@ -1043,6 +1043,24 @@ func (m *Monitor) Eject(id string) error { return nil } +// UpdateBlockSize updates the size of a disk. +func (m *Monitor) UpdateBlockSize(id string) error { + var args struct { + NodeName string `json:"node-name"` + Size int64 `json:"size"` + } + + args.NodeName = id + args.Size = 1 + + err := m.run("block_resize", args, nil) + if err != nil { + return err + } + + return nil +} + // SetBlockThrottle applies an I/O limit on a disk. func (m *Monitor) SetBlockThrottle(id string, bytesRead int, bytesWrite int, iopsRead int, iopsWrite int) error { var args struct { From de3ea2ec6e7ac112ad0e91c0c08339adbae368b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Mon, 26 Aug 2024 20:42:38 -0400 Subject: [PATCH 5/5] incusd/instance/qemu: Add disk resize handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Graber --- .../server/instance/drivers/driver_qemu.go | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/internal/server/instance/drivers/driver_qemu.go b/internal/server/instance/drivers/driver_qemu.go index d96955729f3..21c13fa4d7c 100644 --- a/internal/server/instance/drivers/driver_qemu.go +++ b/internal/server/instance/drivers/driver_qemu.go @@ -8129,7 +8129,7 @@ func (d *qemu) DeviceEventHandler(runConf *deviceConfig.RunConfig) error { // Handle disk reconfiguration. for _, mount := range runConf.Mounts { - if mount.Limits == nil { + if mount.Limits == nil && mount.Size == 0 { continue } @@ -8142,10 +8142,20 @@ func (d *qemu) DeviceEventHandler(runConf *deviceConfig.RunConfig) error { // Figure out the QEMU device ID. devID := fmt.Sprintf("%s%s", qemuDeviceIDPrefix, linux.PathNameEncode(mount.DevName)) - // Apply the limits. - err = m.SetBlockThrottle(devID, int(mount.Limits.ReadBytes), int(mount.Limits.WriteBytes), int(mount.Limits.ReadIOps), int(mount.Limits.WriteIOps)) - if err != nil { - return fmt.Errorf("Failed applying limits for disk device %q: %w", mount.DevName, err) + if mount.Limits != nil { + // Apply the limits. + err = m.SetBlockThrottle(devID, int(mount.Limits.ReadBytes), int(mount.Limits.WriteBytes), int(mount.Limits.ReadIOps), int(mount.Limits.WriteIOps)) + if err != nil { + return fmt.Errorf("Failed applying limits for disk device %q: %w", mount.DevName, err) + } + } + + if mount.Size > 0 { + // Update the size. + err = m.UpdateBlockSize(strings.SplitN(devID, "-", 2)[1]) + if err != nil { + return fmt.Errorf("Failed updating disk size %q: %w", mount.DevName, err) + } } }