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(a)redhat.com>
---
src/qemu/qemu_block.h | 3 +
src/qemu/qemu_conf.h | 2 +
src/qemu/qemu_domain.h | 25 +-
src/qemu/qemu_block.c | 12 +
src/qemu/qemu_conf.c | 5 +
src/qemu/qemu_domain.c | 132 +++++-
src/qemu/qemu_driver.c | 881 ++++++++++++++++++++++++++++++++++++++++-
7 files changed, 1051 insertions(+), 9 deletions(-)
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 fe474170dc..fed271d22d 100644
--- a/src/qemu/qemu_domain.h
+++ b/src/qemu/qemu_domain.h
@@ -1,7 +1,7 @@
/*
* qemu_domain.h: QEMU domain private state
*
- * Copyright (C) 2006-2016 Red Hat, Inc.
+ * Copyright (C) 2006-2018 Red Hat, Inc.
* Copyright (C) 2006 Daniel P. Berrange
*
* This library is free software; you can redistribute it and/or
@@ -697,9 +697,10 @@ int qemuDomainSnapshotDiscard(virQEMUDriverPtr driver,
bool update_current,
bool metadata_only);
-typedef struct _virQEMUSnapRemove virQEMUSnapRemove;
-typedef virQEMUSnapRemove *virQEMUSnapRemovePtr;
-struct _virQEMUSnapRemove {
+/* Used for both snapshots and checkpoints */
+typedef struct _virQEMUDependentRemove virQEMUDependentRemove;
+typedef virQEMUDependentRemove *virQEMUDependentRemovePtr;
+struct _virQEMUDependentRemove {
virQEMUDriverPtr driver;
virDomainObjPtr vm;
int err;
@@ -714,6 +715,22 @@ int qemuDomainSnapshotDiscardAll(void *payload,
int qemuDomainSnapshotDiscardAllMetadata(virQEMUDriverPtr driver,
virDomainObjPtr vm);
+int qemuDomainCheckpointWriteMetadata(virDomainObjPtr vm,
+ virDomainCheckpointObjPtr checkpoint,
+ virCapsPtr caps,
+ virDomainXMLOptionPtr xmlopt,
+ char *checkpointDir);
+
+int qemuDomainCheckpointDiscard(virQEMUDriverPtr driver,
+ virDomainObjPtr vm,
+ virDomainCheckpointObjPtr chk,
+ bool update_current,
+ bool metadata_only);
+
+int qemuDomainCheckpointDiscardAll(void *payload,
+ const void *name,
+ void *data);
+
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 2f5ef8d0c4..cf1c9c9daf 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 b6c1a0e4e5..6a6266a05b 100644
--- a/src/qemu/qemu_domain.c
+++ b/src/qemu/qemu_domain.c
@@ -1,7 +1,7 @@
/*
* qemu_domain.c: QEMU domain private state
*
- * Copyright (C) 2006-2016 Red Hat, Inc.
+ * Copyright (C) 2006-2018 Red Hat, Inc.
* Copyright (C) 2006 Daniel P. Berrange
*
* This library is free software; you can redistribute it and/or
@@ -8424,6 +8424,45 @@ qemuDomainSnapshotWriteMetadata(virDomainObjPtr vm,
return ret;
}
+int
+qemuDomainCheckpointWriteMetadata(virDomainObjPtr vm,
+ virDomainCheckpointObjPtr checkpoint,
+ virCapsPtr caps,
+ virDomainXMLOptionPtr xmlopt,
+ char *checkpointDir)
+{
+ char *newxml = NULL;
+ int ret = -1;
+ char *chkDir = NULL;
+ char *chkFile = NULL;
+
+ newxml = virDomainCheckpointDefFormat(
+ checkpoint->def, caps, xmlopt,
+ virDomainDefFormatConvertXMLFlags(QEMU_DOMAIN_FORMAT_LIVE_FLAGS),
+ 1);
+ if (newxml == NULL)
+ return -1;
+
+ if (virAsprintf(&chkDir, "%s/%s", checkpointDir, vm->def->name)
< 0)
+ goto cleanup;
+ if (virFileMakePath(chkDir) < 0) {
+ virReportSystemError(errno, _("cannot create checkpoint directory
'%s'"),
+ chkDir);
+ goto cleanup;
+ }
+
+ if (virAsprintf(&chkFile, "%s/%s.xml", chkDir,
checkpoint->def->name) < 0)
+ goto cleanup;
+
+ ret = virXMLSaveFile(chkFile, NULL, "checkpoint-edit", newxml);
+
+ cleanup:
+ VIR_FREE(chkFile);
+ VIR_FREE(chkDir);
+ VIR_FREE(newxml);
+ return ret;
+}
+
/* 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
@@ -8591,7 +8630,7 @@ int qemuDomainSnapshotDiscardAll(void *payload,
void *data)
{
virDomainSnapshotObjPtr snap = payload;
- virQEMUSnapRemovePtr curr = data;
+ virQEMUDependentRemovePtr curr = data;
int err;
if (snap->def->current)
@@ -8607,7 +8646,7 @@ int
qemuDomainSnapshotDiscardAllMetadata(virQEMUDriverPtr driver,
virDomainObjPtr vm)
{
- virQEMUSnapRemove rem;
+ virQEMUDependentRemove rem;
rem.driver = driver;
rem.vm = vm;
@@ -8620,6 +8659,93 @@ qemuDomainSnapshotDiscardAllMetadata(virQEMUDriverPtr driver,
}
+/* Discard one checkpoint (or its metadata), without reparenting any children. */
+int
+qemuDomainCheckpointDiscard(virQEMUDriverPtr driver,
+ virDomainObjPtr vm,
+ virDomainCheckpointObjPtr chk,
+ bool update_parent,
+ bool metadata_only)
+{
+ char *chkFile = NULL;
+ int ret = -1;
+ virDomainCheckpointObjPtr 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 == vm->current_checkpoint) {
+ 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 {
+ parentchk->def->current = true;
+ if (qemuDomainCheckpointWriteMetadata(vm, parentchk, driver->caps,
+ driver->xmlopt,
+ cfg->checkpointDir) < 0) {
+ VIR_WARN("failed to set parent checkpoint '%s' as
current",
+ chk->def->parent);
+ parentchk->def->current = false;
+ parentchk = NULL;
+ }
+ }
+ }
+ vm->current_checkpoint = parentchk;
+ }
+
+ if (unlink(chkFile) < 0)
+ VIR_WARN("Failed to unlink %s", chkFile);
+ if (update_parent)
+ virDomainCheckpointDropParent(chk);
+ virDomainCheckpointObjListRemove(vm->checkpoints, chk);
+
+ ret = 0;
+
+ cleanup:
+ VIR_FREE(chkFile);
+ virObjectUnref(cfg);
+ return ret;
+}
+
+/* Hash iterator callback to discard multiple checkpoints. */
+int qemuDomainCheckpointDiscardAll(void *payload,
+ const void *name ATTRIBUTE_UNUSED,
+ void *data)
+{
+ virDomainCheckpointObjPtr chk = payload;
+ virQEMUDependentRemovePtr curr = data;
+ int err;
+
+ if (chk->def->current)
+ curr->current = true;
+ err = qemuDomainCheckpointDiscard(curr->driver, curr->vm, chk, false,
+ curr->metadata_only);
+ if (err && !curr->err)
+ curr->err = err;
+ return 0;
+}
+
static void
qemuDomainRemoveInactiveCommon(virQEMUDriverPtr driver,
virDomainObjPtr vm)
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c
index 2d39e9de96..bc5ced5ab2 100644
--- a/src/qemu/qemu_driver.c
+++ b/src/qemu/qemu_driver.c
@@ -1,7 +1,7 @@
/*
* qemu_driver.c: core driver methods for managing qemu guests
*
- * Copyright (C) 2006-2016 Red Hat, Inc.
+ * Copyright (C) 2006-2019 Red Hat, Inc.
* Copyright (C) 2006 Daniel P. Berrange
*
* This library is free software; you can redistribute it and/or
@@ -219,6 +219,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 virDomainCheckpointObjPtr
+qemuCheckObjFromName(virDomainObjPtr vm,
+ const char *name)
+{
+ virDomainCheckpointObjPtr 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 virDomainCheckpointObjPtr
+qemuCheckObjFromCheckpoint(virDomainObjPtr vm,
+ virDomainCheckpointPtr checkpoint)
+{
+ return qemuCheckObjFromName(vm, checkpoint->name);
+}
+
static int
qemuAutostartDomain(virDomainObjPtr vm,
void *opaque)
@@ -526,6 +560,127 @@ 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;
+ virDomainCheckpointObjPtr chk = NULL;
+ virDomainCheckpointObjPtr current = NULL;
+ unsigned int flags = (VIR_DOMAIN_CHECKPOINT_PARSE_REDEFINE |
+ VIR_DOMAIN_CHECKPOINT_PARSE_DISKS |
+ 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,
+ 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 (chk->def->current) {
+ current = chk;
+ if (!vm->current_checkpoint)
+ vm->current_checkpoint = chk;
+ }
+
+ VIR_FREE(fullpath);
+ VIR_FREE(xmlStr);
+ }
+ if (direrr < 0)
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Failed to fully read directory %s"),
+ chkDir);
+
+ if (vm->current_checkpoint != current) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Too many checkpoints claiming to be current for domain
%s"),
+ vm->def->name);
+ vm->current_checkpoint = NULL;
+ }
+
+ 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)
@@ -656,6 +811,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);
@@ -763,6 +923,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"),
@@ -901,6 +1068,10 @@ qemuStateInitialize(bool privileged,
qemuDomainSnapshotLoad,
cfg->snapshotDir);
+ virDomainObjListForEach(qemu_driver->domains,
+ qemuDomainCheckpointLoad,
+ cfg->checkpointDir);
+
virDomainObjListForEach(qemu_driver->domains,
qemuDomainManagedSaveLoad,
qemu_driver);
@@ -7804,6 +7975,7 @@ qemuDomainUndefineFlags(virDomainPtr dom,
if (qemuDomainSnapshotDiscardAllMetadata(driver, vm) < 0)
goto endjob;
}
+ /* TODO: Restrict deletion if checkpoints exist? */
name = qemuDomainManagedSavePath(driver, vm);
if (name == NULL)
@@ -16726,7 +16898,7 @@ qemuDomainSnapshotDelete(virDomainSnapshotPtr snapshot,
virDomainObjPtr vm = NULL;
int ret = -1;
virDomainSnapshotObjPtr snap = NULL;
- virQEMUSnapRemove rem;
+ virQEMUDependentRemove rem;
virQEMUSnapReparent rep;
bool metadata_only = !!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY);
int external = 0;
@@ -16830,6 +17002,693 @@ 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 = VIR_DOMAIN_CHECKPOINT_PARSE_DISKS;
+
+ if (flags & VIR_DOMAIN_CHECKPOINT_CREATE_REDEFINE)
+ parse_flags |= VIR_DOMAIN_CHECKPOINT_PARSE_REDEFINE;
+ if (!(def = virDomainCheckpointDefParseString(xmlDesc, caps, driver->xmlopt,
+ 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->name, '/')) {
+ virReportError(VIR_ERR_XML_DETAIL,
+ _("invalid checkpoint name '%s': "
+ "name can't contain '/'"),
+ def->name);
+ goto cleanup;
+ }
+
+ if (def->name[0] == '.') {
+ virReportError(VIR_ERR_XML_DETAIL,
+ _("invalid checkpoint name '%s': "
+ "name can't start with '.'"),
+ def->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->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;
+ virDomainCheckpointObjPtr chk = NULL;
+ virDomainCheckpointPtr checkpoint = NULL;
+ virDomainCheckpointDefPtr def = NULL;
+ bool update_current = true;
+ bool redefine = flags & VIR_DOMAIN_CHECKPOINT_CREATE_REDEFINE;
+ virDomainCheckpointObjPtr 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 && !(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) < 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,
+ 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;
+ }
+
+ if (update_current)
+ chk->def->current = true;
+ if (vm->current_checkpoint) {
+ if (!redefine &&
+ VIR_STRDUP(chk->def->parent,
vm->current_checkpoint->def->name) < 0)
+ goto endjob;
+ if (update_current) {
+ vm->current_checkpoint->def->current = false;
+ if (qemuDomainCheckpointWriteMetadata(vm, vm->current_checkpoint,
+ driver->caps, driver->xmlopt,
+ cfg->checkpointDir) < 0)
+ goto endjob;
+ vm->current_checkpoint = NULL;
+ }
+ }
+
+ /* 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 (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 {
+ if (update_current)
+ vm->current_checkpoint = chk;
+ other = virDomainCheckpointFindByName(vm->checkpoints,
+ chk->def->parent);
+ chk->parent = other;
+ other->nchildren++;
+ chk->sibling = other->first_child;
+ other->first_child = chk;
+ }
+ } 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
+qemuDomainListCheckpoints(virDomainPtr domain,
+ virDomainCheckpointPtr **chks,
+ unsigned int flags)
+{
+ virDomainObjPtr vm = NULL;
+ int n = -1;
+
+ virCheckFlags(VIR_DOMAIN_CHECKPOINT_LIST_ROOTS |
+ VIR_DOMAIN_CHECKPOINT_FILTERS_ALL, -1);
+
+ if (!(vm = qemuDomObjFromDomain(domain)))
+ return -1;
+
+ if (virDomainListCheckpointsEnsureACL(domain->conn, vm->def) < 0)
+ goto cleanup;
+
+ n = virDomainListAllCheckpoints(vm->checkpoints, NULL, domain, chks, flags);
+
+ cleanup:
+ virDomainObjEndAPI(&vm);
+ return n;
+}
+
+
+static int
+qemuDomainCheckpointListChildren(virDomainCheckpointPtr checkpoint,
+ virDomainCheckpointPtr **chks,
+ unsigned int flags)
+{
+ virDomainObjPtr vm = NULL;
+ virDomainCheckpointObjPtr chk = NULL;
+ int n = -1;
+
+ virCheckFlags(VIR_DOMAIN_CHECKPOINT_LIST_DESCENDANTS |
+ VIR_DOMAIN_CHECKPOINT_FILTERS_ALL, -1);
+
+ if (!(vm = qemuDomObjFromCheckpoint(checkpoint)))
+ return -1;
+
+ if (virDomainCheckpointListChildrenEnsureACL(checkpoint->domain->conn,
vm->def) < 0)
+ goto cleanup;
+
+ if (!(chk = qemuCheckObjFromCheckpoint(vm, checkpoint)))
+ goto cleanup;
+
+ n = virDomainListAllCheckpoints(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;
+ virDomainCheckpointObjPtr 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 = (vm->current_checkpoint != NULL);
+
+ cleanup:
+ virDomainObjEndAPI(&vm);
+ return ret;
+}
+
+
+static virDomainCheckpointPtr
+qemuDomainCheckpointGetParent(virDomainCheckpointPtr checkpoint,
+ unsigned int flags)
+{
+ virDomainObjPtr vm;
+ virDomainCheckpointObjPtr 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;
+
+ virCheckFlags(0, NULL);
+
+ if (!(vm = qemuDomObjFromDomain(domain)))
+ return NULL;
+
+ if (virDomainCheckpointCurrentEnsureACL(domain->conn, vm->def) < 0)
+ goto cleanup;
+
+ if (!vm->current_checkpoint) {
+ virReportError(VIR_ERR_NO_DOMAIN_CHECKPOINT, "%s",
+ _("the domain does not have a current checkpoint"));
+ goto cleanup;
+ }
+
+ checkpoint = virGetDomainCheckpoint(domain,
vm->current_checkpoint->def->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;
+ virDomainCheckpointObjPtr chk = NULL;
+ qemuDomainObjPrivatePtr priv;
+ int rc;
+ size_t i;
+
+ 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;
+
+ 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 (!vm->current_checkpoint ||
+ STRNEQ(checkpoint->name, vm->current_checkpoint->def->name)) {
+ 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 < chk->def->ndisks; i++) {
+ virDomainCheckpointDiskDef *disk = &chk->def->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, chk->def);
+ if (qemuDomainObjExitMonitor(driver, vm) < 0)
+ goto endjob;
+ if (rc < 0)
+ goto endjob;
+ }
+
+ xml = virDomainCheckpointDefFormat(chk->def, driver->caps, driver->xmlopt,
+ flags, 0);
+
+ 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;
+ virDomainCheckpointObjPtr 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 = (vm->current_checkpoint &&
+ STREQ(checkpoint->name, vm->current_checkpoint->def->name));
+
+ cleanup:
+ virDomainObjEndAPI(&vm);
+ return ret;
+}
+
+
+static int
+qemuDomainCheckpointHasMetadata(virDomainCheckpointPtr checkpoint,
+ unsigned int flags)
+{
+ virDomainObjPtr vm = NULL;
+ int ret = -1;
+ virDomainCheckpointObjPtr 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;
+}
+
+
+typedef struct _virQEMUCheckReparent virQEMUCheckReparent;
+typedef virQEMUCheckReparent *virQEMUCheckReparentPtr;
+struct _virQEMUCheckReparent {
+ virQEMUDriverConfigPtr cfg;
+ virDomainCheckpointObjPtr parent;
+ virDomainObjPtr vm;
+ virCapsPtr caps;
+ virDomainXMLOptionPtr xmlopt;
+ int err;
+ virDomainCheckpointObjPtr last;
+};
+
+
+static int
+qemuDomainCheckpointReparentChildren(void *payload,
+ const void *name ATTRIBUTE_UNUSED,
+ void *data)
+{
+ virDomainCheckpointObjPtr chk = payload;
+ virQEMUCheckReparentPtr rep = data;
+
+ if (rep->err < 0)
+ return 0;
+
+ VIR_FREE(chk->def->parent);
+ chk->parent = rep->parent;
+
+ if (rep->parent->def &&
+ VIR_STRDUP(chk->def->parent, rep->parent->def->name) < 0) {
+ rep->err = -1;
+ return 0;
+ }
+
+ if (!chk->sibling)
+ rep->last = chk;
+
+ rep->err = qemuDomainCheckpointWriteMetadata(rep->vm, chk,
+ rep->caps, rep->xmlopt,
+ rep->cfg->checkpointDir);
+ return 0;
+}
+
+
+static int
+qemuDomainCheckpointDelete(virDomainCheckpointPtr checkpoint,
+ unsigned int flags)
+{
+ virQEMUDriverPtr driver = checkpoint->domain->conn->privateData;
+ virDomainObjPtr vm = NULL;
+ int ret = -1;
+ virDomainCheckpointObjPtr chk = NULL;
+ virQEMUDependentRemove rem;
+ virQEMUCheckReparent 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 = false;
+ virDomainCheckpointForEachDescendant(chk,
+ qemuDomainCheckpointDiscardAll,
+ &rem);
+ if (rem.err < 0)
+ goto endjob;
+ if (rem.current) {
+ if (flags & VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN_ONLY) {
+ chk->def->current = true;
+ 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);
+ chk->def->current = false;
+ goto endjob;
+ }
+ }
+ vm->current_checkpoint = chk;
+ }
+ } else if (chk->nchildren) {
+ rep.cfg = cfg;
+ rep.parent = chk->parent;
+ rep.vm = vm;
+ rep.err = 0;
+ rep.last = NULL;
+ rep.caps = driver->caps;
+ rep.xmlopt = driver->xmlopt;
+ virDomainCheckpointForEachChild(chk,
+ qemuDomainCheckpointReparentChildren,
+ &rep);
+ if (rep.err < 0)
+ goto endjob;
+ /* Can't modify siblings during ForEachChild, so do it now. */
+ chk->parent->nchildren += chk->nchildren;
+ rep.last->sibling = chk->parent->first_child;
+ chk->parent->first_child = chk->first_child;
+ }
+
+ if (flags & VIR_DOMAIN_CHECKPOINT_DELETE_CHILDREN_ONLY) {
+ chk->nchildren = 0;
+ chk->first_child = NULL;
+ 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)
{
@@ -21770,6 +22629,12 @@ static int qemuDomainRename(virDomainPtr dom,
goto endjob;
}
+ if (virDomainListAllCheckpoints(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;
@@ -22590,6 +23455,18 @@ static virHypervisorDriver qemuHypervisorDriver = {
.connectBaselineHypervisorCPU = qemuConnectBaselineHypervisorCPU, /* 4.4.0 */
.nodeGetSEVInfo = qemuNodeGetSEVInfo, /* 4.5.0 */
.domainGetLaunchSecurityInfo = qemuDomainGetLaunchSecurityInfo, /* 4.5.0 */
+ .domainCheckpointCreateXML = qemuDomainCheckpointCreateXML, /* 5.1.0 */
+ .domainCheckpointGetXMLDesc = qemuDomainCheckpointGetXMLDesc, /* 5.1.0 */
+
+ .domainListCheckpoints = qemuDomainListCheckpoints, /* 5.1.0 */
+ .domainCheckpointListChildren = qemuDomainCheckpointListChildren, /* 5.1.0 */
+ .domainCheckpointLookupByName = qemuDomainCheckpointLookupByName, /* 5.1.0 */
+ .domainHasCurrentCheckpoint = qemuDomainHasCurrentCheckpoint, /* 5.1.0 */
+ .domainCheckpointGetParent = qemuDomainCheckpointGetParent, /* 5.1.0 */
+ .domainCheckpointCurrent = qemuDomainCheckpointCurrent, /* 5.1.0 */
+ .domainCheckpointIsCurrent = qemuDomainCheckpointIsCurrent, /* 5.1.0 */
+ .domainCheckpointHasMetadata = qemuDomainCheckpointHasMetadata, /* 5.1.0 */
+ .domainCheckpointDelete = qemuDomainCheckpointDelete, /* 5.1.0 */
};
--
2.20.1