Compare commits
2 Commits
9a435fe2ae
...
624eb4e752
Author | SHA1 | Date |
---|---|---|
Joshua Boniface | 624eb4e752 | |
Joshua Boniface | d060787503 |
|
@ -3290,6 +3290,68 @@ class API_VM_Snapshot_Rollback(Resource):
|
|||
api.add_resource(API_VM_Snapshot_Rollback, "/vm/<vm>/snapshot/rollback")
|
||||
|
||||
|
||||
# /vm/<vm>/snapshot/export
|
||||
class API_VM_Snapshot_Export(Resource):
|
||||
@RequestParser(
|
||||
[
|
||||
{
|
||||
"name": "snapshot_name",
|
||||
"required": True,
|
||||
"helptext": "A snapshot name must be specified",
|
||||
},
|
||||
{
|
||||
"name": "export_path",
|
||||
"required": True,
|
||||
"helptext": "An absolute directory path on the PVC primary coordinator to export files to",
|
||||
},
|
||||
{
|
||||
"name": "incremental_parent",
|
||||
"required": False,
|
||||
"helptext": "A snapshot name to generate an incremental diff from",
|
||||
},
|
||||
]
|
||||
)
|
||||
@Authenticator
|
||||
def post(self, vm, reqargs):
|
||||
"""
|
||||
Roll back to a snapshot of a VM's disks and configuration
|
||||
---
|
||||
tags:
|
||||
- vm
|
||||
parameters:
|
||||
- in: query
|
||||
name: snapshot_name
|
||||
type: string
|
||||
required: true
|
||||
description: The name of the snapshot to roll back to
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
schema:
|
||||
type: object
|
||||
id: Message
|
||||
400:
|
||||
description: Execution error
|
||||
schema:
|
||||
type: object
|
||||
id: Message
|
||||
404:
|
||||
description: Not found
|
||||
schema:
|
||||
type: object
|
||||
id: Message
|
||||
"""
|
||||
snapshot_name = reqargs.get("snapshot_name", None)
|
||||
export_path = reqargs.get("export_path", None)
|
||||
incremental_parent = reqargs.get("incremental_parent", None)
|
||||
return api_helper.export_vm_snapshot(
|
||||
vm, snapshot_name, export_path, incremental_parent
|
||||
)
|
||||
|
||||
|
||||
api.add_resource(API_VM_Snapshot_Export, "/vm/<vm>/snapshot/export")
|
||||
|
||||
|
||||
##########################################################
|
||||
# Client API - Network
|
||||
##########################################################
|
||||
|
|
|
@ -837,6 +837,34 @@ def rollback_vm_snapshot(
|
|||
return output, retcode
|
||||
|
||||
|
||||
@ZKConnection(config)
|
||||
def export_vm_snapshot(
|
||||
zkhandler,
|
||||
domain,
|
||||
snapshot_name,
|
||||
export_path,
|
||||
incremental_parent=None,
|
||||
):
|
||||
"""
|
||||
Export a snapshot of a VM to files.
|
||||
"""
|
||||
retflag, retdata = pvc_vm.export_vm_snapshot(
|
||||
zkhandler,
|
||||
domain,
|
||||
snapshot_name,
|
||||
export_path,
|
||||
incremental_parent,
|
||||
)
|
||||
|
||||
if retflag:
|
||||
retcode = 200
|
||||
else:
|
||||
retcode = 400
|
||||
|
||||
output = {"message": retdata.replace('"', "'")}
|
||||
return output, retcode
|
||||
|
||||
|
||||
@ZKConnection(config)
|
||||
def vm_attach_device(zkhandler, vm, device_spec_xml):
|
||||
"""
|
||||
|
|
|
@ -1870,6 +1870,45 @@ def cli_vm_snapshot_rollback(domain, snapshot_name):
|
|||
finish(retcode, retmsg)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# > pvc vm snapshot export
|
||||
###############################################################################
|
||||
@click.command(
|
||||
name="export", short_help="Export a snapshot of a virtual machine to files."
|
||||
)
|
||||
@connection_req
|
||||
@click.argument("domain")
|
||||
@click.argument("snapshot_name")
|
||||
@click.argument("export_path")
|
||||
@click.option(
|
||||
"-i",
|
||||
"--incremental",
|
||||
"incremental_parent",
|
||||
default=None,
|
||||
help="Perform an incremental volume backup from this parent snapshot.",
|
||||
)
|
||||
def cli_vm_snapshot_export(domain, snapshot_name, export_path, incremental_parent):
|
||||
"""
|
||||
Export the (existing) snapshot SNAPSHOT_NAME of virtual machine DOMAIN to the absolute path
|
||||
EXPORT_PATH on the current PVC primary coordinator. DOMAIN may be a UUID or name.
|
||||
"""
|
||||
|
||||
_, primary_node = pvc.lib.cluster.get_primary_node(CLI_CONFIG)
|
||||
echo(
|
||||
CLI_CONFIG,
|
||||
f'Exporting snapshot "{snapshot_name}" of VM "{domain}" to "{export_path}" on "{primary_node}"...',
|
||||
newline=False,
|
||||
)
|
||||
retcode, retmsg = pvc.lib.vm.vm_export_snapshot(
|
||||
CLI_CONFIG, domain, snapshot_name, export_path, incremental_parent
|
||||
)
|
||||
if retcode:
|
||||
echo(CLI_CONFIG, "done.")
|
||||
else:
|
||||
echo(CLI_CONFIG, "failed.")
|
||||
finish(retcode, retmsg)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# > pvc vm backup
|
||||
###############################################################################
|
||||
|
@ -6410,6 +6449,7 @@ cli_vm.add_command(cli_vm_flush_locks)
|
|||
cli_vm_snapshot.add_command(cli_vm_snapshot_create)
|
||||
cli_vm_snapshot.add_command(cli_vm_snapshot_remove)
|
||||
cli_vm_snapshot.add_command(cli_vm_snapshot_rollback)
|
||||
cli_vm_snapshot.add_command(cli_vm_snapshot_export)
|
||||
cli_vm.add_command(cli_vm_snapshot)
|
||||
cli_vm_backup.add_command(cli_vm_backup_create)
|
||||
cli_vm_backup.add_command(cli_vm_backup_restore)
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
import json
|
||||
|
||||
from time import sleep
|
||||
|
||||
from pvc.lib.common import call_api
|
||||
|
||||
|
||||
|
@ -114,3 +116,22 @@ def get_info(config):
|
|||
return True, response.json()
|
||||
else:
|
||||
return False, response.json().get("message", "")
|
||||
|
||||
|
||||
def get_primary_node(config):
|
||||
"""
|
||||
Get the current primary node of the PVC cluster
|
||||
|
||||
API endpoint: GET /api/v1/status/primary_node
|
||||
API arguments:
|
||||
API schema: {json_data_object}
|
||||
"""
|
||||
while True:
|
||||
response = call_api(config, "get", "/status/primary_node")
|
||||
resp_code = response.status_code
|
||||
if resp_code == 200:
|
||||
break
|
||||
else:
|
||||
sleep(1)
|
||||
|
||||
return True, response.json()["primary_node"]
|
||||
|
|
|
@ -557,6 +557,32 @@ def vm_rollback_snapshot(config, vm, snapshot_name):
|
|||
return True, response.json().get("message", "")
|
||||
|
||||
|
||||
def vm_export_snapshot(config, vm, snapshot_name, export_path, incremental_parent):
|
||||
"""
|
||||
Export an (existing) snapshot of a VM's disks and configuration to export_path, optionally
|
||||
incremental with incremental_parent
|
||||
|
||||
API endpoint: POST /vm/{vm}/snapshot/export
|
||||
API arguments: snapshot_name=snapshot_name, export_path=export_path, incremental_parent=incremental_parent
|
||||
API schema: {"message":"{data}"}
|
||||
"""
|
||||
params = {
|
||||
"snapshot_name": snapshot_name,
|
||||
"export_path": export_path,
|
||||
}
|
||||
if incremental_parent is not None:
|
||||
params["incremental_parent"] = incremental_parent
|
||||
|
||||
response = call_api(
|
||||
config, "post", "/vm/{vm}/snapshot/export".format(vm=vm), params=params
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return False, response.json().get("message", "")
|
||||
else:
|
||||
return True, response.json().get("message", "")
|
||||
|
||||
|
||||
def vm_vcpus_set(config, vm, vcpus, topology, restart):
|
||||
"""
|
||||
Set the vCPU count of the VM with topology
|
||||
|
|
|
@ -1459,6 +1459,224 @@ def rollback_vm_snapshot(zkhandler, domain, snapshot_name):
|
|||
)
|
||||
|
||||
|
||||
def export_vm_snapshot(
|
||||
zkhandler, domain, snapshot_name, export_path, incremental_parent=None
|
||||
):
|
||||
# 0b. Validations part 1
|
||||
# Validate that the target path is valid
|
||||
if not re.match(r"^/", export_path):
|
||||
return (
|
||||
False,
|
||||
f"ERROR: Target path {export_path} is not a valid absolute path on the primary coordinator!",
|
||||
)
|
||||
|
||||
# Ensure that backup_path (on this node) exists
|
||||
if not os.path.isdir(export_path):
|
||||
return (
|
||||
False,
|
||||
f"ERROR: Target path {export_path} does not exist!",
|
||||
)
|
||||
|
||||
# 1a. Create destination directory
|
||||
export_target_root = f"{export_path}/{domain}"
|
||||
export_target_path = f"{export_path}/{domain}/{snapshot_name}/images"
|
||||
if not os.path.isdir(export_target_path):
|
||||
try:
|
||||
os.makedirs(export_target_path)
|
||||
except Exception as e:
|
||||
return (
|
||||
False,
|
||||
f"ERROR: Failed to create target directory {export_target_path}: {e}",
|
||||
)
|
||||
|
||||
tstart = time.time()
|
||||
export_type = "incremental" if incremental_parent is not None else "full"
|
||||
|
||||
# 1b. Prepare export JSON writer (it will write on any result)
|
||||
def write_export_json(
|
||||
result=False,
|
||||
result_message="",
|
||||
vm_configuration=None,
|
||||
export_files=None,
|
||||
export_files_size=0,
|
||||
ttot=None,
|
||||
):
|
||||
if ttot is None:
|
||||
tend = time.time()
|
||||
ttot = round(tend - tstart, 2)
|
||||
|
||||
export_details = {
|
||||
"type": export_type,
|
||||
"snapshot_name": snapshot_name,
|
||||
"incremental_parent": incremental_parent,
|
||||
"result": result,
|
||||
"result_message": result_message,
|
||||
"runtime_secs": ttot,
|
||||
"vm_configuration": vm_configuration,
|
||||
"export_files": export_files,
|
||||
"export_size_bytes": export_files_size,
|
||||
}
|
||||
with open(f"{export_target_root}/{snapshot_name}/snapshot.json", "w") as fh:
|
||||
jdump(export_details, fh)
|
||||
|
||||
# 2. Validations part 2
|
||||
# Validate that VM exists in cluster
|
||||
dom_uuid = getDomainUUID(zkhandler, domain)
|
||||
if not dom_uuid:
|
||||
error_message = f'Could not find VM "{domain}" in the cluster!'
|
||||
write_export_json(result=False, result_message=f"ERROR: {error_message}")
|
||||
return False, f"ERROR: {error_message}"
|
||||
|
||||
# Validate that the given snapshot exists
|
||||
if not zkhandler.exists(
|
||||
("domain.snapshots", dom_uuid, "domain_snapshot.name", snapshot_name)
|
||||
):
|
||||
error_message = (
|
||||
f'ERROR: Could not find snapshot "{snapshot_name}" of VM "{domain}"!',
|
||||
)
|
||||
write_export_json(result=False, result_message=f"ERROR: {error_message}")
|
||||
return False, f"ERROR: {error_message}"
|
||||
|
||||
if incremental_parent is not None and not zkhandler.exists(
|
||||
("domain.snapshots", dom_uuid, "domain_snapshot.name", incremental_parent)
|
||||
):
|
||||
error_message = (
|
||||
f'ERROR: Could not find snapshot "{snapshot_name}" of VM "{domain}"!',
|
||||
)
|
||||
write_export_json(result=False, result_message=f"ERROR: {error_message}")
|
||||
return False, f"ERROR: {error_message}"
|
||||
|
||||
# Get details about VM snapshot
|
||||
_, snapshot_timestamp, snapshot_xml, snapshot_rbdsnaps = zkhandler.read_many(
|
||||
[
|
||||
(
|
||||
(
|
||||
"domain.snapshots",
|
||||
dom_uuid,
|
||||
"domain_snapshot.name",
|
||||
snapshot_name,
|
||||
)
|
||||
),
|
||||
(
|
||||
(
|
||||
"domain.snapshots",
|
||||
dom_uuid,
|
||||
"domain_snapshot.timestamp",
|
||||
snapshot_name,
|
||||
)
|
||||
),
|
||||
(
|
||||
(
|
||||
"domain.snapshots",
|
||||
dom_uuid,
|
||||
"domain_snapshot.xml",
|
||||
snapshot_name,
|
||||
)
|
||||
),
|
||||
(
|
||||
(
|
||||
"domain.snapshots",
|
||||
dom_uuid,
|
||||
"domain_snapshot.rbd_snapshots",
|
||||
snapshot_name,
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
snapshot_volumes = list()
|
||||
for rbdsnap in snapshot_rbdsnaps.split(","):
|
||||
pool, _volume = rbdsnap.split("/")
|
||||
volume, name = _volume.split("@")
|
||||
ret, snapshots = ceph.get_list_snapshot(
|
||||
zkhandler, pool, volume, limit=name, is_fuzzy=False
|
||||
)
|
||||
if ret:
|
||||
snapshot_volumes += snapshots
|
||||
|
||||
# 4b. Validate that, if an incremental_parent is given, it is valid
|
||||
# The incremental parent is just a datestring
|
||||
if incremental_parent is not None:
|
||||
export_fileext = "rbddiff"
|
||||
else:
|
||||
export_fileext = "rbdimg"
|
||||
|
||||
# 6. Dump snapshot to folder with `rbd export` (full) or `rbd export-diff` (incremental)
|
||||
is_snapshot_export_failed = False
|
||||
which_snapshot_export_failed = list()
|
||||
export_files = list()
|
||||
for snapshot_volume in snapshot_volumes:
|
||||
pool = snapshot_volume["pool"]
|
||||
volume = snapshot_volume["volume"]
|
||||
snapshot_name = snapshot_volume["snapshot"]
|
||||
size = snapshot_volume["stats"]["size"]
|
||||
|
||||
if incremental_parent is not None:
|
||||
retcode, stdout, stderr = common.run_os_command(
|
||||
f"rbd export-diff --from-snap {incremental_parent} {pool}/{volume}@{snapshot_name} {export_target_path}/{pool}.{volume}.{export_fileext}"
|
||||
)
|
||||
if retcode:
|
||||
is_snapshot_export_failed = True
|
||||
which_snapshot_export_failed.append(f"{pool}/{volume}")
|
||||
else:
|
||||
export_files.append((f"images/{pool}.{volume}.{export_fileext}", size))
|
||||
else:
|
||||
retcode, stdout, stderr = common.run_os_command(
|
||||
f"rbd export --export-format 2 {pool}/{volume}@{snapshot_name} {export_target_path}/{pool}.{volume}.{export_fileext}"
|
||||
)
|
||||
if retcode:
|
||||
is_snapshot_export_failed = True
|
||||
which_snapshot_export_failed.append(f"{pool}/{volume}")
|
||||
else:
|
||||
export_files.append((f"images/{pool}.{volume}.{export_fileext}", size))
|
||||
|
||||
def get_dir_size(path):
|
||||
total = 0
|
||||
with scandir(path) as it:
|
||||
for entry in it:
|
||||
if entry.is_file():
|
||||
total += entry.stat().st_size
|
||||
elif entry.is_dir():
|
||||
total += get_dir_size(entry.path)
|
||||
return total
|
||||
|
||||
export_files_size = get_dir_size(export_target_path)
|
||||
|
||||
if is_snapshot_export_failed:
|
||||
error_message = f'Failed to export snapshot for volume(s) {", ".join(which_snapshot_export_failed)}'
|
||||
write_export_json(
|
||||
result=False,
|
||||
result_message=f"ERROR: {error_message}",
|
||||
vm_configuration=snapshot_xml,
|
||||
export_files=export_files,
|
||||
export_files_size=export_files_size,
|
||||
)
|
||||
return (
|
||||
False,
|
||||
f"ERROR: {error_message}",
|
||||
)
|
||||
|
||||
tend = time.time()
|
||||
ttot = round(tend - tstart, 2)
|
||||
|
||||
retlines = list()
|
||||
|
||||
myhostname = gethostname().split(".")[0]
|
||||
result_message = f"Successfully exported VM '{domain}' snapshot '{snapshot_name}' ({export_type}) to '{myhostname}:{export_path}' in {ttot}s."
|
||||
retlines.append(result_message)
|
||||
|
||||
write_export_json(
|
||||
result=True,
|
||||
result_message=result_message,
|
||||
vm_configuration=snapshot_xml,
|
||||
export_files=export_files,
|
||||
export_files_size=export_files_size,
|
||||
ttot=ttot,
|
||||
)
|
||||
|
||||
return True, "\n".join(retlines)
|
||||
|
||||
|
||||
#
|
||||
# VM Backup Tasks
|
||||
#
|
||||
|
|
Loading…
Reference in New Issue