commit 06c2fb4a1eca9bf2be51a3e91bf58dd0046c1c60
Author: Dimitris Aragiorgis <dimara@grnet.gr>
Date:   Wed Oct 16 21:46:25 2013 +0300

    (snap) Snapshot support for ExtStorage
    
    Extend existing RPC params with the snapshot name and
    add allow snapshot not only for LVM but also for EXT.
    
    Signed-off-by: Dimitris Aragiorgis <dimara@grnet.gr>

diff --git a/doc/hooks.rst b/doc/hooks.rst
index c7fa9fb..4961414 100644
--- a/doc/hooks.rst
+++ b/doc/hooks.rst
@@ -370,6 +370,16 @@ Modifies the instance parameters.
 :pre-execution: master node, primary and secondary nodes
 :post-execution: master node, primary and secondary nodes
 
+OP_INSTANCE_SNAPSHOT
+++++++++++++++++++++
+
+Takes a snapshot of instance's disk (must be ext template).
+
+:directory: instance-snapshot
+:env. vars:
+:pre-execution: master node, primary and secondary nodes
+:post-execution: master node, primary and secondary nodes
+
 OP_INSTANCE_FAILOVER
 ++++++++++++++++++++
 
diff --git a/doc/rapi.rst b/doc/rapi.rst
index fba7837..be2bf06 100644
--- a/doc/rapi.rst
+++ b/doc/rapi.rst
@@ -1266,7 +1266,7 @@ Job result:
 .. _rapi-res-instances-instance_name-reinstall:
 
 ``/2/instances/[instance_name]/reinstall``
-++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++
 
 Installs the operating system again.
 
@@ -1581,6 +1581,33 @@ Job result:
 .. opcode_result:: OP_INSTANCE_SET_PARAMS
 
 
+.. _rapi-res-instances-instance_name-snapshot:
+
+``/2/instances/[instance_name]/snapshot``
++++++++++++++++++++++++++++++++++++++++++
+
+Takes snapshot of an instance's disk (must be ext template).
+
+.. rapi_resource_details:: /2/instances/[instance_name]/snapshot
+
+
+.. _rapi-res-instances-instance_name-snapshot+put:
+
+``PUT``
+~~~~~~~
+
+Returns a job ID.
+
+Body parameters:
+
+.. opcode_params:: OP_INSTANCE_SNAPSHOT
+   :exclude: instance_name
+
+Job result:
+
+.. opcode_result:: OP_INSTANCE_SNAPSHOT
+
+
 .. _rapi-res-instances-instance_name-console:
 
 ``/2/instances/[instance_name]/console``
diff --git a/lib/backend.py b/lib/backend.py
index 2c5e5ae..28f7eb6 100644
--- a/lib/backend.py
+++ b/lib/backend.py
@@ -2700,7 +2700,7 @@ def BlockdevGrow(disk, amount, dryrun, backingstore):
     _Fail("Failed to grow block device: %s", err, exc=True)
 
 
-def BlockdevSnapshot(disk):
+def BlockdevSnapshot(disk, snapshot_name=None):
   """Create a snapshot copy of a block device.
 
   This function is called recursively, and the snapshot is actually created
@@ -2717,7 +2717,7 @@ def BlockdevSnapshot(disk):
       _Fail("DRBD device '%s' without backing storage cannot be snapshotted",
             disk.unique_id)
     return BlockdevSnapshot(disk.children[0])
-  elif disk.dev_type == constants.LD_LV:
+  elif disk.dev_type == constants.LD_LV and not snapshot_name:
     r_dev = _RecursiveFindBD(disk)
     if r_dev is not None:
       # FIXME: choose a saner value for the snapshot size
@@ -2725,6 +2725,12 @@ def BlockdevSnapshot(disk):
       return r_dev.Snapshot(disk.size)
     else:
       _Fail("Cannot find block device %s", disk)
+  elif disk.dev_type == constants.DT_EXT:
+    r_dev = _RecursiveFindBD(disk)
+    if r_dev is not None:
+      r_dev.Snapshot(snapshot_name)
+    else:
+      _Fail("Cannot find block device %s", disk)
   else:
     _Fail("Cannot snapshot non-lvm block device '%s' of type '%s'",
           disk.unique_id, disk.dev_type)
diff --git a/lib/bdev.py b/lib/bdev.py
index 7226f1f..b2a6971 100644
--- a/lib/bdev.py
+++ b/lib/bdev.py
@@ -3149,9 +3149,19 @@ class ExtStorageDevice(BlockDev):
     _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
                       self.ext_params, metadata=text)
 
+  def Snapshot(self, snapshot_name):
+    """Take a snapshot of the block device.
+
+    """
+    # Call the External Storage's setinfo script,
+    # to set metadata for an existing Volume inside the External Storage
+    _ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id,
+                      self.ext_params, snapshot_name=snapshot_name)
+
 
 def _ExtStorageAction(action, unique_id, ext_params,
-                      size=None, grow=None, metadata=None):
+                      size=None, grow=None, metadata=None,
+                      snapshot_name=None):
   """Take an External Storage action.
 
   Take an External Storage action concerning or affecting
@@ -3183,7 +3193,7 @@ def _ExtStorageAction(action, unique_id, ext_params,
 
   # Create the basic environment for the driver's scripts
   create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
-                                      grow, metadata)
+                                      grow, metadata, snapshot_name)
 
   # Do not use log file for action `attach' as we need
   # to get the output from RunResult
@@ -3295,12 +3305,14 @@ def ExtStorageFromDisk(name, base_dir=None):
                        detach_script=es_files[constants.ES_SCRIPT_DETACH],
                        setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
                        verify_script=es_files[constants.ES_SCRIPT_VERIFY],
+                       snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT],
                        supported_parameters=parameters)
   return True, es_obj
 
 
 def _ExtStorageEnvironment(unique_id, ext_params,
-                           size=None, grow=None, metadata=None):
+                           size=None, grow=None, metadata=None,
+                           snapshot_name=None):
   """Calculate the environment for an External Storage script.
 
   @type unique_id: tuple (driver, vol_name)
@@ -3335,6 +3347,9 @@ def _ExtStorageEnvironment(unique_id, ext_params,
   if metadata is not None:
     result["VOL_METADATA"] = metadata
 
+  if snapshot_name is not None:
+    result["VOL_SNAPSHOT_NAME"] = snapshot_name
+
   return result
 
 
diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py
index 5a08ee0..9424f84 100644
--- a/lib/client/gnt_instance.py
+++ b/lib/client/gnt_instance.py
@@ -403,6 +403,36 @@ def ReinstallInstance(opts, args):
     return constants.EXIT_FAILURE
 
 
+def SnapshotInstance(opts, args):
+  """Snapshot an instance.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: should contain only one element, the name of the
+      instance to be reinstalled
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  instance_name  = args[0]
+  inames = _ExpandMultiNames(_EXPAND_INSTANCES, [instance_name])
+  if not inames:
+    raise errors.OpPrereqError("Selection filter does not match any instances",
+                               errors.ECODE_INVAL)
+  multi_on = len(inames) > 1
+  jex = JobExecutor(verbose=multi_on, opts=opts)
+  for instance_name in inames:
+    op = opcodes.OpInstanceSnapshot(instance_name=instance_name,
+                                    disks=opts.disks)
+    jex.QueueJob(instance_name, op)
+
+  results = jex.WaitOrShow(not opts.submit_only)
+
+  if compat.all(map(compat.fst, results)):
+    return constants.EXIT_SUCCESS
+  else:
+    return constants.EXIT_FAILURE
+
 def RemoveInstance(opts, args):
   """Remove an instance.
 
@@ -1522,6 +1552,10 @@ commands = {
      m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
      SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
     "[-f] <instance>", "Reinstall a stopped instance"),
+  "snapshot": (
+    SnapshotInstance, [ArgInstance(min=1,max=1)],
+    [DISK_OPT, SUBMIT_OPT, DRY_RUN_OPT],
+    "<instance>", "Snapshot an instance's disk(s)"),
   "remove": (
     RemoveInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT, SUBMIT_OPT,
diff --git a/lib/cmdlib/__init__.py b/lib/cmdlib/__init__.py
index 3f67039..3cae350 100644
--- a/lib/cmdlib/__init__.py
+++ b/lib/cmdlib/__init__.py
@@ -88,6 +88,7 @@ from ganeti.cmdlib.instance_operation import \
   LUInstanceStartup, \
   LUInstanceShutdown, \
   LUInstanceReinstall, \
+  LUInstanceSnapshot, \
   LUInstanceReboot, \
   LUInstanceConsole
 from ganeti.cmdlib.instance_query import \
diff --git a/lib/cmdlib/instance_operation.py b/lib/cmdlib/instance_operation.py
index 7137c15..07b6cb3 100644
--- a/lib/cmdlib/instance_operation.py
+++ b/lib/cmdlib/instance_operation.py
@@ -42,6 +42,7 @@ from ganeti.cmdlib.instance_storage import StartInstanceDisks, \
   ShutdownInstanceDisks
 from ganeti.cmdlib.instance_utils import BuildInstanceHookEnvByObject, \
   CheckInstanceBridgesExist, CheckNodeFreeMemory, CheckNodeHasOS
+from ganeti.cmdlib.instance import GetItemFromContainer
 
 
 class LUInstanceStartup(LogicalUnit):
@@ -500,3 +501,67 @@ class LUInstanceConsole(NoHooksLU):
     logging.debug("Connecting to console of %s on %s", instance.name, node)
 
     return GetInstanceConsole(self.cfg.GetClusterInfo(), instance)
+
+
+class LUInstanceSnapshot(LogicalUnit):
+  """Take a snapshot of the instance.
+
+  """
+  HPATH = "instance-snapshot"
+  HTYPE = constants.HTYPE_INSTANCE
+  REQ_BGL = False
+
+  def ExpandNames(self):
+    self._ExpandAndLockInstance()
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on master, primary and secondary nodes of the instance.
+
+    """
+    return BuildInstanceHookEnvByObject(self, self.instance)
+
+  def BuildHooksNodes(self):
+    """Build hooks nodes.
+
+    """
+    nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes)
+    return (nl, nl)
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster and is not running.
+
+    """
+    instance = self.cfg.GetInstanceInfo(self.op.instance_name)
+    assert instance is not None, \
+      "Cannot retrieve locked instance %s" % self.op.instance_name
+    CheckNodeOnline(self, instance.primary_node, "Instance primary node"
+                    " offline, cannot snapshot")
+
+    self.snapshots = []
+    for ident, params in self.op.disks:
+      idx, disk = GetItemFromContainer(ident, 'disk', instance.disks)
+      snapshot_name = params.get("snapshot_name", None)
+      if not snapshot_name:
+        raise errors.OpPrereqError("No snapshot_name passed for disk %s", ident)
+      self.snapshots.append((idx, disk, snapshot_name))
+
+    self.instance = instance
+
+  def Exec(self, feedback_fn):
+    """Take a snapshot of the instance the instance.
+
+    """
+    inst = self.instance
+    node_uuid = inst.primary_node
+    for idx, disk, snapshot_name in self.snapshots:
+      self.cfg.SetDiskID(disk, node_uuid)
+      feedback_fn("Taking a snapshot of instance...")
+      result = self.rpc.call_blockdev_snapshot(node_uuid,
+                                               (disk, inst),
+                                               snapshot_name)
+      result.Raise("Could not take a snapshot for instance %s disk/%d %s"
+                   " on node %s" % (inst, idx, disk.uuid, inst.primary_node))
diff --git a/lib/cmdlib/instance_storage.py b/lib/cmdlib/instance_storage.py
index e3128fe..5f99724 100644
--- a/lib/cmdlib/instance_storage.py
+++ b/lib/cmdlib/instance_storage.py
@@ -537,6 +537,7 @@ class LUInstanceRecreateDisks(LogicalUnit):
     constants.IDISK_METAVG,
     constants.IDISK_PROVIDER,
     constants.IDISK_NAME,
+    constants.IDISK_SNAPSHOT_NAME,
     ]))
 
   def _RunAllocator(self):
diff --git a/lib/constants.py b/lib/constants.py
index 7d00387..d72afc8 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -825,6 +825,7 @@ ES_ACTION_ATTACH = "attach"
 ES_ACTION_DETACH = "detach"
 ES_ACTION_SETINFO = "setinfo"
 ES_ACTION_VERIFY = "verify"
+ES_ACTION_SNAPSHOT = "snapshot"
 
 ES_SCRIPT_CREATE = ES_ACTION_CREATE
 ES_SCRIPT_REMOVE = ES_ACTION_REMOVE
@@ -833,6 +834,7 @@ ES_SCRIPT_ATTACH = ES_ACTION_ATTACH
 ES_SCRIPT_DETACH = ES_ACTION_DETACH
 ES_SCRIPT_SETINFO = ES_ACTION_SETINFO
 ES_SCRIPT_VERIFY = ES_ACTION_VERIFY
+ES_SCRIPT_SNAPSHOT = ES_ACTION_SNAPSHOT
 ES_SCRIPTS = frozenset([
   ES_SCRIPT_CREATE,
   ES_SCRIPT_REMOVE,
@@ -840,7 +842,8 @@ ES_SCRIPTS = frozenset([
   ES_SCRIPT_ATTACH,
   ES_SCRIPT_DETACH,
   ES_SCRIPT_SETINFO,
-  ES_SCRIPT_VERIFY
+  ES_SCRIPT_VERIFY,
+  ES_SCRIPT_SNAPSHOT
   ])
 
 ES_PARAMETERS_FILE = "parameters.list"
@@ -974,7 +977,6 @@ HV_VNET_HDR = "vnet_hdr"
 HV_VIRIDIAN = "viridian"
 HV_VIF_SCRIPT = "vif_script"
 
-
 HVS_PARAMETER_TYPES = {
   HV_KVM_PATH: VTYPE_STRING,
   HV_BOOT_ORDER: VTYPE_STRING,
@@ -1364,6 +1366,7 @@ IDISK_VG = "vg"
 IDISK_METAVG = "metavg"
 IDISK_PROVIDER = "provider"
 IDISK_NAME = "name"
+IDISK_SNAPSHOT_NAME = "snapshot_name"
 IDISK_PARAMS_TYPES = {
   IDISK_SIZE: VTYPE_SIZE,
   IDISK_MODE: VTYPE_STRING,
@@ -1372,6 +1375,7 @@ IDISK_PARAMS_TYPES = {
   IDISK_METAVG: VTYPE_STRING,
   IDISK_PROVIDER: VTYPE_STRING,
   IDISK_NAME: VTYPE_MAYBE_STRING,
+  IDISK_SNAPSHOT_NAME: VTYPE_STRING,
   }
 IDISK_PARAMS = frozenset(IDISK_PARAMS_TYPES.keys())
 
diff --git a/lib/masterd/instance.py b/lib/masterd/instance.py
index 0954011..9123bc6 100644
--- a/lib/masterd/instance.py
+++ b/lib/masterd/instance.py
@@ -1165,7 +1165,9 @@ class ExportInstanceHelper:
 
       # result.payload will be a snapshot of an lvm leaf of the one we
       # passed
-      result = self._lu.rpc.call_blockdev_snapshot(src_node, (disk, instance))
+      result = self._lu.rpc.call_blockdev_snapshot(src_node,
+                                                   (disk, instance),
+                                                   None)
       new_dev = False
       msg = result.fail_msg
       if msg:
diff --git a/lib/objects.py b/lib/objects.py
index a00e749..a1d5e4c 100644
--- a/lib/objects.py
+++ b/lib/objects.py
@@ -1326,6 +1326,7 @@ class ExtStorage(ConfigObject):
     "detach_script",
     "setinfo_script",
     "verify_script",
+    "snapshot_script",
     "supported_parameters",
     ]
 
diff --git a/lib/opcodes.py b/lib/opcodes.py
index 7b4a05d..5ba7fdf 100644
--- a/lib/opcodes.py
+++ b/lib/opcodes.py
@@ -1425,6 +1425,22 @@ class OpInstanceReinstall(OpCode):
   OP_RESULT = ht.TNone
 
 
+class OpInstanceSnapshot(OpCode):
+  """Snapshot an instance."""
+  OP_DSC_FIELD = "instance_name"
+  OP_PARAMS = [
+    _PInstanceName,
+    ("disks", ht.EmptyList,
+     ht.TListOf(ht.TItems([ht.TOr(ht.TInt, ht.TString),
+                           ht.TDictOf(ht.TElemOf([
+                                      constants.IDISK_SNAPSHOT_NAME]),
+                                      ht.TNonEmptyString)
+                          ])),
+    "Disks to snapshot"),
+    ]
+  OP_RESULT = ht.TNone
+
+
 class OpInstanceRemove(OpCode):
   """Remove an instance."""
   OP_DSC_FIELD = "instance_name"
diff --git a/lib/rapi/client.py b/lib/rapi/client.py
index c2bc03d..d517ec1 100644
--- a/lib/rapi/client.py
+++ b/lib/rapi/client.py
@@ -867,6 +867,23 @@ class GanetiRapiClient(object): # pylint: disable=R0904
                              ("/%s/instances/%s/modify" %
                               (GANETI_RAPI_VERSION, instance)), None, body)
 
+  def SnapshotInstance(self, instance, **kwargs):
+    """Takes snapshot of instance's disks.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type instance: string
+    @param instance: Instance name
+    @rtype: string
+    @return: job id
+
+    """
+    body = kwargs
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/snapshot" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
   def ActivateInstanceDisks(self, instance, ignore_size=None):
     """Activates an instance's disks.
 
diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py
index 46b89da..fdc6033 100644
--- a/lib/rapi/connector.py
+++ b/lib/rapi/connector.py
@@ -197,6 +197,8 @@ def GetHandlers(node_name_pattern, instance_name_pattern,
       rlib2.R_2_instances_name_reboot,
     translate_fn("/2/instances/", instance_name, "/reinstall"):
       rlib2.R_2_instances_name_reinstall,
+    translate_fn("/2/instances/", instance_name, "/snapshot"):
+      rlib2.R_2_instances_name_snapshot,
     translate_fn("/2/instances/", instance_name, "/replace-disks"):
       rlib2.R_2_instances_name_replace_disks,
     translate_fn("/2/instances/", instance_name, "/shutdown"):
diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py
index ef87f4f..51cfe60 100644
--- a/lib/rapi/rlib2.py
+++ b/lib/rapi/rlib2.py
@@ -1332,6 +1332,24 @@ class R_2_instances_name_modify(baserlib.OpcodeResource):
       })
 
 
+class R_2_instances_name_snapshot(baserlib.OpcodeResource):
+  """/2/instances/[instance_name]/snapshot resource.
+
+  Implements an instance snapshot.
+
+  """
+  PUT_OPCODE = opcodes.OpInstanceSnapshot
+
+  def GetPutOpInput(self):
+    """Snapshot disks of an instance.
+
+    """
+    return (self.request_body, {
+        "instance_name": self.items[0],
+        "dry_run": self.dryRun(),
+      })
+
+
 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
 
diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py
index 2b191d9..06bf1de 100644
--- a/lib/rpc_defs.py
+++ b/lib/rpc_defs.py
@@ -429,6 +429,7 @@ _BLOCKDEV_CALLS = [
     ], None, None, "Export a given disk to another node"),
   ("blockdev_snapshot", SINGLE, None, constants.RPC_TMO_NORMAL, [
     ("cf_bdev", ED_SINGLE_DISK_DICT_DP, None),
+    ("snapshot_name", None, None),
     ], None, None, "Export a given disk to another node"),
   ("blockdev_rename", SINGLE, None, constants.RPC_TMO_NORMAL, [
     ("devlist", ED_BLOCKDEV_RENAME, None),
diff --git a/lib/server/noded.py b/lib/server/noded.py
index b5182ea..4f291ee 100644
--- a/lib/server/noded.py
+++ b/lib/server/noded.py
@@ -353,8 +353,9 @@ class NodeRequestHandler(http.server.HttpServerHandler):
     remove by calling the generic block device remove call.
 
     """
-    cfbd = objects.Disk.FromDict(params[0])
-    return backend.BlockdevSnapshot(cfbd)
+    (disk, snapshot_name) = params
+    cfbd = objects.Disk.FromDict(disk)
+    return backend.BlockdevSnapshot(cfbd, snapshot_name)
 
   @staticmethod
   def perspective_blockdev_grow(params):
diff --git a/man/gnt-instance.rst b/man/gnt-instance.rst
index 2e96cab..b0512cd 100644
--- a/man/gnt-instance.rst
+++ b/man/gnt-instance.rst
@@ -1209,6 +1209,22 @@ options.
 Most of the changes take effect at the next restart. If the instance is
 running, there is no effect on the instance.
 
+
+SNAPSHOT
+^^^^^^^^
+
+| **snapshot**
+| {\--disk=*ID*:snapshot_name=*VAL*
+| [\--submit]
+| {*instance*}
+
+This only works for instances with ext disk template. It eventualla runs
+the snapshot script of the corresponding extstorage provider.
+The ``--disk 0:snapshot_name=snap1`` will take snapshot of the first disk
+by exporting snapshot name (via VOL_SNAPSHOT_NAME) and disk related info
+to the script environment. *ID* can be a disk index, name or UUID.
+
+
 REINSTALL
 ^^^^^^^^^
 
diff --git a/src/Ganeti/OpCodes.hs b/src/Ganeti/OpCodes.hs
index ad3bdf9..60876e6 100644
--- a/src/Ganeti/OpCodes.hs
+++ b/src/Ganeti/OpCodes.hs
@@ -316,6 +316,10 @@ $(genOpCode "OpCode"
      , pInstOs
      , pTempOsParams
      ])
+  , ("OpInstanceSnapshot",
+     [ pInstanceName
+     , pInstSnaps
+     ])
   , ("OpInstanceRemove",
      [ pInstanceName
      , pShutdownTimeout
@@ -577,6 +581,7 @@ opSummaryVal OpNodeMigrate { opNodeName = s } = Just (fromNonEmpty s)
 opSummaryVal OpNodeEvacuate { opNodeName = s } = Just (fromNonEmpty s)
 opSummaryVal OpInstanceCreate { opInstanceName = s } = Just s
 opSummaryVal OpInstanceReinstall { opInstanceName = s } = Just s
+opSummaryVal OpInstanceSnapshot { opInstanceName = s } = Just s
 opSummaryVal OpInstanceRemove { opInstanceName = s } = Just s
 -- FIXME: instance rename should show both names; currently it shows none
 -- opSummaryVal OpInstanceRename { opInstanceName = s } = Just s
diff --git a/src/Ganeti/OpParams.hs b/src/Ganeti/OpParams.hs
index 278fb8f..b7e04ed 100644
--- a/src/Ganeti/OpParams.hs
+++ b/src/Ganeti/OpParams.hs
@@ -45,9 +45,11 @@ module Ganeti.OpParams
   , DiskAccess(..)
   , INicParams(..)
   , IDiskParams(..)
+  , ISnapParams(..)
   , RecreateDisksInfo(..)
   , DdmOldChanges(..)
   , SetParamsMods(..)
+  , SetSnapParams(..)
   , ExportTarget(..)
   , pInstanceName
   , pInstances
@@ -98,6 +100,7 @@ module Ganeti.OpParams
   , pHotplugIfPossible
   , pAllowRuntimeChgs
   , pInstDisks
+  , pInstSnaps
   , pDiskTemplate
   , pOptDiskTemplate
   , pFileDriver
@@ -425,6 +428,10 @@ $(buildObject "IDiskParams" "idisk"
   , optionalField $ simpleField C.idiskName   [t| NonEmptyString |]
   ])
 
+-- | Disk snapshot definition.
+$(buildObject "ISnapParams" "idisk"
+  [ simpleField C.idiskSnapshotName [t| NonEmptyString |]])
+
 -- | Disk changes type for OpInstanceRecreateDisks. This is a bit
 -- strange, because the type in Python is something like Either
 -- [DiskIndex] [DiskChanges], but we can't represent the type of an
@@ -493,6 +500,24 @@ instance (JSON a) => JSON (SetParamsMods a) where
   showJSON (SetParamsNew v) = showJSON v
   readJSON = readSetParams
 
+-- | Instance snapshot params
+data SetSnapParams a
+  = SetSnapParamsEmpty
+  | SetSnapParamsValid (NonEmpty (Int, a))
+    deriving (Eq, Show)
+
+readSetSnapParams :: (JSON a) => JSValue -> Text.JSON.Result (SetSnapParams a)
+readSetSnapParams (JSArray []) = return SetSnapParamsEmpty
+readSetSnapParams v =
+  case readJSON v::Text.JSON.Result [(Int, JSValue)] of
+    Text.JSON.Ok _ -> liftM SetSnapParamsValid $ readJSON v
+    _ -> fail "Cannot parse snapshot params."
+
+instance (JSON a) => JSON (SetSnapParams a) where
+  showJSON SetSnapParamsEmpty = showJSON ()
+  showJSON (SetSnapParamsValid v) = showJSON v
+  readJSON = readSetSnapParams
+
 -- | Custom type for target_node parameter of OpBackupExport, which
 -- varies depending on mode. FIXME: this uses an UncheckedList since
 -- we don't care about individual rows (just like the Python code
@@ -748,6 +773,12 @@ type TestClusterOsList = [TestClusterOsListItem]
 pInstDisks :: Field
 pInstDisks = renameField "instDisks" $ simpleField "disks" [t| [IDiskParams] |]
 
+-- | List of instance snaps.
+pInstSnaps :: Field
+pInstSnaps =
+  renameField "instSnaps" $
+  simpleField "disks" [t| SetSnapParams ISnapParams |]
+
 -- | Instance disk template.
 pDiskTemplate :: Field
 pDiskTemplate = simpleField "disk_template" [t| DiskTemplate |]
diff --git a/test/hs/Test/Ganeti/OpCodes.hs b/test/hs/Test/Ganeti/OpCodes.hs
index 05c8132..826816f 100644
--- a/test/hs/Test/Ganeti/OpCodes.hs
+++ b/test/hs/Test/Ganeti/OpCodes.hs
@@ -99,6 +99,14 @@ instance (Arbitrary a) => Arbitrary (SetParamsMods a) where
                     , SetParamsNew        <$> arbitrary
                     ]
 
+instance Arbitrary ISnapParams where
+  arbitrary = ISnapParams <$> genNameNE
+
+instance (Arbitrary a) => Arbitrary (SetSnapParams a) where
+  arbitrary = oneof [ pure SetSnapParamsEmpty
+                    , SetSnapParamsValid <$> arbitrary
+                    ]
+
 instance Arbitrary ExportTarget where
   arbitrary = oneof [ ExportTargetLocal <$> genNodeNameNE
                     , ExportTargetRemote <$> pure []
@@ -228,7 +236,8 @@ instance Arbitrary OpCodes.OpCode where
         OpCodes.OpInstanceReinstall <$> genFQDN <*> arbitrary <*>
           genMaybe genNameNE <*> genMaybe (pure emptyJSObject)
       "OP_INSTANCE_REMOVE" ->
-        OpCodes.OpInstanceRemove <$> genFQDN <*> arbitrary <*> arbitrary
+        OpCodes.OpInstanceRemove <$> genFQDN <*> arbitrary <*>
+          arbitrary <*> arbitrary
       "OP_INSTANCE_RENAME" ->
         OpCodes.OpInstanceRename <$> genFQDN <*> genNodeNameNE <*>
           arbitrary <*> arbitrary
@@ -339,6 +348,8 @@ instance Arbitrary OpCodes.OpCode where
         OpCodes.OpNetworkDisconnect <$> genNameNE <*> genNameNE
       "OP_NETWORK_QUERY" ->
         OpCodes.OpNetworkQuery <$> genFieldsNE <*> genNamesNE <*> arbitrary
+      "OP_INSTANCE_SNAPSHOT" ->
+        OpCodes.OpInstanceSnapshot <$> genFQDN <*> arbitrary
       "OP_RESTRICTED_COMMAND" ->
         OpCodes.OpRestrictedCommand <$> arbitrary <*> genNodeNamesNE <*>
           genNameNE
diff --git a/test/py/ganeti.rapi.client_unittest.py b/test/py/ganeti.rapi.client_unittest.py
index a219673..67a9e69 100755
--- a/test/py/ganeti.rapi.client_unittest.py
+++ b/test/py/ganeti.rapi.client_unittest.py
@@ -1267,6 +1267,16 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()),
                      { "os_name": "linux", })
 
+  def testSnapshotInstance(self):
+    self.rapi.AddResponse("23681")
+    snap = [0, {"snapshot_name": "snap1"}]
+    job_id = self.client.SnapshotInstance("inst7210", disks=[snap])
+    self.assertEqual(job_id, 23681)
+    self.assertItems(["inst7210"])
+    self.assertHandler(rlib2.R_2_instances_name_snapshot)
+    self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()),
+                     {"disks": [snap]})
+
   def testModifyCluster(self):
     for mnh in [None, False, True]:
       self.rapi.AddResponse("14470")
