Handle snapshots on restore and provide options

Also rename the retain option to remove superfluous plural.
This commit is contained in:
Joshua Boniface 2023-10-24 00:23:12 -04:00
parent 0769f1ea52
commit 55ca131c2c
6 changed files with 87 additions and 46 deletions

View File

@ -2307,7 +2307,7 @@ class API_VM_Backup(Resource):
"required": False, "required": False,
}, },
{ {
"name": "retain_snapshots", "name": "retain_snapshot",
"required": False, "required": False,
}, },
] ]
@ -2331,11 +2331,11 @@ class API_VM_Backup(Resource):
required: false required: false
description: A previous backup datestamp to use as an incremental parent; if unspecified a full backup is taken description: A previous backup datestamp to use as an incremental parent; if unspecified a full backup is taken
- in: query - in: query
name: retain_snapshots name: retain_snapshot
type: boolean type: boolean
required: false required: false
default: false default: false
description: Whether or not to retain this backup's volume snapshots to use as a future incremental parent description: Whether or not to retain this backup's volume snapshots to use as a future incremental parent; full backups only
responses: responses:
200: 200:
description: OK description: OK
@ -2355,9 +2355,9 @@ class API_VM_Backup(Resource):
""" """
target_path = reqargs.get("target_path", None) target_path = reqargs.get("target_path", None)
incremental_parent = reqargs.get("incremental_parent", None) incremental_parent = reqargs.get("incremental_parent", None)
retain_snapshots = bool(strtobool(reqargs.get("retain_snapshots", "false"))) retain_snapshot = bool(strtobool(reqargs.get("retain_snapshot", "false")))
return api_helper.vm_backup( return api_helper.vm_backup(
vm, target_path, incremental_parent, retain_snapshots vm, target_path, incremental_parent, retain_snapshot
) )
@ -2377,7 +2377,11 @@ class API_VM_Restore(Resource):
"name": "backup_datestring", "name": "backup_datestring",
"required": True, "required": True,
"helptext": "A backup datestring must be specified", "helptext": "A backup datestring must be specified",
} },
{
"name": "retain_snapshot",
"required": False,
},
] ]
) )
@Authenticator @Authenticator
@ -2398,6 +2402,12 @@ class API_VM_Restore(Resource):
type: string type: string
required: true required: true
description: The backup datestring identifier (e.g. 20230102030405) description: The backup datestring identifier (e.g. 20230102030405)
- in: query
name: retain_snapshot
type: boolean
required: false
default: true
description: Whether or not to retain the (parent, if incremental) volume snapshot after restore
responses: responses:
200: 200:
description: OK description: OK
@ -2417,8 +2427,9 @@ class API_VM_Restore(Resource):
""" """
target_path = reqargs.get("target_path", None) target_path = reqargs.get("target_path", None)
backup_datestring = reqargs.get("backup_datestring", None) backup_datestring = reqargs.get("backup_datestring", None)
retain_snapshot = bool(strtobool(reqargs.get("retain_snapshot", "true")))
return api_helper.vm_restore( return api_helper.vm_restore(
vm, target_path, backup_datestring vm, target_path, backup_datestring, retain_snapshot
) )

View File

@ -476,7 +476,7 @@ def vm_backup(
domain, domain,
target_path, target_path,
incremental_parent=None, incremental_parent=None,
retain_snapshots=False, retain_snapshot=False,
): ):
""" """
Back up a VM to a local (primary coordinator) filesystem path. Back up a VM to a local (primary coordinator) filesystem path.
@ -486,7 +486,7 @@ def vm_backup(
domain, domain,
target_path, target_path,
incremental_parent, incremental_parent,
retain_snapshots, retain_snapshot,
) )
if retflag: if retflag:
@ -504,6 +504,7 @@ def vm_restore(
domain, domain,
target_path, target_path,
datestring, datestring,
retain_snapshot=False,
): ):
""" """
Restore a VM from a local (primary coordinator) filesystem path. Restore a VM from a local (primary coordinator) filesystem path.
@ -513,6 +514,7 @@ def vm_restore(
domain, domain,
target_path, target_path,
datestring, datestring,
retain_snapshot,
) )
if retflag: if retflag:

View File

@ -1606,13 +1606,13 @@ def cli_vm_flush_locks(domain):
) )
@click.option( @click.option(
"-r", "-r",
"--retain-snapshots", "--retain-snapshot",
"retain_snapshots", "retain_snapshot",
is_flag=True, is_flag=True,
default=False, default=False,
help="Retain volume snapshots for future incremental use.", help="Retain volume snapshot for future incremental use (full only).",
) )
def cli_vm_backup(domain, target_path, incremental_parent, retain_snapshots): def cli_vm_backup(domain, target_path, incremental_parent, retain_snapshot):
""" """
Create a backup of virtual machine DOMAIN to TARGET_PATH on the cluster primary coordinator. DOMAIN may be a UUID or name. Create a backup of virtual machine DOMAIN to TARGET_PATH on the cluster primary coordinator. DOMAIN may be a UUID or name.
@ -1622,7 +1622,7 @@ def cli_vm_backup(domain, target_path, incremental_parent, retain_snapshots):
The virtual machine DOMAIN may be running, and due to snapshots the backup should be crash-consistent, but will be in an unclean state and this must be considered when restoring from backups. The virtual machine DOMAIN may be running, and due to snapshots the backup should be crash-consistent, but will be in an unclean state and this must be considered when restoring from backups.
Incremental snapshots are possible by specifying the "-i"/"--incremental" option along with a source backup datestring. The snapshots from that source backup must have been retained using the "-r"/"--retain-snapshots" option. Arbitrary snapshots, assuming they are valid for all attached RBD volumes, may also be used, as long as they are prefixed with "backup_". Retaining snapshots of incremental backups is supported, though it is not recommended to "chain" incremental backups in this way as it can make managing restores more difficult. Incremental snapshots are possible by specifying the "-i"/"--incremental" option along with a source backup datestring. The snapshots from that source backup must have been retained using the "-r"/"--retain-snapshots" option. Retaining snapshots of incremental backups is not supported as incremental backups cannot be chained.
Full backup volume images are sparse-allocated, however it is recommended for safety to consider their maximum allocated size when allocated space for the TARGET_PATH. Incremental volume images are generally small but are dependent entirely on the rate of data change in each volume. Full backup volume images are sparse-allocated, however it is recommended for safety to consider their maximum allocated size when allocated space for the TARGET_PATH. Incremental volume images are generally small but are dependent entirely on the rate of data change in each volume.
""" """
@ -1633,7 +1633,7 @@ def cli_vm_backup(domain, target_path, incremental_parent, retain_snapshots):
newline=False, newline=False,
) )
retcode, retmsg = pvc.lib.vm.vm_backup( retcode, retmsg = pvc.lib.vm.vm_backup(
CLI_CONFIG, domain, target_path, incremental_parent, retain_snapshots CLI_CONFIG, domain, target_path, incremental_parent, retain_snapshot
) )
if retcode: if retcode:
echo(CLI_CONFIG, "done.") echo(CLI_CONFIG, "done.")
@ -1650,7 +1650,15 @@ def cli_vm_backup(domain, target_path, incremental_parent, retain_snapshots):
@click.argument("domain") @click.argument("domain")
@click.argument("backup_datestring") @click.argument("backup_datestring")
@click.argument("target_path") @click.argument("target_path")
def cli_vm_restore(domain, backup_datestring, target_path): @click.option(
"-r/-R",
"--retain-snapshot/--remove-snapshot",
"retain_snapshot",
is_flag=True,
default=True,
help="Retain or remove restored (parent, if incremental) snapshot.",
)
def cli_vm_restore(domain, backup_datestring, target_path, retain_snapshot):
""" """
Restore the backup BACKUP_DATESTRING of virtual machine DOMAIN stored in TARGET_PATH on the cluster primary coordinator. DOMAIN may be a UUID or name. Restore the backup BACKUP_DATESTRING of virtual machine DOMAIN stored in TARGET_PATH on the cluster primary coordinator. DOMAIN may be a UUID or name.
@ -1659,6 +1667,10 @@ def cli_vm_restore(domain, backup_datestring, target_path):
The restore will import the VM configuration, metainfo, and the point-in-time snapshot of all attached RBD volumes. Incremental backups will be automatically handled. The restore will import the VM configuration, metainfo, and the point-in-time snapshot of all attached RBD volumes. Incremental backups will be automatically handled.
A VM named DOMAIN must not exist; if the VM already exists, it must be removed before restoring. Renaming is not sufficient as the UUID will remain the same. A VM named DOMAIN must not exist; if the VM already exists, it must be removed before restoring. Renaming is not sufficient as the UUID will remain the same.
If the "-r"/"--retain-snapshot" option is specified (the default), for incremental restores, only the parent snapshot is kept; for full restores, the restored snapshot is kept. If the "-R"/"--remove-snapshot" option is specified, the imported snapshot is removed.
WARNING: The "-R"/"--remove-snapshot" option will invalidate any existing incremental backups based on the same incremental parent for the restored VM.
""" """
echo( echo(
@ -1667,7 +1679,7 @@ def cli_vm_restore(domain, backup_datestring, target_path):
newline=False, newline=False,
) )
retcode, retmsg = pvc.lib.vm.vm_restore( retcode, retmsg = pvc.lib.vm.vm_restore(
CLI_CONFIG, domain, target_path, backup_datestring CLI_CONFIG, domain, target_path, backup_datestring, retain_snapshot
) )
if retcode: if retcode:
echo(CLI_CONFIG, "done.") echo(CLI_CONFIG, "done.")

View File

@ -433,18 +433,18 @@ def vm_locks(config, vm):
return retstatus, response.json().get("message", "") return retstatus, response.json().get("message", "")
def vm_backup(config, vm, target_path, incremental_parent=None, retain_snapshots=False): def vm_backup(config, vm, target_path, incremental_parent=None, retain_snapshot=False):
""" """
Create a backup of {vm} and its volumes to a local primary coordinator filesystem path Create a backup of {vm} and its volumes to a local primary coordinator filesystem path
API endpoint: POST /vm/{vm}/backup API endpoint: POST /vm/{vm}/backup
API arguments: target_path={target_path}, incremental_parent={incremental_parent}, retain_snapshots={retain_snapshots} API arguments: target_path={target_path}, incremental_parent={incremental_parent}, retain_snapshot={retain_snapshot}
API schema: {"message":"{data}"} API schema: {"message":"{data}"}
""" """
params = { params = {
"target_path": target_path, "target_path": target_path,
"incremental_parent": incremental_parent, "incremental_parent": incremental_parent,
"retain_snapshots": retain_snapshots, "retain_snapshot": retain_snapshot,
} }
response = call_api(config, "post", "/vm/{vm}/backup".format(vm=vm), params=params) response = call_api(config, "post", "/vm/{vm}/backup".format(vm=vm), params=params)
@ -454,17 +454,18 @@ def vm_backup(config, vm, target_path, incremental_parent=None, retain_snapshots
return True, response.json().get("message", "") return True, response.json().get("message", "")
def vm_restore(config, vm, target_path, backup_datestring): def vm_restore(config, vm, target_path, backup_datestring, retain_snapshot=False):
""" """
Restore a backup of {vm} and its volumes from a local primary coordinator filesystem path Restore a backup of {vm} and its volumes from a local primary coordinator filesystem path
API endpoint: POST /vm/{vm}/restore API endpoint: POST /vm/{vm}/restore
API arguments: target_path={target_path}, backup_datestring={backup_datestring} API arguments: target_path={target_path}, backup_datestring={backup_datestring}, retain_snapshot={retain_snapshot}
API schema: {"message":"{data}"} API schema: {"message":"{data}"}
""" """
params = { params = {
"target_path": target_path, "target_path": target_path,
"backup_datestring": backup_datestring, "backup_datestring": backup_datestring,
"retain_snapshot": retain_snapshot,
} }
response = call_api(config, "post", "/vm/{vm}/restore".format(vm=vm), params=params) response = call_api(config, "post", "/vm/{vm}/restore".format(vm=vm), params=params)

View File

@ -1113,13 +1113,14 @@ def getCephSnapshots(zkhandler, pool, volume):
return snapshot_list return snapshot_list
def add_snapshot(zkhandler, pool, volume, name): def add_snapshot(zkhandler, pool, volume, name, zk_only=False):
if not verifyVolume(zkhandler, pool, volume): if not verifyVolume(zkhandler, pool, volume):
return False, 'ERROR: No volume with name "{}" is present in pool "{}".'.format( return False, 'ERROR: No volume with name "{}" is present in pool "{}".'.format(
volume, pool volume, pool
) )
# 1. Create the snapshot # 1. Create the snapshot
if not zk_only:
retcode, stdout, stderr = common.run_os_command( retcode, stdout, stderr = common.run_os_command(
"rbd snap create {}/{}@{}".format(pool, volume, name) "rbd snap create {}/{}@{}".format(pool, volume, name)
) )

View File

@ -1307,12 +1307,16 @@ def get_list(
def backup_vm( def backup_vm(
zkhandler, domain, target_path, incremental_parent=None, retain_snapshots=False zkhandler, domain, target_path, incremental_parent=None, retain_snapshot=False
): ):
tstart = time.time() tstart = time.time()
# 0. Validations # 0. Validations
# Disallow retaining snapshots with an incremental parent
if incremental_parent is not None and retain_snapshot:
return False, 'ERROR: Retaining snapshots of incremental backups is not supported!'
# Validate that VM exists in cluster # Validate that VM exists in cluster
dom_uuid = getDomainUUID(zkhandler, domain) dom_uuid = getDomainUUID(zkhandler, domain)
if not dom_uuid: if not dom_uuid:
@ -1466,7 +1470,7 @@ def backup_vm(
is_snapshot_remove_failed = False is_snapshot_remove_failed = False
which_snapshot_remove_failed = list() which_snapshot_remove_failed = list()
msg_snapshot_remove_failed = list() msg_snapshot_remove_failed = list()
if not retain_snapshots: if not retain_snapshot:
for pool, volume, _ in vm_volumes: for pool, volume, _ in vm_volumes:
if ceph.verifySnapshot(zkhandler, pool, volume, snapshot_name): if ceph.verifySnapshot(zkhandler, pool, volume, snapshot_name):
retcode, retmsg = ceph.remove_snapshot( retcode, retmsg = ceph.remove_snapshot(
@ -1485,7 +1489,7 @@ def backup_vm(
retlines.append(f"WARNING: Failed to remove snapshot as requested for volume(s) {', '.join(which_snapshot_remove_failed)}: {', '.join(msg_snapshot_remove_failed)}") retlines.append(f"WARNING: Failed to remove snapshot as requested for volume(s) {', '.join(which_snapshot_remove_failed)}: {', '.join(msg_snapshot_remove_failed)}")
myhostname = gethostname().split(".")[0] myhostname = gethostname().split(".")[0]
if retain_snapshots: if retain_snapshot:
retlines.append(f"Successfully backed up VM '{domain}' ({backup_type}@{datestring}, snapshots retained) to '{myhostname}:{target_path}' in {ttot}s.") retlines.append(f"Successfully backed up VM '{domain}' ({backup_type}@{datestring}, snapshots retained) to '{myhostname}:{target_path}' in {ttot}s.")
else: else:
retlines.append(f"Successfully backed up VM '{domain}' ({backup_type}@{datestring}) to '{myhostname}:{target_path}' in {ttot}s.") retlines.append(f"Successfully backed up VM '{domain}' ({backup_type}@{datestring}) to '{myhostname}:{target_path}' in {ttot}s.")
@ -1493,7 +1497,7 @@ def backup_vm(
return True, '\n'.join(retlines) return True, '\n'.join(retlines)
def restore_vm(zkhandler, domain, source_path, datestring): def restore_vm(zkhandler, domain, source_path, datestring, retain_snapshot=False):
tstart = time.time() tstart = time.time()
@ -1613,6 +1617,11 @@ def restore_vm(zkhandler, domain, source_path, datestring):
return False, f"ERROR: Failed to import incremental backup image {volume_file}: {stderr}" return False, f"ERROR: Failed to import incremental backup image {volume_file}: {stderr}"
# Finally we remove the parent and child snapshots (no longer required required) # Finally we remove the parent and child snapshots (no longer required required)
if retain_snapshot:
retcode, retmsg = ceph.add_snapshot(zkhandler, pool, volume, f"backup_{incremental_parent}", zk_only=True)
if not retcode:
return False, f"ERROR: Failed to add imported image snapshot for {parent_volume_file}: {retmsg}"
else:
retcode, stdout, stderr = common.run_os_command( retcode, stdout, stderr = common.run_os_command(
f"rbd snap rm {pool}/{volume}@backup_{incremental_parent}" f"rbd snap rm {pool}/{volume}@backup_{incremental_parent}"
) )
@ -1651,6 +1660,11 @@ def restore_vm(zkhandler, domain, source_path, datestring):
return False, f"ERROR: Failed to import backup image {volume_file}: {stderr}" return False, f"ERROR: Failed to import backup image {volume_file}: {stderr}"
# Finally we remove the source snapshot (not required) # Finally we remove the source snapshot (not required)
if retain_snapshot:
retcode, retmsg = ceph.add_snapshot(zkhandler, pool, volume, f"backup_{incremental_parent}", zk_only=True)
if not retcode:
return False, f"ERROR: Failed to add imported image snapshot for {volume_file}: {retmsg}"
else:
retcode, stdout, stderr = common.run_os_command( retcode, stdout, stderr = common.run_os_command(
f"rbd snap rm {pool}/{volume}@backup_{datestring}" f"rbd snap rm {pool}/{volume}@backup_{datestring}"
) )