nbd patches for 2020-07-28

- fix NBD handling of trim/zero requests larger than 2G
 - allow no-op resizes on NBD (in turn fixing qemu-img convert -c into NBD)
 - several deadlock fixes when using NBD reconnect
 -----BEGIN PGP SIGNATURE-----
 
 iQEzBAABCAAdFiEEccLMIrHEYCkn0vOqp6FrSiUnQ2oFAl8gPV4ACgkQp6FrSiUn
 Q2ozdQgAiDHaHG2NX4jmduID7677/XhsLoVl1MV7UZnU+y9qQ2p+Mbsw1oMneu8P
 Dtfgx/mlWVGu68gn31f4xVq74VTZH6p3IGV7PMcYZ50xbESoFs6CYUwUWUp1GeC3
 +kPOl0EpLvm1W/V93sKmg8FflGmNiJHNkfl/ddfk0gs6Z3EfjkmGJt7IP/pv1UCs
 4icWvCJsqw2z8TnEwtTpMX5HZlWth1x37lUOShlPL5kA5hZqU+zYU/bYB5iKx+16
 MebYg7C7CXYCCtH9cDH/swUWhOdQLkywA6yBAwc1zENsKy84aIAJIUls/Ji0q6CY
 A4s5c0FovLBuMDd9oLr0kJbkJQeVZA==
 =DD6l
 -----END PGP SIGNATURE-----

Merge remote-tracking branch 'remotes/ericb/tags/pull-nbd-2020-07-28' into staging

nbd patches for 2020-07-28

- fix NBD handling of trim/zero requests larger than 2G
- allow no-op resizes on NBD (in turn fixing qemu-img convert -c into NBD)
- several deadlock fixes when using NBD reconnect

# gpg: Signature made Tue 28 Jul 2020 15:59:42 BST
# gpg:                using RSA key 71C2CC22B1C4602927D2F3AAA7A16B4A2527436A
# gpg: Good signature from "Eric Blake <eblake@redhat.com>" [full]
# gpg:                 aka "Eric Blake (Free Software Programmer) <ebb9@byu.net>" [full]
# gpg:                 aka "[jpeg image of size 6874]" [full]
# Primary key fingerprint: 71C2 CC22 B1C4 6029 27D2  F3AA A7A1 6B4A 2527 436A

* remotes/ericb/tags/pull-nbd-2020-07-28:
  block/nbd: nbd_co_reconnect_loop(): don't sleep if drained
  block/nbd: on shutdown terminate connection attempt
  block/nbd: allow drain during reconnect attempt
  block/nbd: split nbd_establish_connection out of nbd_client_connect
  iotests: Test convert to qcow2 compressed to NBD
  iotests: Add more qemu_img helpers
  iotests: Make qemu_nbd_popen() a contextmanager
  block: nbd: Fix convert qcow2 compressed to nbd
  nbd: Fix large trim/zero requests

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
Peter Maydell 2020-07-28 20:43:03 +01:00
commit 5045be872d
10 changed files with 340 additions and 87 deletions

View file

@ -93,7 +93,10 @@ typedef struct BDRVNBDState {
char *x_dirty_bitmap; char *x_dirty_bitmap;
} BDRVNBDState; } BDRVNBDState;
static int nbd_client_connect(BlockDriverState *bs, Error **errp); static QIOChannelSocket *nbd_establish_connection(SocketAddress *saddr,
Error **errp);
static int nbd_client_handshake(BlockDriverState *bs, QIOChannelSocket *sioc,
Error **errp);
static void nbd_clear_bdrvstate(BDRVNBDState *s) static void nbd_clear_bdrvstate(BDRVNBDState *s)
{ {
@ -206,11 +209,15 @@ static void nbd_teardown_connection(BlockDriverState *bs)
{ {
BDRVNBDState *s = (BDRVNBDState *)bs->opaque; BDRVNBDState *s = (BDRVNBDState *)bs->opaque;
if (s->state == NBD_CLIENT_CONNECTED) { if (s->ioc) {
/* finish any pending coroutines */ /* finish any pending coroutines */
assert(s->ioc);
qio_channel_shutdown(s->ioc, QIO_CHANNEL_SHUTDOWN_BOTH, NULL); qio_channel_shutdown(s->ioc, QIO_CHANNEL_SHUTDOWN_BOTH, NULL);
} else if (s->sioc) {
/* abort negotiation */
qio_channel_shutdown(QIO_CHANNEL(s->sioc), QIO_CHANNEL_SHUTDOWN_BOTH,
NULL);
} }
s->state = NBD_CLIENT_QUIT; s->state = NBD_CLIENT_QUIT;
if (s->connection_co) { if (s->connection_co) {
if (s->connection_co_sleep_ns_state) { if (s->connection_co_sleep_ns_state) {
@ -241,7 +248,9 @@ static bool nbd_client_connecting_wait(BDRVNBDState *s)
static coroutine_fn void nbd_reconnect_attempt(BDRVNBDState *s) static coroutine_fn void nbd_reconnect_attempt(BDRVNBDState *s)
{ {
int ret;
Error *local_err = NULL; Error *local_err = NULL;
QIOChannelSocket *sioc;
if (!nbd_client_connecting(s)) { if (!nbd_client_connecting(s)) {
return; return;
@ -280,20 +289,40 @@ static coroutine_fn void nbd_reconnect_attempt(BDRVNBDState *s)
s->ioc = NULL; s->ioc = NULL;
} }
s->connect_status = nbd_client_connect(s->bs, &local_err); sioc = nbd_establish_connection(s->saddr, &local_err);
if (!sioc) {
ret = -ECONNREFUSED;
goto out;
}
bdrv_dec_in_flight(s->bs);
ret = nbd_client_handshake(s->bs, sioc, &local_err);
if (s->drained) {
s->wait_drained_end = true;
while (s->drained) {
/*
* We may be entered once from nbd_client_attach_aio_context_bh
* and then from nbd_client_co_drain_end. So here is a loop.
*/
qemu_coroutine_yield();
}
}
bdrv_inc_in_flight(s->bs);
out:
s->connect_status = ret;
error_free(s->connect_err); error_free(s->connect_err);
s->connect_err = NULL; s->connect_err = NULL;
error_propagate(&s->connect_err, local_err); error_propagate(&s->connect_err, local_err);
if (s->connect_status < 0) { if (ret >= 0) {
/* failed attempt */
return;
}
/* successfully connected */ /* successfully connected */
s->state = NBD_CLIENT_CONNECTED; s->state = NBD_CLIENT_CONNECTED;
qemu_co_queue_restart_all(&s->free_sema); qemu_co_queue_restart_all(&s->free_sema);
} }
}
static coroutine_fn void nbd_co_reconnect_loop(BDRVNBDState *s) static coroutine_fn void nbd_co_reconnect_loop(BDRVNBDState *s)
{ {
@ -312,8 +341,6 @@ static coroutine_fn void nbd_co_reconnect_loop(BDRVNBDState *s)
qemu_co_queue_restart_all(&s->free_sema); qemu_co_queue_restart_all(&s->free_sema);
} }
qemu_co_sleep_ns_wakeable(QEMU_CLOCK_REALTIME, timeout,
&s->connection_co_sleep_ns_state);
if (s->drained) { if (s->drained) {
bdrv_dec_in_flight(s->bs); bdrv_dec_in_flight(s->bs);
s->wait_drained_end = true; s->wait_drained_end = true;
@ -325,10 +352,13 @@ static coroutine_fn void nbd_co_reconnect_loop(BDRVNBDState *s)
qemu_coroutine_yield(); qemu_coroutine_yield();
} }
bdrv_inc_in_flight(s->bs); bdrv_inc_in_flight(s->bs);
} } else {
qemu_co_sleep_ns_wakeable(QEMU_CLOCK_REALTIME, timeout,
&s->connection_co_sleep_ns_state);
if (timeout < max_timeout) { if (timeout < max_timeout) {
timeout *= 2; timeout *= 2;
} }
}
nbd_reconnect_attempt(s); nbd_reconnect_attempt(s);
} }
@ -1425,24 +1455,18 @@ static QIOChannelSocket *nbd_establish_connection(SocketAddress *saddr,
return sioc; return sioc;
} }
static int nbd_client_connect(BlockDriverState *bs, Error **errp) /* nbd_client_handshake takes ownership on sioc. On failure it is unref'ed. */
static int nbd_client_handshake(BlockDriverState *bs, QIOChannelSocket *sioc,
Error **errp)
{ {
BDRVNBDState *s = (BDRVNBDState *)bs->opaque; BDRVNBDState *s = (BDRVNBDState *)bs->opaque;
AioContext *aio_context = bdrv_get_aio_context(bs); AioContext *aio_context = bdrv_get_aio_context(bs);
int ret; int ret;
/* trace_nbd_client_handshake(s->export);
* establish TCP connection, return error if it fails
* TODO: Configurable retry-until-timeout behaviour.
*/
QIOChannelSocket *sioc = nbd_establish_connection(s->saddr, errp);
if (!sioc) { s->sioc = sioc;
return -ECONNREFUSED;
}
/* NBD handshake */
trace_nbd_client_connect(s->export);
qio_channel_set_blocking(QIO_CHANNEL(sioc), false, NULL); qio_channel_set_blocking(QIO_CHANNEL(sioc), false, NULL);
qio_channel_attach_aio_context(QIO_CHANNEL(sioc), aio_context); qio_channel_attach_aio_context(QIO_CHANNEL(sioc), aio_context);
@ -1457,6 +1481,7 @@ static int nbd_client_connect(BlockDriverState *bs, Error **errp)
g_free(s->info.name); g_free(s->info.name);
if (ret < 0) { if (ret < 0) {
object_unref(OBJECT(sioc)); object_unref(OBJECT(sioc));
s->sioc = NULL;
return ret; return ret;
} }
if (s->x_dirty_bitmap && !s->info.base_allocation) { if (s->x_dirty_bitmap && !s->info.base_allocation) {
@ -1482,14 +1507,12 @@ static int nbd_client_connect(BlockDriverState *bs, Error **errp)
} }
} }
s->sioc = sioc;
if (!s->ioc) { if (!s->ioc) {
s->ioc = QIO_CHANNEL(sioc); s->ioc = QIO_CHANNEL(sioc);
object_ref(OBJECT(s->ioc)); object_ref(OBJECT(s->ioc));
} }
trace_nbd_client_connect_success(s->export); trace_nbd_client_handshake_success(s->export);
return 0; return 0;
@ -1504,6 +1527,7 @@ static int nbd_client_connect(BlockDriverState *bs, Error **errp)
nbd_send_request(s->ioc ?: QIO_CHANNEL(sioc), &request); nbd_send_request(s->ioc ?: QIO_CHANNEL(sioc), &request);
object_unref(OBJECT(sioc)); object_unref(OBJECT(sioc));
s->sioc = NULL;
return ret; return ret;
} }
@ -1894,6 +1918,7 @@ static int nbd_open(BlockDriverState *bs, QDict *options, int flags,
{ {
int ret; int ret;
BDRVNBDState *s = (BDRVNBDState *)bs->opaque; BDRVNBDState *s = (BDRVNBDState *)bs->opaque;
QIOChannelSocket *sioc;
ret = nbd_process_options(bs, options, errp); ret = nbd_process_options(bs, options, errp);
if (ret < 0) { if (ret < 0) {
@ -1904,7 +1929,16 @@ static int nbd_open(BlockDriverState *bs, QDict *options, int flags,
qemu_co_mutex_init(&s->send_mutex); qemu_co_mutex_init(&s->send_mutex);
qemu_co_queue_init(&s->free_sema); qemu_co_queue_init(&s->free_sema);
ret = nbd_client_connect(bs, errp); /*
* establish TCP connection, return error if it fails
* TODO: Configurable retry-until-timeout behaviour.
*/
sioc = nbd_establish_connection(s->saddr, errp);
if (!sioc) {
return -ECONNREFUSED;
}
ret = nbd_client_handshake(bs, sioc, errp);
if (ret < 0) { if (ret < 0) {
nbd_clear_bdrvstate(s); nbd_clear_bdrvstate(s);
return ret; return ret;
@ -1966,6 +2000,33 @@ static void nbd_close(BlockDriverState *bs)
nbd_clear_bdrvstate(s); nbd_clear_bdrvstate(s);
} }
/*
* NBD cannot truncate, but if the caller asks to truncate to the same size, or
* to a smaller size with exact=false, there is no reason to fail the
* operation.
*
* Preallocation mode is ignored since it does not seems useful to fail when
* we never change anything.
*/
static int coroutine_fn nbd_co_truncate(BlockDriverState *bs, int64_t offset,
bool exact, PreallocMode prealloc,
BdrvRequestFlags flags, Error **errp)
{
BDRVNBDState *s = bs->opaque;
if (offset != s->info.size && exact) {
error_setg(errp, "Cannot resize NBD nodes");
return -ENOTSUP;
}
if (offset > s->info.size) {
error_setg(errp, "Cannot grow NBD nodes");
return -EINVAL;
}
return 0;
}
static int64_t nbd_getlength(BlockDriverState *bs) static int64_t nbd_getlength(BlockDriverState *bs)
{ {
BDRVNBDState *s = bs->opaque; BDRVNBDState *s = bs->opaque;
@ -2045,6 +2106,7 @@ static BlockDriver bdrv_nbd = {
.bdrv_co_flush_to_os = nbd_co_flush, .bdrv_co_flush_to_os = nbd_co_flush,
.bdrv_co_pdiscard = nbd_client_co_pdiscard, .bdrv_co_pdiscard = nbd_client_co_pdiscard,
.bdrv_refresh_limits = nbd_refresh_limits, .bdrv_refresh_limits = nbd_refresh_limits,
.bdrv_co_truncate = nbd_co_truncate,
.bdrv_getlength = nbd_getlength, .bdrv_getlength = nbd_getlength,
.bdrv_detach_aio_context = nbd_client_detach_aio_context, .bdrv_detach_aio_context = nbd_client_detach_aio_context,
.bdrv_attach_aio_context = nbd_client_attach_aio_context, .bdrv_attach_aio_context = nbd_client_attach_aio_context,
@ -2072,6 +2134,7 @@ static BlockDriver bdrv_nbd_tcp = {
.bdrv_co_flush_to_os = nbd_co_flush, .bdrv_co_flush_to_os = nbd_co_flush,
.bdrv_co_pdiscard = nbd_client_co_pdiscard, .bdrv_co_pdiscard = nbd_client_co_pdiscard,
.bdrv_refresh_limits = nbd_refresh_limits, .bdrv_refresh_limits = nbd_refresh_limits,
.bdrv_co_truncate = nbd_co_truncate,
.bdrv_getlength = nbd_getlength, .bdrv_getlength = nbd_getlength,
.bdrv_detach_aio_context = nbd_client_detach_aio_context, .bdrv_detach_aio_context = nbd_client_detach_aio_context,
.bdrv_attach_aio_context = nbd_client_attach_aio_context, .bdrv_attach_aio_context = nbd_client_attach_aio_context,
@ -2099,6 +2162,7 @@ static BlockDriver bdrv_nbd_unix = {
.bdrv_co_flush_to_os = nbd_co_flush, .bdrv_co_flush_to_os = nbd_co_flush,
.bdrv_co_pdiscard = nbd_client_co_pdiscard, .bdrv_co_pdiscard = nbd_client_co_pdiscard,
.bdrv_refresh_limits = nbd_refresh_limits, .bdrv_refresh_limits = nbd_refresh_limits,
.bdrv_co_truncate = nbd_co_truncate,
.bdrv_getlength = nbd_getlength, .bdrv_getlength = nbd_getlength,
.bdrv_detach_aio_context = nbd_client_detach_aio_context, .bdrv_detach_aio_context = nbd_client_detach_aio_context,
.bdrv_attach_aio_context = nbd_client_attach_aio_context, .bdrv_attach_aio_context = nbd_client_attach_aio_context,

View file

@ -168,8 +168,8 @@ nbd_parse_blockstatus_compliance(const char *err) "ignoring extra data from non-
nbd_structured_read_compliance(const char *type) "server sent non-compliant unaligned read %s chunk" nbd_structured_read_compliance(const char *type) "server sent non-compliant unaligned read %s chunk"
nbd_read_reply_entry_fail(int ret, const char *err) "ret = %d, err: %s" nbd_read_reply_entry_fail(int ret, const char *err) "ret = %d, err: %s"
nbd_co_request_fail(uint64_t from, uint32_t len, uint64_t handle, uint16_t flags, uint16_t type, const char *name, int ret, const char *err) "Request failed { .from = %" PRIu64", .len = %" PRIu32 ", .handle = %" PRIu64 ", .flags = 0x%" PRIx16 ", .type = %" PRIu16 " (%s) } ret = %d, err: %s" nbd_co_request_fail(uint64_t from, uint32_t len, uint64_t handle, uint16_t flags, uint16_t type, const char *name, int ret, const char *err) "Request failed { .from = %" PRIu64", .len = %" PRIu32 ", .handle = %" PRIu64 ", .flags = 0x%" PRIx16 ", .type = %" PRIu16 " (%s) } ret = %d, err: %s"
nbd_client_connect(const char *export_name) "export '%s'" nbd_client_handshake(const char *export_name) "export '%s'"
nbd_client_connect_success(const char *export_name) "export '%s'" nbd_client_handshake_success(const char *export_name) "export '%s'"
# ssh.c # ssh.c
ssh_restart_coroutine(void *co) "co=%p" ssh_restart_coroutine(void *co) "co=%p"

View file

@ -2378,8 +2378,17 @@ static coroutine_fn int nbd_handle_request(NBDClient *client,
if (request->flags & NBD_CMD_FLAG_FAST_ZERO) { if (request->flags & NBD_CMD_FLAG_FAST_ZERO) {
flags |= BDRV_REQ_NO_FALLBACK; flags |= BDRV_REQ_NO_FALLBACK;
} }
ret = 0;
/* FIXME simplify this when blk_pwrite_zeroes switches to 64-bit */
while (ret >= 0 && request->len) {
int align = client->check_align ?: 1;
int len = MIN(request->len, QEMU_ALIGN_DOWN(BDRV_REQUEST_MAX_BYTES,
align));
ret = blk_pwrite_zeroes(exp->blk, request->from + exp->dev_offset, ret = blk_pwrite_zeroes(exp->blk, request->from + exp->dev_offset,
request->len, flags); len, flags);
request->len -= len;
request->from += len;
}
return nbd_send_generic_reply(client, request->handle, ret, return nbd_send_generic_reply(client, request->handle, ret,
"writing to file failed", errp); "writing to file failed", errp);
@ -2393,9 +2402,18 @@ static coroutine_fn int nbd_handle_request(NBDClient *client,
"flush failed", errp); "flush failed", errp);
case NBD_CMD_TRIM: case NBD_CMD_TRIM:
ret = 0;
/* FIXME simplify this when blk_co_pdiscard switches to 64-bit */
while (ret >= 0 && request->len) {
int align = client->check_align ?: 1;
int len = MIN(request->len, QEMU_ALIGN_DOWN(BDRV_REQUEST_MAX_BYTES,
align));
ret = blk_co_pdiscard(exp->blk, request->from + exp->dev_offset, ret = blk_co_pdiscard(exp->blk, request->from + exp->dev_offset,
request->len); len);
if (ret == 0 && request->flags & NBD_CMD_FLAG_FUA) { request->len -= len;
request->from += len;
}
if (ret >= 0 && request->flags & NBD_CMD_FLAG_FUA) {
ret = blk_co_flush(exp->blk); ret = blk_co_flush(exp->blk);
} }
return nbd_send_generic_reply(client, request->handle, ret, return nbd_send_generic_reply(client, request->handle, ret,

View file

@ -1715,7 +1715,7 @@ static int truncate_f(BlockBackend *blk, int argc, char **argv)
* exact=true. It is better to err on the "emit more errors" side * exact=true. It is better to err on the "emit more errors" side
* than to be overly permissive. * than to be overly permissive.
*/ */
ret = blk_truncate(blk, offset, true, PREALLOC_MODE_OFF, 0, &local_err); ret = blk_truncate(blk, offset, false, PREALLOC_MODE_OFF, 0, &local_err);
if (ret < 0) { if (ret < 0) {
error_report_err(local_err); error_report_err(local_err);
return ret; return ret;

View file

@ -36,20 +36,8 @@ wait_step = 0.2
qemu_img_create('-f', iotests.imgfmt, disk_a, str(size)) qemu_img_create('-f', iotests.imgfmt, disk_a, str(size))
qemu_img_create('-f', iotests.imgfmt, disk_b, str(size)) qemu_img_create('-f', iotests.imgfmt, disk_b, str(size))
srv = qemu_nbd_popen('-k', nbd_sock, '-f', iotests.imgfmt, disk_b)
# Wait for NBD server availability
t = 0
ok = False
while t < wait_limit:
ok = qemu_io_silent_check('-f', 'raw', '-c', 'read 0 512', nbd_uri)
if ok:
break
time.sleep(wait_step)
t += wait_step
assert ok
with qemu_nbd_popen('-k', nbd_sock, '-f', iotests.imgfmt, disk_b):
vm = iotests.VM().add_drive(disk_a) vm = iotests.VM().add_drive(disk_a)
vm.launch() vm.launch()
vm.hmp_qemu_io('drive0', 'write 0 {}'.format(size)) vm.hmp_qemu_io('drive0', 'write 0 {}'.format(size))
@ -75,10 +63,6 @@ while t < wait_limit:
if jobs and jobs[0]['offset'] > 0: if jobs and jobs[0]['offset'] > 0:
log('Backup job is started') log('Backup job is started')
log('Kill NBD server')
srv.kill()
srv.wait()
jobs = vm.qmp('query-block-jobs')['return'] jobs = vm.qmp('query-block-jobs')['return']
if jobs and jobs[0]['offset'] < jobs[0]['len']: if jobs and jobs[0]['offset'] < jobs[0]['len']:
log('Backup job is still in progress') log('Backup job is still in progress')
@ -88,12 +72,8 @@ vm.qmp_log('block-job-set-speed', device='drive0', speed=0)
# Emulate server down time for 1 second # Emulate server down time for 1 second
time.sleep(1) time.sleep(1)
log('Start NBD server') with qemu_nbd_popen('-k', nbd_sock, '-f', iotests.imgfmt, disk_b):
srv = qemu_nbd_popen('-k', nbd_sock, '-f', iotests.imgfmt, disk_b)
e = vm.event_wait('BLOCK_JOB_COMPLETED') e = vm.event_wait('BLOCK_JOB_COMPLETED')
log('Backup completed: {}'.format(e['data']['offset'])) log('Backup completed: {}'.format(e['data']['offset']))
vm.qmp_log('blockdev-del', node_name='backup0') vm.qmp_log('blockdev-del', node_name='backup0')
srv.kill()
vm.shutdown() vm.shutdown()

View file

@ -1,3 +1,4 @@
Start NBD server
{"execute": "blockdev-add", "arguments": {"driver": "raw", "file": {"driver": "nbd", "reconnect-delay": 10, "server": {"path": "TEST_DIR/PID-nbd-sock", "type": "unix"}}, "node-name": "backup0"}} {"execute": "blockdev-add", "arguments": {"driver": "raw", "file": {"driver": "nbd", "reconnect-delay": 10, "server": {"path": "TEST_DIR/PID-nbd-sock", "type": "unix"}}, "node-name": "backup0"}}
{"return": {}} {"return": {}}
{"execute": "blockdev-backup", "arguments": {"device": "drive0", "speed": 1048576, "sync": "full", "target": "backup0"}} {"execute": "blockdev-backup", "arguments": {"device": "drive0", "speed": 1048576, "sync": "full", "target": "backup0"}}
@ -11,3 +12,4 @@ Start NBD server
Backup completed: 5242880 Backup completed: 5242880
{"execute": "blockdev-del", "arguments": {"node-name": "backup0"}} {"execute": "blockdev-del", "arguments": {"node-name": "backup0"}}
{"return": {}} {"return": {}}
Kill NBD server

127
tests/qemu-iotests/302 Executable file
View file

@ -0,0 +1,127 @@
#!/usr/bin/env python3
#
# Tests converting qcow2 compressed to NBD
#
# Copyright (c) 2020 Nir Soffer <nirsof@gmail.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/>.
#
# owner=nirsof@gmail.com
import io
import tarfile
import iotests
from iotests import (
file_path,
qemu_img,
qemu_img_check,
qemu_img_create,
qemu_img_log,
qemu_img_measure,
qemu_io,
qemu_nbd_popen,
)
iotests.script_initialize(supported_fmts=["qcow2"])
# Create source disk. Using qcow2 to enable strict comparing later, and
# avoid issues with random filesystem on CI environment.
src_disk = file_path("disk.qcow2")
qemu_img_create("-f", iotests.imgfmt, src_disk, "1g")
qemu_io("-f", iotests.imgfmt, "-c", "write 1m 64k", src_disk)
# The use case is writing qcow2 image directly into an ova file, which
# is a tar file with specific layout. This is tricky since we don't know the
# size of the image before compressing, so we have to do:
# 1. Add an ovf file.
# 2. Find the offset of the next member data.
# 3. Make room for image data, allocating for the worst case.
# 4. Write compressed image data into the tar.
# 5. Add a tar entry with the actual image size.
# 6. Shrink the tar to the actual size, aligned to 512 bytes.
tar_file = file_path("test.ova")
with tarfile.open(tar_file, "w") as tar:
# 1. Add an ovf file.
ovf_data = b"<xml/>"
ovf = tarfile.TarInfo("vm.ovf")
ovf.size = len(ovf_data)
tar.addfile(ovf, io.BytesIO(ovf_data))
# 2. Find the offset of the next member data.
offset = tar.fileobj.tell() + 512
# 3. Make room for image data, allocating for the worst case.
measure = qemu_img_measure("-O", "qcow2", src_disk)
tar.fileobj.truncate(offset + measure["required"])
# 4. Write compressed image data into the tar.
nbd_sock = file_path("nbd-sock", base_dir=iotests.sock_dir)
nbd_uri = "nbd+unix:///exp?socket=" + nbd_sock
# Use raw format to allow creating qcow2 directly into tar file.
with qemu_nbd_popen(
"--socket", nbd_sock,
"--export-name", "exp",
"--format", "raw",
"--offset", str(offset),
tar_file):
iotests.log("=== Target image info ===")
qemu_img_log("info", nbd_uri)
qemu_img(
"convert",
"-f", iotests.imgfmt,
"-O", "qcow2",
"-c",
src_disk,
nbd_uri)
iotests.log("=== Converted image info ===")
qemu_img_log("info", nbd_uri)
iotests.log("=== Converted image check ===")
qemu_img_log("check", nbd_uri)
iotests.log("=== Comparing to source disk ===")
qemu_img_log("compare", src_disk, nbd_uri)
actual_size = qemu_img_check(nbd_uri)["image-end-offset"]
# 5. Add a tar entry with the actual image size.
disk = tarfile.TarInfo("disk")
disk.size = actual_size
tar.addfile(disk)
# 6. Shrink the tar to the actual size, aligned to 512 bytes.
tar_size = offset + (disk.size + 511) & ~511
tar.fileobj.seek(tar_size)
tar.fileobj.truncate(tar_size)
with tarfile.open(tar_file) as tar:
members = [{"name": m.name, "size": m.size, "offset": m.offset_data}
for m in tar]
iotests.log("=== OVA file contents ===")
iotests.log(members)

View file

@ -0,0 +1,31 @@
Start NBD server
=== Target image info ===
image: nbd+unix:///exp?socket=SOCK_DIR/PID-nbd-sock
file format: raw
virtual size: 448 KiB (458752 bytes)
disk size: unavailable
=== Converted image info ===
image: nbd+unix:///exp?socket=SOCK_DIR/PID-nbd-sock
file format: qcow2
virtual size: 1 GiB (1073741824 bytes)
disk size: unavailable
cluster_size: 65536
Format specific information:
compat: 1.1
compression type: zlib
lazy refcounts: false
refcount bits: 16
corrupt: false
=== Converted image check ===
No errors were found on the image.
1/16384 = 0.01% allocated, 100.00% fragmented, 100.00% compressed clusters
Image end offset: 393216
=== Comparing to source disk ===
Images are identical.
Kill NBD server
=== OVA file contents ===
[{"name": "vm.ovf", "offset": 512, "size": 6}, {"name": "disk", "offset": 1536, "size": 393216}]

View file

@ -308,3 +308,4 @@
297 meta 297 meta
299 auto quick 299 auto quick
301 backing quick 301 backing quick
302 quick

View file

@ -28,10 +28,13 @@ import signal
import struct import struct
import subprocess import subprocess
import sys import sys
import time
from typing import (Any, Callable, Dict, Iterable, from typing import (Any, Callable, Dict, Iterable,
List, Optional, Sequence, Tuple, TypeVar) List, Optional, Sequence, Tuple, TypeVar)
import unittest import unittest
from contextlib import contextmanager
# pylint: disable=import-error, wrong-import-position # pylint: disable=import-error, wrong-import-position
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
from qemu import qtest from qemu import qtest
@ -138,6 +141,12 @@ def qemu_img_create(*args):
return qemu_img(*args) return qemu_img(*args)
def qemu_img_measure(*args):
return json.loads(qemu_img_pipe("measure", "--output", "json", *args))
def qemu_img_check(*args):
return json.loads(qemu_img_pipe("check", "--output", "json", *args))
def qemu_img_verbose(*args): def qemu_img_verbose(*args):
'''Run qemu-img without suppressing its output and return the exit code''' '''Run qemu-img without suppressing its output and return the exit code'''
exitcode = subprocess.call(qemu_img_args + list(args)) exitcode = subprocess.call(qemu_img_args + list(args))
@ -270,9 +279,30 @@ def qemu_nbd_early_pipe(*args):
return subp.returncode, output if subp.returncode else '' return subp.returncode, output if subp.returncode else ''
@contextmanager
def qemu_nbd_popen(*args): def qemu_nbd_popen(*args):
'''Run qemu-nbd in daemon mode and return the parent's exit code''' '''Context manager running qemu-nbd within the context'''
return subprocess.Popen(qemu_nbd_args + ['--persistent'] + list(args)) pid_file = file_path("pid")
cmd = list(qemu_nbd_args)
cmd.extend(('--persistent', '--pid-file', pid_file))
cmd.extend(args)
log('Start NBD server')
p = subprocess.Popen(cmd)
try:
while not os.path.exists(pid_file):
if p.poll() is not None:
raise RuntimeError(
"qemu-nbd terminated with exit code {}: {}"
.format(p.returncode, ' '.join(cmd)))
time.sleep(0.01)
yield
finally:
log('Kill NBD server')
p.kill()
p.wait()
def compare_images(img1, img2, fmt1=imgfmt, fmt2=imgfmt): def compare_images(img1, img2, fmt1=imgfmt, fmt2=imgfmt):
'''Return True if two image files are identical''' '''Return True if two image files are identical'''