1#!/usr/bin/env python3
2# group: rw quick
3#
4# Test what happens when errors occur to a mirror job after it has
5# been cancelled in the READY phase
6#
7# Copyright (C) 2021 Red Hat, Inc.
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program.  If not, see <http://www.gnu.org/licenses/>.
21#
22
23import os
24import iotests
25
26
27image_size = 1 * 1024 * 1024
28source = os.path.join(iotests.test_dir, 'source.img')
29target = os.path.join(iotests.test_dir, 'target.img')
30
31
32class TestMirrorReadyCancelError(iotests.QMPTestCase):
33    def setUp(self) -> None:
34        iotests.qemu_img_create('-f', iotests.imgfmt, source, str(image_size))
35        iotests.qemu_img_create('-f', iotests.imgfmt, target, str(image_size))
36
37        # Ensure that mirror will copy something before READY so the
38        # target format layer will forward the pre-READY flush to its
39        # file child
40        iotests.qemu_io('-c', 'write -P 1 0 64k', source)
41
42        self.vm = iotests.VM()
43        self.vm.launch()
44
45    def tearDown(self) -> None:
46        self.vm.shutdown()
47        os.remove(source)
48        os.remove(target)
49
50    def add_blockdevs(self, once: bool) -> None:
51        res = self.vm.qmp('blockdev-add',
52                          **{'node-name': 'source',
53                             'driver': iotests.imgfmt,
54                             'file': {
55                                 'driver': 'file',
56                                 'filename': source
57                             }})
58        self.assert_qmp(res, 'return', {})
59
60        # blkdebug notes:
61        # Enter state 2 on the first flush, which happens before the
62        # job enters the READY state.  The second flush will happen
63        # when the job is about to complete, and we want that one to
64        # fail.
65        res = self.vm.qmp('blockdev-add',
66                          **{'node-name': 'target',
67                             'driver': iotests.imgfmt,
68                             'file': {
69                                 'driver': 'blkdebug',
70                                 'image': {
71                                     'driver': 'file',
72                                     'filename': target
73                                 },
74                                 'set-state': [{
75                                     'event': 'flush_to_disk',
76                                     'state': 1,
77                                     'new_state': 2
78                                 }],
79                                 'inject-error': [{
80                                     'event': 'flush_to_disk',
81                                     'once': once,
82                                     'immediately': True,
83                                     'state': 2
84                                 }]}})
85        self.assert_qmp(res, 'return', {})
86
87    def start_mirror(self) -> None:
88        res = self.vm.qmp('blockdev-mirror',
89                          job_id='mirror',
90                          device='source',
91                          target='target',
92                          filter_node_name='mirror-top',
93                          sync='full',
94                          on_target_error='stop')
95        self.assert_qmp(res, 'return', {})
96
97    def cancel_mirror_with_error(self) -> None:
98        self.vm.event_wait('BLOCK_JOB_READY')
99
100        # Write something so will not leave the job immediately, but
101        # flush first (which will fail, thanks to blkdebug)
102        res = self.vm.qmp('human-monitor-command',
103                          command_line='qemu-io mirror-top "write -P 2 0 64k"')
104        self.assert_qmp(res, 'return', '')
105
106        # Drain status change events
107        while self.vm.event_wait('JOB_STATUS_CHANGE', timeout=0.0) is not None:
108            pass
109
110        res = self.vm.qmp('block-job-cancel', device='mirror')
111        self.assert_qmp(res, 'return', {})
112
113        self.vm.event_wait('BLOCK_JOB_ERROR')
114
115    def test_transient_error(self) -> None:
116        self.add_blockdevs(True)
117        self.start_mirror()
118        self.cancel_mirror_with_error()
119
120        while True:
121            e = self.vm.event_wait('JOB_STATUS_CHANGE')
122            if e['data']['status'] == 'standby':
123                # Transient error, try again
124                self.vm.qmp('block-job-resume', device='mirror')
125            elif e['data']['status'] == 'null':
126                break
127
128    def test_persistent_error(self) -> None:
129        self.add_blockdevs(False)
130        self.start_mirror()
131        self.cancel_mirror_with_error()
132
133        while True:
134            e = self.vm.event_wait('JOB_STATUS_CHANGE')
135            if e['data']['status'] == 'standby':
136                # Persistent error, no point in continuing
137                self.vm.qmp('block-job-cancel', device='mirror', force=True)
138            elif e['data']['status'] == 'null':
139                break
140
141
142if __name__ == '__main__':
143    # LUKS would require special key-secret handling in add_blockdevs()
144    iotests.main(supported_fmts=['generic'],
145                 unsupported_fmts=['luks'],
146                 supported_protocols=['file'])
147