Signed-off-by: Chris Lalancette <clalance(a)redhat.com>
---
src/qemu/qemu_conf.c | 9 +-
src/qemu/qemu_conf.h | 4 +-
src/qemu/qemu_driver.c | 830 +++++++++++++++++++++++++++++++++++++++++-
src/qemu/qemu_monitor.c | 39 ++
src/qemu/qemu_monitor.h | 4 +
src/qemu/qemu_monitor_json.c | 66 ++++
src/qemu/qemu_monitor_json.h | 4 +
src/qemu/qemu_monitor_text.c | 141 +++++++
src/qemu/qemu_monitor_text.h | 4 +
tests/qemuxml2argvtest.c | 2 +-
10 files changed, 1089 insertions(+), 14 deletions(-)
diff --git a/src/qemu/qemu_conf.c b/src/qemu/qemu_conf.c
index 55397cd..0e431c6 100644
--- a/src/qemu/qemu_conf.c
+++ b/src/qemu/qemu_conf.c
@@ -3373,7 +3373,9 @@ int qemudBuildCommandLine(virConnectPtr conn,
const char ***retenv,
int **tapfds,
int *ntapfds,
- const char *migrateFrom) {
+ const char *migrateFrom,
+ virDomainSnapshotObjPtr current_snapshot)
+{
int i;
char memory[50];
char boot[VIR_DOMAIN_BOOT_LAST];
@@ -4578,6 +4580,11 @@ int qemudBuildCommandLine(virConnectPtr conn,
ADD_ARG_LIT("virtio");
}
+ if (current_snapshot && current_snapshot->def->active) {
+ ADD_ARG_LIT("-loadvm");
+ ADD_ARG_LIT(current_snapshot->def->name);
+ }
+
ADD_ARG(NULL);
ADD_ENV(NULL);
diff --git a/src/qemu/qemu_conf.h b/src/qemu/qemu_conf.h
index 39518ca..44b7daa 100644
--- a/src/qemu/qemu_conf.h
+++ b/src/qemu/qemu_conf.h
@@ -120,6 +120,7 @@ struct qemud_driver {
* the QEMU user/group */
char *libDir;
char *cacheDir;
+ char *snapshotDir;
unsigned int vncTLS : 1;
unsigned int vncTLSx509verify : 1;
unsigned int vncSASL : 1;
@@ -198,7 +199,8 @@ int qemudBuildCommandLine (virConnectPtr conn,
const char ***retenv,
int **tapfds,
int *ntapfds,
- const char *migrateFrom)
+ const char *migrateFrom,
+ virDomainSnapshotObjPtr current_snapshot)
ATTRIBUTE_NONNULL(1);
/* With vlan == -1, use netdev syntax, else old hostnet */
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c
index 02ed95f..8c9bb46 100644
--- a/src/qemu/qemu_driver.c
+++ b/src/qemu/qemu_driver.c
@@ -1342,6 +1342,93 @@ no_memory:
return NULL;
}
+static void qemuDomainSnapshotLoad(void *payload,
+ const char *name ATTRIBUTE_UNUSED,
+ void *data)
+{
+ virDomainObjPtr vm = (virDomainObjPtr)payload;
+ char *baseDir = (char *)data;
+ char *snapDir = NULL;
+ DIR *dir = NULL;
+ struct dirent *entry;
+ char *xmlStr;
+ int ret;
+ char *fullpath;
+ virDomainSnapshotObjPtr snap = NULL;
+ virDomainSnapshotDefPtr def = NULL;
+ char ebuf[1024];
+
+ virDomainObjLock(vm);
+ if (virAsprintf(&snapDir, "%s/%s", baseDir, vm->def->name) <
0) {
+ VIR_ERROR("Failed to allocate memory for snapshot directory for domain
%s",
+ vm->def->name);
+ goto cleanup;
+ }
+
+ VIR_INFO("Scanning for snapshots for domain %s in %s",
vm->def->name,
+ snapDir);
+
+ if (!(dir = opendir(snapDir))) {
+ if (errno != ENOENT)
+ VIR_ERROR("Failed to open snapshot directory %s for domain %s:
%s",
+ snapDir, vm->def->name,
+ virStrerror(errno, ebuf, sizeof(ebuf)));
+ goto cleanup;
+ }
+
+ while ((entry = readdir(dir))) {
+ if (entry->d_name[0] == '.')
+ continue;
+
+ /* NB: ignoring errors, so one malformed config doesn't
+ kill the whole process */
+ VIR_INFO("Loading snapshot file '%s'", entry->d_name);
+
+ if (virAsprintf(&fullpath, "%s/%s", snapDir, entry->d_name) <
0) {
+ VIR_ERROR0("Failed to allocate memory for path");
+ continue;
+ }
+
+ ret = virFileReadAll(fullpath, 1024*1024*1, &xmlStr);
+ VIR_FREE(fullpath);
+ if (ret < 0) {
+ /* Nothing we can do here, skip this one */
+ VIR_ERROR("Failed to read snapshot file %s: %s", fullpath,
+ virStrerror(errno, ebuf, sizeof(ebuf)));
+ continue;
+ }
+
+ def = virDomainSnapshotDefParseString(xmlStr, 0);
+ if (def == NULL) {
+ /* Nothing we can do here, skip this one */
+ VIR_ERROR("Failed to parse snapshot XML from file '%s'",
fullpath);
+ VIR_FREE(xmlStr);
+ continue;
+ }
+
+ snap = virDomainSnapshotAssignDef(&vm->snapshots, def);
+
+ VIR_FREE(xmlStr);
+ }
+
+ /* FIXME: qemu keeps internal track of snapshots. We can get access
+ * to this info via the "info snapshots" monitor command for running
+ * domains, or via "qemu-img snapshot -l" for shutoff domains. It would
+ * be nice to update our internal state based on that, but there is a
+ * a problem. qemu doesn't track all of the same metadata that we do.
+ * In particular we wouldn't be able to fill in the <parent>, which is
+ * pretty important in our metadata.
+ */
+
+ virResetLastError();
+
+cleanup:
+ if (dir)
+ closedir(dir);
+ VIR_FREE(snapDir);
+ virDomainObjUnlock(vm);
+}
+
/**
* qemudStartup:
*
@@ -1399,6 +1486,9 @@ qemudStartup(int privileged) {
if (virAsprintf(&qemu_driver->cacheDir,
"%s/cache/libvirt/qemu", LOCAL_STATE_DIR) == -1)
goto out_of_memory;
+ if (virAsprintf(&qemu_driver->snapshotDir,
+ "%s/run/libvirt/qemu/snapshot", LOCAL_STATE_DIR) ==
-1)
+ goto out_of_memory;
} else {
uid_t uid = geteuid();
char *userdir = virGetUserDirectory(uid);
@@ -1423,6 +1513,8 @@ qemudStartup(int privileged) {
goto out_of_memory;
if (virAsprintf(&qemu_driver->cacheDir, "%s/qemu/cache", base)
== -1)
goto out_of_memory;
+ if (virAsprintf(&qemu_driver->snapshotDir, "%s/qemu/snapshot",
base) == -1)
+ goto out_of_memory;
}
if (virFileMakePath(qemu_driver->stateDir) != 0) {
@@ -1443,6 +1535,12 @@ qemudStartup(int privileged) {
qemu_driver->cacheDir, virStrerror(errno, ebuf, sizeof ebuf));
goto error;
}
+ if (virFileMakePath(qemu_driver->snapshotDir) != 0) {
+ char ebuf[1024];
+ VIR_ERROR(_("Failed to create save dir '%s': %s"),
+ qemu_driver->snapshotDir, virStrerror(errno, ebuf, sizeof ebuf));
+ goto error;
+ }
/* Configuration paths are either ~/.libvirt/qemu/... (session) or
* /etc/libvirt/qemu/... (system).
@@ -1493,6 +1591,12 @@ qemudStartup(int privileged) {
qemu_driver->cacheDir, qemu_driver->user,
qemu_driver->group);
goto error;
}
+ if (chown(qemu_driver->snapshotDir, qemu_driver->user,
qemu_driver->group) < 0) {
+ virReportSystemError(errno,
+ _("unable to set ownership of '%s' to
%d:%d"),
+ qemu_driver->snapshotDir, qemu_driver->user,
qemu_driver->group);
+ goto error;
+ }
}
/* If hugetlbfs is present, then we need to create a sub-directory within
@@ -1543,6 +1647,11 @@ qemudStartup(int privileged) {
qemu_driver->autostartDir,
0, NULL, NULL) < 0)
goto error;
+
+
+ virHashForEach(qemu_driver->domains.objs, qemuDomainSnapshotLoad,
+ qemu_driver->snapshotDir);
+
qemuDriverUnlock(qemu_driver);
qemudAutostartConfigs(qemu_driver);
@@ -1645,6 +1754,7 @@ qemudShutdown(void) {
VIR_FREE(qemu_driver->stateDir);
VIR_FREE(qemu_driver->libDir);
VIR_FREE(qemu_driver->cacheDir);
+ VIR_FREE(qemu_driver->snapshotDir);
VIR_FREE(qemu_driver->vncTLSx509certdir);
VIR_FREE(qemu_driver->vncListen);
VIR_FREE(qemu_driver->vncPassword);
@@ -3008,6 +3118,11 @@ qemuPrepareMonitorChr(struct qemud_driver *driver,
return 0;
}
+static int qemuDomainSnapshotSetActive(virDomainObjPtr vm,
+ char *snapshotDir);
+static int qemuDomainSnapshotSetInactive(virDomainObjPtr vm,
+ char *snapshotDir);
+
static int qemudStartVMDaemon(virConnectPtr conn,
struct qemud_driver *driver,
virDomainObjPtr vm,
@@ -3175,7 +3290,11 @@ static int qemudStartVMDaemon(virConnectPtr conn,
vm->def->id = driver->nextvmid++;
if (qemudBuildCommandLine(conn, driver, vm->def, priv->monConfig,
priv->monJSON, qemuCmdFlags, &argv, &progenv,
- &tapfds, &ntapfds, migrateFrom) < 0)
+ &tapfds, &ntapfds, migrateFrom,
+ vm->current_snapshot) < 0)
+ goto cleanup;
+
+ if (qemuDomainSnapshotSetInactive(vm, driver->snapshotDir) < 0)
goto cleanup;
/* now that we know it is about to start call the hook if present */
@@ -5911,7 +6030,7 @@ static char *qemuDomainXMLToNative(virConnectPtr conn,
&monConfig, 0, qemuCmdFlags,
&retargv, &retenv,
NULL, NULL, /* Don't want it to create TAP devices */
- NULL) < 0) {
+ NULL, NULL) < 0) {
goto cleanup;
}
@@ -10226,6 +10345,695 @@ cleanup:
return ret;
}
+static char *qemuFindQemuImgBinary(void)
+{
+ char *ret;
+
+ ret = virFindFileInPath("kvm-img");
+ if (ret == NULL)
+ ret = virFindFileInPath("qemu-img");
+ if (ret == NULL)
+ qemuReportError(VIR_ERR_INTERNAL_ERROR,
+ "%s", _("unable to find kvm-img or
qemu-img"));
+
+ return ret;
+}
+
+static int qemuDomainSnapshotWriteSnapshotMetadata(virDomainObjPtr vm,
+ char *snapshotDir)
+{
+ int fd = -1;
+ char *newxml = NULL;
+ int ret = -1;
+ char *snapDir = NULL;
+ char *snapFile = NULL;
+ int err;
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+
+ virUUIDFormat(vm->def->uuid, uuidstr);
+ newxml = virDomainSnapshotDefFormat(uuidstr, vm->current_snapshot->def, 1);
+ if (newxml == NULL) {
+ virReportOOMError();
+ return -1;
+ }
+
+ if (virAsprintf(&snapDir, "%s/%s", snapshotDir, vm->def->name)
< 0) {
+ virReportOOMError();
+ goto cleanup;
+ }
+ err = virFileMakePath(snapDir);
+ if (err < 0) {
+ virReportSystemError(err, _("cannot create snapshot directory
'%s'"),
+ snapDir);
+ goto cleanup;
+ }
+
+ if (virAsprintf(&snapFile, "%s/%s.xml", snapDir,
+ vm->current_snapshot->def->name) < 0) {
+ virReportOOMError();
+ goto cleanup;
+ }
+ fd = open(snapFile, O_CREAT|O_TRUNC|O_WRONLY, S_IRUSR|S_IWUSR);
+ if (fd < 0) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED,
+ _("failed to create snapshot file '%s'"),
snapFile);
+ goto cleanup;
+ }
+ if (safewrite(fd, newxml, strlen(newxml)) != strlen(newxml)) {
+ virReportSystemError(errno, _("Failed to write snapshot data to %s"),
+ snapFile);
+ goto cleanup;
+ }
+
+ ret = 0;
+
+cleanup:
+ VIR_FREE(snapFile);
+ VIR_FREE(snapDir);
+ VIR_FREE(newxml);
+ if (fd != -1)
+ close(fd);
+ return ret;
+}
+
+static int qemuDomainSnapshotSetActive(virDomainObjPtr vm,
+ char *snapshotDir)
+{
+ if (vm->current_snapshot) {
+ vm->current_snapshot->def->active = 1;
+
+ return qemuDomainSnapshotWriteSnapshotMetadata(vm, snapshotDir);
+ }
+
+ return 0;
+}
+
+static int qemuDomainSnapshotSetInactive(virDomainObjPtr vm,
+ char *snapshotDir)
+{
+ if (vm->current_snapshot) {
+ vm->current_snapshot->def->active = 0;
+
+ return qemuDomainSnapshotWriteSnapshotMetadata(vm, snapshotDir);
+ }
+
+ return 0;
+}
+
+
+static int qemuDomainSnapshotIsAllowed(virDomainObjPtr vm)
+{
+ int i;
+
+ /* FIXME: we need to figure out what else here might succeed; in
+ * particular, if it's a raw device but on LVM, we could probably make
+ * that succeed as well
+ */
+ for (i = 0; i < vm->def->ndisks; i++) {
+ if (vm->def->disks[i]->device == VIR_DOMAIN_DISK_DEVICE_DISK &&
+ (!vm->def->disks[i]->driverType ||
+ STRNEQ(vm->def->disks[i]->driverType, "qcow2"))) {
+ qemuReportError(VIR_ERR_OPERATION_INVALID,
+ _("Disk device '%s' does not support
snapshotting"),
+ vm->def->disks[i]->info.alias);
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+static virDomainSnapshotPtr qemuDomainSnapshotCreateXML(virDomainPtr domain,
+ const char *xmlDesc,
+ unsigned int flags
ATTRIBUTE_UNUSED)
+{
+ struct qemud_driver *driver = domain->conn->privateData;
+ virDomainObjPtr vm = NULL;
+ virDomainSnapshotObjPtr snap = NULL;
+ virDomainSnapshotPtr snapshot = NULL;
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+ virDomainSnapshotDefPtr def;
+ qemuDomainObjPrivatePtr priv;
+ const char *qemuimgarg[] = { NULL, "snapshot", "-c", NULL, NULL,
NULL };
+ int i;
+
+ qemuDriverLock(driver);
+ virUUIDFormat(domain->uuid, uuidstr);
+ vm = virDomainFindByUUID(&driver->domains, domain->uuid);
+ if (!vm) {
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ /* in a perfect world, we would allow qemu to tell us this. The problem
+ * is that qemu only does this check device-by-device; so if you had a
+ * domain that booted from a large qcow2 device, but had a secondary raw
+ * device attached, you wouldn't find out that you can't snapshot your
+ * guest until *after* it had spent the time to snapshot the boot device.
+ * This is probably a bug in qemu, but we'll work around it here for now.
+ */
+ if (!qemuDomainSnapshotIsAllowed(vm))
+ goto cleanup;
+
+ if (!(def = virDomainSnapshotDefParseString(xmlDesc, 1)))
+ goto cleanup;
+
+ if (!(snap = virDomainSnapshotAssignDef(&vm->snapshots, def)))
+ goto cleanup;
+
+ /* actually do the snapshot */
+ if (!virDomainObjIsActive(vm)) {
+ qemuimgarg[0] = qemuFindQemuImgBinary();
+ if (qemuimgarg[0] == NULL)
+ /* qemuFindQemuImgBinary set the error */
+ goto cleanup;
+
+ qemuimgarg[3] = snap->def->name;
+
+ for (i = 0; i < vm->def->ndisks; i++) {
+ /* FIXME: we also need to handle LVM here */
+ /* FIXME: if we fail halfway through this loop, we are in an
+ * inconsistent state. I'm not quite sure what to do about that
+ */
+ if (vm->def->disks[i]->device == VIR_DOMAIN_DISK_DEVICE_DISK) {
+ if (!vm->def->disks[i]->driverType ||
+ STRNEQ(vm->def->disks[i]->driverType, "qcow2")) {
+ qemuReportError(VIR_ERR_OPERATION_INVALID,
+ _("Disk device '%s' does not support
snapshotting"),
+ vm->def->disks[i]->info.alias);
+ goto cleanup;
+ }
+
+ qemuimgarg[4] = vm->def->disks[i]->src;
+
+ if (virRun(qemuimgarg, NULL) < 0) {
+ virReportSystemError(errno,
+ _("Failed to run '%s' to create
snapshot '%s' from disk '%s'"),
+ qemuimgarg[0], snap->def->name,
+ vm->def->disks[i]->src);
+ goto cleanup;
+ }
+ }
+ }
+ }
+ else {
+ priv = vm->privateData;
+ qemuDomainObjEnterMonitorWithDriver(driver, vm);
+ if (qemuMonitorCreateSnapshot(priv->mon, def->name) < 0) {
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ goto cleanup;
+ }
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+
+ }
+
+ snap->def->state = vm->state;
+
+ /* FIXME: if we fail after this point, there's not a whole lot we can
+ * do; we've successfully taken the snapshot, and we are now running
+ * on it, so we have to go forward the best we can
+ */
+
+ if (vm->current_snapshot) {
+ def->parent = strdup(vm->current_snapshot->def->name);
+ if (def->parent == NULL) {
+ virReportOOMError();
+ goto cleanup;
+ }
+ }
+
+ /* Now we set the new current_snapshot for the domain */
+ vm->current_snapshot = snap;
+
+ if (qemuDomainSnapshotWriteSnapshotMetadata(vm, driver->snapshotDir) < 0)
+ /* qemuDomainSnapshotWriteSnapshotMetadata set the error */
+ goto cleanup;
+
+ snapshot = virGetDomainSnapshot(domain, snap->def->name);
+
+cleanup:
+ VIR_FREE(qemuimgarg[0]);
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+ return snapshot;
+}
+
+static int qemuDomainSnapshotListNames(virDomainPtr domain, char **names,
+ int nameslen,
+ unsigned int flags ATTRIBUTE_UNUSED)
+{
+ struct qemud_driver *driver = domain->conn->privateData;
+ virDomainObjPtr vm = NULL;
+ int n = -1;
+
+ qemuDriverLock(driver);
+ vm = virDomainFindByUUID(&driver->domains, domain->uuid);
+ if (!vm) {
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+ virUUIDFormat(domain->uuid, uuidstr);
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ n = virDomainSnapshotObjListGetNames(&vm->snapshots, names, nameslen);
+
+cleanup:
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+ return n;
+}
+
+static int qemuDomainSnapshotNum(virDomainPtr domain,
+ unsigned int flags ATTRIBUTE_UNUSED)
+{
+ struct qemud_driver *driver = domain->conn->privateData;
+ virDomainObjPtr vm = NULL;
+ int n = -1;
+
+ qemuDriverLock(driver);
+ vm = virDomainFindByUUID(&driver->domains, domain->uuid);
+ if (!vm) {
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+ virUUIDFormat(domain->uuid, uuidstr);
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ n = virDomainSnapshotObjListNum(&vm->snapshots);
+
+cleanup:
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+ return n;
+}
+
+static virDomainSnapshotPtr qemuDomainSnapshotLookupByName(virDomainPtr domain,
+ const char *name,
+ unsigned int flags
ATTRIBUTE_UNUSED)
+{
+ struct qemud_driver *driver = domain->conn->privateData;
+ virDomainObjPtr vm;
+ virDomainSnapshotObjPtr snap = NULL;
+ virDomainSnapshotPtr snapshot = NULL;
+
+ qemuDriverLock(driver);
+ vm = virDomainFindByUUID(&driver->domains, domain->uuid);
+ if (!vm) {
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+ virUUIDFormat(domain->uuid, uuidstr);
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ snap = virDomainSnapshotFindByName(&vm->snapshots, name);
+ if (!snap) {
+ qemuReportError(VIR_ERR_NO_DOMAIN_SNAPSHOT,
+ _("no snapshot with matching name '%s'"),
name);
+ goto cleanup;
+ }
+
+ snapshot = virGetDomainSnapshot(domain, snap->def->name);
+
+cleanup:
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+ return snapshot;
+}
+
+static int qemuDomainHasCurrentSnapshot(virDomainPtr domain,
+ unsigned int flags ATTRIBUTE_UNUSED)
+{
+ struct qemud_driver *driver = domain->conn->privateData;
+ virDomainObjPtr vm;
+ int ret = -1;
+
+ qemuDriverLock(driver);
+ vm = virDomainFindByUUID(&driver->domains, domain->uuid);
+ if (!vm) {
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+ virUUIDFormat(domain->uuid, uuidstr);
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ ret = (vm->current_snapshot != NULL);
+
+cleanup:
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+ return ret;
+}
+
+static virDomainSnapshotPtr qemuDomainSnapshotCurrent(virDomainPtr domain,
+ unsigned int flags
ATTRIBUTE_UNUSED)
+{
+ struct qemud_driver *driver = domain->conn->privateData;
+ virDomainObjPtr vm;
+ virDomainSnapshotPtr snapshot = NULL;
+
+ qemuDriverLock(driver);
+ vm = virDomainFindByUUID(&driver->domains, domain->uuid);
+ if (!vm) {
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+ virUUIDFormat(domain->uuid, uuidstr);
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ if (!vm->current_snapshot) {
+ qemuReportError(VIR_ERR_NO_DOMAIN_SNAPSHOT, "%s",
+ _("the domain does not have a current snapshot"));
+ goto cleanup;
+ }
+
+ snapshot = virGetDomainSnapshot(domain, vm->current_snapshot->def->name);
+
+cleanup:
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+ return snapshot;
+}
+
+static char *qemuDomainSnapshotDumpXML(virDomainSnapshotPtr snapshot,
+ unsigned int flags ATTRIBUTE_UNUSED)
+{
+ struct qemud_driver *driver = snapshot->domain->conn->privateData;
+ virDomainObjPtr vm = NULL;
+ char *xml = NULL;
+ virDomainSnapshotObjPtr snap = NULL;
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+
+ qemuDriverLock(driver);
+ virUUIDFormat(snapshot->domain->uuid, uuidstr);
+ vm = virDomainFindByUUID(&driver->domains, snapshot->domain->uuid);
+ if (!vm) {
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ snap = virDomainSnapshotFindByName(&vm->snapshots, snapshot->name);
+ if (!snap) {
+ qemuReportError(VIR_ERR_NO_DOMAIN_SNAPSHOT,
+ _("no domain snapshot with matching name
'%s'"),
+ snapshot->name);
+ goto cleanup;
+ }
+
+ xml = virDomainSnapshotDefFormat(uuidstr, snap->def, 0);
+
+cleanup:
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+ return xml;
+}
+
+static int qemuDomainRevertToSnapshot(virDomainSnapshotPtr snapshot,
+ unsigned int flags ATTRIBUTE_UNUSED)
+{
+ struct qemud_driver *driver = snapshot->domain->conn->privateData;
+ virDomainObjPtr vm = NULL;
+ int ret = -1;
+ virDomainSnapshotObjPtr snap = NULL;
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+ virDomainEventPtr event = NULL;
+ qemuDomainObjPrivatePtr priv;
+ int rc;
+
+ qemuDriverLock(driver);
+ virUUIDFormat(snapshot->domain->uuid, uuidstr);
+ vm = virDomainFindByUUID(&driver->domains, snapshot->domain->uuid);
+ if (!vm) {
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ snap = virDomainSnapshotFindByName(&vm->snapshots, snapshot->name);
+ if (!snap) {
+ qemuReportError(VIR_ERR_NO_DOMAIN_SNAPSHOT,
+ _("no domain snapshot with matching name
'%s'"),
+ snapshot->name);
+ goto cleanup;
+ }
+
+ vm->current_snapshot = snap;
+
+ if (qemuDomainObjBeginJobWithDriver(driver, vm) < 0)
+ goto cleanup;
+
+ if (snap->def->state == VIR_DOMAIN_RUNNING
+ || snap->def->state == VIR_DOMAIN_PAUSED) {
+
+ if (virDomainObjIsActive(vm)) {
+ priv = vm->privateData;
+ qemuDomainObjEnterMonitorWithDriver(driver, vm);
+ rc = qemuMonitorLoadSnapshot(priv->mon, snap->def->name);
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ if (rc < 0)
+ goto cleanup;
+ }
+ else {
+ if (qemuDomainSnapshotSetActive(vm, driver->snapshotDir) < 0)
+ goto cleanup;
+
+ rc = qemudStartVMDaemon(snapshot->domain->conn, driver, vm, NULL,
+ -1);
+ if (qemuDomainSnapshotSetInactive(vm, driver->snapshotDir) < 0)
+ goto cleanup;
+ if (rc < 0)
+ goto cleanup;
+ }
+
+ if (snap->def->state == VIR_DOMAIN_PAUSED) {
+ /* qemu unconditionally starts the domain running again after
+ * loadvm, so let's pause it to keep consistency
+ */
+ priv = vm->privateData;
+ qemuDomainObjEnterMonitorWithDriver(driver, vm);
+ rc = qemuMonitorStopCPUs(priv->mon);
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ if (rc < 0)
+ goto cleanup;
+ }
+
+ event = virDomainEventNewFromObj(vm,
+ VIR_DOMAIN_EVENT_STARTED,
+ VIR_DOMAIN_EVENT_STARTED_FROM_SNAPSHOT);
+ }
+ else {
+ /* qemu is a little funny with running guests and the restoration
+ * of snapshots. If the snapshot was taken online,
+ * then after a "loadvm" monitor command, the VM is set running
+ * again. If the snapshot was taken offline, then after a "loadvm"
+ * monitor command the VM is left paused. Unpausing it leads to
+ * the memory state *before* the loadvm with the disk *after* the
+ * loadvm, which obviously is bound to corrupt something.
+ * Therefore we destroy the domain and set it to "off" in this case.
+ */
+
+ if (virDomainObjIsActive(vm)) {
+ qemudShutdownVMDaemon(driver, vm);
+ event = virDomainEventNewFromObj(vm,
+ VIR_DOMAIN_EVENT_STOPPED,
+ VIR_DOMAIN_EVENT_STOPPED_FROM_SNAPSHOT);
+ }
+
+ if (qemuDomainSnapshotSetActive(vm, driver->snapshotDir) < 0)
+ goto cleanup;
+ }
+
+ vm->state = snap->def->state;
+
+ ret = 0;
+
+cleanup:
+ if (vm && qemuDomainObjEndJob(vm) == 0)
+ vm = NULL;
+
+ if (event)
+ qemuDomainEventQueue(driver, event);
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+
+ return ret;
+}
+
+static int qemuDomainSnapshotDiscard(struct qemud_driver *driver,
+ virDomainObjPtr vm,
+ virDomainSnapshotObjPtr snap)
+{
+ const char *qemuimgarg[] = { NULL, "snapshot", "-d", NULL, NULL,
NULL };
+ char *snapFile = NULL;
+ int ret = -1;
+ int i;
+ qemuDomainObjPrivatePtr priv;
+ virDomainSnapshotObjPtr parentsnap;
+
+ if (!virDomainObjIsActive(vm)) {
+ qemuimgarg[0] = qemuFindQemuImgBinary();
+ if (qemuimgarg[0] == NULL)
+ /* qemuFindQemuImgBinary set the error */
+ goto cleanup;
+
+ qemuimgarg[3] = snap->def->name;
+
+ for (i = 0; i < vm->def->ndisks; i++) {
+ /* FIXME: we also need to handle LVM here */
+ if (vm->def->disks[i]->device == VIR_DOMAIN_DISK_DEVICE_DISK) {
+ if (!vm->def->disks[i]->driverType ||
+ STRNEQ(vm->def->disks[i]->driverType, "qcow2")) {
+ /* we continue on even in the face of error, since other
+ * disks in this VM may have this snapshot in place
+ */
+ continue;
+ }
+
+ qemuimgarg[4] = vm->def->disks[i]->src;
+
+ if (virRun(qemuimgarg, NULL) < 0) {
+ /* we continue on even in the face of error, since other
+ * disks in this VM may have this snapshot in place
+ */
+ continue;
+ }
+ }
+ }
+ }
+ else {
+ priv = vm->privateData;
+ qemuDomainObjEnterMonitorWithDriver(driver, vm);
+ /* we continue on even in the face of error */
+ qemuMonitorDeleteSnapshot(priv->mon, snap->def->name);
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ }
+
+ if (snap == vm->current_snapshot) {
+ if (snap->def->parent) {
+ parentsnap = virDomainSnapshotFindByName(&vm->snapshots,
+ snap->def->parent);
+ if (!parentsnap) {
+ qemuReportError(VIR_ERR_NO_DOMAIN_SNAPSHOT,
+ _("no domain snapshot parent with matching name
'%s'"),
+ snap->def->parent);
+ goto cleanup;
+ }
+
+ /* Now we set the new current_snapshot for the domain */
+ vm->current_snapshot = parentsnap;
+ }
+ else
+ vm->current_snapshot = NULL;
+ }
+
+ if (virAsprintf(&snapFile, "%s/%s/%s.xml", driver->snapshotDir,
+ vm->def->name, snap->def->name) < 0) {
+ virReportOOMError();
+ goto cleanup;
+ }
+ unlink(snapFile);
+
+ virDomainSnapshotObjListRemove(&vm->snapshots, snap);
+
+ ret = 0;
+
+cleanup:
+ VIR_FREE(snapFile);
+ VIR_FREE(qemuimgarg[0]);
+
+ return ret;
+}
+
+struct snap_remove {
+ struct qemud_driver *driver;
+ virDomainObjPtr vm;
+ char *parent;
+ int err;
+};
+
+static void qemuDomainSnapshotDiscardChildren(void *payload,
+ const char *name ATTRIBUTE_UNUSED,
+ void *data)
+{
+ virDomainSnapshotObjPtr snap = payload;
+ struct snap_remove *curr = data;
+ struct snap_remove this;
+
+ if (snap->def->parent && STREQ(snap->def->parent,
curr->parent)) {
+ this.driver = curr->driver;
+ this.vm = curr->vm;
+ this.parent = snap->def->name;
+ this.err = 0;
+ virHashForEach(curr->vm->snapshots.objs,
+ qemuDomainSnapshotDiscardChildren, &this);
+
+ if (this.err)
+ curr->err = this.err;
+ else
+ this.err = qemuDomainSnapshotDiscard(curr->driver, curr->vm, snap);
+ }
+}
+
+static int qemuDomainSnapshotDelete(virDomainSnapshotPtr snapshot,
+ unsigned int flags)
+{
+ struct qemud_driver *driver = snapshot->domain->conn->privateData;
+ virDomainObjPtr vm = NULL;
+ int ret = -1;
+ virDomainSnapshotObjPtr snap = NULL;
+ char uuidstr[VIR_UUID_STRING_BUFLEN];
+ struct snap_remove rem;
+
+ qemuDriverLock(driver);
+ virUUIDFormat(snapshot->domain->uuid, uuidstr);
+ vm = virDomainFindByUUID(&driver->domains, snapshot->domain->uuid);
+ if (!vm) {
+ qemuReportError(VIR_ERR_NO_DOMAIN,
+ _("no domain with matching uuid '%s'"),
uuidstr);
+ goto cleanup;
+ }
+
+ snap = virDomainSnapshotFindByName(&vm->snapshots, snapshot->name);
+ if (!snap) {
+ qemuReportError(VIR_ERR_NO_DOMAIN_SNAPSHOT,
+ _("no domain snapshot with matching name
'%s'"),
+ snapshot->name);
+ goto cleanup;
+ }
+
+ if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN) {
+ rem.driver = driver;
+ rem.vm = vm;
+ rem.parent = snap->def->name;
+ rem.err = 0;
+ virHashForEach(vm->snapshots.objs, qemuDomainSnapshotDiscardChildren,
+ &rem);
+ if (rem.err < 0)
+ goto cleanup;
+ }
+
+ ret = qemuDomainSnapshotDiscard(driver, vm, snap);
+
+cleanup:
+ if (vm)
+ virDomainObjUnlock(vm);
+ qemuDriverUnlock(driver);
+ return ret;
+}
static virDriver qemuDriver = {
VIR_DRV_QEMU,
@@ -10312,15 +11120,15 @@ static virDriver qemuDriver = {
qemuDomainMigrateSetMaxDowntime, /* domainMigrateSetMaxDowntime */
qemuDomainEventRegisterAny, /* domainEventRegisterAny */
qemuDomainEventDeregisterAny, /* domainEventDeregisterAny */
- NULL, /* domainSnapshotCreateXML */
- NULL, /* domainSnapshotDumpXML */
- NULL, /* domainSnapshotNum */
- NULL, /* domainSnapshotListNames */
- NULL, /* domainSnapshotLookupByName */
- NULL, /* domainHasCurrentSnapshot */
- NULL, /* domainSnapshotCurrent */
- NULL, /* domainRevertToSnapshot */
- NULL, /* domainSnapshotDelete */
+ qemuDomainSnapshotCreateXML, /* domainSnapshotCreateXML */
+ qemuDomainSnapshotDumpXML, /* domainSnapshotDumpXML */
+ qemuDomainSnapshotNum, /* domainSnapshotNum */
+ qemuDomainSnapshotListNames, /* domainSnapshotListNames */
+ qemuDomainSnapshotLookupByName, /* domainSnapshotLookupByName */
+ qemuDomainHasCurrentSnapshot, /* domainHasCurrentSnapshot */
+ qemuDomainSnapshotCurrent, /* domainSnapshotCurrent */
+ qemuDomainRevertToSnapshot, /* domainRevertToSnapshot */
+ qemuDomainSnapshotDelete, /* domainSnapshotDelete */
};
diff --git a/src/qemu/qemu_monitor.c b/src/qemu/qemu_monitor.c
index 64779ac..01e3a46 100644
--- a/src/qemu/qemu_monitor.c
+++ b/src/qemu/qemu_monitor.c
@@ -1491,3 +1491,42 @@ int qemuMonitorSetDrivePassphrase(qemuMonitorPtr mon,
ret = qemuMonitorTextSetDrivePassphrase(mon, alias, passphrase);
return ret;
}
+
+int qemuMonitorCreateSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ int ret;
+
+ DEBUG("mon=%p, name=%s",mon,name);
+
+ if (mon->json)
+ ret = qemuMonitorJSONCreateSnapshot(mon, name);
+ else
+ ret = qemuMonitorTextCreateSnapshot(mon, name);
+ return ret;
+}
+
+int qemuMonitorLoadSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ int ret;
+
+ DEBUG("mon=%p, name=%s",mon,name);
+
+ if (mon->json)
+ ret = qemuMonitorJSONLoadSnapshot(mon, name);
+ else
+ ret = qemuMonitorTextLoadSnapshot(mon, name);
+ return ret;
+}
+
+int qemuMonitorDeleteSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ int ret;
+
+ DEBUG("mon=%p, name=%s",mon,name);
+
+ if (mon->json)
+ ret = qemuMonitorJSONDeleteSnapshot(mon, name);
+ else
+ ret = qemuMonitorTextDeleteSnapshot(mon, name);
+ return ret;
+}
diff --git a/src/qemu/qemu_monitor.h b/src/qemu/qemu_monitor.h
index 07773bd..21b8989 100644
--- a/src/qemu/qemu_monitor.h
+++ b/src/qemu/qemu_monitor.h
@@ -344,4 +344,8 @@ int qemuMonitorSetDrivePassphrase(qemuMonitorPtr mon,
const char *alias,
const char *passphrase);
+int qemuMonitorCreateSnapshot(qemuMonitorPtr mon, const char *name);
+int qemuMonitorLoadSnapshot(qemuMonitorPtr mon, const char *name);
+int qemuMonitorDeleteSnapshot(qemuMonitorPtr mon, const char *name);
+
#endif /* QEMU_MONITOR_H */
diff --git a/src/qemu/qemu_monitor_json.c b/src/qemu/qemu_monitor_json.c
index eac3aca..1b0ecdf 100644
--- a/src/qemu/qemu_monitor_json.c
+++ b/src/qemu/qemu_monitor_json.c
@@ -2156,3 +2156,69 @@ int qemuMonitorJSONSetDrivePassphrase(qemuMonitorPtr mon,
virJSONValueFree(reply);
return ret;
}
+
+int qemuMonitorJSONCreateSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ int ret;
+ virJSONValuePtr cmd;
+ virJSONValuePtr reply = NULL;
+
+ cmd = qemuMonitorJSONMakeCommand("savevm",
+ "s:name", name,
+ NULL);
+ if (!cmd)
+ return -1;
+
+ ret = qemuMonitorJSONCommand(mon, cmd, &reply);
+
+ if (ret == 0)
+ ret = qemuMonitorJSONCheckError(cmd, reply);
+
+ virJSONValueFree(cmd);
+ virJSONValueFree(reply);
+ return ret;
+}
+
+int qemuMonitorJSONLoadSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ int ret;
+ virJSONValuePtr cmd;
+ virJSONValuePtr reply = NULL;
+
+ cmd = qemuMonitorJSONMakeCommand("loadvm",
+ "s:name", name,
+ NULL);
+ if (!cmd)
+ return -1;
+
+ ret = qemuMonitorJSONCommand(mon, cmd, &reply);
+
+ if (ret == 0)
+ ret = qemuMonitorJSONCheckError(cmd, reply);
+
+ virJSONValueFree(cmd);
+ virJSONValueFree(reply);
+ return ret;
+}
+
+int qemuMonitorJSONDeleteSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ int ret;
+ virJSONValuePtr cmd;
+ virJSONValuePtr reply = NULL;
+
+ cmd = qemuMonitorJSONMakeCommand("delvm",
+ "s:name", name,
+ NULL);
+ if (!cmd)
+ return -1;
+
+ ret = qemuMonitorJSONCommand(mon, cmd, &reply);
+
+ if (ret == 0)
+ ret = qemuMonitorJSONCheckError(cmd, reply);
+
+ virJSONValueFree(cmd);
+ virJSONValueFree(reply);
+ return ret;
+}
diff --git a/src/qemu/qemu_monitor_json.h b/src/qemu/qemu_monitor_json.h
index fc05153..e7baf84 100644
--- a/src/qemu/qemu_monitor_json.h
+++ b/src/qemu/qemu_monitor_json.h
@@ -175,4 +175,8 @@ int qemuMonitorJSONSetDrivePassphrase(qemuMonitorPtr mon,
const char *alias,
const char *passphrase);
+int qemuMonitorJSONCreateSnapshot(qemuMonitorPtr mon, const char *name);
+int qemuMonitorJSONLoadSnapshot(qemuMonitorPtr mon, const char *name);
+int qemuMonitorJSONDeleteSnapshot(qemuMonitorPtr mon, const char *name);
+
#endif /* QEMU_MONITOR_JSON_H */
diff --git a/src/qemu/qemu_monitor_text.c b/src/qemu/qemu_monitor_text.c
index a199de7..e057bbe 100644
--- a/src/qemu/qemu_monitor_text.c
+++ b/src/qemu/qemu_monitor_text.c
@@ -2290,3 +2290,144 @@ cleanup:
VIR_FREE(safe_str);
return ret;
}
+
+int qemuMonitorTextCreateSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ char *cmd;
+ char *reply = NULL;
+ int ret = -1;
+
+ if (virAsprintf(&cmd, "savevm \"%s\"", name) < 0) {
+ virReportOOMError();
+ return -1;
+ }
+
+ if (qemuMonitorCommand(mon, cmd, &reply)) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED,
+ _("failed to take snapshot using command
'%s'"), cmd);
+ goto cleanup;
+ }
+
+ if (strstr(reply, "Error while creating snapshot") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED,
+ _("Failed to take snapshot: %s"), reply);
+ goto cleanup;
+ }
+ else if (strstr(reply, "No block device can accept snapshots") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_INVALID, "%s",
+ _("this domain does not have a device to take
snapshots"));
+ goto cleanup;
+ }
+ else if (strstr(reply, "Could not open VM state file") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED, "%s", reply);
+ goto cleanup;
+ }
+ else if (strstr(reply, "Error") != NULL
+ && strstr(reply, "while writing VM") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED, "%s", reply);
+ goto cleanup;
+ }
+
+ ret = 0;
+
+cleanup:
+ VIR_FREE(cmd);
+ VIR_FREE(reply);
+ return ret;
+}
+
+int qemuMonitorTextLoadSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ char *cmd;
+ char *reply = NULL;
+ int ret = -1;
+
+ if (virAsprintf(&cmd, "loadvm \"%s\"", name) < 0) {
+ virReportOOMError();
+ return -1;
+ }
+
+ if (qemuMonitorCommand(mon, cmd, &reply)) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED,
+ _("failed to restore snapshot using command
'%s'"),
+ cmd);
+ goto cleanup;
+ }
+
+ if (strstr(reply, "No block device supports snapshots") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_INVALID, "%s",
+ _("this domain does not have a device to load
snapshots"));
+ goto cleanup;
+ }
+ else if (strstr(reply, "Could not find snapshot") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_INVALID,
+ _("the snapshot '%s' does not exist, and was not
loaded"),
+ name);
+ goto cleanup;
+ }
+ else if (strstr(reply, "Snapshots not supported on device") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_INVALID, "%s", reply);
+ goto cleanup;
+ }
+ else if (strstr(reply, "Could not open VM state file") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED, "%s", reply);
+ goto cleanup;
+ }
+ else if (strstr(reply, "Error") != NULL
+ && strstr(reply, "while loading VM state") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED, "%s", reply);
+ goto cleanup;
+ }
+ else if (strstr(reply, "Error") != NULL
+ && strstr(reply, "while activating snapshot on") != NULL)
{
+ qemuReportError(VIR_ERR_OPERATION_FAILED, "%s", reply);
+ goto cleanup;
+ }
+
+ ret = 0;
+
+cleanup:
+ VIR_FREE(cmd);
+ VIR_FREE(reply);
+ return ret;
+}
+
+int qemuMonitorTextDeleteSnapshot(qemuMonitorPtr mon, const char *name)
+{
+ char *cmd;
+ char *reply = NULL;
+ int ret = -1;
+
+ if (virAsprintf(&cmd, "delvm \"%s\"", name) < 0) {
+ virReportOOMError();
+ return -1;
+ }
+ if (qemuMonitorCommand(mon, cmd, &reply)) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED,
+ _("failed to delete snapshot using command
'%s'"),
+ cmd);
+ goto cleanup;
+ }
+
+ if (strstr(reply, "No block device supports snapshots") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_INVALID, "%s",
+ _("this domain does not have a device to delete
snapshots"));
+ goto cleanup;
+ }
+ else if (strstr(reply, "Snapshots not supported on device") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_INVALID, "%s", reply);
+ goto cleanup;
+ }
+ else if (strstr(reply, "Error") != NULL
+ && strstr(reply, "while deleting snapshot") != NULL) {
+ qemuReportError(VIR_ERR_OPERATION_FAILED, "%s", reply);
+ goto cleanup;
+ }
+
+ ret = 0;
+
+cleanup:
+ VIR_FREE(cmd);
+ VIR_FREE(reply);
+ return ret;
+}
diff --git a/src/qemu/qemu_monitor_text.h b/src/qemu/qemu_monitor_text.h
index 4e1939c..fb7d08b 100644
--- a/src/qemu/qemu_monitor_text.h
+++ b/src/qemu/qemu_monitor_text.h
@@ -177,4 +177,8 @@ int qemuMonitorTextSetDrivePassphrase(qemuMonitorPtr mon,
const char *alias,
const char *passphrase);
+int qemuMonitorTextCreateSnapshot(qemuMonitorPtr mon, const char *name);
+int qemuMonitorTextLoadSnapshot(qemuMonitorPtr mon, const char *name);
+int qemuMonitorTextDeleteSnapshot(qemuMonitorPtr mon, const char *name);
+
#endif /* QEMU_MONITOR_TEXT_H */
diff --git a/tests/qemuxml2argvtest.c b/tests/qemuxml2argvtest.c
index c98de19..9e4d5bf 100644
--- a/tests/qemuxml2argvtest.c
+++ b/tests/qemuxml2argvtest.c
@@ -83,7 +83,7 @@ static int testCompareXMLToArgvFiles(const char *xml,
if (qemudBuildCommandLine(conn, &driver,
vmdef, &monitor_chr, 0, flags,
&argv, &qenv,
- NULL, NULL, migrateFrom) < 0)
+ NULL, NULL, migrateFrom, NULL) < 0)
goto fail;
len = 1; /* for trailing newline */
--
1.6.6.1