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

Reply via email to