[libvirt] [PATCH v7 18/23] backup: qemu: Implement metadata tracking for checkpoint APIs

Eric Blake eblake at redhat.com
Wed Mar 27 10:10:49 UTC 2019


A lot of this work heavily copies from the existing snapshot
APIs.  The interaction with qemu during create/delete still
needs to be implemented, but this takes care of all the libvirt
metadata (saving and restoring XML, and tracking the relations
between multiple checkpoints).

Signed-off-by: Eric Blake <eblake at redhat.com>
---
 src/qemu/qemu_block.h  |   3 +
 src/qemu/qemu_conf.h   |   2 +
 src/qemu/qemu_domain.h |  15 +
 src/qemu/qemu_block.c  |  12 +
 src/qemu/qemu_conf.c   |   5 +
 src/qemu/qemu_domain.c | 133 +++++++
 src/qemu/qemu_driver.c | 843 +++++++++++++++++++++++++++++++++++++++++
 7 files changed, 1013 insertions(+)

diff --git a/src/qemu/qemu_block.h b/src/qemu/qemu_block.h
index 9401ab4e12..fbcd019d5c 100644
--- a/src/qemu/qemu_block.h
+++ b/src/qemu/qemu_block.h
@@ -125,4 +125,7 @@ qemuBlockSnapshotAddLegacy(virJSONValuePtr actions,
                            virStorageSourcePtr newsrc,
                            bool reuse);

+const char *
+qemuBlockNodeLookup(virDomainObjPtr vm, const char *disk);
+
 #endif /* LIBVIRT_QEMU_BLOCK_H */
diff --git a/src/qemu/qemu_conf.h b/src/qemu/qemu_conf.h
index 14c9d15a72..a94c229526 100644
--- a/src/qemu/qemu_conf.h
+++ b/src/qemu/qemu_conf.h
@@ -29,6 +29,7 @@
 # include "capabilities.h"
 # include "network_conf.h"
 # include "domain_conf.h"
+# include "checkpoint_conf.h"
 # include "snapshot_conf.h"
 # include "domain_event.h"
 # include "virthread.h"
@@ -110,6 +111,7 @@ struct _virQEMUDriverConfig {
     char *cacheDir;
     char *saveDir;
     char *snapshotDir;
+    char *checkpointDir;
     char *channelTargetDir;
     char *nvramDir;
     char *swtpmStorageDir;
diff --git a/src/qemu/qemu_domain.h b/src/qemu/qemu_domain.h
index 00b497e6a6..9b29628107 100644
--- a/src/qemu/qemu_domain.h
+++ b/src/qemu/qemu_domain.h
@@ -719,6 +719,21 @@ int qemuDomainMomentDiscardAll(void *payload,
 int qemuDomainSnapshotDiscardAllMetadata(virQEMUDriverPtr driver,
                                          virDomainObjPtr vm);

+int qemuDomainCheckpointWriteMetadata(virDomainObjPtr vm,
+                                      virDomainMomentObjPtr checkpoint,
+                                      virCapsPtr caps,
+                                      virDomainXMLOptionPtr xmlopt,
+                                      const char *checkpointDir);
+
+int qemuDomainCheckpointDiscard(virQEMUDriverPtr driver,
+                                virDomainObjPtr vm,
+                                virDomainMomentObjPtr chk,
+                                bool update_current,
+                                bool metadata_only);
+
+int qemuDomainCheckpointDiscardAllMetadata(virQEMUDriverPtr driver,
+                                           virDomainObjPtr vm);
+
 void qemuDomainRemoveInactive(virQEMUDriverPtr driver,
                               virDomainObjPtr vm);

diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c
index cbf0aa4189..df04abeb07 100644
--- a/src/qemu/qemu_block.c
+++ b/src/qemu/qemu_block.c
@@ -1767,3 +1767,15 @@ qemuBlockStorageGetCopyOnReadProps(virDomainDiskDefPtr disk)

     return ret;
 }
+
+const char *
+qemuBlockNodeLookup(virDomainObjPtr vm, const char *disk)
+{
+    size_t i;
+
+    for (i = 0; i < vm->def->ndisks; i++) {
+        if (STREQ(vm->def->disks[i]->dst, disk))
+            return vm->def->disks[i]->src->nodeformat;
+    }
+    return NULL;
+}
diff --git a/src/qemu/qemu_conf.c b/src/qemu/qemu_conf.c
index 42122dcd97..e58683f571 100644
--- a/src/qemu/qemu_conf.c
+++ b/src/qemu/qemu_conf.c
@@ -179,6 +179,8 @@ virQEMUDriverConfigPtr virQEMUDriverConfigNew(bool privileged)
             goto error;
         if (virAsprintf(&cfg->snapshotDir, "%s/snapshot", cfg->libDir) < 0)
             goto error;
+        if (virAsprintf(&cfg->checkpointDir, "%s/checkpoint", cfg->libDir) < 0)
+            goto error;
         if (virAsprintf(&cfg->autoDumpPath, "%s/dump", cfg->libDir) < 0)
             goto error;
         if (virAsprintf(&cfg->channelTargetDir,
@@ -242,6 +244,8 @@ virQEMUDriverConfigPtr virQEMUDriverConfigNew(bool privileged)
             goto error;
         if (virAsprintf(&cfg->snapshotDir, "%s/qemu/snapshot", cfg->configBaseDir) < 0)
             goto error;
+        if (virAsprintf(&cfg->checkpointDir, "%s/qemu/checkpoint", cfg->configBaseDir) < 0)
+            goto error;
         if (virAsprintf(&cfg->autoDumpPath, "%s/qemu/dump", cfg->configBaseDir) < 0)
             goto error;
         if (virAsprintf(&cfg->channelTargetDir,
@@ -354,6 +358,7 @@ static void virQEMUDriverConfigDispose(void *obj)
     VIR_FREE(cfg->cacheDir);
     VIR_FREE(cfg->saveDir);
     VIR_FREE(cfg->snapshotDir);
+    VIR_FREE(cfg->checkpointDir);
     VIR_FREE(cfg->channelTargetDir);
     VIR_FREE(cfg->nvramDir);

diff --git a/src/qemu/qemu_domain.c b/src/qemu/qemu_domain.c
index 54afb6dd7b..f003747ed2 100644
--- a/src/qemu/qemu_domain.c
+++ b/src/qemu/qemu_domain.c
@@ -56,6 +56,7 @@
 #include "logging/log_manager.h"
 #include "locking/domain_lock.h"
 #include "virdomainsnapshotobjlist.h"
+#include "virdomaincheckpointobjlist.h"

 #ifdef MAJOR_IN_MKDEV
 # include <sys/mkdev.h>
@@ -8489,6 +8490,40 @@ qemuDomainSnapshotWriteMetadata(virDomainObjPtr vm,
     return ret;
 }

+int
+qemuDomainCheckpointWriteMetadata(virDomainObjPtr vm,
+                                  virDomainMomentObjPtr checkpoint,
+                                  virCapsPtr caps,
+                                  virDomainXMLOptionPtr xmlopt,
+                                  const char *checkpointDir)
+{
+    unsigned int flags = VIR_DOMAIN_CHECKPOINT_FORMAT_SECURE |
+        VIR_DOMAIN_CHECKPOINT_FORMAT_INTERNAL;
+    virDomainCheckpointDefPtr def = virDomainCheckpointObjGetDef(checkpoint);
+    VIR_AUTOFREE(char *) newxml = NULL;
+    VIR_AUTOFREE(char *) chkDir = NULL;
+    VIR_AUTOFREE(char *) chkFile = NULL;
+
+    if (virDomainCheckpointGetCurrent(vm->checkpoints) == checkpoint)
+        flags |= VIR_DOMAIN_CHECKPOINT_FORMAT_CURRENT;
+    newxml = virDomainCheckpointDefFormat(def, caps, xmlopt, flags);
+    if (newxml == NULL)
+        return -1;
+
+    if (virAsprintf(&chkDir, "%s/%s", checkpointDir, vm->def->name) < 0)
+        return -1;
+    if (virFileMakePath(chkDir) < 0) {
+        virReportSystemError(errno, _("cannot create checkpoint directory '%s'"),
+                             chkDir);
+        return -1;
+    }
+
+    if (virAsprintf(&chkFile, "%s/%s.xml", chkDir, def->common.name) < 0)
+        return -1;
+
+    return virXMLSaveFile(chkFile, NULL, "checkpoint-edit", newxml);
+}
+
 /* The domain is expected to be locked and inactive. Return -1 on normal
  * failure, 1 if we skipped a disk due to try_all.  */
 static int
@@ -8684,12 +8719,99 @@ qemuDomainSnapshotDiscardAllMetadata(virQEMUDriverPtr driver,
 }


+/* Discard one checkpoint (or its metadata), without reparenting any children.  */
+int
+qemuDomainCheckpointDiscard(virQEMUDriverPtr driver,
+                            virDomainObjPtr vm,
+                            virDomainMomentObjPtr chk,
+                            bool update_parent,
+                            bool metadata_only)
+{
+    char *chkFile = NULL;
+    int ret = -1;
+    virDomainMomentObjPtr parentchk = NULL;
+    virQEMUDriverConfigPtr cfg = virQEMUDriverGetConfig(driver);
+
+    if (!metadata_only) {
+        if (!virDomainObjIsActive(vm)) {
+            virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s",
+                           _("cannot remove checkpoint from inactive domain"));
+            goto cleanup;
+        } else {
+            /* TODO: Implement QMP sequence to merge bitmaps */
+            // qemuDomainObjPrivatePtr priv;
+            // priv = vm->privateData;
+            // qemuDomainObjEnterMonitor(driver, vm);
+            // /* we continue on even in the face of error */
+            // qemuMonitorDeleteCheckpoint(priv->mon, chk->def->name);
+            // ignore_value(qemuDomainObjExitMonitor(driver, vm));
+        }
+    }
+
+    if (virAsprintf(&chkFile, "%s/%s/%s.xml", cfg->checkpointDir,
+                    vm->def->name, chk->def->name) < 0)
+        goto cleanup;
+
+    if (chk == virDomainCheckpointGetCurrent(vm->checkpoints)) {
+        virDomainCheckpointSetCurrent(vm->checkpoints, NULL);
+        if (update_parent && chk->def->parent) {
+            parentchk = virDomainCheckpointFindByName(vm->checkpoints,
+                                                      chk->def->parent);
+            if (!parentchk) {
+                VIR_WARN("missing parent checkpoint matching name '%s'",
+                         chk->def->parent);
+            } else {
+                virDomainCheckpointSetCurrent(vm->checkpoints, parentchk);
+                if (qemuDomainCheckpointWriteMetadata(vm, parentchk, driver->caps,
+                                                      driver->xmlopt,
+                                                      cfg->checkpointDir) < 0) {
+                    VIR_WARN("failed to set parent checkpoint '%s' as current",
+                             chk->def->parent);
+                    virDomainCheckpointSetCurrent(vm->checkpoints, NULL);
+                }
+            }
+        }
+    }
+
+    if (unlink(chkFile) < 0)
+        VIR_WARN("Failed to unlink %s", chkFile);
+    if (update_parent)
+        virDomainMomentDropParent(chk);
+    virDomainCheckpointObjListRemove(vm->checkpoints, chk);
+
+    ret = 0;
+
+ cleanup:
+    VIR_FREE(chkFile);
+    virObjectUnref(cfg);
+    return ret;
+}
+
+int
+qemuDomainCheckpointDiscardAllMetadata(virQEMUDriverPtr driver,
+                                       virDomainObjPtr vm)
+{
+    virQEMUMomentRemove rem;
+
+    rem.driver = driver;
+    rem.vm = vm;
+    rem.metadata_only = true;
+    rem.err = 0;
+    virDomainCheckpointForEach(vm->checkpoints, qemuDomainMomentDiscardAll,
+                               &rem);
+    virDomainCheckpointObjListRemoveAll(vm->checkpoints);
+
+    return rem.err;
+}
+
+
 static void
 qemuDomainRemoveInactiveCommon(virQEMUDriverPtr driver,
                                virDomainObjPtr vm)
 {
     virQEMUDriverConfigPtr cfg;
     VIR_AUTOFREE(char *) snapDir = NULL;
+    VIR_AUTOFREE(char *) chkDir = NULL;

     cfg = virQEMUDriverGetConfig(driver);

@@ -8704,6 +8826,17 @@ qemuDomainRemoveInactiveCommon(virQEMUDriverPtr driver,
     } else if (rmdir(snapDir) < 0 && errno != ENOENT) {
         VIR_WARN("unable to remove snapshot directory %s", snapDir);
     }
+    /* Remove any checkpoint metadata prior to removing the domain */
+    if (qemuDomainCheckpointDiscardAllMetadata(driver, vm) < 0) {
+        VIR_WARN("unable to remove all checkpoints for domain %s",
+                 vm->def->name);
+    } else if (virAsprintf(&chkDir, "%s/%s", cfg->checkpointDir,
+                           vm->def->name) < 0) {
+        VIR_WARN("unable to remove checkpoint directory %s/%s",
+                 cfg->snapshotDir, vm->def->name);
+    } else if (rmdir(chkDir) < 0 && errno != ENOENT) {
+        VIR_WARN("unable to remove checkpoint directory %s", chkDir);
+    }
     qemuExtDevicesCleanupHost(driver, vm->def);

     virObjectUnref(cfg);
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c
index 62d8d977c5..22e023253e 100644
--- a/src/qemu/qemu_driver.c
+++ b/src/qemu/qemu_driver.c
@@ -102,6 +102,7 @@
 #include "netdev_bandwidth_conf.h"
 #include "virqemu.h"
 #include "virdomainsnapshotobjlist.h"
+#include "virdomaincheckpointobjlist.h"

 #define VIR_FROM_THIS VIR_FROM_QEMU

@@ -221,6 +222,40 @@ qemuSnapObjFromSnapshot(virDomainObjPtr vm,
     return qemuSnapObjFromName(vm, snapshot->name);
 }

+/* Looks up the domain object from checkpoint and unlocks the
+ * driver. The returned domain object is locked and ref'd and the
+ * caller must call virDomainObjEndAPI() on it. */
+static virDomainObjPtr
+qemuDomObjFromCheckpoint(virDomainCheckpointPtr checkpoint)
+{
+    return qemuDomObjFromDomain(checkpoint->domain);
+}
+
+
+/* Looks up checkpoint object from VM and name */
+static virDomainMomentObjPtr
+qemuCheckObjFromName(virDomainObjPtr vm,
+                     const char *name)
+{
+    virDomainMomentObjPtr chk = NULL;
+    chk = virDomainCheckpointFindByName(vm->checkpoints, name);
+    if (!chk)
+        virReportError(VIR_ERR_NO_DOMAIN_CHECKPOINT,
+                       _("no domain checkpoint with matching name '%s'"),
+                       name);
+
+    return chk;
+}
+
+
+/* Looks up checkpoint object from VM and checkpointPtr */
+static virDomainMomentObjPtr
+qemuCheckObjFromCheckpoint(virDomainObjPtr vm,
+                           virDomainCheckpointPtr checkpoint)
+{
+    return qemuCheckObjFromName(vm, checkpoint->name);
+}
+
 static int
 qemuAutostartDomain(virDomainObjPtr vm,
                     void *opaque)
@@ -524,6 +559,124 @@ qemuDomainSnapshotLoad(virDomainObjPtr vm,
 }


+static int
+qemuDomainCheckpointLoad(virDomainObjPtr vm,
+                         void *data)
+{
+    char *baseDir = (char *)data;
+    char *chkDir = NULL;
+    DIR *dir = NULL;
+    struct dirent *entry;
+    char *xmlStr;
+    char *fullpath;
+    virDomainCheckpointDefPtr def = NULL;
+    virDomainMomentObjPtr chk = NULL;
+    virDomainMomentObjPtr current = NULL;
+    bool cur;
+    unsigned int flags = (VIR_DOMAIN_CHECKPOINT_PARSE_REDEFINE |
+                          VIR_DOMAIN_CHECKPOINT_PARSE_INTERNAL);
+    int ret = -1;
+    virCapsPtr caps = NULL;
+    int direrr;
+
+    virObjectLock(vm);
+    if (virAsprintf(&chkDir, "%s/%s", baseDir, vm->def->name) < 0) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("Failed to allocate memory for "
+                       "checkpoint directory for domain %s"),
+                       vm->def->name);
+        goto cleanup;
+    }
+
+    if (!(caps = virQEMUDriverGetCapabilities(qemu_driver, false)))
+        goto cleanup;
+
+    VIR_INFO("Scanning for checkpoints for domain %s in %s", vm->def->name,
+             chkDir);
+
+    if (virDirOpenIfExists(&dir, chkDir) <= 0)
+        goto cleanup;
+
+    while ((direrr = virDirRead(dir, &entry, NULL)) > 0) {
+        /* NB: ignoring errors, so one malformed config doesn't
+           kill the whole process */
+        VIR_INFO("Loading checkpoint file '%s'", entry->d_name);
+
+        if (virAsprintf(&fullpath, "%s/%s", chkDir, entry->d_name) < 0) {
+            virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                           _("Failed to allocate memory for path"));
+            continue;
+        }
+
+        if (virFileReadAll(fullpath, 1024*1024*1, &xmlStr) < 0) {
+            /* Nothing we can do here, skip this one */
+            virReportSystemError(errno,
+                                 _("Failed to read checkpoint file %s"),
+                                 fullpath);
+            VIR_FREE(fullpath);
+            continue;
+        }
+
+        def = virDomainCheckpointDefParseString(xmlStr, caps,
+                                                qemu_driver->xmlopt, &cur,
+                                                flags);
+        if (!def || virDomainCheckpointAlignDisks(def) < 0) {
+            /* Nothing we can do here, skip this one */
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                           _("Failed to parse checkpoint XML from file '%s'"),
+                           fullpath);
+            VIR_FREE(fullpath);
+            VIR_FREE(xmlStr);
+            continue;
+        }
+
+        chk = virDomainCheckpointAssignDef(vm->checkpoints, def);
+        if (chk == NULL) {
+            virDomainCheckpointDefFree(def);
+        } else if (cur) {
+            if (current)
+                virReportError(VIR_ERR_INTERNAL_ERROR,
+                               _("Too many snapshots claiming to be current for domain %s"),
+                               vm->def->name);
+            current = chk;
+        }
+
+        VIR_FREE(fullpath);
+        VIR_FREE(xmlStr);
+    }
+    if (direrr < 0)
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("Failed to fully read directory %s"),
+                       chkDir);
+
+    virDomainCheckpointSetCurrent(vm->checkpoints, current);
+
+    if (virDomainCheckpointUpdateRelations(vm->checkpoints) < 0)
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("Checkpoints have inconsistent relations for domain %s"),
+                       vm->def->name);
+
+    /* FIXME: qemu keeps internal track of bitmaps, which form the
+     * basis for checkpoints; it would be nice if we could update our
+     * internal state to reflect that information automatically.  But
+     * qemu 3.0 did not have access to this via qemu-img for offline
+     * images (you have to use QMP commands on a running guest), and
+     * it also does not track <parent> relations which we find
+     * important in our metadata.
+     */
+
+    virResetLastError();
+
+    ret = 0;
+ cleanup:
+    VIR_DIR_CLOSE(dir);
+    VIR_FREE(chkDir);
+    virObjectUnref(caps);
+    virObjectUnlock(vm);
+    return ret;
+}
+
+
 static int
 qemuDomainNetsRestart(virDomainObjPtr vm,
                       void *data ATTRIBUTE_UNUSED)
@@ -654,6 +807,11 @@ qemuStateInitialize(bool privileged,
                              cfg->snapshotDir);
         goto error;
     }
+    if (virFileMakePath(cfg->checkpointDir) < 0) {
+        virReportSystemError(errno, _("Failed to create checkpoint dir %s"),
+                             cfg->checkpointDir);
+        goto error;
+    }
     if (virFileMakePath(cfg->autoDumpPath) < 0) {
         virReportSystemError(errno, _("Failed to create dump dir %s"),
                              cfg->autoDumpPath);
@@ -761,6 +919,13 @@ qemuStateInitialize(bool privileged,
                                  (int)cfg->group);
             goto error;
         }
+        if (chown(cfg->checkpointDir, cfg->user, cfg->group) < 0) {
+            virReportSystemError(errno,
+                                 _("unable to set ownership of '%s' to %d:%d"),
+                                 cfg->checkpointDir, (int)cfg->user,
+                                 (int)cfg->group);
+            goto error;
+        }
         if (chown(cfg->autoDumpPath, cfg->user, cfg->group) < 0) {
             virReportSystemError(errno,
                                  _("unable to set ownership of '%s' to %d:%d"),
@@ -899,6 +1064,10 @@ qemuStateInitialize(bool privileged,
                             qemuDomainSnapshotLoad,
                             cfg->snapshotDir);

+    virDomainObjListForEach(qemu_driver->domains,
+                            qemuDomainCheckpointLoad,
+                            cfg->checkpointDir);
+
     virDomainObjListForEach(qemu_driver->domains,
                             qemuDomainManagedSaveLoad,
                             qemu_driver);
@@ -7773,6 +7942,7 @@ qemuDomainUndefineFlags(virDomainPtr dom,
         if (qemuDomainSnapshotDiscardAllMetadata(driver, vm) < 0)
             goto endjob;
     }
+    /* TODO: Restrict deletion if checkpoints exist? */

     name = qemuDomainManagedSavePath(driver, vm);
     if (name == NULL)
@@ -16822,6 +16992,651 @@ qemuDomainSnapshotDelete(virDomainSnapshotPtr snapshot,
     return ret;
 }

+
+/* Called prior to job lock */
+static virDomainCheckpointDefPtr
+qemuDomainCheckpointDefParseString(virQEMUDriverPtr driver, virCapsPtr caps,
+                                   const char *xmlDesc, unsigned int flags)
+{
+    virDomainCheckpointDefPtr def = NULL;
+    virDomainCheckpointDefPtr ret = NULL;
+    unsigned int parse_flags = 0;
+
+    if (flags & VIR_DOMAIN_CHECKPOINT_CREATE_REDEFINE)
+        parse_flags |= VIR_DOMAIN_CHECKPOINT_PARSE_REDEFINE;
+    if (!(def = virDomainCheckpointDefParseString(xmlDesc, caps, driver->xmlopt,
+                                                  NULL, parse_flags)))
+        goto cleanup;
+
+    /* reject checkpoint names containing slashes or starting with dot as
+     * checkpoint definitions are saved in files named by the checkpoint name */
+    if (!(flags & VIR_DOMAIN_CHECKPOINT_CREATE_NO_METADATA)) {
+        if (strchr(def->common.name, '/')) {
+            virReportError(VIR_ERR_XML_DETAIL,
+                           _("invalid checkpoint name '%s': "
+                             "name can't contain '/'"),
+                           def->common.name);
+            goto cleanup;
+        }
+
+        if (def->common.name[0] == '.') {
+            virReportError(VIR_ERR_XML_DETAIL,
+                           _("invalid checkpoint name '%s': "
+                             "name can't start with '.'"),
+                           def->common.name);
+            goto cleanup;
+        }
+    }
+
+    VIR_STEAL_PTR(ret, def);
+
+ cleanup:
+    virDomainCheckpointDefFree(def);
+    return ret;
+}
+
+
+/* Called inside job lock */
+static int
+qemuDomainCheckpointPrepare(virQEMUDriverPtr driver, virCapsPtr caps,
+                            virDomainObjPtr vm,
+                            virDomainCheckpointDefPtr def)
+{
+    int ret = -1;
+    size_t i;
+    char *xml = NULL;
+    qemuDomainObjPrivatePtr priv = vm->privateData;
+
+    /* Easiest way to clone inactive portion of vm->def is via
+     * conversion in and back out of xml.  */
+    if (!(xml = qemuDomainDefFormatLive(driver, vm->def, priv->origCPU,
+                                        true, true)) ||
+        !(def->common.dom = virDomainDefParseString(xml, caps, driver->xmlopt, NULL,
+                                                    VIR_DOMAIN_DEF_PARSE_INACTIVE |
+                                                    VIR_DOMAIN_DEF_PARSE_SKIP_VALIDATE)))
+        goto cleanup;
+
+    if (virDomainCheckpointAlignDisks(def) < 0 ||
+        qemuBlockNodeNamesDetect(driver, vm, QEMU_ASYNC_JOB_NONE) < 0)
+        goto cleanup;
+
+    for (i = 0; i < def->ndisks; i++) {
+        virDomainCheckpointDiskDefPtr disk = &def->disks[i];
+
+        if (disk->type != VIR_DOMAIN_CHECKPOINT_TYPE_BITMAP)
+            continue;
+
+        if (vm->def->disks[i]->src->format > 0 &&
+            vm->def->disks[i]->src->format != VIR_STORAGE_FILE_QCOW2) {
+            virReportError(VIR_ERR_CONFIG_UNSUPPORTED,
+                           _("checkpoint for disk %s unsupported "
+                             "for storage type %s"),
+                           disk->name,
+                           virStorageFileFormatTypeToString(
+                               vm->def->disks[i]->src->format));
+            goto cleanup;
+        }
+    }
+
+    ret = 0;
+
+ cleanup:
+    VIR_FREE(xml);
+    return ret;
+}
+
+
+static virDomainCheckpointPtr
+qemuDomainCheckpointCreateXML(virDomainPtr domain,
+                              const char *xmlDesc,
+                              unsigned int flags)
+{
+    virQEMUDriverPtr driver = domain->conn->privateData;
+    virDomainObjPtr vm = NULL;
+    char *xml = NULL;
+    virDomainMomentObjPtr chk = NULL;
+    virDomainCheckpointPtr checkpoint = NULL;
+    virDomainCheckpointDefPtr def = NULL;
+    virDomainMomentObjPtr current = NULL;
+    bool update_current = true;
+    bool redefine = flags & VIR_DOMAIN_CHECKPOINT_CREATE_REDEFINE;
+    unsigned int parse_flags = 0;
+    virDomainMomentObjPtr other = NULL;
+    virQEMUDriverConfigPtr cfg = NULL;
+    virCapsPtr caps = NULL;
+
+    virCheckFlags(VIR_DOMAIN_CHECKPOINT_CREATE_REDEFINE |
+                  VIR_DOMAIN_CHECKPOINT_CREATE_CURRENT |
+                  VIR_DOMAIN_CHECKPOINT_CREATE_NO_METADATA, NULL);
+    /* TODO: VIR_DOMAIN_CHECKPOINT_CREATE_QUIESCE */
+
+    if (redefine)
+        parse_flags |= VIR_DOMAIN_CHECKPOINT_PARSE_REDEFINE;
+    if ((redefine && !(flags & VIR_DOMAIN_CHECKPOINT_CREATE_CURRENT)) ||
+        (flags & VIR_DOMAIN_CHECKPOINT_CREATE_NO_METADATA))
+        update_current = false;
+
+    if (!(vm = qemuDomObjFromDomain(domain)))
+        goto cleanup;
+
+    cfg = virQEMUDriverGetConfig(driver);
+
+    if (virDomainCheckpointCreateXMLEnsureACL(domain->conn, vm->def, flags) < 0)
+        goto cleanup;
+
+    if (!(caps = virQEMUDriverGetCapabilities(driver, false)))
+        goto cleanup;
+
+    if (qemuProcessAutoDestroyActive(driver, vm)) {
+        virReportError(VIR_ERR_OPERATION_INVALID,
+                       "%s", _("domain is marked for auto destroy"));
+        goto cleanup;
+    }
+
+    if (!virDomainObjIsActive(vm)) {
+        virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s",
+                       _("cannot create checkpoint for inactive domain"));
+        goto cleanup;
+    }
+
+    if (!(def = qemuDomainCheckpointDefParseString(driver, caps, xmlDesc,
+                                                   parse_flags)))
+        goto cleanup;
+
+    /* We are going to modify the domain below. */
+    if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_MODIFY) < 0)
+        goto cleanup;
+
+    if (redefine) {
+        if (virDomainCheckpointRedefinePrep(domain, vm, &def, &chk,
+                                            driver->xmlopt,
+                                            &update_current) < 0)
+            goto endjob;
+    } else if (qemuDomainCheckpointPrepare(driver, caps, vm, def) < 0) {
+        goto endjob;
+    }
+
+    if (!chk) {
+        if (!(chk = virDomainCheckpointAssignDef(vm->checkpoints, def)))
+            goto endjob;
+
+        def = NULL;
+    }
+
+    current = virDomainCheckpointGetCurrent(vm->checkpoints);
+    if (current) {
+        if (!redefine &&
+            VIR_STRDUP(chk->def->parent, current->def->name) < 0)
+            goto endjob;
+        if (update_current) {
+            virDomainCheckpointSetCurrent(vm->checkpoints, NULL);
+            if (qemuDomainCheckpointWriteMetadata(vm, current,
+                                                  driver->caps, driver->xmlopt,
+                                                  cfg->checkpointDir) < 0)
+                goto endjob;
+        }
+    }
+
+    /* actually do the checkpoint */
+    if (redefine) {
+        /* XXX Should we validate that the redefined checkpoint even
+         * makes sense, such as checking that qemu-img recognizes the
+         * checkpoint bitmap name in at least one of the domain's disks?  */
+    } else {
+        /* TODO: issue QMP transaction command */
+    }
+
+    /* If we fail after this point, there's not a whole lot we can do;
+     * we've successfully created the checkpoint, so we have to go
+     * forward the best we can.
+     */
+    checkpoint = virGetDomainCheckpoint(domain, chk->def->name);
+
+ endjob:
+    if (checkpoint && !(flags & VIR_DOMAIN_CHECKPOINT_CREATE_NO_METADATA)) {
+        if (update_current)
+            virDomainCheckpointSetCurrent(vm->checkpoints, chk);
+        if (qemuDomainCheckpointWriteMetadata(vm, chk, driver->caps,
+                                              driver->xmlopt,
+                                              cfg->checkpointDir) < 0) {
+            /* if writing of metadata fails, error out rather than trying
+             * to silently carry on without completing the checkpoint */
+            virObjectUnref(checkpoint);
+            checkpoint = NULL;
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                           _("unable to save metadata for checkpoint %s"),
+                           chk->def->name);
+            virDomainCheckpointObjListRemove(vm->checkpoints, chk);
+        } else {
+            other = virDomainCheckpointFindByName(vm->checkpoints,
+                                                  chk->def->parent);
+            virDomainMomentSetParent(chk, other);
+        }
+    } else if (chk) {
+        virDomainCheckpointObjListRemove(vm->checkpoints, chk);
+    }
+
+    qemuDomainObjEndJob(driver, vm);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    virDomainCheckpointDefFree(def);
+    VIR_FREE(xml);
+    virObjectUnref(caps);
+    virObjectUnref(cfg);
+    return checkpoint;
+}
+
+
+static int
+qemuDomainListAllCheckpoints(virDomainPtr domain,
+                             virDomainCheckpointPtr **chks,
+                             unsigned int flags)
+{
+    virDomainObjPtr vm = NULL;
+    int n = -1;
+
+    virCheckFlags(VIR_DOMAIN_CHECKPOINT_LIST_ROOTS |
+                  VIR_DOMAIN_CHECKPOINT_LIST_TOPOLOGICAL |
+                  VIR_DOMAIN_CHECKPOINT_FILTERS_ALL, -1);
+
+    if (!(vm = qemuDomObjFromDomain(domain)))
+        return -1;
+
+    if (virDomainListAllCheckpointsEnsureACL(domain->conn, vm->def) < 0)
+        goto cleanup;
+
+    n = virDomainListCheckpoints(vm->checkpoints, NULL, domain, chks, flags);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return n;
+}
+
+
+static int
+qemuDomainCheckpointListAllChildren(virDomainCheckpointPtr checkpoint,
+                                    virDomainCheckpointPtr **chks,
+                                    unsigned int flags)
+{
+    virDomainObjPtr vm = NULL;
+    virDomainMomentObjPtr chk = NULL;
+    int n = -1;
+
+    virCheckFlags(VIR_DOMAIN_CHECKPOINT_LIST_DESCENDANTS |
+                  VIR_DOMAIN_CHECKPOINT_LIST_TOPOLOGICAL |
+                  VIR_DOMAIN_CHECKPOINT_FILTERS_ALL, -1);
+
+    if (!(vm = qemuDomObjFromCheckpoint(checkpoint)))
+        return -1;
+
+    if (virDomainCheckpointListAllChildrenEnsureACL(checkpoint->domain->conn,
+                                                    vm->def) < 0)
+        goto cleanup;
+
+    if (!(chk = qemuCheckObjFromCheckpoint(vm, checkpoint)))
+        goto cleanup;
+
+    n = virDomainListCheckpoints(vm->checkpoints, chk, checkpoint->domain,
+                                 chks, flags);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return n;
+}
+
+
+static virDomainCheckpointPtr
+qemuDomainCheckpointLookupByName(virDomainPtr domain,
+                                 const char *name,
+                                 unsigned int flags)
+{
+    virDomainObjPtr vm;
+    virDomainMomentObjPtr chk = NULL;
+    virDomainCheckpointPtr checkpoint = NULL;
+
+    virCheckFlags(0, NULL);
+
+    if (!(vm = qemuDomObjFromDomain(domain)))
+        return NULL;
+
+    if (virDomainCheckpointLookupByNameEnsureACL(domain->conn, vm->def) < 0)
+        goto cleanup;
+
+    if (!(chk = qemuCheckObjFromName(vm, name)))
+        goto cleanup;
+
+    checkpoint = virGetDomainCheckpoint(domain, chk->def->name);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return checkpoint;
+}
+
+
+static int
+qemuDomainHasCurrentCheckpoint(virDomainPtr domain,
+                               unsigned int flags)
+{
+    virDomainObjPtr vm;
+    int ret = -1;
+
+    virCheckFlags(0, -1);
+
+    if (!(vm = qemuDomObjFromDomain(domain)))
+        return -1;
+
+    if (virDomainHasCurrentCheckpointEnsureACL(domain->conn, vm->def) < 0)
+        goto cleanup;
+
+    ret = (virDomainCheckpointGetCurrent(vm->checkpoints) != NULL);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return ret;
+}
+
+
+static virDomainCheckpointPtr
+qemuDomainCheckpointGetParent(virDomainCheckpointPtr checkpoint,
+                              unsigned int flags)
+{
+    virDomainObjPtr vm;
+    virDomainMomentObjPtr chk = NULL;
+    virDomainCheckpointPtr parent = NULL;
+
+    virCheckFlags(0, NULL);
+
+    if (!(vm = qemuDomObjFromCheckpoint(checkpoint)))
+        return NULL;
+
+    if (virDomainCheckpointGetParentEnsureACL(checkpoint->domain->conn, vm->def) < 0)
+        goto cleanup;
+
+    if (!(chk = qemuCheckObjFromCheckpoint(vm, checkpoint)))
+        goto cleanup;
+
+    if (!chk->def->parent) {
+        virReportError(VIR_ERR_NO_DOMAIN_CHECKPOINT,
+                       _("checkpoint '%s' does not have a parent"),
+                       chk->def->name);
+        goto cleanup;
+    }
+
+    parent = virGetDomainCheckpoint(checkpoint->domain, chk->def->parent);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return parent;
+}
+
+
+static virDomainCheckpointPtr
+qemuDomainCheckpointCurrent(virDomainPtr domain,
+                            unsigned int flags)
+{
+    virDomainObjPtr vm;
+    virDomainCheckpointPtr checkpoint = NULL;
+    const char *name;
+
+    virCheckFlags(0, NULL);
+
+    if (!(vm = qemuDomObjFromDomain(domain)))
+        return NULL;
+
+    if (virDomainCheckpointCurrentEnsureACL(domain->conn, vm->def) < 0)
+        goto cleanup;
+
+    name = virDomainCheckpointGetCurrentName(vm->checkpoints);
+    if (!name) {
+        virReportError(VIR_ERR_NO_DOMAIN_CHECKPOINT, "%s",
+                       _("the domain does not have a current checkpoint"));
+        goto cleanup;
+    }
+
+    checkpoint = virGetDomainCheckpoint(domain, name);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return checkpoint;
+}
+
+
+static char *
+qemuDomainCheckpointGetXMLDesc(virDomainCheckpointPtr checkpoint,
+                               unsigned int flags)
+{
+    virQEMUDriverPtr driver = checkpoint->domain->conn->privateData;
+    virDomainObjPtr vm = NULL;
+    char *xml = NULL;
+    virDomainMomentObjPtr chk = NULL;
+    qemuDomainObjPrivatePtr priv;
+    int rc;
+    size_t i;
+    virDomainCheckpointDefPtr chkdef;
+
+    virCheckFlags(VIR_DOMAIN_CHECKPOINT_XML_SECURE |
+                  VIR_DOMAIN_CHECKPOINT_XML_NO_DOMAIN |
+                  VIR_DOMAIN_CHECKPOINT_XML_SIZE, NULL);
+
+    if (!(vm = qemuDomObjFromCheckpoint(checkpoint)))
+        return NULL;
+
+    if (virDomainCheckpointGetXMLDescEnsureACL(checkpoint->domain->conn, vm->def, flags) < 0)
+        goto cleanup;
+
+    if (!(chk = qemuCheckObjFromCheckpoint(vm, checkpoint)))
+        goto cleanup;
+    chkdef = virDomainCheckpointObjGetDef(chk);
+
+    if (flags & VIR_DOMAIN_CHECKPOINT_XML_SIZE) {
+        /* TODO: for non-current checkpoint, this requires a QMP sequence per
+           disk, since the stat of one bitmap in isolation is too low,
+           and merely adding bitmap sizes may be too high:
+             block-dirty-bitmap-create tmp
+             for each bitmap from checkpoint to current:
+               add bitmap to src_list
+             block-dirty-bitmap-merge dst=tmp src_list
+             query-block and read tmp size
+             block-dirty-bitmap-remove tmp
+           So for now, go with simpler query-blocks only for current.
+        */
+        if (virDomainCheckpointGetCurrent(vm->checkpoints) != chk) {
+            virReportError(VIR_ERR_OPERATION_UNSUPPORTED,
+                           _("cannot compute size for non-current checkpoint '%s'"),
+                           checkpoint->name);
+            goto cleanup;
+        }
+
+        if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_QUERY) < 0)
+            goto cleanup;
+
+        if (virDomainObjCheckActive(vm) < 0)
+            goto endjob;
+
+        if (qemuBlockNodeNamesDetect(driver, vm, QEMU_ASYNC_JOB_NONE) < 0)
+            goto endjob;
+
+        /* TODO: Shouldn't need to recompute node names. */
+        for (i = 0; i < chkdef->ndisks; i++) {
+            virDomainCheckpointDiskDef *disk = &chkdef->disks[i];
+
+            if (disk->type != VIR_DOMAIN_CHECKPOINT_TYPE_BITMAP)
+                continue;
+            VIR_FREE(chk->def->dom->disks[disk->idx]->src->nodeformat);
+            if (VIR_STRDUP(chk->def->dom->disks[disk->idx]->src->nodeformat,
+                           qemuBlockNodeLookup(vm, disk->name)) < 0)
+                goto endjob;
+        }
+
+        priv = vm->privateData;
+        qemuDomainObjEnterMonitor(driver, vm);
+        rc = qemuMonitorUpdateCheckpointSize(priv->mon, chkdef);
+        if (qemuDomainObjExitMonitor(driver, vm) < 0)
+            goto endjob;
+        if (rc < 0)
+            goto endjob;
+    }
+
+    xml = virDomainCheckpointDefFormat(chkdef, driver->caps, driver->xmlopt,
+                                       flags);
+
+ endjob:
+    if (flags & VIR_DOMAIN_CHECKPOINT_XML_SIZE)
+        qemuDomainObjEndJob(driver, vm);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return xml;
+}
+
+
+static int
+qemuDomainCheckpointIsCurrent(virDomainCheckpointPtr checkpoint,
+                              unsigned int flags)
+{
+    virDomainObjPtr vm = NULL;
+    int ret = -1;
+    virDomainMomentObjPtr chk = NULL;
+
+    virCheckFlags(0, -1);
+
+    if (!(vm = qemuDomObjFromCheckpoint(checkpoint)))
+        return -1;
+
+    if (virDomainCheckpointIsCurrentEnsureACL(checkpoint->domain->conn, vm->def) < 0)
+        goto cleanup;
+
+    if (!(chk = qemuCheckObjFromCheckpoint(vm, checkpoint)))
+        goto cleanup;
+
+    ret = chk == virDomainCheckpointGetCurrent(vm->checkpoints);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return ret;
+}
+
+
+static int
+qemuDomainCheckpointHasMetadata(virDomainCheckpointPtr checkpoint,
+                                unsigned int flags)
+{
+    virDomainObjPtr vm = NULL;
+    int ret = -1;
+    virDomainMomentObjPtr chk = NULL;
+
+    virCheckFlags(0, -1);
+
+    if (!(vm = qemuDomObjFromCheckpoint(checkpoint)))
+        return -1;
+
+    if (virDomainCheckpointHasMetadataEnsureACL(checkpoint->domain->conn, vm->def) < 0)
+        goto cleanup;
+
+    if (!(chk = qemuCheckObjFromCheckpoint(vm, checkpoint)))
+        goto cleanup;
+
+    /* XXX Someday, we should recognize internal bitmaps in qcow2
+     * images that are not tied to a libvirt checkpoint; if we ever do
+     * that, then we would have a reason to return 0 here.  */
+    ret = 1;
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    return ret;
+}
+
+
+static int
+qemuDomainCheckpointDelete(virDomainCheckpointPtr checkpoint,
+                           unsigned int flags)
+{
+    virQEMUDriverPtr driver = checkpoint->domain->conn->privateData;
+    virDomainObjPtr vm = NULL;
+    int ret = -1;
+    virDomainMomentObjPtr chk = NULL;
+    virQEMUMomentRemove rem;
+    virQEMUMomentReparent rep;
+    bool metadata_only = !!(flags & VIR_DOMAIN_CHECKPOINT_DELETE_METADATA_ONLY);
+    virQEMUDriverConfigPtr cfg = NULL;
+
+    virCheckFlags(VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN |
+                  VIR_DOMAIN_CHECKPOINT_DELETE_METADATA_ONLY |
+                  VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN_ONLY, -1);
+
+    if (!(vm = qemuDomObjFromCheckpoint(checkpoint)))
+        return -1;
+
+    cfg = virQEMUDriverGetConfig(driver);
+
+    if (virDomainCheckpointDeleteEnsureACL(checkpoint->domain->conn, vm->def) < 0)
+        goto cleanup;
+
+    if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_MODIFY) < 0)
+        goto cleanup;
+
+    if (!(chk = qemuCheckObjFromCheckpoint(vm, checkpoint)))
+        goto endjob;
+
+    if (flags & (VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN |
+                 VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN_ONLY)) {
+        rem.driver = driver;
+        rem.vm = vm;
+        rem.metadata_only = metadata_only;
+        rem.err = 0;
+        rem.current = virDomainCheckpointGetCurrent(vm->checkpoints);
+        rem.found = false;
+        rem.momentDiscard = qemuDomainCheckpointDiscard;
+        virDomainMomentForEachDescendant(chk, qemuDomainMomentDiscardAll,
+                                         &rem);
+        if (rem.err < 0)
+            goto endjob;
+        if (rem.found) {
+            virDomainCheckpointSetCurrent(vm->checkpoints, chk);
+            if (flags & VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN_ONLY) {
+                if (qemuDomainCheckpointWriteMetadata(vm, chk, driver->caps,
+                                                      driver->xmlopt,
+                                                      cfg->checkpointDir) < 0) {
+                    virReportError(VIR_ERR_INTERNAL_ERROR,
+                                   _("failed to set checkpoint '%s' as current"),
+                                   chk->def->name);
+                    virDomainCheckpointSetCurrent(vm->checkpoints, NULL);
+                    goto endjob;
+                }
+            }
+        }
+    } else if (chk->nchildren) {
+        rep.dir = cfg->checkpointDir;
+        rep.parent = chk->parent;
+        rep.vm = vm;
+        rep.err = 0;
+        rep.caps = driver->caps;
+        rep.xmlopt = driver->xmlopt;
+        rep.writeMetadata = qemuDomainCheckpointWriteMetadata;
+        virDomainMomentForEachChild(chk, qemuDomainMomentReparentChildren,
+                                    &rep);
+        if (rep.err < 0)
+            goto endjob;
+        virDomainMomentMoveChildren(chk, chk->parent);
+    }
+
+    if (flags & VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN_ONLY) {
+        virDomainMomentDropChildren(chk);
+        ret = 0;
+    } else {
+        ret = qemuDomainCheckpointDiscard(driver, vm, chk, true, metadata_only);
+    }
+
+ endjob:
+    qemuDomainObjEndJob(driver, vm);
+
+ cleanup:
+    virDomainObjEndAPI(&vm);
+    virObjectUnref(cfg);
+    return ret;
+}
+
 static int qemuDomainQemuMonitorCommand(virDomainPtr domain, const char *cmd,
                                         char **result, unsigned int flags)
 {
@@ -16932,6 +17747,16 @@ static virDomainPtr qemuDomainQemuAttach(virConnectPtr conn,
         goto cleanup;
     }

+    if (qemuProcessAttach(conn, driver, vm, pid,
+                          pidfile, monConfig, monJSON) < 0) {
+        monConfig = NULL;
+        qemuDomainRemoveInactive(driver, vm);
+        qemuDomainObjEndJob(driver, vm);
+        goto cleanup;
+    }
+
+    monConfig = NULL;
+
     if (qemuProcessAttach(conn, driver, vm, pid,
                           pidfile, monConfig, monJSON) < 0) {
         qemuDomainRemoveInactive(driver, vm);
@@ -21739,6 +22564,12 @@ static int qemuDomainRename(virDomainPtr dom,
         goto endjob;
     }

+    if (virDomainListCheckpoints(vm->checkpoints, NULL, dom, NULL, flags) > 0) {
+        virReportError(VIR_ERR_OPERATION_INVALID,
+                       "%s", _("cannot rename domain with checkpoints"));
+        goto endjob;
+    }
+
     if (virDomainObjListRename(driver->domains, vm, new_name, flags,
                                qemuDomainRenameCallback, driver) < 0)
         goto endjob;
@@ -22559,6 +23390,18 @@ static virHypervisorDriver qemuHypervisorDriver = {
     .connectBaselineHypervisorCPU = qemuConnectBaselineHypervisorCPU, /* 4.4.0 */
     .nodeGetSEVInfo = qemuNodeGetSEVInfo, /* 4.5.0 */
     .domainGetLaunchSecurityInfo = qemuDomainGetLaunchSecurityInfo, /* 4.5.0 */
+    .domainCheckpointCreateXML = qemuDomainCheckpointCreateXML, /* 5.2.0 */
+    .domainCheckpointGetXMLDesc = qemuDomainCheckpointGetXMLDesc, /* 5.2.0 */
+
+    .domainListAllCheckpoints = qemuDomainListAllCheckpoints, /* 5.2.0 */
+    .domainCheckpointListAllChildren = qemuDomainCheckpointListAllChildren, /* 5.2.0 */
+    .domainCheckpointLookupByName = qemuDomainCheckpointLookupByName, /* 5.2.0 */
+    .domainHasCurrentCheckpoint = qemuDomainHasCurrentCheckpoint, /* 5.2.0 */
+    .domainCheckpointGetParent = qemuDomainCheckpointGetParent, /* 5.2.0 */
+    .domainCheckpointCurrent = qemuDomainCheckpointCurrent, /* 5.2.0 */
+    .domainCheckpointIsCurrent = qemuDomainCheckpointIsCurrent, /* 5.2.0 */
+    .domainCheckpointHasMetadata = qemuDomainCheckpointHasMetadata, /* 5.2.0 */
+    .domainCheckpointDelete = qemuDomainCheckpointDelete, /* 5.2.0 */
 };


-- 
2.20.1




More information about the libvir-list mailing list