diff options
author | Peter Maydell <peter.maydell@linaro.org> | 2021-09-02 13:00:52 +0100 |
---|---|---|
committer | Peter Maydell <peter.maydell@linaro.org> | 2021-09-02 13:00:52 +0100 |
commit | 9093028dd48c50bc0392791f78aab44afef57ead (patch) | |
tree | ac9ab96c78172fff29ac3da160f39e0bc012637e | |
parent | 59a89510b62ec23dbeab8b02fa4e3526e353d8b6 (diff) | |
parent | ebd979c74e2b8a7275090475df36dde4ab858320 (diff) |
Merge remote-tracking branch 'remotes/hreitz/tags/pull-block-2021-09-01' into staging
Block patches:
- Make the backup-top filter driver available for user-created block
nodes (i.e. via blockdev-add)
- Allow running iotests with gdb or valgrind being attached to qemu
instances
- Fix the raw format driver's permissions: There is no metadata, so we
only need WRITE or RESIZE when the parent needs it
- Basic reopen implementation for win32 files (file-win32.c) so that
qemu-img commit can work
- uclibc/musl build fix for the FUSE export code
- Some iotests delinting
- block-hmp-cmds.c refactoring
# gpg: Signature made Wed 01 Sep 2021 16:01:54 BST
# gpg: using RSA key CB62D7A0EE3829E45F004D34A1FA40D098019CDF
# gpg: issuer "hreitz@redhat.com"
# gpg: Good signature from "Hanna Reitz <hreitz@redhat.com>" [marginal]
# gpg: WARNING: This key is not certified with sufficiently trusted signatures!
# gpg: It is not certain that the signature belongs to the owner.
# Primary key fingerprint: CB62 D7A0 EE38 29E4 5F00 4D34 A1FA 40D0 9801 9CDF
* remotes/hreitz/tags/pull-block-2021-09-01: (56 commits)
block/file-win32: add reopen handlers
block/export/fuse.c: fix fuse-lseek on uclibc or musl
block/block-copy: block_copy_state_new(): drop extra arguments
iotests/image-fleecing: add test-case for copy-before-write filter
iotests/image-fleecing: prepare for adding new test-case
iotests/image-fleecing: rename tgt_node
iotests/image-fleecing: proper source device
iotests.py: hmp_qemu_io: support qdev
iotests: move 222 to tests/image-fleecing
iotests/222: constantly use single quotes for strings
iotests/222: fix pylint and mypy complains
python:QEMUMachine: template typing for self returning methods
python/qemu/machine: QEMUMachine: improve qmp() method
python/qemu/machine.py: refactor _qemu_args()
qapi: publish copy-before-write filter
block/copy-before-write: make public block driver
block/block-copy: make setting progress optional
block/copy-before-write: initialize block-copy bitmap
block/copy-before-write: cbw_init(): use options
block/copy-before-write: bdrv_cbw_append(): drop unused compress arg
...
Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
37 files changed, 1172 insertions, 711 deletions
diff --git a/.gitlab-ci.d/buildtest.yml b/.gitlab-ci.d/buildtest.yml index 903ee65f32..e74998efb9 100644 --- a/.gitlab-ci.d/buildtest.yml +++ b/.gitlab-ci.d/buildtest.yml @@ -305,11 +305,11 @@ build-tcg-disabled: - cd tests/qemu-iotests/ - ./check -raw 001 002 003 004 005 008 009 010 011 012 021 025 032 033 048 052 063 077 086 101 104 106 113 148 150 151 152 157 159 160 163 - 170 171 183 184 192 194 208 221 222 226 227 236 253 277 + 170 171 183 184 192 194 208 221 226 227 236 253 277 image-fleecing - ./check -qcow2 028 051 056 057 058 065 068 082 085 091 095 096 102 122 124 132 139 142 144 145 151 152 155 157 165 194 196 200 202 - 208 209 216 218 222 227 234 246 247 248 250 254 255 257 258 - 260 261 262 263 264 270 272 273 277 279 + 208 209 216 218 227 234 246 247 248 250 254 255 257 258 + 260 261 262 263 264 270 272 273 277 279 image-fleecing build-user: extends: .native_build_job_template diff --git a/MAINTAINERS b/MAINTAINERS index c8221650b8..e42e2028e5 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2391,8 +2391,8 @@ F: block/mirror.c F: qapi/job.json F: block/block-copy.c F: include/block/block-copy.c -F: block/backup-top.h -F: block/backup-top.c +F: block/copy-before-write.h +F: block/copy-before-write.c F: include/block/aio_task.h F: block/aio_task.c F: util/qemu-co-shared-resource.c @@ -5048,6 +5048,37 @@ out: return ret; } +/* Not for empty child */ +int bdrv_replace_child_bs(BdrvChild *child, BlockDriverState *new_bs, + Error **errp) +{ + int ret; + Transaction *tran = tran_new(); + g_autoptr(GHashTable) found = NULL; + g_autoptr(GSList) refresh_list = NULL; + BlockDriverState *old_bs = child->bs; + + bdrv_ref(old_bs); + bdrv_drained_begin(old_bs); + bdrv_drained_begin(new_bs); + + bdrv_replace_child_tran(child, new_bs, tran); + + found = g_hash_table_new(NULL, NULL); + refresh_list = bdrv_topological_dfs(refresh_list, found, old_bs); + refresh_list = bdrv_topological_dfs(refresh_list, found, new_bs); + + ret = bdrv_list_refresh_perms(refresh_list, NULL, tran, errp); + + tran_finalize(tran, ret); + + bdrv_drained_end(old_bs); + bdrv_drained_end(new_bs); + bdrv_unref(old_bs); + + return ret; +} + static void bdrv_delete(BlockDriverState *bs) { assert(bdrv_op_blocker_is_empty(bs)); diff --git a/block/backup-top.c b/block/backup-top.c deleted file mode 100644 index 425e3778be..0000000000 --- a/block/backup-top.c +++ /dev/null @@ -1,253 +0,0 @@ -/* - * backup-top filter driver - * - * The driver performs Copy-Before-Write (CBW) operation: it is injected above - * some node, and before each write it copies _old_ data to the target node. - * - * Copyright (c) 2018-2019 Virtuozzo International GmbH. - * - * Author: - * Sementsov-Ogievskiy Vladimir <vsementsov@virtuozzo.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include "qemu/osdep.h" - -#include "sysemu/block-backend.h" -#include "qemu/cutils.h" -#include "qapi/error.h" -#include "block/block_int.h" -#include "block/qdict.h" -#include "block/block-copy.h" - -#include "block/backup-top.h" - -typedef struct BDRVBackupTopState { - BlockCopyState *bcs; - BdrvChild *target; - int64_t cluster_size; -} BDRVBackupTopState; - -static coroutine_fn int backup_top_co_preadv( - BlockDriverState *bs, uint64_t offset, uint64_t bytes, - QEMUIOVector *qiov, int flags) -{ - return bdrv_co_preadv(bs->backing, offset, bytes, qiov, flags); -} - -static coroutine_fn int backup_top_cbw(BlockDriverState *bs, uint64_t offset, - uint64_t bytes, BdrvRequestFlags flags) -{ - BDRVBackupTopState *s = bs->opaque; - uint64_t off, end; - - if (flags & BDRV_REQ_WRITE_UNCHANGED) { - return 0; - } - - off = QEMU_ALIGN_DOWN(offset, s->cluster_size); - end = QEMU_ALIGN_UP(offset + bytes, s->cluster_size); - - return block_copy(s->bcs, off, end - off, true); -} - -static int coroutine_fn backup_top_co_pdiscard(BlockDriverState *bs, - int64_t offset, int bytes) -{ - int ret = backup_top_cbw(bs, offset, bytes, 0); - if (ret < 0) { - return ret; - } - - return bdrv_co_pdiscard(bs->backing, offset, bytes); -} - -static int coroutine_fn backup_top_co_pwrite_zeroes(BlockDriverState *bs, - int64_t offset, int bytes, BdrvRequestFlags flags) -{ - int ret = backup_top_cbw(bs, offset, bytes, flags); - if (ret < 0) { - return ret; - } - - return bdrv_co_pwrite_zeroes(bs->backing, offset, bytes, flags); -} - -static coroutine_fn int backup_top_co_pwritev(BlockDriverState *bs, - uint64_t offset, - uint64_t bytes, - QEMUIOVector *qiov, int flags) -{ - int ret = backup_top_cbw(bs, offset, bytes, flags); - if (ret < 0) { - return ret; - } - - return bdrv_co_pwritev(bs->backing, offset, bytes, qiov, flags); -} - -static int coroutine_fn backup_top_co_flush(BlockDriverState *bs) -{ - if (!bs->backing) { - return 0; - } - - return bdrv_co_flush(bs->backing->bs); -} - -static void backup_top_refresh_filename(BlockDriverState *bs) -{ - if (bs->backing == NULL) { - /* - * we can be here after failed bdrv_attach_child in - * bdrv_set_backing_hd - */ - return; - } - pstrcpy(bs->exact_filename, sizeof(bs->exact_filename), - bs->backing->bs->filename); -} - -static void backup_top_child_perm(BlockDriverState *bs, BdrvChild *c, - BdrvChildRole role, - BlockReopenQueue *reopen_queue, - uint64_t perm, uint64_t shared, - uint64_t *nperm, uint64_t *nshared) -{ - if (!(role & BDRV_CHILD_FILTERED)) { - /* - * Target child - * - * Share write to target (child_file), to not interfere - * with guest writes to its disk which may be in target backing chain. - * Can't resize during a backup block job because we check the size - * only upfront. - */ - *nshared = BLK_PERM_ALL & ~BLK_PERM_RESIZE; - *nperm = BLK_PERM_WRITE; - } else { - /* Source child */ - bdrv_default_perms(bs, c, role, reopen_queue, - perm, shared, nperm, nshared); - - if (perm & BLK_PERM_WRITE) { - *nperm = *nperm | BLK_PERM_CONSISTENT_READ; - } - *nshared &= ~(BLK_PERM_WRITE | BLK_PERM_RESIZE); - } -} - -BlockDriver bdrv_backup_top_filter = { - .format_name = "backup-top", - .instance_size = sizeof(BDRVBackupTopState), - - .bdrv_co_preadv = backup_top_co_preadv, - .bdrv_co_pwritev = backup_top_co_pwritev, - .bdrv_co_pwrite_zeroes = backup_top_co_pwrite_zeroes, - .bdrv_co_pdiscard = backup_top_co_pdiscard, - .bdrv_co_flush = backup_top_co_flush, - - .bdrv_refresh_filename = backup_top_refresh_filename, - - .bdrv_child_perm = backup_top_child_perm, - - .is_filter = true, -}; - -BlockDriverState *bdrv_backup_top_append(BlockDriverState *source, - BlockDriverState *target, - const char *filter_node_name, - uint64_t cluster_size, - BackupPerf *perf, - BdrvRequestFlags write_flags, - BlockCopyState **bcs, - Error **errp) -{ - ERRP_GUARD(); - int ret; - BDRVBackupTopState *state; - BlockDriverState *top; - bool appended = false; - - assert(source->total_sectors == target->total_sectors); - - top = bdrv_new_open_driver(&bdrv_backup_top_filter, filter_node_name, - BDRV_O_RDWR, errp); - if (!top) { - return NULL; - } - - state = top->opaque; - top->total_sectors = source->total_sectors; - top->supported_write_flags = BDRV_REQ_WRITE_UNCHANGED | - (BDRV_REQ_FUA & source->supported_write_flags); - top->supported_zero_flags = BDRV_REQ_WRITE_UNCHANGED | - ((BDRV_REQ_FUA | BDRV_REQ_MAY_UNMAP | BDRV_REQ_NO_FALLBACK) & - source->supported_zero_flags); - - bdrv_ref(target); - state->target = bdrv_attach_child(top, target, "target", &child_of_bds, - BDRV_CHILD_DATA, errp); - if (!state->target) { - bdrv_unref(target); - bdrv_unref(top); - return NULL; - } - - bdrv_drained_begin(source); - - ret = bdrv_append(top, source, errp); - if (ret < 0) { - error_prepend(errp, "Cannot append backup-top filter: "); - goto fail; - } - appended = true; - - state->cluster_size = cluster_size; - state->bcs = block_copy_state_new(top->backing, state->target, - cluster_size, perf->use_copy_range, - write_flags, errp); - if (!state->bcs) { - error_prepend(errp, "Cannot create block-copy-state: "); - goto fail; - } - *bcs = state->bcs; - - bdrv_drained_end(source); - - return top; - -fail: - if (appended) { - bdrv_backup_top_drop(top); - } else { - bdrv_unref(top); - } - - bdrv_drained_end(source); - - return NULL; -} - -void bdrv_backup_top_drop(BlockDriverState *bs) -{ - BDRVBackupTopState *s = bs->opaque; - - bdrv_drop_filter(bs, &error_abort); - - block_copy_state_free(s->bcs); - - bdrv_unref(bs); -} diff --git a/block/backup.c b/block/backup.c index bd3614ce70..687d2882bc 100644 --- a/block/backup.c +++ b/block/backup.c @@ -27,13 +27,11 @@ #include "qemu/bitmap.h" #include "qemu/error-report.h" -#include "block/backup-top.h" - -#define BACKUP_CLUSTER_SIZE_DEFAULT (1 << 16) +#include "block/copy-before-write.h" typedef struct BackupBlockJob { BlockJob common; - BlockDriverState *backup_top; + BlockDriverState *cbw; BlockDriverState *source_bs; BlockDriverState *target_bs; @@ -104,7 +102,7 @@ static void backup_clean(Job *job) { BackupBlockJob *s = container_of(job, BackupBlockJob, common.job); block_job_remove_all_bdrv(&s->common); - bdrv_backup_top_drop(s->backup_top); + bdrv_cbw_drop(s->cbw); } void backup_do_checkpoint(BlockJob *job, Error **errp) @@ -235,18 +233,16 @@ static void backup_init_bcs_bitmap(BackupBlockJob *job) BdrvDirtyBitmap *bcs_bitmap = block_copy_dirty_bitmap(job->bcs); if (job->sync_mode == MIRROR_SYNC_MODE_BITMAP) { + bdrv_clear_dirty_bitmap(bcs_bitmap, NULL); ret = bdrv_dirty_bitmap_merge_internal(bcs_bitmap, job->sync_bitmap, NULL, true); assert(ret); - } else { - if (job->sync_mode == MIRROR_SYNC_MODE_TOP) { - /* - * We can't hog the coroutine to initialize this thoroughly. - * Set a flag and resume work when we are able to yield safely. - */ - block_copy_set_skip_unallocated(job->bcs, true); - } - bdrv_set_dirty_bitmap(bcs_bitmap, 0, job->len); + } else if (job->sync_mode == MIRROR_SYNC_MODE_TOP) { + /* + * We can't hog the coroutine to initialize this thoroughly. + * Set a flag and resume work when we are able to yield safely. + */ + block_copy_set_skip_unallocated(job->bcs, true); } estimate = bdrv_get_dirty_count(bcs_bitmap); @@ -354,43 +350,6 @@ static const BlockJobDriver backup_job_driver = { .set_speed = backup_set_speed, }; -static int64_t backup_calculate_cluster_size(BlockDriverState *target, - Error **errp) -{ - int ret; - BlockDriverInfo bdi; - bool target_does_cow = bdrv_backing_chain_next(target); - - /* - * If there is no backing file on the target, we cannot rely on COW if our - * backup cluster size is smaller than the target cluster size. Even for - * targets with a backing file, try to avoid COW if possible. - */ - ret = bdrv_get_info(target, &bdi); - if (ret == -ENOTSUP && !target_does_cow) { - /* Cluster size is not defined */ - warn_report("The target block device doesn't provide " - "information about the block size and it doesn't have a " - "backing file. The default block size of %u bytes is " - "used. If the actual block size of the target exceeds " - "this default, the backup may be unusable", - BACKUP_CLUSTER_SIZE_DEFAULT); - return BACKUP_CLUSTER_SIZE_DEFAULT; - } else if (ret < 0 && !target_does_cow) { - error_setg_errno(errp, -ret, - "Couldn't determine the cluster size of the target image, " - "which has no backing file"); - error_append_hint(errp, - "Aborting, since this may create an unusable destination image\n"); - return ret; - } else if (ret < 0 && target_does_cow) { - /* Not fatal; just trudge on ahead. */ - return BACKUP_CLUSTER_SIZE_DEFAULT; - } - - return MAX(BACKUP_CLUSTER_SIZE_DEFAULT, bdi.cluster_size); -} - BlockJob *backup_job_create(const char *job_id, BlockDriverState *bs, BlockDriverState *target, int64_t speed, MirrorSyncMode sync_mode, BdrvDirtyBitmap *sync_bitmap, @@ -407,8 +366,7 @@ BlockJob *backup_job_create(const char *job_id, BlockDriverState *bs, int64_t len, target_len; BackupBlockJob *job = NULL; int64_t cluster_size; - BdrvRequestFlags write_flags; - BlockDriverState *backup_top = NULL; + BlockDriverState *cbw = NULL; BlockCopyState *bcs = NULL; assert(bs); @@ -449,11 +407,6 @@ BlockJob *backup_job_create(const char *job_id, BlockDriverState *bs, return NULL; } - cluster_size = backup_calculate_cluster_size(target, errp); - if (cluster_size < 0) { - goto error; - } - if (perf->max_workers < 1) { error_setg(errp, "max-workers must be greater than zero"); return NULL; @@ -465,13 +418,6 @@ BlockJob *backup_job_create(const char *job_id, BlockDriverState *bs, return NULL; } - if (perf->max_chunk && perf->max_chunk < cluster_size) { - error_setg(errp, "Required max-chunk (%" PRIi64 ") is less than backup " - "cluster size (%" PRIi64 ")", perf->max_chunk, cluster_size); - return NULL; - } - - if (sync_bitmap) { /* If we need to write to this bitmap, check that we can: */ if (bitmap_mode != BITMAP_SYNC_MODE_NEVER && @@ -504,39 +450,28 @@ BlockJob *backup_job_create(const char *job_id, BlockDriverState *bs, goto error; } - /* - * If source is in backing chain of target assume that target is going to be - * used for "image fleecing", i.e. it should represent a kind of snapshot of - * source at backup-start point in time. And target is going to be read by - * somebody (for example, used as NBD export) during backup job. - * - * In this case, we need to add BDRV_REQ_SERIALISING write flag to avoid - * intersection of backup writes and third party reads from target, - * otherwise reading from target we may occasionally read already updated by - * guest data. - * - * For more information see commit f8d59dfb40bb and test - * tests/qemu-iotests/222 - */ - write_flags = (bdrv_chain_contains(target, bs) ? BDRV_REQ_SERIALISING : 0) | - (compress ? BDRV_REQ_WRITE_COMPRESSED : 0), + cbw = bdrv_cbw_append(bs, target, filter_node_name, &bcs, errp); + if (!cbw) { + goto error; + } - backup_top = bdrv_backup_top_append(bs, target, filter_node_name, - cluster_size, perf, - write_flags, &bcs, errp); - if (!backup_top) { + cluster_size = block_copy_cluster_size(bcs); + + if (perf->max_chunk && perf->max_chunk < cluster_size) { + error_setg(errp, "Required max-chunk (%" PRIi64 ") is less than backup " + "cluster size (%" PRIi64 ")", perf->max_chunk, cluster_size); goto error; } /* job->len is fixed, so we can't allow resize */ - job = block_job_create(job_id, &backup_job_driver, txn, backup_top, + job = block_job_create(job_id, &backup_job_driver, txn, cbw, 0, BLK_PERM_ALL, speed, creation_flags, cb, opaque, errp); if (!job) { goto error; } - job->backup_top = backup_top; + job->cbw = cbw; job->source_bs = bs; job->target_bs = target; job->on_source_error = on_source_error; @@ -549,10 +484,11 @@ BlockJob *backup_job_create(const char *job_id, BlockDriverState *bs, job->len = len; job->perf = *perf; + block_copy_set_copy_opts(bcs, perf->use_copy_range, compress); block_copy_set_progress_meter(bcs, &job->common.job.progress); block_copy_set_speed(bcs, speed); - /* Required permissions are already taken by backup-top target */ + /* Required permissions are taken by copy-before-write filter target */ block_job_add_bdrv(&job->common, "target", target, 0, BLK_PERM_ALL, &error_abort); @@ -562,8 +498,8 @@ BlockJob *backup_job_create(const char *job_id, BlockDriverState *bs, if (sync_bitmap) { bdrv_reclaim_dirty_bitmap(sync_bitmap, NULL); } - if (backup_top) { - bdrv_backup_top_drop(backup_top); + if (cbw) { + bdrv_cbw_drop(cbw); } return NULL; diff --git a/block/block-backend.c b/block/block-backend.c index deb55c272e..6140d133e2 100644 --- a/block/block-backend.c +++ b/block/block-backend.c @@ -870,6 +870,14 @@ int blk_insert_bs(BlockBackend *blk, BlockDriverState *bs, Error **errp) } /* + * Change BlockDriverState associated with @blk. + */ +int blk_replace_bs(BlockBackend *blk, BlockDriverState *new_bs, Error **errp) +{ + return bdrv_replace_child_bs(blk->root, new_bs, errp); +} + +/* * Sets the permission bitmasks that the user of the BlockBackend needs. */ int blk_set_perm(BlockBackend *blk, uint64_t perm, uint64_t shared_perm, diff --git a/block/block-copy.c b/block/block-copy.c index 0becad52da..ce116318b5 100644 --- a/block/block-copy.c +++ b/block/block-copy.c @@ -21,12 +21,14 @@ #include "qemu/units.h" #include "qemu/coroutine.h" #include "block/aio_task.h" +#include "qemu/error-report.h" #define BLOCK_COPY_MAX_COPY_RANGE (16 * MiB) #define BLOCK_COPY_MAX_BUFFER (1 * MiB) #define BLOCK_COPY_MAX_MEM (128 * MiB) #define BLOCK_COPY_MAX_WORKERS 64 #define BLOCK_COPY_SLICE_TIME 100000000ULL /* ns */ +#define BLOCK_COPY_CLUSTER_SIZE_DEFAULT (1 << 16) typedef enum { COPY_READ_WRITE_CLUSTER, @@ -290,9 +292,11 @@ static void coroutine_fn block_copy_task_end(BlockCopyTask *task, int ret) bdrv_set_dirty_bitmap(task->s->copy_bitmap, task->offset, task->bytes); } QLIST_REMOVE(task, list); - progress_set_remaining(task->s->progress, - bdrv_get_dirty_count(task->s->copy_bitmap) + - task->s->in_flight_bytes); + if (task->s->progress) { + progress_set_remaining(task->s->progress, + bdrv_get_dirty_count(task->s->copy_bitmap) + + task->s->in_flight_bytes); + } qemu_co_queue_restart_all(&task->wait_queue); } @@ -315,12 +319,82 @@ static uint32_t block_copy_max_transfer(BdrvChild *source, BdrvChild *target) target->bs->bl.max_transfer)); } +void block_copy_set_copy_opts(BlockCopyState *s, bool use_copy_range, + bool compress) +{ + /* Keep BDRV_REQ_SERIALISING set (or not set) in block_copy_state_new() */ + s->write_flags = (s->write_flags & BDRV_REQ_SERIALISING) | + (compress ? BDRV_REQ_WRITE_COMPRESSED : 0); + + if (s->max_transfer < s->cluster_size) { + /* + * copy_range does not respect max_transfer. We don't want to bother + * with requests smaller than block-copy cluster size, so fallback to + * buffered copying (read and write respect max_transfer on their + * behalf). + */ + s->method = COPY_READ_WRITE_CLUSTER; + } else if (compress) { + /* Compression supports only cluster-size writes and no copy-range. */ + s->method = COPY_READ_WRITE_CLUSTER; + } else { + /* + * If copy range enabled, start with COPY_RANGE_SMALL, until first + * successful copy_range (look at block_copy_do_copy). + */ + s->method = use_copy_range ? COPY_RANGE_SMALL : COPY_READ_WRITE; + } +} + +static int64_t block_copy_calculate_cluster_size(BlockDriverState *target, + Error **errp) +{ + int ret; + BlockDriverInfo bdi; + bool target_does_cow = bdrv_backing_chain_next(target); + + /* + * If there is no backing file on the target, we cannot rely on COW if our + * backup cluster size is smaller than the target cluster size. Even for + * targets with a backing file, try to avoid COW if possible. + */ + ret = bdrv_get_info(target, &bdi); + if (ret == -ENOTSUP && !target_does_cow) { + /* Cluster size is not defined */ + warn_report("The target block device doesn't provide " + "information about the block size and it doesn't have a " + "backing file. The default block size of %u bytes is " + "used. If the actual block size of the target exceeds " + "this default, the backup may be unusable", + BLOCK_COPY_CLUSTER_SIZE_DEFAULT); + return BLOCK_COPY_CLUSTER_SIZE_DEFAULT; + } else if (ret < 0 && !target_does_cow) { + error_setg_errno(errp, -ret, + "Couldn't determine the cluster size of the target image, " + "which has no backing file"); + error_append_hint(errp, + "Aborting, since this may create an unusable destination image\n"); + return ret; + } else if (ret < 0 && target_does_cow) { + /* Not fatal; just trudge on ahead. */ + return BLOCK_COPY_CLUSTER_SIZE_DEFAULT; + } + + return MAX(BLOCK_COPY_CLUSTER_SIZE_DEFAULT, bdi.cluster_size); +} + BlockCopyState *block_copy_state_new(BdrvChild *source, BdrvChild *target, - int64_t cluster_size, bool use_copy_range, - BdrvRequestFlags write_flags, Error **errp) + Error **errp) { BlockCopyState *s; + int64_t cluster_size; BdrvDirtyBitmap *copy_bitmap; + bool is_fleecing; + + cluster_size = block_copy_calculate_cluster_size(target->bs, errp); + if (cluster_size < 0) { + return NULL; + } copy_bitmap = bdrv_create_dirty_bitmap(source->bs, cluster_size, NULL, errp); @@ -329,6 +403,22 @@ BlockCopyState *block_copy_state_new(BdrvChild *source, BdrvChild *target, } bdrv_disable_dirty_bitmap(copy_bitmap); + /* + * If source is in backing chain of target assume that target is going to be + * used for "image fleecing", i.e. it should represent a kind of snapshot of + * source at backup-start point in time. And target is going to be read by + * somebody (for example, used as NBD export) during backup job. + * + * In this case, we need to add BDRV_REQ_SERIALISING write flag to avoid + * intersection of backup writes and third party reads from target, + * otherwise reading from target we may occasionally read already updated by + * guest data. + * + * For more information see commit f8d59dfb40bb and test + * tests/qemu-iotests/222 + */ + is_fleecing = bdrv_chain_contains(target->bs, source->bs); + s = g_new(BlockCopyState, 1); *s = (BlockCopyState) { .source = source, @@ -336,31 +426,14 @@ BlockCopyState *block_copy_state_new(BdrvChild *source, BdrvChild *target, .copy_bitmap = copy_bitmap, .cluster_size = cluster_size, .len = bdrv_dirty_bitmap_size(copy_bitmap), - .write_flags = write_flags, + .write_flags = (is_fleecing ? BDRV_REQ_SERIALISING : 0), .mem = shres_create(BLOCK_COPY_MAX_MEM), .max_transfer = QEMU_ALIGN_DOWN( block_copy_max_transfer(source, target), cluster_size), }; - if (s->max_transfer < cluster_size) { - /* - * copy_range does not respect max_transfer. We don't want to bother - * with requests smaller than block-copy cluster size, so fallback to - * buffered copying (read and write respect max_transfer on their - * behalf). - */ - s->method = COPY_READ_WRITE_CLUSTER; - } else if (write_flags & BDRV_REQ_WRITE_COMPRESSED) { - /* Compression supports only cluster-size writes and no copy-range. */ - s->method = COPY_READ_WRITE_CLUSTER; - } else { - /* - * If copy range enabled, start with COPY_RANGE_SMALL, until first - * successful copy_range (look at block_copy_do_copy). - */ - s->method = use_copy_range ? COPY_RANGE_SMALL : COPY_READ_WRITE; - } + block_copy_set_copy_opts(s, false, false); ratelimit_init(&s->rate_limit); qemu_co_mutex_init(&s->lock); @@ -522,7 +595,7 @@ static coroutine_fn int block_copy_task_entry(AioTask *task) t->call_state->ret = ret; t->call_state->error_is_read = error_is_read; } - } else { + } else if (s->progress) { progress_work_done(s->progress, t->bytes); } } @@ -628,9 +701,11 @@ int64_t block_copy_reset_unallocated(BlockCopyState *s, if (!ret) { qemu_co_mutex_lock(&s->lock); bdrv_reset_dirty_bitmap(s->copy_bitmap, offset, bytes); - progress_set_remaining(s->progress, - bdrv_get_dirty_count(s->copy_bitmap) + - s->in_flight_bytes); + if (s->progress) { + progress_set_remaining(s->progress, + bdrv_get_dirty_count(s->copy_bitmap) + + s->in_flight_bytes); + } qemu_co_mutex_unlock(&s->lock); } @@ -933,6 +1008,11 @@ BdrvDirtyBitmap *block_copy_dirty_bitmap(BlockCopyState *s) return s->copy_bitmap; } +int64_t block_copy_cluster_size(BlockCopyState *s) +{ + return s->cluster_size; +} + void block_copy_set_skip_unallocated(BlockCopyState *s, bool skip) { qatomic_set(&s->skip_unallocated, skip); diff --git a/block/copy-before-write.c b/block/copy-before-write.c new file mode 100644 index 0000000000..2a5e57deca --- /dev/null +++ b/block/copy-before-write.c @@ -0,0 +1,256 @@ +/* + * copy-before-write filter driver + * + * The driver performs Copy-Before-Write (CBW) operation: it is injected above + * some node, and before each write it copies _old_ data to the target node. + * + * Copyright (c) 2018-2021 Virtuozzo International GmbH. + * + * Author: + * Sementsov-Ogievskiy Vladimir <vsementsov@virtuozzo.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "qemu/osdep.h" + +#include "sysemu/block-backend.h" +#include "qemu/cutils.h" +#include "qapi/error.h" +#include "block/block_int.h" +#include "block/qdict.h" +#include "block/block-copy.h" + +#include "block/copy-before-write.h" + +typedef struct BDRVCopyBeforeWriteState { + BlockCopyState *bcs; + BdrvChild *target; +} BDRVCopyBeforeWriteState; + +static coroutine_fn int cbw_co_preadv( + BlockDriverState *bs, uint64_t offset, uint64_t bytes, + QEMUIOVector *qiov, int flags) +{ + return bdrv_co_preadv(bs->file, offset, bytes, qiov, flags); +} + +static coroutine_fn int cbw_do_copy_before_write(BlockDriverState *bs, + uint64_t offset, uint64_t bytes, BdrvRequestFlags flags) +{ + BDRVCopyBeforeWriteState *s = bs->opaque; + uint64_t off, end; + int64_t cluster_size = block_copy_cluster_size(s->bcs); + + if (flags & BDRV_REQ_WRITE_UNCHANGED) { + return 0; + } + + off = QEMU_ALIGN_DOWN(offset, cluster_size); + end = QEMU_ALIGN_UP(offset + bytes, cluster_size); + + return block_copy(s->bcs, off, end - off, true); +} + +static int coroutine_fn cbw_co_pdiscard(BlockDriverState *bs, + int64_t offset, int bytes) +{ + int ret = cbw_do_copy_before_write(bs, offset, bytes, 0); + if (ret < 0) { + return ret; + } + + return bdrv_co_pdiscard(bs->file, offset, bytes); +} + +static int coroutine_fn cbw_co_pwrite_zeroes(BlockDriverState *bs, + int64_t offset, int bytes, BdrvRequestFlags flags) +{ + int ret = cbw_do_copy_before_write(bs, offset, bytes, flags); + if (ret < 0) { + return ret; + } + + return bdrv_co_pwrite_zeroes(bs->file, offset, bytes, flags); +} + +static coroutine_fn int cbw_co_pwritev(BlockDriverState *bs, + uint64_t offset, + uint64_t bytes, + QEMUIOVector *qiov, int flags) +{ + int ret = cbw_do_copy_before_write(bs, offset, bytes, flags); + if (ret < 0) { + return ret; + } + + return bdrv_co_pwritev(bs->file, offset, bytes, qiov, flags); +} + +static int coroutine_fn cbw_co_flush(BlockDriverState *bs) +{ + if (!bs->file) { + return 0; + } + + return bdrv_co_flush(bs->file->bs); +} + +static void cbw_refresh_filename(BlockDriverState *bs) +{ + pstrcpy(bs->exact_filename, sizeof(bs->exact_filename), + bs->file->bs->filename); +} + +static void cbw_child_perm(BlockDriverState *bs, BdrvChild *c, + BdrvChildRole role, + BlockReopenQueue *reopen_queue, + uint64_t perm, uint64_t shared, + uint64_t *nperm, uint64_t *nshared) +{ + if (!(role & BDRV_CHILD_FILTERED)) { + /* + * Target child + * + * Share write to target (child_file), to not interfere + * with guest writes to its disk which may be in target backing chain. + * Can't resize during a backup block job because we check the size + * only upfront. + */ + *nshared = BLK_PERM_ALL & ~BLK_PERM_RESIZE; + *nperm = BLK_PERM_WRITE; + } else { + /* Source child */ + bdrv_default_perms(bs, c, role, reopen_queue, + perm, shared, nperm, nshared); + + if (!QLIST_EMPTY(&bs->parents)) { + if (perm & BLK_PERM_WRITE) { + *nperm = *nperm | BLK_PERM_CONSISTENT_READ; + } + *nshared &= ~(BLK_PERM_WRITE | BLK_PERM_RESIZE); + } + } +} + +static int cbw_open(BlockDriverState *bs, QDict *options, int flags, + Error **errp) +{ + BDRVCopyBeforeWriteState *s = bs->opaque; + BdrvDirtyBitmap *copy_bitmap; + + bs->file = bdrv_open_child(NULL, options, "file", bs, &child_of_bds, + BDRV_CHILD_FILTERED | BDRV_CHILD_PRIMARY, + false, errp); + if (!bs->file) { + return -EINVAL; + } + + s->target = bdrv_open_child(NULL, options, "target", bs, &child_of_bds, + BDRV_CHILD_DATA, false, errp); + if (!s->target) { + return -EINVAL; + } + + bs->total_sectors = bs->file->bs->total_sectors; + bs->supported_write_flags = BDRV_REQ_WRITE_UNCHANGED | + (BDRV_REQ_FUA & bs->file->bs->supported_write_flags); + bs->supported_zero_flags = BDRV_REQ_WRITE_UNCHANGED | + ((BDRV_REQ_FUA | BDRV_REQ_MAY_UNMAP | BDRV_REQ_NO_FALLBACK) & + bs->file->bs->supported_zero_flags); + + s->bcs = block_copy_state_new(bs->file, s->target, errp); + if (!s->bcs) { + error_prepend(errp, "Cannot create block-copy-state: "); + return -EINVAL; + } + + copy_bitmap = block_copy_dirty_bitmap(s->bcs); + bdrv_set_dirty_bitmap(copy_bitmap, 0, bdrv_dirty_bitmap_size(copy_bitmap)); + + return 0; +} + +static void cbw_close(BlockDriverState *bs) +{ + BDRVCopyBeforeWriteState *s = bs->opaque; + + block_copy_state_free(s->bcs); + s->bcs = NULL; +} + +BlockDriver bdrv_cbw_filter = { + .format_name = "copy-before-write", + .instance_size = sizeof(BDRVCopyBeforeWriteState), + + .bdrv_open = cbw_open, + .bdrv_close = cbw_close, + + .bdrv_co_preadv = cbw_co_preadv, + .bdrv_co_pwritev = cbw_co_pwritev, + .bdrv_co_pwrite_zeroes = cbw_co_pwrite_zeroes, + .bdrv_co_pdiscard = cbw_co_pdiscard, + .bdrv_co_flush = cbw_co_flush, + + .bdrv_refresh_filename = cbw_refresh_filename, + + .bdrv_child_perm = cbw_child_perm, + + .is_filter = true, +}; + +BlockDriverState *bdrv_cbw_append(BlockDriverState *source, + BlockDriverState *target, + const char *filter_node_name, + BlockCopyState **bcs, + Error **errp) +{ + ERRP_GUARD(); + BDRVCopyBeforeWriteState *state; + BlockDriverState *top; + QDict *opts; + + assert(source->total_sectors == target->total_sectors); + + opts = qdict_new(); + qdict_put_str(opts, "driver", "copy-before-write"); + if (filter_node_name) { + qdict_put_str(opts, "node-name", filter_node_name); + } + qdict_put_str(opts, "file", bdrv_get_node_name(source)); + qdict_put_str(opts, "target", bdrv_get_node_name(target)); + + top = bdrv_insert_node(source, opts, BDRV_O_RDWR, errp); + if (!top) { + return NULL; + } + + state = top->opaque; + *bcs = state->bcs; + + return top; +} + +void bdrv_cbw_drop(BlockDriverState *bs) +{ + bdrv_drop_filter(bs, &error_abort); + bdrv_unref(bs); +} + +static void cbw_init(void) +{ + bdrv_register(&bdrv_cbw_filter); +} + +block_init(cbw_init); diff --git a/block/backup-top.h b/block/copy-before-write.h index b28b0031c4..51847e711a 100644 --- a/block/backup-top.h +++ b/block/copy-before-write.h @@ -1,10 +1,10 @@ /* - * backup-top filter driver + * copy-before-write filter driver * * The driver performs Copy-Before-Write (CBW) operation: it is injected above * some node, and before each write it copies _old_ data to the target node. * - * Copyright (c) 2018-2019 Virtuozzo International GmbH. + * Copyright (c) 2018-2021 Virtuozzo International GmbH. * * Author: * Sementsov-Ogievskiy Vladimir <vsementsov@virtuozzo.com> @@ -23,20 +23,17 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef BACKUP_TOP_H -#define BACKUP_TOP_H +#ifndef COPY_BEFORE_WRITE_H +#define COPY_BEFORE_WRITE_H #include "block/block_int.h" #include "block/block-copy.h" -BlockDriverState *bdrv_backup_top_append(BlockDriverState *source, - BlockDriverState *target, - const char *filter_node_name, - uint64_t cluster_size, - BackupPerf *perf, - BdrvRequestFlags write_flags, - BlockCopyState **bcs, - Error **errp); -void bdrv_backup_top_drop(BlockDriverState *bs); +BlockDriverState *bdrv_cbw_append(BlockDriverState *source, + BlockDriverState *target, + const char *filter_node_name, + BlockCopyState **bcs, + Error **errp); +void bdrv_cbw_drop(BlockDriverState *bs); -#endif /* BACKUP_TOP_H */ +#endif /* COPY_BEFORE_WRITE_H */ diff --git a/block/export/fuse.c b/block/export/fuse.c index fc7b07d2b5..2e3bf8270b 100644 --- a/block/export/fuse.c +++ b/block/export/fuse.c @@ -31,6 +31,9 @@ #include <fuse.h> #include <fuse_lowlevel.h> +#ifdef __linux__ +#include <linux/fs.h> +#endif /* Prevent overly long bounce buffer allocations */ #define FUSE_MAX_BOUNCE_BYTES (MIN(BDRV_REQUEST_MAX_BYTES, 64 * 1024 * 1024)) diff --git a/block/file-win32.c b/block/file-win32.c index 2642088bd6..b97c58d642 100644 --- a/block/file-win32.c +++ b/block/file-win32.c @@ -58,6 +58,10 @@ typedef struct BDRVRawState { QEMUWin32AIOState *aio; } BDRVRawState; +typedef struct BDRVRawReopenState { + HANDLE hfile; +} BDRVRawReopenState; + /* * Read/writes the data to/from a given linear buffer. * @@ -392,7 +396,7 @@ static int raw_open(BlockDriverState *bs, QDict *options, int flags, } s->hfile = CreateFile(filename, access_flags, - FILE_SHARE_READ, NULL, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, overlapped, NULL); if (s->hfile == INVALID_HANDLE_VALUE) { int err = GetLastError(); @@ -634,6 +638,97 @@ static int coroutine_fn raw_co_create_opts(BlockDriver *drv, return raw_co_create(&options, errp); } +static int raw_reopen_prepare(BDRVReopenState *state, + BlockReopenQueue *queue, Error **errp) +{ + BDRVRawState *s = state->bs->opaque; + BDRVRawReopenState *rs; + int access_flags; + DWORD overlapped; + int ret = 0; + + if (s->type != FTYPE_FILE) { + error_setg(errp, "Can only reopen files"); + return -EINVAL; + } + + rs = g_new0(BDRVRawReopenState, 1); + + /* + * We do not support changing any options (only flags). By leaving + * all options in state->options, we tell the generic reopen code + * that we do not support changing any of them, so it will verify + * that their values did not change. + */ + + raw_parse_flags(state->flags, s->aio != NULL, &access_flags, &overlapped); + rs->hfile = CreateFile(state->bs->filename, access_flags, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, + OPEN_EXISTING, overlapped, NULL); + + if (rs->hfile == INVALID_HANDLE_VALUE) { + int err = GetLastError(); + + error_setg_win32(errp, err, "Could not reopen '%s'", + state->bs->filename); + if (err == ERROR_ACCESS_DENIED) { + ret = -EACCES; + } else { + ret = -EINVAL; + } + goto fail; + } + + if (s->aio) { + ret = win32_aio_attach(s->aio, rs->hfile); + if (ret < 0) { + error_setg_errno(errp, -ret, "Could not enable AIO"); + CloseHandle(rs->hfile); + goto fail; + } + } + + state->opaque = rs; + + return 0; + +fail: + g_free(rs); + state->opaque = NULL; + + return ret; +} + +static void raw_reopen_commit(BDRVReopenState *state) +{ + BDRVRawState *s = state->bs->opaque; + BDRVRawReopenState *rs = state->opaque; + + assert(rs != NULL); + + CloseHandle(s->hfile); + s->hfile = rs->hfile; + + g_free(rs); + state->opaque = NULL; +} + +static void raw_reopen_abort(BDRVReopenState *state) +{ + BDRVRawReopenState *rs = state->opaque; + + if (!rs) { + return; + } + + if (rs->hfile != INVALID_HANDLE_VALUE) { + CloseHandle(rs->hfile); + } + + g_free(rs); + state->opaque = NULL; +} + static QemuOptsList raw_create_opts = { .name = "raw-create-opts", .head = QTAILQ_HEAD_INITIALIZER(raw_create_opts.head), @@ -659,6 +754,10 @@ BlockDriver bdrv_file = { .bdrv_co_create_opts = raw_co_create_opts, .bdrv_has_zero_init = bdrv_has_zero_init_1, + .bdrv_reopen_prepare = raw_reopen_prepare, + .bdrv_reopen_commit = raw_reopen_commit, + .bdrv_reopen_abort = raw_reopen_abort, + .bdrv_aio_preadv = raw_aio_preadv, .bdrv_aio_pwritev = raw_aio_pwritev, .bdrv_aio_flush = raw_aio_flush, diff --git a/block/meson.build b/block/meson.build index 0450914c7a..66ee11e62c 100644 --- a/block/meson.build +++ b/block/meson.build @@ -4,7 +4,7 @@ block_ss.add(files( 'aio_task.c', 'amend.c', 'backup.c', - 'backup-top.c', + 'copy-before-write.c', 'blkdebug.c', 'blklogwrites.c', 'blkverify.c', diff --git a/block/monitor/block-hmp-cmds.c b/block/monitor/block-hmp-cmds.c index 3e6670c963..2ac4aedfff 100644 --- a/block/monitor/block-hmp-cmds.c +++ b/block/monitor/block-hmp-cmds.c @@ -251,10 +251,10 @@ void hmp_drive_mirror(Monitor *mon, const QDict *qdict) if (!filename) { error_setg(&err, QERR_MISSING_PARAMETER, "target"); - hmp_handle_error(mon, err); - return; + goto end; } qmp_drive_mirror(&mirror, &err); +end: hmp_handle_error(mon, err); } @@ -281,11 +281,11 @@ void hmp_drive_backup(Monitor *mon, const QDict *qdict) if (!filename) { error_setg(&err, QERR_MISSING_PARAMETER, "target"); - hmp_handle_error(mon, err); - return; + goto end; } qmp_drive_backup(&backup, &err); +end: hmp_handle_error(mon, err); } @@ -356,8 +356,7 @@ void hmp_snapshot_blkdev(Monitor *mon, const QDict *qdict) * will be taken internally. Today it's actually required. */ error_setg(&err, QERR_MISSING_PARAMETER, "snapshot-file"); - hmp_handle_error(mon, err); - return; + goto end; } mode = reuse ? NEW_IMAGE_MODE_EXISTING : NEW_IMAGE_MODE_ABSOLUTE_PATHS; @@ -365,6 +364,7 @@ void hmp_snapshot_blkdev(Monitor *mon, const QDict *qdict) filename, false, NULL, !!format, format, true, mode, &err); +end: hmp_handle_error(mon, err); } diff --git a/block/raw-format.c b/block/raw-format.c index 7717578ed6..c26f493688 100644 --- a/block/raw-format.c +++ b/block/raw-format.c @@ -580,6 +580,25 @@ static void raw_cancel_in_flight(BlockDriverState *bs) bdrv_cancel_in_flight(bs->file->bs); } +static void raw_child_perm(BlockDriverState *bs, BdrvChild *c, + BdrvChildRole role, + BlockReopenQueue *reopen_queue, + uint64_t parent_perm, uint64_t parent_shared, + uint64_t *nperm, uint64_t *nshared) +{ + bdrv_default_perms(bs, c, role, reopen_queue, parent_perm, + parent_shared, nperm, nshared); + + /* + * bdrv_default_perms() may add WRITE and/or RESIZE (see comment in + * bdrv_default_perms_for_storage() for an explanation) but we only need + * them if they are in parent_perm. Drop WRITE and RESIZE whenever possible + * to avoid permission conflicts. + */ + *nperm &= ~(BLK_PERM_WRITE | BLK_PERM_RESIZE); + *nperm |= parent_perm & (BLK_PERM_WRITE | BLK_PERM_RESIZE); +} + BlockDriver bdrv_raw = { .format_name = "raw", .instance_size = sizeof(BDRVRawState), @@ -588,7 +607,7 @@ BlockDriver bdrv_raw = { .bdrv_reopen_commit = &raw_reopen_commit, .bdrv_reopen_abort = &raw_reopen_abort, .bdrv_open = &raw_open, - .bdrv_child_perm = bdrv_default_perms, + .bdrv_child_perm = raw_child_perm, .bdrv_co_create_opts = &raw_co_create_opts, .bdrv_co_preadv = &raw_co_preadv, .bdrv_co_pwritev = &raw_co_pwritev, diff --git a/docs/devel/testing.rst b/docs/devel/testing.rst index 8a9cda33a5..4a0abbf23d 100644 --- a/docs/devel/testing.rst +++ b/docs/devel/testing.rst @@ -224,6 +224,35 @@ another application on the host may have locked the file, possibly leading to a test failure. If using such devices are explicitly desired, consider adding ``locking=off`` option to disable image locking. +Debugging a test case +----------------------- +The following options to the ``check`` script can be useful when debugging +a failing test: + +* ``-gdb`` wraps every QEMU invocation in a ``gdbserver``, which waits for a + connection from a gdb client. The options given to ``gdbserver`` (e.g. the + address on which to listen for connections) are taken from the ``$GDB_OPTIONS`` + environment variable. By default (if ``$GDB_OPTIONS`` is empty), it listens on + ``localhost:12345``. + It is possible to connect to it for example with + ``gdb -iex "target remote $addr"``, where ``$addr`` is the address + ``gdbserver`` listens on. + If the ``-gdb`` option is not used, ``$GDB_OPTIONS`` is ignored, + regardless of whether it is set or not. + +* ``-valgrind`` attaches a valgrind instance to QEMU. If it detects + warnings, it will print and save the log in + ``$TEST_DIR/<valgrind_pid>.valgrind``. + The final command line will be ``valgrind --log-file=$TEST_DIR/ + <valgrind_pid>.valgrind --error-exitcode=99 $QEMU ...`` + +* ``-d`` (debug) just increases the logging verbosity, showing + for example the QMP commands and answers. + +* ``-p`` (print) redirects QEMU’s stdout and stderr to the test output, + instead of saving it into a log file in + ``$TEST_DIR/qemu-machine-<random_string>``. + Test case groups ---------------- diff --git a/hw/core/qdev-properties-system.c b/hw/core/qdev-properties-system.c index 2760c21f11..e71f5d64d1 100644 --- a/hw/core/qdev-properties-system.c +++ b/hw/core/qdev-properties-system.c @@ -36,11 +36,11 @@ static bool check_prop_still_unset(Object *obj, const char *name, const void *old_val, const char *new_val, - Error **errp) + bool allow_override, Error **errp) { const GlobalProperty *prop = qdev_find_global_prop(obj, name); - if (!old_val) { + if (!old_val || (!prop && allow_override)) { return true; } @@ -93,16 +93,34 @@ static void set_drive_helper(Object *obj, Visitor *v, const char *name, BlockBackend *blk; bool blk_created = false; int ret; + BlockDriverState *bs; + AioContext *ctx; if (!visit_type_str(v, name, &str, errp)) { return; } - /* - * TODO Should this really be an error? If no, the old value - * needs to be released before we store the new one. - */ - if (!check_prop_still_unset(obj, name, *ptr, str, errp)) { + if (!check_prop_still_unset(obj, name, *ptr, str, true, errp)) { + return; + } + + if (*ptr) { + /* BlockBackend alread exists. So, we want to change attached node */ + blk = *ptr; + ctx = blk_get_aio_context(blk); + bs = bdrv_lookup_bs(NULL, str, errp); + if (!bs) { + return; + } + + if (ctx != bdrv_get_aio_context(bs)) { + error_setg(errp, "Different aio context is not supported for new " + "node"); + } + + aio_context_acquire(ctx); + blk_replace_bs(blk, bs, errp); + aio_context_release(ctx); return; } @@ -114,7 +132,7 @@ static void set_drive_helper(Object *obj, Visitor *v, const char *name, blk = blk_by_name(str); if (!blk) { - BlockDriverState *bs = bdrv_lookup_bs(NULL, str, NULL); + bs = bdrv_lookup_bs(NULL, str, NULL); if (bs) { /* * If the device supports iothreads, it will make sure to move the @@ -123,8 +141,7 @@ static void set_drive_helper(Object *obj, Visitor *v, const char *name, * aware of iothreads require their BlockBackends to be in the main * AioContext. */ - AioContext *ctx = iothread ? bdrv_get_aio_context(bs) : - qemu_get_aio_context(); + ctx = iothread ? bdrv_get_aio_context(bs) : qemu_get_aio_context(); blk = blk_new(ctx, 0, BLK_PERM_ALL); blk_created = true; @@ -196,6 +213,7 @@ static void release_drive(Object *obj, const char *name, void *opaque) const PropertyInfo qdev_prop_drive = { .name = "str", .description = "Node name or ID of a block device to use as a backend", + .realized_set_allowed = true, .get = get_drive, .set = set_drive, .release = release_drive, @@ -204,6 +222,7 @@ const PropertyInfo qdev_prop_drive = { const PropertyInfo qdev_prop_drive_iothread = { .name = "str", .description = "Node name or ID of a block device to use as a backend", + .realized_set_allowed = true, .get = get_drive, .set = set_drive_iothread, .release = release_drive, @@ -238,7 +257,7 @@ static void set_chr(Object *obj, Visitor *v, const char *name, void *opaque, * TODO Should this really be an error? If no, the old value * needs to be released before we store the new one. */ - if (!check_prop_still_unset(obj, name, be->chr, str, errp)) { + if (!check_prop_still_unset(obj, name, be->chr, str, false, errp)) { return; } @@ -408,7 +427,7 @@ static void set_netdev(Object *obj, Visitor *v, const char *name, * TODO Should this really be an error? If no, the old value * needs to be released before we store the new one. */ - if (!check_prop_still_unset(obj, name, ncs[i], str, errp)) { + if (!check_prop_still_unset(obj, name, ncs[i], str, false, errp)) { goto out; } diff --git a/hw/core/qdev-properties.c b/hw/core/qdev-properties.c index 50f40949f5..c34aac6ebc 100644 --- a/hw/core/qdev-properties.c +++ b/hw/core/qdev-properties.c @@ -26,11 +26,11 @@ void qdev_prop_set_after_realize(DeviceState *dev, const char *name, /* returns: true if property is allowed to be set, false otherwise */ static bool qdev_prop_allow_set(Object *obj, const char *name, - Error **errp) + const PropertyInfo *info, Error **errp) { DeviceState *dev = DEVICE(obj); - if (dev->realized) { + if (dev->realized && !info->realized_set_allowed) { qdev_prop_set_after_realize(dev, name, errp); return false; } @@ -79,7 +79,7 @@ static void field_prop_set(Object *obj, Visitor *v, const char *name, { Property *prop = opaque; - if (!qdev_prop_allow_set(obj, name, errp)) { + if (!qdev_prop_allow_set(obj, name, prop->info, errp)) { return; } diff --git a/include/block/block-copy.h b/include/block/block-copy.h index 5c8278895c..99370fa38b 100644 --- a/include/block/block-copy.h +++ b/include/block/block-copy.h @@ -25,10 +25,11 @@ typedef struct BlockCopyState BlockCopyState; typedef struct BlockCopyCallState BlockCopyCallState; BlockCopyState *block_copy_state_new(BdrvChild *source, BdrvChild *target, - int64_t cluster_size, bool use_copy_range, - BdrvRequestFlags write_flags, Error **errp); +/* Function should be called prior any actual copy request */ +void block_copy_set_copy_opts(BlockCopyState *s, bool use_copy_range, + bool compress); void block_copy_set_progress_meter(BlockCopyState *s, ProgressMeter *pm); void block_copy_state_free(BlockCopyState *s); @@ -89,6 +90,7 @@ void block_copy_kick(BlockCopyCallState *call_state); void block_copy_call_cancel(BlockCopyCallState *call_state); BdrvDirtyBitmap *block_copy_dirty_bitmap(BlockCopyState *s); +int64_t block_copy_cluster_size(BlockCopyState *s); void block_copy_set_skip_unallocated(BlockCopyState *s, bool skip); #endif /* BLOCK_COPY_H */ diff --git a/include/block/block.h b/include/block/block.h index 3477290f9a..740038a892 100644 --- a/include/block/block.h +++ b/include/block/block.h @@ -361,6 +361,8 @@ int bdrv_append(BlockDriverState *bs_new, BlockDriverState *bs_top, Error **errp); int bdrv_replace_node(BlockDriverState *from, BlockDriverState *to, Error **errp); +int bdrv_replace_child_bs(BdrvChild *child, BlockDriverState *new_bs, + Error **errp); BlockDriverState *bdrv_insert_node(BlockDriverState *bs, QDict *node_options, int flags, Error **errp); int bdrv_drop_filter(BlockDriverState *bs, Error **errp); diff --git a/include/hw/qdev-properties.h b/include/hw/qdev-properties.h index 0ef97d60ce..f7925f67d0 100644 --- a/include/hw/qdev-properties.h +++ b/include/hw/qdev-properties.h @@ -32,6 +32,7 @@ struct PropertyInfo { const char *name; const char *description; const QEnumLookup *enum_table; + bool realized_set_allowed; /* allow setting property on realized device */ int (*print)(Object *obj, Property *prop, char *dest, size_t len); void (*set_default_value)(ObjectProperty *op, const Property *prop); ObjectProperty *(*create)(ObjectClass *oc, const char *name, diff --git a/include/sysemu/block-backend.h b/include/sysemu/block-backend.h index 9ac5f7bbd3..29d4fdbf63 100644 --- a/include/sysemu/block-backend.h +++ b/include/sysemu/block-backend.h @@ -102,6 +102,7 @@ BlockBackend *blk_by_public(BlockBackendPublic *public); BlockDriverState *blk_bs(BlockBackend *blk); void blk_remove_bs(BlockBackend *blk); int blk_insert_bs(BlockBackend *blk, BlockDriverState *bs, Error **errp); +int blk_replace_bs(BlockBackend *blk, BlockDriverState *new_bs, Error **errp); bool bdrv_has_blk(BlockDriverState *bs); bool bdrv_is_root_node(BlockDriverState *bs); int blk_set_perm(BlockBackend *blk, uint64_t perm, uint64_t shared_perm, diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py index 971ed7e8c6..a7081b1845 100644 --- a/python/qemu/machine/machine.py +++ b/python/qemu/machine/machine.py @@ -36,6 +36,7 @@ from typing import ( Sequence, Tuple, Type, + TypeVar, ) from qemu.qmp import ( # pylint: disable=import-error @@ -73,6 +74,9 @@ class AbnormalShutdown(QEMUMachineError): """ +_T = TypeVar('_T', bound='QEMUMachine') + + class QEMUMachine: """ A QEMU VM. @@ -97,7 +101,8 @@ class QEMUMachine: sock_dir: Optional[str] = None, drain_console: bool = False, console_log: Optional[str] = None, - log_dir: Optional[str] = None): + log_dir: Optional[str] = None, + qmp_timer: Optional[float] = None): ''' Initialize a QEMUMachine @@ -112,6 +117,7 @@ class QEMUMachine: @param drain_console: (optional) True to drain console socket to buffer @param console_log: (optional) path to console log file @param log_dir: where to create and keep log files + @param qmp_timer: (optional) default QMP socket timeout @note: Qemu process is not started until launch() is used. ''' # pylint: disable=too-many-arguments @@ -121,6 +127,7 @@ class QEMUMachine: self._binary = binary self._args = list(args) self._wrapper = wrapper + self._qmp_timer = qmp_timer self._name = name or "qemu-%d" % os.getpid() self._base_temp_dir = base_temp_dir @@ -166,7 +173,7 @@ class QEMUMachine: self._remove_files: List[str] = [] self._user_killed = False - def __enter__(self) -> 'QEMUMachine': + def __enter__(self: _T) -> _T: return self def __exit__(self, @@ -182,8 +189,8 @@ class QEMUMachine: self._args.append('-monitor') self._args.append('null') - def add_fd(self, fd: int, fdset: int, - opaque: str, opts: str = '') -> 'QEMUMachine': + def add_fd(self: _T, fd: int, fdset: int, + opaque: str, opts: str = '') -> _T: """ Pass a file descriptor to the VM """ @@ -343,7 +350,12 @@ class QEMUMachine: def _post_launch(self) -> None: if self._qmp_connection: - self._qmp.accept() + self._qmp.accept(self._qmp_timer) + + def _close_qemu_log_file(self) -> None: + if self._qemu_log_file is not None: + self._qemu_log_file.close() + self._qemu_log_file = None def _post_shutdown(self) -> None: """ @@ -357,9 +369,7 @@ class QEMUMachine: self._qmp.close() self._qmp_connection = None - if self._qemu_log_file is not None: - self._qemu_log_file.close() - self._qemu_log_file = None + self._close_qemu_log_file() self._load_io_log() @@ -564,22 +574,30 @@ class QEMUMachine: return self._qmp_connection @classmethod - def _qmp_args(cls, _conv_keys: bool = True, **args: Any) -> Dict[str, Any]: - qmp_args = dict() - for key, value in args.items(): - if _conv_keys: - qmp_args[key.replace('_', '-')] = value - else: - qmp_args[key] = value - return qmp_args + def _qmp_args(cls, conv_keys: bool, + args: Dict[str, Any]) -> Dict[str, object]: + if conv_keys: + return {k.replace('_', '-'): v for k, v in args.items()} + + return args def qmp(self, cmd: str, - conv_keys: bool = True, + args_dict: Optional[Dict[str, object]] = None, + conv_keys: Optional[bool] = None, **args: Any) -> QMPMessage: """ Invoke a QMP command and return the response dict """ - qmp_args = self._qmp_args(conv_keys, **args) + if args_dict is not None: + assert not args + assert conv_keys is None + args = args_dict + conv_keys = False + + if conv_keys is None: + conv_keys = True + + qmp_args = self._qmp_args(conv_keys, args) return self._qmp.cmd(cmd, args=qmp_args) def command(self, cmd: str, @@ -590,7 +608,7 @@ class QEMUMachine: On success return the response dict. On failure raise an exception. """ - qmp_args = self._qmp_args(conv_keys, **args) + qmp_args = self._qmp_args(conv_keys, args) return self._qmp.command(cmd, **qmp_args) def get_qmp_event(self, wait: bool = False) -> Optional[QMPMessage]: diff --git a/python/qemu/machine/qtest.py b/python/qemu/machine/qtest.py index d6d9c6a34a..395cc8fbfe 100644 --- a/python/qemu/machine/qtest.py +++ b/python/qemu/machine/qtest.py @@ -112,19 +112,22 @@ class QEMUQtestMachine(QEMUMachine): def __init__(self, binary: str, args: Sequence[str] = (), + wrapper: Sequence[str] = (), name: Optional[str] = None, base_temp_dir: str = "/var/tmp", socket_scm_helper: Optional[str] = None, - sock_dir: Optional[str] = None): + sock_dir: Optional[str] = None, + qmp_timer: Optional[float] = None): # pylint: disable=too-many-arguments if name is None: name = "qemu-%d" % os.getpid() if sock_dir is None: sock_dir = base_temp_dir - super().__init__(binary, args, name=name, base_temp_dir=base_temp_dir, + super().__init__(binary, args, wrapper=wrapper, name=name, + base_temp_dir=base_temp_dir, socket_scm_helper=socket_scm_helper, - sock_dir=sock_dir) + sock_dir=sock_dir, qmp_timer=qmp_timer) self._qtest: Optional[QEMUQtestProtocol] = None self._qtest_path = os.path.join(sock_dir, name + "-qtest.sock") diff --git a/python/setup.cfg b/python/setup.cfg index 14bab90288..83909c1c97 100644 --- a/python/setup.cfg +++ b/python/setup.cfg @@ -105,6 +105,11 @@ good-names=i, # Ignore imports when computing similarities. ignore-imports=yes +# Minimum lines number of a similarity. +# TODO: Remove after we opt in to Pylint 2.8.3. See commit msg. +min-similarity-lines=6 + + [isort] force_grid_wrap=4 force_sort_within_sections=True diff --git a/qapi/block-core.json b/qapi/block-core.json index 06674c25c9..c8ce1d9d5d 100644 --- a/qapi/block-core.json +++ b/qapi/block-core.json @@ -2825,13 +2825,14 @@ # @blklogwrites: Since 3.0 # @blkreplay: Since 4.2 # @compress: Since 5.0 +# @copy-before-write: Since 6.2 # # Since: 2.9 ## { 'enum': 'BlockdevDriver', 'data': [ 'blkdebug', 'blklogwrites', 'blkreplay', 'blkverify', 'bochs', - 'cloop', 'compress', 'copy-on-read', 'dmg', 'file', 'ftp', 'ftps', - 'gluster', + 'cloop', 'compress', 'copy-before-write', 'copy-on-read', 'dmg', + 'file', 'ftp', 'ftps', 'gluster', {'name': 'host_cdrom', 'if': 'HAVE_HOST_BLOCK_DEVICE' }, {'name': 'host_device', 'if': 'HAVE_HOST_BLOCK_DEVICE' }, 'http', 'https', 'iscsi', @@ -4050,6 +4051,25 @@ 'data': { '*bottom': 'str' } } ## +# @BlockdevOptionsCbw: +# +# Driver specific block device options for the copy-before-write driver, +# which does so called copy-before-write operations: when data is +# written to the filter, the filter first reads corresponding blocks +# from its file child and copies them to @target child. After successfully +# copying, the write request is propagated to file child. If copying +# fails, the original write request is failed too and no data is written +# to file child. +# +# @target: The target for copy-before-write operations. +# +# Since: 6.2 +## +{ 'struct': 'BlockdevOptionsCbw', + 'base': 'BlockdevOptionsGenericFormat', + 'data': { 'target': 'BlockdevRef' } } + +## # @BlockdevOptions: # # Options for creating a block device. Many options are available for all @@ -4101,6 +4121,7 @@ 'bochs': 'BlockdevOptionsGenericFormat', 'cloop': 'BlockdevOptionsGenericFormat', 'compress': 'BlockdevOptionsGenericFormat', + 'copy-before-write':'BlockdevOptionsCbw', 'copy-on-read':'BlockdevOptionsCor', 'dmg': 'BlockdevOptionsGenericFormat', 'file': 'BlockdevOptionsFile', diff --git a/tests/qemu-iotests/222 b/tests/qemu-iotests/222 deleted file mode 100755 index b48afe623e..0000000000 --- a/tests/qemu-iotests/222 +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -# group: rw quick -# -# This test covers the basic fleecing workflow, which provides a -# point-in-time snapshot of a node that can be queried over NBD. -# -# Copyright (C) 2018 Red Hat, Inc. -# John helped, too. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Creator/Owner: John Snow <jsnow@redhat.com> - -import iotests -from iotests import log, qemu_img, qemu_io, qemu_io_silent - -iotests.script_initialize( - supported_fmts=['qcow2', 'qcow', 'qed', 'vmdk', 'vhdx', 'raw'], - supported_platforms=['linux'], -) - -patterns = [("0x5d", "0", "64k"), - ("0xd5", "1M", "64k"), - ("0xdc", "32M", "64k"), - ("0xcd", "0x3ff0000", "64k")] # 64M - 64K - -overwrite = [("0xab", "0", "64k"), # Full overwrite - ("0xad", "0x00f8000", "64k"), # Partial-left (1M-32K) - ("0x1d", "0x2008000", "64k"), # Partial-right (32M+32K) - ("0xea", "0x3fe0000", "64k")] # Adjacent-left (64M - 128K) - -zeroes = [("0", "0x00f8000", "32k"), # Left-end of partial-left (1M-32K) - ("0", "0x2010000", "32k"), # Right-end of partial-right (32M+64K) - ("0", "0x3fe0000", "64k")] # overwrite[3] - -remainder = [("0xd5", "0x108000", "32k"), # Right-end of partial-left [1] - ("0xdc", "32M", "32k"), # Left-end of partial-right [2] - ("0xcd", "0x3ff0000", "64k")] # patterns[3] - -with iotests.FilePath('base.img') as base_img_path, \ - iotests.FilePath('fleece.img') as fleece_img_path, \ - iotests.FilePath('nbd.sock', base_dir=iotests.sock_dir) as nbd_sock_path, \ - iotests.VM() as vm: - - log('--- Setting up images ---') - log('') - - assert qemu_img('create', '-f', iotests.imgfmt, base_img_path, '64M') == 0 - assert qemu_img('create', '-f', "qcow2", fleece_img_path, '64M') == 0 - - for p in patterns: - qemu_io('-f', iotests.imgfmt, - '-c', 'write -P%s %s %s' % p, base_img_path) - - log('Done') - - log('') - log('--- Launching VM ---') - log('') - - vm.add_drive(base_img_path) - vm.launch() - log('Done') - - log('') - log('--- Setting up Fleecing Graph ---') - log('') - - src_node = "drive0" - tgt_node = "fleeceNode" - - # create tgt_node backed by src_node - log(vm.qmp("blockdev-add", **{ - "driver": "qcow2", - "node-name": tgt_node, - "file": { - "driver": "file", - "filename": fleece_img_path, - }, - "backing": src_node, - })) - - # Establish COW from source to fleecing node - log(vm.qmp("blockdev-backup", - device=src_node, - target=tgt_node, - sync="none")) - - log('') - log('--- Setting up NBD Export ---') - log('') - - nbd_uri = 'nbd+unix:///%s?socket=%s' % (tgt_node, nbd_sock_path) - log(vm.qmp("nbd-server-start", - **{"addr": { "type": "unix", - "data": { "path": nbd_sock_path } } })) - - log(vm.qmp("nbd-server-add", device=tgt_node)) - - log('') - log('--- Sanity Check ---') - log('') - - for p in (patterns + zeroes): - cmd = "read -P%s %s %s" % p - log(cmd) - assert qemu_io_silent('-r', '-f', 'raw', '-c', cmd, nbd_uri) == 0 - - log('') - log('--- Testing COW ---') - log('') - - for p in overwrite: - cmd = "write -P%s %s %s" % p - log(cmd) - log(vm.hmp_qemu_io(src_node, cmd)) - - log('') - log('--- Verifying Data ---') - log('') - - for p in (patterns + zeroes): - cmd = "read -P%s %s %s" % p - log(cmd) - assert qemu_io_silent('-r', '-f', 'raw', '-c', cmd, nbd_uri) == 0 - - log('') - log('--- Cleanup ---') - log('') - - log(vm.qmp('block-job-cancel', device=src_node)) - log(vm.event_wait('BLOCK_JOB_CANCELLED'), - filters=[iotests.filter_qmp_event]) - log(vm.qmp('nbd-server-stop')) - log(vm.qmp('blockdev-del', node_name=tgt_node)) - vm.shutdown() - - log('') - log('--- Confirming writes ---') - log('') - - for p in (overwrite + remainder): - cmd = "read -P%s %s %s" % p - log(cmd) - assert qemu_io_silent(base_img_path, '-c', cmd) == 0 - - log('') - log('Done') diff --git a/tests/qemu-iotests/222.out b/tests/qemu-iotests/222.out deleted file mode 100644 index 16643dde30..0000000000 --- a/tests/qemu-iotests/222.out +++ /dev/null @@ -1,67 +0,0 @@ ---- Setting up images --- - -Done - ---- Launching VM --- - -Done - ---- Setting up Fleecing Graph --- - -{"return": {}} -{"return": {}} - ---- Setting up NBD Export --- - -{"return": {}} -{"return": {}} - ---- Sanity Check --- - -read -P0x5d 0 64k -read -P0xd5 1M 64k -read -P0xdc 32M 64k -read -P0xcd 0x3ff0000 64k -read -P0 0x00f8000 32k -read -P0 0x2010000 32k -read -P0 0x3fe0000 64k - ---- Testing COW --- - -write -P0xab 0 64k -{"return": ""} -write -P0xad 0x00f8000 64k -{"return": ""} -write -P0x1d 0x2008000 64k -{"return": ""} -write -P0xea 0x3fe0000 64k -{"return": ""} - ---- Verifying Data --- - -read -P0x5d 0 64k -read -P0xd5 1M 64k -read -P0xdc 32M 64k -read -P0xcd 0x3ff0000 64k -read -P0 0x00f8000 32k -read -P0 0x2010000 32k -read -P0 0x3fe0000 64k - ---- Cleanup --- - -{"return": {}} -{"data": {"device": "drive0", "len": 67108864, "offset": 393216, "speed": 0, "type": "backup"}, "event": "BLOCK_JOB_CANCELLED", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}} -{"return": {}} -{"return": {}} - ---- Confirming writes --- - -read -P0xab 0 64k -read -P0xad 0x00f8000 64k -read -P0x1d 0x2008000 64k -read -P0xea 0x3fe0000 64k -read -P0xd5 0x108000 32k -read -P0xdc 32M 32k -read -P0xcd 0x3ff0000 64k - -Done diff --git a/tests/qemu-iotests/283 b/tests/qemu-iotests/283 index 010c22f0a2..a09e0183ae 100755 --- a/tests/qemu-iotests/283 +++ b/tests/qemu-iotests/283 @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # group: auto quick # -# Test for backup-top filter permission activation failure +# Test for copy-before-write filter permission conflict # # Copyright (c) 2019 Virtuozzo International GmbH. # @@ -31,13 +31,13 @@ size = 1024 * 1024 """ Test description When performing a backup, all writes on the source subtree must go through the -backup-top filter so it can copy all data to the target before it is changed. -backup-top filter is appended above source node, to achieve this thing, so all -parents of source node are handled. A configuration with side parents of source -sub-tree with write permission is unsupported (we'd have append several -backup-top filter like nodes to handle such parents). The test create an -example of such configuration and checks that a backup is then not allowed -(blockdev-backup command should fail). +copy-before-write filter so it can copy all data to the target before it is +changed. copy-before-write filter is appended above source node, to achieve +this thing, so all parents of source node are handled. A configuration with +side parents of source sub-tree with write permission is unsupported (we'd have +append several copy-before-write filter like nodes to handle such parents). The +test create an example of such configuration and checks that a backup is then +not allowed (blockdev-backup command should fail). The configuration: @@ -57,11 +57,10 @@ The configuration: │ base │ ◀──────────── │ other │ └─────────────┘ └───────┘ -On activation (see .active field of backup-top state in block/backup-top.c), -backup-top is going to unshare write permission on its source child. Write -unsharing will be propagated to the "source->base" link and will conflict with -other node write permission. So permission update will fail and backup job will -not be started. +copy-before-write filter wants to unshare write permission on its source child. +Write unsharing will be propagated to the "source->base" link and will conflict +with other node write permission. So permission update will fail and backup job +will not be started. Note, that the only thing which prevents backup of running on such configuration is default permission propagation scheme. It may be altered by @@ -99,13 +98,9 @@ vm.qmp_log('blockdev-backup', sync='full', device='source', target='target') vm.shutdown() -print('\n=== backup-top should be gone after job-finalize ===\n') +print('\n=== copy-before-write filter should be gone after job-finalize ===\n') -# Check that the backup-top node is gone after job-finalize. -# -# During finalization, the node becomes inactive and can no longer -# function. If it is still present, new parents might be attached, and -# there would be no meaningful way to handle their I/O requests. +# Check that the copy-before-write node is gone after job-finalize. vm = iotests.VM() vm.launch() @@ -131,7 +126,7 @@ vm.qmp_log('blockdev-backup', vm.event_wait('BLOCK_JOB_PENDING', 5.0) -# The backup-top filter should still be present prior to finalization +# The copy-before-write filter should still be present prior to finalization assert vm.node_info('backup-filter') is not None vm.qmp_log('job-finalize', id='backup') diff --git a/tests/qemu-iotests/283.out b/tests/qemu-iotests/283.out index c6e12b15c5..5bb75952ef 100644 --- a/tests/qemu-iotests/283.out +++ b/tests/qemu-iotests/283.out @@ -5,9 +5,9 @@ {"execute": "blockdev-add", "arguments": {"driver": "blkdebug", "image": "base", "node-name": "other", "take-child-perms": ["write"]}} {"return": {}} {"execute": "blockdev-backup", "arguments": {"device": "source", "sync": "full", "target": "target"}} -{"error": {"class": "GenericError", "desc": "Cannot append backup-top filter: Permission conflict on node 'base': permissions 'write' are both required by node 'other' (uses node 'base' as 'image' child) and unshared by node 'source' (uses node 'base' as 'image' child)."}} +{"error": {"class": "GenericError", "desc": "Permission conflict on node 'base': permissions 'write' are both required by node 'other' (uses node 'base' as 'image' child) and unshared by node 'source' (uses node 'base' as 'image' child)."}} -=== backup-top should be gone after job-finalize === +=== copy-before-write filter should be gone after job-finalize === {"execute": "blockdev-add", "arguments": {"driver": "null-co", "node-name": "source"}} {"return": {}} diff --git a/tests/qemu-iotests/297 b/tests/qemu-iotests/297 index 433b732336..345b617b34 100755 --- a/tests/qemu-iotests/297 +++ b/tests/qemu-iotests/297 @@ -31,7 +31,7 @@ SKIP_FILES = ( '096', '118', '124', '132', '136', '139', '147', '148', '149', '151', '152', '155', '163', '165', '169', '194', '196', '199', '202', '203', '205', '206', '207', '208', '210', '211', '212', '213', '216', - '218', '219', '222', '224', '228', '234', '235', '236', '237', '238', + '218', '219', '224', '228', '234', '235', '236', '237', '238', '240', '242', '245', '246', '248', '255', '256', '257', '258', '260', '262', '264', '266', '274', '277', '280', '281', '295', '296', '298', '299', '302', '303', '304', '307', diff --git a/tests/qemu-iotests/check b/tests/qemu-iotests/check index 2dd529eb75..da1bfb839e 100755 --- a/tests/qemu-iotests/check +++ b/tests/qemu-iotests/check @@ -36,6 +36,15 @@ def make_argparser() -> argparse.ArgumentParser: help='pretty print output for make check') p.add_argument('-d', dest='debug', action='store_true', help='debug') + p.add_argument('-p', dest='print', action='store_true', + help='redirects qemu\'s stdout and stderr to the test output') + p.add_argument('-gdb', action='store_true', + help="start gdbserver with $GDB_OPTIONS options \ + ('localhost:12345' if $GDB_OPTIONS is empty)") + p.add_argument('-valgrind', action='store_true', + help='use valgrind, sets VALGRIND_QEMU environment ' + 'variable') + p.add_argument('-misalign', action='store_true', help='misalign memory allocations') p.add_argument('--color', choices=['on', 'off', 'auto'], @@ -85,9 +94,6 @@ def make_argparser() -> argparse.ArgumentParser: g_bash.add_argument('-o', dest='imgopts', help='options to pass to qemu-img create/convert, ' 'sets IMGOPTS environment variable') - g_bash.add_argument('-valgrind', action='store_true', - help='use valgrind, sets VALGRIND_QEMU environment ' - 'variable') g_sel = p.add_argument_group('test selecting options', 'The following options specify test set ' @@ -114,7 +120,8 @@ if __name__ == '__main__': env = TestEnv(imgfmt=args.imgfmt, imgproto=args.imgproto, aiomode=args.aiomode, cachemode=args.cachemode, imgopts=args.imgopts, misalign=args.misalign, - debug=args.debug, valgrind=args.valgrind) + debug=args.debug, valgrind=args.valgrind, + gdb=args.gdb, qprint=args.print) if len(sys.argv) > 1 and sys.argv[-len(args.tests)-1] == '--': if not args.tests: diff --git a/tests/qemu-iotests/common.qemu b/tests/qemu-iotests/common.qemu index 0fc52d20d7..0f1fecc68e 100644 --- a/tests/qemu-iotests/common.qemu +++ b/tests/qemu-iotests/common.qemu @@ -85,7 +85,12 @@ _timed_wait_for() timeout=yes QEMU_STATUS[$h]=0 - while IFS= read -t ${QEMU_COMM_TIMEOUT} resp <&${QEMU_OUT[$h]} + read_timeout="-t ${QEMU_COMM_TIMEOUT}" + if [ -n "${GDB_OPTIONS}" ]; then + read_timeout= + fi + + while IFS= read ${read_timeout} resp <&${QEMU_OUT[$h]} do if [ -n "$capture_events" ]; then capture=0 diff --git a/tests/qemu-iotests/common.rc b/tests/qemu-iotests/common.rc index 609d82de89..d8582454de 100644 --- a/tests/qemu-iotests/common.rc +++ b/tests/qemu-iotests/common.rc @@ -166,8 +166,14 @@ _qemu_wrapper() if [ -n "${QEMU_NEED_PID}" ]; then echo $BASHPID > "${QEMU_TEST_DIR}/qemu-${_QEMU_HANDLE}.pid" fi + + GDB="" + if [ -n "${GDB_OPTIONS}" ]; then + GDB="gdbserver ${GDB_OPTIONS}" + fi + VALGRIND_QEMU="${VALGRIND_QEMU_VM}" _qemu_proc_exec "${VALGRIND_LOGFILE}" \ - "$QEMU_PROG" $QEMU_OPTIONS "$@" + $GDB "$QEMU_PROG" $QEMU_OPTIONS "$@" ) RETVAL=$? _qemu_proc_valgrind_log "${VALGRIND_LOGFILE}" $RETVAL diff --git a/tests/qemu-iotests/iotests.py b/tests/qemu-iotests/iotests.py index 89663dac06..11276f380a 100644 --- a/tests/qemu-iotests/iotests.py +++ b/tests/qemu-iotests/iotests.py @@ -74,6 +74,13 @@ if os.environ.get('QEMU_NBD_OPTIONS'): qemu_prog = os.environ.get('QEMU_PROG', 'qemu') qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ') +gdb_qemu_env = os.environ.get('GDB_OPTIONS') +qemu_gdb = [] +if gdb_qemu_env: + qemu_gdb = ['gdbserver'] + gdb_qemu_env.strip().split(' ') + +qemu_print = os.environ.get('PRINT_QEMU', False) + imgfmt = os.environ.get('IMGFMT', 'raw') imgproto = os.environ.get('IMGPROTO', 'file') output_dir = os.environ.get('OUTPUT_DIR', '.') @@ -91,6 +98,17 @@ except KeyError: sys.stderr.write('Please run this test via the "check" script\n') sys.exit(os.EX_USAGE) +qemu_valgrind = [] +if os.environ.get('VALGRIND_QEMU') == "y" and \ + os.environ.get('NO_VALGRIND') != "y": + valgrind_logfile = "--log-file=" + test_dir + # %p allows to put the valgrind process PID, since + # we don't know it a priori (subprocess.Popen is + # not yet invoked) + valgrind_logfile += "/%p.valgrind" + + qemu_valgrind = ['valgrind', valgrind_logfile, '--error-exitcode=99'] + socket_scm_helper = os.environ.get('SOCKET_SCM_HELPER', 'socket_scm_helper') luks_default_secret_object = 'secret,id=keysec0,data=' + \ @@ -219,18 +237,18 @@ def qemu_io_silent(*args): default_args = qemu_io_args args = default_args + list(args) - exitcode = subprocess.call(args, stdout=open('/dev/null', 'w')) - if exitcode < 0: + result = subprocess.run(args, stdout=subprocess.DEVNULL, check=False) + if result.returncode < 0: sys.stderr.write('qemu-io received signal %i: %s\n' % - (-exitcode, ' '.join(args))) - return exitcode + (-result.returncode, ' '.join(args))) + return result.returncode def qemu_io_silent_check(*args): '''Run qemu-io and return the true if subprocess returned 0''' args = qemu_io_args + list(args) - exitcode = subprocess.call(args, stdout=open('/dev/null', 'w'), - stderr=subprocess.STDOUT) - return exitcode == 0 + result = subprocess.run(args, stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, check=False) + return result.returncode == 0 class QemuIoInteractive: def __init__(self, *args): @@ -472,10 +490,14 @@ class Timeout: self.seconds = seconds self.errmsg = errmsg def __enter__(self): + if qemu_gdb or qemu_valgrind: + return self signal.signal(signal.SIGALRM, self.timeout) signal.setitimer(signal.ITIMER_REAL, self.seconds) return self def __exit__(self, exc_type, value, traceback): + if qemu_gdb or qemu_valgrind: + return False signal.setitimer(signal.ITIMER_REAL, 0) return False def timeout(self, signum, frame): @@ -570,12 +592,35 @@ class VM(qtest.QEMUQtestMachine): def __init__(self, path_suffix=''): name = "qemu%s-%d" % (path_suffix, os.getpid()) - super().__init__(qemu_prog, qemu_opts, name=name, + timer = 15.0 if not (qemu_gdb or qemu_valgrind) else None + if qemu_gdb and qemu_valgrind: + sys.stderr.write('gdb and valgrind are mutually exclusive\n') + sys.exit(1) + wrapper = qemu_gdb if qemu_gdb else qemu_valgrind + super().__init__(qemu_prog, qemu_opts, wrapper=wrapper, + name=name, base_temp_dir=test_dir, socket_scm_helper=socket_scm_helper, - sock_dir=sock_dir) + sock_dir=sock_dir, qmp_timer=timer) self._num_drives = 0 + def _post_shutdown(self) -> None: + super()._post_shutdown() + if not qemu_valgrind or not self._popen: + return + valgrind_filename = f"{test_dir}/{self._popen.pid}.valgrind" + if self.exitcode() == 99: + with open(valgrind_filename) as f: + print(f.read()) + else: + os.remove(valgrind_filename) + + def _pre_launch(self) -> None: + super()._pre_launch() + if qemu_print: + # set QEMU binary output to stdout + self._close_qemu_log_file() + def add_object(self, opts): self._args.append('-object') self._args.append(opts) @@ -651,9 +696,10 @@ class VM(qtest.QEMUQtestMachine): self.hmp(f'qemu-io {drive} "remove_break bp_{drive}"') def hmp_qemu_io(self, drive: str, cmd: str, - use_log: bool = False) -> QMPMessage: + use_log: bool = False, qdev: bool = False) -> QMPMessage: """Write to a given drive using an HMP command""" - return self.hmp(f'qemu-io {drive} "{cmd}"', use_log=use_log) + d = '-d ' if qdev else '' + return self.hmp(f'qemu-io {d}{drive} "{cmd}"', use_log=use_log) def flatten_qmp_object(self, obj, output=None, basestr=''): if output is None: @@ -1075,7 +1121,8 @@ def notrun(reason): # Each test in qemu-iotests has a number ("seq") seq = os.path.basename(sys.argv[0]) - open('%s/%s.notrun' % (output_dir, seq), 'w').write(reason + '\n') + with open('%s/%s.notrun' % (output_dir, seq), 'w') as outfile: + outfile.write(reason + '\n') logger.warning("%s not run: %s", seq, reason) sys.exit(0) @@ -1088,8 +1135,8 @@ def case_notrun(reason): # Each test in qemu-iotests has a number ("seq") seq = os.path.basename(sys.argv[0]) - open('%s/%s.casenotrun' % (output_dir, seq), 'a').write( - ' [case not run] ' + reason + '\n') + with open('%s/%s.casenotrun' % (output_dir, seq), 'a') as outfile: + outfile.write(' [case not run] ' + reason + '\n') def _verify_image_format(supported_fmts: Sequence[str] = (), unsupported_fmts: Sequence[str] = ()) -> None: diff --git a/tests/qemu-iotests/testenv.py b/tests/qemu-iotests/testenv.py index 0c3fe75636..70da0d60c8 100644 --- a/tests/qemu-iotests/testenv.py +++ b/tests/qemu-iotests/testenv.py @@ -27,6 +27,7 @@ import subprocess import glob from typing import List, Dict, Any, Optional, ContextManager +DEF_GDB_OPTIONS = 'localhost:12345' def isxfile(path: str) -> bool: return os.path.isfile(path) and os.access(path, os.X_OK) @@ -72,7 +73,8 @@ class TestEnv(ContextManager['TestEnv']): 'QEMU_NBD_OPTIONS', 'IMGOPTS', 'IMGFMT', 'IMGPROTO', 'AIOMODE', 'CACHEMODE', 'VALGRIND_QEMU', 'CACHEMODE_IS_DEFAULT', 'IMGFMT_GENERIC', 'IMGOPTSSYNTAX', - 'IMGKEYSECRET', 'QEMU_DEFAULT_MACHINE', 'MALLOC_PERTURB_'] + 'IMGKEYSECRET', 'QEMU_DEFAULT_MACHINE', 'MALLOC_PERTURB_', + 'GDB_OPTIONS', 'PRINT_QEMU'] def prepare_subprocess(self, args: List[str]) -> Dict[str, str]: if self.debug: @@ -178,7 +180,9 @@ class TestEnv(ContextManager['TestEnv']): imgopts: Optional[str] = None, misalign: bool = False, debug: bool = False, - valgrind: bool = False) -> None: + valgrind: bool = False, + gdb: bool = False, + qprint: bool = False) -> None: self.imgfmt = imgfmt self.imgproto = imgproto self.aiomode = aiomode @@ -186,6 +190,18 @@ class TestEnv(ContextManager['TestEnv']): self.misalign = misalign self.debug = debug + if qprint: + self.print_qemu = 'y' + + if gdb: + self.gdb_options = os.getenv('GDB_OPTIONS', DEF_GDB_OPTIONS) + if not self.gdb_options: + # cover the case 'export GDB_OPTIONS=' + self.gdb_options = DEF_GDB_OPTIONS + elif 'GDB_OPTIONS' in os.environ: + # to not propagate it in prepare_subprocess() + del os.environ['GDB_OPTIONS'] + if valgrind: self.valgrind_qemu = 'y' @@ -285,6 +301,9 @@ PLATFORM -- {platform} TEST_DIR -- {TEST_DIR} SOCK_DIR -- {SOCK_DIR} SOCKET_SCM_HELPER -- {SOCKET_SCM_HELPER} +GDB_OPTIONS -- {GDB_OPTIONS} +VALGRIND_QEMU -- {VALGRIND_QEMU} +PRINT_QEMU_OUTPUT -- {PRINT_QEMU} """ args = collections.defaultdict(str, self.get_env()) diff --git a/tests/qemu-iotests/tests/image-fleecing b/tests/qemu-iotests/tests/image-fleecing new file mode 100755 index 0000000000..f6318492c6 --- /dev/null +++ b/tests/qemu-iotests/tests/image-fleecing @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# group: rw quick +# +# This test covers the basic fleecing workflow, which provides a +# point-in-time snapshot of a node that can be queried over NBD. +# +# Copyright (C) 2018 Red Hat, Inc. +# John helped, too. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creator/Owner: John Snow <jsnow@redhat.com> + +import iotests +from iotests import log, qemu_img, qemu_io, qemu_io_silent + +iotests.script_initialize( + supported_fmts=['qcow2', 'qcow', 'qed', 'vmdk', 'vhdx', 'raw'], + supported_platforms=['linux'], +) + +patterns = [('0x5d', '0', '64k'), + ('0xd5', '1M', '64k'), + ('0xdc', '32M', '64k'), + ('0xcd', '0x3ff0000', '64k')] # 64M - 64K + +overwrite = [('0xab', '0', '64k'), # Full overwrite + ('0xad', '0x00f8000', '64k'), # Partial-left (1M-32K) + ('0x1d', '0x2008000', '64k'), # Partial-right (32M+32K) + ('0xea', '0x3fe0000', '64k')] # Adjacent-left (64M - 128K) + +zeroes = [('0', '0x00f8000', '32k'), # Left-end of partial-left (1M-32K) + ('0', '0x2010000', '32k'), # Right-end of partial-right (32M+64K) + ('0', '0x3fe0000', '64k')] # overwrite[3] + +remainder = [('0xd5', '0x108000', '32k'), # Right-end of partial-left [1] + ('0xdc', '32M', '32k'), # Left-end of partial-right [2] + ('0xcd', '0x3ff0000', '64k')] # patterns[3] + +def do_test(use_cbw, base_img_path, fleece_img_path, nbd_sock_path, vm): + log('--- Setting up images ---') + log('') + + assert qemu_img('create', '-f', iotests.imgfmt, base_img_path, '64M') == 0 + assert qemu_img('create', '-f', 'qcow2', fleece_img_path, '64M') == 0 + + for p in patterns: + qemu_io('-f', iotests.imgfmt, + '-c', 'write -P%s %s %s' % p, base_img_path) + + log('Done') + + log('') + log('--- Launching VM ---') + log('') + + src_node = 'source' + tmp_node = 'temp' + qom_path = '/machine/peripheral/sda' + vm.add_blockdev(f'driver={iotests.imgfmt},file.driver=file,' + f'file.filename={base_img_path},node-name={src_node}') + vm.add_device('virtio-scsi') + vm.add_device(f'scsi-hd,id=sda,drive={src_node}') + vm.launch() + log('Done') + + log('') + log('--- Setting up Fleecing Graph ---') + log('') + + + # create tmp_node backed by src_node + log(vm.qmp('blockdev-add', { + 'driver': 'qcow2', + 'node-name': tmp_node, + 'file': { + 'driver': 'file', + 'filename': fleece_img_path, + }, + 'backing': src_node, + })) + + # Establish CBW from source to fleecing node + if use_cbw: + log(vm.qmp('blockdev-add', { + 'driver': 'copy-before-write', + 'node-name': 'fl-cbw', + 'file': src_node, + 'target': tmp_node + })) + + log(vm.qmp('qom-set', path=qom_path, property='drive', value='fl-cbw')) + else: + log(vm.qmp('blockdev-backup', + job_id='fleecing', + device=src_node, + target=tmp_node, + sync='none')) + + log('') + log('--- Setting up NBD Export ---') + log('') + + nbd_uri = 'nbd+unix:///%s?socket=%s' % (tmp_node, nbd_sock_path) + log(vm.qmp('nbd-server-start', + {'addr': { 'type': 'unix', + 'data': { 'path': nbd_sock_path } } })) + + log(vm.qmp('nbd-server-add', device=tmp_node)) + + log('') + log('--- Sanity Check ---') + log('') + + for p in patterns + zeroes: + cmd = 'read -P%s %s %s' % p + log(cmd) + assert qemu_io_silent('-r', '-f', 'raw', '-c', cmd, nbd_uri) == 0 + + log('') + log('--- Testing COW ---') + log('') + + for p in overwrite: + cmd = 'write -P%s %s %s' % p + log(cmd) + log(vm.hmp_qemu_io(qom_path, cmd, qdev=True)) + + log('') + log('--- Verifying Data ---') + log('') + + for p in patterns + zeroes: + cmd = 'read -P%s %s %s' % p + log(cmd) + assert qemu_io_silent('-r', '-f', 'raw', '-c', cmd, nbd_uri) == 0 + + log('') + log('--- Cleanup ---') + log('') + + if use_cbw: + log(vm.qmp('qom-set', path=qom_path, property='drive', value=src_node)) + log(vm.qmp('blockdev-del', node_name='fl-cbw')) + else: + log(vm.qmp('block-job-cancel', device='fleecing')) + e = vm.event_wait('BLOCK_JOB_CANCELLED') + assert e is not None + log(e, filters=[iotests.filter_qmp_event]) + + log(vm.qmp('nbd-server-stop')) + log(vm.qmp('blockdev-del', node_name=tmp_node)) + vm.shutdown() + + log('') + log('--- Confirming writes ---') + log('') + + for p in overwrite + remainder: + cmd = 'read -P%s %s %s' % p + log(cmd) + assert qemu_io_silent(base_img_path, '-c', cmd) == 0 + + log('') + log('Done') + + +def test(use_cbw): + with iotests.FilePath('base.img') as base_img_path, \ + iotests.FilePath('fleece.img') as fleece_img_path, \ + iotests.FilePath('nbd.sock', + base_dir=iotests.sock_dir) as nbd_sock_path, \ + iotests.VM() as vm: + do_test(use_cbw, base_img_path, fleece_img_path, nbd_sock_path, vm) + + +log('=== Test backup(sync=none) based fleecing ===\n') +test(False) + +log('=== Test filter based fleecing ===\n') +test(True) diff --git a/tests/qemu-iotests/tests/image-fleecing.out b/tests/qemu-iotests/tests/image-fleecing.out new file mode 100644 index 0000000000..e96d122a8b --- /dev/null +++ b/tests/qemu-iotests/tests/image-fleecing.out @@ -0,0 +1,139 @@ +=== Test backup(sync=none) based fleecing === + +--- Setting up images --- + +Done + +--- Launching VM --- + +Done + +--- Setting up Fleecing Graph --- + +{"return": {}} +{"return": {}} + +--- Setting up NBD Export --- + +{"return": {}} +{"return": {}} + +--- Sanity Check --- + +read -P0x5d 0 64k +read -P0xd5 1M 64k +read -P0xdc 32M 64k +read -P0xcd 0x3ff0000 64k +read -P0 0x00f8000 32k +read -P0 0x2010000 32k +read -P0 0x3fe0000 64k + +--- Testing COW --- + +write -P0xab 0 64k +{"return": ""} +write -P0xad 0x00f8000 64k +{"return": ""} +write -P0x1d 0x2008000 64k +{"return": ""} +write -P0xea 0x3fe0000 64k +{"return": ""} + +--- Verifying Data --- + +read -P0x5d 0 64k +read -P0xd5 1M 64k +read -P0xdc 32M 64k +read -P0xcd 0x3ff0000 64k +read -P0 0x00f8000 32k +read -P0 0x2010000 32k +read -P0 0x3fe0000 64k + +--- Cleanup --- + +{"return": {}} +{"data": {"device": "fleecing", "len": 67108864, "offset": 393216, "speed": 0, "type": "backup"}, "event": "BLOCK_JOB_CANCELLED", "timestamp": {"microseconds": "USECS", "seconds": "SECS"}} +{"return": {}} +{"return": {}} + +--- Confirming writes --- + +read -P0xab 0 64k +read -P0xad 0x00f8000 64k +read -P0x1d 0x2008000 64k +read -P0xea 0x3fe0000 64k +read -P0xd5 0x108000 32k +read -P0xdc 32M 32k +read -P0xcd 0x3ff0000 64k + +Done +=== Test filter based fleecing === + +--- Setting up images --- + +Done + +--- Launching VM --- + +Done + +--- Setting up Fleecing Graph --- + +{"return": {}} +{"return": {}} +{"return": {}} + +--- Setting up NBD Export --- + +{"return": {}} +{"return": {}} + +--- Sanity Check --- + +read -P0x5d 0 64k +read -P0xd5 1M 64k +read -P0xdc 32M 64k +read -P0xcd 0x3ff0000 64k +read -P0 0x00f8000 32k +read -P0 0x2010000 32k +read -P0 0x3fe0000 64k + +--- Testing COW --- + +write -P0xab 0 64k +{"return": ""} +write -P0xad 0x00f8000 64k +{"return": ""} +write -P0x1d 0x2008000 64k +{"return": ""} +write -P0xea 0x3fe0000 64k +{"return": ""} + +--- Verifying Data --- + +read -P0x5d 0 64k +read -P0xd5 1M 64k +read -P0xdc 32M 64k +read -P0xcd 0x3ff0000 64k +read -P0 0x00f8000 32k +read -P0 0x2010000 32k +read -P0 0x3fe0000 64k + +--- Cleanup --- + +{"return": {}} +{"return": {}} +{"return": {}} +{"return": {}} + +--- Confirming writes --- + +read -P0xab 0 64k +read -P0xad 0x00f8000 64k +read -P0x1d 0x2008000 64k +read -P0xea 0x3fe0000 64k +read -P0xd5 0x108000 32k +read -P0xdc 32M 32k +read -P0xcd 0x3ff0000 64k + +Done |