mirror of
https://gitlab.com/qemu-project/qemu
synced 2024-11-05 20:35:44 +00:00
4b028cbe75
Introduce a new flag 'backing-mask-protocol' for the block-commit QMP command which instructs the internals to use 'raw' instead of the protocol driver in case when a image is used without a dummy 'raw' wrapper. The flag is designed such that it can be always asserted by management tools even when there isn't any update to backing files. The flag will be used by libvirt so that the backing images still reference the proper format even when libvirt will stop using the dummy raw driver (raw driver with no other config). Libvirt needs this so that the images stay compatible with older libvirt versions which didn't expect that a protocol driver name can appear in the backing file format field. Signed-off-by: Peter Krempa <pkrempa@redhat.com> Reviewed-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> Message-ID: <2cb46e37093ce793ea1604abc8bbb90f4c8e434b.1701796348.git.pkrempa@redhat.com> Reviewed-by: Kevin Wolf <kwolf@redhat.com> Signed-off-by: Kevin Wolf <kwolf@redhat.com>
600 lines
18 KiB
C
600 lines
18 KiB
C
/*
|
|
* Live block commit
|
|
*
|
|
* Copyright Red Hat, Inc. 2012
|
|
*
|
|
* Authors:
|
|
* Jeff Cody <jcody@redhat.com>
|
|
* Based on stream.c by Stefan Hajnoczi
|
|
*
|
|
* This work is licensed under the terms of the GNU LGPL, version 2 or later.
|
|
* See the COPYING.LIB file in the top-level directory.
|
|
*
|
|
*/
|
|
|
|
#include "qemu/osdep.h"
|
|
#include "qemu/cutils.h"
|
|
#include "trace.h"
|
|
#include "block/block_int.h"
|
|
#include "block/blockjob_int.h"
|
|
#include "qapi/error.h"
|
|
#include "qemu/ratelimit.h"
|
|
#include "qemu/memalign.h"
|
|
#include "sysemu/block-backend.h"
|
|
|
|
enum {
|
|
/*
|
|
* Size of data buffer for populating the image file. This should be large
|
|
* enough to process multiple clusters in a single call, so that populating
|
|
* contiguous regions of the image is efficient.
|
|
*/
|
|
COMMIT_BUFFER_SIZE = 512 * 1024, /* in bytes */
|
|
};
|
|
|
|
typedef struct CommitBlockJob {
|
|
BlockJob common;
|
|
BlockDriverState *commit_top_bs;
|
|
BlockBackend *top;
|
|
BlockBackend *base;
|
|
BlockDriverState *base_bs;
|
|
BlockDriverState *base_overlay;
|
|
BlockdevOnError on_error;
|
|
bool base_read_only;
|
|
bool chain_frozen;
|
|
char *backing_file_str;
|
|
bool backing_mask_protocol;
|
|
} CommitBlockJob;
|
|
|
|
static int commit_prepare(Job *job)
|
|
{
|
|
CommitBlockJob *s = container_of(job, CommitBlockJob, common.job);
|
|
|
|
bdrv_graph_rdlock_main_loop();
|
|
bdrv_unfreeze_backing_chain(s->commit_top_bs, s->base_bs);
|
|
s->chain_frozen = false;
|
|
bdrv_graph_rdunlock_main_loop();
|
|
|
|
/* Remove base node parent that still uses BLK_PERM_WRITE/RESIZE before
|
|
* the normal backing chain can be restored. */
|
|
blk_unref(s->base);
|
|
s->base = NULL;
|
|
|
|
/* FIXME: bdrv_drop_intermediate treats total failures and partial failures
|
|
* identically. Further work is needed to disambiguate these cases. */
|
|
return bdrv_drop_intermediate(s->commit_top_bs, s->base_bs,
|
|
s->backing_file_str,
|
|
s->backing_mask_protocol);
|
|
}
|
|
|
|
static void commit_abort(Job *job)
|
|
{
|
|
CommitBlockJob *s = container_of(job, CommitBlockJob, common.job);
|
|
BlockDriverState *top_bs = blk_bs(s->top);
|
|
BlockDriverState *commit_top_backing_bs;
|
|
|
|
if (s->chain_frozen) {
|
|
bdrv_graph_rdlock_main_loop();
|
|
bdrv_unfreeze_backing_chain(s->commit_top_bs, s->base_bs);
|
|
bdrv_graph_rdunlock_main_loop();
|
|
}
|
|
|
|
/* Make sure commit_top_bs and top stay around until bdrv_replace_node() */
|
|
bdrv_ref(top_bs);
|
|
bdrv_ref(s->commit_top_bs);
|
|
|
|
if (s->base) {
|
|
blk_unref(s->base);
|
|
}
|
|
|
|
/* free the blockers on the intermediate nodes so that bdrv_replace_nodes
|
|
* can succeed */
|
|
block_job_remove_all_bdrv(&s->common);
|
|
|
|
/* If bdrv_drop_intermediate() failed (or was not invoked), remove the
|
|
* commit filter driver from the backing chain now. Do this as the final
|
|
* step so that the 'consistent read' permission can be granted.
|
|
*
|
|
* XXX Can (or should) we somehow keep 'consistent read' blocked even
|
|
* after the failed/cancelled commit job is gone? If we already wrote
|
|
* something to base, the intermediate images aren't valid any more. */
|
|
bdrv_graph_rdlock_main_loop();
|
|
commit_top_backing_bs = s->commit_top_bs->backing->bs;
|
|
bdrv_graph_rdunlock_main_loop();
|
|
|
|
bdrv_drained_begin(commit_top_backing_bs);
|
|
bdrv_graph_wrlock();
|
|
bdrv_replace_node(s->commit_top_bs, commit_top_backing_bs, &error_abort);
|
|
bdrv_graph_wrunlock();
|
|
bdrv_drained_end(commit_top_backing_bs);
|
|
|
|
bdrv_unref(s->commit_top_bs);
|
|
bdrv_unref(top_bs);
|
|
}
|
|
|
|
static void commit_clean(Job *job)
|
|
{
|
|
CommitBlockJob *s = container_of(job, CommitBlockJob, common.job);
|
|
|
|
/* restore base open flags here if appropriate (e.g., change the base back
|
|
* to r/o). These reopens do not need to be atomic, since we won't abort
|
|
* even on failure here */
|
|
if (s->base_read_only) {
|
|
bdrv_reopen_set_read_only(s->base_bs, true, NULL);
|
|
}
|
|
|
|
g_free(s->backing_file_str);
|
|
blk_unref(s->top);
|
|
}
|
|
|
|
static int coroutine_fn commit_run(Job *job, Error **errp)
|
|
{
|
|
CommitBlockJob *s = container_of(job, CommitBlockJob, common.job);
|
|
int64_t offset;
|
|
int ret = 0;
|
|
int64_t n = 0; /* bytes */
|
|
QEMU_AUTO_VFREE void *buf = NULL;
|
|
int64_t len, base_len;
|
|
|
|
len = blk_co_getlength(s->top);
|
|
if (len < 0) {
|
|
return len;
|
|
}
|
|
job_progress_set_remaining(&s->common.job, len);
|
|
|
|
base_len = blk_co_getlength(s->base);
|
|
if (base_len < 0) {
|
|
return base_len;
|
|
}
|
|
|
|
if (base_len < len) {
|
|
ret = blk_co_truncate(s->base, len, false, PREALLOC_MODE_OFF, 0, NULL);
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
buf = blk_blockalign(s->top, COMMIT_BUFFER_SIZE);
|
|
|
|
for (offset = 0; offset < len; offset += n) {
|
|
bool copy;
|
|
bool error_in_source = true;
|
|
|
|
/* Note that even when no rate limit is applied we need to yield
|
|
* with no pending I/O here so that bdrv_drain_all() returns.
|
|
*/
|
|
block_job_ratelimit_sleep(&s->common);
|
|
if (job_is_cancelled(&s->common.job)) {
|
|
break;
|
|
}
|
|
/* Copy if allocated above the base */
|
|
ret = blk_co_is_allocated_above(s->top, s->base_overlay, true,
|
|
offset, COMMIT_BUFFER_SIZE, &n);
|
|
copy = (ret > 0);
|
|
trace_commit_one_iteration(s, offset, n, ret);
|
|
if (copy) {
|
|
assert(n < SIZE_MAX);
|
|
|
|
ret = blk_co_pread(s->top, offset, n, buf, 0);
|
|
if (ret >= 0) {
|
|
ret = blk_co_pwrite(s->base, offset, n, buf, 0);
|
|
if (ret < 0) {
|
|
error_in_source = false;
|
|
}
|
|
}
|
|
}
|
|
if (ret < 0) {
|
|
BlockErrorAction action =
|
|
block_job_error_action(&s->common, s->on_error,
|
|
error_in_source, -ret);
|
|
if (action == BLOCK_ERROR_ACTION_REPORT) {
|
|
return ret;
|
|
} else {
|
|
n = 0;
|
|
continue;
|
|
}
|
|
}
|
|
/* Publish progress */
|
|
job_progress_update(&s->common.job, n);
|
|
|
|
if (copy) {
|
|
block_job_ratelimit_processed_bytes(&s->common, n);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const BlockJobDriver commit_job_driver = {
|
|
.job_driver = {
|
|
.instance_size = sizeof(CommitBlockJob),
|
|
.job_type = JOB_TYPE_COMMIT,
|
|
.free = block_job_free,
|
|
.user_resume = block_job_user_resume,
|
|
.run = commit_run,
|
|
.prepare = commit_prepare,
|
|
.abort = commit_abort,
|
|
.clean = commit_clean
|
|
},
|
|
};
|
|
|
|
static int coroutine_fn GRAPH_RDLOCK
|
|
bdrv_commit_top_preadv(BlockDriverState *bs, int64_t offset, int64_t bytes,
|
|
QEMUIOVector *qiov, BdrvRequestFlags flags)
|
|
{
|
|
return bdrv_co_preadv(bs->backing, offset, bytes, qiov, flags);
|
|
}
|
|
|
|
static GRAPH_RDLOCK void bdrv_commit_top_refresh_filename(BlockDriverState *bs)
|
|
{
|
|
pstrcpy(bs->exact_filename, sizeof(bs->exact_filename),
|
|
bs->backing->bs->filename);
|
|
}
|
|
|
|
static void bdrv_commit_top_child_perm(BlockDriverState *bs, BdrvChild *c,
|
|
BdrvChildRole role,
|
|
BlockReopenQueue *reopen_queue,
|
|
uint64_t perm, uint64_t shared,
|
|
uint64_t *nperm, uint64_t *nshared)
|
|
{
|
|
*nperm = 0;
|
|
*nshared = BLK_PERM_ALL;
|
|
}
|
|
|
|
/* Dummy node that provides consistent read to its users without requiring it
|
|
* from its backing file and that allows writes on the backing file chain. */
|
|
static BlockDriver bdrv_commit_top = {
|
|
.format_name = "commit_top",
|
|
.bdrv_co_preadv = bdrv_commit_top_preadv,
|
|
.bdrv_refresh_filename = bdrv_commit_top_refresh_filename,
|
|
.bdrv_child_perm = bdrv_commit_top_child_perm,
|
|
|
|
.is_filter = true,
|
|
.filtered_child_is_backing = true,
|
|
};
|
|
|
|
void commit_start(const char *job_id, BlockDriverState *bs,
|
|
BlockDriverState *base, BlockDriverState *top,
|
|
int creation_flags, int64_t speed,
|
|
BlockdevOnError on_error, const char *backing_file_str,
|
|
bool backing_mask_protocol,
|
|
const char *filter_node_name, Error **errp)
|
|
{
|
|
CommitBlockJob *s;
|
|
BlockDriverState *iter;
|
|
BlockDriverState *commit_top_bs = NULL;
|
|
BlockDriverState *filtered_base;
|
|
int64_t base_size, top_size;
|
|
uint64_t base_perms, iter_shared_perms;
|
|
int ret;
|
|
|
|
GLOBAL_STATE_CODE();
|
|
|
|
assert(top != bs);
|
|
bdrv_graph_rdlock_main_loop();
|
|
if (bdrv_skip_filters(top) == bdrv_skip_filters(base)) {
|
|
error_setg(errp, "Invalid files for merge: top and base are the same");
|
|
bdrv_graph_rdunlock_main_loop();
|
|
return;
|
|
}
|
|
bdrv_graph_rdunlock_main_loop();
|
|
|
|
base_size = bdrv_getlength(base);
|
|
if (base_size < 0) {
|
|
error_setg_errno(errp, -base_size, "Could not inquire base image size");
|
|
return;
|
|
}
|
|
|
|
top_size = bdrv_getlength(top);
|
|
if (top_size < 0) {
|
|
error_setg_errno(errp, -top_size, "Could not inquire top image size");
|
|
return;
|
|
}
|
|
|
|
base_perms = BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE;
|
|
if (base_size < top_size) {
|
|
base_perms |= BLK_PERM_RESIZE;
|
|
}
|
|
|
|
s = block_job_create(job_id, &commit_job_driver, NULL, bs, 0, BLK_PERM_ALL,
|
|
speed, creation_flags, NULL, NULL, errp);
|
|
if (!s) {
|
|
return;
|
|
}
|
|
|
|
/* convert base to r/w, if necessary */
|
|
s->base_read_only = bdrv_is_read_only(base);
|
|
if (s->base_read_only) {
|
|
if (bdrv_reopen_set_read_only(base, false, errp) != 0) {
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
/* Insert commit_top block node above top, so we can block consistent read
|
|
* on the backing chain below it */
|
|
commit_top_bs = bdrv_new_open_driver(&bdrv_commit_top, filter_node_name, 0,
|
|
errp);
|
|
if (commit_top_bs == NULL) {
|
|
goto fail;
|
|
}
|
|
if (!filter_node_name) {
|
|
commit_top_bs->implicit = true;
|
|
}
|
|
|
|
/* So that we can always drop this node */
|
|
commit_top_bs->never_freeze = true;
|
|
|
|
commit_top_bs->total_sectors = top->total_sectors;
|
|
|
|
ret = bdrv_append(commit_top_bs, top, errp);
|
|
bdrv_unref(commit_top_bs); /* referenced by new parents or failed */
|
|
if (ret < 0) {
|
|
commit_top_bs = NULL;
|
|
goto fail;
|
|
}
|
|
|
|
s->commit_top_bs = commit_top_bs;
|
|
|
|
/*
|
|
* Block all nodes between top and base, because they will
|
|
* disappear from the chain after this operation.
|
|
* Note that this assumes that the user is fine with removing all
|
|
* nodes (including R/W filters) between top and base. Assuring
|
|
* this is the responsibility of the interface (i.e. whoever calls
|
|
* commit_start()).
|
|
*/
|
|
bdrv_graph_wrlock();
|
|
s->base_overlay = bdrv_find_overlay(top, base);
|
|
assert(s->base_overlay);
|
|
|
|
/*
|
|
* The topmost node with
|
|
* bdrv_skip_filters(filtered_base) == bdrv_skip_filters(base)
|
|
*/
|
|
filtered_base = bdrv_cow_bs(s->base_overlay);
|
|
assert(bdrv_skip_filters(filtered_base) == bdrv_skip_filters(base));
|
|
|
|
/*
|
|
* XXX BLK_PERM_WRITE needs to be allowed so we don't block ourselves
|
|
* at s->base (if writes are blocked for a node, they are also blocked
|
|
* for its backing file). The other options would be a second filter
|
|
* driver above s->base.
|
|
*/
|
|
iter_shared_perms = BLK_PERM_WRITE_UNCHANGED | BLK_PERM_WRITE;
|
|
|
|
for (iter = top; iter != base; iter = bdrv_filter_or_cow_bs(iter)) {
|
|
if (iter == filtered_base) {
|
|
/*
|
|
* From here on, all nodes are filters on the base. This
|
|
* allows us to share BLK_PERM_CONSISTENT_READ.
|
|
*/
|
|
iter_shared_perms |= BLK_PERM_CONSISTENT_READ;
|
|
}
|
|
|
|
ret = block_job_add_bdrv(&s->common, "intermediate node", iter, 0,
|
|
iter_shared_perms, errp);
|
|
if (ret < 0) {
|
|
bdrv_graph_wrunlock();
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
if (bdrv_freeze_backing_chain(commit_top_bs, base, errp) < 0) {
|
|
bdrv_graph_wrunlock();
|
|
goto fail;
|
|
}
|
|
s->chain_frozen = true;
|
|
|
|
ret = block_job_add_bdrv(&s->common, "base", base, 0, BLK_PERM_ALL, errp);
|
|
bdrv_graph_wrunlock();
|
|
|
|
if (ret < 0) {
|
|
goto fail;
|
|
}
|
|
|
|
s->base = blk_new(s->common.job.aio_context,
|
|
base_perms,
|
|
BLK_PERM_CONSISTENT_READ
|
|
| BLK_PERM_WRITE_UNCHANGED);
|
|
ret = blk_insert_bs(s->base, base, errp);
|
|
if (ret < 0) {
|
|
goto fail;
|
|
}
|
|
blk_set_disable_request_queuing(s->base, true);
|
|
s->base_bs = base;
|
|
|
|
/* Required permissions are already taken with block_job_add_bdrv() */
|
|
s->top = blk_new(s->common.job.aio_context, 0, BLK_PERM_ALL);
|
|
ret = blk_insert_bs(s->top, top, errp);
|
|
if (ret < 0) {
|
|
goto fail;
|
|
}
|
|
blk_set_disable_request_queuing(s->top, true);
|
|
|
|
s->backing_file_str = g_strdup(backing_file_str);
|
|
s->backing_mask_protocol = backing_mask_protocol;
|
|
s->on_error = on_error;
|
|
|
|
trace_commit_start(bs, base, top, s);
|
|
job_start(&s->common.job);
|
|
return;
|
|
|
|
fail:
|
|
if (s->chain_frozen) {
|
|
bdrv_graph_rdlock_main_loop();
|
|
bdrv_unfreeze_backing_chain(commit_top_bs, base);
|
|
bdrv_graph_rdunlock_main_loop();
|
|
}
|
|
if (s->base) {
|
|
blk_unref(s->base);
|
|
}
|
|
if (s->top) {
|
|
blk_unref(s->top);
|
|
}
|
|
if (s->base_read_only) {
|
|
bdrv_reopen_set_read_only(base, true, NULL);
|
|
}
|
|
job_early_fail(&s->common.job);
|
|
/* commit_top_bs has to be replaced after deleting the block job,
|
|
* otherwise this would fail because of lack of permissions. */
|
|
if (commit_top_bs) {
|
|
bdrv_drained_begin(top);
|
|
bdrv_graph_wrlock();
|
|
bdrv_replace_node(commit_top_bs, top, &error_abort);
|
|
bdrv_graph_wrunlock();
|
|
bdrv_drained_end(top);
|
|
}
|
|
}
|
|
|
|
|
|
#define COMMIT_BUF_SIZE (2048 * BDRV_SECTOR_SIZE)
|
|
|
|
/* commit COW file into the raw image */
|
|
int bdrv_commit(BlockDriverState *bs)
|
|
{
|
|
BlockBackend *src, *backing;
|
|
BlockDriverState *backing_file_bs = NULL;
|
|
BlockDriverState *commit_top_bs = NULL;
|
|
BlockDriver *drv = bs->drv;
|
|
AioContext *ctx;
|
|
int64_t offset, length, backing_length;
|
|
int ro;
|
|
int64_t n;
|
|
int ret = 0;
|
|
QEMU_AUTO_VFREE uint8_t *buf = NULL;
|
|
Error *local_err = NULL;
|
|
|
|
GLOBAL_STATE_CODE();
|
|
GRAPH_RDLOCK_GUARD_MAINLOOP();
|
|
|
|
if (!drv)
|
|
return -ENOMEDIUM;
|
|
|
|
backing_file_bs = bdrv_cow_bs(bs);
|
|
|
|
if (!backing_file_bs) {
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
if (bdrv_op_is_blocked(bs, BLOCK_OP_TYPE_COMMIT_SOURCE, NULL) ||
|
|
bdrv_op_is_blocked(backing_file_bs, BLOCK_OP_TYPE_COMMIT_TARGET, NULL))
|
|
{
|
|
return -EBUSY;
|
|
}
|
|
|
|
ro = bdrv_is_read_only(backing_file_bs);
|
|
|
|
if (ro) {
|
|
if (bdrv_reopen_set_read_only(backing_file_bs, false, NULL)) {
|
|
return -EACCES;
|
|
}
|
|
}
|
|
|
|
ctx = bdrv_get_aio_context(bs);
|
|
/* WRITE_UNCHANGED is required for bdrv_make_empty() */
|
|
src = blk_new(ctx, BLK_PERM_CONSISTENT_READ | BLK_PERM_WRITE_UNCHANGED,
|
|
BLK_PERM_ALL);
|
|
backing = blk_new(ctx, BLK_PERM_WRITE | BLK_PERM_RESIZE, BLK_PERM_ALL);
|
|
|
|
ret = blk_insert_bs(src, bs, &local_err);
|
|
if (ret < 0) {
|
|
error_report_err(local_err);
|
|
goto ro_cleanup;
|
|
}
|
|
|
|
/* Insert commit_top block node above backing, so we can write to it */
|
|
commit_top_bs = bdrv_new_open_driver(&bdrv_commit_top, NULL, BDRV_O_RDWR,
|
|
&local_err);
|
|
if (commit_top_bs == NULL) {
|
|
error_report_err(local_err);
|
|
goto ro_cleanup;
|
|
}
|
|
|
|
bdrv_set_backing_hd(commit_top_bs, backing_file_bs, &error_abort);
|
|
bdrv_set_backing_hd(bs, commit_top_bs, &error_abort);
|
|
|
|
ret = blk_insert_bs(backing, backing_file_bs, &local_err);
|
|
if (ret < 0) {
|
|
error_report_err(local_err);
|
|
goto ro_cleanup;
|
|
}
|
|
|
|
length = blk_getlength(src);
|
|
if (length < 0) {
|
|
ret = length;
|
|
goto ro_cleanup;
|
|
}
|
|
|
|
backing_length = blk_getlength(backing);
|
|
if (backing_length < 0) {
|
|
ret = backing_length;
|
|
goto ro_cleanup;
|
|
}
|
|
|
|
/* If our top snapshot is larger than the backing file image,
|
|
* grow the backing file image if possible. If not possible,
|
|
* we must return an error */
|
|
if (length > backing_length) {
|
|
ret = blk_truncate(backing, length, false, PREALLOC_MODE_OFF, 0,
|
|
&local_err);
|
|
if (ret < 0) {
|
|
error_report_err(local_err);
|
|
goto ro_cleanup;
|
|
}
|
|
}
|
|
|
|
/* blk_try_blockalign() for src will choose an alignment that works for
|
|
* backing as well, so no need to compare the alignment manually. */
|
|
buf = blk_try_blockalign(src, COMMIT_BUF_SIZE);
|
|
if (buf == NULL) {
|
|
ret = -ENOMEM;
|
|
goto ro_cleanup;
|
|
}
|
|
|
|
for (offset = 0; offset < length; offset += n) {
|
|
ret = bdrv_is_allocated(bs, offset, COMMIT_BUF_SIZE, &n);
|
|
if (ret < 0) {
|
|
goto ro_cleanup;
|
|
}
|
|
if (ret) {
|
|
ret = blk_pread(src, offset, n, buf, 0);
|
|
if (ret < 0) {
|
|
goto ro_cleanup;
|
|
}
|
|
|
|
ret = blk_pwrite(backing, offset, n, buf, 0);
|
|
if (ret < 0) {
|
|
goto ro_cleanup;
|
|
}
|
|
}
|
|
}
|
|
|
|
ret = blk_make_empty(src, NULL);
|
|
/* Ignore -ENOTSUP */
|
|
if (ret < 0 && ret != -ENOTSUP) {
|
|
goto ro_cleanup;
|
|
}
|
|
|
|
blk_flush(src);
|
|
|
|
/*
|
|
* Make sure all data we wrote to the backing device is actually
|
|
* stable on disk.
|
|
*/
|
|
blk_flush(backing);
|
|
|
|
ret = 0;
|
|
ro_cleanup:
|
|
blk_unref(backing);
|
|
if (bdrv_cow_bs(bs) != backing_file_bs) {
|
|
bdrv_set_backing_hd(bs, backing_file_bs, &error_abort);
|
|
}
|
|
bdrv_unref(commit_top_bs);
|
|
blk_unref(src);
|
|
|
|
if (ro) {
|
|
/* ignoring error return here */
|
|
bdrv_reopen_set_read_only(backing_file_bs, true, NULL);
|
|
}
|
|
|
|
return ret;
|
|
}
|