This patch adds 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 | 15 ++++++-- lib/cli_opts.py | 6 +++ lib/client/gnt_instance.py | 5 ++- lib/cmdlib/instance_set_params.py | 43 +++++++++++++++++++-- lib/cmdlib/instance_storage.py | 49 ++++++++++++++++++++++-- lib/hypervisor/hv_base.py | 8 ---- lib/hypervisor/hv_kvm/__init__.py | 79 ++++++++++++++++++++++++--------------- lib/hypervisor/hv_kvm/monitor.py | 53 ++++++++++++++++++++++++++ lib/rapi/client.py | 3 +- lib/rpc_defs.py | 2 + lib/server/noded.py | 5 ++- src/Ganeti/OpCodes.hs | 1 + src/Ganeti/OpParams.hs | 4 ++ test/hs/Test/Ganeti/OpCodes.hs | 1 + 14 files changed, 218 insertions(+), 56 deletions(-) diff --git a/lib/backend.py b/lib/backend.py index f80fc20..8021301 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -2993,17 +2993,24 @@ def HotplugDevice(instance, action, dev_type, device, extra, seq): return fn(instance, dev_type, device, extra, seq) -def HotplugSupported(instance): - """Checks if hotplug is generally supported. +def VerifyHotplugSupport(instance, action, dev_type): + """Checks if hotplug is supported on the specified device type + + @type instance: L{objects.Instance} + @param instance: the instance to which we want to hotplug a device + @type action: string + @param action: the hotplug action to perform + @type dev_type: string + @param dev_type: the device type to hotplug + @raise RPCFail: in case instance does not have KVM hypervisor """ hyper = hypervisor.GetHypervisor(instance.hypervisor) try: - hyper.HotplugSupported(instance) + hyper.VerifyHotplugSupport(instance, action, dev_type) except errors.HotplugError, err: _Fail("Hotplug 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 224ee44..c2931a5 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 @@ -1705,7 +1706,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_set_params.py b/lib/cmdlib/instance_set_params.py index 12d573a..f200df3 100644 --- a/lib/cmdlib/instance_set_params.py +++ b/lib/cmdlib/instance_set_params.py @@ -883,10 +883,45 @@ class LUInstanceSetParams(LogicalUnit): cluster_hvparams) return instance_info - def _CheckHotplug(self): + def _CheckDeviceHotplug(self, device_type, action): + """_CheckDeviceHotplug checks if an hotplug action is possible on the + specified device type. + + @type device_type: string + @param device_type: the device type to hotplug + @type action: string + @param action: the hotplug action to perform + + """ + if action in (constants.DDM_ADD, constants.DDM_ATTACH): + hotplug_action = constants.HOTPLUG_ACTION_ADD + elif action in (constants.DDM_REMOVE, constants.DDM_DETACH): + hotplug_action = constants.HOTPLUG_ACTION_REMOVE + else: + hotplug_action = constants.HOTPLUG_ACTION_MODIFY + + return self.rpc.call_hotplug_supported(self.instance.primary_node, + self.instance, + hotplug_action, + device_type) + + def _CheckDisksNicsHotplug(self): + """_CheckDisksNicsHotplug checks if hotplug is available for each disk and + NIC of the request. + If one check fail, hotplug will be globally unavailable. + + """ if self.op.hotplug or self.op.hotplug_if_possible: - result = self.rpc.call_hotplug_supported(self.instance.primary_node, - self.instance) + for op, _, _ in self.op.nics: + result = self._CheckDeviceHotplug(constants.HOTPLUG_TARGET_NIC, op) + if result.fail_msg: + break + + for op, _, _ in self.op.disks: + result = self._CheckDeviceHotplug(constants.HOTPLUG_TARGET_DISK, op) + if result.fail_msg: + break + if result.fail_msg: if self.op.hotplug: result.Raise("Hotplug is not possible: %s" % result.fail_msg, @@ -1148,7 +1183,7 @@ class LUInstanceSetParams(LogicalUnit): # dictionary with instance information after the modification ispec = {} - self._CheckHotplug() + self._CheckDisksNicsHotplug() self._PrepareNicCommunication() diff --git a/lib/cmdlib/instance_storage.py b/lib/cmdlib/instance_storage.py index 3f609d6..1a2a2a0 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,30 @@ class LUInstanceGrowDisk(LogicalUnit): else: raise errors.OpPrereqError(msg, errors.ECODE_INVAL) + def _CheckDiskHotResize(self): + if self.op.hot_resize: + result = self.rpc.call_hotplug_supported(self.instance.primary_node, + self.instance, + constants.HOTPLUG_ACTION_MODIFY, + constants.HOTPLUG_TARGET_DISK) + 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_hotplug_device(self.instance.primary_node, + self.instance, constants.HOTPLUG_ACTION_MODIFY, + constants.HOTPLUG_TARGET_DISK, + (device, self.instance), + (size,), device.logical_id) + 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 +1862,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 +1916,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..ba669d0 100644 --- a/lib/hypervisor/hv_base.py +++ b/lib/hypervisor/hv_base.py @@ -773,11 +773,3 @@ class BaseHypervisor(object): """ raise errors.HotplugError("Hotplug is not supported.") - - def HotplugSupported(self, instance): - """Checks if hotplug is supported. - - By default is not. Currently only KVM hypervisor supports it. - - """ - raise errors.HotplugError("Hotplug is not supported by this hypervisor") diff --git a/lib/hypervisor/hv_kvm/__init__.py b/lib/hypervisor/hv_kvm/__init__.py index 340ddb1..b08de59 100644 --- a/lib/hypervisor/hv_kvm/__init__.py +++ b/lib/hypervisor/hv_kvm/__init__.py @@ -1830,36 +1830,18 @@ class KVMHypervisor(hv_base.BaseHypervisor): @raise errors.HypervisorError: in one of the previous cases """ - if dev_type == constants.HOTPLUG_TARGET_DISK: - if action == constants.HOTPLUG_ACTION_ADD: - self.qmp.CheckDiskHotAddSupport() - if dev_type == constants.HOTPLUG_TARGET_NIC: - if action == constants.HOTPLUG_ACTION_ADD: - self.qmp.CheckNicHotAddSupport() - - def HotplugSupported(self, instance): - """Checks if hotplug is generally supported. - - Hotplug is *not* supported in case of: - - qemu versions < 1.7 (where all qmp related commands are supported) - - for stopped instances - - @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") - - match = self._INFO_VERSION_RE.search(output.stdout) - if not match: - raise errors.HotplugError("Cannot parse qemu version via monitor") - - #TODO: delegate more fine-grained checks to VerifyHotplugSupport - v_major, v_min, _, _ = match.groups() - if (int(v_major), int(v_min)) < (1, 7): - raise errors.HotplugError("Hotplug not supported for qemu versions < 1.7") + if action == constants.HOTPLUG_ACTION_ADD: + if dev_type == constants.HOTPLUG_TARGET_DISK: + self.qmp.CheckDiskHotAddSupport() + elif dev_type == constants.HOTPLUG_TARGET_NIC: + self.qmp.CheckNicHotAddSupport() + elif action == constants.HOTPLUG_ACTION_MODIFY: + if dev_type == constants.HOTPLUG_TARGET_DISK: + # Currently, it's the only modification supported on disks + self.qmp.CheckDiskHotResizeSupport() + elif dev_type == constants.HOTPLUG_TARGET_NIC: + # It consists of removing the NIC and adding it again + self.qmp.CheckNicHotAddSupport() @_with_qmp def _VerifyHotplugCommand(self, _instance, @@ -1952,17 +1934,52 @@ class KVMHypervisor(hv_base.BaseHypervisor): return kvm_device.pci - def HotModDevice(self, instance, dev_type, device, _, seq): + def HotModDevice(self, instance, dev_type, device, extra, seq): """ Helper method for hot-mod device It gets device info from runtime file, generates the device name and invokes the device specific method. Currently only NICs support hot-mod """ + runtime = self._LoadKVMRuntime(instance) + entry = _GetExistingDeviceInfo(dev_type, device, runtime) + kvm_device = _RUNTIME_DEVICE[dev_type](entry) + kvm_devid = _GenerateDeviceKVMId(dev_type, kvm_device) + if dev_type == constants.HOTPLUG_TARGET_NIC: # putting it back in the same pci slot device.pci = self.HotDelDevice(instance, dev_type, device, _, seq) self.HotAddDevice(instance, dev_type, device, _, seq) + elif dev_type == constants.HOTPLUG_TARGET_DISK: + new_size = extra[0] + self.qmp.HotResizeDisk(kvm_devid, new_size) + self._VerifyHotResizeCommand(kvm_devid, new_size) + + @_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. + + @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) @classmethod def _ParseKVMVersion(cls, text): diff --git a/lib/hypervisor/hv_kvm/monitor.py b/lib/hypervisor/hv_kvm/monitor.py index f2b7d02..0db8318 100644 --- a/lib/hypervisor/hv_kvm/monitor.py +++ b/lib/hypervisor/hv_kvm/monitor.py @@ -579,6 +579,45 @@ 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 to the specified size + + 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 device JSON object of the specified id + + """ + for d in self._GetBlockDevices(): + if d["device"] == devid: + return d + + return False + + @_ensure_connection + def GetBlockDeviceSize(self, devid): + """Get the size of the specified block device + + """ + 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 +656,20 @@ class QmpConnection(MonitorSocket): return utils.GetFreeSlot(slots) @_ensure_connection + def CheckDiskHotResizeSupport(self): + """Check if disk hot resizing is possible + block_resize and query-block qmp commands need to be available. + + """ + 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..3c25a71 100644 --- a/lib/rpc_defs.py +++ b/lib/rpc_defs.py @@ -304,6 +304,8 @@ _INSTANCE_CALLS = [ ], None, None, "Hoplug a device to a running instance"), ("hotplug_supported", SINGLE, None, constants.RPC_TMO_NORMAL, [ ("instance", ED_INST_DICT, "Instance object"), + ("action", None, "Hotplug Action"), + ("dev_type", None, "Device type"), ], None, None, "Check if hotplug is supported"), ("instance_metadata_modify", SINGLE, None, constants.RPC_TMO_URGENT, [ ("instance", None, "Instance object"), diff --git a/lib/server/noded.py b/lib/server/noded.py index fe1be33..6910abb 100644 --- a/lib/server/noded.py +++ b/lib/server/noded.py @@ -666,8 +666,9 @@ class NodeRequestHandler(http.server.HttpServerHandler): """Checks if hotplug is supported. """ - instance = objects.Instance.FromDict(params[0]) - return backend.HotplugSupported(instance) + (idict, action, dev_type) = params + instance = objects.Instance.FromDict(idict) + return backend.VerifyHotplugSupport(instance, action, dev_type) @staticmethod def perspective_instance_metadata_modify(params): diff --git a/src/Ganeti/OpCodes.hs b/src/Ganeti/OpCodes.hs index 271b409..cf38673 100644 --- a/src/Ganeti/OpCodes.hs +++ b/src/Ganeti/OpCodes.hs @@ -710,6 +710,7 @@ $(genOpCode "OpCode" , pDiskChgAmount , pDiskChgAbsolute , pIgnoreIpolicy + , pHotResize ], "instance_name") , ("OpInstanceChangeGroup", diff --git a/src/Ganeti/OpParams.hs b/src/Ganeti/OpParams.hs index 63bf34b..5aba89c 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 dfcb483..b5a433c 100644 --- a/test/hs/Test/Ganeti/OpCodes.hs +++ b/test/hs/Test/Ganeti/OpCodes.hs @@ -427,6 +427,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.1.4
