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