Test what happens when there is an I/O error after a mirror job in the READY phase has been cancelled.
Signed-off-by: Max Reitz <mre...@redhat.com> Reviewed-by: Vladimir Sementsov-Ogievskiy <vsement...@virtuozzo.com> Tested-by: Vladimir Sementsov-Ogievskiy <vsement...@virtuozzo.com> --- .../tests/mirror-ready-cancel-error | 143 ++++++++++++++++++ .../tests/mirror-ready-cancel-error.out | 5 + 2 files changed, 148 insertions(+) create mode 100755 tests/qemu-iotests/tests/mirror-ready-cancel-error create mode 100644 tests/qemu-iotests/tests/mirror-ready-cancel-error.out diff --git a/tests/qemu-iotests/tests/mirror-ready-cancel-error b/tests/qemu-iotests/tests/mirror-ready-cancel-error new file mode 100755 index 0000000000..f2dc88881f --- /dev/null +++ b/tests/qemu-iotests/tests/mirror-ready-cancel-error @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# group: rw quick +# +# Test what happens when errors occur to a mirror job after it has +# been cancelled in the READY phase +# +# Copyright (C) 2021 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import iotests + + +image_size = 1 * 1024 * 1024 +source = os.path.join(iotests.test_dir, 'source.img') +target = os.path.join(iotests.test_dir, 'target.img') + + +class TestMirrorReadyCancelError(iotests.QMPTestCase): + def setUp(self) -> None: + assert iotests.qemu_img_create('-f', iotests.imgfmt, source, + str(image_size)) == 0 + assert iotests.qemu_img_create('-f', iotests.imgfmt, target, + str(image_size)) == 0 + + self.vm = iotests.VM() + self.vm.launch() + + def tearDown(self) -> None: + self.vm.shutdown() + os.remove(source) + os.remove(target) + + def add_blockdevs(self, once: bool) -> None: + res = self.vm.qmp('blockdev-add', + **{'node-name': 'source', + 'driver': iotests.imgfmt, + 'file': { + 'driver': 'file', + 'filename': source + }}) + self.assert_qmp(res, 'return', {}) + + # blkdebug notes: + # Enter state 2 on the first flush, which happens before the + # job enters the READY state. The second flush will happen + # when the job is about to complete, and we want that one to + # fail. + res = self.vm.qmp('blockdev-add', + **{'node-name': 'target', + 'driver': iotests.imgfmt, + 'file': { + 'driver': 'blkdebug', + 'image': { + 'driver': 'file', + 'filename': target + }, + 'set-state': [{ + 'event': 'flush_to_disk', + 'state': 1, + 'new_state': 2 + }], + 'inject-error': [{ + 'event': 'flush_to_disk', + 'once': once, + 'immediately': True, + 'state': 2 + }]}}) + self.assert_qmp(res, 'return', {}) + + def start_mirror(self) -> None: + res = self.vm.qmp('blockdev-mirror', + job_id='mirror', + device='source', + target='target', + filter_node_name='mirror-top', + sync='full', + on_target_error='stop') + self.assert_qmp(res, 'return', {}) + + def cancel_mirror_with_error(self) -> None: + self.vm.event_wait('BLOCK_JOB_READY') + + # Write something so will not leave the job immediately, but + # flush first (which will fail, thanks to blkdebug) + res = self.vm.qmp('human-monitor-command', + command_line='qemu-io mirror-top "write 0 64k"') + self.assert_qmp(res, 'return', '') + + # Drain status change events + while self.vm.event_wait('JOB_STATUS_CHANGE', timeout=0.0) is not None: + pass + + res = self.vm.qmp('block-job-cancel', device='mirror') + self.assert_qmp(res, 'return', {}) + + self.vm.event_wait('BLOCK_JOB_ERROR') + + def test_transient_error(self) -> None: + self.add_blockdevs(True) + self.start_mirror() + self.cancel_mirror_with_error() + + while True: + e = self.vm.event_wait('JOB_STATUS_CHANGE') + if e['data']['status'] == 'standby': + # Transient error, try again + self.vm.qmp('block-job-resume', device='mirror') + elif e['data']['status'] == 'null': + break + + def test_persistent_error(self) -> None: + self.add_blockdevs(False) + self.start_mirror() + self.cancel_mirror_with_error() + + while True: + e = self.vm.event_wait('JOB_STATUS_CHANGE') + if e['data']['status'] == 'standby': + # Persistent error, no point in continuing + self.vm.qmp('block-job-cancel', device='mirror', force=True) + elif e['data']['status'] == 'null': + break + + +if __name__ == '__main__': + # LUKS would require special key-secret handling in add_blockdevs() + iotests.main(supported_fmts=['generic'], + unsupported_fmts=['luks'], + supported_protocols=['file']) diff --git a/tests/qemu-iotests/tests/mirror-ready-cancel-error.out b/tests/qemu-iotests/tests/mirror-ready-cancel-error.out new file mode 100644 index 0000000000..fbc63e62f8 --- /dev/null +++ b/tests/qemu-iotests/tests/mirror-ready-cancel-error.out @@ -0,0 +1,5 @@ +.. +---------------------------------------------------------------------- +Ran 2 tests + +OK -- 2.31.1