Revision 06c2fb4a

b/doc/hooks.rst
370 370
:pre-execution: master node, primary and secondary nodes
371 371
:post-execution: master node, primary and secondary nodes
372 372

  
373
OP_INSTANCE_SNAPSHOT
374
++++++++++++++++++++
375

  
376
Takes a snapshot of instance's disk (must be ext template).
377

  
378
:directory: instance-snapshot
379
:env. vars:
380
:pre-execution: master node, primary and secondary nodes
381
:post-execution: master node, primary and secondary nodes
382

  
373 383
OP_INSTANCE_FAILOVER
374 384
++++++++++++++++++++
375 385

  
b/doc/rapi.rst
1266 1266
.. _rapi-res-instances-instance_name-reinstall:
1267 1267

  
1268 1268
``/2/instances/[instance_name]/reinstall``
1269
++++++++++++++++++++++++++++++++++++++++++++++
1269
++++++++++++++++++++++++++++++++++++++++++
1270 1270

  
1271 1271
Installs the operating system again.
1272 1272

  
......
1581 1581
.. opcode_result:: OP_INSTANCE_SET_PARAMS
1582 1582

  
1583 1583

  
1584
.. _rapi-res-instances-instance_name-snapshot:
1585

  
1586
``/2/instances/[instance_name]/snapshot``
1587
+++++++++++++++++++++++++++++++++++++++++
1588

  
1589
Takes snapshot of an instance's disk (must be ext template).
1590

  
1591
.. rapi_resource_details:: /2/instances/[instance_name]/snapshot
1592

  
1593

  
1594
.. _rapi-res-instances-instance_name-snapshot+put:
1595

  
1596
``PUT``
1597
~~~~~~~
1598

  
1599
Returns a job ID.
1600

  
1601
Body parameters:
1602

  
1603
.. opcode_params:: OP_INSTANCE_SNAPSHOT
1604
   :exclude: instance_name
1605

  
1606
Job result:
1607

  
1608
.. opcode_result:: OP_INSTANCE_SNAPSHOT
1609

  
1610

  
1584 1611
.. _rapi-res-instances-instance_name-console:
1585 1612

  
1586 1613
``/2/instances/[instance_name]/console``
b/lib/backend.py
2700 2700
    _Fail("Failed to grow block device: %s", err, exc=True)
2701 2701

  
2702 2702

  
2703
def BlockdevSnapshot(disk):
2703
def BlockdevSnapshot(disk, snapshot_name=None):
2704 2704
  """Create a snapshot copy of a block device.
2705 2705

  
2706 2706
  This function is called recursively, and the snapshot is actually created
......
2717 2717
      _Fail("DRBD device '%s' without backing storage cannot be snapshotted",
2718 2718
            disk.unique_id)
2719 2719
    return BlockdevSnapshot(disk.children[0])
2720
  elif disk.dev_type == constants.LD_LV:
2720
  elif disk.dev_type == constants.LD_LV and not snapshot_name:
2721 2721
    r_dev = _RecursiveFindBD(disk)
2722 2722
    if r_dev is not None:
2723 2723
      # FIXME: choose a saner value for the snapshot size
......
2725 2725
      return r_dev.Snapshot(disk.size)
2726 2726
    else:
2727 2727
      _Fail("Cannot find block device %s", disk)
2728
  elif disk.dev_type == constants.DT_EXT:
2729
    r_dev = _RecursiveFindBD(disk)
2730
    if r_dev is not None:
2731
      r_dev.Snapshot(snapshot_name)
2732
    else:
2733
      _Fail("Cannot find block device %s", disk)
2728 2734
  else:
2729 2735
    _Fail("Cannot snapshot non-lvm block device '%s' of type '%s'",
2730 2736
          disk.unique_id, disk.dev_type)
b/lib/bdev.py
3149 3149
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
3150 3150
                      self.ext_params, metadata=text)
3151 3151

  
3152
  def Snapshot(self, snapshot_name):
3153
    """Take a snapshot of the block device.
3154

  
3155
    """
3156
    # Call the External Storage's setinfo script,
3157
    # to set metadata for an existing Volume inside the External Storage
3158
    _ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id,
3159
                      self.ext_params, snapshot_name=snapshot_name)
3160

  
3152 3161

  
3153 3162
def _ExtStorageAction(action, unique_id, ext_params,
3154
                      size=None, grow=None, metadata=None):
3163
                      size=None, grow=None, metadata=None,
3164
                      snapshot_name=None):
3155 3165
  """Take an External Storage action.
3156 3166

  
3157 3167
  Take an External Storage action concerning or affecting
......
3183 3193

  
3184 3194
  # Create the basic environment for the driver's scripts
3185 3195
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
3186
                                      grow, metadata)
3196
                                      grow, metadata, snapshot_name)
3187 3197

  
3188 3198
  # Do not use log file for action `attach' as we need
3189 3199
  # to get the output from RunResult
......
3295 3305
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
3296 3306
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
3297 3307
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
3308
                       snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT],
3298 3309
                       supported_parameters=parameters)
3299 3310
  return True, es_obj
3300 3311

  
3301 3312

  
3302 3313
def _ExtStorageEnvironment(unique_id, ext_params,
3303
                           size=None, grow=None, metadata=None):
3314
                           size=None, grow=None, metadata=None,
3315
                           snapshot_name=None):
3304 3316
  """Calculate the environment for an External Storage script.
3305 3317

  
3306 3318
  @type unique_id: tuple (driver, vol_name)
......
3335 3347
  if metadata is not None:
3336 3348
    result["VOL_METADATA"] = metadata
3337 3349

  
3350
  if snapshot_name is not None:
3351
    result["VOL_SNAPSHOT_NAME"] = snapshot_name
3352

  
3338 3353
  return result
3339 3354

  
3340 3355

  
b/lib/client/gnt_instance.py
403 403
    return constants.EXIT_FAILURE
404 404

  
405 405

  
406
def SnapshotInstance(opts, args):
407
  """Snapshot an instance.
408

  
409
  @param opts: the command line options selected by the user
410
  @type args: list
411
  @param args: should contain only one element, the name of the
412
      instance to be reinstalled
413
  @rtype: int
414
  @return: the desired exit code
415

  
416
  """
417
  instance_name  = args[0]
418
  inames = _ExpandMultiNames(_EXPAND_INSTANCES, [instance_name])
419
  if not inames:
420
    raise errors.OpPrereqError("Selection filter does not match any instances",
421
                               errors.ECODE_INVAL)
422
  multi_on = len(inames) > 1
423
  jex = JobExecutor(verbose=multi_on, opts=opts)
424
  for instance_name in inames:
425
    op = opcodes.OpInstanceSnapshot(instance_name=instance_name,
426
                                    disks=opts.disks)
427
    jex.QueueJob(instance_name, op)
428

  
429
  results = jex.WaitOrShow(not opts.submit_only)
430

  
431
  if compat.all(map(compat.fst, results)):
432
    return constants.EXIT_SUCCESS
433
  else:
434
    return constants.EXIT_FAILURE
435

  
406 436
def RemoveInstance(opts, args):
407 437
  """Remove an instance.
408 438

  
......
1522 1552
     m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
1523 1553
     SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
1524 1554
    "[-f] <instance>", "Reinstall a stopped instance"),
1555
  "snapshot": (
1556
    SnapshotInstance, [ArgInstance(min=1,max=1)],
1557
    [DISK_OPT, SUBMIT_OPT, DRY_RUN_OPT],
1558
    "<instance>", "Snapshot an instance's disk(s)"),
1525 1559
  "remove": (
1526 1560
    RemoveInstance, ARGS_ONE_INSTANCE,
1527 1561
    [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT, SUBMIT_OPT,
b/lib/cmdlib/__init__.py
88 88
  LUInstanceStartup, \
89 89
  LUInstanceShutdown, \
90 90
  LUInstanceReinstall, \
91
  LUInstanceSnapshot, \
91 92
  LUInstanceReboot, \
92 93
  LUInstanceConsole
93 94
from ganeti.cmdlib.instance_query import \
b/lib/cmdlib/instance_operation.py
42 42
  ShutdownInstanceDisks
43 43
from ganeti.cmdlib.instance_utils import BuildInstanceHookEnvByObject, \
44 44
  CheckInstanceBridgesExist, CheckNodeFreeMemory, CheckNodeHasOS
45
from ganeti.cmdlib.instance import GetItemFromContainer
45 46

  
46 47

  
47 48
class LUInstanceStartup(LogicalUnit):
......
500 501
    logging.debug("Connecting to console of %s on %s", instance.name, node)
501 502

  
502 503
    return GetInstanceConsole(self.cfg.GetClusterInfo(), instance)
504

  
505

  
506
class LUInstanceSnapshot(LogicalUnit):
507
  """Take a snapshot of the instance.
508

  
509
  """
510
  HPATH = "instance-snapshot"
511
  HTYPE = constants.HTYPE_INSTANCE
512
  REQ_BGL = False
513

  
514
  def ExpandNames(self):
515
    self._ExpandAndLockInstance()
516

  
517
  def BuildHooksEnv(self):
518
    """Build hooks env.
519

  
520
    This runs on master, primary and secondary nodes of the instance.
521

  
522
    """
523
    return BuildInstanceHookEnvByObject(self, self.instance)
524

  
525
  def BuildHooksNodes(self):
526
    """Build hooks nodes.
527

  
528
    """
529
    nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes)
530
    return (nl, nl)
531

  
532
  def CheckPrereq(self):
533
    """Check prerequisites.
534

  
535
    This checks that the instance is in the cluster and is not running.
536

  
537
    """
538
    instance = self.cfg.GetInstanceInfo(self.op.instance_name)
539
    assert instance is not None, \
540
      "Cannot retrieve locked instance %s" % self.op.instance_name
541
    CheckNodeOnline(self, instance.primary_node, "Instance primary node"
542
                    " offline, cannot snapshot")
543

  
544
    self.snapshots = []
545
    for ident, params in self.op.disks:
546
      idx, disk = GetItemFromContainer(ident, 'disk', instance.disks)
547
      snapshot_name = params.get("snapshot_name", None)
548
      if not snapshot_name:
549
        raise errors.OpPrereqError("No snapshot_name passed for disk %s", ident)
550
      self.snapshots.append((idx, disk, snapshot_name))
551

  
552
    self.instance = instance
553

  
554
  def Exec(self, feedback_fn):
555
    """Take a snapshot of the instance the instance.
556

  
557
    """
558
    inst = self.instance
559
    node_uuid = inst.primary_node
560
    for idx, disk, snapshot_name in self.snapshots:
561
      self.cfg.SetDiskID(disk, node_uuid)
562
      feedback_fn("Taking a snapshot of instance...")
563
      result = self.rpc.call_blockdev_snapshot(node_uuid,
564
                                               (disk, inst),
565
                                               snapshot_name)
566
      result.Raise("Could not take a snapshot for instance %s disk/%d %s"
567
                   " on node %s" % (inst, idx, disk.uuid, inst.primary_node))
b/lib/cmdlib/instance_storage.py
537 537
    constants.IDISK_METAVG,
538 538
    constants.IDISK_PROVIDER,
539 539
    constants.IDISK_NAME,
540
    constants.IDISK_SNAPSHOT_NAME,
540 541
    ]))
541 542

  
542 543
  def _RunAllocator(self):
b/lib/constants.py
825 825
ES_ACTION_DETACH = "detach"
826 826
ES_ACTION_SETINFO = "setinfo"
827 827
ES_ACTION_VERIFY = "verify"
828
ES_ACTION_SNAPSHOT = "snapshot"
828 829

  
829 830
ES_SCRIPT_CREATE = ES_ACTION_CREATE
830 831
ES_SCRIPT_REMOVE = ES_ACTION_REMOVE
......
833 834
ES_SCRIPT_DETACH = ES_ACTION_DETACH
834 835
ES_SCRIPT_SETINFO = ES_ACTION_SETINFO
835 836
ES_SCRIPT_VERIFY = ES_ACTION_VERIFY
837
ES_SCRIPT_SNAPSHOT = ES_ACTION_SNAPSHOT
836 838
ES_SCRIPTS = frozenset([
837 839
  ES_SCRIPT_CREATE,
838 840
  ES_SCRIPT_REMOVE,
......
840 842
  ES_SCRIPT_ATTACH,
841 843
  ES_SCRIPT_DETACH,
842 844
  ES_SCRIPT_SETINFO,
843
  ES_SCRIPT_VERIFY
845
  ES_SCRIPT_VERIFY,
846
  ES_SCRIPT_SNAPSHOT
844 847
  ])
845 848

  
846 849
ES_PARAMETERS_FILE = "parameters.list"
......
974 977
HV_VIRIDIAN = "viridian"
975 978
HV_VIF_SCRIPT = "vif_script"
976 979

  
977

  
978 980
HVS_PARAMETER_TYPES = {
979 981
  HV_KVM_PATH: VTYPE_STRING,
980 982
  HV_BOOT_ORDER: VTYPE_STRING,
......
1364 1366
IDISK_METAVG = "metavg"
1365 1367
IDISK_PROVIDER = "provider"
1366 1368
IDISK_NAME = "name"
1369
IDISK_SNAPSHOT_NAME = "snapshot_name"
1367 1370
IDISK_PARAMS_TYPES = {
1368 1371
  IDISK_SIZE: VTYPE_SIZE,
1369 1372
  IDISK_MODE: VTYPE_STRING,
......
1372 1375
  IDISK_METAVG: VTYPE_STRING,
1373 1376
  IDISK_PROVIDER: VTYPE_STRING,
1374 1377
  IDISK_NAME: VTYPE_MAYBE_STRING,
1378
  IDISK_SNAPSHOT_NAME: VTYPE_STRING,
1375 1379
  }
1376 1380
IDISK_PARAMS = frozenset(IDISK_PARAMS_TYPES.keys())
1377 1381

  
b/lib/masterd/instance.py
1165 1165

  
1166 1166
      # result.payload will be a snapshot of an lvm leaf of the one we
1167 1167
      # passed
1168
      result = self._lu.rpc.call_blockdev_snapshot(src_node, (disk, instance))
1168
      result = self._lu.rpc.call_blockdev_snapshot(src_node,
1169
                                                   (disk, instance),
1170
                                                   None)
1169 1171
      new_dev = False
1170 1172
      msg = result.fail_msg
1171 1173
      if msg:
b/lib/objects.py
1326 1326
    "detach_script",
1327 1327
    "setinfo_script",
1328 1328
    "verify_script",
1329
    "snapshot_script",
1329 1330
    "supported_parameters",
1330 1331
    ]
1331 1332

  
b/lib/opcodes.py
1425 1425
  OP_RESULT = ht.TNone
1426 1426

  
1427 1427

  
1428
class OpInstanceSnapshot(OpCode):
1429
  """Snapshot an instance."""
1430
  OP_DSC_FIELD = "instance_name"
1431
  OP_PARAMS = [
1432
    _PInstanceName,
1433
    ("disks", ht.EmptyList,
1434
     ht.TListOf(ht.TItems([ht.TOr(ht.TInt, ht.TString),
1435
                           ht.TDictOf(ht.TElemOf([
1436
                                      constants.IDISK_SNAPSHOT_NAME]),
1437
                                      ht.TNonEmptyString)
1438
                          ])),
1439
    "Disks to snapshot"),
1440
    ]
1441
  OP_RESULT = ht.TNone
1442

  
1443

  
1428 1444
class OpInstanceRemove(OpCode):
1429 1445
  """Remove an instance."""
1430 1446
  OP_DSC_FIELD = "instance_name"
b/lib/rapi/client.py
867 867
                             ("/%s/instances/%s/modify" %
868 868
                              (GANETI_RAPI_VERSION, instance)), None, body)
869 869

  
870
  def SnapshotInstance(self, instance, **kwargs):
871
    """Takes snapshot of instance's disks.
872

  
873
    More details for parameters can be found in the RAPI documentation.
874

  
875
    @type instance: string
876
    @param instance: Instance name
877
    @rtype: string
878
    @return: job id
879

  
880
    """
881
    body = kwargs
882

  
883
    return self._SendRequest(HTTP_PUT,
884
                             ("/%s/instances/%s/snapshot" %
885
                              (GANETI_RAPI_VERSION, instance)), None, body)
886

  
870 887
  def ActivateInstanceDisks(self, instance, ignore_size=None):
871 888
    """Activates an instance's disks.
872 889

  
b/lib/rapi/connector.py
197 197
      rlib2.R_2_instances_name_reboot,
198 198
    translate_fn("/2/instances/", instance_name, "/reinstall"):
199 199
      rlib2.R_2_instances_name_reinstall,
200
    translate_fn("/2/instances/", instance_name, "/snapshot"):
201
      rlib2.R_2_instances_name_snapshot,
200 202
    translate_fn("/2/instances/", instance_name, "/replace-disks"):
201 203
      rlib2.R_2_instances_name_replace_disks,
202 204
    translate_fn("/2/instances/", instance_name, "/shutdown"):
b/lib/rapi/rlib2.py
1332 1332
      })
1333 1333

  
1334 1334

  
1335
class R_2_instances_name_snapshot(baserlib.OpcodeResource):
1336
  """/2/instances/[instance_name]/snapshot resource.
1337

  
1338
  Implements an instance snapshot.
1339

  
1340
  """
1341
  PUT_OPCODE = opcodes.OpInstanceSnapshot
1342

  
1343
  def GetPutOpInput(self):
1344
    """Snapshot disks of an instance.
1345

  
1346
    """
1347
    return (self.request_body, {
1348
        "instance_name": self.items[0],
1349
        "dry_run": self.dryRun(),
1350
      })
1351

  
1352

  
1335 1353
class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1336 1354
  """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1337 1355

  
b/lib/rpc_defs.py
429 429
    ], None, None, "Export a given disk to another node"),
430 430
  ("blockdev_snapshot", SINGLE, None, constants.RPC_TMO_NORMAL, [
431 431
    ("cf_bdev", ED_SINGLE_DISK_DICT_DP, None),
432
    ("snapshot_name", None, None),
432 433
    ], None, None, "Export a given disk to another node"),
433 434
  ("blockdev_rename", SINGLE, None, constants.RPC_TMO_NORMAL, [
434 435
    ("devlist", ED_BLOCKDEV_RENAME, None),
b/lib/server/noded.py
353 353
    remove by calling the generic block device remove call.
354 354

  
355 355
    """
356
    cfbd = objects.Disk.FromDict(params[0])
357
    return backend.BlockdevSnapshot(cfbd)
356
    (disk, snapshot_name) = params
357
    cfbd = objects.Disk.FromDict(disk)
358
    return backend.BlockdevSnapshot(cfbd, snapshot_name)
358 359

  
359 360
  @staticmethod
360 361
  def perspective_blockdev_grow(params):
b/man/gnt-instance.rst
1209 1209
Most of the changes take effect at the next restart. If the instance is
1210 1210
running, there is no effect on the instance.
1211 1211

  
1212

  
1213
SNAPSHOT
1214
^^^^^^^^
1215

  
1216
| **snapshot**
1217
| {\--disk=*ID*:snapshot_name=*VAL*
1218
| [\--submit]
1219
| {*instance*}
1220

  
1221
This only works for instances with ext disk template. It eventualla runs
1222
the snapshot script of the corresponding extstorage provider.
1223
The ``--disk 0:snapshot_name=snap1`` will take snapshot of the first disk
1224
by exporting snapshot name (via VOL_SNAPSHOT_NAME) and disk related info
1225
to the script environment. *ID* can be a disk index, name or UUID.
1226

  
1227

  
1212 1228
REINSTALL
1213 1229
^^^^^^^^^
1214 1230

  
b/src/Ganeti/OpCodes.hs
316 316
     , pInstOs
317 317
     , pTempOsParams
318 318
     ])
319
  , ("OpInstanceSnapshot",
320
     [ pInstanceName
321
     , pInstSnaps
322
     ])
319 323
  , ("OpInstanceRemove",
320 324
     [ pInstanceName
321 325
     , pShutdownTimeout
......
577 581
opSummaryVal OpNodeEvacuate { opNodeName = s } = Just (fromNonEmpty s)
578 582
opSummaryVal OpInstanceCreate { opInstanceName = s } = Just s
579 583
opSummaryVal OpInstanceReinstall { opInstanceName = s } = Just s
584
opSummaryVal OpInstanceSnapshot { opInstanceName = s } = Just s
580 585
opSummaryVal OpInstanceRemove { opInstanceName = s } = Just s
581 586
-- FIXME: instance rename should show both names; currently it shows none
582 587
-- opSummaryVal OpInstanceRename { opInstanceName = s } = Just s
b/src/Ganeti/OpParams.hs
45 45
  , DiskAccess(..)
46 46
  , INicParams(..)
47 47
  , IDiskParams(..)
48
  , ISnapParams(..)
48 49
  , RecreateDisksInfo(..)
49 50
  , DdmOldChanges(..)
50 51
  , SetParamsMods(..)
52
  , SetSnapParams(..)
51 53
  , ExportTarget(..)
52 54
  , pInstanceName
53 55
  , pInstances
......
98 100
  , pHotplugIfPossible
99 101
  , pAllowRuntimeChgs
100 102
  , pInstDisks
103
  , pInstSnaps
101 104
  , pDiskTemplate
102 105
  , pOptDiskTemplate
103 106
  , pFileDriver
......
425 428
  , optionalField $ simpleField C.idiskName   [t| NonEmptyString |]
426 429
  ])
427 430

  
431
-- | Disk snapshot definition.
432
$(buildObject "ISnapParams" "idisk"
433
  [ simpleField C.idiskSnapshotName [t| NonEmptyString |]])
434

  
428 435
-- | Disk changes type for OpInstanceRecreateDisks. This is a bit
429 436
-- strange, because the type in Python is something like Either
430 437
-- [DiskIndex] [DiskChanges], but we can't represent the type of an
......
493 500
  showJSON (SetParamsNew v) = showJSON v
494 501
  readJSON = readSetParams
495 502

  
503
-- | Instance snapshot params
504
data SetSnapParams a
505
  = SetSnapParamsEmpty
506
  | SetSnapParamsValid (NonEmpty (Int, a))
507
    deriving (Eq, Show)
508

  
509
readSetSnapParams :: (JSON a) => JSValue -> Text.JSON.Result (SetSnapParams a)
510
readSetSnapParams (JSArray []) = return SetSnapParamsEmpty
511
readSetSnapParams v =
512
  case readJSON v::Text.JSON.Result [(Int, JSValue)] of
513
    Text.JSON.Ok _ -> liftM SetSnapParamsValid $ readJSON v
514
    _ -> fail "Cannot parse snapshot params."
515

  
516
instance (JSON a) => JSON (SetSnapParams a) where
517
  showJSON SetSnapParamsEmpty = showJSON ()
518
  showJSON (SetSnapParamsValid v) = showJSON v
519
  readJSON = readSetSnapParams
520

  
496 521
-- | Custom type for target_node parameter of OpBackupExport, which
497 522
-- varies depending on mode. FIXME: this uses an UncheckedList since
498 523
-- we don't care about individual rows (just like the Python code
......
748 773
pInstDisks :: Field
749 774
pInstDisks = renameField "instDisks" $ simpleField "disks" [t| [IDiskParams] |]
750 775

  
776
-- | List of instance snaps.
777
pInstSnaps :: Field
778
pInstSnaps =
779
  renameField "instSnaps" $
780
  simpleField "disks" [t| SetSnapParams ISnapParams |]
781

  
751 782
-- | Instance disk template.
752 783
pDiskTemplate :: Field
753 784
pDiskTemplate = simpleField "disk_template" [t| DiskTemplate |]
b/test/hs/Test/Ganeti/OpCodes.hs
99 99
                    , SetParamsNew        <$> arbitrary
100 100
                    ]
101 101

  
102
instance Arbitrary ISnapParams where
103
  arbitrary = ISnapParams <$> genNameNE
104

  
105
instance (Arbitrary a) => Arbitrary (SetSnapParams a) where
106
  arbitrary = oneof [ pure SetSnapParamsEmpty
107
                    , SetSnapParamsValid <$> arbitrary
108
                    ]
109

  
102 110
instance Arbitrary ExportTarget where
103 111
  arbitrary = oneof [ ExportTargetLocal <$> genNodeNameNE
104 112
                    , ExportTargetRemote <$> pure []
......
228 236
        OpCodes.OpInstanceReinstall <$> genFQDN <*> arbitrary <*>
229 237
          genMaybe genNameNE <*> genMaybe (pure emptyJSObject)
230 238
      "OP_INSTANCE_REMOVE" ->
231
        OpCodes.OpInstanceRemove <$> genFQDN <*> arbitrary <*> arbitrary
239
        OpCodes.OpInstanceRemove <$> genFQDN <*> arbitrary <*>
240
          arbitrary <*> arbitrary
232 241
      "OP_INSTANCE_RENAME" ->
233 242
        OpCodes.OpInstanceRename <$> genFQDN <*> genNodeNameNE <*>
234 243
          arbitrary <*> arbitrary
......
339 348
        OpCodes.OpNetworkDisconnect <$> genNameNE <*> genNameNE
340 349
      "OP_NETWORK_QUERY" ->
341 350
        OpCodes.OpNetworkQuery <$> genFieldsNE <*> genNamesNE <*> arbitrary
351
      "OP_INSTANCE_SNAPSHOT" ->
352
        OpCodes.OpInstanceSnapshot <$> genFQDN <*> arbitrary
342 353
      "OP_RESTRICTED_COMMAND" ->
343 354
        OpCodes.OpRestrictedCommand <$> arbitrary <*> genNodeNamesNE <*>
344 355
          genNameNE
b/test/py/ganeti.rapi.client_unittest.py
1267 1267
    self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()),
1268 1268
                     { "os_name": "linux", })
1269 1269

  
1270
  def testSnapshotInstance(self):
1271
    self.rapi.AddResponse("23681")
1272
    snap = [0, {"snapshot_name": "snap1"}]
1273
    job_id = self.client.SnapshotInstance("inst7210", disks=[snap])
1274
    self.assertEqual(job_id, 23681)
1275
    self.assertItems(["inst7210"])
1276
    self.assertHandler(rlib2.R_2_instances_name_snapshot)
1277
    self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()),
1278
                     {"disks": [snap]})
1279

  
1270 1280
  def testModifyCluster(self):
1271 1281
    for mnh in [None, False, True]:
1272 1282
      self.rapi.AddResponse("14470")

Also available in: Unified diff