diff mbox series

[22/22] iotests: Mirror must not attempt to create loops

Message ID 20190920152804.12875-23-mreitz@redhat.com (mailing list archive)
State New, archived
Headers show
Series block: Fix check_to_replace_node() | expand

Commit Message

Max Reitz Sept. 20, 2019, 3:28 p.m. UTC
Signed-off-by: Max Reitz <mreitz@redhat.com>
---
 tests/qemu-iotests/041     | 152 +++++++++++++++++++++++++++++++++++++
 tests/qemu-iotests/041.out |   4 +-
 2 files changed, 154 insertions(+), 2 deletions(-)

Comments

Vladimir Sementsov-Ogievskiy Sept. 26, 2019, 3:03 p.m. UTC | #1
20.09.2019 18:28, Max Reitz wrote:
> Signed-off-by: Max Reitz <mreitz@redhat.com>
> ---
>   tests/qemu-iotests/041     | 152 +++++++++++++++++++++++++++++++++++++
>   tests/qemu-iotests/041.out |   4 +-
>   2 files changed, 154 insertions(+), 2 deletions(-)
> 
> diff --git a/tests/qemu-iotests/041 b/tests/qemu-iotests/041
> index e4cc829fe2..6ea4764ae8 100755
> --- a/tests/qemu-iotests/041
> +++ b/tests/qemu-iotests/041
> @@ -1265,6 +1265,158 @@ class TestReplaces(iotests.QMPTestCase):
>   
>           self.vm.assert_block_path('filter0/file', 'target')
>   
> +    '''
> +    See what happens when the @sync/@replaces configuration dictates
> +    creating a loop.
> +    '''
> +    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[qcow2] --file--> ...

or qed, actually

> +
> +        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[qcow2] --file--> ...
> +          target[qcow2] --file--> ...
> +
> +        Then, because of sync=none and drive-mirror in absolute-paths mode,
> +        the source is attached to the target:
> +          source[throttle] --file--> filtered[qcow2] --file--> ...
> +                 ^
                     |
> +              backing
> +                 |
> +            target[qcow2] --file--> ...
> +
> +        Replacing filtered by target would yield:
> +          source[throttle] --file--> target[qcow2] --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.
> +    '''
> +    def test_loop_during_mirror(self):
> +        qemu_img('create', '-f', iotests.imgfmt, test_img, str(1 * 1024 * 1024))
> +
> +        result = self.vm.qmp('blockdev-add', **{
> +                                 'driver': 'null-co',
> +                                 'node-name': 'common-base',
> +                                 'read-zeroes': True,

why do you need read-zeroes?

> +                                 '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', {})

Hmm, why don't you create them both in one query?

> +
> +        '''

the following is hard to read without some hint like, "We are going to ..."

> +        x-blockdev-change can only add children to a quorum node that
> +        have no parent yet, so we need an intermediate node between
> +        target and common-base that has no parents other than target.
> +        We cannot use any parent that would forward the RESIZE
> +        permission (because the job takes it, but unshares it on the
> +        source), so we make it a backing child of a qcow2 node.
> +        Unfortunately, we cannot add backing files to Quorum nodes
> +        (because of an op blocker), so we put another raw node between
> +        the qcow2 node and common-base.
> +        '''
> +        result = self.vm.qmp('blockdev-add', **{
> +                                 'driver': 'qcow2',
> +                                 '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
> +        result = self.vm.qmp('blockdev-add', **{
> +                                 'driver': 'quorum',

It would be good to skip test-cases if quorum unsupported, like other test-cases
with quorum.

> +                                 'node-name': 'target',
> +                                 'vote-threshold': 1,
> +                                 'children': [
> +                                     {
> +                                         'driver': 'null-co',
> +                                         'read-zeroes': True
> +                                     }
> +                                 ]
> +                             })
> +        self.assert_qmp(result, 'return', {})

It would be nice to comment out current block graph here...

> +
> +        result = self.vm.qmp('blockdev-mirror', job_id='mirror',
> +                             device='source', target='target', sync='full',
> +                             replaces='common-base')
> +        self.assert_qmp(result, 'return', {})
> +
> +        result = self.vm.qmp('x-blockdev-change',
> +                             parent='target', node='base-parent')
> +        self.assert_qmp(result, 'return', {})
> +
> +        '''

and here, like you do in previous test-case. And here it even nicer, as this test-case
is more complex.

> +        This asks for this configuration post-mirror:
> +
> +        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
>
Max Reitz Oct. 2, 2019, 12:46 p.m. UTC | #2
On 26.09.19 17:03, Vladimir Sementsov-Ogievskiy wrote:
> 20.09.2019 18:28, Max Reitz wrote:
>> Signed-off-by: Max Reitz <mreitz@redhat.com>
>> ---
>>   tests/qemu-iotests/041     | 152 +++++++++++++++++++++++++++++++++++++
>>   tests/qemu-iotests/041.out |   4 +-
>>   2 files changed, 154 insertions(+), 2 deletions(-)
>>
>> diff --git a/tests/qemu-iotests/041 b/tests/qemu-iotests/041
>> index e4cc829fe2..6ea4764ae8 100755
>> --- a/tests/qemu-iotests/041
>> +++ b/tests/qemu-iotests/041
>> @@ -1265,6 +1265,158 @@ class TestReplaces(iotests.QMPTestCase):
>>   
>>           self.vm.assert_block_path('filter0/file', 'target')
>>   
>> +    '''
>> +    See what happens when the @sync/@replaces configuration dictates
>> +    creating a loop.
>> +    '''
>> +    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[qcow2] --file--> ...
> 
> or qed, actually

Yep.

>> +
>> +        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[qcow2] --file--> ...
>> +          target[qcow2] --file--> ...
>> +
>> +        Then, because of sync=none and drive-mirror in absolute-paths mode,
>> +        the source is attached to the target:
>> +          source[throttle] --file--> filtered[qcow2] --file--> ...
>> +                 ^
>                      |
>> +              backing
>> +                 |
>> +            target[qcow2] --file--> ...
>> +
>> +        Replacing filtered by target would yield:
>> +          source[throttle] --file--> target[qcow2] --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.
>> +    '''
>> +    def test_loop_during_mirror(self):
>> +        qemu_img('create', '-f', iotests.imgfmt, test_img, str(1 * 1024 * 1024))
>> +
>> +        result = self.vm.qmp('blockdev-add', **{
>> +                                 'driver': 'null-co',
>> +                                 'node-name': 'common-base',
>> +                                 'read-zeroes': True,
> 
> why do you need read-zeroes?

It’s my understanding that we’d better always set it to 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', {})
> 
> Hmm, why don't you create them both in one query?

No good reason, I think.

>> +
>> +        '''
> 
> the following is hard to read without some hint like, "We are going to ..."

I’ll see what I can come up with.

>> +        x-blockdev-change can only add children to a quorum node that
>> +        have no parent yet, so we need an intermediate node between
>> +        target and common-base that has no parents other than target.
>> +        We cannot use any parent that would forward the RESIZE
>> +        permission (because the job takes it, but unshares it on the
>> +        source), so we make it a backing child of a qcow2 node.
>> +        Unfortunately, we cannot add backing files to Quorum nodes
>> +        (because of an op blocker), so we put another raw node between
>> +        the qcow2 node and common-base.
>> +        '''
>> +        result = self.vm.qmp('blockdev-add', **{
>> +                                 'driver': 'qcow2',
>> +                                 '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
>> +        result = self.vm.qmp('blockdev-add', **{
>> +                                 'driver': 'quorum',
> 
> It would be good to skip test-cases if quorum unsupported, like other test-cases
> with quorum.

Will do.

>> +                                 'node-name': 'target',
>> +                                 'vote-threshold': 1,
>> +                                 'children': [
>> +                                     {
>> +                                         'driver': 'null-co',
>> +                                         'read-zeroes': True
>> +                                     }
>> +                                 ]
>> +                             })
>> +        self.assert_qmp(result, 'return', {})
> 
> It would be nice to comment out current block graph here...

OK.

>> +
>> +        result = self.vm.qmp('blockdev-mirror', job_id='mirror',
>> +                             device='source', target='target', sync='full',
>> +                             replaces='common-base')
>> +        self.assert_qmp(result, 'return', {})
>> +
>> +        result = self.vm.qmp('x-blockdev-change',
>> +                             parent='target', node='base-parent')
>> +        self.assert_qmp(result, 'return', {})
>> +
>> +        '''
> 
> and here, like you do in previous test-case. And here it even nicer, as this test-case
> is more complex.

OK.

>> +        This asks for this configuration post-mirror:
>> +
>> +        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
>>
> 
>
diff mbox series

Patch

diff --git a/tests/qemu-iotests/041 b/tests/qemu-iotests/041
index e4cc829fe2..6ea4764ae8 100755
--- a/tests/qemu-iotests/041
+++ b/tests/qemu-iotests/041
@@ -1265,6 +1265,158 @@  class TestReplaces(iotests.QMPTestCase):
 
         self.vm.assert_block_path('filter0/file', 'target')
 
+    '''
+    See what happens when the @sync/@replaces configuration dictates
+    creating a loop.
+    '''
+    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[qcow2] --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[qcow2] --file--> ...
+          target[qcow2] --file--> ...
+
+        Then, because of sync=none and drive-mirror in absolute-paths mode,
+        the source is attached to the target:
+          source[throttle] --file--> filtered[qcow2] --file--> ...
+                 ^
+              backing
+                 |
+            target[qcow2] --file--> ...
+
+        Replacing filtered by target would yield:
+          source[throttle] --file--> target[qcow2] --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.
+    '''
+    def test_loop_during_mirror(self):
+        qemu_img('create', '-f', iotests.imgfmt, test_img, str(1 * 1024 * 1024))
+
+        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', {})
+
+        '''
+        x-blockdev-change can only add children to a quorum node that
+        have no parent yet, so we need an intermediate node between
+        target and common-base that has no parents other than target.
+        We cannot use any parent that would forward the RESIZE
+        permission (because the job takes it, but unshares it on the
+        source), so we make it a backing child of a qcow2 node.
+        Unfortunately, we cannot add backing files to Quorum nodes
+        (because of an op blocker), so we put another raw node between
+        the qcow2 node and common-base.
+        '''
+        result = self.vm.qmp('blockdev-add', **{
+                                 'driver': 'qcow2',
+                                 '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
+        result = self.vm.qmp('blockdev-add', **{
+                                 'driver': 'quorum',
+                                 'node-name': 'target',
+                                 'vote-threshold': 1,
+                                 'children': [
+                                     {
+                                         'driver': 'null-co',
+                                         'read-zeroes': True
+                                     }
+                                 ]
+                             })
+        self.assert_qmp(result, 'return', {})
+
+        result = self.vm.qmp('blockdev-mirror', job_id='mirror',
+                             device='source', target='target', sync='full',
+                             replaces='common-base')
+        self.assert_qmp(result, 'return', {})
+
+        result = self.vm.qmp('x-blockdev-change',
+                             parent='target', node='base-parent')
+        self.assert_qmp(result, 'return', {})
+
+        '''
+        This asks for this configuration post-mirror:
+
+        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