bitmaps patches for 2020-08-21

- Andrey Shinkevich: Enhance qcow2.py for iotest inspection of qcow2 images
 - Max Reitz: Add block-bitmap-mapping migration parameter
 -----BEGIN PGP SIGNATURE-----
 
 iQEzBAABCAAdFiEEccLMIrHEYCkn0vOqp6FrSiUnQ2oFAl8/1JMACgkQp6FrSiUn
 Q2rfQAf+IxDfjueqHm5+KdrA6FJ1qfoeLDndvOopm7ax2KPIsrhNRJQZ2i0Ts/RU
 oIQaY9BESYiQkTdw6q4THmCBlkMDYqDnJuWQRRFC5NAuTA4Q4EbO4j3WZhvh7Vfb
 OPcvdBYo16+ujD/h76mSpeuQvA8fnFUJ1pRhmJvBX78nj0uHE5UMxXjB9v2hoFOx
 tg5ApM3l4Fzm/eUz/5MY5+eX7XUMpmeN7G4qMfJxZGgIAeh6UiZaDVz7J2bVNo/c
 L3EqjPiWJcgTk3Tt8DhAZynVlqkdvgwyGwsntYybAyz+GSPCOPd5gdoAxIdv0wxx
 roVRJ3ARrQl5agYLc2A2nXcmxz9WZA==
 =cec/
 -----END PGP SIGNATURE-----

Merge remote-tracking branch 'remotes/ericb/tags/pull-bitmaps-2020-08-21' into staging

bitmaps patches for 2020-08-21

- Andrey Shinkevich: Enhance qcow2.py for iotest inspection of qcow2 images
- Max Reitz: Add block-bitmap-mapping migration parameter

# gpg: Signature made Fri 21 Aug 2020 15:05:07 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-bitmaps-2020-08-21:
  iotests: Test node/bitmap aliases during migration
  iotests.py: Let wait_migration() return on failure
  migration: Add block-bitmap-mapping parameter
  iotests: dump QCOW2 header in JSON in #303
  qcow2_format.py: support dumping metadata in JSON format
  qcow2_format.py: collect fields to dump in JSON format
  qcow2.py: Introduce '-j' key to dump in JSON format
  qcow2_format.py: Dump bitmap table serialized entries
  qcow2_format.py: pass cluster size to substructures
  qcow2_format.py: Dump bitmap directory information
  qcow2_format.py: dump bitmap flags in human readable way.
  qcow2_format.py: change Qcow2BitmapExt initialization method
  qcow2_format.py: make printable data an extension class member
  iotests: add test for QCOW2 header dump

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
Peter Maydell 2020-08-22 19:57:23 +01:00
commit 66e01f1cdc
13 changed files with 1568 additions and 85 deletions

View File

@ -29,10 +29,10 @@
*
* # Header (shared for different chunk types)
* 1, 2 or 4 bytes: flags (see qemu_{put,put}_flags)
* [ 1 byte: node name size ] \ flags & DEVICE_NAME
* [ n bytes: node name ] /
* [ 1 byte: bitmap name size ] \ flags & BITMAP_NAME
* [ n bytes: bitmap name ] /
* [ 1 byte: node alias size ] \ flags & DEVICE_NAME
* [ n bytes: node alias ] /
* [ 1 byte: bitmap alias size ] \ flags & BITMAP_NAME
* [ n bytes: bitmap alias ] /
*
* # Start of bitmap migration (flags & START)
* header
@ -72,7 +72,9 @@
#include "migration/register.h"
#include "qemu/hbitmap.h"
#include "qemu/cutils.h"
#include "qemu/id.h"
#include "qapi/error.h"
#include "qapi/qapi-commands-migration.h"
#include "trace.h"
#define CHUNK_SIZE (1 << 10)
@ -104,7 +106,8 @@
typedef struct SaveBitmapState {
/* Written during setup phase. */
BlockDriverState *bs;
const char *node_name;
char *node_alias;
char *bitmap_alias;
BdrvDirtyBitmap *bitmap;
uint64_t total_sectors;
uint64_t sectors_per_chunk;
@ -138,8 +141,9 @@ typedef struct LoadBitmapState {
/* State of the dirty bitmap migration (DBM) during load process */
typedef struct DBMLoadState {
uint32_t flags;
char node_name[256];
char bitmap_name[256];
char node_alias[256];
char bitmap_alias[256];
char bitmap_name[BDRV_BITMAP_MAX_NAME_SIZE + 1];
BlockDriverState *bs;
BdrvDirtyBitmap *bitmap;
@ -165,6 +169,188 @@ typedef struct DBMState {
static DBMState dbm_state;
/* For hash tables that map node/bitmap names to aliases */
typedef struct AliasMapInnerNode {
char *string;
GHashTable *subtree;
} AliasMapInnerNode;
static void free_alias_map_inner_node(void *amin_ptr)
{
AliasMapInnerNode *amin = amin_ptr;
g_free(amin->string);
g_hash_table_unref(amin->subtree);
g_free(amin);
}
/**
* Construct an alias map based on the given QMP structure.
*
* (Note that we cannot store such maps in the MigrationParameters
* object, because that struct is defined by the QAPI schema, which
* makes it basically impossible to have dicts with arbitrary keys.
* Therefore, we instead have to construct these maps when migration
* starts.)
*
* @bbm is the block_bitmap_mapping from the migration parameters.
*
* If @name_to_alias is true, the returned hash table will map node
* and bitmap names to their respective aliases (for outgoing
* migration).
*
* If @name_to_alias is false, the returned hash table will map node
* and bitmap aliases to their respective names (for incoming
* migration).
*
* The hash table maps node names/aliases to AliasMapInnerNode
* objects, whose .string is the respective node alias/name, and whose
* .subtree table maps bitmap names/aliases to the respective bitmap
* alias/name.
*/
static GHashTable *construct_alias_map(const BitmapMigrationNodeAliasList *bbm,
bool name_to_alias,
Error **errp)
{
GHashTable *alias_map;
size_t max_node_name_len = sizeof_field(BlockDriverState, node_name) - 1;
alias_map = g_hash_table_new_full(g_str_hash, g_str_equal,
g_free, free_alias_map_inner_node);
for (; bbm; bbm = bbm->next) {
const BitmapMigrationNodeAlias *bmna = bbm->value;
const BitmapMigrationBitmapAliasList *bmbal;
AliasMapInnerNode *amin;
GHashTable *bitmaps_map;
const char *node_map_from, *node_map_to;
if (!id_wellformed(bmna->alias)) {
error_setg(errp, "The node alias '%s' is not well-formed",
bmna->alias);
goto fail;
}
if (strlen(bmna->alias) > UINT8_MAX) {
error_setg(errp, "The node alias '%s' is longer than %u bytes",
bmna->alias, UINT8_MAX);
goto fail;
}
if (strlen(bmna->node_name) > max_node_name_len) {
error_setg(errp, "The node name '%s' is longer than %zu bytes",
bmna->node_name, max_node_name_len);
goto fail;
}
if (name_to_alias) {
if (g_hash_table_contains(alias_map, bmna->node_name)) {
error_setg(errp, "The node name '%s' is mapped twice",
bmna->node_name);
goto fail;
}
node_map_from = bmna->node_name;
node_map_to = bmna->alias;
} else {
if (g_hash_table_contains(alias_map, bmna->alias)) {
error_setg(errp, "The node alias '%s' is used twice",
bmna->alias);
goto fail;
}
node_map_from = bmna->alias;
node_map_to = bmna->node_name;
}
bitmaps_map = g_hash_table_new_full(g_str_hash, g_str_equal,
g_free, g_free);
amin = g_new(AliasMapInnerNode, 1);
*amin = (AliasMapInnerNode){
.string = g_strdup(node_map_to),
.subtree = bitmaps_map,
};
g_hash_table_insert(alias_map, g_strdup(node_map_from), amin);
for (bmbal = bmna->bitmaps; bmbal; bmbal = bmbal->next) {
const BitmapMigrationBitmapAlias *bmba = bmbal->value;
const char *bmap_map_from, *bmap_map_to;
if (strlen(bmba->alias) > UINT8_MAX) {
error_setg(errp,
"The bitmap alias '%s' is longer than %u bytes",
bmba->alias, UINT8_MAX);
goto fail;
}
if (strlen(bmba->name) > BDRV_BITMAP_MAX_NAME_SIZE) {
error_setg(errp, "The bitmap name '%s' is longer than %d bytes",
bmba->name, BDRV_BITMAP_MAX_NAME_SIZE);
goto fail;
}
if (name_to_alias) {
bmap_map_from = bmba->name;
bmap_map_to = bmba->alias;
if (g_hash_table_contains(bitmaps_map, bmba->name)) {
error_setg(errp, "The bitmap '%s'/'%s' is mapped twice",
bmna->node_name, bmba->name);
goto fail;
}
} else {
bmap_map_from = bmba->alias;
bmap_map_to = bmba->name;
if (g_hash_table_contains(bitmaps_map, bmba->alias)) {
error_setg(errp, "The bitmap alias '%s'/'%s' is used twice",
bmna->alias, bmba->alias);
goto fail;
}
}
g_hash_table_insert(bitmaps_map,
g_strdup(bmap_map_from), g_strdup(bmap_map_to));
}
}
return alias_map;
fail:
g_hash_table_destroy(alias_map);
return NULL;
}
/**
* Run construct_alias_map() in both directions to check whether @bbm
* is valid.
* (This function is to be used by migration/migration.c to validate
* the user-specified block-bitmap-mapping migration parameter.)
*
* Returns true if and only if the mapping is valid.
*/
bool check_dirty_bitmap_mig_alias_map(const BitmapMigrationNodeAliasList *bbm,
Error **errp)
{
GHashTable *alias_map;
alias_map = construct_alias_map(bbm, true, errp);
if (!alias_map) {
return false;
}
g_hash_table_destroy(alias_map);
alias_map = construct_alias_map(bbm, false, errp);
if (!alias_map) {
return false;
}
g_hash_table_destroy(alias_map);
return true;
}
static uint32_t qemu_get_bitmap_flags(QEMUFile *f)
{
uint8_t flags = qemu_get_byte(f);
@ -207,11 +393,11 @@ static void send_bitmap_header(QEMUFile *f, DBMSaveState *s,
qemu_put_bitmap_flags(f, flags);
if (flags & DIRTY_BITMAP_MIG_FLAG_DEVICE_NAME) {
qemu_put_counted_string(f, dbms->node_name);
qemu_put_counted_string(f, dbms->node_alias);
}
if (flags & DIRTY_BITMAP_MIG_FLAG_BITMAP_NAME) {
qemu_put_counted_string(f, bdrv_dirty_bitmap_name(bitmap));
qemu_put_counted_string(f, dbms->bitmap_alias);
}
}
@ -282,18 +468,25 @@ static void dirty_bitmap_do_save_cleanup(DBMSaveState *s)
QSIMPLEQ_REMOVE_HEAD(&s->dbms_list, entry);
bdrv_dirty_bitmap_set_busy(dbms->bitmap, false);
bdrv_unref(dbms->bs);
g_free(dbms->node_alias);
g_free(dbms->bitmap_alias);
g_free(dbms);
}
}
/* Called with iothread lock taken. */
static int add_bitmaps_to_list(DBMSaveState *s, BlockDriverState *bs,
const char *bs_name)
const char *bs_name, GHashTable *alias_map)
{
BdrvDirtyBitmap *bitmap;
SaveBitmapState *dbms;
GHashTable *bitmap_aliases;
const char *node_alias, *bitmap_name, *bitmap_alias;
Error *local_err = NULL;
/* When an alias map is given, @bs_name must be @bs's node name */
assert(!alias_map || !strcmp(bs_name, bdrv_get_node_name(bs)));
FOR_EACH_DIRTY_BITMAP(bs, bitmap) {
if (bdrv_dirty_bitmap_name(bitmap)) {
break;
@ -303,21 +496,39 @@ static int add_bitmaps_to_list(DBMSaveState *s, BlockDriverState *bs,
return 0;
}
bitmap_name = bdrv_dirty_bitmap_name(bitmap);
if (!bs_name || strcmp(bs_name, "") == 0) {
error_report("Bitmap '%s' in unnamed node can't be migrated",
bdrv_dirty_bitmap_name(bitmap));
bitmap_name);
return -1;
}
if (bs_name[0] == '#') {
if (alias_map) {
const AliasMapInnerNode *amin = g_hash_table_lookup(alias_map, bs_name);
if (!amin) {
/* Skip bitmaps on nodes with no alias */
return 0;
}
node_alias = amin->string;
bitmap_aliases = amin->subtree;
} else {
node_alias = bs_name;
bitmap_aliases = NULL;
}
if (node_alias[0] == '#') {
error_report("Bitmap '%s' in a node with auto-generated "
"name '%s' can't be migrated",
bdrv_dirty_bitmap_name(bitmap), bs_name);
bitmap_name, node_alias);
return -1;
}
FOR_EACH_DIRTY_BITMAP(bs, bitmap) {
if (!bdrv_dirty_bitmap_name(bitmap)) {
bitmap_name = bdrv_dirty_bitmap_name(bitmap);
if (!bitmap_name) {
continue;
}
@ -326,12 +537,29 @@ static int add_bitmaps_to_list(DBMSaveState *s, BlockDriverState *bs,
return -1;
}
if (bitmap_aliases) {
bitmap_alias = g_hash_table_lookup(bitmap_aliases, bitmap_name);
if (!bitmap_alias) {
/* Skip bitmaps with no alias */
continue;
}
} else {
if (strlen(bitmap_name) > UINT8_MAX) {
error_report("Cannot migrate bitmap '%s' on node '%s': "
"Name is longer than %u bytes",
bitmap_name, bs_name, UINT8_MAX);
return -1;
}
bitmap_alias = bitmap_name;
}
bdrv_ref(bs);
bdrv_dirty_bitmap_set_busy(bitmap, true);
dbms = g_new0(SaveBitmapState, 1);
dbms->bs = bs;
dbms->node_name = bs_name;
dbms->node_alias = g_strdup(node_alias);
dbms->bitmap_alias = g_strdup(bitmap_alias);
dbms->bitmap = bitmap;
dbms->total_sectors = bdrv_nb_sectors(bs);
dbms->sectors_per_chunk = CHUNK_SIZE * 8 *
@ -356,43 +584,52 @@ static int init_dirty_bitmap_migration(DBMSaveState *s)
SaveBitmapState *dbms;
GHashTable *handled_by_blk = g_hash_table_new(NULL, NULL);
BlockBackend *blk;
const MigrationParameters *mig_params = &migrate_get_current()->parameters;
GHashTable *alias_map = NULL;
if (mig_params->has_block_bitmap_mapping) {
alias_map = construct_alias_map(mig_params->block_bitmap_mapping, true,
&error_abort);
}
s->bulk_completed = false;
s->prev_bs = NULL;
s->prev_bitmap = NULL;
s->no_bitmaps = false;
/*
* Use blockdevice name for direct (or filtered) children of named block
* backends.
*/
for (blk = blk_next(NULL); blk; blk = blk_next(blk)) {
const char *name = blk_name(blk);
if (!alias_map) {
/*
* Use blockdevice name for direct (or filtered) children of named block
* backends.
*/
for (blk = blk_next(NULL); blk; blk = blk_next(blk)) {
const char *name = blk_name(blk);
if (!name || strcmp(name, "") == 0) {
continue;
}
bs = blk_bs(blk);
/* Skip filters without bitmaps */
while (bs && bs->drv && bs->drv->is_filter &&
!bdrv_has_named_bitmaps(bs))
{
if (bs->backing) {
bs = bs->backing->bs;
} else if (bs->file) {
bs = bs->file->bs;
} else {
bs = NULL;
if (!name || strcmp(name, "") == 0) {
continue;
}
}
if (bs && bs->drv && !bs->drv->is_filter) {
if (add_bitmaps_to_list(s, bs, name)) {
goto fail;
bs = blk_bs(blk);
/* Skip filters without bitmaps */
while (bs && bs->drv && bs->drv->is_filter &&
!bdrv_has_named_bitmaps(bs))
{
if (bs->backing) {
bs = bs->backing->bs;
} else if (bs->file) {
bs = bs->file->bs;
} else {
bs = NULL;
}
}
if (bs && bs->drv && !bs->drv->is_filter) {
if (add_bitmaps_to_list(s, bs, name, NULL)) {
goto fail;
}
g_hash_table_add(handled_by_blk, bs);
}
g_hash_table_add(handled_by_blk, bs);
}
}
@ -401,7 +638,7 @@ static int init_dirty_bitmap_migration(DBMSaveState *s)
continue;
}
if (add_bitmaps_to_list(s, bs, bdrv_get_node_name(bs))) {
if (add_bitmaps_to_list(s, bs, bdrv_get_node_name(bs), alias_map)) {
goto fail;
}
}
@ -416,11 +653,17 @@ static int init_dirty_bitmap_migration(DBMSaveState *s)
}
g_hash_table_destroy(handled_by_blk);
if (alias_map) {
g_hash_table_destroy(alias_map);
}
return 0;
fail:
g_hash_table_destroy(handled_by_blk);
if (alias_map) {
g_hash_table_destroy(alias_map);
}
dirty_bitmap_do_save_cleanup(s);
return -1;
@ -770,8 +1013,10 @@ static int dirty_bitmap_load_bits(QEMUFile *f, DBMLoadState *s)
return 0;
}
static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s)
static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s,
GHashTable *alias_map)
{
GHashTable *bitmap_alias_map = NULL;
Error *local_err = NULL;
bool nothing;
s->flags = qemu_get_bitmap_flags(f);
@ -780,28 +1025,75 @@ static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s)
nothing = s->flags == (s->flags & DIRTY_BITMAP_MIG_FLAG_EOS);
if (s->flags & DIRTY_BITMAP_MIG_FLAG_DEVICE_NAME) {
if (!qemu_get_counted_string(f, s->node_name)) {
error_report("Unable to read node name string");
if (!qemu_get_counted_string(f, s->node_alias)) {
error_report("Unable to read node alias string");
return -EINVAL;
}
if (!s->cancelled) {
s->bs = bdrv_lookup_bs(s->node_name, s->node_name, &local_err);
if (alias_map) {
const AliasMapInnerNode *amin;
amin = g_hash_table_lookup(alias_map, s->node_alias);
if (!amin) {
error_setg(&local_err, "Error: Unknown node alias '%s'",
s->node_alias);
s->bs = NULL;
} else {
bitmap_alias_map = amin->subtree;
s->bs = bdrv_lookup_bs(NULL, amin->string, &local_err);
}
} else {
s->bs = bdrv_lookup_bs(s->node_alias, s->node_alias,
&local_err);
}
if (!s->bs) {
error_report_err(local_err);
cancel_incoming_locked(s);
}
}
} else if (!s->bs && !nothing && !s->cancelled) {
} else if (s->bs) {
if (alias_map) {
const AliasMapInnerNode *amin;
/* Must be present in the map, or s->bs would not be set */
amin = g_hash_table_lookup(alias_map, s->node_alias);
assert(amin != NULL);
bitmap_alias_map = amin->subtree;
}
} else if (!nothing && !s->cancelled) {
error_report("Error: block device name is not set");
cancel_incoming_locked(s);
}
assert(nothing || s->cancelled || !!alias_map == !!bitmap_alias_map);
if (s->flags & DIRTY_BITMAP_MIG_FLAG_BITMAP_NAME) {
if (!qemu_get_counted_string(f, s->bitmap_name)) {
error_report("Unable to read bitmap name string");
const char *bitmap_name;
if (!qemu_get_counted_string(f, s->bitmap_alias)) {
error_report("Unable to read bitmap alias string");
return -EINVAL;
}
if (!s->cancelled) {
if (bitmap_alias_map) {
bitmap_name = g_hash_table_lookup(bitmap_alias_map,
s->bitmap_alias);
if (!bitmap_name) {
error_report("Error: Unknown bitmap alias '%s' on node "
"'%s' (alias '%s')", s->bitmap_alias,
s->bs->node_name, s->node_alias);
cancel_incoming_locked(s);
}
} else {
bitmap_name = s->bitmap_alias;
}
}
if (!s->cancelled) {
g_strlcpy(s->bitmap_name, bitmap_name, sizeof(s->bitmap_name));
s->bitmap = bdrv_find_dirty_bitmap(s->bs, s->bitmap_name);
/*
@ -811,7 +1103,7 @@ static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s)
if (!s->bitmap && !(s->flags & DIRTY_BITMAP_MIG_FLAG_START)) {
error_report("Error: unknown dirty bitmap "
"'%s' for block device '%s'",
s->bitmap_name, s->node_name);
s->bitmap_name, s->bs->node_name);
cancel_incoming_locked(s);
}
}
@ -835,6 +1127,8 @@ static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s)
*/
static int dirty_bitmap_load(QEMUFile *f, void *opaque, int version_id)
{
GHashTable *alias_map = NULL;
const MigrationParameters *mig_params = &migrate_get_current()->parameters;
DBMLoadState *s = &((DBMState *)opaque)->load;
int ret = 0;
@ -846,13 +1140,18 @@ static int dirty_bitmap_load(QEMUFile *f, void *opaque, int version_id)
return -EINVAL;
}
if (mig_params->has_block_bitmap_mapping) {
alias_map = construct_alias_map(mig_params->block_bitmap_mapping,
false, &error_abort);
}
do {
QEMU_LOCK_GUARD(&s->lock);
ret = dirty_bitmap_load_header(f, s);
ret = dirty_bitmap_load_header(f, s, alias_map);
if (ret < 0) {
cancel_incoming_locked(s);
return ret;
goto fail;
}
if (s->flags & DIRTY_BITMAP_MIG_FLAG_START) {
@ -869,12 +1168,17 @@ static int dirty_bitmap_load(QEMUFile *f, void *opaque, int version_id)
if (ret) {
cancel_incoming_locked(s);
return ret;
goto fail;
}
} while (!(s->flags & DIRTY_BITMAP_MIG_FLAG_EOS));
trace_dirty_bitmap_load_success();
return 0;
ret = 0;
fail:
if (alias_map) {
g_hash_table_destroy(alias_map);
}
return ret;
}
static int dirty_bitmap_save_setup(QEMUFile *f, void *opaque)

View File

@ -36,6 +36,7 @@
#include "block/block.h"
#include "qapi/error.h"
#include "qapi/clone-visitor.h"
#include "qapi/qapi-visit-migration.h"
#include "qapi/qapi-visit-sockets.h"
#include "qapi/qapi-commands-migration.h"
#include "qapi/qapi-events-migration.h"
@ -843,6 +844,13 @@ MigrationParameters *qmp_query_migrate_parameters(Error **errp)
params->has_announce_step = true;
params->announce_step = s->parameters.announce_step;
if (s->parameters.has_block_bitmap_mapping) {
params->has_block_bitmap_mapping = true;
params->block_bitmap_mapping =
QAPI_CLONE(BitmapMigrationNodeAliasList,
s->parameters.block_bitmap_mapping);
}
return params;
}
@ -1308,6 +1316,13 @@ static bool migrate_params_check(MigrationParameters *params, Error **errp)
"is invalid, it must be in the range of 1 to 10000 ms");
return false;
}
if (params->has_block_bitmap_mapping &&
!check_dirty_bitmap_mig_alias_map(params->block_bitmap_mapping, errp)) {
error_prepend(errp, "Invalid mapping given for block-bitmap-mapping: ");
return false;
}
return true;
}
@ -1402,6 +1417,11 @@ static void migrate_params_test_apply(MigrateSetParameters *params,
if (params->has_announce_step) {
dest->announce_step = params->announce_step;
}
if (params->has_block_bitmap_mapping) {
dest->has_block_bitmap_mapping = true;
dest->block_bitmap_mapping = params->block_bitmap_mapping;
}
}
static void migrate_params_apply(MigrateSetParameters *params, Error **errp)
@ -1514,6 +1534,16 @@ static void migrate_params_apply(MigrateSetParameters *params, Error **errp)
if (params->has_announce_step) {
s->parameters.announce_step = params->announce_step;
}
if (params->has_block_bitmap_mapping) {
qapi_free_BitmapMigrationNodeAliasList(
s->parameters.block_bitmap_mapping);
s->parameters.has_block_bitmap_mapping = true;
s->parameters.block_bitmap_mapping =
QAPI_CLONE(BitmapMigrationNodeAliasList,
params->block_bitmap_mapping);
}
}
void qmp_migrate_set_parameters(MigrateSetParameters *params, Error **errp)

View File

@ -337,6 +337,9 @@ void migrate_send_rp_resume_ack(MigrationIncomingState *mis, uint32_t value);
void dirty_bitmap_mig_before_vm_start(void);
void dirty_bitmap_mig_cancel_outgoing(void);
void dirty_bitmap_mig_cancel_incoming(void);
bool check_dirty_bitmap_mig_alias_map(const BitmapMigrationNodeAliasList *bbm,
Error **errp);
void migrate_add_address(SocketAddress *address);
int foreach_not_ignored_block(RAMBlockIterFunc func, void *opaque);

View File

@ -469,6 +469,32 @@ void hmp_info_migrate_parameters(Monitor *mon, const QDict *qdict)
monitor_printf(mon, "%s: '%s'\n",
MigrationParameter_str(MIGRATION_PARAMETER_TLS_AUTHZ),
params->tls_authz);
if (params->has_block_bitmap_mapping) {
const BitmapMigrationNodeAliasList *bmnal;
monitor_printf(mon, "%s:\n",
MigrationParameter_str(
MIGRATION_PARAMETER_BLOCK_BITMAP_MAPPING));
for (bmnal = params->block_bitmap_mapping;
bmnal;
bmnal = bmnal->next)
{
const BitmapMigrationNodeAlias *bmna = bmnal->value;
const BitmapMigrationBitmapAliasList *bmbal;
monitor_printf(mon, " '%s' -> '%s'\n",
bmna->node_name, bmna->alias);
for (bmbal = bmna->bitmaps; bmbal; bmbal = bmbal->next) {
const BitmapMigrationBitmapAlias *bmba = bmbal->value;
monitor_printf(mon, " '%s' -> '%s'\n",
bmba->name, bmba->alias);
}
}
}
}
qapi_free_MigrationParameters(params);
@ -1384,6 +1410,10 @@ void hmp_migrate_set_parameter(Monitor *mon, const QDict *qdict)
p->has_announce_step = true;
visit_type_size(v, param, &p->announce_step, &err);
break;
case MIGRATION_PARAMETER_BLOCK_BITMAP_MAPPING:
error_setg(&err, "The block-bitmap-mapping parameter can only be set "
"through QMP");
break;
default:
assert(0);
}

View File

@ -508,6 +508,44 @@
'data': [ 'none', 'zlib',
{ 'name': 'zstd', 'if': 'defined(CONFIG_ZSTD)' } ] }
##
# @BitmapMigrationBitmapAlias:
#
# @name: The name of the bitmap.
#
# @alias: An alias name for migration (for example the bitmap name on
# the opposite site).
#
# Since: 5.2
##
{ 'struct': 'BitmapMigrationBitmapAlias',
'data': {
'name': 'str',
'alias': 'str'
} }
##
# @BitmapMigrationNodeAlias:
#
# Maps a block node name and the bitmaps it has to aliases for dirty
# bitmap migration.
#
# @node-name: A block node name.
#
# @alias: An alias block node name for migration (for example the
# node name on the opposite site).
#
# @bitmaps: Mappings for the bitmaps on this node.
#
# Since: 5.2
##
{ 'struct': 'BitmapMigrationNodeAlias',
'data': {
'node-name': 'str',
'alias': 'str',
'bitmaps': [ 'BitmapMigrationBitmapAlias' ]
} }
##
# @MigrationParameter:
#
@ -642,6 +680,25 @@
# will consume more CPU.
# Defaults to 1. (Since 5.0)
#
# @block-bitmap-mapping: Maps block nodes and bitmaps on them to
# aliases for the purpose of dirty bitmap migration. Such
# aliases may for example be the corresponding names on the
# opposite site.
# The mapping must be one-to-one, but not necessarily
# complete: On the source, unmapped bitmaps and all bitmaps
# on unmapped nodes will be ignored. On the destination,
# encountering an unmapped alias in the incoming migration
# stream will result in a report, and all further bitmap
# migration data will then be discarded.
# Note that the destination does not know about bitmaps it
# does not receive, so there is no limitation or requirement
# regarding the number of bitmaps received, or how they are
# named, or on which nodes they are placed.
# By default (when this parameter has never been set), bitmap
# names are mapped to themselves. Nodes are mapped to their
# block device name if there is one, and to their node name
# otherwise. (Since 5.2)
#
# Since: 2.4
##
{ 'enum': 'MigrationParameter',
@ -656,7 +713,8 @@
'multifd-channels',
'xbzrle-cache-size', 'max-postcopy-bandwidth',
'max-cpu-throttle', 'multifd-compression',
'multifd-zlib-level' ,'multifd-zstd-level' ] }
'multifd-zlib-level' ,'multifd-zstd-level',
'block-bitmap-mapping' ] }
##
# @MigrateSetParameters:
@ -782,6 +840,25 @@
# will consume more CPU.
# Defaults to 1. (Since 5.0)
#
# @block-bitmap-mapping: Maps block nodes and bitmaps on them to
# aliases for the purpose of dirty bitmap migration. Such
# aliases may for example be the corresponding names on the
# opposite site.
# The mapping must be one-to-one, but not necessarily
# complete: On the source, unmapped bitmaps and all bitmaps
# on unmapped nodes will be ignored. On the destination,
# encountering an unmapped alias in the incoming migration
# stream will result in a report, and all further bitmap
# migration data will then be discarded.
# Note that the destination does not know about bitmaps it
# does not receive, so there is no limitation or requirement
# regarding the number of bitmaps received, or how they are
# named, or on which nodes they are placed.
# By default (when this parameter has never been set), bitmap
# names are mapped to themselves. Nodes are mapped to their
# block device name if there is one, and to their node name
# otherwise. (Since 5.2)
#
# Since: 2.4
##
# TODO either fuse back into MigrationParameters, or make
@ -812,7 +889,8 @@
'*max-cpu-throttle': 'int',
'*multifd-compression': 'MultiFDCompression',
'*multifd-zlib-level': 'int',
'*multifd-zstd-level': 'int' } }
'*multifd-zstd-level': 'int',
'*block-bitmap-mapping': [ 'BitmapMigrationNodeAlias' ] } }
##
# @migrate-set-parameters:
@ -958,6 +1036,25 @@
# will consume more CPU.
# Defaults to 1. (Since 5.0)
#
# @block-bitmap-mapping: Maps block nodes and bitmaps on them to
# aliases for the purpose of dirty bitmap migration. Such
# aliases may for example be the corresponding names on the
# opposite site.
# The mapping must be one-to-one, but not necessarily
# complete: On the source, unmapped bitmaps and all bitmaps
# on unmapped nodes will be ignored. On the destination,
# encountering an unmapped alias in the incoming migration
# stream will result in a report, and all further bitmap
# migration data will then be discarded.
# Note that the destination does not know about bitmaps it
# does not receive, so there is no limitation or requirement
# regarding the number of bitmaps received, or how they are
# named, or on which nodes they are placed.
# By default (when this parameter has never been set), bitmap
# names are mapped to themselves. Nodes are mapped to their
# block device name if there is one, and to their node name
# otherwise. (Since 5.2)
#
# Since: 2.4
##
{ 'struct': 'MigrationParameters',
@ -986,7 +1083,8 @@
'*max-cpu-throttle': 'uint8',
'*multifd-compression': 'MultiFDCompression',
'*multifd-zlib-level': 'uint8',
'*multifd-zstd-level': 'uint8' } }
'*multifd-zstd-level': 'uint8',
'*block-bitmap-mapping': [ 'BitmapMigrationNodeAlias' ] } }
##
# @query-migrate-parameters:

593
tests/qemu-iotests/300 Executable file
View File

@ -0,0 +1,593 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Tests for dirty bitmaps migration with node aliases
#
# 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/>.
#
import os
import random
import re
from typing import Dict, List, Optional, Union
import iotests
import qemu
BlockBitmapMapping = List[Dict[str, Union[str, List[Dict[str, str]]]]]
assert iotests.sock_dir is not None
mig_sock = os.path.join(iotests.sock_dir, 'mig_sock')
class TestDirtyBitmapMigration(iotests.QMPTestCase):
src_node_name: str = ''
dst_node_name: str = ''
src_bmap_name: str = ''
dst_bmap_name: str = ''
def setUp(self) -> None:
self.vm_a = iotests.VM(path_suffix='-a')
self.vm_a.add_blockdev(f'node-name={self.src_node_name},'
'driver=null-co')
self.vm_a.launch()
self.vm_b = iotests.VM(path_suffix='-b')
self.vm_b.add_blockdev(f'node-name={self.dst_node_name},'
'driver=null-co')
self.vm_b.add_incoming(f'unix:{mig_sock}')
self.vm_b.launch()
result = self.vm_a.qmp('block-dirty-bitmap-add',
node=self.src_node_name,
name=self.src_bmap_name)
self.assert_qmp(result, 'return', {})
# Dirty some random megabytes
for _ in range(9):
mb_ofs = random.randrange(1024)
self.vm_a.hmp_qemu_io(self.src_node_name, f'discard {mb_ofs}M 1M')
result = self.vm_a.qmp('x-debug-block-dirty-bitmap-sha256',
node=self.src_node_name,
name=self.src_bmap_name)
self.bitmap_hash_reference = result['return']['sha256']
caps = [{'capability': name, 'state': True}
for name in ('dirty-bitmaps', 'events')]
for vm in (self.vm_a, self.vm_b):
result = vm.qmp('migrate-set-capabilities', capabilities=caps)
self.assert_qmp(result, 'return', {})
def tearDown(self) -> None:
self.vm_a.shutdown()
self.vm_b.shutdown()
try:
os.remove(mig_sock)
except OSError:
pass
def check_bitmap(self, bitmap_name_valid: bool) -> None:
result = self.vm_b.qmp('x-debug-block-dirty-bitmap-sha256',
node=self.dst_node_name,
name=self.dst_bmap_name)
if bitmap_name_valid:
self.assert_qmp(result, 'return/sha256',
self.bitmap_hash_reference)
else:
self.assert_qmp(result, 'error/desc',
f"Dirty bitmap '{self.dst_bmap_name}' not found")
def migrate(self, bitmap_name_valid: bool = True,
migration_success: bool = True) -> None:
result = self.vm_a.qmp('migrate', uri=f'unix:{mig_sock}')
self.assert_qmp(result, 'return', {})
with iotests.Timeout(5, 'Timeout waiting for migration to complete'):
self.assertEqual(self.vm_a.wait_migration('postmigrate'),
migration_success)
self.assertEqual(self.vm_b.wait_migration('running'),
migration_success)
if migration_success:
self.check_bitmap(bitmap_name_valid)
def verify_dest_error(self, msg: Optional[str]) -> None:
"""
Check whether the given error message is present in vm_b's log.
(vm_b is shut down to do so.)
If @msg is None, check that there has not been any error.
"""
self.vm_b.shutdown()
if msg is None:
self.assertNotIn('qemu-system-', self.vm_b.get_log())
else:
self.assertIn(msg, self.vm_b.get_log())
@staticmethod
def mapping(node_name: str, node_alias: str,
bitmap_name: str, bitmap_alias: str) -> BlockBitmapMapping:
return [{
'node-name': node_name,
'alias': node_alias,
'bitmaps': [{
'name': bitmap_name,
'alias': bitmap_alias
}]
}]
def set_mapping(self, vm: iotests.VM, mapping: BlockBitmapMapping,
error: Optional[str] = None) -> None:
"""
Invoke migrate-set-parameters on @vm to set the given @mapping.
Check for success if @error is None, or verify the error message
if it is not.
On success, verify that "info migrate_parameters" on HMP returns
our mapping. (Just to check its formatting code.)
"""
result = vm.qmp('migrate-set-parameters',
block_bitmap_mapping=mapping)
if error is None:
self.assert_qmp(result, 'return', {})
result = vm.qmp('human-monitor-command',
command_line='info migrate_parameters')
m = re.search(r'^block-bitmap-mapping:\r?(\n .*)*\n',
result['return'], flags=re.MULTILINE)
hmp_mapping = m.group(0).replace('\r', '') if m else None
self.assertEqual(hmp_mapping, self.to_hmp_mapping(mapping))
else:
self.assert_qmp(result, 'error/desc', error)
@staticmethod
def to_hmp_mapping(mapping: BlockBitmapMapping) -> str:
result = 'block-bitmap-mapping:\n'
for node in mapping:
result += f" '{node['node-name']}' -> '{node['alias']}'\n"
assert isinstance(node['bitmaps'], list)
for bitmap in node['bitmaps']:
result += f" '{bitmap['name']}' -> '{bitmap['alias']}'\n"
return result
class TestAliasMigration(TestDirtyBitmapMigration):
src_node_name = 'node0'
dst_node_name = 'node0'
src_bmap_name = 'bmap0'
dst_bmap_name = 'bmap0'
def test_migration_without_alias(self) -> None:
self.migrate(self.src_node_name == self.dst_node_name and
self.src_bmap_name == self.dst_bmap_name)
# Check for error message on the destination
if self.src_node_name != self.dst_node_name:
self.verify_dest_error(f"Cannot find "
f"device={self.src_node_name} nor "
f"node_name={self.src_node_name}")
else:
self.verify_dest_error(None)
def test_alias_on_src_migration(self) -> None:
mapping = self.mapping(self.src_node_name, self.dst_node_name,
self.src_bmap_name, self.dst_bmap_name)
self.set_mapping(self.vm_a, mapping)
self.migrate()
self.verify_dest_error(None)
def test_alias_on_dst_migration(self) -> None:
mapping = self.mapping(self.dst_node_name, self.src_node_name,
self.dst_bmap_name, self.src_bmap_name)
self.set_mapping(self.vm_b, mapping)
self.migrate()
self.verify_dest_error(None)
def test_alias_on_both_migration(self) -> None:
src_map = self.mapping(self.src_node_name, 'node-alias',
self.src_bmap_name, 'bmap-alias')
dst_map = self.mapping(self.dst_node_name, 'node-alias',
self.dst_bmap_name, 'bmap-alias')
self.set_mapping(self.vm_a, src_map)
self.set_mapping(self.vm_b, dst_map)
self.migrate()
self.verify_dest_error(None)
class TestNodeAliasMigration(TestAliasMigration):
src_node_name = 'node-src'
dst_node_name = 'node-dst'
class TestBitmapAliasMigration(TestAliasMigration):
src_bmap_name = 'bmap-src'
dst_bmap_name = 'bmap-dst'
class TestFullAliasMigration(TestAliasMigration):
src_node_name = 'node-src'
dst_node_name = 'node-dst'
src_bmap_name = 'bmap-src'
dst_bmap_name = 'bmap-dst'
class TestLongBitmapNames(TestAliasMigration):
# Giving long bitmap names is OK, as long as there is a short alias for
# migration
src_bmap_name = 'a' * 512
dst_bmap_name = 'b' * 512
# Skip all tests that do not use the intermediate alias
def test_migration_without_alias(self) -> None:
pass
def test_alias_on_src_migration(self) -> None:
pass
def test_alias_on_dst_migration(self) -> None:
pass
class TestBlockBitmapMappingErrors(TestDirtyBitmapMigration):
src_node_name = 'node0'
dst_node_name = 'node0'
src_bmap_name = 'bmap0'
dst_bmap_name = 'bmap0'
"""
Note that mapping nodes or bitmaps that do not exist is not an error.
"""
def test_non_injective_node_mapping(self) -> None:
mapping: BlockBitmapMapping = [
{
'node-name': 'node0',
'alias': 'common-alias',
'bitmaps': [{
'name': 'bmap0',
'alias': 'bmap-alias0'
}]
},
{
'node-name': 'node1',
'alias': 'common-alias',
'bitmaps': [{
'name': 'bmap1',
'alias': 'bmap-alias1'
}]
}
]
self.set_mapping(self.vm_a, mapping,
"Invalid mapping given for block-bitmap-mapping: "
"The node alias 'common-alias' is used twice")
def test_non_injective_bitmap_mapping(self) -> None:
mapping: BlockBitmapMapping = [{
'node-name': 'node0',
'alias': 'node-alias0',
'bitmaps': [
{
'name': 'bmap0',
'alias': 'common-alias'
},
{
'name': 'bmap1',
'alias': 'common-alias'
}
]
}]
self.set_mapping(self.vm_a, mapping,
"Invalid mapping given for block-bitmap-mapping: "
"The bitmap alias 'node-alias0'/'common-alias' is "
"used twice")
def test_ambiguous_node_mapping(self) -> None:
mapping: BlockBitmapMapping = [
{
'node-name': 'node0',
'alias': 'node-alias0',
'bitmaps': [{
'name': 'bmap0',
'alias': 'bmap-alias0'
}]
},
{
'node-name': 'node0',
'alias': 'node-alias1',
'bitmaps': [{
'name': 'bmap0',
'alias': 'bmap-alias0'
}]
}
]
self.set_mapping(self.vm_a, mapping,
"Invalid mapping given for block-bitmap-mapping: "
"The node name 'node0' is mapped twice")
def test_ambiguous_bitmap_mapping(self) -> None:
mapping: BlockBitmapMapping = [{
'node-name': 'node0',
'alias': 'node-alias0',
'bitmaps': [
{
'name': 'bmap0',
'alias': 'bmap-alias0'
},
{
'name': 'bmap0',
'alias': 'bmap-alias1'
}
]
}]
self.set_mapping(self.vm_a, mapping,
"Invalid mapping given for block-bitmap-mapping: "
"The bitmap 'node0'/'bmap0' is mapped twice")
def test_migratee_node_is_not_mapped_on_src(self) -> None:
self.set_mapping(self.vm_a, [])
# Should just ignore all bitmaps on unmapped nodes
self.migrate(False)
self.verify_dest_error(None)
def test_migratee_node_is_not_mapped_on_dst(self) -> None:
self.set_mapping(self.vm_b, [])
self.migrate(False)
self.verify_dest_error(f"Unknown node alias '{self.src_node_name}'")
def test_migratee_bitmap_is_not_mapped_on_src(self) -> None:
mapping: BlockBitmapMapping = [{
'node-name': self.src_node_name,
'alias': self.dst_node_name,
'bitmaps': []
}]
self.set_mapping(self.vm_a, mapping)
# Should just ignore all unmapped bitmaps
self.migrate(False)
self.verify_dest_error(None)
def test_migratee_bitmap_is_not_mapped_on_dst(self) -> None:
mapping: BlockBitmapMapping = [{
'node-name': self.dst_node_name,
'alias': self.src_node_name,
'bitmaps': []
}]
self.set_mapping(self.vm_b, mapping)
self.migrate(False)
self.verify_dest_error(f"Unknown bitmap alias "
f"'{self.src_bmap_name}' "
f"on node '{self.dst_node_name}' "
f"(alias '{self.src_node_name}')")
def test_unused_mapping_on_dst(self) -> None:
# Let the source not send any bitmaps
self.set_mapping(self.vm_a, [])
# Establish some mapping on the destination
self.set_mapping(self.vm_b, [])
# The fact that there is a mapping on B without any bitmaps
# being received should be fine, not fatal
self.migrate(False)
self.verify_dest_error(None)
def test_non_wellformed_node_alias(self) -> None:
alias = '123-foo'
mapping: BlockBitmapMapping = [{
'node-name': self.src_node_name,
'alias': alias,
'bitmaps': []
}]
self.set_mapping(self.vm_a, mapping,
f"Invalid mapping given for block-bitmap-mapping: "
f"The node alias '{alias}' is not well-formed")
def test_node_alias_too_long(self) -> None:
alias = 'a' * 256
mapping: BlockBitmapMapping = [{
'node-name': self.src_node_name,
'alias': alias,
'bitmaps': []
}]
self.set_mapping(self.vm_a, mapping,
f"Invalid mapping given for block-bitmap-mapping: "
f"The node alias '{alias}' is longer than 255 bytes")
def test_bitmap_alias_too_long(self) -> None:
alias = 'a' * 256
mapping = self.mapping(self.src_node_name, self.dst_node_name,
self.src_bmap_name, alias)
self.set_mapping(self.vm_a, mapping,
f"Invalid mapping given for block-bitmap-mapping: "
f"The bitmap alias '{alias}' is longer than 255 "
f"bytes")
def test_bitmap_name_too_long(self) -> None:
name = 'a' * 256
result = self.vm_a.qmp('block-dirty-bitmap-add',
node=self.src_node_name,
name=name)
self.assert_qmp(result, 'return', {})
self.migrate(False, False)
# Check for the error in the source's log
self.vm_a.shutdown()
self.assertIn(f"Cannot migrate bitmap '{name}' on node "
f"'{self.src_node_name}': Name is longer than 255 bytes",
self.vm_a.get_log())
# Expect abnormal shutdown of the destination VM because of
# the failed migration
try:
self.vm_b.shutdown()
except qemu.machine.AbnormalShutdown:
pass
def test_aliased_bitmap_name_too_long(self) -> None:
# Longer than the maximum for bitmap names
self.dst_bmap_name = 'a' * 1024
mapping = self.mapping(self.dst_node_name, self.src_node_name,
self.dst_bmap_name, self.src_bmap_name)
# We would have to create this bitmap during migration, and
# that would fail, because the name is too long. Better to
# catch it early.
self.set_mapping(self.vm_b, mapping,
f"Invalid mapping given for block-bitmap-mapping: "
f"The bitmap name '{self.dst_bmap_name}' is longer "
f"than 1023 bytes")
def test_node_name_too_long(self) -> None:
# Longer than the maximum for node names
self.dst_node_name = 'a' * 32
mapping = self.mapping(self.dst_node_name, self.src_node_name,
self.dst_bmap_name, self.src_bmap_name)
# During migration, this would appear simply as a node that
# cannot be found. Still better to catch impossible node
# names early (similar to test_non_wellformed_node_alias).
self.set_mapping(self.vm_b, mapping,
f"Invalid mapping given for block-bitmap-mapping: "
f"The node name '{self.dst_node_name}' is longer "
f"than 31 bytes")
class TestCrossAliasMigration(TestDirtyBitmapMigration):
"""
Swap aliases, both to see that qemu does not get confused, and
that we can migrate multiple things at once.
So we migrate this:
node-a.bmap-a -> node-b.bmap-b
node-a.bmap-b -> node-b.bmap-a
node-b.bmap-a -> node-a.bmap-b
node-b.bmap-b -> node-a.bmap-a
"""
src_node_name = 'node-a'
dst_node_name = 'node-b'
src_bmap_name = 'bmap-a'
dst_bmap_name = 'bmap-b'
def setUp(self) -> None:
TestDirtyBitmapMigration.setUp(self)
# Now create another block device and let both have two bitmaps each
result = self.vm_a.qmp('blockdev-add',
node_name='node-b', driver='null-co')
self.assert_qmp(result, 'return', {})
result = self.vm_b.qmp('blockdev-add',
node_name='node-a', driver='null-co')
self.assert_qmp(result, 'return', {})
bmaps_to_add = (('node-a', 'bmap-b'),
('node-b', 'bmap-a'),
('node-b', 'bmap-b'))
for (node, bmap) in bmaps_to_add:
result = self.vm_a.qmp('block-dirty-bitmap-add',
node=node, name=bmap)
self.assert_qmp(result, 'return', {})
@staticmethod
def cross_mapping() -> BlockBitmapMapping:
return [
{
'node-name': 'node-a',
'alias': 'node-b',
'bitmaps': [
{
'name': 'bmap-a',
'alias': 'bmap-b'
},
{
'name': 'bmap-b',
'alias': 'bmap-a'
}
]
},
{
'node-name': 'node-b',
'alias': 'node-a',
'bitmaps': [
{
'name': 'bmap-b',
'alias': 'bmap-a'
},
{
'name': 'bmap-a',
'alias': 'bmap-b'
}
]
}
]
def verify_dest_has_all_bitmaps(self) -> None:
bitmaps = self.vm_b.query_bitmaps()
# Extract and sort bitmap names
for node in bitmaps:
bitmaps[node] = sorted((bmap['name'] for bmap in bitmaps[node]))
self.assertEqual(bitmaps,
{'node-a': ['bmap-a', 'bmap-b'],
'node-b': ['bmap-a', 'bmap-b']})
def test_alias_on_src(self) -> None:
self.set_mapping(self.vm_a, self.cross_mapping())
# Checks that node-a.bmap-a was migrated to node-b.bmap-b, and
# that is enough
self.migrate()
self.verify_dest_has_all_bitmaps()
self.verify_dest_error(None)
def test_alias_on_dst(self) -> None:
self.set_mapping(self.vm_b, self.cross_mapping())
# Checks that node-a.bmap-a was migrated to node-b.bmap-b, and
# that is enough
self.migrate()
self.verify_dest_has_all_bitmaps()
self.verify_dest_error(None)
if __name__ == '__main__':
iotests.main(supported_protocols=['file'])

View File

@ -0,0 +1,5 @@
.....................................
----------------------------------------------------------------------
Ran 37 tests
OK

63
tests/qemu-iotests/303 Executable file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
#
# Test for dumping of qcow2 image metadata
#
# Copyright (c) 2020 Virtuozzo International GmbH
#
# 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/>.
#
import iotests
import subprocess
from iotests import qemu_img_create, qemu_io, file_path, log, filter_qemu_io
iotests.script_initialize(supported_fmts=['qcow2'])
disk = file_path('disk')
chunk = 1024 * 1024
def create_bitmap(bitmap_number, disabled):
granularity = 1 << (14 + bitmap_number)
bitmap_name = 'bitmap-' + str(bitmap_number)
args = ['bitmap', '--add', '-g', f'{granularity}', '-f', iotests.imgfmt,
disk, bitmap_name]
if disabled:
args.append('--disable')
iotests.qemu_img_pipe(*args)
def write_to_disk(offset, size):
write = f'write {offset} {size}'
log(qemu_io('-c', write, disk), filters=[filter_qemu_io])
def add_bitmap(num, begin, end, disabled):
log(f'Add bitmap {num}')
create_bitmap(num, disabled)
for i in range(begin, end):
write_to_disk((i) * chunk, chunk)
log('')
qemu_img_create('-f', iotests.imgfmt, disk, '10M')
add_bitmap(1, 0, 6, False)
add_bitmap(2, 6, 8, True)
dump = ['qcow2.py', disk, 'dump-header']
subprocess.run(dump)
# Dump the metadata in JSON format
dump.append('-j')
subprocess.run(dump)

158
tests/qemu-iotests/303.out Normal file
View File

@ -0,0 +1,158 @@
Add bitmap 1
wrote 1048576/1048576 bytes at offset 0
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
wrote 1048576/1048576 bytes at offset 1048576
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
wrote 1048576/1048576 bytes at offset 2097152
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
wrote 1048576/1048576 bytes at offset 3145728
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
wrote 1048576/1048576 bytes at offset 4194304
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
wrote 1048576/1048576 bytes at offset 5242880
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
Add bitmap 2
wrote 1048576/1048576 bytes at offset 6291456
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
wrote 1048576/1048576 bytes at offset 7340032
1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
magic 0x514649fb
version 3
backing_file_offset 0x0
backing_file_size 0x0
cluster_bits 16
size 10485760
crypt_method 0
l1_size 1
l1_table_offset 0x30000
refcount_table_offset 0x10000
refcount_table_clusters 1
nb_snapshots 0
snapshot_offset 0x0
incompatible_features []
compatible_features []
autoclear_features [0]
refcount_order 4
header_length 112
Header extension:
magic 0x6803f857 (Feature table)
length 336
data <binary>
Header extension:
magic 0x23852875 (Bitmaps)
length 24
nb_bitmaps 2
reserved32 0
bitmap_directory_size 0x40
bitmap_directory_offset 0x9d0000
Bitmap name bitmap-1
bitmap_table_offset 0x9b0000
bitmap_table_size 1
flags 0x2 (['auto'])
type 1
granularity_bits 15
name_size 8
extra_data_size 0
Bitmap table type size offset
0 serialized 65536 10092544
Bitmap name bitmap-2
bitmap_table_offset 0x9c0000
bitmap_table_size 1
flags 0x0 ([])
type 1
granularity_bits 16
name_size 8
extra_data_size 0
Bitmap table type size offset
0 all-zeroes 0 0
{
"magic": 1363560955,
"version": 3,
"backing_file_offset": 0,
"backing_file_size": 0,
"cluster_bits": 16,
"size": 10485760,
"crypt_method": 0,
"l1_size": 1,
"l1_table_offset": 196608,
"refcount_table_offset": 65536,
"refcount_table_clusters": 1,
"nb_snapshots": 0,
"snapshot_offset": 0,
"incompatible_features": 0,
"compatible_features": 0,
"autoclear_features": 1,
"refcount_order": 4,
"header_length": 112
}
[
{
"name": "Feature table",
"magic": 1745090647,
"length": 336,
"data_str": "<binary>"
},
{
"name": "Bitmaps",
"magic": 595929205,
"length": 24,
"data": {
"nb_bitmaps": 2,
"reserved32": 0,
"bitmap_directory_size": 64,
"bitmap_directory_offset": 10289152,
"bitmap_directory": [
{
"name": "bitmap-1",
"bitmap_table_offset": 10158080,
"bitmap_table_size": 1,
"flags": 2,
"type": 1,
"granularity_bits": 15,
"name_size": 8,
"extra_data_size": 0,
"bitmap_table": [
{
"type": "serialized",
"offset": 10092544,
"reserved": 0
}
]
},
{
"name": "bitmap-2",
"bitmap_table_offset": 10223616,
"bitmap_table_size": 1,
"flags": 0,
"type": 1,
"granularity_bits": 16,
"name_size": 8,
"extra_data_size": 0,
"bitmap_table": [
{
"type": "all-zeroes",
"offset": 0,
"reserved": 0
}
]
}
]
}
}
]

View File

@ -307,6 +307,8 @@
296 rw
297 meta
299 auto quick
300 migration
301 backing quick
302 quick
303 rw quick
304 rw quick

View File

@ -729,16 +729,22 @@ class VM(qtest.QEMUQtestMachine):
}
]))
def wait_migration(self, expect_runstate):
def wait_migration(self, expect_runstate: Optional[str]) -> bool:
while True:
event = self.event_wait('MIGRATION')
log(event, filters=[filter_qmp_event])
if event['data']['status'] == 'completed':
if event['data']['status'] in ('completed', 'failed'):
break
# The event may occur in finish-migrate, so wait for the expected
# post-migration runstate
while self.qmp('query-status')['return']['status'] != expect_runstate:
pass
if event['data']['status'] == 'completed':
# The event may occur in finish-migrate, so wait for the expected
# post-migration runstate
runstate = None
while runstate != expect_runstate:
runstate = self.qmp('query-status')['return']['status']
return True
else:
return False
def node_info(self, node_name):
nodes = self.qmp('query-named-block-nodes')

View File

@ -26,16 +26,19 @@ from qcow2_format import (
)
is_json = False
def cmd_dump_header(fd):
h = QcowHeader(fd)
h.dump()
h.dump(is_json)
print()
h.dump_extensions()
h.dump_extensions(is_json)
def cmd_dump_header_exts(fd):
h = QcowHeader(fd)
h.dump_extensions()
h.dump_extensions(is_json)
def cmd_set_header(fd, name, value):
@ -151,11 +154,14 @@ def main(filename, cmd, args):
def usage():
print("Usage: %s <file> <cmd> [<arg>, ...]" % sys.argv[0])
print("Usage: %s <file> <cmd> [<arg>, ...] [<key>, ...]" % sys.argv[0])
print("")
print("Supported commands:")
for name, handler, num_args, desc in cmds:
print(" %-20s - %s" % (name, desc))
print("")
print("Supported keys:")
print(" %-20s - %s" % ('-j', 'Dump in JSON format'))
if __name__ == '__main__':
@ -163,4 +169,8 @@ if __name__ == '__main__':
usage()
sys.exit(1)
is_json = '-j' in sys.argv
if is_json:
sys.argv.remove('-j')
main(sys.argv[1], sys.argv[2], sys.argv[3:])

View File

@ -19,6 +19,15 @@
import struct
import string
import json
class ComplexEncoder(json.JSONEncoder):
def default(self, obj):
if hasattr(obj, 'to_json'):
return obj.to_json()
else:
return json.JSONEncoder.default(self, obj)
class Qcow2Field:
@ -40,6 +49,22 @@ class Flags64(Qcow2Field):
return str(bits)
class BitmapFlags(Qcow2Field):
flags = {
0x1: 'in-use',
0x2: 'auto'
}
def __str__(self):
bits = []
for bit in range(64):
flag = self.value & (1 << bit)
if flag:
bits.append(self.flags.get(flag, f'bit-{bit}'))
return f'{self.value:#x} ({bits})'
class Enum(Qcow2Field):
def __str__(self):
@ -93,7 +118,11 @@ class Qcow2Struct(metaclass=Qcow2StructMeta):
self.__dict__ = dict((field[2], values[i])
for i, field in enumerate(self.fields))
def dump(self):
def dump(self, is_json=False):
if is_json:
print(json.dumps(self.to_json(), indent=4, cls=ComplexEncoder))
return
for f in self.fields:
value = self.__dict__[f[2]]
if isinstance(f[1], str):
@ -103,6 +132,9 @@ class Qcow2Struct(metaclass=Qcow2StructMeta):
print('{:<25} {}'.format(f[2], value_str))
def to_json(self):
return dict((f[2], self.__dict__[f[2]]) for f in self.fields)
class Qcow2BitmapExt(Qcow2Struct):
@ -113,6 +145,131 @@ class Qcow2BitmapExt(Qcow2Struct):
('u64', '{:#x}', 'bitmap_directory_offset')
)
def __init__(self, fd, cluster_size):
super().__init__(fd=fd)
tail = struct.calcsize(self.fmt) % 8
if tail:
fd.seek(8 - tail, 1)
position = fd.tell()
self.cluster_size = cluster_size
self.read_bitmap_directory(fd)
fd.seek(position)
def read_bitmap_directory(self, fd):
fd.seek(self.bitmap_directory_offset)
self.bitmap_directory = \
[Qcow2BitmapDirEntry(fd, cluster_size=self.cluster_size)
for _ in range(self.nb_bitmaps)]
def dump(self):
super().dump()
for entry in self.bitmap_directory:
print()
entry.dump()
def to_json(self):
fields_dict = super().to_json()
fields_dict['bitmap_directory'] = self.bitmap_directory
return fields_dict
class Qcow2BitmapDirEntry(Qcow2Struct):
fields = (
('u64', '{:#x}', 'bitmap_table_offset'),
('u32', '{}', 'bitmap_table_size'),
('u32', BitmapFlags, 'flags'),
('u8', '{}', 'type'),
('u8', '{}', 'granularity_bits'),
('u16', '{}', 'name_size'),
('u32', '{}', 'extra_data_size')
)
def __init__(self, fd, cluster_size):
super().__init__(fd=fd)
self.cluster_size = cluster_size
# Seek relative to the current position in the file
fd.seek(self.extra_data_size, 1)
bitmap_name = fd.read(self.name_size)
self.name = bitmap_name.decode('ascii')
# Move position to the end of the entry in the directory
entry_raw_size = self.bitmap_dir_entry_raw_size()
padding = ((entry_raw_size + 7) & ~7) - entry_raw_size
fd.seek(padding, 1)
self.bitmap_table = Qcow2BitmapTable(fd=fd,
offset=self.bitmap_table_offset,
nb_entries=self.bitmap_table_size,
cluster_size=self.cluster_size)
def bitmap_dir_entry_raw_size(self):
return struct.calcsize(self.fmt) + self.name_size + \
self.extra_data_size
def dump(self):
print(f'{"Bitmap name":<25} {self.name}')
super(Qcow2BitmapDirEntry, self).dump()
self.bitmap_table.dump()
def to_json(self):
# Put the name ahead of the dict
return {
'name': self.name,
**super().to_json(),
'bitmap_table': self.bitmap_table
}
class Qcow2BitmapTableEntry(Qcow2Struct):
fields = (
('u64', '{}', 'entry'),
)
BME_TABLE_ENTRY_RESERVED_MASK = 0xff000000000001fe
BME_TABLE_ENTRY_OFFSET_MASK = 0x00fffffffffffe00
BME_TABLE_ENTRY_FLAG_ALL_ONES = 1
def __init__(self, fd):
super().__init__(fd=fd)
self.reserved = self.entry & self.BME_TABLE_ENTRY_RESERVED_MASK
self.offset = self.entry & self.BME_TABLE_ENTRY_OFFSET_MASK
if self.offset:
if self.entry & self.BME_TABLE_ENTRY_FLAG_ALL_ONES:
self.type = 'invalid'
else:
self.type = 'serialized'
elif self.entry & self.BME_TABLE_ENTRY_FLAG_ALL_ONES:
self.type = 'all-ones'
else:
self.type = 'all-zeroes'
def to_json(self):
return {'type': self.type, 'offset': self.offset,
'reserved': self.reserved}
class Qcow2BitmapTable:
def __init__(self, fd, offset, nb_entries, cluster_size):
self.cluster_size = cluster_size
position = fd.tell()
fd.seek(offset)
self.entries = [Qcow2BitmapTableEntry(fd) for _ in range(nb_entries)]
fd.seek(position)
def dump(self):
bitmap_table = enumerate(self.entries)
print(f'{"Bitmap table":<14} {"type":<15} {"size":<12} {"offset"}')
for i, entry in bitmap_table:
if entry.type == 'serialized':
size = self.cluster_size
else:
size = 0
print(f'{i:<14} {entry.type:<15} {size:<12} {entry.offset}')
def to_json(self):
return self.entries
QCOW2_EXT_MAGIC_BITMAPS = 0x23852875
@ -128,6 +285,9 @@ class QcowHeaderExtension(Qcow2Struct):
0x44415441: 'Data file'
}
def to_json(self):
return self.mapping.get(self.value, "<unknown>")
fields = (
('u32', Magic, 'magic'),
('u32', '{}', 'length')
@ -135,11 +295,13 @@ class QcowHeaderExtension(Qcow2Struct):
# then padding to next multiply of 8
)
def __init__(self, magic=None, length=None, data=None, fd=None):
def __init__(self, magic=None, length=None, data=None, fd=None,
cluster_size=None):
"""
Support both loading from fd and creation from user data.
For fd-based creation current position in a file will be used to read
the data.
The cluster_size value may be obtained by dependent structures.
This should be somehow refactored and functionality should be moved to
superclass (to allow creation of any qcow2 struct), but then, fields
@ -161,28 +323,43 @@ class QcowHeaderExtension(Qcow2Struct):
else:
assert all(v is None for v in (magic, length, data))
super().__init__(fd=fd)
padded = (self.length + 7) & ~7
self.data = fd.read(padded)
assert self.data is not None
if self.magic == QCOW2_EXT_MAGIC_BITMAPS:
self.obj = Qcow2BitmapExt(fd=fd, cluster_size=cluster_size)
self.data = None
else:
padded = (self.length + 7) & ~7
self.data = fd.read(padded)
assert self.data is not None
self.obj = None
if self.data is not None:
data_str = self.data[:self.length]
if all(c in string.printable.encode(
'ascii') for c in data_str):
data_str = f"'{ data_str.decode('ascii') }'"
else:
data_str = '<binary>'
self.data_str = data_str
if self.magic == QCOW2_EXT_MAGIC_BITMAPS:
self.obj = Qcow2BitmapExt(data=self.data)
else:
self.obj = None
def dump(self):
super().dump()
if self.obj is None:
data = self.data[:self.length]
if all(c in string.printable.encode('ascii') for c in data):
data = f"'{ data.decode('ascii') }'"
else:
data = '<binary>'
print(f'{"data":<25} {data}')
print(f'{"data":<25} {self.data_str}')
else:
self.obj.dump()
def to_json(self):
# Put the name ahead of the dict
res = {'name': self.Magic(self.magic), **super().to_json()}
if self.obj is not None:
res['data'] = self.obj
else:
res['data_str'] = self.data_str
return res
@classmethod
def create(cls, magic, data):
return QcowHeaderExtension(magic, len(data), data)
@ -246,7 +423,7 @@ class QcowHeader(Qcow2Struct):
end = self.cluster_size
while fd.tell() < end:
ext = QcowHeaderExtension(fd=fd)
ext = QcowHeaderExtension(fd=fd, cluster_size=self.cluster_size)
if ext.magic == 0:
break
else:
@ -280,7 +457,11 @@ class QcowHeader(Qcow2Struct):
buf = buf[0:header_bytes-1]
fd.write(buf)
def dump_extensions(self):
def dump_extensions(self, is_json=False):
if is_json:
print(json.dumps(self.extensions, indent=4, cls=ComplexEncoder))
return
for ex in self.extensions:
print('Header extension:')
ex.dump()