
On Wed, Apr 17, 2019 at 09:09:19 -0500, Eric Blake wrote:
Time to actually issue the QMP transactions that start and stop backup commands (for now, just pull mode, not push). Starting a job has to kick off several pre-req steps, then a transaction, and additionally spawn an NBD server for pull mode; ending a job as well as failing partway through beginning a job has to unwind the earlier steps. Implementing push mode, as well as incremental pull and checkpoint creation, is deferred to later patches.
Signed-off-by: Eric Blake <eblake@redhat.com> --- src/qemu/qemu_domain.c | 18 ++- src/qemu/qemu_driver.c | 310 ++++++++++++++++++++++++++++++++++++++-- src/qemu/qemu_process.c | 9 ++ 3 files changed, 323 insertions(+), 14 deletions(-)
diff --git a/src/qemu/qemu_domain.c b/src/qemu/qemu_domain.c index f4be948bae..db00c473d0 100644 --- a/src/qemu/qemu_domain.c +++ b/src/qemu/qemu_domain.c @@ -2684,17 +2684,25 @@ qemuDomainObjPrivateXMLParseBlockjobs(virQEMUDriverPtr driver, { xmlNodePtr node; int tmp; + size_t i; VIR_AUTOFREE(char *) active = NULL;
if ((active = virXPathString("string(./blockjobs/@active)", ctxt)) && (tmp = virTristateBoolTypeFromString(active)) > 0) priv->reconnectBlockjobs = tmp;
- if ((node = virXPathNode("./domainbackup", ctxt)) && - !(priv->backup = virDomainBackupDefParseNode(ctxt->doc, node, - driver->xmlopt, - VIR_DOMAIN_BACKUP_PARSE_INTERNAL))) - return -1; + if ((node = virXPathNode("./domainbackup", ctxt))) { + if (!(priv->backup = virDomainBackupDefParseNode(ctxt->doc, node, + driver->xmlopt, + VIR_DOMAIN_BACKUP_PARSE_INTERNAL))) + return -1; + /* The backup job is only stored in XML if backupBegin + * succeeded at exporting the disk, so no need to store disk + * state when we can just force-reset it to a known-good + * value. */
So why isn't it stored in a known-good state in the first place?
+ for (i = 0; i < priv->backup->ndisks; i++) + priv->backup->disks[i].state = VIR_DOMAIN_BACKUP_DISK_STATE_EXPORT; + }
return 0; } diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index cca0b550b8..044b26196e 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -17693,8 +17693,80 @@ qemuDomainCheckpointDelete(virDomainCheckpointPtr checkpoint, return ret; }
-static int qemuDomainBackupBegin(virDomainPtr domain, const char *diskXml, - const char *checkpointXml, unsigned int flags) +static int +qemuDomainBackupPrepare(virQEMUDriverPtr driver, virDomainObjPtr vm, + virDomainBackupDefPtr def) +{ + int ret = -1; + size_t i; + + if (qemuBlockNodeNamesDetect(driver, vm, QEMU_ASYNC_JOB_NONE) < 0) + goto cleanup;
See previous comments regarding this function call.
+ for (i = 0; i < def->ndisks; i++) { + virDomainBackupDiskDef *disk = &def->disks[i]; + virStorageSourcePtr src = vm->def->disks[disk->idx]->src; + + if (!disk->store) + continue; + if (virAsprintf(&disk->store->nodeformat, "tmp-%s", disk->name) < 0)
Use a somewhat more descriptive temporary prefix. Also if blockdev-del fails after a job you won't be ever able to start a new job until the VM is restarted as the node name will always be the same.
+ goto cleanup; + if (!disk->store->format) + disk->store->format = VIR_STORAGE_FILE_QCOW2; + if (def->incremental) { + if (src->format != VIR_STORAGE_FILE_QCOW2) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, + _("incremental backup of %s requires qcow2"), + disk->name); + goto cleanup; + } + } + } + ret = 0; + cleanup: + return ret; +} + +/* Called while monitor lock is held. Best-effort cleanup. */ +static int +qemuDomainBackupDiskCleanup(virQEMUDriverPtr driver, virDomainObjPtr vm, + virDomainBackupDiskDef *disk, bool incremental) +{ + qemuDomainObjPrivatePtr priv = vm->privateData; + const char *node = vm->def->disks[disk->idx]->src->nodeformat; + int ret = 0; + + if (!disk->store) + return 0; + if (disk->state >= VIR_DOMAIN_BACKUP_DISK_STATE_EXPORT) { + /* No real need to use nbd-server-remove, since we will + * shortly be calling nbd-server-stop. */ + } + if (incremental && disk->state >= VIR_DOMAIN_BACKUP_DISK_STATE_BITMAP && + qemuMonitorDeleteBitmap(priv->mon, node, disk->store->nodeformat) < 0) { + VIR_WARN("Unable to remove temp bitmap for disk %s after backup", + disk->name); + ret = -1; + } + if (disk->state >= VIR_DOMAIN_BACKUP_DISK_STATE_READY && + qemuMonitorBlockdevDel(priv->mon, disk->store->nodeformat) < 0) {
You also need to blockdev-del the storage layer here.
+ VIR_WARN("Unable to remove temp disk %s after backup", + disk->name); + ret = -1; + } + if (disk->state >= VIR_DOMAIN_BACKUP_DISK_STATE_LABEL) + qemuDomainDiskChainElementRevoke(driver, vm, disk->store); + if (disk->state >= VIR_DOMAIN_BACKUP_DISK_STATE_CREATED && + disk->store->detected && unlink(disk->store->path) < 0) { + VIR_WARN("Unable to unlink temp disk %s after backup", + disk->store->path); + ret = -1; + } + return ret; +} + +static int +qemuDomainBackupBegin(virDomainPtr domain, const char *diskXml, + const char *checkpointXml, unsigned int flags) { virQEMUDriverPtr driver = domain->conn->privateData; virDomainObjPtr vm = NULL;
[...]
@@ -17747,25 +17826,145 @@ static int qemuDomainBackupBegin(virDomainPtr domain, const char *diskXml, if (!(def = virDomainBackupDefParseString(diskXml, driver->xmlopt, 0))) goto cleanup;
+ if (def->type == VIR_DOMAIN_BACKUP_TYPE_PULL) { + if (!virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_NBD_BITMAP)) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s", + _("qemu binary lacks pull-mode backup support")); + goto cleanup; + } + if (!def->server) { + if (VIR_ALLOC(def->server) < 0) + goto cleanup; + def->server->transport = VIR_STORAGE_NET_HOST_TRANS_TCP; + if (VIR_STRDUP(def->server->name, "localhost") < 0) + goto cleanup; + } + switch ((virStorageNetHostTransport)def->server->transport) { + case VIR_STORAGE_NET_HOST_TRANS_TCP: + /* TODO: Update qemu.conf to provide a port range, + * probably starting at 10809, for obtaining automatic + * port via virPortAllocatorAcquire, as well as store + * somewhere if we need to call virPortAllocatorRelease + * during BackupEnd. Until then, user must provide port */ + if (!def->server->port) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("<domainbackup> must specify TCP port for now")); + goto cleanup; + } + break; + case VIR_STORAGE_NET_HOST_TRANS_UNIX: + /* TODO: Do we need to mess with selinux? */ + break; + case VIR_STORAGE_NET_HOST_TRANS_RDMA: + case VIR_STORAGE_NET_HOST_TRANS_LAST: + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("unexpected transport in <domainbackup>")); + goto cleanup; + } + } else { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("push mode backups not supported yet")); + goto cleanup; + } + if (def->incremental) { + if (!virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_BITMAP_MERGE)) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s", + _("qemu binary lacks persistent bitmaps support")); + goto cleanup; + } + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("cannot create incremental backups yet")); + goto cleanup; + } + + if (!(qemuImgPath = qemuFindQemuImgBinary(driver))) + goto cleanup; + /* We are going to modify the domain below. */ if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_MODIFY) < 0) goto cleanup;
- priv = vm->privateData; if (priv->backup) { virReportError(VIR_ERR_OPERATION_INVALID, "%s", _("another backup job is already running")); goto endjob; }
- if (virDomainBackupAlignDisks(def, vm->def, suffix) < 0) + if (virDomainBackupAlignDisks(def, vm->def, suffix) < 0 || + qemuDomainBackupPrepare(driver, vm, def) < 0) goto endjob;
/* actually start the checkpoint. 2x2 array of push/pull, full/incr, plus additional tweak if checkpoint requested */ - /* TODO: issue QMP commands: - - pull: nbd-server-start with <server> from user (or autogenerate server) - - push/pull: blockdev-add per <disk> + qemuDomainObjEnterMonitor(driver, vm); + /* - push/pull: blockdev-add per <disk> */ + for (i = 0; i < def->ndisks; i++) { + virDomainBackupDiskDef *disk = &def->disks[i]; + virJSONValuePtr file; + virStorageSourcePtr src = vm->def->disks[disk->idx]->src; + const char *node = src->nodeformat; + + if (!disk->store) + continue; + if (qemuDomainStorageFileInit(driver, vm, disk->store, src) < 0)
virStorageFileDeinit is never called in this function.
+ goto endmon; + if (disk->store->detected) { + if (virStorageFileCreate(disk->store) < 0) { + virReportSystemError(errno, + _("failed to create image file '%s'"), + NULLSTR(disk->store->path)); + goto endmon; + } + disk->state = VIR_DOMAIN_BACKUP_DISK_STATE_CREATED; + } + if (qemuDomainDiskChainElementPrepare(driver, vm, disk->store, false, + true) < 0) + goto endmon; + disk->state = VIR_DOMAIN_BACKUP_DISK_STATE_LABEL; + if (disk->store->detected) { + virBuffer buf = VIR_BUFFER_INITIALIZER; + + /* Force initialization of scratch/target file to new qcow2 */ + if (!(cmd = virCommandNewArgList(qemuImgPath, + "create", + "-f", + virStorageFileFormatTypeToString(disk->store->format), + "-o", + NULL))) + goto endmon; + virBufferAsprintf(&buf, "backing_fmt=%s,backing_file=", + virStorageFileFormatTypeToString(src->format)); + virQEMUBuildBufferEscapeComma(&buf, src->path); + virCommandAddArgBuffer(cmd, &buf); + + virQEMUBuildBufferEscapeComma(&buf, disk->store->path); + virCommandAddArgBuffer(cmd, &buf); + if (virCommandRun(cmd, NULL) < 0) + goto endmon; + virCommandFree(cmd); + cmd = NULL; + } + + if (virJSONValueObjectCreate(&file, + "s:driver", "file", + "s:filename", disk->store->path, + NULL) < 0) + goto endmon; + if (virJSONValueObjectCreate(&json, + "s:driver", virStorageFileFormatTypeToString(disk->store->format),
We have helpers to create these froma virStorageSource. How about reusing those?
+ "s:node-name", disk->store->nodeformat, + "a:file", &file,
The rest of the blockdev integration code which is already present always plugs in the storage and format layer separately. This should do the same.
+ "s:backing", node, NULL) < 0) { + virJSONValueFree(file); + goto endmon; + } + if (qemuMonitorBlockdevAdd(priv->mon, json) < 0) + goto endmon; + json = NULL; + disk->state = VIR_DOMAIN_BACKUP_DISK_STATE_READY; + } + + /* TODO: - incr: bitmap-add of tmp, bitmap-merge per <disk> - transaction, containing: - push+full: blockdev-backup sync:full @@ -17773,8 +17972,76 @@ static int qemuDomainBackupBegin(virDomainPtr domain, const char *diskXml, - pull+full: blockdev-backup sync:none - pull+incr: blockdev-backup sync:none, bitmap-disable of tmp - if checkpoint: bitmap-disable of old, bitmap-add of new + */ + if (!(json = virJSONValueNewArray())) + goto endmon; + for (i = 0; i < def->ndisks; i++) { + virDomainBackupDiskDef *disk = &def->disks[i]; + virStorageSourcePtr src = vm->def->disks[disk->idx]->src; + + if (!disk->store) + continue; + if (qemuMonitorJSONTransactionAdd(json, + "blockdev-backup", + "s:device", src->nodeformat, + "s:target", disk->store->nodeformat, + "s:sync", "none", + "s:job-id", disk->name,
This should probably be somewhat more descriptive to prevent collisions if we ever want to allow multiple jobs
+ NULL) < 0) + goto endmon; + } + if (qemuMonitorTransaction(priv->mon, &json) < 0) + goto endmon; + job_started = true; + + /* + - pull: nbd-server-start with <server> from user (or autogenerate server) - pull: nbd-server-add per <disk>, including bitmap for incr */ + if (def->type == VIR_DOMAIN_BACKUP_TYPE_PULL) { + if (qemuMonitorNBDServerStart(priv->mon, def->server, NULL) < 0) + goto endmon; + nbd_running = true; + for (i = 0; i < def->ndisks; i++) { + virDomainBackupDiskDef *disk = &def->disks[i]; + + if (!disk->store) + continue; + if (qemuMonitorNBDServerAdd(priv->mon, disk->store->nodeformat, + disk->name, false, + def->incremental ? disk->name : + NULL) < 0) + goto endmon; + disk->state = VIR_DOMAIN_BACKUP_DISK_STATE_EXPORT; + } + } + + ret = 0;