Add a new option to qemu-img measure to specify the base node you want
to merge the image into.
This will open all the images between top and base, and calculate the
size required for merging this layer into the base image.

Also modify the calculation so it takes into account the
discard-no-unref setting of the base image.
If discard-no-unref is enabled on the base, discarded blocks in the
layers above the base will not free clusters in the base image, but will
only mark them ZERO.

[1]: https://gitlab.com/qemu-project/qemu/-/issues/2369

Signed-off-by: Jean-Louis Dupond <[email protected]>
---
 block/qcow2.c                    | 90 ++++++++++++++++++++++++++++----
 qemu-img-cmds.hx                 |  4 +-
 qemu-img.c                       | 37 +++++++++++--
 tests/qemu-iotests/178.out.qcow2 |  6 +--
 tests/qemu-iotests/178.out.raw   |  6 +--
 5 files changed, 121 insertions(+), 22 deletions(-)

diff --git a/block/qcow2.c b/block/qcow2.c
index 81fd299b4c..3e3578e831 100644
--- a/block/qcow2.c
+++ b/block/qcow2.c
@@ -5231,6 +5231,8 @@ static BlockMeasureInfo *qcow2_measure(QemuOpts *opts, 
BlockDriverState *in_bs,
     bool has_backing_file;
     bool has_luks;
     bool extended_l2;
+    BlockDriverState *base_bs = NULL;
+    bool base_discard_no_unref = false;
     size_t l2e_size;
 
     /* Parse image creation options */
@@ -5311,6 +5313,21 @@ static BlockMeasureInfo *qcow2_measure(QemuOpts *opts, 
BlockDriverState *in_bs,
             goto err;
         }
 
+        base_bs = bdrv_find_base(in_bs);
+        /*
+         * bdrv_find_base() returns in_bs itself when the image has no backing
+         * chain. Then this can stay NULL.
+         */
+        if (base_bs == in_bs) {
+            base_bs = NULL;
+        }
+        if (base_bs && base_bs->drv &&
+            !strcmp(base_bs->drv->format_name, "qcow2")) {
+            BDRVQcow2State *base_s = base_bs->opaque;
+
+            base_discard_no_unref = base_s->discard_no_unref;
+        }
+
         virtual_size = ROUND_UP(ssize, cluster_size);
 
         if (has_backing_file) {
@@ -5326,8 +5343,10 @@ static BlockMeasureInfo *qcow2_measure(QemuOpts *opts, 
BlockDriverState *in_bs,
 
             for (offset = 0; offset < ssize; offset += pnum) {
                 int ret;
+                int retp = 0;
+                bool count = false;
 
-                ret = bdrv_block_status_above(in_bs, NULL, offset,
+                ret = bdrv_block_status_above(in_bs, base_bs, offset,
                                               ssize - offset, &pnum, NULL,
                                               NULL);
                 if (ret < 0) {
@@ -5336,15 +5355,68 @@ static BlockMeasureInfo *qcow2_measure(QemuOpts *opts, 
BlockDriverState *in_bs,
                     goto err;
                 }
 
-                if (ret & BDRV_BLOCK_ZERO) {
+                if (ret & BDRV_BLOCK_ZERO && !base_bs &&
+                    !base_discard_no_unref) {
                     /* Skip zero regions (safe with no backing file) */
-                } else if ((ret & (BDRV_BLOCK_DATA | BDRV_BLOCK_ALLOCATED)) ==
-                           (BDRV_BLOCK_DATA | BDRV_BLOCK_ALLOCATED)) {
-                    /* Extend pnum to end of cluster for next iteration */
-                    pnum = ROUND_UP(offset + pnum, cluster_size) - offset;
-
-                    /* Count clusters we've seen */
-                    required += offset % cluster_size + pnum;
+                } else {
+                    /*
+                     * If there is a base image in the chain, query its
+                     * allocation status for this region so we can decide
+                     * whether the cluster survives a commit into the base.
+                     */
+                    if (base_bs) {
+                        int64_t pnum_base = 0;
+                        retp = bdrv_block_status_above(base_bs, NULL, offset,
+                                            ssize - offset, &pnum_base, NULL,
+                                            NULL);
+                        if (retp < 0) {
+                            error_setg_errno(&local_err, -retp,
+                                    "Unable to get block status of the base");
+                            goto err;
+                        }
+                        /*
+                         * If the base contiguous block is smaller,
+                         * use that pnum, so the next iteration starts with
+                         * the smallest offset.
+                         */
+                        if (pnum_base > 0 && pnum_base < pnum) {
+                            pnum = pnum_base;
+                        }
+                    }
+
+                    if ((ret & (BDRV_BLOCK_DATA | BDRV_BLOCK_ALLOCATED)) ==
+                        (BDRV_BLOCK_DATA | BDRV_BLOCK_ALLOCATED)) {
+                        /* The overlay has its own data here; it is written. */
+                        count = true;
+                    } else if (base_discard_no_unref) {
+                        /*
+                         * With discard-no-unref enabled on the base, clusters
+                         * allocated in the base keep their reference when the
+                         * overlay is committed (discarded clusters are only
+                         * marked zero), so count any cluster that has a valid
+                         * offset in the base.
+                         */
+                        count = retp & BDRV_BLOCK_OFFSET_VALID;
+                    } else {
+                        /*
+                         * Without discard-no-unref, committing the overlay
+                         * frees the base clusters that the overlay discards
+                         * (the overlay marks them zero). Every other cluster
+                         * that is allocated in the base is retained, so count
+                         * it unless the overlay zeroes it out.
+                         */
+                        count = (retp & BDRV_BLOCK_ALLOCATED) &&
+                                !((ret & BDRV_BLOCK_ALLOCATED) &&
+                                  (ret & BDRV_BLOCK_ZERO));
+                    }
+
+                    if (count) {
+                        /* Extend pnum to end of cluster for next iteration */
+                        pnum = ROUND_UP(offset + pnum, cluster_size) - offset;
+
+                        /* Count clusters we've seen */
+                        required += offset % cluster_size + pnum;
+                    }
                 }
             }
         }
diff --git a/qemu-img-cmds.hx b/qemu-img-cmds.hx
index 6bc8265cfb..f3f5c63773 100644
--- a/qemu-img-cmds.hx
+++ b/qemu-img-cmds.hx
@@ -78,9 +78,9 @@ SRST
 ERST
 
 DEF("measure", img_measure,
-"measure [--output=ofmt] [-O output_fmt] [-o options] [--size N | [--object 
objectdef] [--image-opts] [-f fmt] [-l snapshot_param] filename]")
+"measure [--output=ofmt] [-O output_fmt] [-o options] [--size N | [--object 
objectdef] [--image-opts] [-f fmt] [-l snapshot_param] [-b base] filename]")
 SRST
-.. option:: measure [--output=OFMT] [-O OUTPUT_FMT] [-o OPTIONS] [--size N | 
[--object OBJECTDEF] [--image-opts] [-f FMT] [-l SNAPSHOT_PARAM] FILENAME]
+.. option:: measure [--output=OFMT] [-O OUTPUT_FMT] [-o OPTIONS] [--size N | 
[--object OBJECTDEF] [--image-opts] [-f FMT] [-l SNAPSHOT_PARAM] [-b BASE] 
FILENAME]
 ERST
 
 DEF("snapshot", img_snapshot,
diff --git a/qemu-img.c b/qemu-img.c
index c42dd4e995..0aa53d7ef8 100644
--- a/qemu-img.c
+++ b/qemu-img.c
@@ -5653,6 +5653,7 @@ static int img_measure(const img_cmd_t *ccmd, int argc, 
char **argv)
     BlockBackend *in_blk = NULL;
     BlockDriver *drv;
     const char *filename = NULL;
+    const char *base_filename = NULL;
     const char *fmt = NULL;
     const char *out_fmt = "raw";
     char *options = NULL;
@@ -5676,6 +5677,7 @@ static int img_measure(const img_cmd_t *ccmd, int argc, 
char **argv)
         {"image-opts", no_argument, 0, OPTION_IMAGE_OPTS},
         {"source-image-opts", no_argument, 0, OPTION_IMAGE_OPTS}, /* 
img_convert */
         {"snapshot", required_argument, 0, 'l'},
+        {"base", required_argument, 0, 'b'},
         {"target-format", required_argument, 0, 'O'},
         {"target-format-options", required_argument, 0, 'o'}, /* img_convert */
         {"options", required_argument, 0, 'o'},
@@ -5686,11 +5688,11 @@ static int img_measure(const img_cmd_t *ccmd, int argc, 
char **argv)
         {0, 0, 0, 0}
     };
 
-    while ((c = getopt_long(argc, argv, "hf:l:O:o:Us:",
+    while ((c = getopt_long(argc, argv, "hf:l:b:O:o:Us:",
                             long_options, NULL)) != -1) {
         switch (c) {
         case 'h':
-            cmd_help(ccmd, "[-f FMT|--image-opts] [-l SNAPSHOT]\n"
+            cmd_help(ccmd, "[-f FMT|--image-opts] [-l SNAPSHOT] [-b BASE]\n"
 "       [-O TARGET_FMT] [-o TARGET_FMT_OPTS] [--output human|json]\n"
 "       [--object OBJDEF] (--size SIZE | FILE)\n"
 ,
@@ -5701,6 +5703,8 @@ static int img_measure(const img_cmd_t *ccmd, int argc, 
char **argv)
 "     instead of a file name (incompatible with --format)\n"
 "  -l, --snapshot SNAPSHOT\n"
 "     use this snapshot in FILE as source\n"
+"  -b, --base BASE\n"
+"     open FILE backing chain up to BASE (inclusive)\n"
 "  -O, --target-format TARGET_FMT\n"
 "     desired target/output image format (default: raw)\n"
 "  -o TARGET_FMT_OPTS\n"
@@ -5738,6 +5742,9 @@ static int img_measure(const img_cmd_t *ccmd, int argc, 
char **argv)
                 snapshot_name = optarg;
             }
             break;
+        case 'b':
+            base_filename = optarg;
+            break;
         case 'O':
             out_fmt = optarg;
             break;
@@ -5773,8 +5780,10 @@ static int img_measure(const img_cmd_t *ccmd, int argc, 
char **argv)
         filename = argv[optind];
     }
 
-    if (!filename && (image_opts || fmt || snapshot_name || sn_opts)) {
-        error_report("--image-opts, -f, and -l require a filename argument.");
+    if (!filename && (image_opts || fmt || snapshot_name || sn_opts ||
+                      base_filename)) {
+        error_report("--image-opts, -f, -l, and -b require a filename "
+                     "argument.");
         goto out;
     }
     if (filename && img_size != -1) {
@@ -5787,12 +5796,30 @@ static int img_measure(const img_cmd_t *ccmd, int argc, 
char **argv)
     }
 
     if (filename) {
-        in_blk = img_open(image_opts, filename, fmt, 0,
+        int src_flags = 0;
+
+        /*
+         * When measuring with --base, avoid opening the full backing chain.
+         * We selectively open only up to the requested base afterwards.
+         */
+        if (base_filename) {
+            src_flags |= BDRV_O_NO_BACKING;
+        }
+
+        in_blk = img_open(image_opts, filename, fmt, src_flags,
                           false, false, force_share);
         if (!in_blk) {
             goto out;
         }
 
+        if (base_filename) {
+            if (bdrv_open_backing_chain_until(blk_bs(in_blk), base_filename,
+                                              &local_err) < 0) {
+                error_report_err(local_err);
+                goto out;
+            }
+        }
+
         if (sn_opts) {
             bdrv_snapshot_load_tmp(blk_bs(in_blk),
                     qemu_opt_get(sn_opts, SNAPSHOT_OPT_ID),
diff --git a/tests/qemu-iotests/178.out.qcow2 b/tests/qemu-iotests/178.out.qcow2
index 61506b519f..0984a74a50 100644
--- a/tests/qemu-iotests/178.out.qcow2
+++ b/tests/qemu-iotests/178.out.qcow2
@@ -6,9 +6,9 @@ qemu-img: Either --size N or one filename must be specified.
 qemu-img: --size N cannot be used together with a filename.
 qemu-img: At most one filename argument is allowed.
 qemu-img: Either --size N or one filename must be specified.
-qemu-img: --image-opts, -f, and -l require a filename argument.
-qemu-img: --image-opts, -f, and -l require a filename argument.
-qemu-img: --image-opts, -f, and -l require a filename argument.
+qemu-img: --image-opts, -f, -l, and -b require a filename argument.
+qemu-img: --image-opts, -f, -l, and -b require a filename argument.
+qemu-img: --image-opts, -f, -l, and -b require a filename argument.
 qemu-img: Invalid option list: ,
 qemu-img: Invalid parameter 'snapshot.foo'
 qemu-img: Failed in parsing snapshot param 'snapshot.foo=bar'
diff --git a/tests/qemu-iotests/178.out.raw b/tests/qemu-iotests/178.out.raw
index 6d994a433a..81249e718b 100644
--- a/tests/qemu-iotests/178.out.raw
+++ b/tests/qemu-iotests/178.out.raw
@@ -6,9 +6,9 @@ qemu-img: Either --size N or one filename must be specified.
 qemu-img: --size N cannot be used together with a filename.
 qemu-img: At most one filename argument is allowed.
 qemu-img: Either --size N or one filename must be specified.
-qemu-img: --image-opts, -f, and -l require a filename argument.
-qemu-img: --image-opts, -f, and -l require a filename argument.
-qemu-img: --image-opts, -f, and -l require a filename argument.
+qemu-img: --image-opts, -f, -l, and -b require a filename argument.
+qemu-img: --image-opts, -f, -l, and -b require a filename argument.
+qemu-img: --image-opts, -f, -l, and -b require a filename argument.
 qemu-img: Invalid option list: ,
 qemu-img: Invalid parameter 'snapshot.foo'
 qemu-img: Failed in parsing snapshot param 'snapshot.foo=bar'
-- 
2.54.0


Reply via email to