[libvirt PATCH 00/30] introduce external snapshot delete support

This implements virDomainSnapshotDelete API to support external snapshots. The support doesn't include flags VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN and VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY as it would add more complexity and IMHO these flags should not existed at all. Gitlab repo with the patches: https://gitlab.com/phrdina/libvirt/-/commits/snapshot-delete-external Pavel Hrdina (30): libvirt: introduce VIR_DOMAIN_JOB_OPERATION_SNAPSHOT_DELETE qemu_block: extract block commit code to separate function qemu_block: move qemuDomainBlockPivot out of qemu_driver qemu_block: add async domain job support to qemuBlockCommit qemu_block: add async domain job support to qemuBlockPivot qemu_monitor: introduce qemuMonitorJobFinalize qemu_monitor_json: allow configuring autofinalize for block commit qemu_block: allow configuring autofinalize for block commit qemu_block: introduce qemuBlockFinalize qemu_block: change qemuBlockCommit to return job pointer qemu_blockjob: process QEMU_MONITOR_JOB_STATUS_PENDING signal qemu_snapshot: refactor qemuSnapshotDelete qemu_snapshot: introduce qemuSnapshotDeleteSingle qemu_snapshot: introduce qemuSnapshotDeleteChildren qemu_snapshot: rework snapshot children deletion qemu_snapshot: move snapshot discard out of qemu_domain.c qemu_snapshot: move snapshot metadata reparent code qemu_snapshot: introduce qemuSnapshotDiscardMetadata qemu_snapshot: introduce qemuSnapshotDeleteValidate function qemu_snapshot: refactor validation of snapshot delete qemu_snapshot: error out when deleting internal snapshot on non-active disk qemu_snapshot: convert snapshot delete to async domain job qemu_snapshot: prepare data for external snapshot deletion qemu_snapshot: implement deletion of external snapshot qemu_snapshot: update metadata when deleting snapshots qemu_snapshot: when deleting snapshot invalidate parent snapshot qemu_process: abort snapshot delete when daemon starts qemu_snapshot: enable deletion of external snapshots api: document support for external snapshot deletion NEWS: document support for external snapshot deletion NEWS.rst | 6 + include/libvirt/libvirt-domain.h | 1 + src/conf/snapshot_conf.c | 6 + src/conf/snapshot_conf.h | 1 + src/libvirt-domain-snapshot.c | 7 + src/qemu/qemu_backup.c | 1 + src/qemu/qemu_block.c | 336 +++++++++++ src/qemu/qemu_block.h | 22 + src/qemu/qemu_blockjob.c | 20 +- src/qemu/qemu_blockjob.h | 1 + src/qemu/qemu_domain.c | 95 +-- src/qemu/qemu_domain.h | 9 - src/qemu/qemu_driver.c | 292 +--------- src/qemu/qemu_monitor.c | 21 +- src/qemu/qemu_monitor.h | 8 +- src/qemu/qemu_monitor_json.c | 26 +- src/qemu/qemu_monitor_json.h | 8 +- src/qemu/qemu_process.c | 32 ++ src/qemu/qemu_snapshot.c | 956 ++++++++++++++++++++++++++++--- src/qemu/qemu_snapshot.h | 4 + tests/qemumonitorjsontest.c | 4 +- tools/virsh-domain.c | 1 + 22 files changed, 1381 insertions(+), 476 deletions(-) -- 2.38.1

This will be used by snapshot delete async domain job. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- include/libvirt/libvirt-domain.h | 1 + tools/virsh-domain.c | 1 + 2 files changed, 2 insertions(+) diff --git a/include/libvirt/libvirt-domain.h b/include/libvirt/libvirt-domain.h index 295fd30c93..4b1de1d5b8 100644 --- a/include/libvirt/libvirt-domain.h +++ b/include/libvirt/libvirt-domain.h @@ -4172,6 +4172,7 @@ typedef enum { VIR_DOMAIN_JOB_OPERATION_SNAPSHOT_REVERT = 7, /* (Since: 3.3.0) */ VIR_DOMAIN_JOB_OPERATION_DUMP = 8, /* (Since: 3.3.0) */ VIR_DOMAIN_JOB_OPERATION_BACKUP = 9, /* (Since: 6.0.0) */ + VIR_DOMAIN_JOB_OPERATION_SNAPSHOT_DELETE = 10, /* (Since: 9.0.0) */ # ifdef VIR_ENUM_SENTINELS VIR_DOMAIN_JOB_OPERATION_LAST /* (Since: 3.3.0) */ diff --git a/tools/virsh-domain.c b/tools/virsh-domain.c index 2d162cf8c0..3e94744c95 100644 --- a/tools/virsh-domain.c +++ b/tools/virsh-domain.c @@ -6141,6 +6141,7 @@ VIR_ENUM_IMPL(virshDomainJobOperation, N_("Snapshot revert"), N_("Dump"), N_("Backup"), + N_("Snapshot delete"), ); static const char * -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:37 +0100, Pavel Hrdina wrote:
This will be used by snapshot delete async domain job.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- include/libvirt/libvirt-domain.h | 1 + tools/virsh-domain.c | 1 + 2 files changed, 2 insertions(+)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 179 +++++++++++++++++++++++++++++++++++++++++ src/qemu/qemu_block.h | 9 +++ src/qemu/qemu_driver.c | 162 +------------------------------------ 3 files changed, 189 insertions(+), 161 deletions(-) diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 8a6f601b29..4cca7555f3 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3196,3 +3196,182 @@ qemuBlockExportAddNBD(virDomainObj *vm, return qemuMonitorBlockExportAdd(priv->mon, &nbdprops); } + + +int +qemuBlockCommit(virDomainObj *vm, + virDomainDiskDef *disk, + virStorageSource *baseSource, + virStorageSource *topSource, + virStorageSource *top_parent, + unsigned long bandwidth, + unsigned int flags) +{ + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + int rc = -1; + bool clean_access = false; + g_autofree char *backingPath = NULL; + qemuBlockJobData *job = NULL; + g_autoptr(virStorageSource) mirror = NULL; + + if (virDomainObjCheckActive(vm) < 0) + return -1; + + /* Convert bandwidth MiB to bytes, if necessary */ + if (!(flags & VIR_DOMAIN_BLOCK_COMMIT_BANDWIDTH_BYTES)) { + if (bandwidth > LLONG_MAX >> 20) { + virReportError(VIR_ERR_OVERFLOW, + _("bandwidth must be less than %llu"), + LLONG_MAX >> 20); + return -1; + } + bandwidth <<= 20; + } + + if (!qemuDomainDiskBlockJobIsSupported(disk)) + return -1; + + if (virStorageSourceIsEmpty(disk->src)) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, + _("disk %s has no source file to be committed"), + disk->dst); + return -1; + } + + if (qemuDomainDiskBlockJobIsActive(disk)) + return -1; + + if (qemuDomainSupportsCheckpointsBlockjobs(vm) < 0) + return -1; + + if (topSource == disk->src) { + /* XXX Should we auto-pivot when COMMIT_ACTIVE is not specified? */ + if (!(flags & VIR_DOMAIN_BLOCK_COMMIT_ACTIVE)) { + virReportError(VIR_ERR_INVALID_ARG, + _("commit of '%s' active layer requires active flag"), + disk->dst); + return -1; + } + } else if (flags & VIR_DOMAIN_BLOCK_COMMIT_ACTIVE) { + virReportError(VIR_ERR_INVALID_ARG, + _("active commit requested but '%s' is not active"), + topSource->path); + return -1; + } + + if (!virStorageSourceHasBacking(topSource)) { + virReportError(VIR_ERR_INVALID_ARG, + _("top '%s' in chain for '%s' has no backing file"), + topSource->path, disk->src->path); + return -1; + } + + if ((flags & VIR_DOMAIN_BLOCK_COMMIT_SHALLOW) && + baseSource != topSource->backingStore) { + virReportError(VIR_ERR_INVALID_ARG, + _("base '%s' is not immediately below '%s' in chain " + "for '%s'"), + baseSource->path, topSource->path, disk->src->path); + return -1; + } + + /* For an active commit, clone enough of the base to act as the mirror */ + if (topSource == disk->src) { + if (!(mirror = virStorageSourceCopy(baseSource, false))) + return -1; + if (virStorageSourceInitChainElement(mirror, + disk->src, + true) < 0) + return -1; + } + + if (flags & VIR_DOMAIN_BLOCK_COMMIT_RELATIVE && + topSource != disk->src) { + if (top_parent && + qemuBlockUpdateRelativeBacking(vm, top_parent, disk->src) < 0) + return -1; + + if (virStorageSourceGetRelativeBackingPath(topSource, baseSource, + &backingPath) < 0) + return -1; + + if (!backingPath) { + virReportError(VIR_ERR_OPERATION_INVALID, "%s", + _("can't keep relative backing relationship")); + return -1; + } + } + + /* For the commit to succeed, we must allow qemu to open both the + * 'base' image and the parent of 'top' as read/write; 'top' might + * not have a parent, or might already be read-write. XXX It + * would also be nice to revert 'base' to read-only, as well as + * revoke access to files removed from the chain, when the commit + * operation succeeds, but doing that requires tracking the + * operation in XML across libvirtd restarts. */ + clean_access = true; + if (qemuDomainStorageSourceAccessAllow(driver, vm, baseSource, + false, false, false) < 0) + goto error; + + if (top_parent && top_parent != disk->src) { + /* While top_parent is topmost image, we don't need to remember its + * owner as it will be overwritten upon finishing the commit. Hence, + * pass chainTop = false. */ + if (qemuDomainStorageSourceAccessAllow(driver, vm, top_parent, + false, false, false) < 0) + goto error; + } + + if (!(job = qemuBlockJobDiskNewCommit(vm, disk, top_parent, topSource, + baseSource, + flags & VIR_DOMAIN_BLOCK_COMMIT_DELETE, + flags))) + goto error; + + disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_NONE; + + if (!backingPath && top_parent && + !(backingPath = qemuBlockGetBackingStoreString(baseSource, false))) + goto error; + + qemuDomainObjEnterMonitor(vm); + + rc = qemuMonitorBlockCommit(priv->mon, + qemuDomainDiskGetTopNodename(disk), + job->name, + topSource->nodeformat, + baseSource->nodeformat, + backingPath, bandwidth); + + qemuDomainObjExitMonitor(vm); + + if (rc < 0) + goto error; + + if (mirror) { + disk->mirror = g_steal_pointer(&mirror); + disk->mirrorJob = VIR_DOMAIN_BLOCK_JOB_TYPE_ACTIVE_COMMIT; + } + qemuBlockJobStarted(job, vm); + + return 0; + + error: + if (clean_access) { + virErrorPtr orig_err; + virErrorPreserveLast(&orig_err); + /* Revert access to read-only, if possible. */ + qemuDomainStorageSourceAccessAllow(driver, vm, baseSource, + true, false, false); + if (top_parent && top_parent != disk->src) + qemuDomainStorageSourceAccessAllow(driver, vm, top_parent, + true, false, false); + + virErrorRestore(&orig_err); + } + qemuBlockJobStartupFinalize(vm, job); + + return -1; +} diff --git a/src/qemu/qemu_block.h b/src/qemu/qemu_block.h index 8a3a10344e..85b4805d89 100644 --- a/src/qemu/qemu_block.h +++ b/src/qemu/qemu_block.h @@ -276,3 +276,12 @@ qemuBlockExportAddNBD(virDomainObj *vm, const char *exportname, bool writable, const char *bitmap); + +int +qemuBlockCommit(virDomainObj *vm, + virDomainDiskDef *disk, + virStorageSource *baseSource, + virStorageSource *topSource, + virStorageSource *top_parent, + unsigned long bandwidth, + unsigned int flags); diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index d509582719..2f05da3d8c 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -15114,19 +15114,12 @@ qemuDomainBlockCommit(virDomainPtr dom, unsigned long bandwidth, unsigned int flags) { - virQEMUDriver *driver = dom->conn->privateData; - qemuDomainObjPrivate *priv; virDomainObj *vm = NULL; int ret = -1; virDomainDiskDef *disk = NULL; virStorageSource *topSource; virStorageSource *baseSource = NULL; virStorageSource *top_parent = NULL; - bool clean_access = false; - g_autofree char *backingPath = NULL; - unsigned long long speed = bandwidth; - qemuBlockJobData *job = NULL; - g_autoptr(virStorageSource) mirror = NULL; virCheckFlags(VIR_DOMAIN_BLOCK_COMMIT_SHALLOW | VIR_DOMAIN_BLOCK_COMMIT_ACTIVE | @@ -15136,7 +15129,6 @@ qemuDomainBlockCommit(virDomainPtr dom, if (!(vm = qemuDomainObjFromDomain(dom))) goto cleanup; - priv = vm->privateData; if (virDomainBlockCommitEnsureACL(dom->conn, vm->def) < 0) goto cleanup; @@ -15144,176 +15136,24 @@ qemuDomainBlockCommit(virDomainPtr dom, if (virDomainObjBeginJob(vm, VIR_JOB_MODIFY) < 0) goto cleanup; - if (virDomainObjCheckActive(vm) < 0) - goto endjob; - - /* Convert bandwidth MiB to bytes, if necessary */ - if (!(flags & VIR_DOMAIN_BLOCK_COMMIT_BANDWIDTH_BYTES)) { - if (speed > LLONG_MAX >> 20) { - virReportError(VIR_ERR_OVERFLOW, - _("bandwidth must be less than %llu"), - LLONG_MAX >> 20); - goto endjob; - } - speed <<= 20; - } - if (!(disk = qemuDomainDiskByName(vm->def, path))) goto endjob; - if (!qemuDomainDiskBlockJobIsSupported(disk)) - goto endjob; - - if (virStorageSourceIsEmpty(disk->src)) { - virReportError(VIR_ERR_CONFIG_UNSUPPORTED, - _("disk %s has no source file to be committed"), - disk->dst); - goto endjob; - } - - if (qemuDomainDiskBlockJobIsActive(disk)) - goto endjob; - - if (qemuDomainSupportsCheckpointsBlockjobs(vm) < 0) - goto endjob; - if (!top || STREQ(top, disk->dst)) topSource = disk->src; else if (!(topSource = virStorageSourceChainLookup(disk->src, NULL, top, disk->dst, &top_parent))) goto endjob; - if (topSource == disk->src) { - /* XXX Should we auto-pivot when COMMIT_ACTIVE is not specified? */ - if (!(flags & VIR_DOMAIN_BLOCK_COMMIT_ACTIVE)) { - virReportError(VIR_ERR_INVALID_ARG, - _("commit of '%s' active layer requires active flag"), - disk->dst); - goto endjob; - } - } else if (flags & VIR_DOMAIN_BLOCK_COMMIT_ACTIVE) { - virReportError(VIR_ERR_INVALID_ARG, - _("active commit requested but '%s' is not active"), - topSource->path); - goto endjob; - } - - if (!virStorageSourceHasBacking(topSource)) { - virReportError(VIR_ERR_INVALID_ARG, - _("top '%s' in chain for '%s' has no backing file"), - topSource->path, path); - goto endjob; - } - if (!base && (flags & VIR_DOMAIN_BLOCK_COMMIT_SHALLOW)) baseSource = topSource->backingStore; else if (!(baseSource = virStorageSourceChainLookup(disk->src, topSource, base, disk->dst, NULL))) goto endjob; - if ((flags & VIR_DOMAIN_BLOCK_COMMIT_SHALLOW) && - baseSource != topSource->backingStore) { - virReportError(VIR_ERR_INVALID_ARG, - _("base '%s' is not immediately below '%s' in chain " - "for '%s'"), - base, topSource->path, path); - goto endjob; - } - - /* For an active commit, clone enough of the base to act as the mirror */ - if (topSource == disk->src) { - if (!(mirror = virStorageSourceCopy(baseSource, false))) - goto endjob; - if (virStorageSourceInitChainElement(mirror, - disk->src, - true) < 0) - goto endjob; - } - - if (flags & VIR_DOMAIN_BLOCK_COMMIT_RELATIVE && - topSource != disk->src) { - if (top_parent && - qemuBlockUpdateRelativeBacking(vm, top_parent, disk->src) < 0) - goto endjob; - - if (virStorageSourceGetRelativeBackingPath(topSource, baseSource, - &backingPath) < 0) - goto endjob; - - if (!backingPath) { - virReportError(VIR_ERR_OPERATION_INVALID, "%s", - _("can't keep relative backing relationship")); - goto endjob; - } - } - - /* For the commit to succeed, we must allow qemu to open both the - * 'base' image and the parent of 'top' as read/write; 'top' might - * not have a parent, or might already be read-write. XXX It - * would also be nice to revert 'base' to read-only, as well as - * revoke access to files removed from the chain, when the commit - * operation succeeds, but doing that requires tracking the - * operation in XML across libvirtd restarts. */ - clean_access = true; - if (qemuDomainStorageSourceAccessAllow(driver, vm, baseSource, - false, false, false) < 0) - goto endjob; - - if (top_parent && top_parent != disk->src) { - /* While top_parent is topmost image, we don't need to remember its - * owner as it will be overwritten upon finishing the commit. Hence, - * pass chainTop = false. */ - if (qemuDomainStorageSourceAccessAllow(driver, vm, top_parent, - false, false, false) < 0) - goto endjob; - } - - if (!(job = qemuBlockJobDiskNewCommit(vm, disk, top_parent, topSource, - baseSource, - flags & VIR_DOMAIN_BLOCK_COMMIT_DELETE, - flags))) - goto endjob; - - disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_NONE; - - if (!backingPath && top_parent && - !(backingPath = qemuBlockGetBackingStoreString(baseSource, false))) - goto endjob; - - qemuDomainObjEnterMonitor(vm); - - ret = qemuMonitorBlockCommit(priv->mon, - qemuDomainDiskGetTopNodename(disk), - job->name, - topSource->nodeformat, - baseSource->nodeformat, - backingPath, speed); - - qemuDomainObjExitMonitor(vm); - - if (ret < 0) - goto endjob; - - if (mirror) { - disk->mirror = g_steal_pointer(&mirror); - disk->mirrorJob = VIR_DOMAIN_BLOCK_JOB_TYPE_ACTIVE_COMMIT; - } - qemuBlockJobStarted(job, vm); + ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, bandwidth, flags); endjob: - if (ret < 0 && clean_access) { - virErrorPtr orig_err; - virErrorPreserveLast(&orig_err); - /* Revert access to read-only, if possible. */ - qemuDomainStorageSourceAccessAllow(driver, vm, baseSource, - true, false, false); - if (top_parent && top_parent != disk->src) - qemuDomainStorageSourceAccessAllow(driver, vm, top_parent, - true, false, false); - - virErrorRestore(&orig_err); - } - qemuBlockJobStartupFinalize(vm, job); virDomainObjEndJob(vm); cleanup: -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:38 +0100, Pavel Hrdina wrote: Please mention the name of the new function in the commit message.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 179 +++++++++++++++++++++++++++++++++++++++++ src/qemu/qemu_block.h | 9 +++ src/qemu/qemu_driver.c | 162 +------------------------------------ 3 files changed, 189 insertions(+), 161 deletions(-)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

On Thu, Dec 08, 2022 at 14:30:38 +0100, Pavel Hrdina wrote:
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 179 +++++++++++++++++++++++++++++++++++++++++ src/qemu/qemu_block.h | 9 +++ src/qemu/qemu_driver.c | 162 +------------------------------------ 3 files changed, 189 insertions(+), 161 deletions(-)
diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 8a6f601b29..4cca7555f3 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3196,3 +3196,182 @@ qemuBlockExportAddNBD(virDomainObj *vm,
[...]
+ if (rc < 0) + goto error; + + if (mirror) { + disk->mirror = g_steal_pointer(&mirror); + disk->mirrorJob = VIR_DOMAIN_BLOCK_JOB_TYPE_ACTIVE_COMMIT; + } + qemuBlockJobStarted(job, vm);
[1]
+ + return 0; + + error: + if (clean_access) { + virErrorPtr orig_err; + virErrorPreserveLast(&orig_err); + /* Revert access to read-only, if possible. */ + qemuDomainStorageSourceAccessAllow(driver, vm, baseSource, + true, false, false); + if (top_parent && top_parent != disk->src) + qemuDomainStorageSourceAccessAllow(driver, vm, top_parent, + true, false, false); + + virErrorRestore(&orig_err); + } + qemuBlockJobStartupFinalize(vm, job);
[2]
+ + return -1; +}
[...]
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index d509582719..2f05da3d8c 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c
[...]
- - ret = qemuMonitorBlockCommit(priv->mon, - qemuDomainDiskGetTopNodename(disk), - job->name, - topSource->nodeformat, - baseSource->nodeformat, - backingPath, speed); - - qemuDomainObjExitMonitor(vm); - - if (ret < 0) - goto endjob; - - if (mirror) { - disk->mirror = g_steal_pointer(&mirror); - disk->mirrorJob = VIR_DOMAIN_BLOCK_JOB_TYPE_ACTIVE_COMMIT; - } - qemuBlockJobStarted(job, vm); + ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, bandwidth, flags);
endjob: - if (ret < 0 && clean_access) { - virErrorPtr orig_err; - virErrorPreserveLast(&orig_err); - /* Revert access to read-only, if possible. */ - qemuDomainStorageSourceAccessAllow(driver, vm, baseSource, - true, false, false); - if (top_parent && top_parent != disk->src) - qemuDomainStorageSourceAccessAllow(driver, vm, top_parent, - true, false, false); - - virErrorRestore(&orig_err); - } - qemuBlockJobStartupFinalize(vm, job);
So before this patch qemuBlockJobStartupFinalize() gets called both on error and success code paths. After this patch [1] the success code path doesn't call it any more, just the error code path [2]. Thus the reference to 'job' is leaked in the new code. The new function must also call qemuBlockJobStartupFinalize() in both cases.

Move the code for finishing a job in the ready state to qemu_block.c. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> Reviewed-by: Peter Krempa <pkrempa@redhat.com> --- src/qemu/qemu_block.c | 121 ++++++++++++++++++++++++++++++++++++++++ src/qemu/qemu_block.h | 5 ++ src/qemu/qemu_driver.c | 123 +---------------------------------------- 3 files changed, 127 insertions(+), 122 deletions(-) diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 4cca7555f3..721145fa42 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3375,3 +3375,124 @@ qemuBlockCommit(virDomainObj *vm, return -1; } + + +/* Called while holding the VM job lock, to implement a block job + * abort with pivot; this updates the VM definition as appropriate, on + * either success or failure. */ +int +qemuBlockPivot(virDomainObj *vm, + qemuBlockJobData *job, + virDomainDiskDef *disk) +{ + g_autoptr(qemuBlockStorageSourceChainData) chainattachdata = NULL; + int ret = -1; + qemuDomainObjPrivate *priv = vm->privateData; + g_autoptr(virJSONValue) bitmapactions = NULL; + g_autoptr(virJSONValue) reopenactions = NULL; + int rc = 0; + + if (job->state != QEMU_BLOCKJOB_STATE_READY) { + virReportError(VIR_ERR_BLOCK_COPY_ACTIVE, + _("block job '%s' not ready for pivot yet"), + job->name); + return -1; + } + + switch ((qemuBlockJobType) job->type) { + case QEMU_BLOCKJOB_TYPE_NONE: + case QEMU_BLOCKJOB_TYPE_LAST: + virReportError(VIR_ERR_INTERNAL_ERROR, + _("invalid job type '%d'"), job->type); + return -1; + + case QEMU_BLOCKJOB_TYPE_PULL: + case QEMU_BLOCKJOB_TYPE_COMMIT: + case QEMU_BLOCKJOB_TYPE_BACKUP: + case QEMU_BLOCKJOB_TYPE_INTERNAL: + case QEMU_BLOCKJOB_TYPE_CREATE: + case QEMU_BLOCKJOB_TYPE_BROKEN: + virReportError(VIR_ERR_OPERATION_INVALID, + _("job type '%s' does not support pivot"), + qemuBlockjobTypeToString(job->type)); + return -1; + + case QEMU_BLOCKJOB_TYPE_COPY: + if (!job->jobflagsmissing) { + bool shallow = job->jobflags & VIR_DOMAIN_BLOCK_COPY_SHALLOW; + bool reuse = job->jobflags & VIR_DOMAIN_BLOCK_COPY_REUSE_EXT; + + bitmapactions = virJSONValueNewArray(); + + if (qemuMonitorTransactionBitmapAdd(bitmapactions, + disk->mirror->nodeformat, + "libvirt-tmp-activewrite", + false, + false, + 0) < 0) + return -1; + + /* Open and install the backing chain of 'mirror' late if we can use + * blockdev-snapshot to do it. This is to appease oVirt that wants + * to copy data into the backing chain while the top image is being + * copied shallow */ + if (reuse && shallow && + virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_BLOCKDEV_SNAPSHOT_ALLOW_WRITE_ONLY) && + virStorageSourceHasBacking(disk->mirror)) { + + if (!(chainattachdata = qemuBuildStorageSourceChainAttachPrepareBlockdev(disk->mirror->backingStore))) + return -1; + + reopenactions = virJSONValueNewArray(); + + if (qemuMonitorTransactionSnapshotBlockdev(reopenactions, + disk->mirror->backingStore->nodeformat, + disk->mirror->nodeformat)) + return -1; + } + + } + break; + + case QEMU_BLOCKJOB_TYPE_ACTIVE_COMMIT: + bitmapactions = virJSONValueNewArray(); + + if (qemuMonitorTransactionBitmapAdd(bitmapactions, + job->data.commit.base->nodeformat, + "libvirt-tmp-activewrite", + false, + false, + 0) < 0) + return -1; + + break; + } + + qemuDomainObjEnterMonitor(vm); + + if (chainattachdata) { + if ((rc = qemuBlockStorageSourceChainAttach(priv->mon, chainattachdata)) == 0) { + /* install backing images on success, or unplug them on failure */ + if ((rc = qemuMonitorTransaction(priv->mon, &reopenactions)) != 0) + qemuBlockStorageSourceChainDetach(priv->mon, chainattachdata); + } + } + + if (bitmapactions && rc == 0) + ignore_value(qemuMonitorTransaction(priv->mon, &bitmapactions)); + + if (rc == 0) + ret = qemuMonitorJobComplete(priv->mon, job->name); + + qemuDomainObjExitMonitor(vm); + + /* The pivot failed. The block job in QEMU remains in the synchronised state */ + if (ret < 0) + return -1; + + if (disk && disk->mirror) + disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_PIVOT; + job->state = QEMU_BLOCKJOB_STATE_PIVOTING; + + return ret; +} diff --git a/src/qemu/qemu_block.h b/src/qemu/qemu_block.h index 85b4805d89..52deb15a3d 100644 --- a/src/qemu/qemu_block.h +++ b/src/qemu/qemu_block.h @@ -285,3 +285,8 @@ qemuBlockCommit(virDomainObj *vm, virStorageSource *top_parent, unsigned long bandwidth, unsigned int flags); + +int +qemuBlockPivot(virDomainObj *vm, + qemuBlockJobData *job, + virDomainDiskDef *disk); diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 2f05da3d8c..d8db7188f7 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -14001,127 +14001,6 @@ qemuDomainOpenChannel(virDomainPtr dom, } -/* Called while holding the VM job lock, to implement a block job - * abort with pivot; this updates the VM definition as appropriate, on - * either success or failure. */ -static int -qemuDomainBlockPivot(virDomainObj *vm, - qemuBlockJobData *job, - virDomainDiskDef *disk) -{ - g_autoptr(qemuBlockStorageSourceChainData) chainattachdata = NULL; - int ret = -1; - qemuDomainObjPrivate *priv = vm->privateData; - g_autoptr(virJSONValue) bitmapactions = NULL; - g_autoptr(virJSONValue) reopenactions = NULL; - int rc = 0; - - if (job->state != QEMU_BLOCKJOB_STATE_READY) { - virReportError(VIR_ERR_BLOCK_COPY_ACTIVE, - _("block job '%s' not ready for pivot yet"), - job->name); - return -1; - } - - switch ((qemuBlockJobType) job->type) { - case QEMU_BLOCKJOB_TYPE_NONE: - case QEMU_BLOCKJOB_TYPE_LAST: - virReportError(VIR_ERR_INTERNAL_ERROR, - _("invalid job type '%d'"), job->type); - return -1; - - case QEMU_BLOCKJOB_TYPE_PULL: - case QEMU_BLOCKJOB_TYPE_COMMIT: - case QEMU_BLOCKJOB_TYPE_BACKUP: - case QEMU_BLOCKJOB_TYPE_INTERNAL: - case QEMU_BLOCKJOB_TYPE_CREATE: - case QEMU_BLOCKJOB_TYPE_BROKEN: - virReportError(VIR_ERR_OPERATION_INVALID, - _("job type '%s' does not support pivot"), - qemuBlockjobTypeToString(job->type)); - return -1; - - case QEMU_BLOCKJOB_TYPE_COPY: - if (!job->jobflagsmissing) { - bool shallow = job->jobflags & VIR_DOMAIN_BLOCK_COPY_SHALLOW; - bool reuse = job->jobflags & VIR_DOMAIN_BLOCK_COPY_REUSE_EXT; - - bitmapactions = virJSONValueNewArray(); - - if (qemuMonitorTransactionBitmapAdd(bitmapactions, - disk->mirror->nodeformat, - "libvirt-tmp-activewrite", - false, - false, - 0) < 0) - return -1; - - /* Open and install the backing chain of 'mirror' late if we can use - * blockdev-snapshot to do it. This is to appease oVirt that wants - * to copy data into the backing chain while the top image is being - * copied shallow */ - if (reuse && shallow && - virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_BLOCKDEV_SNAPSHOT_ALLOW_WRITE_ONLY) && - virStorageSourceHasBacking(disk->mirror)) { - - if (!(chainattachdata = qemuBuildStorageSourceChainAttachPrepareBlockdev(disk->mirror->backingStore))) - return -1; - - reopenactions = virJSONValueNewArray(); - - if (qemuMonitorTransactionSnapshotBlockdev(reopenactions, - disk->mirror->backingStore->nodeformat, - disk->mirror->nodeformat)) - return -1; - } - - } - break; - - case QEMU_BLOCKJOB_TYPE_ACTIVE_COMMIT: - bitmapactions = virJSONValueNewArray(); - - if (qemuMonitorTransactionBitmapAdd(bitmapactions, - job->data.commit.base->nodeformat, - "libvirt-tmp-activewrite", - false, - false, - 0) < 0) - return -1; - - break; - } - - qemuDomainObjEnterMonitor(vm); - - if (chainattachdata) { - if ((rc = qemuBlockStorageSourceChainAttach(priv->mon, chainattachdata)) == 0) { - /* install backing images on success, or unplug them on failure */ - if ((rc = qemuMonitorTransaction(priv->mon, &reopenactions)) != 0) - qemuBlockStorageSourceChainDetach(priv->mon, chainattachdata); - } - } - - if (bitmapactions && rc == 0) - ignore_value(qemuMonitorTransaction(priv->mon, &bitmapactions)); - - if (rc == 0) - ret = qemuMonitorJobComplete(priv->mon, job->name); - - qemuDomainObjExitMonitor(vm); - - /* The pivot failed. The block job in QEMU remains in the synchronised state */ - if (ret < 0) - return -1; - - if (disk && disk->mirror) - disk->mirrorState = VIR_DOMAIN_DISK_MIRROR_STATE_PIVOT; - job->state = QEMU_BLOCKJOB_STATE_PIVOTING; - - return ret; -} - - /* bandwidth in MiB/s per public API. Caller must lock vm beforehand, * and not access it afterwards. */ static int @@ -14284,7 +14163,7 @@ qemuDomainBlockJobAbort(virDomainPtr dom, qemuBlockJobSyncBegin(job); if (pivot) { - if ((ret = qemuDomainBlockPivot(vm, job, disk)) < 0) + if ((ret = qemuBlockPivot(vm, job, disk)) < 0) goto endjob; } else { qemuDomainObjEnterMonitor(vm); -- 2.38.1

This will allow to use it while having async domain job active which we will use when deleting external snapshots. At the same time we will need to have the block job started as synchronous. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 7 ++++++- src/qemu/qemu_block.h | 1 + src/qemu/qemu_driver.c | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 721145fa42..999e43c630 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3205,6 +3205,7 @@ qemuBlockCommit(virDomainObj *vm, virStorageSource *topSource, virStorageSource *top_parent, unsigned long bandwidth, + virDomainAsyncJob asyncJob, unsigned int flags) { qemuDomainObjPrivate *priv = vm->privateData; @@ -3336,7 +3337,11 @@ qemuBlockCommit(virDomainObj *vm, !(backingPath = qemuBlockGetBackingStoreString(baseSource, false))) goto error; - qemuDomainObjEnterMonitor(vm); + if (asyncJob != VIR_ASYNC_JOB_NONE) + qemuBlockJobSyncBegin(job); + + if (qemuDomainObjEnterMonitorAsync(vm, asyncJob) < 0) + goto error; rc = qemuMonitorBlockCommit(priv->mon, qemuDomainDiskGetTopNodename(disk), diff --git a/src/qemu/qemu_block.h b/src/qemu/qemu_block.h index 52deb15a3d..a76d9be711 100644 --- a/src/qemu/qemu_block.h +++ b/src/qemu/qemu_block.h @@ -284,6 +284,7 @@ qemuBlockCommit(virDomainObj *vm, virStorageSource *topSource, virStorageSource *top_parent, unsigned long bandwidth, + virDomainAsyncJob asyncJob, unsigned int flags); int diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index d8db7188f7..5ee6e2698d 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -15030,7 +15030,8 @@ qemuDomainBlockCommit(virDomainPtr dom, base, disk->dst, NULL))) goto endjob; - ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, bandwidth, flags); + ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, + bandwidth, VIR_ASYNC_JOB_NONE, flags); endjob: virDomainObjEndJob(vm); -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:40 +0100, Pavel Hrdina wrote:
This will allow to use it while having async domain job active which we will use when deleting external snapshots. At the same time we will need to have the block job started as synchronous.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 7 ++++++- src/qemu/qemu_block.h | 1 + src/qemu/qemu_driver.c | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 721145fa42..999e43c630 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3205,6 +3205,7 @@ qemuBlockCommit(virDomainObj *vm, virStorageSource *topSource, virStorageSource *top_parent, unsigned long bandwidth, + virDomainAsyncJob asyncJob, unsigned int flags)
Please add a function comment which describes parameters and mention in it that setting asyncJob makes the block job behave synchronous in the comment. It's quite an important semantic fact. Reviewed-by: Peter Krempa <pkrempa@redhat.com>

This will allow to use it while having async domain job active which we will use when deleting external snapshots. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 4 +++- src/qemu/qemu_block.h | 1 + src/qemu/qemu_driver.c | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 999e43c630..8a633d4423 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3388,6 +3388,7 @@ qemuBlockCommit(virDomainObj *vm, int qemuBlockPivot(virDomainObj *vm, qemuBlockJobData *job, + virDomainAsyncJob asyncJob, virDomainDiskDef *disk) { g_autoptr(qemuBlockStorageSourceChainData) chainattachdata = NULL; @@ -3473,7 +3474,8 @@ qemuBlockPivot(virDomainObj *vm, break; } - qemuDomainObjEnterMonitor(vm); + if (qemuDomainObjEnterMonitorAsync(vm, asyncJob) < 0) + return -1; if (chainattachdata) { if ((rc = qemuBlockStorageSourceChainAttach(priv->mon, chainattachdata)) == 0) { diff --git a/src/qemu/qemu_block.h b/src/qemu/qemu_block.h index a76d9be711..3f2082f102 100644 --- a/src/qemu/qemu_block.h +++ b/src/qemu/qemu_block.h @@ -290,4 +290,5 @@ qemuBlockCommit(virDomainObj *vm, int qemuBlockPivot(virDomainObj *vm, qemuBlockJobData *job, + virDomainAsyncJob asyncJob, virDomainDiskDef *disk); diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 5ee6e2698d..acd67b1f5a 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -14163,7 +14163,7 @@ qemuDomainBlockJobAbort(virDomainPtr dom, qemuBlockJobSyncBegin(job); if (pivot) { - if ((ret = qemuBlockPivot(vm, job, disk)) < 0) + if ((ret = qemuBlockPivot(vm, job, VIR_ASYNC_JOB_NONE, disk)) < 0) goto endjob; } else { qemuDomainObjEnterMonitor(vm); -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:41 +0100, Pavel Hrdina wrote:
This will allow to use it while having async domain job active which we will use when deleting external snapshots.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 4 +++- src/qemu/qemu_block.h | 1 + src/qemu/qemu_driver.c | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Upcoming snapshot deletion code will require that multiple commit jobs are finished in sync. To allow aborting then if one fails we will need to use manual finalization of the jobs. This commit implements the monitor code for `job-finalize`. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_monitor.c | 12 ++++++++++++ src/qemu/qemu_monitor.h | 5 +++++ src/qemu/qemu_monitor_json.c | 22 ++++++++++++++++++++++ src/qemu/qemu_monitor_json.h | 5 +++++ tests/qemumonitorjsontest.c | 2 ++ 5 files changed, 46 insertions(+) diff --git a/src/qemu/qemu_monitor.c b/src/qemu/qemu_monitor.c index 734364e070..0354832fa5 100644 --- a/src/qemu/qemu_monitor.c +++ b/src/qemu/qemu_monitor.c @@ -2932,6 +2932,18 @@ qemuMonitorJobDismiss(qemuMonitor *mon, } +int +qemuMonitorJobFinalize(qemuMonitor *mon, + const char *jobname) +{ + VIR_DEBUG("jobname=%s", jobname); + + QEMU_CHECK_MONITOR(mon); + + return qemuMonitorJSONJobFinalize(mon, jobname); +} + + int qemuMonitorJobComplete(qemuMonitor *mon, const char *jobname) diff --git a/src/qemu/qemu_monitor.h b/src/qemu/qemu_monitor.h index 906a919f52..eebac918c1 100644 --- a/src/qemu/qemu_monitor.h +++ b/src/qemu/qemu_monitor.h @@ -1029,6 +1029,11 @@ int qemuMonitorJobDismiss(qemuMonitor *mon, const char *jobname) ATTRIBUTE_NONNULL(2); +int +qemuMonitorJobFinalize(qemuMonitor *mon, + const char *jobname) + ATTRIBUTE_NONNULL(2); + int qemuMonitorJobComplete(qemuMonitor *mon, const char *jobname) ATTRIBUTE_NONNULL(2); diff --git a/src/qemu/qemu_monitor_json.c b/src/qemu/qemu_monitor_json.c index 9822097bd7..baf01a7206 100644 --- a/src/qemu/qemu_monitor_json.c +++ b/src/qemu/qemu_monitor_json.c @@ -4405,6 +4405,28 @@ qemuMonitorJSONJobDismiss(qemuMonitor *mon, } +int +qemuMonitorJSONJobFinalize(qemuMonitor *mon, + const char *jobname) +{ + g_autoptr(virJSONValue) cmd = NULL; + g_autoptr(virJSONValue) reply = NULL; + + if (!(cmd = qemuMonitorJSONMakeCommand("job-finalize", + "s:id", jobname, + NULL))) + return -1; + + if (qemuMonitorJSONCommand(mon, cmd, &reply) < 0) + return -1; + + if (qemuMonitorJSONBlockJobError(cmd, reply, jobname) < 0) + return -1; + + return 0; +} + + int qemuMonitorJSONJobComplete(qemuMonitor *mon, const char *jobname) diff --git a/src/qemu/qemu_monitor_json.h b/src/qemu/qemu_monitor_json.h index 484cb09830..073a0579b5 100644 --- a/src/qemu/qemu_monitor_json.h +++ b/src/qemu/qemu_monitor_json.h @@ -377,6 +377,11 @@ qemuMonitorJSONJobDismiss(qemuMonitor *mon, const char *jobname) ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); +int +qemuMonitorJSONJobFinalize(qemuMonitor *mon, + const char *jobname) + ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); + int qemuMonitorJSONJobComplete(qemuMonitor *mon, const char *jobname) diff --git a/tests/qemumonitorjsontest.c b/tests/qemumonitorjsontest.c index 59f7322711..f224ccb364 100644 --- a/tests/qemumonitorjsontest.c +++ b/tests/qemumonitorjsontest.c @@ -1223,6 +1223,7 @@ GEN_TEST_FUNC(qemuMonitorJSONBlockdevMediumInsert, "foodev", "newnode") GEN_TEST_FUNC(qemuMonitorJSONBitmapRemove, "foodev", "newnode") GEN_TEST_FUNC(qemuMonitorJSONJobDismiss, "jobname") GEN_TEST_FUNC(qemuMonitorJSONJobComplete, "jobname") +GEN_TEST_FUNC(qemuMonitorJSONJobFinalize, "jobname") GEN_TEST_FUNC(qemuMonitorJSONBlockJobCancel, "jobname", true) GEN_TEST_FUNC(qemuMonitorJSONSetAction, QEMU_MONITOR_ACTION_SHUTDOWN_PAUSE, @@ -2921,6 +2922,7 @@ mymain(void) DO_TEST_GEN(qemuMonitorJSONBitmapRemove); DO_TEST_GEN(qemuMonitorJSONJobDismiss); DO_TEST_GEN(qemuMonitorJSONJobComplete); + DO_TEST_GEN(qemuMonitorJSONJobFinalize); DO_TEST_GEN(qemuMonitorJSONBlockJobCancel); DO_TEST_GEN(qemuMonitorJSONSetAction); DO_TEST_GEN(qemuMonitorJSONSetLaunchSecurityState); -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:42 +0100, Pavel Hrdina wrote:
Upcoming snapshot deletion code will require that multiple commit jobs are finished in sync. To allow aborting then if one fails we will need to use manual finalization of the jobs.
This commit implements the monitor code for `job-finalize`.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_monitor.c | 12 ++++++++++++ src/qemu/qemu_monitor.h | 5 +++++ src/qemu/qemu_monitor_json.c | 22 ++++++++++++++++++++++ src/qemu/qemu_monitor_json.h | 5 +++++ tests/qemumonitorjsontest.c | 2 ++
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Deleting external snapshots will require configuring autofinalize to synchronize the block jobs for disks withing single snapshot in order to be able safely abort of one of the jobs fails. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 3 ++- src/qemu/qemu_monitor.c | 9 +++++---- src/qemu/qemu_monitor.h | 3 ++- src/qemu/qemu_monitor_json.c | 4 ++-- src/qemu/qemu_monitor_json.h | 3 ++- tests/qemumonitorjsontest.c | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 8a633d4423..5600abf6aa 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3348,7 +3348,8 @@ qemuBlockCommit(virDomainObj *vm, job->name, topSource->nodeformat, baseSource->nodeformat, - backingPath, bandwidth); + backingPath, bandwidth, + VIR_TRISTATE_BOOL_YES); qemuDomainObjExitMonitor(vm); diff --git a/src/qemu/qemu_monitor.c b/src/qemu/qemu_monitor.c index 0354832fa5..43525def0c 100644 --- a/src/qemu/qemu_monitor.c +++ b/src/qemu/qemu_monitor.c @@ -2795,16 +2795,17 @@ qemuMonitorBlockCommit(qemuMonitor *mon, const char *topNode, const char *baseNode, const char *backingName, - unsigned long long bandwidth) + unsigned long long bandwidth, + virTristateBool autofinalize) { - VIR_DEBUG("device=%s, jobname=%s, topNode=%s, baseNode=%s, backingName=%s, bandwidth=%llu", + VIR_DEBUG("device=%s, jobname=%s, topNode=%s, baseNode=%s, backingName=%s, bandwidth=%llu, autofinalize=%d", device, NULLSTR(jobname), NULLSTR(topNode), - NULLSTR(baseNode), NULLSTR(backingName), bandwidth); + NULLSTR(baseNode), NULLSTR(backingName), bandwidth, autofinalize); QEMU_CHECK_MONITOR(mon); return qemuMonitorJSONBlockCommit(mon, device, jobname, topNode, baseNode, - backingName, bandwidth); + backingName, bandwidth, autofinalize); } diff --git a/src/qemu/qemu_monitor.h b/src/qemu/qemu_monitor.h index eebac918c1..d09439911f 100644 --- a/src/qemu/qemu_monitor.h +++ b/src/qemu/qemu_monitor.h @@ -973,7 +973,8 @@ int qemuMonitorBlockCommit(qemuMonitor *mon, const char *topNode, const char *baseNode, const char *backingName, - unsigned long long bandwidth) + unsigned long long bandwidth, + virTristateBool autofinalize) ATTRIBUTE_NONNULL(2); int qemuMonitorArbitraryCommand(qemuMonitor *mon, diff --git a/src/qemu/qemu_monitor_json.c b/src/qemu/qemu_monitor_json.c index baf01a7206..9aee84084e 100644 --- a/src/qemu/qemu_monitor_json.c +++ b/src/qemu/qemu_monitor_json.c @@ -4028,11 +4028,11 @@ qemuMonitorJSONBlockCommit(qemuMonitor *mon, const char *topNode, const char *baseNode, const char *backingName, - unsigned long long speed) + unsigned long long speed, + virTristateBool autofinalize) { g_autoptr(virJSONValue) cmd = NULL; g_autoptr(virJSONValue) reply = NULL; - virTristateBool autofinalize = VIR_TRISTATE_BOOL_YES; virTristateBool autodismiss = VIR_TRISTATE_BOOL_NO; cmd = qemuMonitorJSONMakeCommand("block-commit", diff --git a/src/qemu/qemu_monitor_json.h b/src/qemu/qemu_monitor_json.h index 073a0579b5..a03caf7740 100644 --- a/src/qemu/qemu_monitor_json.h +++ b/src/qemu/qemu_monitor_json.h @@ -315,7 +315,8 @@ qemuMonitorJSONBlockCommit(qemuMonitor *mon, const char *topNode, const char *baseNode, const char *backingName, - unsigned long long bandwidth) + unsigned long long bandwidth, + virTristateBool autofinalize) ATTRIBUTE_NONNULL(1) ATTRIBUTE_NONNULL(2); int diff --git a/tests/qemumonitorjsontest.c b/tests/qemumonitorjsontest.c index f224ccb364..1db1f2b949 100644 --- a/tests/qemumonitorjsontest.c +++ b/tests/qemumonitorjsontest.c @@ -1211,7 +1211,7 @@ GEN_TEST_FUNC(qemuMonitorJSONRemoveNetdev, "net0") GEN_TEST_FUNC(qemuMonitorJSONDelDevice, "ide0") GEN_TEST_FUNC(qemuMonitorJSONBlockdevMirror, "jobname", true, "vdb", "targetnode", 1024, 1234, 31234, true, true) GEN_TEST_FUNC(qemuMonitorJSONBlockStream, "vdb", "jobname", "backingnode", "backingfilename", 1024) -GEN_TEST_FUNC(qemuMonitorJSONBlockCommit, "vdb", "jobname", "topnode", "basenode", "backingfilename", 1024) +GEN_TEST_FUNC(qemuMonitorJSONBlockCommit, "vdb", "jobname", "topnode", "basenode", "backingfilename", 1024, VIR_TRISTATE_BOOL_YES) GEN_TEST_FUNC(qemuMonitorJSONScreendump, "devicename", 1, NULL, "/foo/bar") GEN_TEST_FUNC(qemuMonitorJSONOpenGraphics, "spice", "spicefd", false) GEN_TEST_FUNC(qemuMonitorJSONNBDServerAdd, "vda", "export", true, "bitmap") -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:43 +0100, Pavel Hrdina wrote:
Deleting external snapshots will require configuring autofinalize to synchronize the block jobs for disks withing single snapshot in order to be able safely abort of one of the jobs fails.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 3 ++- src/qemu/qemu_monitor.c | 9 +++++---- src/qemu/qemu_monitor.h | 3 ++- src/qemu/qemu_monitor_json.c | 4 ++-- src/qemu/qemu_monitor_json.h | 3 ++- tests/qemumonitorjsontest.c | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

External snapshots will use this to synchronize qemu block jobs. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 7 ++++++- src/qemu/qemu_block.h | 1 + src/qemu/qemu_driver.c | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 5600abf6aa..2abddf904f 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3198,6 +3198,10 @@ qemuBlockExportAddNBD(virDomainObj *vm, } +/* The autofinalize argument controls if the qemu block job will be automatically + * finalized. This is used when deleting external snapshots where we need to + * disable automatic finalization for some use-case. The default value passed + * to this argument should be VIR_TRISTATE_BOOL_YES.*/ int qemuBlockCommit(virDomainObj *vm, virDomainDiskDef *disk, @@ -3206,6 +3210,7 @@ qemuBlockCommit(virDomainObj *vm, virStorageSource *top_parent, unsigned long bandwidth, virDomainAsyncJob asyncJob, + virTristateBool autofinalize, unsigned int flags) { qemuDomainObjPrivate *priv = vm->privateData; @@ -3349,7 +3354,7 @@ qemuBlockCommit(virDomainObj *vm, topSource->nodeformat, baseSource->nodeformat, backingPath, bandwidth, - VIR_TRISTATE_BOOL_YES); + autofinalize); qemuDomainObjExitMonitor(vm); diff --git a/src/qemu/qemu_block.h b/src/qemu/qemu_block.h index 3f2082f102..7b6aec2a7d 100644 --- a/src/qemu/qemu_block.h +++ b/src/qemu/qemu_block.h @@ -285,6 +285,7 @@ qemuBlockCommit(virDomainObj *vm, virStorageSource *top_parent, unsigned long bandwidth, virDomainAsyncJob asyncJob, + virTristateBool autofinalize, unsigned int flags); int diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index acd67b1f5a..12fca22616 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -15031,7 +15031,8 @@ qemuDomainBlockCommit(virDomainPtr dom, goto endjob; ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, - bandwidth, VIR_ASYNC_JOB_NONE, flags); + bandwidth, VIR_ASYNC_JOB_NONE, VIR_TRISTATE_BOOL_YES, + flags); endjob: virDomainObjEndJob(vm); -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:44 +0100, Pavel Hrdina wrote:
External snapshots will use this to synchronize qemu block jobs.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 7 ++++++- src/qemu/qemu_block.h | 1 + src/qemu/qemu_driver.c | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 5600abf6aa..2abddf904f 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3198,6 +3198,10 @@ qemuBlockExportAddNBD(virDomainObj *vm, }
+/* The autofinalize argument controls if the qemu block job will be automatically + * finalized. This is used when deleting external snapshots where we need to + * disable automatic finalization for some use-case. The default value passed + * to this argument should be VIR_TRISTATE_BOOL_YES.*/
In reply to previous patch I asked to do our usual function parameter description already for the 'asyncJob' so this will then be added to that comment. Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 19 +++++++++++++++++++ src/qemu/qemu_block.h | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 2abddf904f..24c82377b0 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3509,3 +3509,22 @@ qemuBlockPivot(virDomainObj *vm, return ret; } + + +int +qemuBlockFinalize(virDomainObj *vm, + qemuBlockJobData *job, + virDomainAsyncJob asyncJob) +{ + int ret; + qemuDomainObjPrivate *priv = vm->privateData; + + if (qemuDomainObjEnterMonitorAsync(vm, asyncJob) < 0) + return -1; + + ret = qemuMonitorJobFinalize(priv->mon, job->name); + + qemuDomainObjExitMonitor(vm); + + return ret; +} diff --git a/src/qemu/qemu_block.h b/src/qemu/qemu_block.h index 7b6aec2a7d..c169432d9c 100644 --- a/src/qemu/qemu_block.h +++ b/src/qemu/qemu_block.h @@ -293,3 +293,8 @@ qemuBlockPivot(virDomainObj *vm, qemuBlockJobData *job, virDomainAsyncJob asyncJob, virDomainDiskDef *disk); + +int +qemuBlockFinalize(virDomainObj *vm, + qemuBlockJobData *job, + virDomainAsyncJob asyncJob); -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:45 +0100, Pavel Hrdina wrote:
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 19 +++++++++++++++++++ src/qemu/qemu_block.h | 5 +++++ 2 files changed, 24 insertions(+)
diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 2abddf904f..24c82377b0 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3509,3 +3509,22 @@ qemuBlockPivot(virDomainObj *vm,
return ret; } + +
While at the monitor level it's quite obvious here please add a comment and describe what the finalization step actually is and how it's supposed to be used. Reviewed-by: Peter Krempa <pkrempa@redhat.com>

The created job will be needed by external snapshot delete code so rework qemuBlockCommit to return that pointer. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 42 +++++++++++++++++++++++------------------- src/qemu/qemu_block.h | 2 +- src/qemu/qemu_driver.c | 5 ++++- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 24c82377b0..0a0d942e71 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3201,8 +3201,12 @@ qemuBlockExportAddNBD(virDomainObj *vm, /* The autofinalize argument controls if the qemu block job will be automatically * finalized. This is used when deleting external snapshots where we need to * disable automatic finalization for some use-case. The default value passed - * to this argument should be VIR_TRISTATE_BOOL_YES.*/ -int + * to this argument should be VIR_TRISTATE_BOOL_YES. + * + * Returns qemuBlockJobData pointer on success, NULL on error. Caller is responsible + * to call virObjectUnref on the pointer. + */ +qemuBlockJobData * qemuBlockCommit(virDomainObj *vm, virDomainDiskDef *disk, virStorageSource *baseSource, @@ -3222,7 +3226,7 @@ qemuBlockCommit(virDomainObj *vm, g_autoptr(virStorageSource) mirror = NULL; if (virDomainObjCheckActive(vm) < 0) - return -1; + return NULL; /* Convert bandwidth MiB to bytes, if necessary */ if (!(flags & VIR_DOMAIN_BLOCK_COMMIT_BANDWIDTH_BYTES)) { @@ -3230,26 +3234,26 @@ qemuBlockCommit(virDomainObj *vm, virReportError(VIR_ERR_OVERFLOW, _("bandwidth must be less than %llu"), LLONG_MAX >> 20); - return -1; + return NULL; } bandwidth <<= 20; } if (!qemuDomainDiskBlockJobIsSupported(disk)) - return -1; + return NULL; if (virStorageSourceIsEmpty(disk->src)) { virReportError(VIR_ERR_CONFIG_UNSUPPORTED, _("disk %s has no source file to be committed"), disk->dst); - return -1; + return NULL; } if (qemuDomainDiskBlockJobIsActive(disk)) - return -1; + return NULL; if (qemuDomainSupportsCheckpointsBlockjobs(vm) < 0) - return -1; + return NULL; if (topSource == disk->src) { /* XXX Should we auto-pivot when COMMIT_ACTIVE is not specified? */ @@ -3257,20 +3261,20 @@ qemuBlockCommit(virDomainObj *vm, virReportError(VIR_ERR_INVALID_ARG, _("commit of '%s' active layer requires active flag"), disk->dst); - return -1; + return NULL; } } else if (flags & VIR_DOMAIN_BLOCK_COMMIT_ACTIVE) { virReportError(VIR_ERR_INVALID_ARG, _("active commit requested but '%s' is not active"), topSource->path); - return -1; + return NULL; } if (!virStorageSourceHasBacking(topSource)) { virReportError(VIR_ERR_INVALID_ARG, _("top '%s' in chain for '%s' has no backing file"), topSource->path, disk->src->path); - return -1; + return NULL; } if ((flags & VIR_DOMAIN_BLOCK_COMMIT_SHALLOW) && @@ -3279,33 +3283,33 @@ qemuBlockCommit(virDomainObj *vm, _("base '%s' is not immediately below '%s' in chain " "for '%s'"), baseSource->path, topSource->path, disk->src->path); - return -1; + return NULL; } /* For an active commit, clone enough of the base to act as the mirror */ if (topSource == disk->src) { if (!(mirror = virStorageSourceCopy(baseSource, false))) - return -1; + return NULL; if (virStorageSourceInitChainElement(mirror, disk->src, true) < 0) - return -1; + return NULL; } if (flags & VIR_DOMAIN_BLOCK_COMMIT_RELATIVE && topSource != disk->src) { if (top_parent && qemuBlockUpdateRelativeBacking(vm, top_parent, disk->src) < 0) - return -1; + return NULL; if (virStorageSourceGetRelativeBackingPath(topSource, baseSource, &backingPath) < 0) - return -1; + return NULL; if (!backingPath) { virReportError(VIR_ERR_OPERATION_INVALID, "%s", _("can't keep relative backing relationship")); - return -1; + return NULL; } } @@ -3367,7 +3371,7 @@ qemuBlockCommit(virDomainObj *vm, } qemuBlockJobStarted(job, vm); - return 0; + return job; error: if (clean_access) { @@ -3384,7 +3388,7 @@ qemuBlockCommit(virDomainObj *vm, } qemuBlockJobStartupFinalize(vm, job); - return -1; + return NULL; } diff --git a/src/qemu/qemu_block.h b/src/qemu/qemu_block.h index c169432d9c..a8079c2207 100644 --- a/src/qemu/qemu_block.h +++ b/src/qemu/qemu_block.h @@ -277,7 +277,7 @@ qemuBlockExportAddNBD(virDomainObj *vm, bool writable, const char *bitmap); -int +qemuBlockJobData * qemuBlockCommit(virDomainObj *vm, virDomainDiskDef *disk, virStorageSource *baseSource, diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 12fca22616..53533062e1 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -14999,6 +14999,7 @@ qemuDomainBlockCommit(virDomainPtr dom, virStorageSource *topSource; virStorageSource *baseSource = NULL; virStorageSource *top_parent = NULL; + g_autoptr(qemuBlockJobData) job = NULL; virCheckFlags(VIR_DOMAIN_BLOCK_COMMIT_SHALLOW | VIR_DOMAIN_BLOCK_COMMIT_ACTIVE | @@ -15030,9 +15031,11 @@ qemuDomainBlockCommit(virDomainPtr dom, base, disk->dst, NULL))) goto endjob; - ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, + job = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, bandwidth, VIR_ASYNC_JOB_NONE, VIR_TRISTATE_BOOL_YES, flags); + if (job) + ret = 0; endjob: virDomainObjEndJob(vm); -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:46 +0100, Pavel Hrdina wrote:
The created job will be needed by external snapshot delete code so rework qemuBlockCommit to return that pointer.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 42 +++++++++++++++++++++++------------------- src/qemu/qemu_block.h | 2 +- src/qemu/qemu_driver.c | 5 ++++- 3 files changed, 28 insertions(+), 21 deletions(-)
diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 24c82377b0..0a0d942e71 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3201,8 +3201,12 @@ qemuBlockExportAddNBD(virDomainObj *vm, /* The autofinalize argument controls if the qemu block job will be automatically * finalized. This is used when deleting external snapshots where we need to * disable automatic finalization for some use-case. The default value passed - * to this argument should be VIR_TRISTATE_BOOL_YES.*/ -int + * to this argument should be VIR_TRISTATE_BOOL_YES. + * + * Returns qemuBlockJobData pointer on success, NULL on error. Caller is responsible + * to call virObjectUnref on the pointer. + */
See, you are adding and adding to this but you didn't start off properly ;)
+qemuBlockJobData * qemuBlockCommit(virDomainObj *vm, virDomainDiskDef *disk, virStorageSource *baseSource,
[...]
@@ -3367,7 +3371,7 @@ qemuBlockCommit(virDomainObj *vm, } qemuBlockJobStarted(job, vm);
- return 0; + return job;
[...]
error: if (clean_access) {
[...]
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 12fca22616..53533062e1 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -14999,6 +14999,7 @@ qemuDomainBlockCommit(virDomainPtr dom, virStorageSource *topSource; virStorageSource *baseSource = NULL; virStorageSource *top_parent = NULL; + g_autoptr(qemuBlockJobData) job = NULL;
[2]
virCheckFlags(VIR_DOMAIN_BLOCK_COMMIT_SHALLOW | VIR_DOMAIN_BLOCK_COMMIT_ACTIVE | @@ -15030,9 +15031,11 @@ qemuDomainBlockCommit(virDomainPtr dom, base, disk->dst, NULL))) goto endjob;
- ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, + job = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, bandwidth, VIR_ASYNC_JOB_NONE, VIR_TRISTATE_BOOL_YES, flags); + if (job) + ret = 0;
endjob:
Okay so this somehow isn't adding up for me. Prior to this patch before you returned the job [1] there wasn't anything to unref it [2]. So how come it's now needed if you don't increase reference count? Looking back at the refactoring patches moving the code out I think I see the problem there. Either way qemuBlockJobStartupFinalize() is the proper function to call rather than automatically unref it.

On Tue, Dec 13, 2022 at 10:06:41 +0100, Peter Krempa wrote:
On Thu, Dec 08, 2022 at 14:30:46 +0100, Pavel Hrdina wrote:
The created job will be needed by external snapshot delete code so rework qemuBlockCommit to return that pointer.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 42 +++++++++++++++++++++++------------------- src/qemu/qemu_block.h | 2 +- src/qemu/qemu_driver.c | 5 ++++- 3 files changed, 28 insertions(+), 21 deletions(-)
diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 24c82377b0..0a0d942e71 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3201,8 +3201,12 @@ qemuBlockExportAddNBD(virDomainObj *vm, /* The autofinalize argument controls if the qemu block job will be automatically * finalized. This is used when deleting external snapshots where we need to * disable automatic finalization for some use-case. The default value passed - * to this argument should be VIR_TRISTATE_BOOL_YES.*/ -int + * to this argument should be VIR_TRISTATE_BOOL_YES. + * + * Returns qemuBlockJobData pointer on success, NULL on error. Caller is responsible + * to call virObjectUnref on the pointer. + */
See, you are adding and adding to this but you didn't start off properly ;)
+qemuBlockJobData * qemuBlockCommit(virDomainObj *vm, virDomainDiskDef *disk, virStorageSource *baseSource,
[...]
@@ -3367,7 +3371,7 @@ qemuBlockCommit(virDomainObj *vm, } qemuBlockJobStarted(job, vm);
- return 0; + return job;
[...]
error: if (clean_access) {
[...]
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 12fca22616..53533062e1 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -14999,6 +14999,7 @@ qemuDomainBlockCommit(virDomainPtr dom, virStorageSource *topSource; virStorageSource *baseSource = NULL; virStorageSource *top_parent = NULL; + g_autoptr(qemuBlockJobData) job = NULL;
[2]
virCheckFlags(VIR_DOMAIN_BLOCK_COMMIT_SHALLOW | VIR_DOMAIN_BLOCK_COMMIT_ACTIVE | @@ -15030,9 +15031,11 @@ qemuDomainBlockCommit(virDomainPtr dom, base, disk->dst, NULL))) goto endjob;
- ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, + job = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, bandwidth, VIR_ASYNC_JOB_NONE, VIR_TRISTATE_BOOL_YES, flags); + if (job) + ret = 0;
endjob:
Okay so this somehow isn't adding up for me. Prior to this patch before you returned the job [1] there wasn't anything to unref it [2]. So how come it's now needed if you don't increase reference count?
Looking back at the refactoring patches moving the code out I think I see the problem there.
Either way qemuBlockJobStartupFinalize() is the proper function to call rather than automatically unref it.
Once you fix patch 2/30 to call qemuBlockJobStartupFinalize in both cases, this patch will also need to call it directly to finalize the job once the pointer is handed over. And I mean explicitly calling qemuBlockJobStartupFinalize even in the success code path even when for now it just unrefs the object. With that change: Reviewed-by: Peter Krempa <pkrempa@redhat.com>

On Tue, Dec 13, 2022 at 10:11:36AM +0100, Peter Krempa wrote:
On Tue, Dec 13, 2022 at 10:06:41 +0100, Peter Krempa wrote:
On Thu, Dec 08, 2022 at 14:30:46 +0100, Pavel Hrdina wrote:
The created job will be needed by external snapshot delete code so rework qemuBlockCommit to return that pointer.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 42 +++++++++++++++++++++++------------------- src/qemu/qemu_block.h | 2 +- src/qemu/qemu_driver.c | 5 ++++- 3 files changed, 28 insertions(+), 21 deletions(-)
diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 24c82377b0..0a0d942e71 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3201,8 +3201,12 @@ qemuBlockExportAddNBD(virDomainObj *vm, /* The autofinalize argument controls if the qemu block job will be automatically * finalized. This is used when deleting external snapshots where we need to * disable automatic finalization for some use-case. The default value passed - * to this argument should be VIR_TRISTATE_BOOL_YES.*/ -int + * to this argument should be VIR_TRISTATE_BOOL_YES. + * + * Returns qemuBlockJobData pointer on success, NULL on error. Caller is responsible + * to call virObjectUnref on the pointer. + */
See, you are adding and adding to this but you didn't start off properly ;)
+qemuBlockJobData * qemuBlockCommit(virDomainObj *vm, virDomainDiskDef *disk, virStorageSource *baseSource,
[...]
@@ -3367,7 +3371,7 @@ qemuBlockCommit(virDomainObj *vm, } qemuBlockJobStarted(job, vm);
- return 0; + return job;
[...]
error: if (clean_access) {
[...]
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 12fca22616..53533062e1 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -14999,6 +14999,7 @@ qemuDomainBlockCommit(virDomainPtr dom, virStorageSource *topSource; virStorageSource *baseSource = NULL; virStorageSource *top_parent = NULL; + g_autoptr(qemuBlockJobData) job = NULL;
[2]
virCheckFlags(VIR_DOMAIN_BLOCK_COMMIT_SHALLOW | VIR_DOMAIN_BLOCK_COMMIT_ACTIVE | @@ -15030,9 +15031,11 @@ qemuDomainBlockCommit(virDomainPtr dom, base, disk->dst, NULL))) goto endjob;
- ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, + job = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, bandwidth, VIR_ASYNC_JOB_NONE, VIR_TRISTATE_BOOL_YES, flags); + if (job) + ret = 0;
endjob:
Okay so this somehow isn't adding up for me. Prior to this patch before you returned the job [1] there wasn't anything to unref it [2]. So how come it's now needed if you don't increase reference count?
Looking back at the refactoring patches moving the code out I think I see the problem there.
Either way qemuBlockJobStartupFinalize() is the proper function to call rather than automatically unref it.
Once you fix patch 2/30 to call qemuBlockJobStartupFinalize in both cases, this patch will also need to call it directly to finalize the job once the pointer is handed over. And I mean explicitly calling qemuBlockJobStartupFinalize even in the success code path even when for now it just unrefs the object.
Not sure if I understand this correctly. Once I fix patch 2/30 to always call qemuBlockJobStartupFinalize() and the same behavior remains here as well were we return the job pointer or NULL I should also cleanup the job pointer in the caller of qemuBlockCommit() with qemuBlockJobStartupFinalize()? If that understanding is correct it doesn't make sense to me. If we get a NULL there is no job so nothing to do. If we get a pointer it was already updated by qemuBlockJobStarted() so qemuBlockJobStartupFinalize() would only do the unref part and nothing else. If qemuBlockJobStartupFinalize() should be only used as cleanup for qemuBlockJobData we should make it that way. Pavel
With that change:
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

On Tue, Dec 13, 2022 at 18:12:38 +0100, Pavel Hrdina wrote:
On Tue, Dec 13, 2022 at 10:11:36AM +0100, Peter Krempa wrote:
On Tue, Dec 13, 2022 at 10:06:41 +0100, Peter Krempa wrote:
On Thu, Dec 08, 2022 at 14:30:46 +0100, Pavel Hrdina wrote:
The created job will be needed by external snapshot delete code so rework qemuBlockCommit to return that pointer.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 42 +++++++++++++++++++++++------------------- src/qemu/qemu_block.h | 2 +- src/qemu/qemu_driver.c | 5 ++++- 3 files changed, 28 insertions(+), 21 deletions(-)
diff --git a/src/qemu/qemu_block.c b/src/qemu/qemu_block.c index 24c82377b0..0a0d942e71 100644 --- a/src/qemu/qemu_block.c +++ b/src/qemu/qemu_block.c @@ -3201,8 +3201,12 @@ qemuBlockExportAddNBD(virDomainObj *vm, /* The autofinalize argument controls if the qemu block job will be automatically * finalized. This is used when deleting external snapshots where we need to * disable automatic finalization for some use-case. The default value passed - * to this argument should be VIR_TRISTATE_BOOL_YES.*/ -int + * to this argument should be VIR_TRISTATE_BOOL_YES. + * + * Returns qemuBlockJobData pointer on success, NULL on error. Caller is responsible + * to call virObjectUnref on the pointer. + */
See, you are adding and adding to this but you didn't start off properly ;)
+qemuBlockJobData * qemuBlockCommit(virDomainObj *vm, virDomainDiskDef *disk, virStorageSource *baseSource,
[...]
@@ -3367,7 +3371,7 @@ qemuBlockCommit(virDomainObj *vm, } qemuBlockJobStarted(job, vm);
- return 0; + return job;
[...]
error: if (clean_access) {
[...]
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 12fca22616..53533062e1 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -14999,6 +14999,7 @@ qemuDomainBlockCommit(virDomainPtr dom, virStorageSource *topSource; virStorageSource *baseSource = NULL; virStorageSource *top_parent = NULL; + g_autoptr(qemuBlockJobData) job = NULL;
[2]
virCheckFlags(VIR_DOMAIN_BLOCK_COMMIT_SHALLOW | VIR_DOMAIN_BLOCK_COMMIT_ACTIVE | @@ -15030,9 +15031,11 @@ qemuDomainBlockCommit(virDomainPtr dom, base, disk->dst, NULL))) goto endjob;
- ret = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, + job = qemuBlockCommit(vm, disk, baseSource, topSource, top_parent, bandwidth, VIR_ASYNC_JOB_NONE, VIR_TRISTATE_BOOL_YES, flags); + if (job) + ret = 0;
endjob:
Okay so this somehow isn't adding up for me. Prior to this patch before you returned the job [1] there wasn't anything to unref it [2]. So how come it's now needed if you don't increase reference count?
Looking back at the refactoring patches moving the code out I think I see the problem there.
Either way qemuBlockJobStartupFinalize() is the proper function to call rather than automatically unref it.
Once you fix patch 2/30 to call qemuBlockJobStartupFinalize in both cases, this patch will also need to call it directly to finalize the job once the pointer is handed over. And I mean explicitly calling qemuBlockJobStartupFinalize even in the success code path even when for now it just unrefs the object.
Not sure if I understand this correctly. Once I fix patch 2/30 to always call qemuBlockJobStartupFinalize() and the same behavior remains here as
well were we return the job pointer or NULL I should also cleanup the job pointer in the caller of qemuBlockCommit() with qemuBlockJobStartupFinalize()? If that understanding is correct it doesn't make sense to me.
If we get a NULL there is no job so nothing to do. If we get a pointer it was already updated by qemuBlockJobStarted() so qemuBlockJobStartupFinalize() would only do the unref part and nothing else. If qemuBlockJobStartupFinalize() should be only used as cleanup for qemuBlockJobData we should make it that way.
Thinking about it a bit more I think it's okay to use virObjectUnref without an explicit call to qemuBlockJobStartupFinalize() in cases where you are 100% sure that the job was started. Thus in the patch makes qemuBlockCommit() return the job pointer you will no longer call qemuBlockJobStartupFinalize() and the caller will have to unref the returned job. The same thing will need to happen here then. Originally I wanted to insist that all of the job cleanup goes through qemuBlockJobStartupFinalize() but that actually doesn't make much sense.

On Tue, Dec 13, 2022 at 18:28:44 +0100, Peter Krempa wrote:
On Tue, Dec 13, 2022 at 18:12:38 +0100, Pavel Hrdina wrote:
On Tue, Dec 13, 2022 at 10:11:36AM +0100, Peter Krempa wrote:
On Tue, Dec 13, 2022 at 10:06:41 +0100, Peter Krempa wrote:
On Thu, Dec 08, 2022 at 14:30:46 +0100, Pavel Hrdina wrote:
The created job will be needed by external snapshot delete code so rework qemuBlockCommit to return that pointer.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_block.c | 42 +++++++++++++++++++++++------------------- src/qemu/qemu_block.h | 2 +- src/qemu/qemu_driver.c | 5 ++++- 3 files changed, 28 insertions(+), 21 deletions(-)
[...]
Okay so this somehow isn't adding up for me. Prior to this patch before you returned the job [1] there wasn't anything to unref it [2]. So how come it's now needed if you don't increase reference count?
Looking back at the refactoring patches moving the code out I think I see the problem there.
Either way qemuBlockJobStartupFinalize() is the proper function to call rather than automatically unref it.
Once you fix patch 2/30 to call qemuBlockJobStartupFinalize in both cases, this patch will also need to call it directly to finalize the job once the pointer is handed over. And I mean explicitly calling qemuBlockJobStartupFinalize even in the success code path even when for now it just unrefs the object.
Not sure if I understand this correctly. Once I fix patch 2/30 to always call qemuBlockJobStartupFinalize() and the same behavior remains here as
well were we return the job pointer or NULL I should also cleanup the job pointer in the caller of qemuBlockCommit() with qemuBlockJobStartupFinalize()? If that understanding is correct it doesn't make sense to me.
If we get a NULL there is no job so nothing to do. If we get a pointer it was already updated by qemuBlockJobStarted() so qemuBlockJobStartupFinalize() would only do the unref part and nothing else. If qemuBlockJobStartupFinalize() should be only used as cleanup for qemuBlockJobData we should make it that way.
Thinking about it a bit more I think it's okay to use virObjectUnref without an explicit call to qemuBlockJobStartupFinalize() in cases where you are 100% sure that the job was started.
Thus in the patch makes qemuBlockCommit() return the job pointer you will no longer call qemuBlockJobStartupFinalize() and the caller will have to unref the returned job.
The same thing will need to happen here then.
Originally I wanted to insist that all of the job cleanup goes through qemuBlockJobStartupFinalize() but that actually doesn't make much sense.
I wrote the above 3 paragraphs believing this is patch 24/30 where you leak the reference from the snapshot code. Thus in 2/30 you need to add qemuBlockJobStartupFinalize or virObjectUnref on the success code path. In this patch you need to remove it. The caller is okay after this patch as it uses automatic pointer. As said using qemuBlockJobStartupFinalize in the caller is not required if you know the job was started, which is guaranteed after this patch.

QEMU emits this signal when autofinalize is disabled and QEMU is waiting for the caller to start the finalization manually. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_backup.c | 1 + src/qemu/qemu_blockjob.c | 20 +++++++++++++++++++- src/qemu/qemu_blockjob.h | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/qemu/qemu_backup.c b/src/qemu/qemu_backup.c index c7721812a5..6a5aacd9f6 100644 --- a/src/qemu/qemu_backup.c +++ b/src/qemu/qemu_backup.c @@ -1035,6 +1035,7 @@ qemuBackupNotifyBlockjobEnd(virDomainObj *vm, case QEMU_BLOCKJOB_STATE_NEW: case QEMU_BLOCKJOB_STATE_RUNNING: case QEMU_BLOCKJOB_STATE_ABORTING: + case QEMU_BLOCKJOB_STATE_PENDING: case QEMU_BLOCKJOB_STATE_PIVOTING: case QEMU_BLOCKJOB_STATE_LAST: default: diff --git a/src/qemu/qemu_blockjob.c b/src/qemu/qemu_blockjob.c index a7aa7b3940..71dddc256c 100644 --- a/src/qemu/qemu_blockjob.c +++ b/src/qemu/qemu_blockjob.c @@ -55,6 +55,7 @@ VIR_ENUM_IMPL(qemuBlockjobState, "running", "concluded", "aborting", + "pending", "pivoting"); VIR_ENUM_IMPL(qemuBlockjob, @@ -531,6 +532,8 @@ qemuBlockJobRefreshJobs(virDomainObj *vm) if (job->state == QEMU_BLOCKJOB_STATE_NEW || job->state == QEMU_BLOCKJOB_STATE_RUNNING) job->newstate = newstate; + } else if (newstate == QEMU_BLOCKJOB_STATE_PENDING) { + job->newstate = newstate; } /* don't update the job otherwise */ } @@ -1563,6 +1566,18 @@ qemuBlockJobEventProcess(virQEMUDriver *driver, job->newstate = -1; break; + case QEMU_BLOCKJOB_STATE_PENDING: + /* similarly as for 'ready' state we should handle it only when + * previous state was 'new' or 'running' as when aborting job it can + * reset the internally set job state */ + if (job->state == QEMU_BLOCKJOB_STATE_NEW || + job->state == QEMU_BLOCKJOB_STATE_RUNNING) { + job->state = job->newstate; + qemuDomainSaveStatus(vm); + } + job->newstate = -1; + break; + case QEMU_BLOCKJOB_STATE_NEW: case QEMU_BLOCKJOB_STATE_RUNNING: case QEMU_BLOCKJOB_STATE_LAST: @@ -1684,13 +1699,16 @@ qemuBlockjobConvertMonitorStatus(int monitorstatus) ret = QEMU_BLOCKJOB_STATE_CONCLUDED; break; + case QEMU_MONITOR_JOB_STATUS_PENDING: + ret = QEMU_BLOCKJOB_STATE_PENDING; + break; + case QEMU_MONITOR_JOB_STATUS_UNKNOWN: case QEMU_MONITOR_JOB_STATUS_CREATED: case QEMU_MONITOR_JOB_STATUS_RUNNING: case QEMU_MONITOR_JOB_STATUS_PAUSED: case QEMU_MONITOR_JOB_STATUS_STANDBY: case QEMU_MONITOR_JOB_STATUS_WAITING: - case QEMU_MONITOR_JOB_STATUS_PENDING: case QEMU_MONITOR_JOB_STATUS_ABORTING: case QEMU_MONITOR_JOB_STATUS_UNDEFINED: case QEMU_MONITOR_JOB_STATUS_NULL: diff --git a/src/qemu/qemu_blockjob.h b/src/qemu/qemu_blockjob.h index 741d8df6c5..e9b283da20 100644 --- a/src/qemu/qemu_blockjob.h +++ b/src/qemu/qemu_blockjob.h @@ -41,6 +41,7 @@ typedef enum { QEMU_BLOCKJOB_STATE_CONCLUDED, /* job has finished, but it's unknown whether it has failed or not */ QEMU_BLOCKJOB_STATE_ABORTING, + QEMU_BLOCKJOB_STATE_PENDING, QEMU_BLOCKJOB_STATE_PIVOTING, QEMU_BLOCKJOB_STATE_LAST } qemuBlockjobState; -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:47 +0100, Pavel Hrdina wrote:
QEMU emits this signal when autofinalize is disabled and QEMU is waiting for the caller to start the finalization manually.
Note that this is not true. The block job transitions through the pending state also when started with auto-finalization requested.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_backup.c | 1 + src/qemu/qemu_blockjob.c | 20 +++++++++++++++++++- src/qemu/qemu_blockjob.h | 1 + 3 files changed, 21 insertions(+), 1 deletion(-)
[...]
@@ -1563,6 +1566,18 @@ qemuBlockJobEventProcess(virQEMUDriver *driver, job->newstate = -1; break;
+ case QEMU_BLOCKJOB_STATE_PENDING: + /* similarly as for 'ready' state we should handle it only when + * previous state was 'new' or 'running' as when aborting job it can + * reset the internally set job state */
Well this assumption will not work for active layer block commit, which also transitions through the _READY state. Since you are modifying block commit that might be relevant. Let's see how it will be used. If it isn't to be used with active layer block commit you should most likely mention that in the comment for the block commit code or disallow it altogether to prevent surprises for others wanting to use it in the future.
+ if (job->state == QEMU_BLOCKJOB_STATE_NEW || + job->state == QEMU_BLOCKJOB_STATE_RUNNING) { + job->state = job->newstate; + qemuDomainSaveStatus(vm); + } + job->newstate = -1;

Move code around to make it clear what is called when deleting single snapshot or children snapshots. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> Reviewed-by: Peter Krempa <pkrempa@redhat.com> --- src/qemu/qemu_snapshot.c | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index d7983c134f..e9bb5abee2 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2350,25 +2350,28 @@ qemuSnapshotDelete(virDomainObj *vm, } } } - } else if (snap->nchildren) { - rep.dir = cfg->snapshotDir; - rep.parent = snap->parent; - rep.vm = vm; - rep.err = 0; - rep.xmlopt = driver->xmlopt; - rep.writeMetadata = qemuDomainSnapshotWriteMetadata; - virDomainMomentForEachChild(snap, - qemuSnapshotChildrenReparent, - &rep); - if (rep.err < 0) - goto endjob; - virDomainMomentMoveChildren(snap, snap->parent); - } - if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) { - virDomainMomentDropChildren(snap); - ret = 0; + if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) { + virDomainMomentDropChildren(snap); + ret = 0; + } else { + ret = qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); + } } else { + if (snap->nchildren) { + rep.dir = cfg->snapshotDir; + rep.parent = snap->parent; + rep.vm = vm; + rep.err = 0; + rep.xmlopt = driver->xmlopt; + rep.writeMetadata = qemuDomainSnapshotWriteMetadata; + virDomainMomentForEachChild(snap, + qemuSnapshotChildrenReparent, + &rep); + if (rep.err < 0) + goto endjob; + virDomainMomentMoveChildren(snap, snap->parent); + } ret = qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); } -- 2.38.1

Extract code that deletes single snapshot to separate function. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 47 ++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index e9bb5abee2..47239e9e9c 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2281,6 +2281,36 @@ qemuSnapshotChildrenReparent(void *payload, } +static int +qemuSnapshotDeleteSingle(virDomainObj *vm, + virDomainMomentObj *snap, + bool metadata_only) +{ + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + + if (snap->nchildren) { + virQEMUMomentReparent rep; + + rep.dir = cfg->snapshotDir; + rep.parent = snap->parent; + rep.vm = vm; + rep.err = 0; + rep.xmlopt = driver->xmlopt; + rep.writeMetadata = qemuDomainSnapshotWriteMetadata; + virDomainMomentForEachChild(snap, + qemuSnapshotChildrenReparent, + &rep); + if (rep.err < 0) + return -1; + virDomainMomentMoveChildren(snap, snap->parent); + } + + return qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); +} + + int qemuSnapshotDelete(virDomainObj *vm, virDomainSnapshotPtr snapshot, @@ -2290,7 +2320,6 @@ qemuSnapshotDelete(virDomainObj *vm, int ret = -1; virDomainMomentObj *snap = NULL; virQEMUMomentRemove rem; - virQEMUMomentReparent rep; bool metadata_only = !!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY); int external = 0; g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); @@ -2358,21 +2387,7 @@ qemuSnapshotDelete(virDomainObj *vm, ret = qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); } } else { - if (snap->nchildren) { - rep.dir = cfg->snapshotDir; - rep.parent = snap->parent; - rep.vm = vm; - rep.err = 0; - rep.xmlopt = driver->xmlopt; - rep.writeMetadata = qemuDomainSnapshotWriteMetadata; - virDomainMomentForEachChild(snap, - qemuSnapshotChildrenReparent, - &rep); - if (rep.err < 0) - goto endjob; - virDomainMomentMoveChildren(snap, snap->parent); - } - ret = qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); + ret = qemuSnapshotDeleteSingle(vm, snap, metadata_only); } endjob: -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:49 +0100, Pavel Hrdina wrote:
Extract code that deletes single snapshot to separate function.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 47 ++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Extract code that deletes children of specific snapshot to separate function. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 108 ++++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 47239e9e9c..3d5467457b 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2311,18 +2311,76 @@ qemuSnapshotDeleteSingle(virDomainObj *vm, } -int -qemuSnapshotDelete(virDomainObj *vm, - virDomainSnapshotPtr snapshot, - unsigned int flags) +/** + * qemuSnapshotDeleteChildren: + * @vm: domain object + * @snap: snapshot object + * @metadata_only: boolean + * @flags: bitwise-OR of virDomainSnapshotDeleteFlags + * + * Delete children snapshots of snapshot proved by @snap. Based on what @flags + * are provided it will delete children snapshots including @snap or only + * children snapshots. If @metadata_only is true only libvirt metadata files + * are deleted but the actual snapshots are left intact. + * + * Returns 0 on success, -1 on error. + */ +static int +qemuSnapshotDeleteChildren(virDomainObj *vm, + virDomainMomentObj *snap, + bool metadata_only, + unsigned int flags) { - virQEMUDriver *driver = snapshot->domain->conn->privateData; - int ret = -1; - virDomainMomentObj *snap = NULL; virQEMUMomentRemove rem; + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + + rem.driver = driver; + rem.vm = vm; + rem.metadata_only = metadata_only; + rem.err = 0; + rem.current = virDomainSnapshotGetCurrent(vm->snapshots); + rem.found = false; + rem.momentDiscard = qemuDomainSnapshotDiscard; + virDomainMomentForEachDescendant(snap, qemuDomainMomentDiscardAll, + &rem); + if (rem.err < 0) + return -1; + if (rem.found) { + qemuSnapshotSetCurrent(vm, snap); + + if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) { + if (qemuDomainSnapshotWriteMetadata(vm, snap, + driver->xmlopt, + cfg->snapshotDir) < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, + _("failed to set snapshot '%s' as current"), + snap->def->name); + virDomainSnapshotSetCurrent(vm->snapshots, NULL); + return -1; + } + } + } + + if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) { + virDomainMomentDropChildren(snap); + return 0; + } + + return qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); +} + + +int +qemuSnapshotDelete(virDomainObj *vm, + virDomainSnapshotPtr snapshot, + unsigned int flags) +{ + int ret = -1; + virDomainMomentObj *snap = NULL; bool metadata_only = !!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY); int external = 0; - g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); virCheckFlags(VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY | @@ -2353,39 +2411,7 @@ qemuSnapshotDelete(virDomainObj *vm, if (flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) { - rem.driver = driver; - rem.vm = vm; - rem.metadata_only = metadata_only; - rem.err = 0; - rem.current = virDomainSnapshotGetCurrent(vm->snapshots); - rem.found = false; - rem.momentDiscard = qemuDomainSnapshotDiscard; - virDomainMomentForEachDescendant(snap, qemuDomainMomentDiscardAll, - &rem); - if (rem.err < 0) - goto endjob; - if (rem.found) { - qemuSnapshotSetCurrent(vm, snap); - - if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) { - if (qemuDomainSnapshotWriteMetadata(vm, snap, - driver->xmlopt, - cfg->snapshotDir) < 0) { - virReportError(VIR_ERR_INTERNAL_ERROR, - _("failed to set snapshot '%s' as current"), - snap->def->name); - virDomainSnapshotSetCurrent(vm->snapshots, NULL); - goto endjob; - } - } - } - - if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) { - virDomainMomentDropChildren(snap); - ret = 0; - } else { - ret = qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); - } + ret = qemuSnapshotDeleteChildren(vm, snap, metadata_only, flags); } else { ret = qemuSnapshotDeleteSingle(vm, snap, metadata_only); } -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:50 +0100, Pavel Hrdina wrote:
Extract code that deletes children of specific snapshot to separate function.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 108 ++++++++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 41 deletions(-)
diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 47239e9e9c..3d5467457b 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2311,18 +2311,76 @@ qemuSnapshotDeleteSingle(virDomainObj *vm, }
-int -qemuSnapshotDelete(virDomainObj *vm, - virDomainSnapshotPtr snapshot, - unsigned int flags) +/** + * qemuSnapshotDeleteChildren: + * @vm: domain object + * @snap: snapshot object + * @metadata_only: boolean
'boolean' is quite obvious from the function prototype, but it's not that obvious what it actually means. @metadata_only: If true, delete only snapshot metadata, leave data intact.
+ * @flags: bitwise-OR of virDomainSnapshotDeleteFlags
In fact the only thing you use from @flags is VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY. Additionally you already pass 'metadata_only' as boolean which is originally also part of 'flags. Preferrably extract VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY as a boolean too and pass it to this function instead of flags.
+ * + * Delete children snapshots of snapshot proved by @snap. Based on what @flags
s/proved/provided/ ?
+ * are provided it will delete children snapshots including @snap or only + * children snapshots. If @metadata_only is true only libvirt metadata files + * are deleted but the actual snapshots are left intact. + * + * Returns 0 on success, -1 on error. + */ +static int +qemuSnapshotDeleteChildren(virDomainObj *vm, + virDomainMomentObj *snap, + bool metadata_only, + unsigned int flags)
With the above: Reviewed-by: Peter Krempa <pkrempa@redhat.com>

This simplifies the code a bit by reusing existing parts that deletes a single snapshot. The drawback of this change is that we will now call the re-parent bits to keep the metadata in sync for every child even though it will get deleted as well. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 69 +++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 3d5467457b..846f6e9e53 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2311,6 +2311,31 @@ qemuSnapshotDeleteSingle(virDomainObj *vm, } +struct qemuSnapshotDeleteAllData { + virDomainObj *vm; + bool metadata_only; + int error; +}; + + +static int +qemuSnapshotDeleteAll(void *payload, + const char *name G_GNUC_UNUSED, + void *opaque) +{ + int error; + virDomainMomentObj *snap = payload; + struct qemuSnapshotDeleteAllData *data = opaque; + + error = qemuSnapshotDeleteSingle(data->vm, snap, data->metadata_only); + + if (error != 0) + data->error = error; + + return 0; +} + + /** * qemuSnapshotDeleteChildren: * @vm: domain object @@ -2331,44 +2356,22 @@ qemuSnapshotDeleteChildren(virDomainObj *vm, bool metadata_only, unsigned int flags) { - virQEMUMomentRemove rem; - qemuDomainObjPrivate *priv = vm->privateData; - virQEMUDriver *driver = priv->driver; - g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + struct qemuSnapshotDeleteAllData data = { + .vm = vm, + .metadata_only = metadata_only, + }; - rem.driver = driver; - rem.vm = vm; - rem.metadata_only = metadata_only; - rem.err = 0; - rem.current = virDomainSnapshotGetCurrent(vm->snapshots); - rem.found = false; - rem.momentDiscard = qemuDomainSnapshotDiscard; - virDomainMomentForEachDescendant(snap, qemuDomainMomentDiscardAll, - &rem); - if (rem.err < 0) - return -1; - if (rem.found) { - qemuSnapshotSetCurrent(vm, snap); + virDomainMomentForEachDescendant(snap, qemuSnapshotDeleteAll, &data); - if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) { - if (qemuDomainSnapshotWriteMetadata(vm, snap, - driver->xmlopt, - cfg->snapshotDir) < 0) { - virReportError(VIR_ERR_INTERNAL_ERROR, - _("failed to set snapshot '%s' as current"), - snap->def->name); - virDomainSnapshotSetCurrent(vm->snapshots, NULL); - return -1; - } - } - } + if (data.error < 0) + return -1; - if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) { - virDomainMomentDropChildren(snap); - return 0; + if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN && + qemuSnapshotDeleteSingle(vm, snap, metadata_only) < 0) { + return -1; } - return qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); + return 0; } -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:51 +0100, Pavel Hrdina wrote:
This simplifies the code a bit by reusing existing parts that deletes a single snapshot.
The drawback of this change is that we will now call the re-parent bits to keep the metadata in sync for every child even though it will get deleted as well.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 69 +++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 33 deletions(-)
diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 3d5467457b..846f6e9e53 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2311,6 +2311,31 @@ qemuSnapshotDeleteSingle(virDomainObj *vm, }
+struct qemuSnapshotDeleteAllData { + virDomainObj *vm; + bool metadata_only; + int error; +}; + + +static int +qemuSnapshotDeleteAll(void *payload,
Since this itself doesn't delete all snapshots please call it e.g: qemuSnapshotDeleteAllHelper Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Signed-off-by: Pavel Hrdina <phrdina@redhat.com> Reviewed-by: Peter Krempa <pkrempa@redhat.com> --- src/qemu/qemu_domain.c | 95 +-------------------------------------- src/qemu/qemu_domain.h | 9 ---- src/qemu/qemu_driver.c | 2 +- src/qemu/qemu_snapshot.c | 96 +++++++++++++++++++++++++++++++++++++++- src/qemu/qemu_snapshot.h | 4 ++ 5 files changed, 102 insertions(+), 104 deletions(-) diff --git a/src/qemu/qemu_domain.c b/src/qemu/qemu_domain.c index 8892f28fce..a6bb4a01df 100644 --- a/src/qemu/qemu_domain.c +++ b/src/qemu/qemu_domain.c @@ -31,6 +31,7 @@ #include "qemu_migration_params.h" #include "qemu_security.h" #include "qemu_slirp.h" +#include "qemu_snapshot.h" #include "qemu_extdevice.h" #include "qemu_blockjob.h" #include "qemu_checkpoint.h" @@ -7139,81 +7140,6 @@ qemuDomainSnapshotForEachQcow2(virQEMUDriver *driver, op, try_all, def->ndisks); } -/* Discard one snapshot (or its metadata), without reparenting any children. */ -int -qemuDomainSnapshotDiscard(virQEMUDriver *driver, - virDomainObj *vm, - virDomainMomentObj *snap, - bool update_parent, - bool metadata_only) -{ - g_autofree char *snapFile = NULL; - qemuDomainObjPrivate *priv; - virDomainMomentObj *parentsnap = NULL; - g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); - - if (!metadata_only) { - if (!virDomainObjIsActive(vm)) { - size_t i; - /* Ignore any skipped disks */ - - /* Prefer action on the disks in use at the time the snapshot was - * created; but fall back to current definition if dealing with a - * snapshot created prior to libvirt 0.9.5. */ - virDomainDef *def = snap->def->dom; - - if (!def) - def = vm->def; - - for (i = 0; i < def->ndisks; i++) { - if (virDomainDiskTranslateSourcePool(def->disks[i]) < 0) - return -1; - } - - if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) - return -1; - } else { - priv = vm->privateData; - qemuDomainObjEnterMonitor(vm); - /* we continue on even in the face of error */ - qemuMonitorDeleteSnapshot(priv->mon, snap->def->name); - qemuDomainObjExitMonitor(vm); - } - } - - snapFile = g_strdup_printf("%s/%s/%s.xml", cfg->snapshotDir, vm->def->name, - snap->def->name); - - if (snap == virDomainSnapshotGetCurrent(vm->snapshots)) { - virDomainSnapshotSetCurrent(vm->snapshots, NULL); - if (update_parent && snap->def->parent_name) { - parentsnap = virDomainSnapshotFindByName(vm->snapshots, - snap->def->parent_name); - if (!parentsnap) { - VIR_WARN("missing parent snapshot matching name '%s'", - snap->def->parent_name); - } else { - virDomainSnapshotSetCurrent(vm->snapshots, parentsnap); - if (qemuDomainSnapshotWriteMetadata(vm, parentsnap, - driver->xmlopt, - cfg->snapshotDir) < 0) { - VIR_WARN("failed to set parent snapshot '%s' as current", - snap->def->parent_name); - virDomainSnapshotSetCurrent(vm->snapshots, NULL); - } - } - } - } - - if (unlink(snapFile) < 0) - VIR_WARN("Failed to unlink %s", snapFile); - if (update_parent) - virDomainMomentDropParent(snap); - virDomainSnapshotObjListRemove(vm->snapshots, snap); - - return 0; -} - /* Hash iterator callback to discard multiple snapshots. */ int qemuDomainMomentDiscardAll(void *payload, const char *name G_GNUC_UNUSED, @@ -7232,23 +7158,6 @@ int qemuDomainMomentDiscardAll(void *payload, return 0; } -int -qemuDomainSnapshotDiscardAllMetadata(virQEMUDriver *driver, - virDomainObj *vm) -{ - virQEMUMomentRemove rem = { - .driver = driver, - .vm = vm, - .metadata_only = true, - .momentDiscard = qemuDomainSnapshotDiscard, - }; - - virDomainSnapshotForEach(vm->snapshots, qemuDomainMomentDiscardAll, &rem); - virDomainSnapshotObjListRemoveAll(vm->snapshots); - - return rem.err; -} - static void qemuDomainRemoveInactiveCommon(virQEMUDriver *driver, @@ -7261,7 +7170,7 @@ qemuDomainRemoveInactiveCommon(virQEMUDriver *driver, g_autofree char *chkDir = NULL; /* Remove any snapshot metadata prior to removing the domain */ - if (qemuDomainSnapshotDiscardAllMetadata(driver, vm) < 0) { + if (qemuSnapshotDiscardAllMetadata(driver, vm) < 0) { VIR_WARN("unable to remove all snapshots for domain %s", vm->def->name); } else { diff --git a/src/qemu/qemu_domain.h b/src/qemu/qemu_domain.h index 2f027fad87..e34d52c033 100644 --- a/src/qemu/qemu_domain.h +++ b/src/qemu/qemu_domain.h @@ -681,12 +681,6 @@ int qemuDomainSnapshotForEachQcow2(virQEMUDriver *driver, const char *op, bool try_all); -int qemuDomainSnapshotDiscard(virQEMUDriver *driver, - virDomainObj *vm, - virDomainMomentObj *snap, - bool update_current, - bool metadata_only); - typedef struct _virQEMUMomentRemove virQEMUMomentRemove; struct _virQEMUMomentRemove { virQEMUDriver *driver; @@ -703,9 +697,6 @@ int qemuDomainMomentDiscardAll(void *payload, const char *name, void *data); -int qemuDomainSnapshotDiscardAllMetadata(virQEMUDriver *driver, - virDomainObj *vm); - void qemuDomainRemoveInactive(virQEMUDriver *driver, virDomainObj *vm, virDomainUndefineFlagsValues flags, diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 53533062e1..4bd052c4da 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -6526,7 +6526,7 @@ qemuDomainUndefineFlags(virDomainPtr dom, nsnapshots); goto endjob; } - if (qemuDomainSnapshotDiscardAllMetadata(driver, vm) < 0) + if (qemuSnapshotDiscardAllMetadata(driver, vm) < 0) goto endjob; } if (!virDomainObjIsActive(vm) && diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 846f6e9e53..8754022aa4 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2281,6 +2281,100 @@ qemuSnapshotChildrenReparent(void *payload, } +/* Discard one snapshot (or its metadata), without reparenting any children. */ +static int +qemuSnapshotDiscard(virQEMUDriver *driver, + virDomainObj *vm, + virDomainMomentObj *snap, + bool update_parent, + bool metadata_only) +{ + g_autofree char *snapFile = NULL; + qemuDomainObjPrivate *priv; + virDomainMomentObj *parentsnap = NULL; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + + if (!metadata_only) { + if (!virDomainObjIsActive(vm)) { + size_t i; + /* Ignore any skipped disks */ + + /* Prefer action on the disks in use at the time the snapshot was + * created; but fall back to current definition if dealing with a + * snapshot created prior to libvirt 0.9.5. */ + virDomainDef *def = snap->def->dom; + + if (!def) + def = vm->def; + + for (i = 0; i < def->ndisks; i++) { + if (virDomainDiskTranslateSourcePool(def->disks[i]) < 0) + return -1; + } + + if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) + return -1; + } else { + priv = vm->privateData; + qemuDomainObjEnterMonitor(vm); + /* we continue on even in the face of error */ + qemuMonitorDeleteSnapshot(priv->mon, snap->def->name); + qemuDomainObjExitMonitor(vm); + } + } + + snapFile = g_strdup_printf("%s/%s/%s.xml", cfg->snapshotDir, vm->def->name, + snap->def->name); + + if (snap == virDomainSnapshotGetCurrent(vm->snapshots)) { + virDomainSnapshotSetCurrent(vm->snapshots, NULL); + if (update_parent && snap->def->parent_name) { + parentsnap = virDomainSnapshotFindByName(vm->snapshots, + snap->def->parent_name); + if (!parentsnap) { + VIR_WARN("missing parent snapshot matching name '%s'", + snap->def->parent_name); + } else { + virDomainSnapshotSetCurrent(vm->snapshots, parentsnap); + if (qemuDomainSnapshotWriteMetadata(vm, parentsnap, + driver->xmlopt, + cfg->snapshotDir) < 0) { + VIR_WARN("failed to set parent snapshot '%s' as current", + snap->def->parent_name); + virDomainSnapshotSetCurrent(vm->snapshots, NULL); + } + } + } + } + + if (unlink(snapFile) < 0) + VIR_WARN("Failed to unlink %s", snapFile); + if (update_parent) + virDomainMomentDropParent(snap); + virDomainSnapshotObjListRemove(vm->snapshots, snap); + + return 0; +} + + +int +qemuSnapshotDiscardAllMetadata(virQEMUDriver *driver, + virDomainObj *vm) +{ + virQEMUMomentRemove rem = { + .driver = driver, + .vm = vm, + .metadata_only = true, + .momentDiscard = qemuSnapshotDiscard, + }; + + virDomainSnapshotForEach(vm->snapshots, qemuDomainMomentDiscardAll, &rem); + virDomainSnapshotObjListRemoveAll(vm->snapshots); + + return rem.err; +} + + static int qemuSnapshotDeleteSingle(virDomainObj *vm, virDomainMomentObj *snap, @@ -2307,7 +2401,7 @@ qemuSnapshotDeleteSingle(virDomainObj *vm, virDomainMomentMoveChildren(snap, snap->parent); } - return qemuDomainSnapshotDiscard(driver, vm, snap, true, metadata_only); + return qemuSnapshotDiscard(driver, vm, snap, true, metadata_only); } diff --git a/src/qemu/qemu_snapshot.h b/src/qemu/qemu_snapshot.h index f69c05f68c..38437a2fd7 100644 --- a/src/qemu/qemu_snapshot.h +++ b/src/qemu/qemu_snapshot.h @@ -49,6 +49,10 @@ qemuSnapshotRevert(virDomainObj *vm, virDomainSnapshotPtr snapshot, unsigned int flags); +int +qemuSnapshotDiscardAllMetadata(virQEMUDriver *driver, + virDomainObj *vm); + int qemuSnapshotDelete(virDomainObj *vm, virDomainSnapshotPtr snapshot, -- 2.38.1

Previously the reparent happened before the actual snapshot deletion. This change moves the code closer to the rest of the code handling snapshot metadata when deletion happens. Following patch will extract it into separate function Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 8754022aa4..5418496c1e 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2323,6 +2323,25 @@ qemuSnapshotDiscard(virQEMUDriver *driver, } } + if (update_parent) { + if (snap->nchildren) { + virQEMUMomentReparent rep; + + rep.dir = cfg->snapshotDir; + rep.parent = snap->parent; + rep.vm = vm; + rep.err = 0; + rep.xmlopt = driver->xmlopt; + rep.writeMetadata = qemuDomainSnapshotWriteMetadata; + virDomainMomentForEachChild(snap, + qemuSnapshotChildrenReparent, + &rep); + if (rep.err < 0) + return -1; + virDomainMomentMoveChildren(snap, snap->parent); + } + } + snapFile = g_strdup_printf("%s/%s/%s.xml", cfg->snapshotDir, vm->def->name, snap->def->name); @@ -2382,24 +2401,6 @@ qemuSnapshotDeleteSingle(virDomainObj *vm, { qemuDomainObjPrivate *priv = vm->privateData; virQEMUDriver *driver = priv->driver; - g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); - - if (snap->nchildren) { - virQEMUMomentReparent rep; - - rep.dir = cfg->snapshotDir; - rep.parent = snap->parent; - rep.vm = vm; - rep.err = 0; - rep.xmlopt = driver->xmlopt; - rep.writeMetadata = qemuDomainSnapshotWriteMetadata; - virDomainMomentForEachChild(snap, - qemuSnapshotChildrenReparent, - &rep); - if (rep.err < 0) - return -1; - virDomainMomentMoveChildren(snap, snap->parent); - } return qemuSnapshotDiscard(driver, vm, snap, true, metadata_only); } -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:53 +0100, Pavel Hrdina wrote:
Previously the reparent happened before the actual snapshot deletion. This change moves the code closer to the rest of the code handling snapshot metadata when deletion happens.
Also it's now called after the deletion of the on-disk state rather than before, but that makes actually sense.
Following patch will extract it into separate function
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Extract the code deleting external snapshot metadata to separate function. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 91 +++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 5418496c1e..4ea3f578e9 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2281,47 +2281,15 @@ qemuSnapshotChildrenReparent(void *payload, } -/* Discard one snapshot (or its metadata), without reparenting any children. */ static int -qemuSnapshotDiscard(virQEMUDriver *driver, - virDomainObj *vm, - virDomainMomentObj *snap, - bool update_parent, - bool metadata_only) +qemuSnapshotDiscardMetadata(virDomainObj *vm, + virDomainMomentObj *snap, + bool update_parent) { + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); g_autofree char *snapFile = NULL; - qemuDomainObjPrivate *priv; - virDomainMomentObj *parentsnap = NULL; - g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); - - if (!metadata_only) { - if (!virDomainObjIsActive(vm)) { - size_t i; - /* Ignore any skipped disks */ - - /* Prefer action on the disks in use at the time the snapshot was - * created; but fall back to current definition if dealing with a - * snapshot created prior to libvirt 0.9.5. */ - virDomainDef *def = snap->def->dom; - - if (!def) - def = vm->def; - - for (i = 0; i < def->ndisks; i++) { - if (virDomainDiskTranslateSourcePool(def->disks[i]) < 0) - return -1; - } - - if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) - return -1; - } else { - priv = vm->privateData; - qemuDomainObjEnterMonitor(vm); - /* we continue on even in the face of error */ - qemuMonitorDeleteSnapshot(priv->mon, snap->def->name); - qemuDomainObjExitMonitor(vm); - } - } if (update_parent) { if (snap->nchildren) { @@ -2348,6 +2316,7 @@ qemuSnapshotDiscard(virQEMUDriver *driver, if (snap == virDomainSnapshotGetCurrent(vm->snapshots)) { virDomainSnapshotSetCurrent(vm->snapshots, NULL); if (update_parent && snap->def->parent_name) { + virDomainMomentObj *parentsnap = NULL; parentsnap = virDomainSnapshotFindByName(vm->snapshots, snap->def->parent_name); if (!parentsnap) { @@ -2376,6 +2345,52 @@ qemuSnapshotDiscard(virQEMUDriver *driver, } +/* Discard one snapshot (or its metadata), without reparenting any children. */ +static int +qemuSnapshotDiscard(virQEMUDriver *driver, + virDomainObj *vm, + virDomainMomentObj *snap, + bool update_parent, + bool metadata_only) +{ + qemuDomainObjPrivate *priv; + + if (!metadata_only) { + if (!virDomainObjIsActive(vm)) { + size_t i; + /* Ignore any skipped disks */ + + /* Prefer action on the disks in use at the time the snapshot was + * created; but fall back to current definition if dealing with a + * snapshot created prior to libvirt 0.9.5. */ + virDomainDef *def = snap->def->dom; + + if (!def) + def = vm->def; + + for (i = 0; i < def->ndisks; i++) { + if (virDomainDiskTranslateSourcePool(def->disks[i]) < 0) + return -1; + } + + if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) + return -1; + } else { + priv = vm->privateData; + qemuDomainObjEnterMonitor(vm); + /* we continue on even in the face of error */ + qemuMonitorDeleteSnapshot(priv->mon, snap->def->name); + qemuDomainObjExitMonitor(vm); + } + } + + if (qemuSnapshotDiscardMetadata(vm, snap, update_parent) < 0) + return -1; + + return 0; +} + + int qemuSnapshotDiscardAllMetadata(virQEMUDriver *driver, virDomainObj *vm) -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:54 +0100, Pavel Hrdina wrote:
Extract the code deleting external snapshot metadata to separate function.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 91 +++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 38 deletions(-)
diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 5418496c1e..4ea3f578e9 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2281,47 +2281,15 @@ qemuSnapshotChildrenReparent(void *payload, }
-/* Discard one snapshot (or its metadata), without reparenting any children. */ static int -qemuSnapshotDiscard(virQEMUDriver *driver, - virDomainObj *vm, - virDomainMomentObj *snap, - bool update_parent, - bool metadata_only) +qemuSnapshotDiscardMetadata(virDomainObj *vm, + virDomainMomentObj *snap, + bool update_parent) { + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); g_autofree char *snapFile = NULL; - qemuDomainObjPrivate *priv; - virDomainMomentObj *parentsnap = NULL; - g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); - - if (!metadata_only) { - if (!virDomainObjIsActive(vm)) { - size_t i; - /* Ignore any skipped disks */ - - /* Prefer action on the disks in use at the time the snapshot was - * created; but fall back to current definition if dealing with a - * snapshot created prior to libvirt 0.9.5. */ - virDomainDef *def = snap->def->dom; - - if (!def) - def = vm->def; - - for (i = 0; i < def->ndisks; i++) { - if (virDomainDiskTranslateSourcePool(def->disks[i]) < 0) - return -1; - } - - if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) - return -1; - } else { - priv = vm->privateData; - qemuDomainObjEnterMonitor(vm); - /* we continue on even in the face of error */ - qemuMonitorDeleteSnapshot(priv->mon, snap->def->name); - qemuDomainObjExitMonitor(vm); - } - }
if (update_parent) { if (snap->nchildren) { @@ -2348,6 +2316,7 @@ qemuSnapshotDiscard(virQEMUDriver *driver, if (snap == virDomainSnapshotGetCurrent(vm->snapshots)) { virDomainSnapshotSetCurrent(vm->snapshots, NULL); if (update_parent && snap->def->parent_name) { + virDomainMomentObj *parentsnap = NULL; parentsnap = virDomainSnapshotFindByName(vm->snapshots, snap->def->parent_name); if (!parentsnap) { @@ -2376,6 +2345,52 @@ qemuSnapshotDiscard(virQEMUDriver *driver, }
+/* Discard one snapshot (or its metadata), without reparenting any children. */ +static int +qemuSnapshotDiscard(virQEMUDriver *driver, + virDomainObj *vm, + virDomainMomentObj *snap, + bool update_parent, + bool metadata_only) +{ + qemuDomainObjPrivate *priv;
Declare this variable ...
+ + if (!metadata_only) { + if (!virDomainObjIsActive(vm)) { + size_t i; + /* Ignore any skipped disks */ + + /* Prefer action on the disks in use at the time the snapshot was + * created; but fall back to current definition if dealing with a + * snapshot created prior to libvirt 0.9.5. */ + virDomainDef *def = snap->def->dom; + + if (!def) + def = vm->def; + + for (i = 0; i < def->ndisks; i++) { + if (virDomainDiskTranslateSourcePool(def->disks[i]) < 0) + return -1; + } + + if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) + return -1; + } else { + priv = vm->privateData;
... here.
+ qemuDomainObjEnterMonitor(vm); + /* we continue on even in the face of error */ + qemuMonitorDeleteSnapshot(priv->mon, snap->def->name);
but since it's only used for monitor you can also do 'qemuDomainGetMonitor(vm)' here instead.
+ qemuDomainObjExitMonitor(vm); + }
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Signed-off-by: Pavel Hrdina <phrdina@redhat.com> Reviewed-by: Peter Krempa <pkrempa@redhat.com> --- src/qemu/qemu_snapshot.c | 42 ++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 4ea3f578e9..11427716e4 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2485,6 +2485,33 @@ qemuSnapshotDeleteChildren(virDomainObj *vm, } +static int +qemuSnapshotDeleteValidate(virDomainMomentObj *snap, + unsigned int flags) +{ + int external = 0; + + if (!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) && + virDomainSnapshotIsExternal(snap)) + external++; + + if (flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | + VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) + virDomainMomentForEachDescendant(snap, + qemuSnapshotCountExternal, + &external); + + if (external) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, + _("deletion of %d external disk snapshots not " + "supported yet"), external); + return -1; + } + + return 0; +} + + int qemuSnapshotDelete(virDomainObj *vm, virDomainSnapshotPtr snapshot, @@ -2493,7 +2520,6 @@ qemuSnapshotDelete(virDomainObj *vm, int ret = -1; virDomainMomentObj *snap = NULL; bool metadata_only = !!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY); - int external = 0; virCheckFlags(VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY | @@ -2506,20 +2532,8 @@ qemuSnapshotDelete(virDomainObj *vm, goto endjob; if (!metadata_only) { - if (!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) && - virDomainSnapshotIsExternal(snap)) - external++; - if (flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | - VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) - virDomainMomentForEachDescendant(snap, - qemuSnapshotCountExternal, - &external); - if (external) { - virReportError(VIR_ERR_CONFIG_UNSUPPORTED, - _("deletion of %d external disk snapshots not " - "supported yet"), external); + if (qemuSnapshotDeleteValidate(snap, flags) < 0) goto endjob; - } } if (flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | -- 2.38.1

Prepare the validation function for external snapshot delete support. There is one exception when deleting `children-only` snapshots. If the snapshot tree is like this example: snap1 (external) | +- snap2 (internal) | +- snap3 (internal) | +- snap4 (internal) and user calls `snapshot-delete snap1 --children-only` the current snapshot is external but all the children snapshots are internal only and we are able to delete it. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> Reviewed-by: Peter Krempa <pkrempa@redhat.com> --- src/qemu/qemu_snapshot.c | 73 +++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 11427716e4..a4b45d3ba3 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -100,21 +100,6 @@ qemuSnapObjFromSnapshot(virDomainObj *vm, } -/* Count how many snapshots in a set are external snapshots. */ -static int -qemuSnapshotCountExternal(void *payload, - const char *name G_GNUC_UNUSED, - void *data) -{ - virDomainMomentObj *snap = payload; - int *count = data; - - if (virDomainSnapshotIsExternal(snap)) - (*count)++; - return 0; -} - - int qemuSnapshotFSFreeze(virDomainObj *vm, const char **mountpoints, @@ -2485,26 +2470,60 @@ qemuSnapshotDeleteChildren(virDomainObj *vm, } +typedef struct { + int external; + int internal; +} qemuSnapshotCount; + + +static int +qemuSnapshotCountExternalInternal(void *payload, + const char *name G_GNUC_UNUSED, + void *data) +{ + virDomainMomentObj *snap = payload; + qemuSnapshotCount *count = data; + + if (virDomainSnapshotIsExternal(snap)) { + count->external++; + } else { + count->internal++; + } + + return 0; +} + + static int qemuSnapshotDeleteValidate(virDomainMomentObj *snap, unsigned int flags) { - int external = 0; - - if (!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY) && - virDomainSnapshotIsExternal(snap)) - external++; if (flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | - VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) + VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) { + qemuSnapshotCount count = { 0 }; + virDomainMomentForEachDescendant(snap, - qemuSnapshotCountExternal, - &external); + qemuSnapshotCountExternalInternal, + &count); - if (external) { - virReportError(VIR_ERR_CONFIG_UNSUPPORTED, - _("deletion of %d external disk snapshots not " - "supported yet"), external); + if (count.external > 0 && count.internal > 0) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s", + _("deletion of external and internal children disk snapshots not supported")); + return -1; + } + + if (count.external > 0) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s", + _("deletion of external children disk snapshots not supported")); + return -1; + } + } + + if (virDomainSnapshotIsExternal(snap) && + !(flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) { + virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s", + _("deletion of external disk snapshots not supported")); return -1; } -- 2.38.1

Deleting internal snapshot when the currently active disk image is different then where the internal snapshot was taken doesn't work correctly. This applies to a running VM only as we are using QMP command and talking to the QEMU process that is using different disk. This works correctly when the VM is shut of as in this case we spawn qemu-img process to delete the snapshot. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index a4b45d3ba3..adcd4eb73a 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2495,9 +2495,30 @@ qemuSnapshotCountExternalInternal(void *payload, static int -qemuSnapshotDeleteValidate(virDomainMomentObj *snap, +qemuSnapshotDeleteValidate(virDomainObj *vm, + virDomainMomentObj *snap, unsigned int flags) { + if (!virDomainSnapshotIsExternal(snap) && + virDomainObjIsActive(vm)) { + ssize_t i; + virDomainSnapshotDef *snapdef = virDomainSnapshotObjGetDef(snap); + + for (i = 0; i < snapdef->ndisks; i++) { + virDomainSnapshotDiskDef *snapDisk = &(snapdef->disks[i]); + virDomainDiskDef *vmdisk = NULL; + virDomainDiskDef *disk = NULL; + + vmdisk = qemuDomainDiskByName(vm->def, snapDisk->name); + disk = qemuDomainDiskByName(snapdef->parent.dom, snapDisk->name); + + if (!virStorageSourceIsSameLocation(vmdisk->src, disk->src)) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("deleting internal snapshot for non-active disk is not supported")); + return -1; + } + } + } if (flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) { @@ -2551,7 +2572,7 @@ qemuSnapshotDelete(virDomainObj *vm, goto endjob; if (!metadata_only) { - if (qemuSnapshotDeleteValidate(snap, flags) < 0) + if (qemuSnapshotDeleteValidate(vm, snap, flags) < 0) goto endjob; } -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:57 +0100, Pavel Hrdina wrote:
Deleting internal snapshot when the currently active disk image is different then where the internal snapshot was taken doesn't work
than
correctly.
This applies to a running VM only as we are using QMP command and talking to the QEMU process that is using different disk.
This works correctly when the VM is shut of as in this case we spawn qemu-img process to delete the snapshot.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index a4b45d3ba3..adcd4eb73a 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2495,9 +2495,30 @@ qemuSnapshotCountExternalInternal(void *payload,
static int -qemuSnapshotDeleteValidate(virDomainMomentObj *snap, +qemuSnapshotDeleteValidate(virDomainObj *vm, + virDomainMomentObj *snap, unsigned int flags) { + if (!virDomainSnapshotIsExternal(snap) && + virDomainObjIsActive(vm)) { + ssize_t i; + virDomainSnapshotDef *snapdef = virDomainSnapshotObjGetDef(snap); + + for (i = 0; i < snapdef->ndisks; i++) { + virDomainSnapshotDiskDef *snapDisk = &(snapdef->disks[i]); + virDomainDiskDef *vmdisk = NULL; + virDomainDiskDef *disk = NULL; + + vmdisk = qemuDomainDiskByName(vm->def, snapDisk->name); + disk = qemuDomainDiskByName(snapdef->parent.dom, snapDisk->name); + + if (!virStorageSourceIsSameLocation(vmdisk->src, disk->src)) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("deleting internal snapshot for non-active disk is not supported"));
This error is a bit misleading or at least doesn't explain very well what the actual problem is. I think I'd go with: "disk image for internal snapshot '%s' is not the same as when the snapshot was taken" Or something similar. This is such that in case when the user moved the disk image and forgot to modify the snapshot XML the error is still actionable. With a better error: Reviewed-by: Peter Krempa <pkrempa@redhat.com>

On Tue, Dec 13, 2022 at 03:26:52PM +0100, Peter Krempa wrote:
On Thu, Dec 08, 2022 at 14:30:57 +0100, Pavel Hrdina wrote:
Deleting internal snapshot when the currently active disk image is different then where the internal snapshot was taken doesn't work
than
correctly.
This applies to a running VM only as we are using QMP command and talking to the QEMU process that is using different disk.
This works correctly when the VM is shut of as in this case we spawn qemu-img process to delete the snapshot.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index a4b45d3ba3..adcd4eb73a 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2495,9 +2495,30 @@ qemuSnapshotCountExternalInternal(void *payload,
static int -qemuSnapshotDeleteValidate(virDomainMomentObj *snap, +qemuSnapshotDeleteValidate(virDomainObj *vm, + virDomainMomentObj *snap, unsigned int flags) { + if (!virDomainSnapshotIsExternal(snap) && + virDomainObjIsActive(vm)) { + ssize_t i; + virDomainSnapshotDef *snapdef = virDomainSnapshotObjGetDef(snap); + + for (i = 0; i < snapdef->ndisks; i++) { + virDomainSnapshotDiskDef *snapDisk = &(snapdef->disks[i]); + virDomainDiskDef *vmdisk = NULL; + virDomainDiskDef *disk = NULL; + + vmdisk = qemuDomainDiskByName(vm->def, snapDisk->name); + disk = qemuDomainDiskByName(snapdef->parent.dom, snapDisk->name); + + if (!virStorageSourceIsSameLocation(vmdisk->src, disk->src)) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("deleting internal snapshot for non-active disk is not supported"));
This error is a bit misleading or at least doesn't explain very well what the actual problem is. I think I'd go with:
"disk image for internal snapshot '%s' is not the same as when the snapshot was taken"
Or something similar. This is such that in case when the user moved the disk image and forgot to modify the snapshot XML the error is still actionable.
I get the point but to me it feels it still doesn't describe the issue properly. Take this situation, you have VM, first snapshot is internal and second one is external and active and the VM is running. In this situation you are not able to delete the internal snapshot because the HMP commands will not work properly but will not produce any error as well. VM | +- snap1 (internal) | + snap2 (external)(active) So the triggers only if the snapshot that we are trying to delete is internal, the VM is running and the currently active disk in VM is different than where the internal snapshot was taken. How about: "disk image '%s' for internal snapshot '%s' is not the same as disk image currently used by VM" Pavel
With a better error:
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Deleting external snapshots will require to run it as async domain job, the same way as we do for snapshot creation. For internal snapshots modify the job mask in order to forbid any other job to be started. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index adcd4eb73a..97df448363 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2361,11 +2361,16 @@ qemuSnapshotDiscard(virQEMUDriver *driver, if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) return -1; } else { + /* Similarly as internal snapshot creation we would use a regular job + * here so set a mask to forbid any other job. */ + qemuDomainObjSetAsyncJobMask(vm, VIR_JOB_NONE); priv = vm->privateData; - qemuDomainObjEnterMonitor(vm); + if (qemuDomainObjEnterMonitorAsync(vm, VIR_ASYNC_JOB_SNAPSHOT) < 0) + return -1; /* we continue on even in the face of error */ qemuMonitorDeleteSnapshot(priv->mon, snap->def->name); qemuDomainObjExitMonitor(vm); + qemuDomainObjSetAsyncJobMask(vm, VIR_JOB_DEFAULT_MASK); } } @@ -2565,8 +2570,11 @@ qemuSnapshotDelete(virDomainObj *vm, VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY | VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY, -1); - if (virDomainObjBeginJob(vm, VIR_JOB_MODIFY) < 0) + if (virDomainObjBeginAsyncJob(vm, VIR_ASYNC_JOB_SNAPSHOT, + VIR_DOMAIN_JOB_OPERATION_SNAPSHOT_DELETE, + flags) < 0) { return -1; + } if (!(snap = qemuSnapObjFromSnapshot(vm, snapshot))) goto endjob; @@ -2584,7 +2592,7 @@ qemuSnapshotDelete(virDomainObj *vm, } endjob: - virDomainObjEndJob(vm); + virDomainObjEndAsyncJob(vm); return ret; } -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:58 +0100, Pavel Hrdina wrote:
Deleting external snapshots will require to run it as async domain job, the same way as we do for snapshot creation.
For internal snapshots modify the job mask in order to forbid any other job to be started.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

In order to save some CPU cycles we will collect all the necessary data to delete external snapshot before we even start. They will be later used by code that deletes the snapshots and updates metadata when needed. With external snapshots we need data that libvirt gets from running QEMU process so if the VM is not running we need to start paused QEMU process for the snapshot deletion and kill at afterwards. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 163 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 97df448363..882224b0a7 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2232,6 +2232,134 @@ qemuSnapshotRevert(virDomainObj *vm, } +typedef struct _qemuSnapshotDeleteExternalData { + virDomainMomentObj *parentSnap; + virDomainSnapshotDiskDef *snapDisk; + virDomainDiskDef *domDisk; + virDomainDiskDef *parentDomDisk; + virStorageSource *diskSrc; + virStorageSource *parentDiskSrc; + virStorageSource *prevDiskSrc; + qemuBlockJobData *job; +} qemuSnapshotDeleteExternalData; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(qemuSnapshotDeleteExternalData, g_free); + + +/** + * qemuSnapshotFindParentSnapForDisk: + * @snap: snapshot object that is about to be deleted + * @snapDisk: snapshot disk definition + * + * Find parent snapshot object that contains snapshot of the @snapDisk. + * It may not be the next snapshot in the snapshot tree as the disk in + * question may be skipped by using VIR_DOMAIN_SNAPSHOT_LOCATION_NO. + * + * Returns virDomainMomentObj* on success or NULL if there is no parent + * snapshot containing the @snapDisk. + * */ +static virDomainMomentObj* +qemuSnapshotFindParentSnapForDisk(virDomainMomentObj *snap, + virDomainSnapshotDiskDef *snapDisk) +{ + virDomainMomentObj *parentSnap = snap->parent; + + while (parentSnap) { + ssize_t i; + virDomainSnapshotDef *parentSnapdef = virDomainSnapshotObjGetDef(parentSnap); + + if (!parentSnapdef) + break; + + for (i = 0; i < parentSnapdef->ndisks; i++) { + virDomainSnapshotDiskDef *parentSnapDisk = &(parentSnapdef->disks[i]); + + if (parentSnapDisk->snapshot != VIR_DOMAIN_SNAPSHOT_LOCATION_NO && + STREQ(snapDisk->name, parentSnapDisk->name)) { + return parentSnap; + } + } + + parentSnap = parentSnap->parent; + } + + return NULL; +} + + +static GSList* +qemuSnapshotDeleteExternalPrepare(virDomainObj *vm, + virDomainMomentObj *snap) +{ + ssize_t i; + virDomainSnapshotDef *snapdef = virDomainSnapshotObjGetDef(snap); + g_autoslist(qemuSnapshotDeleteExternalData) ret = NULL; + + for (i = 0; i < snapdef->ndisks; i++) { + g_autofree qemuSnapshotDeleteExternalData *data = NULL; + virDomainSnapshotDiskDef *snapDisk = &(snapdef->disks[i]); + + if (snapDisk->snapshot == VIR_DOMAIN_SNAPSHOT_LOCATION_NO) + continue; + + data = g_new0(qemuSnapshotDeleteExternalData, 1); + data->snapDisk = snapDisk; + + data->domDisk = qemuDomainDiskByName(vm->def, snapDisk->name); + if (!data->domDisk) + return NULL; + + data->diskSrc = virStorageSourceChainLookup(data->domDisk->src, NULL, + data->snapDisk->src->path, + NULL, &data->prevDiskSrc); + if (!data->diskSrc) + return NULL; + + if (!virStorageSourceIsSameLocation(data->diskSrc, data->snapDisk->src)) { + virReportError(VIR_ERR_OPERATION_FAILED, "%s", + _("VM disk source and snapshot disk source are not the same")); + return NULL; + } + + data->parentDomDisk = virDomainDiskByTarget(snapdef->parent.dom, + data->snapDisk->name); + if (!data->parentDomDisk) { + virReportError(VIR_ERR_OPERATION_FAILED, + _("failed to find disk '%s' in snapshot VM XML"), + snapDisk->name); + return NULL; + } + + if (virDomainObjIsActive(vm)) { + data->parentDiskSrc = data->diskSrc->backingStore; + if (!data->parentDiskSrc) { + virReportError(VIR_ERR_OPERATION_FAILED, "%s", + _("failed to find parent disk source in backing chain")); + return NULL; + } + + if (!virStorageSourceIsSameLocation(data->parentDiskSrc, data->parentDomDisk->src)) { + virReportError(VIR_ERR_OPERATION_FAILED, "%s", + _("snapshot VM disk source and parent disk source are not the same")); + return NULL; + } + } + + data->parentSnap = qemuSnapshotFindParentSnapForDisk(snap, data->snapDisk); + + if (data->parentSnap && !virDomainSnapshotIsExternal(data->parentSnap)) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("deleting external snapshot that has internal snapshot as parent not supported")); + return NULL; + } + + ret = g_slist_append(ret, g_steal_pointer(&data)); + } + + return g_steal_pointer(&ret); +} + + typedef struct _virQEMUMomentReparent virQEMUMomentReparent; struct _virQEMUMomentReparent { const char *dir; @@ -2565,6 +2693,10 @@ qemuSnapshotDelete(virDomainObj *vm, int ret = -1; virDomainMomentObj *snap = NULL; bool metadata_only = !!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY); + bool stop_qemu = false; + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoslist(qemuSnapshotDeleteExternalData) externalData = NULL; virCheckFlags(VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY | @@ -2582,6 +2714,32 @@ qemuSnapshotDelete(virDomainObj *vm, if (!metadata_only) { if (qemuSnapshotDeleteValidate(vm, snap, flags) < 0) goto endjob; + + if (virDomainSnapshotIsExternal(snap) && + !(flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | + VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY))) { + + externalData = qemuSnapshotDeleteExternalPrepare(vm, snap); + + if (!virDomainObjIsActive(vm)) { + if (qemuProcessStart(NULL, driver, vm, NULL, VIR_ASYNC_JOB_SNAPSHOT, + NULL, -1, NULL, NULL, + VIR_NETDEV_VPORT_PROFILE_OP_CREATE, + VIR_QEMU_PROCESS_START_PAUSED) < 0) { + goto endjob; + } + + stop_qemu = true; + + /* Call the prepare again as some data require that the VM is + * running to get everything we need. */ + g_slist_free_full(externalData, g_free); + externalData = qemuSnapshotDeleteExternalPrepare(vm, snap); + } + + if (!externalData) + goto endjob; + } } if (flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | @@ -2592,6 +2750,11 @@ qemuSnapshotDelete(virDomainObj *vm, } endjob: + if (stop_qemu) { + qemuProcessStop(driver, vm, VIR_DOMAIN_SHUTOFF_SHUTDOWN, + VIR_ASYNC_JOB_SNAPSHOT, 0); + } + virDomainObjEndAsyncJob(vm); return ret; -- 2.38.1

On Thu, Dec 08, 2022 at 14:30:59 +0100, Pavel Hrdina wrote:
In order to save some CPU cycles we will collect all the necessary data to delete external snapshot before we even start. They will be later used by code that deletes the snapshots and updates metadata when needed.
With external snapshots we need data that libvirt gets from running QEMU process so if the VM is not running we need to start paused QEMU process for the snapshot deletion and kill at afterwards.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 163 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+)
diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 97df448363..882224b0a7 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2232,6 +2232,134 @@ qemuSnapshotRevert(virDomainObj *vm, }
+typedef struct _qemuSnapshotDeleteExternalData { + virDomainMomentObj *parentSnap; + virDomainSnapshotDiskDef *snapDisk; + virDomainDiskDef *domDisk; + virDomainDiskDef *parentDomDisk; + virStorageSource *diskSrc; + virStorageSource *parentDiskSrc; + virStorageSource *prevDiskSrc; + qemuBlockJobData *job;
Please annotate which 'source' belongs to which 'disk def' and such. You can also theoretically reorder them.
+} qemuSnapshotDeleteExternalData; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(qemuSnapshotDeleteExternalData, g_free); + + +/** + * qemuSnapshotFindParentSnapForDisk: + * @snap: snapshot object that is about to be deleted + * @snapDisk: snapshot disk definition + * + * Find parent snapshot object that contains snapshot of the @snapDisk. + * It may not be the next snapshot in the snapshot tree as the disk in + * question may be skipped by using VIR_DOMAIN_SNAPSHOT_LOCATION_NO. + * + * Returns virDomainMomentObj* on success or NULL if there is no parent + * snapshot containing the @snapDisk. + * */ +static virDomainMomentObj* +qemuSnapshotFindParentSnapForDisk(virDomainMomentObj *snap, + virDomainSnapshotDiskDef *snapDisk) +{ + virDomainMomentObj *parentSnap = snap->parent; + + while (parentSnap) { + ssize_t i; + virDomainSnapshotDef *parentSnapdef = virDomainSnapshotObjGetDef(parentSnap); + + if (!parentSnapdef) + break; + + for (i = 0; i < parentSnapdef->ndisks; i++) { + virDomainSnapshotDiskDef *parentSnapDisk = &(parentSnapdef->disks[i]); + + if (parentSnapDisk->snapshot != VIR_DOMAIN_SNAPSHOT_LOCATION_NO && + STREQ(snapDisk->name, parentSnapDisk->name)) { + return parentSnap; + } + } + + parentSnap = parentSnap->parent; + } + + return NULL; +} + + +static GSList* +qemuSnapshotDeleteExternalPrepare(virDomainObj *vm, + virDomainMomentObj *snap) +{ + ssize_t i; + virDomainSnapshotDef *snapdef = virDomainSnapshotObjGetDef(snap); + g_autoslist(qemuSnapshotDeleteExternalData) ret = NULL; + + for (i = 0; i < snapdef->ndisks; i++) { + g_autofree qemuSnapshotDeleteExternalData *data = NULL; + virDomainSnapshotDiskDef *snapDisk = &(snapdef->disks[i]); + + if (snapDisk->snapshot == VIR_DOMAIN_SNAPSHOT_LOCATION_NO) + continue; + + data = g_new0(qemuSnapshotDeleteExternalData, 1); + data->snapDisk = snapDisk; + + data->domDisk = qemuDomainDiskByName(vm->def, snapDisk->name); + if (!data->domDisk) + return NULL; + + data->diskSrc = virStorageSourceChainLookup(data->domDisk->src, NULL, + data->snapDisk->src->path,
So here you are looking up based on 'src->path' which works properly really only for file-based/local storage. For anything else it will break. Since you have a virStorageSource to compare to you should probably create a helper which walks the backing chain and uses virStorageSourceIsSameLocation internally instead virStorageSourceChainLookup which should really be used only for user-provided data as the third argument (for backwards compatibility with old-style API args).
+ NULL, &data->prevDiskSrc); + if (!data->diskSrc) + return NULL; + + if (!virStorageSourceIsSameLocation(data->diskSrc, data->snapDisk->src)) { + virReportError(VIR_ERR_OPERATION_FAILED, "%s", + _("VM disk source and snapshot disk source are not the same")); + return NULL; + } + + data->parentDomDisk = virDomainDiskByTarget(snapdef->parent.dom, + data->snapDisk->name); + if (!data->parentDomDisk) { + virReportError(VIR_ERR_OPERATION_FAILED, + _("failed to find disk '%s' in snapshot VM XML"), + snapDisk->name); + return NULL; + } + + if (virDomainObjIsActive(vm)) { + data->parentDiskSrc = data->diskSrc->backingStore; + if (!data->parentDiskSrc) {
Note that active definitions contain also a "terminator" virStorageSource put into the 'backingStore' object, which is a valid pointer. You probably want to use 'virStorageSourceIsBacking' here.
+ virReportError(VIR_ERR_OPERATION_FAILED, "%s", + _("failed to find parent disk source in backing chain")); + return NULL; + } + + if (!virStorageSourceIsSameLocation(data->parentDiskSrc, data->parentDomDisk->src)) { + virReportError(VIR_ERR_OPERATION_FAILED, "%s", + _("snapshot VM disk source and parent disk source are not the same")); + return NULL; + } + } + + data->parentSnap = qemuSnapshotFindParentSnapForDisk(snap, data->snapDisk); + + if (data->parentSnap && !virDomainSnapshotIsExternal(data->parentSnap)) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("deleting external snapshot that has internal snapshot as parent not supported")); + return NULL; + } + + ret = g_slist_append(ret, g_steal_pointer(&data));
The usual way to work with singly-linked lists is to use the prepend operation ...
+ } +
... and reverse it before returning.
+ return g_steal_pointer(&ret); +} + + typedef struct _virQEMUMomentReparent virQEMUMomentReparent; struct _virQEMUMomentReparent { const char *dir; @@ -2565,6 +2693,10 @@ qemuSnapshotDelete(virDomainObj *vm, int ret = -1; virDomainMomentObj *snap = NULL; bool metadata_only = !!(flags & VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY); + bool stop_qemu = false; + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoslist(qemuSnapshotDeleteExternalData) externalData = NULL;
virCheckFlags(VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | VIR_DOMAIN_SNAPSHOT_DELETE_METADATA_ONLY | @@ -2582,6 +2714,32 @@ qemuSnapshotDelete(virDomainObj *vm, if (!metadata_only) { if (qemuSnapshotDeleteValidate(vm, snap, flags) < 0) goto endjob; + + if (virDomainSnapshotIsExternal(snap) && + !(flags & (VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN | + VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY))) { + + externalData = qemuSnapshotDeleteExternalPrepare(vm, snap); + + if (!virDomainObjIsActive(vm)) { + if (qemuProcessStart(NULL, driver, vm, NULL, VIR_ASYNC_JOB_SNAPSHOT, + NULL, -1, NULL, NULL, + VIR_NETDEV_VPORT_PROFILE_OP_CREATE, + VIR_QEMU_PROCESS_START_PAUSED) < 0) { + goto endjob; + } + + stop_qemu = true; + + /* Call the prepare again as some data require that the VM is + * running to get everything we need. */ + g_slist_free_full(externalData, g_free); + externalData = qemuSnapshotDeleteExternalPrepare(vm, snap);
Preferrably don't reuse 'externalData'. You can e.g. instead steal the value into a locally scoped temporary autofreed variable and then re-create it.
+ } + + if (!externalData) + goto endjob; + }
If you deal with the lookup properly you can add: Reviewed-by: Peter Krempa <pkrempa@redhat.com>

When deleting snapshot we are starting block-commit job over all disks that are part of the snapshot. This operation may fail as it writes data changes to the backing qcow2 image so we need to wait for all the disks to finish the operation and wait for correct signal from QEMU. If deleting active snapshot we will get `ready` signal and for inactive snapshots we need to disable autofinalize in order to get `pending` signal. At this point if commit for any disk fails for some reason and we abort the VM is still in consistent state and user can fix the reason why the deletion failed. After that we do `pivot` or `finalize` if it's active snapshot or not to finish the block job. It still may fail but there is nothing else we can do about it. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 266 +++++++++++++++++++++++++++++++++++---- 1 file changed, 245 insertions(+), 21 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 882224b0a7..c493a3e94f 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2394,6 +2394,207 @@ qemuSnapshotChildrenReparent(void *payload, } +/* Deleting external snapshot is started by running qemu block-commit job. + * We need to wait for all block-commit jobs to be 'ready' or 'pending' to + * continue with external snapshot deletion. */ +static int +qemuSnapshotJobIsRunning(qemuBlockjobState state) +{ + switch (state) { + case QEMU_BLOCKJOB_STATE_COMPLETED: + case QEMU_BLOCKJOB_STATE_FAILED: + case QEMU_BLOCKJOB_STATE_CANCELLED: + case QEMU_BLOCKJOB_STATE_READY: + case QEMU_BLOCKJOB_STATE_CONCLUDED: + case QEMU_BLOCKJOB_STATE_PENDING: + return 0; + + case QEMU_BLOCKJOB_STATE_NEW: + case QEMU_BLOCKJOB_STATE_RUNNING: + case QEMU_BLOCKJOB_STATE_ABORTING: + case QEMU_BLOCKJOB_STATE_PIVOTING: + return 1; + + case QEMU_BLOCKJOB_STATE_LAST: + break; + } + + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("invalid block job state")); + return -1; +} + + +/* When finishing or aborting qemu blockjob we only need to know if the + * job is still active or not. */ +static int +qemuSnapshotJobIsActive(qemuBlockjobState state) +{ + switch (state) { + case QEMU_BLOCKJOB_STATE_COMPLETED: + case QEMU_BLOCKJOB_STATE_FAILED: + case QEMU_BLOCKJOB_STATE_CANCELLED: + case QEMU_BLOCKJOB_STATE_CONCLUDED: + return 0; + + case QEMU_BLOCKJOB_STATE_READY: + case QEMU_BLOCKJOB_STATE_NEW: + case QEMU_BLOCKJOB_STATE_RUNNING: + case QEMU_BLOCKJOB_STATE_ABORTING: + case QEMU_BLOCKJOB_STATE_PENDING: + case QEMU_BLOCKJOB_STATE_PIVOTING: + return 1; + + case QEMU_BLOCKJOB_STATE_LAST: + break; + } + + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("invalid block job state")); + return -1; +} + + +/* Wait for qemu blockjob to finish 'block-commit' operation until it is + * ready to be finished by calling 'block-pivot' or 'block-finalize'. */ +static int +qemuSnapshotJobRunning(virDomainObj *vm, + qemuBlockJobData *job) +{ + int rc; + qemuBlockJobUpdate(vm, job, VIR_ASYNC_JOB_SNAPSHOT); + + while ((rc = qemuSnapshotJobIsRunning(job->state)) > 0) { + if (qemuDomainObjWait(vm) < 0) + return -1; + qemuBlockJobUpdate(vm, job, VIR_ASYNC_JOB_SNAPSHOT); + } + + if (rc < 0) + return -1; + + return 0; +} + + +/* Wait for qemu blockjob to be done after 'block-pivot' or 'block-finalize' + * was started. */ +static int +qemuSnapshotJobFinishing(virDomainObj *vm, + qemuBlockJobData *job) +{ + int rc; + qemuBlockJobUpdate(vm, job, VIR_ASYNC_JOB_SNAPSHOT); + + while ((rc = qemuSnapshotJobIsActive(job->state)) > 0) { + if (qemuDomainObjWait(vm) < 0) + return -1; + qemuBlockJobUpdate(vm, job, VIR_ASYNC_JOB_SNAPSHOT); + } + + if (rc < 0) + return -1; + + return 0; +} + + +static int +qemuSnapshotDiscardExternal(virDomainObj *vm, + GSList *externalData) +{ + GSList *cur = NULL; + + for (cur = externalData; cur; cur = g_slist_next(cur)) { + qemuSnapshotDeleteExternalData *data = cur->data; + virTristateBool autofinalize = VIR_TRISTATE_BOOL_NO; + unsigned int commitFlags = VIR_DOMAIN_BLOCK_COMMIT_DELETE; + + if (data->domDisk->src == data->diskSrc) { + commitFlags |= VIR_DOMAIN_BLOCK_COMMIT_ACTIVE; + autofinalize = VIR_TRISTATE_BOOL_YES; + } + + data->job = qemuBlockCommit(vm, + data->domDisk, + data->parentDiskSrc, + data->diskSrc, + data->prevDiskSrc, + 0, + VIR_ASYNC_JOB_SNAPSHOT, + autofinalize, + commitFlags); + + if (!data->job) + goto error; + } + + for (cur = externalData; cur; cur = g_slist_next(cur)) { + qemuSnapshotDeleteExternalData *data = cur->data; + + if (qemuSnapshotJobRunning(vm, data->job) < 0) + goto error; + + if (data->job->state == QEMU_BLOCKJOB_STATE_FAILED) { + virReportError(VIR_ERR_INTERNAL_ERROR, + _("block commit failed while deleting disk '%s' snapshot: '%s'"), + data->snapDisk->name, data->job->errmsg); + goto error; + } + } + + for (cur = externalData; cur; cur = g_slist_next(cur)) { + qemuSnapshotDeleteExternalData *data = cur->data; + + if (data->job->state == QEMU_BLOCKJOB_STATE_READY) { + if (qemuBlockPivot(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT, NULL) < 0) + goto error; + } else if (data->job->state == QEMU_BLOCKJOB_STATE_PENDING) { + if (qemuBlockFinalize(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT) < 0) + goto error; + } + + if (qemuSnapshotJobFinishing(vm, data->job) < 0) + goto error; + + if (data->job->state == QEMU_BLOCKJOB_STATE_FAILED) { + virReportError(VIR_ERR_INTERNAL_ERROR, + _("finishing block job failed while deleting disk '%s' snapshot: '%s'"), + data->snapDisk->name, data->job->errmsg); + goto error; + } + + qemuBlockJobSyncEnd(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT); + } + + return 0; + + error: + for (cur = externalData; cur; cur = g_slist_next(cur)) { + qemuDomainObjPrivate *priv = vm->privateData; + qemuSnapshotDeleteExternalData *data = cur->data; + + if (!data->job) + continue; + + qemuBlockJobUpdate(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT); + + if (qemuSnapshotJobIsActive(data->job->state)) { + if (qemuDomainObjEnterMonitorAsync(vm, VIR_ASYNC_JOB_SNAPSHOT) == 0) { + ignore_value(qemuMonitorBlockJobCancel(priv->mon, data->job->name, false)); + qemuDomainObjExitMonitor(vm); + + data->job->state = QEMU_BLOCKJOB_STATE_ABORTING; + } + } + + qemuBlockJobSyncEnd(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT); + } + + return -1; +} + + static int qemuSnapshotDiscardMetadata(virDomainObj *vm, virDomainMomentObj *snap, @@ -2460,11 +2661,12 @@ qemuSnapshotDiscardMetadata(virDomainObj *vm, /* Discard one snapshot (or its metadata), without reparenting any children. */ static int -qemuSnapshotDiscard(virQEMUDriver *driver, - virDomainObj *vm, - virDomainMomentObj *snap, - bool update_parent, - bool metadata_only) +qemuSnapshotDiscardImpl(virQEMUDriver *driver, + virDomainObj *vm, + virDomainMomentObj *snap, + GSList *externalData, + bool update_parent, + bool metadata_only) { qemuDomainObjPrivate *priv; @@ -2486,19 +2688,29 @@ qemuSnapshotDiscard(virQEMUDriver *driver, return -1; } - if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) - return -1; + if (virDomainSnapshotIsExternal(snap)) { + if (qemuSnapshotDiscardExternal(vm, externalData) < 0) + return -1; + } else { + if (qemuDomainSnapshotForEachQcow2(driver, def, snap, "-d", true) < 0) + return -1; + } } else { - /* Similarly as internal snapshot creation we would use a regular job - * here so set a mask to forbid any other job. */ - qemuDomainObjSetAsyncJobMask(vm, VIR_JOB_NONE); - priv = vm->privateData; - if (qemuDomainObjEnterMonitorAsync(vm, VIR_ASYNC_JOB_SNAPSHOT) < 0) - return -1; - /* we continue on even in the face of error */ - qemuMonitorDeleteSnapshot(priv->mon, snap->def->name); - qemuDomainObjExitMonitor(vm); - qemuDomainObjSetAsyncJobMask(vm, VIR_JOB_DEFAULT_MASK); + if (virDomainSnapshotIsExternal(snap)) { + if (qemuSnapshotDiscardExternal(vm, externalData) < 0) + return -1; + } else { + /* Similarly as internal snapshot creation we would use a regular job + * here so set a mask to forbid any other job. */ + qemuDomainObjSetAsyncJobMask(vm, VIR_JOB_NONE); + priv = vm->privateData; + if (qemuDomainObjEnterMonitorAsync(vm, VIR_ASYNC_JOB_SNAPSHOT) < 0) + return -1; + /* we continue on even in the face of error */ + qemuMonitorDeleteSnapshot(priv->mon, snap->def->name); + qemuDomainObjExitMonitor(vm); + qemuDomainObjSetAsyncJobMask(vm, VIR_JOB_DEFAULT_MASK); + } } } @@ -2509,6 +2721,17 @@ qemuSnapshotDiscard(virQEMUDriver *driver, } +static int +qemuSnapshotDiscard(virQEMUDriver *driver, + virDomainObj *vm, + virDomainMomentObj *snap, + bool update_parent, + bool metadata_only) +{ + return qemuSnapshotDiscardImpl(driver, vm, snap, NULL, update_parent, metadata_only); +} + + int qemuSnapshotDiscardAllMetadata(virQEMUDriver *driver, virDomainObj *vm) @@ -2530,12 +2753,13 @@ qemuSnapshotDiscardAllMetadata(virQEMUDriver *driver, static int qemuSnapshotDeleteSingle(virDomainObj *vm, virDomainMomentObj *snap, + GSList *externalData, bool metadata_only) { qemuDomainObjPrivate *priv = vm->privateData; virQEMUDriver *driver = priv->driver; - return qemuSnapshotDiscard(driver, vm, snap, true, metadata_only); + return qemuSnapshotDiscardImpl(driver, vm, snap, externalData, true, metadata_only); } @@ -2555,7 +2779,7 @@ qemuSnapshotDeleteAll(void *payload, virDomainMomentObj *snap = payload; struct qemuSnapshotDeleteAllData *data = opaque; - error = qemuSnapshotDeleteSingle(data->vm, snap, data->metadata_only); + error = qemuSnapshotDeleteSingle(data->vm, snap, NULL, data->metadata_only); if (error != 0) data->error = error; @@ -2595,7 +2819,7 @@ qemuSnapshotDeleteChildren(virDomainObj *vm, return -1; if (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN && - qemuSnapshotDeleteSingle(vm, snap, metadata_only) < 0) { + qemuSnapshotDeleteSingle(vm, snap, NULL, metadata_only) < 0) { return -1; } @@ -2746,7 +2970,7 @@ qemuSnapshotDelete(virDomainObj *vm, VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) { ret = qemuSnapshotDeleteChildren(vm, snap, metadata_only, flags); } else { - ret = qemuSnapshotDeleteSingle(vm, snap, metadata_only); + ret = qemuSnapshotDeleteSingle(vm, snap, externalData, metadata_only); } endjob: -- 2.38.1

On Thu, Dec 08, 2022 at 14:31:00 +0100, Pavel Hrdina wrote:
When deleting snapshot we are starting block-commit job over all disks that are part of the snapshot.
This operation may fail as it writes data changes to the backing qcow2 image so we need to wait for all the disks to finish the operation and wait for correct signal from QEMU. If deleting active snapshot we will get `ready` signal and for inactive snapshots we need to disable autofinalize in order to get `pending` signal.
At this point if commit for any disk fails for some reason and we abort the VM is still in consistent state and user can fix the reason why the deletion failed.
After that we do `pivot` or `finalize` if it's active snapshot or not to finish the block job. It still may fail but there is nothing else we can do about it.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 266 +++++++++++++++++++++++++++++++++++---- 1 file changed, 245 insertions(+), 21 deletions(-)
diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 882224b0a7..c493a3e94f 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2394,6 +2394,207 @@ qemuSnapshotChildrenReparent(void *payload, }
+/* Deleting external snapshot is started by running qemu block-commit job. + * We need to wait for all block-commit jobs to be 'ready' or 'pending' to + * continue with external snapshot deletion. */ +static int +qemuSnapshotJobIsRunning(qemuBlockjobState state)
This is more considering snapshot deletion blockjobs... so perhaps: qemuSnapshotDeleteBlockjobIsRunning ?
+{ + switch (state) { + case QEMU_BLOCKJOB_STATE_COMPLETED: + case QEMU_BLOCKJOB_STATE_FAILED: + case QEMU_BLOCKJOB_STATE_CANCELLED: + case QEMU_BLOCKJOB_STATE_READY: + case QEMU_BLOCKJOB_STATE_CONCLUDED: + case QEMU_BLOCKJOB_STATE_PENDING: + return 0; + + case QEMU_BLOCKJOB_STATE_NEW: + case QEMU_BLOCKJOB_STATE_RUNNING: + case QEMU_BLOCKJOB_STATE_ABORTING: + case QEMU_BLOCKJOB_STATE_PIVOTING: + return 1; + + case QEMU_BLOCKJOB_STATE_LAST: + break; + } + + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("invalid block job state")); + return -1;
Can this happen?
+} + + +/* When finishing or aborting qemu blockjob we only need to know if the + * job is still active or not. */ +static int +qemuSnapshotJobIsActive(qemuBlockjobState state)
Same naming patern as above.
+{ + switch (state) { + case QEMU_BLOCKJOB_STATE_COMPLETED: + case QEMU_BLOCKJOB_STATE_FAILED: + case QEMU_BLOCKJOB_STATE_CANCELLED: + case QEMU_BLOCKJOB_STATE_CONCLUDED: + return 0; + + case QEMU_BLOCKJOB_STATE_READY: + case QEMU_BLOCKJOB_STATE_NEW: + case QEMU_BLOCKJOB_STATE_RUNNING: + case QEMU_BLOCKJOB_STATE_ABORTING: + case QEMU_BLOCKJOB_STATE_PENDING: + case QEMU_BLOCKJOB_STATE_PIVOTING: + return 1; + + case QEMU_BLOCKJOB_STATE_LAST: + break; + } + + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("invalid block job state")); + return -1;
-||-
+} + + +/* Wait for qemu blockjob to finish 'block-commit' operation until it is + * ready to be finished by calling 'block-pivot' or 'block-finalize'. */ +static int +qemuSnapshotJobRunning(virDomainObj *vm, + qemuBlockJobData *job) +{ + int rc; + qemuBlockJobUpdate(vm, job, VIR_ASYNC_JOB_SNAPSHOT); + + while ((rc = qemuSnapshotJobIsRunning(job->state)) > 0) { + if (qemuDomainObjWait(vm) < 0) + return -1; + qemuBlockJobUpdate(vm, job, VIR_ASYNC_JOB_SNAPSHOT); + } + + if (rc < 0) + return -1; + + return 0; +} + + +/* Wait for qemu blockjob to be done after 'block-pivot' or 'block-finalize' + * was started. */ +static int +qemuSnapshotJobFinishing(virDomainObj *vm, + qemuBlockJobData *job) +{ + int rc; + qemuBlockJobUpdate(vm, job, VIR_ASYNC_JOB_SNAPSHOT); + + while ((rc = qemuSnapshotJobIsActive(job->state)) > 0) { + if (qemuDomainObjWait(vm) < 0) + return -1; + qemuBlockJobUpdate(vm, job, VIR_ASYNC_JOB_SNAPSHOT); + } + + if (rc < 0) + return -1; + + return 0; +}
And both functions above have confusing naming too.
+ + +static int +qemuSnapshotDiscardExternal(virDomainObj *vm, + GSList *externalData) +{ + GSList *cur = NULL; + + for (cur = externalData; cur; cur = g_slist_next(cur)) { + qemuSnapshotDeleteExternalData *data = cur->data; + virTristateBool autofinalize = VIR_TRISTATE_BOOL_NO; + unsigned int commitFlags = VIR_DOMAIN_BLOCK_COMMIT_DELETE; + + if (data->domDisk->src == data->diskSrc) { + commitFlags |= VIR_DOMAIN_BLOCK_COMMIT_ACTIVE; + autofinalize = VIR_TRISTATE_BOOL_YES; + } + + data->job = qemuBlockCommit(vm, + data->domDisk, + data->parentDiskSrc, + data->diskSrc, + data->prevDiskSrc, + 0, + VIR_ASYNC_JOB_SNAPSHOT, + autofinalize, + commitFlags);
[1]
+ + if (!data->job) + goto error; + } + + for (cur = externalData; cur; cur = g_slist_next(cur)) { + qemuSnapshotDeleteExternalData *data = cur->data; + + if (qemuSnapshotJobRunning(vm, data->job) < 0) + goto error; + + if (data->job->state == QEMU_BLOCKJOB_STATE_FAILED) { + virReportError(VIR_ERR_INTERNAL_ERROR, + _("block commit failed while deleting disk '%s' snapshot: '%s'"), + data->snapDisk->name, data->job->errmsg); + goto error; + } + } + + for (cur = externalData; cur; cur = g_slist_next(cur)) { + qemuSnapshotDeleteExternalData *data = cur->data; + + if (data->job->state == QEMU_BLOCKJOB_STATE_READY) { + if (qemuBlockPivot(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT, NULL) < 0) + goto error; + } else if (data->job->state == QEMU_BLOCKJOB_STATE_PENDING) { + if (qemuBlockFinalize(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT) < 0) + goto error; + } + + if (qemuSnapshotJobFinishing(vm, data->job) < 0) + goto error; + + if (data->job->state == QEMU_BLOCKJOB_STATE_FAILED) { + virReportError(VIR_ERR_INTERNAL_ERROR, + _("finishing block job failed while deleting disk '%s' snapshot: '%s'"), + data->snapDisk->name, data->job->errmsg); + goto error; + } + + qemuBlockJobSyncEnd(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT); + }
So 'externalData' is passed here from the caller, and the caller simply calls the equivalent of 'g_free' on the individual struct. Now this leaks the 'job' field allocated in [1] since you are still holding the reference.
+ + return 0; + + error: + for (cur = externalData; cur; cur = g_slist_next(cur)) { + qemuDomainObjPrivate *priv = vm->privateData; + qemuSnapshotDeleteExternalData *data = cur->data; + + if (!data->job) + continue; + + qemuBlockJobUpdate(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT); + + if (qemuSnapshotJobIsActive(data->job->state)) { + if (qemuDomainObjEnterMonitorAsync(vm, VIR_ASYNC_JOB_SNAPSHOT) == 0) { + ignore_value(qemuMonitorBlockJobCancel(priv->mon, data->job->name, false)); + qemuDomainObjExitMonitor(vm); + + data->job->state = QEMU_BLOCKJOB_STATE_ABORTING; + } + } + + qemuBlockJobSyncEnd(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT); + }
And in this code path too.
+ + return -1; +} + + static int qemuSnapshotDiscardMetadata(virDomainObj *vm, virDomainMomentObj *snap,

With external snapshots we need to modify the metadata bit more then what is required for internal snapshots. Mainly the storage source location changes with every external snapshot. This means that if we delete non-leaf snapshot we need to update all children snapshots and modify the disk sources for all affected disks. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 165 +++++++++++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 17 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index c493a3e94f..3fbc3ce65f 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2394,6 +2394,127 @@ qemuSnapshotChildrenReparent(void *payload, } +typedef struct _qemuSnapshotUpdateDisksData qemuSnapshotUpdateDisksData; +struct _qemuSnapshotUpdateDisksData { + virDomainMomentObj *snap; + virDomainObj *vm; + int error; +}; + + +/** + * qemuSnapshotUpdateDisksSingle: + * @snap: snapshot object where we are updating disks + * @def: active or inactive definition from @snap + * @parentDef: parent snapshot object of snapshot that we are deleting + * @snapDisk: snapshot disk definition from snapshot we are deleting + * + * When deleting external snapshots we need to modify remaining metadata + * files stored by libvirt. + * + * The first part updates only metadata for external snapshots where we need + * to update the disk source in the domain definition stored within the + * snapshot metadata. There is no need to do it for internal snapshots as + * they don't create new disk files. + * + * The second part needs to be done for all metadata files. Both internal and + * external snapshot metadata files have in the domain definition backingStore + * that could contain the deleted disk. + * + * Returns 0 on success, -1 on error. + * */ +static int +qemuSnapshotUpdateDisksSingle(virDomainMomentObj *snap, + virDomainDef *def, + virDomainDef *parentDef, + virDomainSnapshotDiskDef *snapDisk) +{ + virDomainDiskDef *disk = NULL; + + if (!(disk = qemuDomainDiskByName(def, snapDisk->name))) + return -1; + + if (virDomainSnapshotIsExternal(snap)) { + virDomainDiskDef *parentDisk = NULL; + + if (!(parentDisk = qemuDomainDiskByName(parentDef, snapDisk->name))) + return -1; + + if (virStorageSourceIsSameLocation(snapDisk->src, disk->src)) { + virObjectUnref(disk->src); + disk->src = virStorageSourceCopy(parentDisk->src, false); + } + } + + if (disk->src->backingStore) { + virStorageSource *cur = disk->src; + virStorageSource *next = disk->src->backingStore; + + while (next) { + if (virStorageSourceIsSameLocation(snapDisk->src, next)) { + cur->backingStore = next->backingStore; + next->backingStore = NULL; + virObjectUnref(next); + break; + } + + cur = next; + next = cur->backingStore; + } + } + + return 0; +} + + +static int +qemuSnapshotDeleteUpdateDisks(void *payload, + const char *name G_GNUC_UNUSED, + void *opaque) +{ + virDomainMomentObj *snap = payload; + qemuSnapshotUpdateDisksData *data = opaque; + qemuDomainObjPrivate *priv = data->vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + virDomainSnapshotDef *snapdef = virDomainSnapshotObjGetDef(data->snap); + ssize_t i; + + for (i = 0; i < snapdef->ndisks; i++) { + virDomainSnapshotDiskDef *snapDisk = &(snapdef->disks[i]); + + if (snapDisk->snapshot == VIR_DOMAIN_SNAPSHOT_LOCATION_NO) + continue; + + if (qemuSnapshotUpdateDisksSingle(snap, snap->def->dom, + data->snap->def->dom, snapDisk) < 0) { + data->error = -1; + } + + if (snap->def->inactiveDom) { + virDomainDef *dom = data->snap->def->inactiveDom; + + if (!dom) + dom = data->snap->def->dom; + + if (qemuSnapshotUpdateDisksSingle(snap, snap->def->inactiveDom, + dom, snapDisk) < 0) { + data->error = -1; + } + } + } + + if (qemuDomainSnapshotWriteMetadata(data->vm, + snap, + driver->xmlopt, + cfg->snapshotDir) < 0) { + data->error = -1; + } + + return 0; +} + + /* Deleting external snapshot is started by running qemu block-commit job. * We need to wait for all block-commit jobs to be 'ready' or 'pending' to * continue with external snapshot deletion. */ @@ -2604,24 +2725,34 @@ qemuSnapshotDiscardMetadata(virDomainObj *vm, virQEMUDriver *driver = priv->driver; g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); g_autofree char *snapFile = NULL; + int ret = 0; - if (update_parent) { - if (snap->nchildren) { - virQEMUMomentReparent rep; + if (update_parent && snap->nchildren) { + virQEMUMomentReparent rep; + qemuSnapshotUpdateDisksData data; - rep.dir = cfg->snapshotDir; - rep.parent = snap->parent; - rep.vm = vm; - rep.err = 0; - rep.xmlopt = driver->xmlopt; - rep.writeMetadata = qemuDomainSnapshotWriteMetadata; - virDomainMomentForEachChild(snap, - qemuSnapshotChildrenReparent, - &rep); - if (rep.err < 0) - return -1; - virDomainMomentMoveChildren(snap, snap->parent); - } + rep.dir = cfg->snapshotDir; + rep.parent = snap->parent; + rep.vm = vm; + rep.err = 0; + rep.xmlopt = driver->xmlopt; + rep.writeMetadata = qemuDomainSnapshotWriteMetadata; + virDomainMomentForEachChild(snap, + qemuSnapshotChildrenReparent, + &rep); + if (rep.err < 0) + ret = -1; + + data.snap = snap; + data.vm = vm; + data.error = 0; + virDomainMomentForEachDescendant(snap, + qemuSnapshotDeleteUpdateDisks, + &data); + if (data.error < 0) + ret = -1; + + virDomainMomentMoveChildren(snap, snap->parent); } snapFile = g_strdup_printf("%s/%s/%s.xml", cfg->snapshotDir, vm->def->name, @@ -2655,7 +2786,7 @@ qemuSnapshotDiscardMetadata(virDomainObj *vm, virDomainMomentDropParent(snap); virDomainSnapshotObjListRemove(vm->snapshots, snap); - return 0; + return ret; } -- 2.38.1

On Thu, Dec 08, 2022 at 14:31:01 +0100, Pavel Hrdina wrote:
With external snapshots we need to modify the metadata bit more then what is required for internal snapshots. Mainly the storage source location changes with every external snapshot.
This means that if we delete non-leaf snapshot we need to update all children snapshots and modify the disk sources for all affected disks.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 165 +++++++++++++++++++++++++++++++++++----
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

When deleting external snapshots the operation may fail at any point which could lead to situation that some disks finished the block commit operation but for some disks it failed and the libvirt job ends. In order to make sure that the qcow2 images are in consistent state introduce new element "<invalid/>" that will mark the disk in snapshot metadata as invalid until the snapshot delete is completed successfully. This will prevent deleting snapshot with the invalid disk and in future reverting to snapshot with the invalid disk. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/conf/snapshot_conf.c | 6 ++++ src/conf/snapshot_conf.h | 1 + src/qemu/qemu_snapshot.c | 59 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/src/conf/snapshot_conf.c b/src/conf/snapshot_conf.c index 4b5b908d66..7517208d79 100644 --- a/src/conf/snapshot_conf.c +++ b/src/conf/snapshot_conf.c @@ -158,6 +158,9 @@ virDomainSnapshotDiskDefParseXML(xmlNodePtr node, return -1; } + if (flags & VIR_DOMAIN_DEF_PARSE_STATUS) + def->invalid = !!virXPathNode("./invalid", ctxt); + if ((cur = virXPathNode("./source", ctxt)) && virDomainStorageSourceParse(cur, ctxt, src, flags, xmlopt) < 0) return -1; @@ -744,6 +747,9 @@ virDomainSnapshotDiskDefFormat(virBuffer *buf, virBufferAsprintf(&attrBuf, " snapshot='%s'", virDomainSnapshotLocationTypeToString(disk->snapshot)); + if (disk->invalid) + virBufferAddLit(&childBuf, "<invalid/>\n"); + if (disk->src->path || disk->src->format != 0) { g_auto(virBuffer) driverAttrBuf = VIR_BUFFER_INITIALIZER; g_auto(virBuffer) driverChildBuf = VIR_BUFFER_INIT_CHILD(&childBuf); diff --git a/src/conf/snapshot_conf.h b/src/conf/snapshot_conf.h index fec4a5a912..dfab38e7d2 100644 --- a/src/conf/snapshot_conf.h +++ b/src/conf/snapshot_conf.h @@ -52,6 +52,7 @@ typedef struct _virDomainSnapshotDiskDef virDomainSnapshotDiskDef; struct _virDomainSnapshotDiskDef { char *name; /* name matching the <target dev='...' of the domain */ virDomainSnapshotLocation snapshot; + bool invalid; /* details of wrapper external file. src is always non-NULL. * XXX optimize this to allow NULL for internal snapshots? */ diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 3fbc3ce65f..253de1196b 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2302,6 +2302,12 @@ qemuSnapshotDeleteExternalPrepare(virDomainObj *vm, if (snapDisk->snapshot == VIR_DOMAIN_SNAPSHOT_LOCATION_NO) continue; + if (snapDisk->invalid) { + virReportError(VIR_ERR_OPERATION_INVALID, "%s", + _("snapshot is in invalid state")); + return NULL; + } + data = g_new0(qemuSnapshotDeleteExternalData, 1); data->snapDisk = snapDisk; @@ -2620,6 +2626,53 @@ qemuSnapshotJobFinishing(virDomainObj *vm, } +/** + * qemuSnapshotSetInvalid: + * @vm: vm object + * @snap: snapshot object that contains parent disk + * @disk: disk from the snapshot we are deleting + * @invalid: boolean to set/unset invalid state + * + * @snap should point to a ancestor snapshot from the snapshot tree that + * affected the @disk which doesn't have to be the direct parent. + * + * When setting snapshot with parent disk as invalid due to snapshot being + * deleted we should not mark the whole snapshot as invalid but only the + * affected disks because the snapshot can contain other disks that we are + * not modifying at the moment. + * + * Return 0 on success, -1 on error. + * */ +static int +qemuSnapshotSetInvalid(virDomainObj *vm, + virDomainMomentObj *snap, + virDomainSnapshotDiskDef *disk, + bool invalid) +{ + ssize_t i; + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + virDomainSnapshotDef *snapdef = NULL; + + if (!snap) + return 0; + + snapdef = virDomainSnapshotObjGetDef(snap); + if (!snapdef) + return 0; + + for (i = 0; i < snapdef->ndisks; i++) { + virDomainSnapshotDiskDef *snapDisk = &(snapdef->disks[i]); + + if (STREQ(snapDisk->name, disk->name)) + snapDisk->invalid = invalid; + } + + return qemuDomainSnapshotWriteMetadata(vm, snap, driver->xmlopt, cfg->snapshotDir); +} + + static int qemuSnapshotDiscardExternal(virDomainObj *vm, GSList *externalData) @@ -2636,6 +2689,9 @@ qemuSnapshotDiscardExternal(virDomainObj *vm, autofinalize = VIR_TRISTATE_BOOL_YES; } + if (qemuSnapshotSetInvalid(vm, data->parentSnap, data->snapDisk, true) < 0) + goto error; + data->job = qemuBlockCommit(vm, data->domDisk, data->parentDiskSrc, @@ -2686,6 +2742,9 @@ qemuSnapshotDiscardExternal(virDomainObj *vm, } qemuBlockJobSyncEnd(vm, data->job, VIR_ASYNC_JOB_SNAPSHOT); + + if (qemuSnapshotSetInvalid(vm, data->parentSnap, data->snapDisk, false) < 0) + goto error; } return 0; -- 2.38.1

On Thu, Dec 08, 2022 at 14:31:02 +0100, Pavel Hrdina wrote:
When deleting external snapshots the operation may fail at any point which could lead to situation that some disks finished the block commit operation but for some disks it failed and the libvirt job ends.
In order to make sure that the qcow2 images are in consistent state introduce new element "<invalid/>" that will mark the disk in snapshot metadata as invalid until the snapshot delete is completed successfully.
This will prevent deleting snapshot with the invalid disk and in future reverting to snapshot with the invalid disk.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/conf/snapshot_conf.c | 6 ++++ src/conf/snapshot_conf.h | 1 + src/qemu/qemu_snapshot.c | 59 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+)
diff --git a/src/conf/snapshot_conf.c b/src/conf/snapshot_conf.c index 4b5b908d66..7517208d79 100644 --- a/src/conf/snapshot_conf.c +++ b/src/conf/snapshot_conf.c @@ -158,6 +158,9 @@ virDomainSnapshotDiskDefParseXML(xmlNodePtr node, return -1; }
+ if (flags & VIR_DOMAIN_DEF_PARSE_STATUS)
You want VIR_DOMAIN_SNAPSHOT_PARSE_REDEFINE as this parser doesn't work with VIR_DOMAIN_DEF_PARSE_* flags. In fact the only reason this works is that VIR_DOMAIN_DEF_PARSE_STATUS and VIR_DOMAIN_SNAPSHOT_PARSE_REDEFINE have the same value.
+ def->invalid = !!virXPathNode("./invalid", ctxt);
Based on my coment below ... the 'invalid' name of this flag might be a bit misleading. I suppose we want to say that there's another operation in progress, but I don't have currently ideas for a better name.
+ if ((cur = virXPathNode("./source", ctxt)) && virDomainStorageSourceParse(cur, ctxt, src, flags, xmlopt) < 0) return -1;
[...]
diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 3fbc3ce65f..253de1196b 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -2302,6 +2302,12 @@ qemuSnapshotDeleteExternalPrepare(virDomainObj *vm, if (snapDisk->snapshot == VIR_DOMAIN_SNAPSHOT_LOCATION_NO) continue;
+ if (snapDisk->invalid) { + virReportError(VIR_ERR_OPERATION_INVALID, "%s", + _("snapshot is in invalid state"));
This error is not really actionable. You want to explain that it's most likely a previous operation that failed and needs to be re-tried.
+ return NULL; + } + data = g_new0(qemuSnapshotDeleteExternalData, 1); data->snapDisk = snapDisk;
@@ -2620,6 +2626,53 @@ qemuSnapshotJobFinishing(virDomainObj *vm, }
+/** + * qemuSnapshotSetInvalid: + * @vm: vm object + * @snap: snapshot object that contains parent disk + * @disk: disk from the snapshot we are deleting + * @invalid: boolean to set/unset invalid state + * + * @snap should point to a ancestor snapshot from the snapshot tree that + * affected the @disk which doesn't have to be the direct parent. + * + * When setting snapshot with parent disk as invalid due to snapshot being + * deleted we should not mark the whole snapshot as invalid but only the + * affected disks because the snapshot can contain other disks that we are + * not modifying at the moment. + * + * Return 0 on success, -1 on error. + * */ +static int +qemuSnapshotSetInvalid(virDomainObj *vm, + virDomainMomentObj *snap, + virDomainSnapshotDiskDef *disk, + bool invalid) +{ + ssize_t i; + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + virDomainSnapshotDef *snapdef = NULL; + + if (!snap) + return 0; + + snapdef = virDomainSnapshotObjGetDef(snap); + if (!snapdef) + return 0; + + for (i = 0; i < snapdef->ndisks; i++) { + virDomainSnapshotDiskDef *snapDisk = &(snapdef->disks[i]); + + if (STREQ(snapDisk->name, disk->name)) + snapDisk->invalid = invalid; + }
I think you also want to mark the inactiveDef if present.
+ + return qemuDomainSnapshotWriteMetadata(vm, snap, driver->xmlopt, cfg->snapshotDir); +}

If the daemon crashes or is restarted while the snapshot delete is in progress we have to handle it gracefully to not leave any block jobs active. For now we will simply abort the snapshot delete operation so user can start it again. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_process.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/qemu/qemu_process.c b/src/qemu/qemu_process.c index 5de55435d2..cc23b4a799 100644 --- a/src/qemu/qemu_process.c +++ b/src/qemu/qemu_process.c @@ -3677,6 +3677,37 @@ qemuProcessRecoverMigration(virQEMUDriver *driver, } +static void +qemuProcessAbortSnapshotDelete(virDomainObj *vm) +{ + size_t i; + qemuDomainObjPrivate *priv = vm->privateData; + + for (i = 0; i < vm->def->ndisks; i++) { + virDomainDiskDef *disk = vm->def->disks[i]; + g_autoptr(qemuBlockJobData) diskJob = qemuBlockJobDiskGetJob(disk); + + if (!diskJob) + continue; + + if (diskJob->type != QEMU_BLOCKJOB_TYPE_COMMIT && + diskJob->type != QEMU_BLOCKJOB_TYPE_ACTIVE_COMMIT) { + continue; + } + + qemuBlockJobSyncBegin(diskJob); + + qemuDomainObjEnterMonitor(vm); + ignore_value(qemuMonitorBlockJobCancel(priv->mon, diskJob->name, false)); + qemuDomainObjExitMonitor(vm); + + diskJob->state = QEMU_BLOCKJOB_STATE_ABORTING; + + qemuBlockJobSyncEnd(vm, diskJob, VIR_ASYNC_JOB_NONE); + } +} + + static int qemuProcessRecoverJob(virQEMUDriver *driver, virDomainObj *vm, @@ -3726,6 +3757,7 @@ qemuProcessRecoverJob(virQEMUDriver *driver, vm->def->name); } } + qemuProcessAbortSnapshotDelete(vm); break; case VIR_ASYNC_JOB_START: -- 2.38.1

On Thu, Dec 08, 2022 at 14:31:03 +0100, Pavel Hrdina wrote:
If the daemon crashes or is restarted while the snapshot delete is in progress we have to handle it gracefully to not leave any block jobs active.
For now we will simply abort the snapshot delete operation so user can start it again.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_process.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+)
diff --git a/src/qemu/qemu_process.c b/src/qemu/qemu_process.c index 5de55435d2..cc23b4a799 100644 --- a/src/qemu/qemu_process.c +++ b/src/qemu/qemu_process.c @@ -3677,6 +3677,37 @@ qemuProcessRecoverMigration(virQEMUDriver *driver, }
+static void +qemuProcessAbortSnapshotDelete(virDomainObj *vm) +{ + size_t i; + qemuDomainObjPrivate *priv = vm->privateData; + + for (i = 0; i < vm->def->ndisks; i++) { + virDomainDiskDef *disk = vm->def->disks[i]; + g_autoptr(qemuBlockJobData) diskJob = qemuBlockJobDiskGetJob(disk); + + if (!diskJob) + continue; + + if (diskJob->type != QEMU_BLOCKJOB_TYPE_COMMIT && + diskJob->type != QEMU_BLOCKJOB_TYPE_ACTIVE_COMMIT) { + continue; + } + + qemuBlockJobSyncBegin(diskJob); + + qemuDomainObjEnterMonitor(vm); + ignore_value(qemuMonitorBlockJobCancel(priv->mon, diskJob->name, false)); + qemuDomainObjExitMonitor(vm); + + diskJob->state = QEMU_BLOCKJOB_STATE_ABORTING; + + qemuBlockJobSyncEnd(vm, diskJob, VIR_ASYNC_JOB_NONE); + } +} + + static int qemuProcessRecoverJob(virQEMUDriver *driver, virDomainObj *vm,
So assume that you have a VM where you've e.g. added a new disk after it had some snapshots. Now you have a running blockjob on the new disk. Now you try delete a snapshot. The un-related job could get restarted as this job doesn't distinguish between those situations. This is as a regular block-job doesn't even register as an async job. The simplest fix will be to call qemuDomainHasBlockjob and refuse the whole deletion. Once you add that to the appropriate place: Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qemu/qemu_snapshot.c b/src/qemu/qemu_snapshot.c index 253de1196b..68acf54917 100644 --- a/src/qemu/qemu_snapshot.c +++ b/src/qemu/qemu_snapshot.c @@ -3089,9 +3089,9 @@ qemuSnapshotDeleteValidate(virDomainObj *vm, } if (virDomainSnapshotIsExternal(snap) && - !(flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY)) { + (flags & VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN)) { virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s", - _("deletion of external disk snapshots not supported")); + _("deletion of external disk snapshots with children not supported")); return -1; } -- 2.38.1

On Thu, Dec 08, 2022 at 14:31:04 +0100, Pavel Hrdina wrote:
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/qemu/qemu_snapshot.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Now that deletion of external snapshot is implemented document the current virDomainSnapshotDelete supported state. Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/libvirt-domain-snapshot.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libvirt-domain-snapshot.c b/src/libvirt-domain-snapshot.c index 2917b8bd90..ab6a029915 100644 --- a/src/libvirt-domain-snapshot.c +++ b/src/libvirt-domain-snapshot.c @@ -1075,6 +1075,13 @@ virDomainRevertToSnapshot(virDomainSnapshotPtr snapshot, * libvirt metadata to track snapshots, then this flag is silently * ignored. * + * Since libvirt 9.0.0 deletion of external snapshots is supported + * for QEMU driver. Using @flags VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN + * and VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY is not supported with + * external snapshots. In case that daemon process is terminated + * while the snapshot delete is in process the operation will be + * aborted when the daemon starts again. + * * Returns 0 if the selected snapshot(s) were successfully deleted, * -1 on error. * -- 2.38.1

On Thu, Dec 08, 2022 at 14:31:05 +0100, Pavel Hrdina wrote:
Now that deletion of external snapshot is implemented document the current virDomainSnapshotDelete supported state.
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- src/libvirt-domain-snapshot.c | 7 +++++++ 1 file changed, 7 insertions(+)
diff --git a/src/libvirt-domain-snapshot.c b/src/libvirt-domain-snapshot.c index 2917b8bd90..ab6a029915 100644 --- a/src/libvirt-domain-snapshot.c +++ b/src/libvirt-domain-snapshot.c @@ -1075,6 +1075,13 @@ virDomainRevertToSnapshot(virDomainSnapshotPtr snapshot, * libvirt metadata to track snapshots, then this flag is silently * ignored. * + * Since libvirt 9.0.0 deletion of external snapshots is supported + * for QEMU driver. Using @flags VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN + * and VIR_DOMAIN_SNAPSHOT_DELETE_CHILDREN_ONLY is not supported with + * external snapshots. In case that daemon process is terminated + * while the snapshot delete is in process the operation will be + * aborted when the daemon starts again. + *
This doesn't look like the best place for this kind of documentation but I don't have a better suggestion. Reviewed-by: Peter Krempa <pkrempa@redhat.com>

Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- NEWS.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 39f508a6ce..fecca64344 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -17,6 +17,12 @@ v9.0.0 (unreleased) * **New features** + * QEMU: implement external snapshot deletion + + External snapshot deletion is now possible using the existing API + ``virDomainSnapshotDelete()``. Flags that allow deleting children + or children only are not supported. + * **Improvements** * qemu: Prefer PNG for domain screenshots -- 2.38.1

On Thu, Dec 08, 2022 at 14:31:06 +0100, Pavel Hrdina wrote:
Signed-off-by: Pavel Hrdina <phrdina@redhat.com> --- NEWS.rst | 6 ++++++ 1 file changed, 6 insertions(+)
Reviewed-by: Peter Krempa <pkrempa@redhat.com>
participants (2)
-
Pavel Hrdina
-
Peter Krempa