From 6fc7f45027786a23e8b4b24cc3632bace4554242 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Tue, 21 May 2024 13:14:55 -0400 Subject: [PATCH] Add snapshot lists and timestamp Adds snapshots to the list of data in VM objects --- api-daemon/pvcapid/flaskapi.py | 26 +++++++++ daemon-common/common.py | 69 +++++++++++++++++++++++ daemon-common/migrations/versions/14.json | 2 +- daemon-common/vm.py | 37 +++++++++++- daemon-common/zkhandler.py | 1 + 5 files changed, 131 insertions(+), 4 deletions(-) diff --git a/api-daemon/pvcapid/flaskapi.py b/api-daemon/pvcapid/flaskapi.py index 490b0a29..abd2b6fd 100755 --- a/api-daemon/pvcapid/flaskapi.py +++ b/api-daemon/pvcapid/flaskapi.py @@ -1600,6 +1600,32 @@ class API_VM_Root(Resource): items: type: object id: VMTag + properties: + name: + type: string + description: The name of the snapshot + timestamp: + type: string + descrpition: Unix timestamp of the snapshot + is_backup: + type: boolean + description: Whether the snapshot is from a backup or not (manual) + rbd_snapshots: + type: array + items: + type: string + description: A list of RBD volume snapshots belonging to this VM snapshot, in '/@' format + xml_diff_lines: + type: array + items: + type: string + description: A list of strings representing the lines of an (n=1) unified diff between the current VM XML specification and the snapshot VM XML specification + snapshots: + type: array + description: The snapshot(s) of the VM + items: + type: object + id: VMSnapshot properties: name: type: string diff --git a/daemon-common/common.py b/daemon-common/common.py index 64eed545..bd35c8d8 100644 --- a/daemon-common/common.py +++ b/daemon-common/common.py @@ -28,6 +28,7 @@ from json import loads from re import match as re_match from re import split as re_split from re import sub as re_sub +from difflib import unified_diff from distutils.util import strtobool from threading import Thread from shlex import split as shlex_split @@ -427,6 +428,72 @@ def getDomainTags(zkhandler, dom_uuid): return tags +# +# Get a list of domain snapshots +# +def getDomainSnapshots(zkhandler, dom_uuid): + """ + Get a list of snapshots for domain dom_uuid + + The UUID must be validated before calling this function! + """ + snapshots = list() + + all_snapshots = zkhandler.children(("domain.snapshots", dom_uuid)) + + current_dom_xml = zkhandler.read(("domain.xml", dom_uuid)) + + snapshots = list() + for snapshot in all_snapshots: + ( + snap_name, + snap_timestamp, + snap_is_backup, + _snap_rbd_snapshots, + snap_dom_xml, + ) = zkhandler.read_many( + [ + ("domain.snapshots", dom_uuid, "domain_snapshot.name", snapshot), + ("domain.snapshots", dom_uuid, "domain_snapshot.timestamp", snapshot), + ("domain.snapshots", dom_uuid, "domain_snapshot.is_backup", snapshot), + ( + "domain.snapshots", + dom_uuid, + "domain_snapshot.rbd_snapshots", + snapshot, + ), + ("domain.snapshots", dom_uuid, "domain_snapshot.xml", snapshot), + ] + ) + + snap_rbd_snapshots = _snap_rbd_snapshots.split(",") + + snap_dom_xml_diff = list( + unified_diff( + current_dom_xml.split("\n"), + snap_dom_xml.split("\n"), + fromfile="current", + tofile="snapshot", + fromfiledate="", + tofiledate="", + n=1, + lineterm="", + ) + ) + + snapshots.append( + { + "name": snap_name, + "timestamp": snap_timestamp, + "xml_diff_lines": snap_dom_xml_diff, + "is_backup": snap_is_backup, + "rbd_snapshots": snap_rbd_snapshots, + } + ) + + return sorted(snapshots, key=lambda s: s["timestamp"], reverse=True) + + # # Get a set of domain metadata # @@ -515,6 +582,7 @@ def getInformationFromXML(zkhandler, uuid): ) = getDomainMetadata(zkhandler, uuid) domain_tags = getDomainTags(zkhandler, uuid) + domain_snapshots = getDomainSnapshots(zkhandler, uuid) if domain_vnc: domain_vnc_listen, domain_vnc_port = domain_vnc.split(":") @@ -574,6 +642,7 @@ def getInformationFromXML(zkhandler, uuid): "migration_method": domain_migration_method, "migration_max_downtime": int(domain_migration_max_downtime), "tags": domain_tags, + "snapshots": domain_snapshots, "description": domain_description, "profile": domain_profile, "memory": int(domain_memory), diff --git a/daemon-common/migrations/versions/14.json b/daemon-common/migrations/versions/14.json index 0d37e7ea..0426b796 100644 --- a/daemon-common/migrations/versions/14.json +++ b/daemon-common/migrations/versions/14.json @@ -1 +1 @@ -{"version": "14", "root": "", "base": {"root": "", "schema": "/schema", "schema.version": "/schema/version", "config": "/config", "config.maintenance": "/config/maintenance", "config.primary_node": "/config/primary_node", "config.primary_node.sync_lock": "/config/primary_node/sync_lock", "config.upstream_ip": "/config/upstream_ip", "config.migration_target_selector": "/config/migration_target_selector", "logs": "/logs", "faults": "/faults", "node": "/nodes", "domain": "/domains", "network": "/networks", "storage": "/ceph", "storage.health": "/ceph/health", "storage.util": "/ceph/util", "osd": "/ceph/osds", "pool": "/ceph/pools", "volume": "/ceph/volumes", "snapshot": "/ceph/snapshots"}, "logs": {"node": "", "messages": "/messages"}, "faults": {"id": "", "last_time": "/last_time", "first_time": "/first_time", "ack_time": "/ack_time", "status": "/status", "delta": "/delta", "message": "/message"}, "node": {"name": "", "keepalive": "/keepalive", "mode": "/daemonmode", "data.active_schema": "/activeschema", "data.latest_schema": "/latestschema", "data.static": "/staticdata", "data.pvc_version": "/pvcversion", "running_domains": "/runningdomains", "count.provisioned_domains": "/domainscount", "count.networks": "/networkscount", "state.daemon": "/daemonstate", "state.router": "/routerstate", "state.domain": "/domainstate", "cpu.load": "/cpuload", "vcpu.allocated": "/vcpualloc", "memory.total": "/memtotal", "memory.used": "/memused", "memory.free": "/memfree", "memory.allocated": "/memalloc", "memory.provisioned": "/memprov", "ipmi.hostname": "/ipmihostname", "ipmi.username": "/ipmiusername", "ipmi.password": "/ipmipassword", "sriov": "/sriov", "sriov.pf": "/sriov/pf", "sriov.vf": "/sriov/vf", "monitoring.plugins": "/monitoring_plugins", "monitoring.data": "/monitoring_data", "monitoring.health": "/monitoring_health", "network.stats": "/network_stats"}, "monitoring_plugin": {"name": "", "last_run": "/last_run", "health_delta": "/health_delta", "message": "/message", "data": "/data", "runtime": "/runtime"}, "sriov_pf": {"phy": "", "mtu": "/mtu", "vfcount": "/vfcount"}, "sriov_vf": {"phy": "", "pf": "/pf", "mtu": "/mtu", "mac": "/mac", "phy_mac": "/phy_mac", "config": "/config", "config.vlan_id": "/config/vlan_id", "config.vlan_qos": "/config/vlan_qos", "config.tx_rate_min": "/config/tx_rate_min", "config.tx_rate_max": "/config/tx_rate_max", "config.spoof_check": "/config/spoof_check", "config.link_state": "/config/link_state", "config.trust": "/config/trust", "config.query_rss": "/config/query_rss", "pci": "/pci", "pci.domain": "/pci/domain", "pci.bus": "/pci/bus", "pci.slot": "/pci/slot", "pci.function": "/pci/function", "used": "/used", "used_by": "/used_by"}, "domain": {"name": "", "xml": "/xml", "state": "/state", "profile": "/profile", "stats": "/stats", "node": "/node", "last_node": "/lastnode", "failed_reason": "/failedreason", "storage.volumes": "/rbdlist", "console.log": "/consolelog", "console.vnc": "/vnc", "meta.autostart": "/node_autostart", "meta.migrate_method": "/migration_method", "meta.migrate_max_downtime": "/migration_max_downtime", "meta.node_selector": "/node_selector", "meta.node_limit": "/node_limit", "meta.tags": "/tags", "migrate.sync_lock": "/migrate_sync_lock", "snapshots": "/snapshots"}, "tag": {"name": "", "type": "/type", "protected": "/protected"}, "domain_snapshot": {"name": "", "is_backup": "/is_backup", "xml": "/xml", "rbd_snapshots": "/rbdsnaplist"}, "network": {"vni": "", "type": "/nettype", "mtu": "/mtu", "rule": "/firewall_rules", "rule.in": "/firewall_rules/in", "rule.out": "/firewall_rules/out", "nameservers": "/name_servers", "domain": "/domain", "reservation": "/dhcp4_reservations", "lease": "/dhcp4_leases", "ip4.gateway": "/ip4_gateway", "ip4.network": "/ip4_network", "ip4.dhcp": "/dhcp4_flag", "ip4.dhcp_start": "/dhcp4_start", "ip4.dhcp_end": "/dhcp4_end", "ip6.gateway": "/ip6_gateway", "ip6.network": "/ip6_network", "ip6.dhcp": "/dhcp6_flag"}, "reservation": {"mac": "", "ip": "/ipaddr", "hostname": "/hostname"}, "lease": {"mac": "", "ip": "/ipaddr", "hostname": "/hostname", "expiry": "/expiry", "client_id": "/clientid"}, "rule": {"description": "", "rule": "/rule", "order": "/order"}, "osd": {"id": "", "node": "/node", "device": "/device", "db_device": "/db_device", "fsid": "/fsid", "ofsid": "/fsid/osd", "cfsid": "/fsid/cluster", "lvm": "/lvm", "vg": "/lvm/vg", "lv": "/lvm/lv", "is_split": "/is_split", "stats": "/stats"}, "pool": {"name": "", "pgs": "/pgs", "tier": "/tier", "stats": "/stats"}, "volume": {"name": "", "stats": "/stats"}, "snapshot": {"name": "", "stats": "/stats"}} \ No newline at end of file +{"version": "14", "root": "", "base": {"root": "", "schema": "/schema", "schema.version": "/schema/version", "config": "/config", "config.maintenance": "/config/maintenance", "config.primary_node": "/config/primary_node", "config.primary_node.sync_lock": "/config/primary_node/sync_lock", "config.upstream_ip": "/config/upstream_ip", "config.migration_target_selector": "/config/migration_target_selector", "logs": "/logs", "faults": "/faults", "node": "/nodes", "domain": "/domains", "network": "/networks", "storage": "/ceph", "storage.health": "/ceph/health", "storage.util": "/ceph/util", "osd": "/ceph/osds", "pool": "/ceph/pools", "volume": "/ceph/volumes", "snapshot": "/ceph/snapshots"}, "logs": {"node": "", "messages": "/messages"}, "faults": {"id": "", "last_time": "/last_time", "first_time": "/first_time", "ack_time": "/ack_time", "status": "/status", "delta": "/delta", "message": "/message"}, "node": {"name": "", "keepalive": "/keepalive", "mode": "/daemonmode", "data.active_schema": "/activeschema", "data.latest_schema": "/latestschema", "data.static": "/staticdata", "data.pvc_version": "/pvcversion", "running_domains": "/runningdomains", "count.provisioned_domains": "/domainscount", "count.networks": "/networkscount", "state.daemon": "/daemonstate", "state.router": "/routerstate", "state.domain": "/domainstate", "cpu.load": "/cpuload", "vcpu.allocated": "/vcpualloc", "memory.total": "/memtotal", "memory.used": "/memused", "memory.free": "/memfree", "memory.allocated": "/memalloc", "memory.provisioned": "/memprov", "ipmi.hostname": "/ipmihostname", "ipmi.username": "/ipmiusername", "ipmi.password": "/ipmipassword", "sriov": "/sriov", "sriov.pf": "/sriov/pf", "sriov.vf": "/sriov/vf", "monitoring.plugins": "/monitoring_plugins", "monitoring.data": "/monitoring_data", "monitoring.health": "/monitoring_health", "network.stats": "/network_stats"}, "monitoring_plugin": {"name": "", "last_run": "/last_run", "health_delta": "/health_delta", "message": "/message", "data": "/data", "runtime": "/runtime"}, "sriov_pf": {"phy": "", "mtu": "/mtu", "vfcount": "/vfcount"}, "sriov_vf": {"phy": "", "pf": "/pf", "mtu": "/mtu", "mac": "/mac", "phy_mac": "/phy_mac", "config": "/config", "config.vlan_id": "/config/vlan_id", "config.vlan_qos": "/config/vlan_qos", "config.tx_rate_min": "/config/tx_rate_min", "config.tx_rate_max": "/config/tx_rate_max", "config.spoof_check": "/config/spoof_check", "config.link_state": "/config/link_state", "config.trust": "/config/trust", "config.query_rss": "/config/query_rss", "pci": "/pci", "pci.domain": "/pci/domain", "pci.bus": "/pci/bus", "pci.slot": "/pci/slot", "pci.function": "/pci/function", "used": "/used", "used_by": "/used_by"}, "domain": {"name": "", "xml": "/xml", "state": "/state", "profile": "/profile", "stats": "/stats", "node": "/node", "last_node": "/lastnode", "failed_reason": "/failedreason", "storage.volumes": "/rbdlist", "console.log": "/consolelog", "console.vnc": "/vnc", "meta.autostart": "/node_autostart", "meta.migrate_method": "/migration_method", "meta.migrate_max_downtime": "/migration_max_downtime", "meta.node_selector": "/node_selector", "meta.node_limit": "/node_limit", "meta.tags": "/tags", "migrate.sync_lock": "/migrate_sync_lock", "snapshots": "/snapshots"}, "tag": {"name": "", "type": "/type", "protected": "/protected"}, "domain_snapshot": {"name": "", "timestamp": "/timestamp", "is_backup": "/is_backup", "xml": "/xml", "rbd_snapshots": "/rbdsnaplist"}, "network": {"vni": "", "type": "/nettype", "mtu": "/mtu", "rule": "/firewall_rules", "rule.in": "/firewall_rules/in", "rule.out": "/firewall_rules/out", "nameservers": "/name_servers", "domain": "/domain", "reservation": "/dhcp4_reservations", "lease": "/dhcp4_leases", "ip4.gateway": "/ip4_gateway", "ip4.network": "/ip4_network", "ip4.dhcp": "/dhcp4_flag", "ip4.dhcp_start": "/dhcp4_start", "ip4.dhcp_end": "/dhcp4_end", "ip6.gateway": "/ip6_gateway", "ip6.network": "/ip6_network", "ip6.dhcp": "/dhcp6_flag"}, "reservation": {"mac": "", "ip": "/ipaddr", "hostname": "/hostname"}, "lease": {"mac": "", "ip": "/ipaddr", "hostname": "/hostname", "expiry": "/expiry", "client_id": "/clientid"}, "rule": {"description": "", "rule": "/rule", "order": "/order"}, "osd": {"id": "", "node": "/node", "device": "/device", "db_device": "/db_device", "fsid": "/fsid", "ofsid": "/fsid/osd", "cfsid": "/fsid/cluster", "lvm": "/lvm", "vg": "/lvm/vg", "lv": "/lvm/lv", "is_split": "/is_split", "stats": "/stats"}, "pool": {"name": "", "pgs": "/pgs", "tier": "/tier", "stats": "/stats"}, "volume": {"name": "", "stats": "/stats"}, "snapshot": {"name": "", "stats": "/stats"}} \ No newline at end of file diff --git a/daemon-common/vm.py b/daemon-common/vm.py index 0877d115..6a8084a6 100644 --- a/daemon-common/vm.py +++ b/daemon-common/vm.py @@ -1300,9 +1300,23 @@ def create_vm_snapshot(zkhandler, domain, snapshot_name=None, is_backup=False): zkhandler.write( [ ( - ("domain.snapshots", dom_uuid, "domain_snapshot.name", snapshot_name), + ( + "domain.snapshots", + dom_uuid, + "domain_snapshot.name", + snapshot_name, + ), snapshot_name, ), + ( + ( + "domain.snapshots", + dom_uuid, + "domain_snapshot.timestamp", + snapshot_name, + ), + tstart, + ), ( ( "domain.snapshots", @@ -1313,7 +1327,12 @@ def create_vm_snapshot(zkhandler, domain, snapshot_name=None, is_backup=False): is_backup, ), ( - ("domain.snapshots", dom_uuid, "domain_snapshot.xml", snapshot_name), + ( + "domain.snapshots", + dom_uuid, + "domain_snapshot.xml", + snapshot_name, + ), vm_config, ), ( @@ -1350,10 +1369,22 @@ def remove_vm_snapshot(zkhandler, domain, snapshot_name, remove_backup=False): f'ERROR: Could not find snapshot "{snapshot_name}" of VM "{domain}"!', ) - if ( + print( zkhandler.read( ("domain.snapshots", dom_uuid, "domain_snapshot.is_backup", snapshot_name) ) + ) + if ( + strtobool( + zkhandler.read( + ( + "domain.snapshots", + dom_uuid, + "domain_snapshot.is_backup", + snapshot_name, + ) + ) + ) and not remove_backup ): # Disallow removing backups normally, but expose `remove_backup` flag for internal usage by refactored backup handlers diff --git a/daemon-common/zkhandler.py b/daemon-common/zkhandler.py index 72d2542a..c47e8de1 100644 --- a/daemon-common/zkhandler.py +++ b/daemon-common/zkhandler.py @@ -724,6 +724,7 @@ class ZKSchema(object): # The schema of an individual domain snapshot entry (/domains/{domain}/snapshots/{snapshot}) "domain_snapshot": { "name": "", # The root key + "timestamp": "/timestamp", "is_backup": "/is_backup", "xml": "/xml", "rbd_snapshots": "/rbdsnaplist",