Add VM snapshot import

This commit is contained in:
Joshua Boniface 2024-08-20 10:53:56 -04:00
parent f10d32987b
commit 0a8bad3418
1 changed files with 246 additions and 0 deletions

View File

@ -1675,6 +1675,252 @@ def export_vm_snapshot(
return True, "\n".join(retlines) return True, "\n".join(retlines)
def import_vm_snapshot(
zkhandler, domain, snapshot_name, export_path, retain_snapshot=False
):
tstart = time.time()
myhostname = gethostname().split(".")[0]
# 0. Validations
# Validate that VM does not exist in cluster
dom_uuid = getDomainUUID(zkhandler, domain)
if dom_uuid:
return (
False,
f'ERROR: VM "{domain}" already exists in the cluster! Remove or rename it before importing a snapshot.',
)
# Validate that the source path is valid
if not re.match(r"^/", export_path):
return (
False,
f"ERROR: Source path {export_path} is not a valid absolute path on the primary coordinator!",
)
# Ensure that export_path (on this node) exists
if not os.path.isdir(export_path):
return False, f"ERROR: Source path {export_path} does not exist!"
# Ensure that domain path (on this node) exists
vm_export_path = f"{export_path}/{domain}"
if not os.path.isdir(vm_export_path):
return False, f"ERROR: Source VM path {vm_export_path} does not exist!"
# Ensure that the archives are present
export_source_snapshot_file = f"{vm_export_path}/{snapshot_name}/snapshot.json"
if not os.path.isfile(export_source_snapshot_file):
return False, "ERROR: The specified source export files do not exist!"
# 1. Read the export file and get VM details
try:
with open(export_source_snapshot_file) as fh:
export_source_details = jload(fh)
except Exception as e:
return False, f"ERROR: Failed to read source export details: {e}"
# Handle incrementals
incremental_parent = export_source_details.get("incremental_parent", None)
if incremental_parent is not None:
export_source_parent_snapshot_file = (
f"{vm_export_path}/{incremental_parent}/snapshot.json"
)
if not os.path.isfile(export_source_parent_snapshot_file):
return (
False,
"ERROR: This export is incremental but the required incremental parent files do not exist at '{myhostname}:{vm_export_path}/{incremental_parent}'!",
)
try:
with open(export_source_parent_snapshot_file) as fh:
export_source_parent_details = jload(fh)
except Exception as e:
return (
False,
f"ERROR: Failed to read source incremental parent export details: {e}",
)
# 2. Import VM config and metadata in provision state
try:
retcode, retmsg = define_vm(
zkhandler,
export_source_details["vm_detail"]["xml"],
export_source_details["vm_detail"]["node"],
export_source_details["vm_detail"]["node_limit"],
export_source_details["vm_detail"]["node_selector"],
export_source_details["vm_detail"]["node_autostart"],
export_source_details["vm_detail"]["migration_method"],
export_source_details["vm_detail"]["migration_max_downtime"],
export_source_details["vm_detail"]["profile"],
export_source_details["vm_detail"]["tags"],
"restore",
)
if not retcode:
return False, f"ERROR: Failed to define restored VM: {retmsg}"
except Exception as e:
return False, f"ERROR: Failed to parse VM export details: {e}"
# 4. Import volumes
is_snapshot_remove_failed = False
which_snapshot_remove_failed = list()
if incremental_parent is not None:
for volume_file, volume_size in export_source_details.get("export_files"):
pool, volume, _ = volume_file.split("/")[-1].split(".")
try:
parent_volume_file = [
f[0]
for f in export_source_parent_details.get("export_files")
if f[0].split("/")[-1].replace(".rbdimg", "")
== volume_file.split("/")[-1].replace(".rbddiff", "")
][0]
except Exception as e:
return (
False,
f"ERROR: Failed to find parent volume for volume {pool}/{volume}; export may be corrupt or invalid: {e}",
)
# First we create the expected volumes then clean them up
# This process is a bit of a hack because rbd import does not expect an existing volume,
# but we need the information in PVC.
# Thus create the RBD volume using ceph.add_volume based on the export size, and then
# manually remove the RBD volume (leaving the PVC metainfo)
retcode, retmsg = ceph.add_volume(zkhandler, pool, volume, volume_size)
if not retcode:
return False, f"ERROR: Failed to create restored volume: {retmsg}"
retcode, stdout, stderr = common.run_os_command(
f"rbd remove {pool}/{volume}"
)
if retcode:
return (
False,
f"ERROR: Failed to remove temporary RBD volume '{pool}/{volume}': {stderr}",
)
# Next we import the parent images
retcode, stdout, stderr = common.run_os_command(
f"rbd import --export-format 2 --dest-pool {pool} {export_path}/{domain}/{incremental_parent}/{parent_volume_file} {volume}"
)
if retcode:
return (
False,
f"ERROR: Failed to import parent export image {parent_volume_file}: {stderr}",
)
# Then we import the incremental diffs
retcode, stdout, stderr = common.run_os_command(
f"rbd import-diff {export_path}/{domain}/{snapshot_name}/{volume_file} {pool}/{volume}"
)
if retcode:
return (
False,
f"ERROR: Failed to import incremental export image {volume_file}: {stderr}",
)
# Finally we remove the parent and child snapshots (no longer required required)
if retain_snapshot:
retcode, retmsg = ceph.add_snapshot(
zkhandler,
pool,
volume,
f"{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(
f"rbd snap rm {pool}/{volume}@{incremental_parent}"
)
if retcode:
is_snapshot_remove_failed = True
which_snapshot_remove_failed.append(f"{pool}/{volume}")
retcode, stdout, stderr = common.run_os_command(
f"rbd snap rm {pool}/{volume}@{snapshot_name}"
)
if retcode:
is_snapshot_remove_failed = True
which_snapshot_remove_failed.append(f"{pool}/{volume}")
else:
for volume_file, volume_size in export_source_details.get("export_files"):
pool, volume, _ = volume_file.split("/")[-1].split(".")
# First we create the expected volumes then clean them up
# This process is a bit of a hack because rbd import does not expect an existing volume,
# but we need the information in PVC.
# Thus create the RBD volume using ceph.add_volume based on the export size, and then
# manually remove the RBD volume (leaving the PVC metainfo)
retcode, retmsg = ceph.add_volume(zkhandler, pool, volume, volume_size)
if not retcode:
return False, f"ERROR: Failed to create restored volume: {retmsg}"
retcode, stdout, stderr = common.run_os_command(
f"rbd remove {pool}/{volume}"
)
if retcode:
return (
False,
f"ERROR: Failed to remove temporary RBD volume '{pool}/{volume}': {stderr}",
)
# Then we perform the actual import
retcode, stdout, stderr = common.run_os_command(
f"rbd import --export-format 2 --dest-pool {pool} {export_path}/{domain}/{snapshot_name}/{volume_file} {volume}"
)
if retcode:
return (
False,
f"ERROR: Failed to import export image {volume_file}: {stderr}",
)
# Finally we remove the source snapshot (not required)
if retain_snapshot:
retcode, retmsg = ceph.add_snapshot(
zkhandler,
pool,
volume,
f"{snapshot_name}",
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(
f"rbd snap rm {pool}/{volume}@{snapshot_name}"
)
if retcode:
return (
False,
f"ERROR: Failed to remove imported image snapshot for {volume_file}: {stderr}",
)
# 5. Start VM
retcode, retmsg = start_vm(zkhandler, domain)
if not retcode:
return False, f"ERROR: Failed to start restored VM {domain}: {retmsg}"
tend = time.time()
ttot = round(tend - tstart, 2)
retlines = list()
if is_snapshot_remove_failed:
retlines.append(
f"WARNING: Failed to remove hanging snapshot(s) as requested for volume(s) {', '.join(which_snapshot_remove_failed)}"
)
retlines.append(
f"Successfully imported VM '{domain}' at snapshot '{snapshot_name}' from '{myhostname}:{export_path}' in {ttot}s."
)
return True, "\n".join(retlines)
# #
# VM Backup Tasks # VM Backup Tasks
# #