This patch add support for disk hot resizing when using a KVM hypervisor. It's done by calling the "block_resize" QMP command. It can be enabled via the new '--hot-resize' option of gnt-instance grow-disk but it remains disabled by default.
Signed-off-by: Yoann Laissus <[email protected]> --- lib/backend.py | 33 ++++++++++++++++++++++ lib/cli_opts.py | 6 ++++ lib/client/gnt_instance.py | 5 ++-- lib/cmdlib/instance_storage.py | 46 ++++++++++++++++++++++++++++--- lib/hypervisor/hv_base.py | 14 ++++++++++ lib/hypervisor/hv_kvm/__init__.py | 58 +++++++++++++++++++++++++++++++++++++++ lib/hypervisor/hv_kvm/monitor.py | 55 +++++++++++++++++++++++++++++++++++++ lib/rapi/client.py | 3 +- lib/rpc_defs.py | 8 ++++++ lib/server/noded.py | 19 +++++++++++++ src/Ganeti/OpCodes.hs | 1 + src/Ganeti/OpParams.hs | 4 +++ test/hs/Test/Ganeti/OpCodes.hs | 1 + 13 files changed, 246 insertions(+), 7 deletions(-) diff --git a/lib/backend.py b/lib/backend.py index f80fc20..d0743f2 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -3004,6 +3004,39 @@ def HotplugSupported(instance): _Fail("Hotplug is not supported: %s", err) +def HotResizeDisk(instance, device, size): + """Hot resize a disk + + Hot resizing is currently supported only for KVM Hypervisor. + It's available since QEMU 0.14. + @type instance: L{objects.Instance} + @param instance: the instance to which we hot resize a disk + @type device: L{objects.Disk} + @param device: the disk object to hot resize + @raise RPCFail: in case instance does not have KVM hypervisor + + """ + hyper = hypervisor.GetHypervisor(instance.hypervisor) + try: + hyper.DiskHotResizeSupported(instance) + except errors.HotplugError, err: + _Fail("Hot resizing is not supported: %s", err) + + hyper.HotResizeDisk(instance, device, size) + + + +def DiskHotResizeSupported(instance): + """Checks if disk hot resizing is generally supported. + + """ + hyper = hypervisor.GetHypervisor(instance.hypervisor) + try: + hyper.DiskHotResizeSupported(instance) + except errors.HotplugError, err: + _Fail("Hot resizing is not supported: %s", err) + + def ModifyInstanceMetadata(metadata): """Sends instance data to the metadata daemon. diff --git a/lib/cli_opts.py b/lib/cli_opts.py index a6126e9..3f8677f 100644 --- a/lib/cli_opts.py +++ b/lib/cli_opts.py @@ -104,6 +104,7 @@ __all__ = [ "HELPER_SHUTDOWN_TIMEOUT_OPT", "HELPER_STARTUP_TIMEOUT_OPT", "HID_OS_OPT", + "HOT_RESIZE_OPT", "HOTPLUG_IF_POSSIBLE_OPT", "HOTPLUG_OPT", "HV_STATE_OPT", @@ -1513,6 +1514,11 @@ INCLUDEDEFAULTS_OPT = cli_option("--include-defaults", dest="include_defaults", default=False, action="store_true", help="Include default values") +HOT_RESIZE_OPT = cli_option("--hot-resize", dest="hot_resize", + action="store_true", default=False, + help="Make the extra space available for the" + "running instance without rebooting.") + HOTPLUG_OPT = cli_option("--hotplug", dest="hotplug", action="store_true", default=False, help="Hotplug supported devices (NICs and Disks)") diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py index 7a49d60..2054e8c 100644 --- a/lib/client/gnt_instance.py +++ b/lib/client/gnt_instance.py @@ -609,7 +609,8 @@ def GrowDisk(opts, args): disk=disk, amount=amount, wait_for_sync=opts.wait_for_sync, absolute=opts.absolute, - ignore_ipolicy=opts.ignore_ipolicy + ignore_ipolicy=opts.ignore_ipolicy, + hot_resize=opts.hot_resize ) SubmitOrSend(op, opts) return 0 @@ -1708,7 +1709,7 @@ commands = { [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1), ArgUnknown(min=1, max=1)], SUBMIT_OPTS + - [NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT, ABSOLUTE_OPT, IGNORE_IPOLICY_OPT], + [NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT, ABSOLUTE_OPT, IGNORE_IPOLICY_OPT, HOT_RESIZE_OPT], "<instance> <disk> <size>", "Grow an instance's disk"), "change-group": ( ChangeGroup, ARGS_ONE_INSTANCE, diff --git a/lib/cmdlib/instance_storage.py b/lib/cmdlib/instance_storage.py index 3f609d6..e16e26f 100644 --- a/lib/cmdlib/instance_storage.py +++ b/lib/cmdlib/instance_storage.py @@ -1710,6 +1710,7 @@ class LUInstanceGrowDisk(LogicalUnit): "DISK": self.op.disk, "AMOUNT": self.op.amount, "ABSOLUTE": self.op.absolute, + "HOT_RESIZE": self.op.hot_resize, } env.update(BuildInstanceHookEnvByObject(self, self.instance)) return env @@ -1764,6 +1765,8 @@ class LUInstanceGrowDisk(LogicalUnit): self._CheckIPolicy(self.target) + self._CheckDiskHotResize() + def _CheckDiskSpace(self, node_uuids, req_vgspace): template = self.disk.dev_type if (template not in constants.DTS_NO_FREE_SPACE_CHECK and @@ -1798,6 +1801,27 @@ class LUInstanceGrowDisk(LogicalUnit): else: raise errors.OpPrereqError(msg, errors.ECODE_INVAL) + def _CheckDiskHotResize(self): + if self.op.hot_resize: + result = self.rpc.call_disk_hot_resize_supported(self.instance.primary_node, + self.instance) + if result.fail_msg: + result.Raise("Hot resizing is not possible: %s" % result.fail_msg, + prereq=True, ecode=errors.ECODE_STATE) + else: + self.LogInfo("Modification will take place without hot resizing.") + + def _HotResizeDisk(self, device, size): + result = self.rpc.call_hot_resize_disk(self.instance.primary_node, + self.instance, + (device, self.instance), + size) + if result.fail_msg: + self.LogWarning("Could not hot resize disk: %s" % result.fail_msg) + self.LogInfo("Continuing execution..") + else: + self.LogInfo("Hot resize successful") + def Exec(self, feedback_fn): """Execute disk grow. @@ -1835,14 +1859,14 @@ class LUInstanceGrowDisk(LogicalUnit): result.Raise("Failed to retrieve disk size from node '%s'" % self.instance.primary_node) - (disk_dimensions, ) = result.payload + (old_disk_dimensions, ) = result.payload - if disk_dimensions is None: + if old_disk_dimensions is None: raise errors.OpExecError("Failed to retrieve disk size from primary" " node '%s'" % self.instance.primary_node) - (disk_size_in_bytes, _) = disk_dimensions + (old_disk_size_in_bytes, _) = old_disk_dimensions - old_disk_size = _DiskSizeInBytesToMebibytes(self, disk_size_in_bytes) + old_disk_size = _DiskSizeInBytesToMebibytes(self, old_disk_size_in_bytes) assert old_disk_size >= self.disk.size, \ ("Retrieved disk size too small (got %s, should be at least %s)" % @@ -1889,6 +1913,20 @@ class LUInstanceGrowDisk(LogicalUnit): WipeDisks(self, self.instance, disks=[(self.op.disk, self.disk, old_disk_size)]) + if self.op.hot_resize: + result = self.rpc.call_blockdev_getdimensions( + self.instance.primary_node, [([self.disk], self.instance)]) + result.Raise("Failed to retrieve disk size from node '%s'" % + self.instance.primary_node) + + (new_disk_dimensions, ) = result.payload + + if new_disk_dimensions is None: + raise errors.OpExecError("Failed to retrieve disk size from primary" + " node '%s'" % self.instance.primary_node) + (new_disk_size_in_bytes, _) = new_disk_dimensions + self._HotResizeDisk(self.disk, new_disk_size_in_bytes) + if self.op.wait_for_sync: disk_abort = not WaitForSync(self, self.instance, disks=[self.disk]) if disk_abort: diff --git a/lib/hypervisor/hv_base.py b/lib/hypervisor/hv_base.py index 8aaa062..b5b973f 100644 --- a/lib/hypervisor/hv_base.py +++ b/lib/hypervisor/hv_base.py @@ -781,3 +781,17 @@ class BaseHypervisor(object): """ raise errors.HotplugError("Hotplug is not supported by this hypervisor") + + def DiskHotResizeSupported(self, instance): + """Checks if hot resizing is supported. + + By default is not. Currently only KVM hypervisor supports it. + + """ + raise errors.HotplugError("Hot resizing is not supported by this hypervisor") + + def HotResizeDisk(self, instance, device, size): + """Hot resize a disk. + + """ + raise errors.HotplugError("Hot resizing is not supported by this hypervisor") \ No newline at end of file diff --git a/lib/hypervisor/hv_kvm/__init__.py b/lib/hypervisor/hv_kvm/__init__.py index 340ddb1..7d9e036 100644 --- a/lib/hypervisor/hv_kvm/__init__.py +++ b/lib/hypervisor/hv_kvm/__init__.py @@ -1964,6 +1964,64 @@ class KVMHypervisor(hv_base.BaseHypervisor): device.pci = self.HotDelDevice(instance, dev_type, device, _, seq) self.HotAddDevice(instance, dev_type, device, _, seq) + @_with_qmp + def _VerifyHotResizeCommand(self, kvm_devid, size): + """Checks if a previous disk hot resize command was successful. + + The current block device size is retrieved and compared to the specified + size (the old one). + + @raise errors.HypervisorError: if result is not the expected one + + """ + for i in range(5): + found_size = self.qmp.GetBlockDeviceSize(kvm_devid) + logging.info("Verifying hotplug command (retry %s): %s", i, found_size) + if found_size == size: + break + time.sleep(1) + + if found_size != size: + msg = ("Device %s should have been resized to %s but its size is still " + "%s" % (kvm_devid, size, found_size)) + raise errors.HypervisorError(msg) + + logging.info("Device %s has been correctly hot resized to %s", + kvm_devid, + size) + + @_with_qmp + def DiskHotResizeSupported(self, instance): + """Verifies that hot resizing is supported. + + @raise errors.HypervisorError: in one of the previous cases + + """ + try: + output = self._CallMonitorCommand(instance.name, self._INFO_VERSION_CMD) + except errors.HypervisorError: + raise errors.HotplugError("Instance is probably down") + + self.qmp.CheckDiskHotResizeSupport() + + @_with_qmp + def HotResizeDisk(self, instance, device, size): + """ Helper method for hot resize a disk + + It gets device info from runtime file, generates the device name and + invokes the device specific method. + """ + + runtime = self._LoadKVMRuntime(instance) + entry = _GetExistingDeviceInfo(constants.HOTPLUG_TARGET_DISK, + device, + runtime) + kvm_device = _RUNTIME_DEVICE[constants.HOTPLUG_TARGET_DISK](entry) + kvm_devid = _GenerateDeviceKVMId(constants.HOTPLUG_TARGET_DISK, kvm_device) + + self.qmp.HotResizeDisk(kvm_devid, size) + self._VerifyHotResizeCommand(kvm_devid, size) + @classmethod def _ParseKVMVersion(cls, text): """Parse the KVM version from the --help output. diff --git a/lib/hypervisor/hv_kvm/monitor.py b/lib/hypervisor/hv_kvm/monitor.py index f2b7d02..e80ced4 100644 --- a/lib/hypervisor/hv_kvm/monitor.py +++ b/lib/hypervisor/hv_kvm/monitor.py @@ -579,6 +579,48 @@ class QmpConnection(MonitorSocket): #TODO: uncomment when drive_del gets implemented in upstream qemu # self.Execute("drive_del", {"id": devid}) + @_ensure_connection + def HotResizeDisk(self, devid, size): + """Hot resize a Disk + + This command is available since QEMU 0.12. + + """ + arguments = { + "device": devid, + "size": size, + } + self.Execute("block_resize", arguments) + + def _GetBlockDevices(self): + """Get the block devices of a running instance. + + """ + self._check_connection() + devices = self.Execute("query-block") + return devices + + def _GetBlockDevice(self, devid): + """Get the block devices of a running instance. + + """ + for d in self._GetBlockDevices(): + if d["device"] == devid: + return d + + return False + + @_ensure_connection + def GetBlockDeviceSize(self, devid): + """Check if a specific device exists or not on a running instance. + + It will match the PCI slot of the device and the id currently + obtained by _GenerateDeviceKVMId(). + + """ + device = self._GetBlockDevice(devid) + return device["inserted"]["image"]["virtual-size"] + def _GetPCIDevices(self): """Get the devices of the first PCI bus of a running instance. @@ -617,6 +659,19 @@ class QmpConnection(MonitorSocket): return utils.GetFreeSlot(slots) @_ensure_connection + def CheckDiskHotResizeSupport(self): + """Check if disk hot resizing is possible + + """ + def _raise(reason): + raise errors.HotplugError("Cannot hot resize disk: %s." % reason) + + if "block_resize" not in self.supported_commands: + _raise("block_resize qmp command is not supported") + if "query-block" not in self.supported_commands: + _raise("query-block qmp command is not supported") + + @_ensure_connection def CheckDiskHotAddSupport(self): """Check if disk hotplug is possible diff --git a/lib/rapi/client.py b/lib/rapi/client.py index 65f82ab..7439ef6 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -1012,7 +1012,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 (GANETI_RAPI_VERSION, instance)), query, body) def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None, - reason=None): + reason=None, hot_resize=None): """Grows a disk of an instance. More details for parameters can be found in the RAPI documentation. @@ -1036,6 +1036,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904 } _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync) + _SetItemIf(body, hot_resize is not None, "hot_resize", hot_resize) query = [] _AppendReason(query, reason) diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py index dfdc5de..c3c965b 100644 --- a/lib/rpc_defs.py +++ b/lib/rpc_defs.py @@ -308,6 +308,14 @@ _INSTANCE_CALLS = [ ("instance_metadata_modify", SINGLE, None, constants.RPC_TMO_URGENT, [ ("instance", None, "Instance object"), ], None, None, "Modify instance metadata"), + ("hot_resize_disk", SINGLE, None, constants.RPC_TMO_NORMAL, [ + ("instance", ED_INST_DICT, "Instance object"), + ("device", ED_DEVICE_DICT, "Device dict"), + ("size", None, None), + ], None, None, "Hot resize a disk of an instance to the specified size"), + ("disk_hot_resize_supported", SINGLE, None, constants.RPC_TMO_NORMAL, [ + ("instance", ED_INST_DICT, "Instance object"), + ], None, None, "Check if disk hot resizing is supported"), ] _IMPEXP_CALLS = [ diff --git a/lib/server/noded.py b/lib/server/noded.py index fe1be33..4ba912e 100644 --- a/lib/server/noded.py +++ b/lib/server/noded.py @@ -670,6 +670,25 @@ class NodeRequestHandler(http.server.HttpServerHandler): return backend.HotplugSupported(instance) @staticmethod + def perspective_hot_resize_disk(params): + """Hot resize a disk of a running instance. + + """ + (idict, ddict, size) = params + instance = objects.Instance.FromDict(idict) + device = objects.Disk.FromDict(ddict) + + return backend.HotResizeDisk(instance, device, size) + + @staticmethod + def perspective_disk_hot_resize_supported(params): + """Checks if disk hot resizing is supported. + + """ + instance = objects.Instance.FromDict(params[0]) + return backend.DiskHotResizeSupported(instance) + + @staticmethod def perspective_instance_metadata_modify(params): """Modify instance metadata. diff --git a/src/Ganeti/OpCodes.hs b/src/Ganeti/OpCodes.hs index c95cc38..794522e 100644 --- a/src/Ganeti/OpCodes.hs +++ b/src/Ganeti/OpCodes.hs @@ -708,6 +708,7 @@ $(genOpCode "OpCode" , pDiskChgAmount , pDiskChgAbsolute , pIgnoreIpolicy + , pHotResize ], "instance_name") , ("OpInstanceChangeGroup", diff --git a/src/Ganeti/OpParams.hs b/src/Ganeti/OpParams.hs index 5a8c35a..55d0c47 100644 --- a/src/Ganeti/OpParams.hs +++ b/src/Ganeti/OpParams.hs @@ -111,6 +111,7 @@ module Ganeti.OpParams , pIgnoreIpolicy , pHotplug , pHotplugIfPossible + , pHotResize , pAllowRuntimeChgs , pInstDisks , pDiskTemplate @@ -589,6 +590,9 @@ pHotplug = defaultFalse "hotplug" pHotplugIfPossible :: Field pHotplugIfPossible = defaultFalse "hotplug_if_possible" +pHotResize :: Field +pHotResize = defaultFalse "hot_resize" + pInstances :: Field pInstances = withDoc "List of instances" . diff --git a/test/hs/Test/Ganeti/OpCodes.hs b/test/hs/Test/Ganeti/OpCodes.hs index f3fc33d..6b9ace7 100644 --- a/test/hs/Test/Ganeti/OpCodes.hs +++ b/test/hs/Test/Ganeti/OpCodes.hs @@ -425,6 +425,7 @@ instance Arbitrary OpCodes.OpCode where "OP_INSTANCE_GROW_DISK" -> OpCodes.OpInstanceGrowDisk <$> genFQDN <*> return Nothing <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary + <*> arbitrary "OP_INSTANCE_CHANGE_GROUP" -> OpCodes.OpInstanceChangeGroup <$> genFQDN <*> return Nothing <*> arbitrary <*> genMaybe genNameNE <*> -- 2.4.2
