1#!/usr/bin/env python3 2# group: rw 3# 4# Test for changing mirror copy mode from background to active 5# 6# Copyright (C) 2023 Proxmox Server Solutions GmbH 7# 8# This program is free software; you can redistribute it and/or modify 9# it under the terms of the GNU General Public License as published by 10# the Free Software Foundation; either version 2 of the License, or 11# (at your option) any later version. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with this program. If not, see <http://www.gnu.org/licenses/>. 20# 21 22import os 23import time 24 25import iotests 26from iotests import qemu_img, QemuStorageDaemon 27 28iops_target = 8 29iops_source = iops_target * 2 30image_size = 1 * 1024 * 1024 31source_img = os.path.join(iotests.test_dir, 'source.' + iotests.imgfmt) 32target_img = os.path.join(iotests.test_dir, 'target.' + iotests.imgfmt) 33nbd_sock = os.path.join(iotests.sock_dir, 'nbd.sock') 34 35class TestMirrorChangeCopyMode(iotests.QMPTestCase): 36 37 def setUp(self): 38 qemu_img('create', '-f', iotests.imgfmt, source_img, str(image_size)) 39 qemu_img('create', '-f', iotests.imgfmt, target_img, str(image_size)) 40 41 self.qsd = QemuStorageDaemon('--nbd-server', 42 f'addr.type=unix,addr.path={nbd_sock}', 43 qmp=True) 44 45 self.qsd.cmd('object-add', { 46 'qom-type': 'throttle-group', 47 'id': 'thrgr-target', 48 'limits': { 49 'iops-write': iops_target, 50 'iops-write-max': iops_target 51 } 52 }) 53 54 self.qsd.cmd('blockdev-add', { 55 'node-name': 'target', 56 'driver': 'throttle', 57 'throttle-group': 'thrgr-target', 58 'file': { 59 'driver': iotests.imgfmt, 60 'file': { 61 'driver': 'file', 62 'filename': target_img 63 } 64 } 65 }) 66 67 self.qsd.cmd('block-export-add', { 68 'id': 'exp0', 69 'type': 'nbd', 70 'node-name': 'target', 71 'writable': True 72 }) 73 74 self.vm = iotests.VM() 75 self.vm.add_args('-drive', 76 f'file={source_img},if=none,format={iotests.imgfmt},' 77 f'iops_wr={iops_source},' 78 f'iops_wr_max={iops_source},' 79 'id=source') 80 self.vm.launch() 81 82 self.vm.cmd('blockdev-add', { 83 'node-name': 'target', 84 'driver': 'nbd', 85 'export': 'target', 86 'server': { 87 'type': 'unix', 88 'path': nbd_sock 89 } 90 }) 91 92 93 def tearDown(self): 94 self.vm.shutdown() 95 self.qsd.stop() 96 self.check_qemu_io_errors() 97 self.check_images_identical() 98 os.remove(source_img) 99 os.remove(target_img) 100 101 # Once the VM is shut down we can parse the log and see if qemu-io ran 102 # without errors. 103 def check_qemu_io_errors(self): 104 self.assertFalse(self.vm.is_running()) 105 log = self.vm.get_log() 106 for line in log.split("\n"): 107 assert not line.startswith("Pattern verification failed") 108 109 def check_images_identical(self): 110 qemu_img('compare', '-f', iotests.imgfmt, source_img, target_img) 111 112 def start_mirror(self): 113 self.vm.cmd('blockdev-mirror', 114 job_id='mirror', 115 device='source', 116 target='target', 117 filter_node_name='mirror-top', 118 sync='full', 119 copy_mode='background') 120 121 def test_background_to_active(self): 122 self.vm.hmp_qemu_io('source', f'write 0 {image_size}') 123 self.vm.hmp_qemu_io('target', f'write 0 {image_size}') 124 125 self.start_mirror() 126 127 result = self.vm.cmd('query-block-jobs') 128 assert not result[0]['actively-synced'] 129 130 self.vm.event_wait('BLOCK_JOB_READY') 131 132 result = self.vm.cmd('query-block-jobs') 133 assert not result[0]['actively-synced'] 134 135 # Start some background requests. 136 reqs = 4 * iops_source 137 req_size = image_size // reqs 138 for i in range(0, reqs): 139 req = f'aio_write -P 7 {req_size * i} {req_size}' 140 self.vm.hmp_qemu_io('source', req) 141 142 # Wait for the first few requests. 143 time.sleep(1) 144 self.vm.qtest(f'clock_step {1 * 1000 * 1000 * 1000}') 145 146 result = self.vm.cmd('query-block-jobs') 147 # There should've been new requests. 148 assert result[0]['len'] > image_size 149 # To verify later that not all requests were completed at this point. 150 len_before_change = result[0]['len'] 151 152 # Change the copy mode while requests are happening. 153 self.vm.cmd('block-job-change', 154 id='mirror', 155 type='mirror', 156 copy_mode='write-blocking') 157 158 # Wait until image is actively synced. 159 while True: 160 time.sleep(0.1) 161 self.vm.qtest(f'clock_step {100 * 1000 * 1000}') 162 result = self.vm.cmd('query-block-jobs') 163 if result[0]['actively-synced']: 164 break 165 166 # Because of throttling, not all requests should have been completed 167 # above. 168 result = self.vm.cmd('query-block-jobs') 169 assert result[0]['len'] > len_before_change 170 171 # Issue enough requests for a few seconds only touching the first half 172 # of the image. 173 reqs = 4 * iops_target 174 req_size = image_size // 2 // reqs 175 for i in range(0, reqs): 176 req = f'aio_write -P 19 {req_size * i} {req_size}' 177 self.vm.hmp_qemu_io('source', req) 178 179 # Now issue a synchronous write in the second half of the image and 180 # immediately verify that it was written to the target too. This would 181 # fail without switching the copy mode. Note that this only produces a 182 # log line and the actual checking happens during tearDown(). 183 req_args = f'-P 37 {3 * (image_size // 4)} {req_size}' 184 self.vm.hmp_qemu_io('source', f'write {req_args}') 185 self.vm.hmp_qemu_io('target', f'read {req_args}') 186 187 self.vm.cmd('block-job-cancel', device='mirror') 188 while len(self.vm.cmd('query-block-jobs')) > 0: 189 time.sleep(0.1) 190 191if __name__ == '__main__': 192 iotests.main(supported_fmts=['qcow2', 'raw'], 193 supported_protocols=['file']) 194