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

Reply via email to