diff --git a/block/qcow2.c b/block/qcow2.c index fadf3422f8..6ad6bdc166 100644 --- a/block/qcow2.c +++ b/block/qcow2.c @@ -66,7 +66,7 @@ typedef struct { } QEMU_PACKED QCowExtension; #define QCOW2_EXT_MAGIC_END 0 -#define QCOW2_EXT_MAGIC_BACKING_FORMAT 0xE2792ACA +#define QCOW2_EXT_MAGIC_BACKING_FORMAT 0xe2792aca #define QCOW2_EXT_MAGIC_FEATURE_TABLE 0x6803f857 #define QCOW2_EXT_MAGIC_CRYPTO_HEADER 0x0537be77 #define QCOW2_EXT_MAGIC_BITMAPS 0x23852875 diff --git a/docs/interop/qcow2.txt b/docs/interop/qcow2.txt index cb723463f2..f072e27900 100644 --- a/docs/interop/qcow2.txt +++ b/docs/interop/qcow2.txt @@ -231,7 +231,7 @@ be stored. Each extension has a structure like the following: Byte 0 - 3: Header extension type: 0x00000000 - End of the header extension area - 0xE2792ACA - Backing file format name string + 0xe2792aca - Backing file format name string 0x6803f857 - Feature name table 0x23852875 - Bitmaps extension 0x0537be77 - Full disk encryption header pointer diff --git a/migration/block-dirty-bitmap.c b/migration/block-dirty-bitmap.c index b0dbf9eeed..784330ebe1 100644 --- a/migration/block-dirty-bitmap.c +++ b/migration/block-dirty-bitmap.c @@ -97,26 +97,28 @@ #define DIRTY_BITMAP_MIG_START_FLAG_ENABLED 0x01 #define DIRTY_BITMAP_MIG_START_FLAG_PERSISTENT 0x02 -/* 0x04 was "AUTOLOAD" flags on elder versions, no it is ignored */ +/* 0x04 was "AUTOLOAD" flags on older versions, now it is ignored */ #define DIRTY_BITMAP_MIG_START_FLAG_RESERVED_MASK 0xf8 -typedef struct DirtyBitmapMigBitmapState { +/* State of one bitmap during save process */ +typedef struct SaveBitmapState { /* Written during setup phase. */ BlockDriverState *bs; const char *node_name; BdrvDirtyBitmap *bitmap; uint64_t total_sectors; uint64_t sectors_per_chunk; - QSIMPLEQ_ENTRY(DirtyBitmapMigBitmapState) entry; + QSIMPLEQ_ENTRY(SaveBitmapState) entry; uint8_t flags; /* For bulk phase. */ bool bulk_completed; uint64_t cur_sector; -} DirtyBitmapMigBitmapState; +} SaveBitmapState; -typedef struct DirtyBitmapMigState { - QSIMPLEQ_HEAD(, DirtyBitmapMigBitmapState) dbms_list; +/* State of the dirty bitmap migration (DBM) during save process */ +typedef struct DBMSaveState { + QSIMPLEQ_HEAD(, SaveBitmapState) dbms_list; bool bulk_completed; bool no_bitmaps; @@ -124,30 +126,44 @@ typedef struct DirtyBitmapMigState { /* for send_bitmap_bits() */ BlockDriverState *prev_bs; BdrvDirtyBitmap *prev_bitmap; -} DirtyBitmapMigState; +} DBMSaveState; -typedef struct DirtyBitmapLoadState { +typedef struct LoadBitmapState { + BlockDriverState *bs; + BdrvDirtyBitmap *bitmap; + bool migrated; + bool enabled; +} 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]; BlockDriverState *bs; BdrvDirtyBitmap *bitmap; -} DirtyBitmapLoadState; -static DirtyBitmapMigState dirty_bitmap_mig_state; + bool before_vm_start_handled; /* set in dirty_bitmap_mig_before_vm_start */ -typedef struct DirtyBitmapLoadBitmapState { - BlockDriverState *bs; - BdrvDirtyBitmap *bitmap; - bool migrated; -} DirtyBitmapLoadBitmapState; -static GSList *enabled_bitmaps; -QemuMutex finish_lock; + /* + * cancelled + * Incoming migration is cancelled for some reason. That means that we + * still should read our chunks from migration stream, to not affect other + * migration objects (like RAM), but just ignore them and do not touch any + * bitmaps or nodes. + */ + bool cancelled; -void init_dirty_bitmap_incoming_migration(void) -{ - qemu_mutex_init(&finish_lock); -} + GSList *bitmaps; + QemuMutex lock; /* protect bitmaps */ +} DBMLoadState; + +typedef struct DBMState { + DBMSaveState save; + DBMLoadState load; +} DBMState; + +static DBMState dbm_state; static uint32_t qemu_get_bitmap_flags(QEMUFile *f) { @@ -164,27 +180,27 @@ static uint32_t qemu_get_bitmap_flags(QEMUFile *f) static void qemu_put_bitmap_flags(QEMUFile *f, uint32_t flags) { - /* The code currently do not send flags more than one byte */ + /* The code currently does not send flags as more than one byte */ assert(!(flags & (0xffffff00 | DIRTY_BITMAP_MIG_EXTRA_FLAGS))); qemu_put_byte(f, flags); } -static void send_bitmap_header(QEMUFile *f, DirtyBitmapMigBitmapState *dbms, - uint32_t additional_flags) +static void send_bitmap_header(QEMUFile *f, DBMSaveState *s, + SaveBitmapState *dbms, uint32_t additional_flags) { BlockDriverState *bs = dbms->bs; BdrvDirtyBitmap *bitmap = dbms->bitmap; uint32_t flags = additional_flags; trace_send_bitmap_header_enter(); - if (bs != dirty_bitmap_mig_state.prev_bs) { - dirty_bitmap_mig_state.prev_bs = bs; + if (bs != s->prev_bs) { + s->prev_bs = bs; flags |= DIRTY_BITMAP_MIG_FLAG_DEVICE_NAME; } - if (bitmap != dirty_bitmap_mig_state.prev_bitmap) { - dirty_bitmap_mig_state.prev_bitmap = bitmap; + if (bitmap != s->prev_bitmap) { + s->prev_bitmap = bitmap; flags |= DIRTY_BITMAP_MIG_FLAG_BITMAP_NAME; } @@ -199,19 +215,22 @@ static void send_bitmap_header(QEMUFile *f, DirtyBitmapMigBitmapState *dbms, } } -static void send_bitmap_start(QEMUFile *f, DirtyBitmapMigBitmapState *dbms) +static void send_bitmap_start(QEMUFile *f, DBMSaveState *s, + SaveBitmapState *dbms) { - send_bitmap_header(f, dbms, DIRTY_BITMAP_MIG_FLAG_START); + send_bitmap_header(f, s, dbms, DIRTY_BITMAP_MIG_FLAG_START); qemu_put_be32(f, bdrv_dirty_bitmap_granularity(dbms->bitmap)); qemu_put_byte(f, dbms->flags); } -static void send_bitmap_complete(QEMUFile *f, DirtyBitmapMigBitmapState *dbms) +static void send_bitmap_complete(QEMUFile *f, DBMSaveState *s, + SaveBitmapState *dbms) { - send_bitmap_header(f, dbms, DIRTY_BITMAP_MIG_FLAG_COMPLETE); + send_bitmap_header(f, s, dbms, DIRTY_BITMAP_MIG_FLAG_COMPLETE); } -static void send_bitmap_bits(QEMUFile *f, DirtyBitmapMigBitmapState *dbms, +static void send_bitmap_bits(QEMUFile *f, DBMSaveState *s, + SaveBitmapState *dbms, uint64_t start_sector, uint32_t nr_sectors) { /* align for buffer_is_zero() */ @@ -236,7 +255,7 @@ static void send_bitmap_bits(QEMUFile *f, DirtyBitmapMigBitmapState *dbms, trace_send_bitmap_bits(flags, start_sector, nr_sectors, buf_size); - send_bitmap_header(f, dbms, flags); + send_bitmap_header(f, s, dbms, flags); qemu_put_be64(f, start_sector); qemu_put_be32(f, nr_sectors); @@ -255,12 +274,12 @@ static void send_bitmap_bits(QEMUFile *f, DirtyBitmapMigBitmapState *dbms, } /* Called with iothread lock taken. */ -static void dirty_bitmap_mig_cleanup(void) +static void dirty_bitmap_do_save_cleanup(DBMSaveState *s) { - DirtyBitmapMigBitmapState *dbms; + SaveBitmapState *dbms; - while ((dbms = QSIMPLEQ_FIRST(&dirty_bitmap_mig_state.dbms_list)) != NULL) { - QSIMPLEQ_REMOVE_HEAD(&dirty_bitmap_mig_state.dbms_list, entry); + while ((dbms = QSIMPLEQ_FIRST(&s->dbms_list)) != NULL) { + QSIMPLEQ_REMOVE_HEAD(&s->dbms_list, entry); bdrv_dirty_bitmap_set_busy(dbms->bitmap, false); bdrv_unref(dbms->bs); g_free(dbms); @@ -268,10 +287,11 @@ static void dirty_bitmap_mig_cleanup(void) } /* Called with iothread lock taken. */ -static int add_bitmaps_to_list(BlockDriverState *bs, const char *bs_name) +static int add_bitmaps_to_list(DBMSaveState *s, BlockDriverState *bs, + const char *bs_name) { BdrvDirtyBitmap *bitmap; - DirtyBitmapMigBitmapState *dbms; + SaveBitmapState *dbms; Error *local_err = NULL; FOR_EACH_DIRTY_BITMAP(bs, bitmap) { @@ -309,7 +329,7 @@ static int add_bitmaps_to_list(BlockDriverState *bs, const char *bs_name) bdrv_ref(bs); bdrv_dirty_bitmap_set_busy(bitmap, true); - dbms = g_new0(DirtyBitmapMigBitmapState, 1); + dbms = g_new0(SaveBitmapState, 1); dbms->bs = bs; dbms->node_name = bs_name; dbms->bitmap = bitmap; @@ -323,25 +343,24 @@ static int add_bitmaps_to_list(BlockDriverState *bs, const char *bs_name) dbms->flags |= DIRTY_BITMAP_MIG_START_FLAG_PERSISTENT; } - QSIMPLEQ_INSERT_TAIL(&dirty_bitmap_mig_state.dbms_list, - dbms, entry); + QSIMPLEQ_INSERT_TAIL(&s->dbms_list, dbms, entry); } return 0; } /* Called with iothread lock taken. */ -static int init_dirty_bitmap_migration(void) +static int init_dirty_bitmap_migration(DBMSaveState *s) { BlockDriverState *bs; - DirtyBitmapMigBitmapState *dbms; + SaveBitmapState *dbms; GHashTable *handled_by_blk = g_hash_table_new(NULL, NULL); BlockBackend *blk; - dirty_bitmap_mig_state.bulk_completed = false; - dirty_bitmap_mig_state.prev_bs = NULL; - dirty_bitmap_mig_state.prev_bitmap = NULL; - dirty_bitmap_mig_state.no_bitmaps = false; + 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 @@ -370,7 +389,7 @@ static int init_dirty_bitmap_migration(void) } if (bs && bs->drv && !bs->drv->is_filter) { - if (add_bitmaps_to_list(bs, name)) { + if (add_bitmaps_to_list(s, bs, name)) { goto fail; } g_hash_table_add(handled_by_blk, bs); @@ -382,18 +401,18 @@ static int init_dirty_bitmap_migration(void) continue; } - if (add_bitmaps_to_list(bs, bdrv_get_node_name(bs))) { + if (add_bitmaps_to_list(s, bs, bdrv_get_node_name(bs))) { goto fail; } } /* unset migration flags here, to not roll back it */ - QSIMPLEQ_FOREACH(dbms, &dirty_bitmap_mig_state.dbms_list, entry) { + QSIMPLEQ_FOREACH(dbms, &s->dbms_list, entry) { bdrv_dirty_bitmap_skip_store(dbms->bitmap, true); } - if (QSIMPLEQ_EMPTY(&dirty_bitmap_mig_state.dbms_list)) { - dirty_bitmap_mig_state.no_bitmaps = true; + if (QSIMPLEQ_EMPTY(&s->dbms_list)) { + s->no_bitmaps = true; } g_hash_table_destroy(handled_by_blk); @@ -402,18 +421,19 @@ static int init_dirty_bitmap_migration(void) fail: g_hash_table_destroy(handled_by_blk); - dirty_bitmap_mig_cleanup(); + dirty_bitmap_do_save_cleanup(s); return -1; } /* Called with no lock taken. */ -static void bulk_phase_send_chunk(QEMUFile *f, DirtyBitmapMigBitmapState *dbms) +static void bulk_phase_send_chunk(QEMUFile *f, DBMSaveState *s, + SaveBitmapState *dbms) { uint32_t nr_sectors = MIN(dbms->total_sectors - dbms->cur_sector, dbms->sectors_per_chunk); - send_bitmap_bits(f, dbms, dbms->cur_sector, nr_sectors); + send_bitmap_bits(f, s, dbms, dbms->cur_sector, nr_sectors); dbms->cur_sector += nr_sectors; if (dbms->cur_sector >= dbms->total_sectors) { @@ -422,61 +442,66 @@ static void bulk_phase_send_chunk(QEMUFile *f, DirtyBitmapMigBitmapState *dbms) } /* Called with no lock taken. */ -static void bulk_phase(QEMUFile *f, bool limit) +static void bulk_phase(QEMUFile *f, DBMSaveState *s, bool limit) { - DirtyBitmapMigBitmapState *dbms; + SaveBitmapState *dbms; - QSIMPLEQ_FOREACH(dbms, &dirty_bitmap_mig_state.dbms_list, entry) { + QSIMPLEQ_FOREACH(dbms, &s->dbms_list, entry) { while (!dbms->bulk_completed) { - bulk_phase_send_chunk(f, dbms); + bulk_phase_send_chunk(f, s, dbms); if (limit && qemu_file_rate_limit(f)) { return; } } } - dirty_bitmap_mig_state.bulk_completed = true; + s->bulk_completed = true; } /* for SaveVMHandlers */ static void dirty_bitmap_save_cleanup(void *opaque) { - dirty_bitmap_mig_cleanup(); + DBMSaveState *s = &((DBMState *)opaque)->save; + + dirty_bitmap_do_save_cleanup(s); } static int dirty_bitmap_save_iterate(QEMUFile *f, void *opaque) { + DBMSaveState *s = &((DBMState *)opaque)->save; + trace_dirty_bitmap_save_iterate(migration_in_postcopy()); - if (migration_in_postcopy() && !dirty_bitmap_mig_state.bulk_completed) { - bulk_phase(f, true); + if (migration_in_postcopy() && !s->bulk_completed) { + bulk_phase(f, s, true); } qemu_put_bitmap_flags(f, DIRTY_BITMAP_MIG_FLAG_EOS); - return dirty_bitmap_mig_state.bulk_completed; + return s->bulk_completed; } /* Called with iothread lock taken. */ static int dirty_bitmap_save_complete(QEMUFile *f, void *opaque) { - DirtyBitmapMigBitmapState *dbms; + DBMSaveState *s = &((DBMState *)opaque)->save; + SaveBitmapState *dbms; trace_dirty_bitmap_save_complete_enter(); - if (!dirty_bitmap_mig_state.bulk_completed) { - bulk_phase(f, false); + if (!s->bulk_completed) { + bulk_phase(f, s, false); } - QSIMPLEQ_FOREACH(dbms, &dirty_bitmap_mig_state.dbms_list, entry) { - send_bitmap_complete(f, dbms); + QSIMPLEQ_FOREACH(dbms, &s->dbms_list, entry) { + send_bitmap_complete(f, s, dbms); } qemu_put_bitmap_flags(f, DIRTY_BITMAP_MIG_FLAG_EOS); trace_dirty_bitmap_save_complete_finish(); - dirty_bitmap_mig_cleanup(); + dirty_bitmap_save_cleanup(opaque); return 0; } @@ -486,12 +511,13 @@ static void dirty_bitmap_save_pending(QEMUFile *f, void *opaque, uint64_t *res_compatible, uint64_t *res_postcopy_only) { - DirtyBitmapMigBitmapState *dbms; + DBMSaveState *s = &((DBMState *)opaque)->save; + SaveBitmapState *dbms; uint64_t pending = 0; qemu_mutex_lock_iothread(); - QSIMPLEQ_FOREACH(dbms, &dirty_bitmap_mig_state.dbms_list, entry) { + QSIMPLEQ_FOREACH(dbms, &s->dbms_list, entry) { uint64_t gran = bdrv_dirty_bitmap_granularity(dbms->bitmap); uint64_t sectors = dbms->bulk_completed ? 0 : dbms->total_sectors - dbms->cur_sector; @@ -507,11 +533,16 @@ static void dirty_bitmap_save_pending(QEMUFile *f, void *opaque, } /* First occurrence of this bitmap. It should be created if doesn't exist */ -static int dirty_bitmap_load_start(QEMUFile *f, DirtyBitmapLoadState *s) +static int dirty_bitmap_load_start(QEMUFile *f, DBMLoadState *s) { Error *local_err = NULL; uint32_t granularity = qemu_get_be32(f); uint8_t flags = qemu_get_byte(f); + LoadBitmapState *b; + + if (s->cancelled) { + return 0; + } if (s->bitmap) { error_report("Bitmap with the same name ('%s') already exists on " @@ -538,90 +569,140 @@ static int dirty_bitmap_load_start(QEMUFile *f, DirtyBitmapLoadState *s) bdrv_disable_dirty_bitmap(s->bitmap); if (flags & DIRTY_BITMAP_MIG_START_FLAG_ENABLED) { - DirtyBitmapLoadBitmapState *b; - bdrv_dirty_bitmap_create_successor(s->bitmap, &local_err); if (local_err) { error_report_err(local_err); return -EINVAL; } - - b = g_new(DirtyBitmapLoadBitmapState, 1); - b->bs = s->bs; - b->bitmap = s->bitmap; - b->migrated = false; - enabled_bitmaps = g_slist_prepend(enabled_bitmaps, b); } + b = g_new(LoadBitmapState, 1); + b->bs = s->bs; + b->bitmap = s->bitmap; + b->migrated = false; + b->enabled = flags & DIRTY_BITMAP_MIG_START_FLAG_ENABLED; + + s->bitmaps = g_slist_prepend(s->bitmaps, b); + return 0; } -void dirty_bitmap_mig_before_vm_start(void) +/* + * before_vm_start_handle_item + * + * g_slist_foreach helper + * + * item is LoadBitmapState* + * opaque is DBMLoadState* + */ +static void before_vm_start_handle_item(void *item, void *opaque) { - GSList *item; - - qemu_mutex_lock(&finish_lock); - - for (item = enabled_bitmaps; item; item = g_slist_next(item)) { - DirtyBitmapLoadBitmapState *b = item->data; + DBMLoadState *s = opaque; + LoadBitmapState *b = item; + if (b->enabled) { if (b->migrated) { - bdrv_enable_dirty_bitmap_locked(b->bitmap); + bdrv_enable_dirty_bitmap(b->bitmap); } else { bdrv_dirty_bitmap_enable_successor(b->bitmap); } - - g_free(b); } - g_slist_free(enabled_bitmaps); - enabled_bitmaps = NULL; - - qemu_mutex_unlock(&finish_lock); + if (b->migrated) { + s->bitmaps = g_slist_remove(s->bitmaps, b); + g_free(b); + } } -static void dirty_bitmap_load_complete(QEMUFile *f, DirtyBitmapLoadState *s) +void dirty_bitmap_mig_before_vm_start(void) +{ + DBMLoadState *s = &dbm_state.load; + qemu_mutex_lock(&s->lock); + + assert(!s->before_vm_start_handled); + g_slist_foreach(s->bitmaps, before_vm_start_handle_item, s); + s->before_vm_start_handled = true; + + qemu_mutex_unlock(&s->lock); +} + +static void cancel_incoming_locked(DBMLoadState *s) +{ + GSList *item; + + if (s->cancelled) { + return; + } + + s->cancelled = true; + s->bs = NULL; + s->bitmap = NULL; + + /* Drop all unfinished bitmaps */ + for (item = s->bitmaps; item; item = g_slist_next(item)) { + LoadBitmapState *b = item->data; + + /* + * Bitmap must be unfinished, as finished bitmaps should already be + * removed from the list. + */ + assert(!s->before_vm_start_handled || !b->migrated); + if (bdrv_dirty_bitmap_has_successor(b->bitmap)) { + bdrv_reclaim_dirty_bitmap(b->bitmap, &error_abort); + } + bdrv_release_dirty_bitmap(b->bitmap); + } + + g_slist_free_full(s->bitmaps, g_free); + s->bitmaps = NULL; +} + +void dirty_bitmap_mig_cancel_outgoing(void) +{ + dirty_bitmap_do_save_cleanup(&dbm_state.save); +} + +void dirty_bitmap_mig_cancel_incoming(void) +{ + DBMLoadState *s = &dbm_state.load; + + qemu_mutex_lock(&s->lock); + + cancel_incoming_locked(s); + + qemu_mutex_unlock(&s->lock); +} + +static void dirty_bitmap_load_complete(QEMUFile *f, DBMLoadState *s) { GSList *item; trace_dirty_bitmap_load_complete(); + + if (s->cancelled) { + return; + } + bdrv_dirty_bitmap_deserialize_finish(s->bitmap); - qemu_mutex_lock(&finish_lock); + if (bdrv_dirty_bitmap_has_successor(s->bitmap)) { + bdrv_reclaim_dirty_bitmap(s->bitmap, &error_abort); + } - for (item = enabled_bitmaps; item; item = g_slist_next(item)) { - DirtyBitmapLoadBitmapState *b = item->data; + for (item = s->bitmaps; item; item = g_slist_next(item)) { + LoadBitmapState *b = item->data; if (b->bitmap == s->bitmap) { b->migrated = true; + if (s->before_vm_start_handled) { + s->bitmaps = g_slist_remove(s->bitmaps, b); + g_free(b); + } break; } } - - if (bdrv_dirty_bitmap_has_successor(s->bitmap)) { - bdrv_dirty_bitmap_lock(s->bitmap); - if (enabled_bitmaps == NULL) { - /* in postcopy */ - bdrv_reclaim_dirty_bitmap_locked(s->bitmap, &error_abort); - bdrv_enable_dirty_bitmap_locked(s->bitmap); - } else { - /* target not started, successor must be empty */ - int64_t count = bdrv_get_dirty_count(s->bitmap); - BdrvDirtyBitmap *ret = bdrv_reclaim_dirty_bitmap_locked(s->bitmap, - NULL); - /* bdrv_reclaim_dirty_bitmap can fail only on no successor (it - * must be) or on merge fail, but merge can't fail when second - * bitmap is empty - */ - assert(ret == s->bitmap && - count == bdrv_get_dirty_count(s->bitmap)); - } - bdrv_dirty_bitmap_unlock(s->bitmap); - } - - qemu_mutex_unlock(&finish_lock); } -static int dirty_bitmap_load_bits(QEMUFile *f, DirtyBitmapLoadState *s) +static int dirty_bitmap_load_bits(QEMUFile *f, DBMLoadState *s) { uint64_t first_byte = qemu_get_be64(f) << BDRV_SECTOR_BITS; uint64_t nr_bytes = (uint64_t)qemu_get_be32(f) << BDRV_SECTOR_BITS; @@ -630,15 +711,46 @@ static int dirty_bitmap_load_bits(QEMUFile *f, DirtyBitmapLoadState *s) if (s->flags & DIRTY_BITMAP_MIG_FLAG_ZEROES) { trace_dirty_bitmap_load_bits_zeroes(); - bdrv_dirty_bitmap_deserialize_zeroes(s->bitmap, first_byte, nr_bytes, - false); + if (!s->cancelled) { + bdrv_dirty_bitmap_deserialize_zeroes(s->bitmap, first_byte, + nr_bytes, false); + } } else { size_t ret; - uint8_t *buf; + g_autofree uint8_t *buf = NULL; uint64_t buf_size = qemu_get_be64(f); - uint64_t needed_size = - bdrv_dirty_bitmap_serialization_size(s->bitmap, - first_byte, nr_bytes); + uint64_t needed_size; + + /* + * The actual check for buf_size is done a bit later. We can't do it in + * cancelled mode as we don't have the bitmap to check the constraints + * (so, we allocate a buffer and read prior to the check). On the other + * hand, we shouldn't blindly g_malloc the number from the stream. + * Actually one chunk should not be larger than CHUNK_SIZE. Let's allow + * a bit larger (which means that bitmap migration will fail anyway and + * the whole migration will most probably fail soon due to broken + * stream). + */ + if (buf_size > 10 * CHUNK_SIZE) { + error_report("Bitmap migration stream buffer allocation request " + "is too large"); + return -EIO; + } + + buf = g_malloc(buf_size); + ret = qemu_get_buffer(f, buf, buf_size); + if (ret != buf_size) { + error_report("Failed to read bitmap bits"); + return -EIO; + } + + if (s->cancelled) { + return 0; + } + + needed_size = bdrv_dirty_bitmap_serialization_size(s->bitmap, + first_byte, + nr_bytes); if (needed_size > buf_size || buf_size > QEMU_ALIGN_UP(needed_size, 4 * sizeof(long)) @@ -647,26 +759,18 @@ static int dirty_bitmap_load_bits(QEMUFile *f, DirtyBitmapLoadState *s) error_report("Migrated bitmap granularity doesn't " "match the destination bitmap '%s' granularity", bdrv_dirty_bitmap_name(s->bitmap)); - return -EINVAL; - } - - buf = g_malloc(buf_size); - ret = qemu_get_buffer(f, buf, buf_size); - if (ret != buf_size) { - error_report("Failed to read bitmap bits"); - g_free(buf); - return -EIO; + cancel_incoming_locked(s); + return 0; } bdrv_dirty_bitmap_deserialize_part(s->bitmap, buf, first_byte, nr_bytes, false); - g_free(buf); } return 0; } -static int dirty_bitmap_load_header(QEMUFile *f, DirtyBitmapLoadState *s) +static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s) { Error *local_err = NULL; bool nothing; @@ -680,14 +784,16 @@ static int dirty_bitmap_load_header(QEMUFile *f, DirtyBitmapLoadState *s) error_report("Unable to read node name string"); return -EINVAL; } - s->bs = bdrv_lookup_bs(s->node_name, s->node_name, &local_err); - if (!s->bs) { - error_report_err(local_err); - return -EINVAL; + if (!s->cancelled) { + s->bs = bdrv_lookup_bs(s->node_name, s->node_name, &local_err); + if (!s->bs) { + error_report_err(local_err); + cancel_incoming_locked(s); + } } - } else if (!s->bs && !nothing) { + } else if (!s->bs && !nothing && !s->cancelled) { error_report("Error: block device name is not set"); - return -EINVAL; + cancel_incoming_locked(s); } if (s->flags & DIRTY_BITMAP_MIG_FLAG_BITMAP_NAME) { @@ -695,47 +801,66 @@ static int dirty_bitmap_load_header(QEMUFile *f, DirtyBitmapLoadState *s) error_report("Unable to read bitmap name string"); return -EINVAL; } - s->bitmap = bdrv_find_dirty_bitmap(s->bs, s->bitmap_name); + if (!s->cancelled) { + s->bitmap = bdrv_find_dirty_bitmap(s->bs, s->bitmap_name); - /* bitmap may be NULL here, it wouldn't be an error if it is the - * first occurrence of the bitmap */ - 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); - return -EINVAL; + /* + * bitmap may be NULL here, it wouldn't be an error if it is the + * first occurrence of the bitmap + */ + 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); + cancel_incoming_locked(s); + } } - } else if (!s->bitmap && !nothing) { + } else if (!s->bitmap && !nothing && !s->cancelled) { error_report("Error: block device name is not set"); - return -EINVAL; + cancel_incoming_locked(s); } return 0; } +/* + * dirty_bitmap_load + * + * Load sequence of dirty bitmap chunks. Return error only on fatal io stream + * violations. On other errors just cancel bitmaps incoming migration and return + * 0. + * + * Note, than when incoming bitmap migration is canceled, we still must read all + * our chunks (and just ignore them), to not affect other migration objects. + */ static int dirty_bitmap_load(QEMUFile *f, void *opaque, int version_id) { - static DirtyBitmapLoadState s; + DBMLoadState *s = &((DBMState *)opaque)->load; int ret = 0; trace_dirty_bitmap_load_enter(); if (version_id != 1) { + QEMU_LOCK_GUARD(&s->lock); + cancel_incoming_locked(s); return -EINVAL; } do { - ret = dirty_bitmap_load_header(f, &s); + QEMU_LOCK_GUARD(&s->lock); + + ret = dirty_bitmap_load_header(f, s); if (ret < 0) { + cancel_incoming_locked(s); return ret; } - if (s.flags & DIRTY_BITMAP_MIG_FLAG_START) { - ret = dirty_bitmap_load_start(f, &s); - } else if (s.flags & DIRTY_BITMAP_MIG_FLAG_COMPLETE) { - dirty_bitmap_load_complete(f, &s); - } else if (s.flags & DIRTY_BITMAP_MIG_FLAG_BITS) { - ret = dirty_bitmap_load_bits(f, &s); + if (s->flags & DIRTY_BITMAP_MIG_FLAG_START) { + ret = dirty_bitmap_load_start(f, s); + } else if (s->flags & DIRTY_BITMAP_MIG_FLAG_COMPLETE) { + dirty_bitmap_load_complete(f, s); + } else if (s->flags & DIRTY_BITMAP_MIG_FLAG_BITS) { + ret = dirty_bitmap_load_bits(f, s); } if (!ret) { @@ -743,9 +868,10 @@ static int dirty_bitmap_load(QEMUFile *f, void *opaque, int version_id) } if (ret) { + cancel_incoming_locked(s); return ret; } - } while (!(s.flags & DIRTY_BITMAP_MIG_FLAG_EOS)); + } while (!(s->flags & DIRTY_BITMAP_MIG_FLAG_EOS)); trace_dirty_bitmap_load_success(); return 0; @@ -753,13 +879,14 @@ static int dirty_bitmap_load(QEMUFile *f, void *opaque, int version_id) static int dirty_bitmap_save_setup(QEMUFile *f, void *opaque) { - DirtyBitmapMigBitmapState *dbms = NULL; - if (init_dirty_bitmap_migration() < 0) { + DBMSaveState *s = &((DBMState *)opaque)->save; + SaveBitmapState *dbms = NULL; + if (init_dirty_bitmap_migration(s) < 0) { return -1; } - QSIMPLEQ_FOREACH(dbms, &dirty_bitmap_mig_state.dbms_list, entry) { - send_bitmap_start(f, dbms); + QSIMPLEQ_FOREACH(dbms, &s->dbms_list, entry) { + send_bitmap_start(f, s, dbms); } qemu_put_bitmap_flags(f, DIRTY_BITMAP_MIG_FLAG_EOS); @@ -768,7 +895,9 @@ static int dirty_bitmap_save_setup(QEMUFile *f, void *opaque) static bool dirty_bitmap_is_active(void *opaque) { - return migrate_dirty_bitmaps() && !dirty_bitmap_mig_state.no_bitmaps; + DBMSaveState *s = &((DBMState *)opaque)->save; + + return migrate_dirty_bitmaps() && !s->no_bitmaps; } static bool dirty_bitmap_is_active_iterate(void *opaque) @@ -796,9 +925,10 @@ static SaveVMHandlers savevm_dirty_bitmap_handlers = { void dirty_bitmap_mig_init(void) { - QSIMPLEQ_INIT(&dirty_bitmap_mig_state.dbms_list); + QSIMPLEQ_INIT(&dbm_state.save.dbms_list); + qemu_mutex_init(&dbm_state.load.lock); register_savevm_live("dirty-bitmap", 0, 1, &savevm_dirty_bitmap_handlers, - &dirty_bitmap_mig_state); + &dbm_state); } diff --git a/migration/migration.c b/migration/migration.c index 2ed9923227..8fe36339db 100644 --- a/migration/migration.c +++ b/migration/migration.c @@ -165,8 +165,6 @@ void migration_object_init(void) qemu_sem_init(¤t_incoming->postcopy_pause_sem_dst, 0); qemu_sem_init(¤t_incoming->postcopy_pause_sem_fault, 0); - init_dirty_bitmap_incoming_migration(); - if (!migration_object_check(current_migration, &err)) { error_report_err(err); exit(1); @@ -190,6 +188,19 @@ void migration_shutdown(void) */ migrate_fd_cancel(current_migration); object_unref(OBJECT(current_migration)); + + /* + * Cancel outgoing migration of dirty bitmaps. It should + * at least unref used block nodes. + */ + dirty_bitmap_mig_cancel_outgoing(); + + /* + * Cancel incoming migration of dirty bitmaps. Dirty bitmaps + * are non-critical data, and their loss never considered as + * something serious. + */ + dirty_bitmap_mig_cancel_incoming(); } /* For outgoing */ diff --git a/migration/migration.h b/migration/migration.h index f617960522..6c6a931d0d 100644 --- a/migration/migration.h +++ b/migration/migration.h @@ -335,7 +335,8 @@ void migrate_send_rp_recv_bitmap(MigrationIncomingState *mis, void migrate_send_rp_resume_ack(MigrationIncomingState *mis, uint32_t value); void dirty_bitmap_mig_before_vm_start(void); -void init_dirty_bitmap_incoming_migration(void); +void dirty_bitmap_mig_cancel_outgoing(void); +void dirty_bitmap_mig_cancel_incoming(void); void migrate_add_address(SocketAddress *address); int foreach_not_ignored_block(RAMBlockIterFunc func, void *opaque); diff --git a/migration/savevm.c b/migration/savevm.c index 45c9dd9d8a..a843d202b5 100644 --- a/migration/savevm.c +++ b/migration/savevm.c @@ -1813,6 +1813,9 @@ static void *postcopy_ram_listen_thread(void *opaque) MigrationIncomingState *mis = migration_incoming_get_current(); QEMUFile *f = mis->from_src_file; int load_res; + MigrationState *migr = migrate_get_current(); + + object_ref(OBJECT(migr)); migrate_set_state(&mis->state, MIGRATION_STATUS_ACTIVE, MIGRATION_STATUS_POSTCOPY_ACTIVE); @@ -1839,11 +1842,24 @@ static void *postcopy_ram_listen_thread(void *opaque) trace_postcopy_ram_listen_thread_exit(); if (load_res < 0) { - error_report("%s: loadvm failed: %d", __func__, load_res); qemu_file_set_error(f, load_res); - migrate_set_state(&mis->state, MIGRATION_STATUS_POSTCOPY_ACTIVE, - MIGRATION_STATUS_FAILED); - } else { + dirty_bitmap_mig_cancel_incoming(); + if (postcopy_state_get() == POSTCOPY_INCOMING_RUNNING && + !migrate_postcopy_ram() && migrate_dirty_bitmaps()) + { + error_report("%s: loadvm failed during postcopy: %d. All states " + "are migrated except dirty bitmaps. Some dirty " + "bitmaps may be lost, and present migrated dirty " + "bitmaps are correctly migrated and valid.", + __func__, load_res); + load_res = 0; /* prevent further exit() */ + } else { + error_report("%s: loadvm failed: %d", __func__, load_res); + migrate_set_state(&mis->state, MIGRATION_STATUS_POSTCOPY_ACTIVE, + MIGRATION_STATUS_FAILED); + } + } + if (load_res >= 0) { /* * This looks good, but it's possible that the device loading in the * main thread hasn't finished yet, and so we might not be in 'RUN' @@ -1879,6 +1895,8 @@ static void *postcopy_ram_listen_thread(void *opaque) mis->have_listen_thread = false; postcopy_state_set(POSTCOPY_INCOMING_END); + object_unref(OBJECT(migr)); + return NULL; } @@ -2437,6 +2455,8 @@ static bool postcopy_pause_incoming(MigrationIncomingState *mis) { trace_postcopy_pause_incoming(); + assert(migrate_postcopy_ram()); + /* Clear the triggered bit to allow one recovery */ mis->postcopy_recover_triggered = false; @@ -2521,15 +2541,22 @@ out: if (ret < 0) { qemu_file_set_error(f, ret); + /* Cancel bitmaps incoming regardless of recovery */ + dirty_bitmap_mig_cancel_incoming(); + /* * If we are during an active postcopy, then we pause instead * of bail out to at least keep the VM's dirty data. Note * that POSTCOPY_INCOMING_LISTENING stage is still not enough, * during which we're still receiving device states and we * still haven't yet started the VM on destination. + * + * Only RAM postcopy supports recovery. Still, if RAM postcopy is + * enabled, canceled bitmaps postcopy will not affect RAM postcopy + * recovering. */ if (postcopy_state_get() == POSTCOPY_INCOMING_RUNNING && - postcopy_pause_incoming(mis)) { + migrate_postcopy_ram() && postcopy_pause_incoming(mis)) { /* Reset f to point to the newly created channel */ f = mis->from_src_file; goto retry; diff --git a/tests/qemu-iotests/199 b/tests/qemu-iotests/199 index 40774eed74..58fad872a1 100755 --- a/tests/qemu-iotests/199 +++ b/tests/qemu-iotests/199 @@ -20,17 +20,76 @@ import os import iotests -import time from iotests import qemu_img +debug = False + disk_a = os.path.join(iotests.test_dir, 'disk_a') disk_b = os.path.join(iotests.test_dir, 'disk_b') size = '256G' fifo = os.path.join(iotests.test_dir, 'mig_fifo') -class TestDirtyBitmapPostcopyMigration(iotests.QMPTestCase): +granularity = 512 +nb_bitmaps = 15 +GiB = 1024 * 1024 * 1024 + +discards1 = ( + (0, GiB), + (2 * GiB + 512 * 5, 512), + (3 * GiB + 512 * 5, 512), + (100 * GiB, GiB) +) + +discards2 = ( + (3 * GiB + 512 * 8, 512), + (4 * GiB + 512 * 8, 512), + (50 * GiB, GiB), + (100 * GiB + GiB // 2, GiB) +) + + +def apply_discards(vm, discards): + for d in discards: + vm.hmp_qemu_io('drive0', 'discard {} {}'.format(*d)) + + +def event_seconds(event): + return event['timestamp']['seconds'] + \ + event['timestamp']['microseconds'] / 1000000.0 + + +def event_dist(e1, e2): + return event_seconds(e2) - event_seconds(e1) + + +def check_bitmaps(vm, count): + result = vm.qmp('query-block') + + if count == 0: + assert 'dirty-bitmaps' not in result['return'][0] + else: + assert len(result['return'][0]['dirty-bitmaps']) == count + + +class TestDirtyBitmapPostcopyMigration(iotests.QMPTestCase): def tearDown(self): + if debug: + self.vm_a_events += self.vm_a.get_qmp_events() + self.vm_b_events += self.vm_b.get_qmp_events() + for e in self.vm_a_events: + e['vm'] = 'SRC' + for e in self.vm_b_events: + e['vm'] = 'DST' + events = (self.vm_a_events + self.vm_b_events) + events = [(e['timestamp']['seconds'], + e['timestamp']['microseconds'], + e['vm'], + e['event'], + e.get('data', '')) for e in events] + for e in sorted(events): + print('{}.{:06} {} {} {}'.format(*e)) + self.vm_a.shutdown() self.vm_b.shutdown() os.remove(disk_a) @@ -41,51 +100,66 @@ class TestDirtyBitmapPostcopyMigration(iotests.QMPTestCase): os.mkfifo(fifo) qemu_img('create', '-f', iotests.imgfmt, disk_a, size) qemu_img('create', '-f', iotests.imgfmt, disk_b, size) - self.vm_a = iotests.VM(path_suffix='a').add_drive(disk_a) - self.vm_b = iotests.VM(path_suffix='b').add_drive(disk_b) + self.vm_a = iotests.VM(path_suffix='a').add_drive(disk_a, + 'discard=unmap') + self.vm_b = iotests.VM(path_suffix='b').add_drive(disk_b, + 'discard=unmap') self.vm_b.add_incoming("exec: cat '" + fifo + "'") self.vm_a.launch() self.vm_b.launch() - def test_postcopy(self): - write_size = 0x40000000 - granularity = 512 - chunk = 4096 + # collect received events for debug + self.vm_a_events = [] + self.vm_b_events = [] - result = self.vm_a.qmp('block-dirty-bitmap-add', node='drive0', - name='bitmap', granularity=granularity) - self.assert_qmp(result, 'return', {}); - - s = 0 - while s < write_size: - self.vm_a.hmp_qemu_io('drive0', 'write %d %d' % (s, chunk)) - s += 0x10000 - s = 0x8000 - while s < write_size: - self.vm_a.hmp_qemu_io('drive0', 'write %d %d' % (s, chunk)) - s += 0x10000 + def start_postcopy(self): + """ Run migration until RESUME event on target. Return this event. """ + for i in range(nb_bitmaps): + result = self.vm_a.qmp('block-dirty-bitmap-add', node='drive0', + name='bitmap{}'.format(i), + granularity=granularity, + persistent=True) + self.assert_qmp(result, 'return', {}) result = self.vm_a.qmp('x-debug-block-dirty-bitmap-sha256', - node='drive0', name='bitmap') - sha256 = result['return']['sha256'] + node='drive0', name='bitmap0') + empty_sha256 = result['return']['sha256'] - result = self.vm_a.qmp('block-dirty-bitmap-clear', node='drive0', - name='bitmap') - self.assert_qmp(result, 'return', {}); - s = 0 - while s < write_size: - self.vm_a.hmp_qemu_io('drive0', 'write %d %d' % (s, chunk)) - s += 0x10000 + apply_discards(self.vm_a, discards1) - bitmaps_cap = {'capability': 'dirty-bitmaps', 'state': True} - events_cap = {'capability': 'events', 'state': True} + result = self.vm_a.qmp('x-debug-block-dirty-bitmap-sha256', + node='drive0', name='bitmap0') + self.discards1_sha256 = result['return']['sha256'] - result = self.vm_a.qmp('migrate-set-capabilities', - capabilities=[bitmaps_cap, events_cap]) + # Check, that updating the bitmap by discards works + assert self.discards1_sha256 != empty_sha256 + + # We want to calculate resulting sha256. Do it in bitmap0, so, disable + # other bitmaps + for i in range(1, nb_bitmaps): + result = self.vm_a.qmp('block-dirty-bitmap-disable', node='drive0', + name='bitmap{}'.format(i)) + self.assert_qmp(result, 'return', {}) + + apply_discards(self.vm_a, discards2) + + result = self.vm_a.qmp('x-debug-block-dirty-bitmap-sha256', + node='drive0', name='bitmap0') + self.all_discards_sha256 = result['return']['sha256'] + + # Now, enable some bitmaps, to be updated during migration + for i in range(2, nb_bitmaps, 2): + result = self.vm_a.qmp('block-dirty-bitmap-enable', node='drive0', + name='bitmap{}'.format(i)) + self.assert_qmp(result, 'return', {}) + + caps = [{'capability': 'dirty-bitmaps', 'state': True}, + {'capability': 'events', 'state': True}] + + result = self.vm_a.qmp('migrate-set-capabilities', capabilities=caps) self.assert_qmp(result, 'return', {}) - result = self.vm_b.qmp('migrate-set-capabilities', - capabilities=[bitmaps_cap]) + result = self.vm_b.qmp('migrate-set-capabilities', capabilities=caps) self.assert_qmp(result, 'return', {}) result = self.vm_a.qmp('migrate', uri='exec:cat>' + fifo) @@ -94,26 +168,94 @@ class TestDirtyBitmapPostcopyMigration(iotests.QMPTestCase): result = self.vm_a.qmp('migrate-start-postcopy') self.assert_qmp(result, 'return', {}) - while True: - event = self.vm_a.event_wait('MIGRATION') - if event['data']['status'] == 'completed': - break + event_resume = self.vm_b.event_wait('RESUME') + self.vm_b_events.append(event_resume) + return event_resume - s = 0x8000 - while s < write_size: - self.vm_b.hmp_qemu_io('drive0', 'write %d %d' % (s, chunk)) - s += 0x10000 + def test_postcopy_success(self): + event_resume = self.start_postcopy() - result = self.vm_b.qmp('query-block'); - while len(result['return'][0]['dirty-bitmaps']) > 1: - time.sleep(2) - result = self.vm_b.qmp('query-block'); + # enabled bitmaps should be updated + apply_discards(self.vm_b, discards2) - result = self.vm_b.qmp('x-debug-block-dirty-bitmap-sha256', - node='drive0', name='bitmap') + match = {'data': {'status': 'completed'}} + event_complete = self.vm_b.event_wait('MIGRATION', match=match) + self.vm_b_events.append(event_complete) + + # take queued event, should already been happened + event_stop = self.vm_a.event_wait('STOP') + self.vm_a_events.append(event_stop) + + downtime = event_dist(event_stop, event_resume) + postcopy_time = event_dist(event_resume, event_complete) + + assert downtime * 10 < postcopy_time + if debug: + print('downtime:', downtime) + print('postcopy_time:', postcopy_time) + + # check that there are no bitmaps stored on source + self.vm_a_events += self.vm_a.get_qmp_events() + self.vm_a.shutdown() + self.vm_a.launch() + check_bitmaps(self.vm_a, 0) + + # check that bitmaps are migrated and persistence works + check_bitmaps(self.vm_b, nb_bitmaps) + self.vm_b.shutdown() + # recreate vm_b, so there is no incoming option, which prevents + # loading bitmaps from disk + self.vm_b = iotests.VM(path_suffix='b').add_drive(disk_b) + self.vm_b.launch() + check_bitmaps(self.vm_b, nb_bitmaps) + + # Check content of migrated bitmaps. Still, don't waste time checking + # every bitmap + for i in range(0, nb_bitmaps, 5): + result = self.vm_b.qmp('x-debug-block-dirty-bitmap-sha256', + node='drive0', name='bitmap{}'.format(i)) + sha = self.discards1_sha256 if i % 2 else self.all_discards_sha256 + self.assert_qmp(result, 'return/sha256', sha) + + def test_early_shutdown_destination(self): + self.start_postcopy() + + self.vm_b_events += self.vm_b.get_qmp_events() + self.vm_b.shutdown() + # recreate vm_b, so there is no incoming option, which prevents + # loading bitmaps from disk + self.vm_b = iotests.VM(path_suffix='b').add_drive(disk_b) + self.vm_b.launch() + check_bitmaps(self.vm_b, 0) + + # Bitmaps will be lost if we just shutdown the vm, as they are marked + # to skip storing to disk when prepared for migration. And that's + # correct, as actual data may be modified in target vm, so we play + # safe. + # Still, this mark would be taken away if we do 'cont', and bitmaps + # become persistent again. (see iotest 169 for such behavior case) + result = self.vm_a.qmp('query-status') + assert not result['return']['running'] + self.vm_a_events += self.vm_a.get_qmp_events() + self.vm_a.shutdown() + self.vm_a.launch() + check_bitmaps(self.vm_a, 0) + + def test_early_kill_source(self): + self.start_postcopy() + + self.vm_a_events = self.vm_a.get_qmp_events() + self.vm_a.kill() + + self.vm_a.launch() + + match = {'data': {'status': 'completed'}} + e_complete = self.vm_b.event_wait('MIGRATION', match=match) + self.vm_b_events.append(e_complete) + + check_bitmaps(self.vm_a, 0) + check_bitmaps(self.vm_b, 0) - self.assert_qmp(result, 'return/sha256', sha256); if __name__ == '__main__': - iotests.main(supported_fmts=['qcow2'], supported_cache_modes=['none'], - supported_protocols=['file']) + iotests.main(supported_fmts=['qcow2']) diff --git a/tests/qemu-iotests/199.out b/tests/qemu-iotests/199.out index ae1213e6f8..8d7e996700 100644 --- a/tests/qemu-iotests/199.out +++ b/tests/qemu-iotests/199.out @@ -1,5 +1,5 @@ -. +... ---------------------------------------------------------------------- -Ran 1 tests +Ran 3 tests OK diff --git a/tests/qemu-iotests/group b/tests/qemu-iotests/group index 1d0252e1f0..806044642c 100644 --- a/tests/qemu-iotests/group +++ b/tests/qemu-iotests/group @@ -112,7 +112,7 @@ 088 rw quick 089 rw auto quick 090 rw auto quick -091 rw migration +091 rw migration quick 092 rw quick 093 throttle 094 rw quick @@ -186,7 +186,7 @@ 162 quick 163 rw 165 rw quick -169 rw quick migration +169 rw migration 170 rw auto quick 171 rw quick 172 auto @@ -197,9 +197,9 @@ 177 rw auto quick 178 img 179 rw auto quick -181 rw auto migration +181 rw auto migration quick 182 rw quick -183 rw migration +183 rw migration quick 184 rw auto quick 185 rw 186 rw auto @@ -216,9 +216,9 @@ 198 rw 199 rw migration 200 rw -201 rw migration +201 rw migration quick 202 rw quick -203 rw auto migration +203 rw auto migration quick 204 rw quick 205 rw quick 206 rw