Revision 3a9fe2bc

b/doc/rapi.rst
1262 1262

  
1263 1263
.. opcode_result:: OP_INSTANCE_STARTUP
1264 1264

  
1265
.. _rapi-res-instances-instance_name-snapshot:
1266

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

  
1270
Snapshot an instance.
1271

  
1272
.. rapi_resource_details:: /2/instances/[instance_name]/snapshot
1273

  
1274

  
1275
.. _rapi-res-instances-instance_name-snapshot+post:
1276

  
1277
``POST``
1278
~~~~~~~~
1265 1279

  
1266 1280
.. _rapi-res-instances-instance_name-reinstall:
1267 1281

  
1282
Returns a job ID.
1283

  
1284
Body parameters:
1285

  
1286
``snapshot_name`` (string, required)
1287
  Name of the snapshot.
1288

  
1289

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

  
b/lib/backend.py
2678 2678
    _Fail("Failed to grow block device: %s", err, exc=True)
2679 2679

  
2680 2680

  
2681
def BlockdevSnapshot(disk):
2681
def BlockdevSnapshot(disk, snapshot_name=None):
2682 2682
  """Create a snapshot copy of a block device.
2683 2683

  
2684 2684
  This function is called recursively, and the snapshot is actually created
......
2695 2695
      _Fail("DRBD device '%s' without backing storage cannot be snapshotted",
2696 2696
            disk.unique_id)
2697 2697
    return BlockdevSnapshot(disk.children[0])
2698
  elif disk.dev_type == constants.LD_LV:
2698
  elif disk.dev_type == constants.LD_LV and not snapshot_name:
2699 2699
    r_dev = _RecursiveFindBD(disk)
2700 2700
    if r_dev is not None:
2701 2701
      # FIXME: choose a saner value for the snapshot size
......
2703 2703
      return r_dev.Snapshot(disk.size)
2704 2704
    else:
2705 2705
      _Fail("Cannot find block device %s", disk)
2706
  elif disk.dev_type == constants.DT_EXT:
2707
    r_dev = _RecursiveFindBD(disk)
2708
    if r_dev is not None:
2709
      r_dev.Snapshot(snapshot_name)
2710
    else:
2711
      _Fail("Cannot find block device %s", disk)
2706 2712
  else:
2707 2713
    _Fail("Cannot snapshot non-lvm block device '%s' of type '%s'",
2708 2714
          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

  
......
1524 1554
     m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
1525 1555
     SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
1526 1556
    "[-f] <instance>", "Reinstall a stopped instance"),
1557
  "snapshot": (
1558
    SnapshotInstance, [ArgInstance(min=1,max=1)],
1559
    [DISK_OPT, SUBMIT_OPT, DRY_RUN_OPT],
1560
    "<instance>", "Snapshot an instance's disk(s)"),
1527 1561
  "remove": (
1528 1562
    RemoveInstance, ARGS_ONE_INSTANCE,
1529 1563
    [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.CheckPrereq("No snapshot_name passed for disk %s", ident)
550
      self.snapshots.append((idx, disk, snapshot_name))
551
    #if not instance.disk_template == constants.ST_EXT:
552
    #  raise errors.OpPrereqError("Instance '%s' disk templates is not 'ext'" %
553
    #                             self.op.instance_name,
554
    #                             errors.ECODE_INVAL)
555
    #CheckInstanceState(self, instance, INSTANCE_DOWN, msg="cannot reinstall")
556

  
557
    self.instance = instance
558

  
559
  def Exec(self, feedback_fn):
560
    """Take a snapshot of the instance the instance.
561

  
562
    """
563
    inst = self.instance
564
    node_uuid = inst.primary_node
565
    for idx, disk, snapshot_name in self.snapshots:
566
      self.cfg.SetDiskID(disk, node_uuid)
567
      feedback_fn("Taking a snapshot of instance...")
568
      result = self.rpc.call_blockdev_snapshot(node_uuid,
569
                                               (disk, inst),
570
                                               snapshot_name)
571
      result.Raise("Could not take a snapshot for instance %s disk %s"
572
                   " on node %s" % (inst, disk.uuid, inst.primary_node))
b/lib/constants.py
822 822
ES_ACTION_DETACH = "detach"
823 823
ES_ACTION_SETINFO = "setinfo"
824 824
ES_ACTION_VERIFY = "verify"
825
ES_ACTION_SNAPSHOT = "snapshot"
825 826

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

  
843 846
ES_PARAMETERS_FILE = "parameters.list"
......
970 973
HV_VNET_HDR = "vnet_hdr"
971 974
HV_VIRIDIAN = "viridian"
972 975

  
973

  
974 976
HVS_PARAMETER_TYPES = {
975 977
  HV_KVM_PATH: VTYPE_STRING,
976 978
  HV_BOOT_ORDER: VTYPE_STRING,
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
1302 1302
    "detach_script",
1303 1303
    "setinfo_script",
1304 1304
    "verify_script",
1305
    "snapshot_script",
1305 1306
    "supported_parameters",
1306 1307
    ]
1307 1308

  
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.NoDefault,
1434
     ht.TListOf(ht.TItems([ht.TString,
1435
                           ht.TDictOf(ht.TElemOf("snapshot_name"),
1436
                                      ht.TMaybeString)
1437
                          ])),
1438
    "Disks to snapshot"),
1439
    ]
1440
  OP_RESULT = ht.TNone
1441

  
1442

  
1428 1443
class OpInstanceRemove(OpCode):
1429 1444
  """Remove an instance."""
1430 1445
  OP_DSC_FIELD = "instance_name"
b/lib/rapi/client.py
1120 1120
                             ("/%s/instances/%s/reinstall" %
1121 1121
                              (GANETI_RAPI_VERSION, instance)), query, None)
1122 1122

  
1123
  def SnapshotInstance(self, instance, disks, dry_run=False):
1124
    """Snapshots disks on an instance.
1125

  
1126
    @type instance: str
1127
    @param instance: instance whose disks to replace
1128
    @type disks: list
1129
    @param disks: list of tuples (idend, snapshot_name)
1130

  
1131
    @rtype: string
1132
    @return: job id
1133

  
1134
    """
1135

  
1136
    body = {
1137
      "disks": disks,
1138
      }
1139

  
1140
    query = []
1141
    _AppendDryRunIf(query, dry_run)
1142

  
1143
    return self._SendRequest(HTTP_POST,
1144
                             ("/%s/instances/%s/snapshot" %
1145
                              (GANETI_RAPI_VERSION, instance)), query, body)
1146

  
1123 1147
  def ReplaceInstanceDisks(self, instance, disks=None, mode=REPLACE_DISK_AUTO,
1124 1148
                           remote_node=None, iallocator=None):
1125 1149
    """Replaces disks on an instance.
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
1143 1143

  
1144 1144
    return self.SubmitJob(ops)
1145 1145

  
1146
class R_2_instances_name_snapshot(baserlib.OpcodeResource):
1147

  
1148
  """/2/instances/[instance_name]/snapshot resource.
1149

  
1150
  Implements an instance snapshot.
1151

  
1152
  """
1153
  POST_OPCODE = opcodes.OpInstanceSnapshot
1154

  
1155
  def GetPostOpInput(self):
1156
    """Snapshot an instance.
1157

  
1158
    The URI takes os=name and nostartup=[0|1] optional
1159
    parameters. By default, the instance will be started
1160
    automatically.
1161

  
1162
    """
1163

  
1164
    return (self.request_body, {
1165
        "instance_name": self.items[0],
1166
        "dry_run": self.dryRun(),
1167
      })
1146 1168

  
1147 1169
class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1148 1170
  """/2/instances/[instance_name]/replace-disks resource.
b/lib/rpc_defs.py
426 426
    ], None, None, "Export a given disk to another node"),
427 427
  ("blockdev_snapshot", SINGLE, None, constants.RPC_TMO_NORMAL, [
428 428
    ("cf_bdev", ED_SINGLE_DISK_DICT_DP, None),
429
    ("snapshot_name", None, None),
429 430
    ], None, None, "Export a given disk to another node"),
430 431
  ("blockdev_rename", SINGLE, None, constants.RPC_TMO_NORMAL, [
431 432
    ("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/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(..)
......
98 99
  , pKeepDisks
99 100
  , pAllowRuntimeChgs
100 101
  , pInstDisks
102
  , pInstSnaps
101 103
  , pDiskTemplate
102 104
  , pOptDiskTemplate
103 105
  , pFileDriver
......
419 421
  , optionalField $ simpleField C.idiskName   [t| NonEmptyString |]
420 422
  ])
421 423

  
424
-- | Disk snapshot definition.
425
$(buildObject "ISnapParams" "snapshot"
426
  [ optionalField $ simpleField C.idiskName [t| String |]])
427

  
422 428
-- | Disk changes type for OpInstanceRecreateDisks. This is a bit
423 429
-- strange, because the type in Python is something like Either
424 430
-- [DiskIndex] [DiskChanges], but we can't represent the type of an
......
743 749
pInstDisks :: Field
744 750
pInstDisks = renameField "instDisks" $ simpleField "disks" [t| [IDiskParams] |]
745 751

  
752
-- | List of instance snaps.
753
pInstSnaps :: Field
754
pInstSnaps =
755
  renameField "instSnaps" $
756
  simpleField "disks" [t| SetParamsMods ISnapParams |]
757

  
746 758
-- | Instance disk template.
747 759
pDiskTemplate :: Field
748 760
pDiskTemplate = simpleField "disk_template" [t| DiskTemplate |]

Also available in: Unified diff