[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[Qemu-block] [RFC PATCH 10/16] qbm: Implement format driver
From: |
Fam Zheng |
Subject: |
[Qemu-block] [RFC PATCH 10/16] qbm: Implement format driver |
Date: |
Tue, 26 Jan 2016 18:38:19 +0800 |
Signed-off-by: Fam Zheng <address@hidden>
---
block/Makefile.objs | 1 +
block/qbm.c | 1315 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 1316 insertions(+)
create mode 100644 block/qbm.c
diff --git a/block/Makefile.objs b/block/Makefile.objs
index cdd8655..1111ba7 100644
--- a/block/Makefile.objs
+++ b/block/Makefile.objs
@@ -5,6 +5,7 @@ block-obj-y += qed-check.o
block-obj-$(CONFIG_VHDX) += vhdx.o vhdx-endian.o vhdx-log.o
block-obj-y += quorum.o
block-obj-y += parallels.o blkdebug.o blkverify.o
+block-obj-y += qbm.o
block-obj-y += block-backend.o snapshot.o qapi.o
block-obj-$(CONFIG_WIN32) += raw-win32.o win32-aio.o
block-obj-$(CONFIG_POSIX) += raw-posix.o
diff --git a/block/qbm.c b/block/qbm.c
new file mode 100644
index 0000000..91e129f
--- /dev/null
+++ b/block/qbm.c
@@ -0,0 +1,1315 @@
+/*
+ * Block driver for the QBM format
+ *
+ * Copyright (c) 2016 Red Hat Inc.
+ *
+ * Authors:
+ * Fam Zheng <address@hidden>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "qemu-common.h"
+#include "block/block_int.h"
+#include "qapi/qmp/qerror.h"
+#include "qemu/error-report.h"
+#include "qemu/module.h"
+#include "migration/migration.h"
+#include "qapi/qmp/qint.h"
+#include "qapi/qmp/qjson.h"
+
+#define QBM_BUF_SIZE_MAX (32 << 20)
+
+typedef enum QBMBitmapType {
+ QBM_TYPE_DIRTY,
+ QBM_TYPE_ALLOC,
+} QBMBitmapType;
+
+typedef struct QBMBitmap {
+ BdrvDirtyBitmap *bitmap;
+ BdrvChild *file;
+ char *name;
+ QBMBitmapType type;
+} QBMBitmap;
+
+typedef struct BDRVQBMState {
+ BdrvChild *image;
+ BdrvDirtyBitmap *alloc_bitmap;
+ QDict *desc;
+ QDict *backing_dict;
+ QBMBitmap *bitmaps;
+ int num_bitmaps;
+} BDRVQBMState;
+
+static const char *qbm_token_consume(const char *p, const char *token)
+{
+ size_t len = strlen(token);
+
+ if (!p) {
+ return NULL;
+ }
+ while (*p && (*p == ' ' ||
+ *p == '\t' ||
+ *p == '\n' ||
+ *p == '\r')) {
+ p++;
+ }
+ if (strncmp(p, token, len)) {
+ return p + len;
+ }
+ return NULL;
+}
+
+static int qbm_probe(const uint8_t *buf, int buf_size, const char *filename)
+{
+ const char *p;
+ p = strstr((const char *)buf, "\"QBM\"");
+ if (!p) {
+ p = strstr((const char *)buf, "'QBM'");
+ }
+ if (!p) {
+ return 0;
+ }
+ p = qbm_token_consume(p, ":");
+ p = qbm_token_consume(p, "{");
+ if (p && *p) {
+ return 100;
+ }
+ return 0;
+}
+
+static void qbm_load_bitmap(BlockDriverState *bs, QBMBitmap *bm, Error **errp)
+{
+ int r;
+ BDRVQBMState *s = bs->opaque;
+ int64_t bitmap_file_size;
+ int64_t bitmap_size;
+ uint8_t *buf = NULL;
+ BlockDriverState *file = bm->file->bs;
+ int64_t image_size = bdrv_getlength(s->image->bs);
+
+ if (image_size < 0) {
+ error_setg(errp, "Cannot get image size: %s", s->image->bs->filename);
+ return;
+ }
+ bitmap_size = bdrv_dirty_bitmap_serialization_size(bm->bitmap, 0,
+ bdrv_dirty_bitmap_size(bm->bitmap));
+ if (bitmap_size > QBM_BUF_SIZE_MAX) {
+ error_setg(errp, "Bitmap too big");
+ return;
+ }
+ bitmap_file_size = bdrv_getlength(file);
+ if (bitmap_file_size < bitmap_size) {
+ error_setg(errp,
+ "Bitmap \"%s\" file too small "
+ "(expecting at least %ld bytes but got %ld bytes): %s",
+ bm->name, bitmap_size, bitmap_file_size, file->filename);
+ goto out;
+ }
+ buf = qemu_blockalign(file, bitmap_size);
+ r = bdrv_pread(file, 0, buf, bitmap_size);
+ if (r < 0) {
+ error_setg(errp, "Failed to read bitmap file \"%s\"",
+ file->filename);
+ goto out;
+ }
+ bdrv_dirty_bitmap_deserialize_part(bm->bitmap, buf, 0, bs->total_sectors,
+ true);
+
+out:
+ g_free(buf);
+}
+
+static int qbm_reopen_prepare(BDRVReopenState *state,
+ BlockReopenQueue *queue, Error **errp)
+{
+ return 0;
+}
+
+static void qbm_get_fullname(BlockDriverState *bs, char *dest, size_t sz,
+ const char *filename)
+{
+ const char *base, *p;
+
+ base = bs->exact_filename[0] ? bs->exact_filename : bs->filename;
+
+ if (strstart(base, "json:", NULL)) {
+ /* There is not much we can do with a json: file name, try bs->file and
+ * cross our fingers. */
+ if (bs->file) {
+ qbm_get_fullname(bs->file->bs, dest, sz, filename);
+ } else {
+ pstrcpy(dest, sz, filename);
+ }
+ return;
+ }
+
+ p = strrchr(base, '/');
+
+ assert(sz > 0);
+ if (path_has_protocol(filename) || path_is_absolute(filename)) {
+ pstrcpy(dest, sz, filename);
+ return;
+ }
+
+ if (p) {
+ pstrcpy(dest, MIN(sz, p - base + 2), base);
+ } else {
+ dest[0] = '\0';
+ }
+ pstrcat(dest, sz, filename);
+}
+
+static BdrvChild *qbm_open_image(BlockDriverState *bs,
+ QDict *image, QDict *options,
+ Error **errp)
+{
+ BdrvChild *child;
+ const char *filename = qdict_get_try_str(image, "file");
+ const char *fmt = qdict_get_try_str(image, "format");
+ const char *checksum = qdict_get_try_str(image, "checksum");
+ char fullname[PATH_MAX];
+
+ if (!filename) {
+ error_setg(errp, "Image missing 'file' field");
+ return NULL;
+ }
+ if (!fmt) {
+ error_setg(errp, "Image missing 'format' field");
+ return NULL;
+ }
+ qbm_get_fullname(bs, fullname, sizeof(fullname), filename);
+ qdict_put(options, "image.driver", qstring_from_str(fmt));
+ child = bdrv_open_child(fullname, options, "image", bs, &child_file, false,
+ errp);
+ if (!child) {
+ goto out;
+ }
+ if (checksum) {
+ /* TODO: compare checksum when we support this */
+ error_setg(errp, "Checksum not supported");
+ }
+out:
+ return child;
+}
+
+/* Open and load the persistent bitmap and return the created QBMBitmap object.
+ * If reuse_bitmap is not NULL, we skip bdrv_create_dirty_bitmap and reuse it.
+ **/
+static QBMBitmap *qbm_open_bitmap(BlockDriverState *bs,
+ const char *name,
+ const char *filename, int granularity,
+ QBMBitmapType type,
+ BdrvDirtyBitmap *reuse_bitmap,
+ Error **errp)
+{
+ BDRVQBMState *s = bs->opaque;
+ QBMBitmap *bm;
+ BdrvChild *file;
+ BdrvDirtyBitmap *bdrv_bitmap;
+ char *key;
+ QDict *options;
+ char fullname[PATH_MAX];
+ Error *local_err = NULL;
+
+ qbm_get_fullname(bs, fullname, sizeof(fullname), filename);
+ if (local_err) {
+ error_propagate(errp, local_err);
+ return NULL;
+ }
+ s->bitmaps = g_realloc_n(s->bitmaps, s->num_bitmaps + 1,
+ sizeof(QBMBitmap));
+
+ /* Create options for the bitmap child BDS */
+ options = qdict_new();
+ key = g_strdup_printf("bitmap-%s.driver", name);
+ qdict_put(options, key, qstring_from_str("raw"));
+ g_free(key);
+
+ /* Open the child as plain "file" */
+ key = g_strdup_printf("bitmap-%s", name);
+ file = bdrv_open_child(fullname, options, key, bs, &child_file, false,
+ errp);
+ g_free(key);
+ QDECREF(options);
+ if (!file) {
+ return NULL;
+ }
+
+ if (reuse_bitmap) {
+ bdrv_bitmap = reuse_bitmap;
+ } else {
+ bdrv_bitmap = bdrv_create_dirty_bitmap(bs, granularity, name, errp);
+ if (!bdrv_bitmap) {
+ bdrv_unref_child(bs, file);
+ return NULL;
+ }
+ bdrv_dirty_bitmap_set_persistent(bs, bdrv_bitmap, true, true, NULL);
+ }
+ bdrv_create_meta_dirty_bitmap(bdrv_bitmap, BDRV_SECTOR_SIZE);
+
+ bm = &s->bitmaps[s->num_bitmaps++];
+ bm->file = file;
+ bm->name = g_strdup(name);
+ bm->type = type;
+ bm->bitmap = bdrv_bitmap;
+ if (type == QBM_TYPE_ALLOC) {
+ assert(!s->alloc_bitmap);
+ s->alloc_bitmap = bdrv_bitmap;
+ /* Align the request to granularity so the block layer will take care
+ * of RMW for partial writes. */
+ bs->request_alignment = granularity;
+ }
+ return bm;
+}
+
+typedef struct QBMIterState {
+ QDict *options;
+ BlockDriverState *bs;
+ Error *err;
+ bool has_backing;
+} QBMIterState;
+
+static void qbm_bitmap_iter(const char *key, QObject *obj, void *opaque)
+{
+ QDict *dict;
+ const char *filename, *typename;
+ QBMBitmapType type;
+ int granularity;
+ QBMIterState *state = opaque;
+ BDRVQBMState *s = state->bs->opaque;
+ QBMBitmap *bm;
+
+ if (state->err) {
+ return;
+ }
+ dict = qobject_to_qdict(obj);
+ if (!dict) {
+ error_setg(&state->err, "'%s' is not a dicionary", key);
+ return;
+ }
+ filename = qdict_get_try_str(dict, "file");
+ if (!filename) {
+ error_setg(&state->err, "\"file\" is missing in bitmap \"%s\"", key);
+ return;
+ }
+ typename = qdict_get_try_str(dict, "type");
+ if (!typename) {
+ error_setg(&state->err, "\"value\" is missing in bitmap \"%s\"", key);
+ return;
+ } else if (!strcmp(typename, "dirty")) {
+ type = QBM_TYPE_DIRTY;
+ } else if (!strcmp(typename, "allocation")) {
+ QDict *backing_dict = qdict_get_qdict(dict, "backing");
+ type = QBM_TYPE_ALLOC;
+ if (backing_dict) {
+ if (state->has_backing) {
+ error_setg(&state->err, "Multiple backing is not supported");
+ return;
+ }
+ state->has_backing = true;
+ pstrcpy(state->bs->backing_file, PATH_MAX,
+ qdict_get_try_str(backing_dict, "file"));
+ if (qdict_haskey(backing_dict, "format")) {
+ pstrcpy(state->bs->backing_format,
+ sizeof(state->bs->backing_format),
+ qdict_get_try_str(backing_dict, "format"));
+ }
+ s->backing_dict = backing_dict;
+ if (!strlen(state->bs->backing_file)) {
+ error_setg(&state->err, "Backing file name not specified");
+ return;
+ }
+ }
+ } else {
+ error_setg(&state->err, "\"value\" is missing in bitmap \"%s\"", key);
+ return;
+ }
+ granularity = qdict_get_try_int(dict, "granularity-bytes", -1);
+ if (granularity == -1) {
+ error_setg(&state->err, "\"granularity\" is missing in bitmap \"%s\"",
+ key);
+ return;
+ } else if (granularity & (granularity - 1)) {
+ error_setg(&state->err, "\"granularity\" must be power of two");
+ return;
+ } else if (granularity < 512) {
+ error_setg(&state->err, "\"granularity\" too small");
+ return;
+ }
+
+ bm = qbm_open_bitmap(state->bs, key, filename, granularity,
+ type, NULL, &state->err);
+ if (!bm) {
+ return;
+ }
+ qbm_load_bitmap(state->bs, bm, &state->err);
+}
+
+static void qbm_release_bitmap(BlockDriverState *bs, QBMBitmap *bm)
+{
+ bdrv_release_meta_dirty_bitmap(bm->bitmap);
+ bdrv_release_dirty_bitmap(bs, bm->bitmap);
+ bdrv_unref_child(bs, bm->file);
+}
+
+static void qbm_release_bitmaps(BlockDriverState *bs)
+{
+ int i;
+ BDRVQBMState *s = bs->opaque;
+
+ for (i = 0; i < s->num_bitmaps; i++) {
+ QBMBitmap *bm = &s->bitmaps[i];
+ bdrv_flush(bm->file->bs);
+ qbm_release_bitmap(bs, bm);
+ g_free(bm->name);
+ }
+}
+
+static int qbm_open_bitmaps(BlockDriverState *bs, QDict *bitmaps,
+ QDict *options, Error **errp)
+{
+ QBMIterState state = (QBMIterState) {
+ .bs = bs,
+ .options = options,
+ };
+ qdict_iter(bitmaps, qbm_bitmap_iter, &state);
+ if (state.err) {
+ qbm_release_bitmaps(bs);
+ error_propagate(errp, state.err);
+ return -EINVAL;
+ }
+ return 0;
+}
+
+static int qbm_open(BlockDriverState *bs, QDict *options, int flags,
+ Error **errp)
+{
+ BDRVQBMState *s = bs->opaque;
+ int ret;
+ int64_t len;
+ char *desc;
+ QDict *dict, *image_dict, *bitmaps;
+
+ len = bdrv_getlength(bs->file->bs);
+ if (len > QBM_BUF_SIZE_MAX) {
+ error_setg(errp, "QBM description file too big.");
+ return -ENOMEM;
+ } else if (len < 0) {
+ error_setg(errp, "Failed to get descriptor file size");
+ return len;
+ } else if (!len) {
+ error_setg(errp, "Empty file");
+ return -EINVAL;
+ }
+
+ desc = qemu_blockalign(bs->file->bs, len);
+ ret = bdrv_pread(bs->file->bs, 0, desc, len);
+ if (ret < 0) {
+ goto out;
+ }
+ dict = qobject_to_qdict(qobject_from_json(desc));
+ if (!dict || !qdict_haskey(dict, "QBM")) {
+ error_setg(errp, "Failed to parse json from file");
+ ret = -EINVAL;
+ goto out;
+ }
+ s->desc = qdict_get_qdict(dict, "QBM");
+ if (!s->desc) {
+ error_setg(errp, "Json doesn't have key \"QBM\"");
+ ret = -EINVAL;
+ goto out;
+ }
+ if (qdict_get_try_int(s->desc, "version", -1) != 1) {
+ error_setg(errp, "Invalid version of json file");
+ ret = -EINVAL;
+ goto out;
+ }
+ if (!qdict_haskey(s->desc, "image")) {
+ error_setg(errp, "Key \"image\" not found in json file");
+ ret = -EINVAL;
+ goto out;
+ }
+ image_dict = qdict_get_qdict(s->desc, "image");
+ if (!image_dict) {
+ error_setg(errp, "\"image\" information invalid");
+ ret = -EINVAL;
+ goto out;
+ }
+
+ s->image = qbm_open_image(bs, image_dict, options, errp);
+ if (!s->image) {
+ ret = -EIO;
+ goto out;
+ }
+ bs->total_sectors = bdrv_nb_sectors(s->image->bs);
+ if (bs->total_sectors < 0) {
+ error_setg(errp, "Failed to get image size");
+ ret = -EINVAL;
+ goto out;
+ }
+
+ bitmaps = qdict_get_qdict(s->desc, "bitmaps");
+ if (!bitmaps) {
+ error_setg(errp, "\"bitmaps\" not found");
+ ret = -EINVAL;
+ goto out;
+ }
+
+ ret = qbm_open_bitmaps(bs, bitmaps, options, errp);
+
+out:
+ g_free(desc);
+ return ret;
+}
+
+
+static void qbm_refresh_limits(BlockDriverState *bs, Error **errp)
+{
+ BDRVQBMState *s = bs->opaque;
+ Error *local_err = NULL;
+
+ bdrv_refresh_limits(s->image->bs, &local_err);
+ if (local_err) {
+ error_propagate(errp, local_err);
+ return;
+ }
+ bs->bl.min_mem_alignment = s->image->bs->bl.min_mem_alignment;
+ bs->bl.opt_mem_alignment = s->image->bs->bl.opt_mem_alignment;
+ bs->bl.write_zeroes_alignment = bdrv_get_cluster_size(bs);
+}
+
+static int64_t coroutine_fn qbm_co_get_block_status(BlockDriverState *bs,
+ int64_t sector_num,
+ int nb_sectors,
+ int *pnum,
+ BlockDriverState **file)
+{
+ bool alloc = true;
+ int64_t next;
+ int cluster_sectors;
+ BDRVQBMState *s = bs->opaque;
+ int64_t ret = BDRV_BLOCK_OFFSET_VALID;
+
+ if (!s->alloc_bitmap) {
+ return bdrv_get_block_status(s->image->bs, sector_num, nb_sectors,
+ pnum, file);
+ }
+
+ ret |= BDRV_BLOCK_OFFSET_MASK & (sector_num << BDRV_SECTOR_BITS);
+ next = sector_num;
+ cluster_sectors = bdrv_dirty_bitmap_granularity(s->alloc_bitmap)
+ >> BDRV_SECTOR_BITS;
+ while (next < sector_num + nb_sectors) {
+ if (next == sector_num) {
+ alloc = bdrv_get_dirty(bs, s->alloc_bitmap, next);
+ } else if (bdrv_get_dirty(bs, s->alloc_bitmap, next) != alloc) {
+ break;
+ }
+ next += cluster_sectors - next % cluster_sectors;
+ }
+ *pnum = MIN(next - sector_num, nb_sectors);
+
+ ret |= alloc ? BDRV_BLOCK_DATA | BDRV_BLOCK_ALLOCATED : 0;
+ *file = alloc ? s->image->bs : NULL;
+
+ return ret;
+}
+
+static int qbm_save_desc(BlockDriverState *desc_file, QDict *desc)
+{
+ int ret;
+ const char *str;
+ size_t len;
+ QString *json_str = NULL;
+ QDict *td;
+
+ ret = bdrv_truncate(desc_file, 0);
+ if (ret) {
+ return ret;
+ }
+
+ td = qdict_new();
+ /* Grab an extra reference so it doesn't get freed with td */
+ QINCREF(desc);
+ qdict_put(td, "QBM", desc);
+
+ json_str = qobject_to_json_pretty(QOBJECT(td));
+ str = qstring_get_str(json_str);
+ len = strlen(str);
+ ret = bdrv_pwrite(desc_file, 0, str, len);
+ /* End the json file with a new line, doesn't hurt if it fails. */
+ bdrv_pwrite(desc_file, len, "\n", 1);
+ /* bdrv_pwrite write padding zeros to align to sector, we don't need that
+ * for a text file */
+ bdrv_truncate(desc_file, len + 1);
+ QDECREF(json_str);
+ QDECREF(td);
+ return ret == len ? 0 : -EIO;
+}
+
+static coroutine_fn int qbm_co_readv(BlockDriverState *bs, int64_t sector_num,
+ int nb_sectors, QEMUIOVector *qiov)
+{
+ QEMUIOVector local_qiov;
+ BDRVQBMState *s = bs->opaque;
+ BdrvDirtyBitmapIter *iter;
+ int done_sectors = 0;
+ int ret;
+ int64_t next_allocated;
+ int64_t cur_sector = sector_num;
+ int granularity_sectors;
+
+ if (!s->alloc_bitmap) {
+ return bdrv_co_readv(s->image->bs, sector_num, nb_sectors, qiov);
+ }
+ granularity_sectors = bdrv_dirty_bitmap_granularity(s->alloc_bitmap)
+ >> BDRV_SECTOR_BITS;
+ iter = bdrv_dirty_iter_new(s->alloc_bitmap, sector_num);
+ qemu_iovec_init(&local_qiov, qiov->niov);
+ do {
+ int64_t n;
+ int64_t consective_end;
+ next_allocated = bdrv_dirty_iter_next(iter);
+ if (next_allocated < 0) {
+ next_allocated = sector_num + nb_sectors;
+ } else {
+ next_allocated = MIN(next_allocated, sector_num + nb_sectors);
+ }
+ if (next_allocated > cur_sector) {
+ /* Read [cur_sector, next_allocated) from backing */
+ n = next_allocated - cur_sector;
+ qemu_iovec_reset(&local_qiov);
+ qemu_iovec_concat(&local_qiov, qiov,
+ done_sectors << BDRV_SECTOR_BITS,
+ n << BDRV_SECTOR_BITS);
+ ret = bdrv_co_readv(bs->backing->bs, cur_sector, n, &local_qiov);
+ if (ret) {
+ goto out;
+ }
+ done_sectors += n;
+ cur_sector += n;
+ if (done_sectors == nb_sectors) {
+ break;
+ }
+ }
+ consective_end = next_allocated;
+ /* Find consective allocated sectors */
+ while (consective_end < sector_num + nb_sectors) {
+ int64_t next = bdrv_dirty_iter_next(iter);
+ if (next < 0 || next - consective_end > granularity_sectors) {
+ /* No more consective sectors */
+ consective_end += granularity_sectors
+ - consective_end % granularity_sectors;
+ break;
+ }
+ consective_end = next;
+ }
+ consective_end = MIN(consective_end, sector_num + nb_sectors);
+ n = consective_end - cur_sector;
+ assert(n > 0);
+ /* Read [cur_sector, consective_end] from image */
+ qemu_iovec_reset(&local_qiov);
+ qemu_iovec_concat(&local_qiov, qiov,
+ done_sectors << BDRV_SECTOR_BITS,
+ n << BDRV_SECTOR_BITS);
+ ret = bdrv_co_readv(s->image->bs, cur_sector, n, &local_qiov);
+ if (ret) {
+ goto out;
+ }
+ done_sectors += n;
+ cur_sector += n;
+ } while (done_sectors < nb_sectors);
+out:
+ qemu_iovec_destroy(&local_qiov);
+ bdrv_dirty_iter_free(iter);
+ return ret;
+}
+
+static inline void qbm_check_alignment(BDRVQBMState *s, int64_t sector_num,
+ int nb_sectors)
+{
+ if (s->alloc_bitmap) {
+ int cluster_sectors = bdrv_dirty_bitmap_granularity(s->alloc_bitmap)
+ >> BDRV_SECTOR_BITS;
+ assert(sector_num % cluster_sectors == 0);
+ assert(nb_sectors % cluster_sectors == 0);
+ }
+}
+
+typedef struct {
+ int inflight;
+ Coroutine *co;
+ int ret;
+} QBMBitmapWriteTracker;
+
+typedef struct {
+ QEMUIOVector qiov;
+ uint8_t *buf;
+ QBMBitmapWriteTracker *tracker;
+ BlockDriverState *bs;
+ QBMBitmap *bitmap;
+ int64_t sector_num;
+ int nb_sectors;
+} QBMBitmapWriteData;
+
+static void qbm_write_bitmap_cb(void *opaque, int ret)
+{
+ QBMBitmapWriteData *data = opaque;
+ QBMBitmapWriteTracker *tracker = data->tracker;
+
+ qemu_iovec_destroy(&data->qiov);
+ qemu_vfree(data->buf);
+ if (!ret) {
+ bdrv_dirty_bitmap_reset_meta(data->bs,
+ data->bitmap->bitmap,
+ data->sector_num, data->nb_sectors);
+ }
+ g_free(data);
+ tracker->ret = tracker->ret ? : ret;
+ if (!--tracker->inflight) {
+ qemu_coroutine_enter(tracker->co, NULL);
+ }
+}
+
+static int qbm_write_bitmap(BlockDriverState *bs, QBMBitmap *bm,
+ int64_t sector_num, int nb_sectors,
+ QBMBitmapWriteTracker *tracker)
+{
+ QBMBitmapWriteData *data;
+ int64_t start, end;
+ int64_t file_sector_num;
+ int file_nb_sectors;
+ size_t buf_size;
+ /* Each bit in the bitmap tracks bdrv_dirty_bitmap_granularity(bm->bitmap)
+ * bytes of guest data, so each sector in the bitmap tracks
+ * (bdrv_dirty_bitmap_granularity(bm->bitmap) * BDRV_SECTOR_SIZE *
+ * BITS_PER_BYTE) bytes of guest data, so in sector unit is: */
+ int64_t sectors_per_bitmap_sector =
+ BITS_PER_BYTE * bdrv_dirty_bitmap_granularity(bm->bitmap);
+ int align = MAX(bdrv_dirty_bitmap_serialization_align(bm->bitmap),
+ sectors_per_bitmap_sector);
+
+ /* The start sector that is being marked dirty. */
+ start = QEMU_ALIGN_DOWN(sector_num, align);
+ /* The end sector that is being marked dirty. */
+ end = MIN(QEMU_ALIGN_UP(sector_num + nb_sectors, align),
+ bs->total_sectors);
+
+ if (!bdrv_dirty_bitmap_get_meta(bs, bm->bitmap, sector_num, nb_sectors)) {
+ return 0;
+ }
+
+ file_sector_num = start / sectors_per_bitmap_sector;
+ buf_size = bdrv_dirty_bitmap_serialization_size(bm->bitmap, start,
+ end - start);
+ buf_size = QEMU_ALIGN_UP(buf_size, BDRV_SECTOR_SIZE);
+ file_nb_sectors = buf_size >> BDRV_SECTOR_BITS;
+
+ data = g_new(QBMBitmapWriteData, 1);
+ data->buf = qemu_blockalign0(bm->file->bs, buf_size);
+ bdrv_dirty_bitmap_serialize_part(bm->bitmap, data->buf, start,
+ end - start);
+ qemu_iovec_init(&data->qiov, 1);
+ qemu_iovec_add(&data->qiov, data->buf, buf_size);
+ data->tracker = tracker;
+ data->sector_num = start;
+ data->nb_sectors = end - start;
+ data->bs = bm->file->bs;
+ data->bitmap = bm;
+ bdrv_aio_writev(bm->file->bs, file_sector_num, &data->qiov,
+ file_nb_sectors, qbm_write_bitmap_cb,
+ data);
+ return -EINPROGRESS;
+}
+
+static int qbm_write_bitmaps(BlockDriverState *bs, int64_t sector_num,
+ int nb_sectors)
+{
+ int i;
+ BDRVQBMState *s = bs->opaque;
+ QBMBitmapWriteTracker tracker = (QBMBitmapWriteTracker) {
+ .inflight = 1, /* So that no aio completion will call
+ qemu_coroutine_enter before we yield. */
+ .co = qemu_coroutine_self(),
+ };
+
+ for (i = 0; i < s->num_bitmaps; i++) {
+ int ret = qbm_write_bitmap(bs, &s->bitmaps[i],
+ sector_num, nb_sectors, &tracker);
+ if (ret == -EINPROGRESS) {
+ tracker.inflight++;
+ } else if (ret < 0) {
+ tracker.ret = ret;
+ break;
+ }
+ }
+ tracker.inflight--;
+ if (tracker.inflight) {
+ /* At least one aio in submitted, wait. */
+ qemu_coroutine_yield();
+ }
+ return tracker.ret;
+}
+
+static coroutine_fn int qbm_co_writev(BlockDriverState *bs, int64_t sector_num,
+ int nb_sectors, QEMUIOVector *qiov)
+{
+ int ret;
+ BDRVQBMState *s = bs->opaque;
+
+ qbm_check_alignment(s, sector_num, nb_sectors);
+ ret = bdrv_co_writev(s->image->bs, sector_num, nb_sectors, qiov);
+ if (ret) {
+ return ret;
+ }
+ return qbm_write_bitmaps(bs, sector_num, nb_sectors);
+}
+
+static int coroutine_fn qbm_co_write_zeroes(BlockDriverState *bs,
+ int64_t sector_num,
+ int nb_sectors,
+ BdrvRequestFlags flags)
+{
+ int ret;
+ BDRVQBMState *s = bs->opaque;
+
+ qbm_check_alignment(s, sector_num, nb_sectors);
+ ret = bdrv_co_write_zeroes(s->image->bs, sector_num, nb_sectors, flags);
+ if (ret) {
+ return ret;
+ }
+ return qbm_write_bitmaps(bs, sector_num, nb_sectors);
+}
+
+static coroutine_fn int qbm_co_discard(BlockDriverState *bs,
+ int64_t sector_num,
+ int nb_sectors)
+{
+ int ret;
+ BDRVQBMState *s = bs->opaque;
+
+ ret = bdrv_co_discard(s->image->bs, sector_num, nb_sectors);
+ if (ret) {
+ return ret;
+ }
+ return qbm_write_bitmaps(bs, sector_num, nb_sectors);
+}
+
+static int qbm_make_empty(BlockDriverState *bs)
+{
+ BDRVQBMState *s = bs->opaque;
+ BlockDriverState *image_bs = s->image->bs;
+ int ret = 0;
+
+ if (image_bs->drv->bdrv_make_empty) {
+ ret = image_bs->drv->bdrv_make_empty(s->image->bs);
+ if (ret) {
+ return ret;
+ }
+ } else if (!s->alloc_bitmap) {
+ return -ENOTSUP;
+ }
+ if (s->alloc_bitmap) {
+ int i;
+ bdrv_clear_dirty_bitmap(s->alloc_bitmap, NULL);
+ for (i = 0; i < s->num_bitmaps; i++) {
+ QBMBitmap *bm = &s->bitmaps[i];
+ if (bm->bitmap != s->alloc_bitmap) {
+ continue;
+ }
+ ret = bdrv_write_zeroes(bm->file->bs, 0,
+ DIV_ROUND_UP(bdrv_getlength(bm->file->bs),
+ BDRV_SECTOR_SIZE),
+ BDRV_REQ_MAY_UNMAP);
+ }
+ }
+ return ret;
+}
+
+/* Create a file with given size, and return the relative path. */
+static char *qbm_create_file(BlockDriverState *bs, const char *name,
+ const char *ext,
+ int64_t size, Error **errp)
+{
+ char *filename = NULL;
+ Error *local_err = NULL;
+ char fullname[PATH_MAX];
+ char path[PATH_MAX];
+ char prefix[PATH_MAX];
+ char postfix[PATH_MAX];
+
+ filename_decompose(bs->filename, path, prefix,
+ postfix, PATH_MAX, &local_err);
+ if (local_err) {
+ error_propagate(errp, local_err);
+ return NULL;
+ }
+ filename = g_strdup_printf("%s-%s.%s%s", prefix, name, ext, postfix);
+ qbm_get_fullname(bs, fullname, sizeof(fullname), filename);
+
+ bdrv_img_create(fullname, "raw", NULL, NULL, NULL, size, 0,
+ &local_err, true);
+ if (local_err) {
+ g_free(filename);
+ filename = NULL;
+ error_propagate(errp, local_err);
+ }
+ return filename;
+}
+
+static QDict *qbm_create_image_dict(BlockDriverState *bs,
+ const char *image_name,
+ const char *format,
+ Error **errp)
+{
+ QDict *dict;
+ char fullname[PATH_MAX];
+
+ qbm_get_fullname(bs, fullname, sizeof(fullname), image_name);
+ dict = qdict_new();
+ qdict_put(dict, "file", qstring_from_str(image_name));
+ qdict_put(dict, "format", qstring_from_str(format ? : ""));
+ /* TODO: Set checksum when we support it. */
+
+ return dict;
+}
+
+static inline QDict *qbm_make_bitmap_dict(const char *filename,
+ int granularity,
+ QBMBitmapType type)
+{
+ QDict *d = qdict_new();
+ qdict_put(d, "file", qstring_from_str(filename));
+ qdict_put(d, "granularity-bytes", qint_from_int(granularity));
+ switch (type) {
+ case QBM_TYPE_DIRTY:
+ qdict_put(d, "type", qstring_from_str("dirty"));
+ break;
+ case QBM_TYPE_ALLOC:
+ qdict_put(d, "type", qstring_from_str("allocation"));
+ break;
+ default:
+ abort();
+ }
+ return d;
+}
+
+static QDict *qbm_create_dirty_bitmaps(BlockDriverState *bs,
+ uint64_t image_size,
+ int granularity,
+ int n, Error **errp)
+{
+ int i;
+ QDict *dict = qdict_new();
+ int64_t bitmap_size = DIV_ROUND_UP(image_size, granularity *
BITS_PER_BYTE);
+
+ for (i = 0; i < n; i++) {
+ char *bitmap_filename;
+ char *key = g_strdup_printf("dirty.%d", i);
+
+ bitmap_filename = qbm_create_file(bs, key, "bitmap", bitmap_size,
+ errp);
+ if (!bitmap_filename) {
+ g_free(key);
+ QDECREF(dict);
+ dict = NULL;
+ goto out;
+ }
+ qdict_put(dict, key,
+ qbm_make_bitmap_dict(bitmap_filename, granularity,
+ QBM_TYPE_DIRTY));
+ g_free(key);
+ }
+out:
+ return dict;
+}
+
+static QDict *qbm_create_allocation(BlockDriverState *bs,
+ uint64_t image_size,
+ int granularity,
+ const char *backing_file,
+ const char *format,
+ Error **errp)
+{
+ char *bitmap_filename;
+ QDict *ret, *backing;
+ int64_t bitmap_size = DIV_ROUND_UP(image_size, granularity *
BITS_PER_BYTE);
+
+ bitmap_filename = qbm_create_file(bs, "allocation", "bitmap",
+ bitmap_size,
+ errp);
+ if (!bitmap_filename) {
+ return NULL;
+ }
+
+ ret = qdict_new();
+
+ qdict_put(ret, "file", qstring_from_str(bitmap_filename));
+ if (format) {
+ qdict_put(ret, "format", qstring_from_str(format));
+ }
+ qdict_put(ret, "type", qstring_from_str("allocation"));
+ qdict_put(ret, "granularity-bytes", qint_from_int(granularity));
+
+ backing = qbm_create_image_dict(bs, backing_file, format, errp);
+ if (!backing) {
+ QDECREF(ret);
+ ret = NULL;
+ goto out;
+ }
+ qdict_put(ret, "backing", backing);
+
+out:
+ g_free(bitmap_filename);
+ return ret;
+}
+
+static int qbm_create(const char *filename, QemuOpts *opts, Error **errp)
+{
+ char *backing_file;
+ const char *image_filename;
+ int granularity, dirty_bitmaps;
+ int64_t image_size;
+ int ret;
+ QDict *dict = NULL, *bitmaps, *image;
+ BlockDriverState *bs = NULL, *image_bs = NULL;
+ char fullname[PATH_MAX];
+
+ ret = bdrv_create_file(filename, NULL, errp);
+ if (ret) {
+ return ret;
+ }
+ ret = bdrv_open(&bs, filename, NULL, NULL,
+ BDRV_O_RDWR | BDRV_O_PROTOCOL, errp);
+ if (ret) {
+ return ret;
+ }
+
+ image_filename = qemu_opt_get_del(opts, "image");
+ if (!image_filename) {
+ /* Try to create one */
+ int64_t size = qemu_opt_get_size_del(opts, "size", -1);
+ if (size == -1) {
+ error_setg(errp, "Invalid size specified for data image");
+ ret = -EINVAL;
+ goto out;
+ }
+ image_filename = qbm_create_file(bs, "data", "img", size, errp);
+ if (!image_filename) {
+ ret = -EIO;
+ goto out;
+ }
+ }
+
+ granularity = qemu_opt_get_number(opts, "granularity", 65536);
+ dirty_bitmaps = qemu_opt_get_number(opts, "dirty-bitmaps", 0);
+
+ qbm_get_fullname(bs, fullname, sizeof(fullname), image_filename);
+ ret = bdrv_open(&image_bs, fullname, NULL, NULL, 0, errp);
+ if (ret) {
+ goto out;
+ }
+ image_size = bdrv_getlength(image_bs);
+
+ dict = qdict_new();
+ bitmaps = qbm_create_dirty_bitmaps(bs, image_size, granularity,
+ dirty_bitmaps, errp);
+ image = qbm_create_image_dict(bs, image_filename,
+ bdrv_get_format_name(image_bs), errp);
+ if (!image) {
+ goto out;
+ }
+
+ qdict_put(dict, "version", qint_from_int(1));
+ qdict_put(dict, "creator", qstring_from_str("QEMU"));
+ qdict_put(dict, "bitmaps", bitmaps);
+ qdict_put(dict, "image", image);
+
+ backing_file = qemu_opt_get_del(opts, BLOCK_OPT_BACKING_FILE);
+ if (backing_file) {
+ char *backing_fmt = qemu_opt_get_del(opts, BLOCK_OPT_BACKING_FMT);
+ QDict *alloc = qbm_create_allocation(bs, image_size,
+ granularity, backing_file,
+ backing_fmt, errp);
+ if (!alloc) {
+ ret = -EIO;
+ goto out;
+ }
+ /* Create "allocation" bitmap. */
+ qdict_put(bitmaps, "allocation", alloc);
+ g_free(backing_file);
+ backing_file = NULL;
+ g_free(backing_fmt);
+ }
+
+ ret = qbm_save_desc(bs, dict);
+
+out:
+ bdrv_unref(image_bs);
+ bdrv_unref(bs);
+ QDECREF(dict);
+ return ret;
+}
+
+static int64_t qbm_getlength(BlockDriverState *bs)
+{
+ BDRVQBMState *s = bs->opaque;
+ return bdrv_getlength(s->image->bs);
+}
+
+static void qbm_close(BlockDriverState *bs)
+{
+ BDRVQBMState *s = bs->opaque;
+
+ qbm_release_bitmaps(bs);
+ bdrv_unref(s->image->bs);
+ g_free(s->bitmaps);
+ QDECREF(s->desc);
+}
+
+static int qbm_truncate(BlockDriverState *bs, int64_t offset)
+{
+ BDRVQBMState *s = bs->opaque;
+ /* Truncate the image only, the bitmaps's sizes will be made correct when
+ * saving. */
+ return bdrv_truncate(s->image->bs, offset);
+}
+
+static coroutine_fn int qbm_co_flush(BlockDriverState *bs)
+{
+ int ret;
+ int i;
+ BDRVQBMState *s = bs->opaque;
+
+ ret = bdrv_flush(s->image->bs);
+ for (i = 0; ret >= 0 && i < s->num_bitmaps; i++) {
+ ret = bdrv_flush(s->bitmaps[i].file->bs);
+ }
+ return ret;
+}
+
+static int qbm_change_backing_file(BlockDriverState *bs,
+ const char *backing_file,
+ const char *backing_fmt)
+{
+ BDRVQBMState *s = bs->opaque;
+ if (!s->backing_dict) {
+ return -ENOTSUP;
+ }
+ if (backing_file) {
+ qdict_put(s->backing_dict, "file", qstring_from_str(backing_file));
+ qdict_put(s->backing_dict, "format",
+ qstring_from_str(backing_fmt ? : ""));
+ } else {
+ int i;
+ QDict *bitmaps = qdict_get_qdict(s->desc, "bitmaps");
+
+ assert(bitmaps);
+ if (!qdict_haskey(bitmaps, "allocation")) {
+ return 0;
+ }
+ qdict_del(bitmaps, "allocation");
+ for (i = 0; i < s->num_bitmaps; i++) {
+ if (s->bitmaps[i].type == QBM_TYPE_ALLOC) {
+ qbm_release_bitmap(bs, &s->bitmaps[i]);
+ s->bitmaps[i] = s->bitmaps[--s->num_bitmaps];
+ break;
+ }
+ }
+ s->alloc_bitmap = NULL;
+ s->backing_dict = NULL;
+ }
+ return qbm_save_desc(bs->file->bs, s->desc);
+}
+
+static int64_t qbm_get_allocated_file_size(BlockDriverState *bs)
+{
+ BDRVQBMState *s = bs->opaque;
+ /* Take the file sizes of descriptor and bitmap files into account? */
+ return bdrv_get_allocated_file_size(s->image->bs);
+}
+
+static int qbm_has_zero_init(BlockDriverState *bs)
+{
+ return 1;
+}
+
+static int qbm_get_info(BlockDriverState *bs, BlockDriverInfo *bdi)
+{
+ BDRVQBMState *s = bs->opaque;
+
+ bdi->unallocated_blocks_are_zero = true;
+ bdi->can_write_zeroes_with_unmap = true;
+ if (s->alloc_bitmap) {
+ bdi->cluster_size = bdrv_dirty_bitmap_granularity(s->alloc_bitmap);
+ } else {
+ bdi->cluster_size = bdrv_get_cluster_size(s->image->bs);
+ }
+ return 0;
+}
+
+static int qbm_check(BlockDriverState *bs, BdrvCheckResult *result,
+ BdrvCheckMode fix)
+{
+ /* TODO: checksum verification and bitmap size checks? */
+ return 0;
+}
+
+static void qbm_detach_aio_context(BlockDriverState *bs)
+{
+ int i;
+ BDRVQBMState *s = bs->opaque;
+
+ bdrv_detach_aio_context(s->image->bs);
+ for (i = 0; i < s->num_bitmaps; i++) {
+ bdrv_detach_aio_context(s->bitmaps[i].file->bs);
+ }
+}
+
+static void qbm_attach_aio_context(BlockDriverState *bs,
+ AioContext *new_context)
+{
+ int i;
+ BDRVQBMState *s = bs->opaque;
+
+ bdrv_attach_aio_context(s->image->bs, new_context);
+ for (i = 0; i < s->num_bitmaps; i++) {
+ bdrv_attach_aio_context(s->bitmaps[i].file->bs, new_context);
+ }
+}
+
+static int qbm_bitmap_set_persistent(BlockDriverState *bs,
+ BdrvDirtyBitmap *bitmap,
+ bool persistent, Error **errp)
+{
+ BDRVQBMState *s = bs->opaque;
+ int ret = 0;
+ QBMBitmap *bm;
+ char *filename;
+ const char *name = bdrv_dirty_bitmap_name(bitmap);
+ int granularity = bdrv_dirty_bitmap_granularity(bitmap);
+ QDict *bitmaps = qdict_get_qdict(s->desc, "bitmaps");
+
+ if (persistent) {
+ filename = qbm_create_file(bs, name, "bin",
+ bdrv_dirty_bitmap_size(bitmap), errp);
+ if (!filename) {
+ return -EIO;
+ }
+
+ bm = qbm_open_bitmap(bs, name, filename, granularity,
+ QBM_TYPE_DIRTY, bitmap, errp);
+ if (!bm) {
+ ret = -EIO;
+ }
+ qdict_put(bitmaps, name, qbm_make_bitmap_dict(filename, granularity,
+ QBM_TYPE_DIRTY));
+ g_free(filename);
+ } else {
+ if (!qdict_haskey(bitmaps, name)) {
+ error_setg(errp, "No persistent bitmap with name '%s'", name);
+ return -ENOENT;
+ }
+ qdict_del(bitmaps, name);
+ }
+ ret = qbm_save_desc(bs->file->bs, s->desc);
+ if (ret) {
+ error_setg(errp, "Failed to save json description to file");
+ }
+ return ret;
+}
+
+static QemuOptsList qbm_create_opts = {
+ .name = "qbm-create-opts",
+ .head = QTAILQ_HEAD_INITIALIZER(qbm_create_opts.head),
+ .desc = {
+ {
+ .name = BLOCK_OPT_SIZE,
+ .type = QEMU_OPT_SIZE,
+ .help = "Virtual disk size"
+ },
+ {
+ .name = "image",
+ .type = QEMU_OPT_STRING,
+ .help = "The file name of the referenced image, if not specified, "
+ "one will be created automatically",
+ },
+ {
+ .name = BLOCK_OPT_BACKING_FILE,
+ .type = QEMU_OPT_STRING,
+ .help = "File name of a base image"
+ },
+ {
+ .name = BLOCK_OPT_BACKING_FMT,
+ .type = QEMU_OPT_STRING,
+ .help = "Image format of the base image"
+ },
+ {
+ .name = "granularity",
+ .type = QEMU_OPT_NUMBER,
+ .help = "Bitmap granularity in bytes"
+ },
+ {
+ .name = "dirty-bitmaps",
+ .type = QEMU_OPT_NUMBER,
+ .help = "The number of dirty bitmaps to create"
+ },
+ { /* end of list */ }
+ }
+};
+
+static BlockDriver bdrv_qbm = {
+ .format_name = "qbm",
+ .protocol_name = "qbm",
+ .instance_size = sizeof(BDRVQBMState),
+ .bdrv_probe = qbm_probe,
+ .bdrv_open = qbm_open,
+ .bdrv_reopen_prepare = qbm_reopen_prepare,
+ .bdrv_co_readv = qbm_co_readv,
+ .bdrv_co_writev = qbm_co_writev,
+ .bdrv_co_write_zeroes = qbm_co_write_zeroes,
+ .bdrv_co_discard = qbm_co_discard,
+ .bdrv_make_empty = qbm_make_empty,
+ .bdrv_close = qbm_close,
+ .bdrv_getlength = qbm_getlength,
+ .bdrv_create = qbm_create,
+ .bdrv_co_flush_to_disk = qbm_co_flush,
+ .bdrv_truncate = qbm_truncate,
+ .bdrv_co_get_block_status = qbm_co_get_block_status,
+ .bdrv_get_allocated_file_size = qbm_get_allocated_file_size,
+ .bdrv_has_zero_init = qbm_has_zero_init,
+ .bdrv_refresh_limits = qbm_refresh_limits,
+ .bdrv_get_info = qbm_get_info,
+ .bdrv_check = qbm_check,
+ .bdrv_detach_aio_context = qbm_detach_aio_context,
+ .bdrv_attach_aio_context = qbm_attach_aio_context,
+ .bdrv_dirty_bitmap_set_persistent
+ = qbm_bitmap_set_persistent,
+ .bdrv_change_backing_file = qbm_change_backing_file,
+ .supports_backing = true,
+ .create_opts = &qbm_create_opts,
+};
+
+static void bdrv_qbm_init(void)
+{
+ bdrv_register(&bdrv_qbm);
+}
+
+block_init(bdrv_qbm_init);
--
2.4.3
- [Qemu-block] [RFC PATCH 03/16] block: Allow .bdrv_close callback to release dirty bitmaps, (continued)
- [Qemu-block] [RFC PATCH 03/16] block: Allow .bdrv_close callback to release dirty bitmaps, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 04/16] block: Move filename_decompose to block.c, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 05/16] block: Make bdrv_get_cluster_size public, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 06/16] block: Introduce bdrv_dirty_bitmap_set_persistent, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 07/16] block: Only swap non-persistent dirty bitmaps, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 08/16] qmp: Add optional parameter "persistent" in block-dirty-bitmap-add, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 09/16] qmp: Add block-dirty-bitmap-set-persistent, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 10/16] qbm: Implement format driver,
Fam Zheng <=
- [Qemu-block] [RFC PATCH 11/16] qapi: Add "qbm" as a generic cow format driver, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 12/16] iotests: Add qbm format to 041, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 13/16] iotests: Add qbm to case 097, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 14/16] iotests: Add qbm to applicable test cases, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 15/16] iotests: Add qbm specific test case 140, Fam Zheng, 2016/01/26
- [Qemu-block] [RFC PATCH 16/16] iotests: Add persistent bitmap test case 141, Fam Zheng, 2016/01/26