All of lore.kernel.org
 help / color / mirror / Atom feed
From: Max Reitz <mreitz@redhat.com>
To: qemu-block@nongnu.org
Cc: Kevin Wolf <kwolf@redhat.com>,
	Vladimir Sementsov-Ogievskiy <vsementsov@virtuozzo.com>,
	Alberto Garcia <berto@igalia.com>,
	qemu-devel@nongnu.org, Max Reitz <mreitz@redhat.com>
Subject: [PATCH for-5.0 v2 23/23] iotests: Mirror must not attempt to create loops
Date: Mon, 11 Nov 2019 17:02:16 +0100	[thread overview]
Message-ID: <20191111160216.197086-24-mreitz@redhat.com> (raw)
In-Reply-To: <20191111160216.197086-1-mreitz@redhat.com>

Signed-off-by: Max Reitz <mreitz@redhat.com>
---
 tests/qemu-iotests/041     | 235 +++++++++++++++++++++++++++++++++++++
 tests/qemu-iotests/041.out |   4 +-
 2 files changed, 237 insertions(+), 2 deletions(-)

diff --git a/tests/qemu-iotests/041 b/tests/qemu-iotests/041
index 9a00cf6f7b..0e43bb699d 100755
--- a/tests/qemu-iotests/041
+++ b/tests/qemu-iotests/041
@@ -1246,6 +1246,241 @@ class TestReplaces(iotests.QMPTestCase):
 
         self.vm.assert_block_path('filter0', '/file', 'target')
 
+    """
+    See what happens when the @sync/@replaces configuration dictates
+    creating a loop.
+    """
+    @iotests.skip_if_unsupported(['throttle'])
+    def test_loop(self):
+        qemu_img('create', '-f', iotests.imgfmt, test_img, str(1 * 1024 * 1024))
+
+        # Dummy group so we can create a NOP filter
+        result = self.vm.qmp('object-add', qom_type='throttle-group', id='tg0')
+        self.assert_qmp(result, 'return', {})
+
+        result = self.vm.qmp('blockdev-add', **{
+                                 'driver': 'throttle',
+                                 'node-name': 'source',
+                                 'throttle-group': 'tg0',
+                                 'file': {
+                                     'driver': iotests.imgfmt,
+                                     'node-name': 'filtered',
+                                     'file': {
+                                         'driver': 'file',
+                                         'filename': test_img
+                                     }
+                                 }
+                             })
+        self.assert_qmp(result, 'return', {})
+
+        # Block graph is now:
+        #   source[throttle] --file--> filtered[imgfmt] --file--> ...
+
+        result = self.vm.qmp('drive-mirror', job_id='mirror', device='source',
+                             target=target_img, format=iotests.imgfmt,
+                             node_name='target', sync='none',
+                             replaces='filtered')
+
+        """
+        Block graph before mirror exits would be (ignoring mirror_top):
+          source[throttle] --file--> filtered[imgfmt] --file--> ...
+          target[imgfmt] --file--> ...
+
+        Then, because of sync=none and drive-mirror in absolute-paths mode,
+        the source is attached to the target:
+          source[throttle] --file--> filtered[imgfmt] --file--> ...
+                 ^
+              backing
+                 |
+            target[imgfmt] --file--> ...
+
+        Replacing filtered by target would yield:
+          source[throttle] --file--> target[imgfmt] --file--> ...
+                 ^                        |
+                 +------- backing --------+
+
+        I.e., a loop.  bdrv_replace_node() detects this and simply
+        does not let source's file link point to target.  However,
+        that means that target cannot really replace source.
+
+        drive-mirror should detect this and not allow this case.
+        """
+
+        self.assert_qmp(result, 'error/desc',
+                        "Replacing 'filtered' by 'target' with this sync " + \
+                        "mode would result in a loop, because the former " + \
+                        "would be a child of the latter's backing file " + \
+                        "('source') after the mirror job")
+
+    """
+    Test what happens when there would be no loop with the pre-mirror
+    configuration, but something changes during the mirror job that asks
+    for a loop to be created during completion.
+    """
+    @iotests.skip_if_unsupported(['copy-on-read', 'quorum'])
+    def test_loop_during_mirror(self):
+        qemu_img('create', '-f', iotests.imgfmt, test_img, str(1 * 1024 * 1024))
+
+        """
+        In this test, we are going to mirror from a node that is a
+        filter above some file "common-base".  The target is a quorum
+        node (with just an unrelated null-co child).
+
+        We will ask the mirror job to replace common-base by the
+        target upon completion.  That is a completely valid
+        configuration so far.
+
+        However, while the job is running, we add common-base as an
+        (indirect[1]) child to the target quorum node.  This way,
+        completing the job as requested would yield a loop, because
+        the target would be supposed to replace common-base -- which
+        is its own (indirect) child.
+
+        [1] It needs to be an indirect child, because if it were a
+        direct child, the mirror job would simply end by effectively
+        injecting the target above common-base.  This is the same
+        effect as when using sync=none: The target ends up above the
+        source.
+
+        So only loops that have a length of more than one node are
+        forbidden, which means common-base must be an indirect child
+        of the target.
+
+        (Furthermore, we are going to use x-blockdev-change to add
+        common-base as a child to the target.  This command only
+        allows doing so for nodes that have no parent yet.
+        common-base will have a parent already, though, namely the
+        source node.  Therefore, this is another reason why we need at
+        least one node above common-base, so this parent can become
+        target's child during the mirror.)
+        """
+
+        result = self.vm.qmp('blockdev-add', **{
+                                 'driver': 'null-co',
+                                 'node-name': 'common-base',
+                                 'read-zeroes': True,
+                                 'size': 1 * 1024 * 1024
+                             })
+        self.assert_qmp(result, 'return', {})
+
+        result = self.vm.qmp('blockdev-add', **{
+                                 'driver': 'copy-on-read',
+                                 'node-name': 'source',
+                                 'file': 'common-base'
+                             })
+        self.assert_qmp(result, 'return', {})
+
+        """
+        As explained above, we have to create a parent above
+        common-base.
+
+        We cannot use any parent that would forward the RESIZE
+        permission, because the job takes it on the target, but
+        unshares it on the source: After the x-blockdev-change
+        operation during the mirror job, this parent will be a child
+        of the target, so common-base will be an (indirect) child of
+        both the mirror's source and target.  Thus, the job would
+        conflict with itself.
+
+        Therefore, we make common-base a backing child of a $imgfmt
+        node.  Unfortunately, we cannot let the mirror job replace a
+        node that acts as a backing child somewhere (because of an op
+        blocker), so we put another raw node between the $imgfmt node
+        and common-base.
+        """
+        result = self.vm.qmp('blockdev-add', **{
+                                 'driver': iotests.imgfmt,
+                                 'node-name': 'base-parent',
+                                 'file': {
+                                     'driver': 'file',
+                                     'filename': test_img
+                                 },
+                                 'backing': {
+                                     'driver': 'raw',
+                                     'file': 'common-base'
+                                 }
+                             })
+
+        """
+        Add a quorum node with a single child, we will add base-parent
+        to prepare a loop later.
+        (We do not care about this single child at all, but it is
+        impossible to create a quorum node without any children.  We
+        will ignore this child from now on.)
+        """
+        result = self.vm.qmp('blockdev-add', **{
+                                 'driver': 'quorum',
+                                 'node-name': 'target',
+                                 'vote-threshold': 1,
+                                 'children': [
+                                     {
+                                         'driver': 'null-co',
+                                         'read-zeroes': True,
+                                         'size': 1 * 1024 * 1024
+                                     }
+                                 ]
+                             })
+        self.assert_qmp(result, 'return', {})
+
+        """
+        Current block graph:
+
+        base-parent[$imgfmt] --backing--> [raw]
+                                            |
+                                           file
+                                            v
+              source[COR] --file--> common-base[null-co]
+
+        target[quorum]
+
+
+        The following blockdev-mirror asks for this graph post-mirror:
+
+        base-parent[$imgfmt] --backing--> [raw]
+                                            |
+                                           file
+                                            v
+                source[COR] --file--> target[quorum]
+
+        That would be a valid configuration without any loops.
+        """
+
+        result = self.vm.qmp('blockdev-mirror', job_id='mirror',
+                             device='source', target='target', sync='full',
+                             replaces='common-base')
+        self.assert_qmp(result, 'return', {})
+
+        """
+        However, now we will make base-parent a child of target.
+        Before the mirror job completes, that is still completely
+        valid:
+
+                                             source
+                                               |
+                                               v
+        target -> base-parent -> [raw] -> common-base
+        """
+
+        result = self.vm.qmp('x-blockdev-change',
+                             parent='target', node='base-parent')
+        self.assert_qmp(result, 'return', {})
+
+        """
+        However, post-mirror, we thus ask for a loop:
+
+        source -> target (replaced common-base) -> base-parent
+                                  ^                    |
+                                  |                    v
+                                  +----------------- [raw]
+
+        bdrv_replace_node() would not allow such a configuration, but
+        we should not pretend we can create it, so the mirror job
+        should fail during completion.
+        """
+
+        self.complete_and_wait('mirror',
+                               completion_error='Operation not permitted')
+
 if __name__ == '__main__':
     iotests.main(supported_fmts=['qcow2', 'qed'],
                  supported_protocols=['file'])
diff --git a/tests/qemu-iotests/041.out b/tests/qemu-iotests/041.out
index 877b76fd31..20a8158b99 100644
--- a/tests/qemu-iotests/041.out
+++ b/tests/qemu-iotests/041.out
@@ -1,5 +1,5 @@
-..............................................................................................
+................................................................................................
 ----------------------------------------------------------------------
-Ran 94 tests
+Ran 96 tests
 
 OK
-- 
2.23.0



  parent reply	other threads:[~2019-11-11 16:36 UTC|newest]

Thread overview: 75+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-11-11 16:01 [PATCH for-5.0 v2 00/23] block: Fix check_to_replace_node() Max Reitz
2019-11-11 16:01 ` [PATCH for-5.0 v2 01/23] blockdev: Allow external snapshots everywhere Max Reitz
2019-11-11 16:01 ` [PATCH for-5.0 v2 02/23] blockdev: Allow resizing everywhere Max Reitz
2019-12-06 14:04   ` Alberto Garcia
2019-12-09 13:56     ` Max Reitz
2019-11-11 16:01 ` [PATCH for-5.0 v2 03/23] block: Drop bdrv_is_first_non_filter() Max Reitz
2019-11-11 16:01 ` [PATCH for-5.0 v2 04/23] iotests: Let 041 use -blockdev for quorum children Max Reitz
2019-11-11 16:01 ` [PATCH for-5.0 v2 05/23] quorum: Fix child permissions Max Reitz
2019-11-29  9:14   ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:01 ` [PATCH for-5.0 v2 06/23] block: Add bdrv_recurse_can_replace() Max Reitz
2019-11-29  9:34   ` Vladimir Sementsov-Ogievskiy
2019-11-29 10:23     ` Max Reitz
2019-11-29 11:04       ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:02 ` [PATCH for-5.0 v2 07/23] blkverify: Implement .bdrv_recurse_can_replace() Max Reitz
2019-11-29  9:41   ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:02 ` [PATCH for-5.0 v2 08/23] quorum: Store children in own structure Max Reitz
2019-11-29  9:46   ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:02 ` [PATCH for-5.0 v2 09/23] quorum: Add QuorumChild.to_be_replaced Max Reitz
2019-11-29  9:59   ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:02 ` [PATCH for-5.0 v2 10/23] quorum: Implement .bdrv_recurse_can_replace() Max Reitz
2019-11-29 10:18   ` Vladimir Sementsov-Ogievskiy
2019-11-29 12:50     ` Max Reitz
2020-02-05 15:55   ` Kevin Wolf
2020-02-05 16:03     ` Kevin Wolf
2020-02-06 10:21     ` Max Reitz
2020-02-06 14:42       ` Kevin Wolf
2020-02-06 15:19         ` Max Reitz
2020-02-06 15:42           ` Kevin Wolf
2020-02-06 16:44             ` Max Reitz
2019-11-11 16:02 ` [PATCH for-5.0 v2 11/23] block: Use bdrv_recurse_can_replace() Max Reitz
2019-11-29 11:07   ` Vladimir Sementsov-Ogievskiy
2020-02-05 15:57   ` Kevin Wolf
2019-11-11 16:02 ` [PATCH for-5.0 v2 12/23] block: Remove bdrv_recurse_is_first_non_filter() Max Reitz
2019-11-11 16:02 ` [PATCH for-5.0 v2 13/23] mirror: Double-check immediately before replacing Max Reitz
2019-11-29 11:18   ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:02 ` [PATCH for-5.0 v2 14/23] quorum: Stop marking it as a filter Max Reitz
2019-11-11 16:02 ` [PATCH for-5.0 v2 15/23] mirror: Prevent loops Max Reitz
2019-11-29 12:01   ` Vladimir Sementsov-Ogievskiy
2019-11-29 13:46     ` Max Reitz
2019-11-29 13:55       ` Vladimir Sementsov-Ogievskiy
2019-11-29 14:17         ` Max Reitz
2019-11-29 14:26           ` Vladimir Sementsov-Ogievskiy
2019-11-29 14:38             ` Max Reitz
2019-12-02 12:12   ` Vladimir Sementsov-Ogievskiy
2019-12-09 14:43     ` Max Reitz
2019-12-13 11:18       ` Vladimir Sementsov-Ogievskiy
2019-12-20 11:39         ` Max Reitz
2019-12-20 11:55           ` Vladimir Sementsov-Ogievskiy
2019-12-20 12:10             ` Max Reitz
2019-11-11 16:02 ` [PATCH for-5.0 v2 16/23] iotests: Use complete_and_wait() in 155 Max Reitz
2019-11-11 16:02 ` [PATCH for-5.0 v2 17/23] iotests: Use skip_if_unsupported decorator in 041 Max Reitz
2019-12-03 12:03   ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:02 ` [PATCH for-5.0 v2 18/23] iotests: Add VM.assert_block_path() Max Reitz
2019-12-03 12:59   ` Vladimir Sementsov-Ogievskiy
2019-12-09 15:10     ` Max Reitz
2019-12-13 11:26       ` Vladimir Sementsov-Ogievskiy
2019-12-13 11:27   ` Vladimir Sementsov-Ogievskiy
2019-12-20 11:42     ` Max Reitz
2019-11-11 16:02 ` [PATCH for-5.0 v2 19/23] iotests: Resolve TODOs in 041 Max Reitz
2019-12-03 13:32   ` Vladimir Sementsov-Ogievskiy
2019-12-03 13:33     ` Vladimir Sementsov-Ogievskiy
2019-12-09 15:15       ` Max Reitz
2019-12-13 11:31         ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:02 ` [PATCH for-5.0 v2 20/23] iotests: Use self.image_len in TestRepairQuorum Max Reitz
2019-11-11 16:02 ` [PATCH for-5.0 v2 21/23] iotests: Add tests for invalid Quorum @replaces Max Reitz
2019-12-03 14:40   ` Vladimir Sementsov-Ogievskiy
2019-11-11 16:02 ` [PATCH for-5.0 v2 22/23] iotests: Check that @replaces can replace filters Max Reitz
2019-12-03 15:58   ` Vladimir Sementsov-Ogievskiy
2019-12-09 15:17     ` Max Reitz
2019-11-11 16:02 ` Max Reitz [this message]
2019-12-03 17:03   ` [PATCH for-5.0 v2 23/23] iotests: Mirror must not attempt to create loops Vladimir Sementsov-Ogievskiy
2019-11-29 12:24 ` [PATCH for-5.0 v2 00/23] block: Fix check_to_replace_node() Vladimir Sementsov-Ogievskiy
2019-11-29 12:49   ` Max Reitz
2019-11-29 12:55     ` Vladimir Sementsov-Ogievskiy
2019-11-29 13:08       ` Max Reitz

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20191111160216.197086-24-mreitz@redhat.com \
    --to=mreitz@redhat.com \
    --cc=berto@igalia.com \
    --cc=kwolf@redhat.com \
    --cc=qemu-block@nongnu.org \
    --cc=qemu-devel@nongnu.org \
    --cc=vsementsov@virtuozzo.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.