: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
++++++++++++++++++++
.. _rapi-res-instances-instance_name-reinstall:
``/2/instances/[instance_name]/reinstall``
-++++++++++++++++++++++++++++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++
Installs the operating system again.
.. 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``
_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
_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
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)
_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
# 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
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)
if metadata is not None:
result["VOL_METADATA"] = metadata
+ if snapshot_name is not None:
+ result["VOL_SNAPSHOT_NAME"] = snapshot_name
+
return result
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.
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,
LUInstanceStartup, \
LUInstanceShutdown, \
LUInstanceReinstall, \
+ LUInstanceSnapshot, \
LUInstanceReboot, \
LUInstanceConsole
from ganeti.cmdlib.instance_query import \
ShutdownInstanceDisks
from ganeti.cmdlib.instance_utils import BuildInstanceHookEnvByObject, \
CheckInstanceBridgesExist, CheckNodeFreeMemory, CheckNodeHasOS
+from ganeti.cmdlib.instance import GetItemFromContainer
class LUInstanceStartup(LogicalUnit):
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))
constants.IDISK_METAVG,
constants.IDISK_PROVIDER,
constants.IDISK_NAME,
+ constants.IDISK_SNAPSHOT_NAME,
]))
def _RunAllocator(self):
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
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,
ES_SCRIPT_ATTACH,
ES_SCRIPT_DETACH,
ES_SCRIPT_SETINFO,
- ES_SCRIPT_VERIFY
+ ES_SCRIPT_VERIFY,
+ ES_SCRIPT_SNAPSHOT
])
ES_PARAMETERS_FILE = "parameters.list"
HV_VIRIDIAN = "viridian"
HV_VIF_SCRIPT = "vif_script"
-
HVS_PARAMETER_TYPES = {
HV_KVM_PATH: VTYPE_STRING,
HV_BOOT_ORDER: VTYPE_STRING,
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,
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())
# 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:
"detach_script",
"setinfo_script",
"verify_script",
+ "snapshot_script",
"supported_parameters",
]
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"
("/%s/instances/%s/modify" %
(GANETI_RAPI_VERSION, instance)), query, 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, reason=None):
"""Activates an instance's disks.
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"):
})
+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.
], 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),
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):
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
^^^^^^^^^
, pInstOs
, pTempOsParams
])
+ , ("OpInstanceSnapshot",
+ [ pInstanceName
+ , pInstSnaps
+ ])
, ("OpInstanceRemove",
[ pInstanceName
, pShutdownTimeout
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
, DiskAccess(..)
, INicParams(..)
, IDiskParams(..)
+ , ISnapParams(..)
, RecreateDisksInfo(..)
, DdmOldChanges(..)
, SetParamsMods(..)
+ , SetSnapParams(..)
, ExportTarget(..)
, pInstanceName
, pInstances
, pHotplugIfPossible
, pAllowRuntimeChgs
, pInstDisks
+ , pInstSnaps
, pDiskTemplate
, pOptDiskTemplate
, pFileDriver
, 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
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
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 |]
, 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 []
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
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
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")