1#!/usr/bin/env python3 2# group: rw migration 3# 4# Tests for dirty bitmaps migration. 5# 6# Copyright (c) 2016-2017 Virtuozzo International GmbH. All rights reserved. 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 itertools 23import operator 24import os 25import re 26 27import iotests 28from iotests import qemu_img, qemu_img_create, Timeout 29 30 31disk_a = os.path.join(iotests.test_dir, 'disk_a') 32disk_b = os.path.join(iotests.test_dir, 'disk_b') 33base_a = os.path.join(iotests.test_dir, 'base_a') 34size = '1M' 35mig_file = os.path.join(iotests.test_dir, 'mig_file') 36mig_cmd = 'exec: cat > ' + mig_file 37incoming_cmd = 'exec: cat ' + mig_file 38 39 40def get_bitmap_hash(vm): 41 result = vm.qmp('x-debug-block-dirty-bitmap-sha256', 42 node='drive0', name='bitmap0') 43 return result['return']['sha256'] 44 45 46class TestDirtyBitmapMigration(iotests.QMPTestCase): 47 def tearDown(self): 48 self.vm_a.shutdown() 49 self.vm_b.shutdown() 50 os.remove(disk_a) 51 os.remove(disk_b) 52 os.remove(mig_file) 53 54 def setUp(self): 55 qemu_img('create', '-f', iotests.imgfmt, disk_a, size) 56 qemu_img('create', '-f', iotests.imgfmt, disk_b, size) 57 58 self.vm_a = iotests.VM(path_suffix='a').add_drive(disk_a) 59 self.vm_a.launch() 60 61 self.vm_b = iotests.VM(path_suffix='b') 62 63 def add_bitmap(self, vm, granularity, persistent): 64 params = {'node': 'drive0', 65 'name': 'bitmap0', 66 'granularity': granularity} 67 if persistent: 68 params['persistent'] = True 69 70 vm.cmd('block-dirty-bitmap-add', params) 71 72 def check_bitmap(self, vm, sha256): 73 result = vm.qmp('x-debug-block-dirty-bitmap-sha256', 74 node='drive0', name='bitmap0') 75 if sha256: 76 self.assert_qmp(result, 'return/sha256', sha256) 77 else: 78 self.assert_qmp(result, 'error/desc', 79 "Dirty bitmap 'bitmap0' not found") 80 81 def do_test_migration_resume_source(self, persistent, migrate_bitmaps): 82 granularity = 512 83 84 # regions = ((start, count), ...) 85 regions = ((0, 0x10000), 86 (0xf0000, 0x10000), 87 (0xa0201, 0x1000)) 88 89 mig_caps = [{'capability': 'events', 'state': True}] 90 if migrate_bitmaps: 91 mig_caps.append({'capability': 'dirty-bitmaps', 'state': True}) 92 93 self.vm_a.cmd('migrate-set-capabilities', 94 capabilities=mig_caps) 95 96 self.add_bitmap(self.vm_a, granularity, persistent) 97 for r in regions: 98 self.vm_a.hmp_qemu_io('drive0', 'write %d %d' % r) 99 sha256 = get_bitmap_hash(self.vm_a) 100 101 self.vm_a.cmd('migrate', uri=mig_cmd) 102 while True: 103 event = self.vm_a.event_wait('MIGRATION') 104 if event['data']['status'] == 'completed': 105 break 106 while True: 107 result = self.vm_a.qmp('query-status') 108 if result['return']['status'] == 'postmigrate': 109 break 110 111 # test that bitmap is still here 112 removed = (not migrate_bitmaps) and persistent 113 self.check_bitmap(self.vm_a, False if removed else sha256) 114 115 self.vm_a.cmd('cont') 116 117 # test that bitmap is still here after invalidation 118 self.check_bitmap(self.vm_a, sha256) 119 120 # shutdown and check that invalidation didn't fail 121 self.vm_a.shutdown() 122 123 # catch 'Could not reopen qcow2 layer: Bitmap already exists' 124 # possible error 125 log = self.vm_a.get_log() 126 log = re.sub(r'^\[I \d+\.\d+\] OPENED\n', '', log) 127 log = re.sub(r'^(wrote .* bytes at offset .*\n.*KiB.*ops.*sec.*\n){3}', 128 '', log) 129 log = re.sub(r'\[I \+\d+\.\d+\] CLOSED\n?$', '', log) 130 self.assertEqual(log, '') 131 132 # test that bitmap is still persistent 133 self.vm_a.launch() 134 self.check_bitmap(self.vm_a, sha256 if persistent else False) 135 136 def do_test_migration(self, persistent, migrate_bitmaps, online, 137 shared_storage, pre_shutdown): 138 granularity = 512 139 140 # regions = ((start, count), ...) 141 regions = ((0, 0x10000), 142 (0xf0000, 0x10000), 143 (0xa0201, 0x1000)) 144 145 should_migrate = \ 146 (migrate_bitmaps and (persistent or not pre_shutdown)) or \ 147 (persistent and shared_storage) 148 mig_caps = [{'capability': 'events', 'state': True}] 149 if migrate_bitmaps: 150 mig_caps.append({'capability': 'dirty-bitmaps', 'state': True}) 151 152 self.vm_b.add_incoming(incoming_cmd if online else "defer") 153 self.vm_b.add_drive(disk_a if shared_storage else disk_b) 154 155 if online: 156 os.mkfifo(mig_file) 157 self.vm_b.launch() 158 self.vm_b.cmd('migrate-set-capabilities', 159 capabilities=mig_caps) 160 161 self.add_bitmap(self.vm_a, granularity, persistent) 162 for r in regions: 163 self.vm_a.hmp_qemu_io('drive0', 'write %d %d' % r) 164 sha256 = get_bitmap_hash(self.vm_a) 165 166 if pre_shutdown: 167 self.vm_a.shutdown() 168 self.vm_a.launch() 169 170 self.vm_a.cmd('migrate-set-capabilities', 171 capabilities=mig_caps) 172 173 self.vm_a.cmd('migrate', uri=mig_cmd) 174 while True: 175 event = self.vm_a.event_wait('MIGRATION') 176 if event['data']['status'] == 'completed': 177 break 178 179 if not online: 180 self.vm_a.shutdown() 181 self.vm_b.launch() 182 self.vm_b.cmd('migrate-set-capabilities', 183 capabilities=mig_caps) 184 self.vm_b.cmd('migrate-incoming', uri=incoming_cmd) 185 186 while True: 187 event = self.vm_b.event_wait('MIGRATION') 188 if event['data']['status'] == 'completed': 189 break 190 191 self.check_bitmap(self.vm_b, sha256 if should_migrate else False) 192 193 if should_migrate: 194 self.vm_b.shutdown() 195 196 # catch 'Could not reopen qcow2 layer: Bitmap already exists' 197 # possible error 198 log = self.vm_b.get_log() 199 log = re.sub(r'^\[I \d+\.\d+\] OPENED\n', '', log) 200 log = re.sub(r'\[I \+\d+\.\d+\] CLOSED\n?$', '', log) 201 self.assertEqual(log, '') 202 203 # recreate vm_b, as we don't want -incoming option (this will lead 204 # to "cat" process left alive after test finish) 205 self.vm_b = iotests.VM(path_suffix='b') 206 self.vm_b.add_drive(disk_a if shared_storage else disk_b) 207 self.vm_b.launch() 208 self.check_bitmap(self.vm_b, sha256 if persistent else False) 209 210 211def inject_test_case(klass, suffix, method, *args, **kwargs): 212 mc = operator.methodcaller(method, *args, **kwargs) 213 # We want to add a function attribute to `klass`, so that it is 214 # correctly converted to a method on instantiation. The 215 # methodcaller object `mc` is a callable, not a function, so we 216 # need the lambda to turn it into a function. 217 # pylint: disable=unnecessary-lambda 218 setattr(klass, 'test_' + method + suffix, lambda self: mc(self)) 219 220 221class TestDirtyBitmapBackingMigration(iotests.QMPTestCase): 222 def setUp(self): 223 qemu_img_create('-f', iotests.imgfmt, base_a, size) 224 qemu_img_create('-f', iotests.imgfmt, '-F', iotests.imgfmt, 225 '-b', base_a, disk_a, size) 226 227 for f in (disk_a, base_a): 228 qemu_img('bitmap', '--add', f, 'bmap0') 229 230 blockdev = { 231 'node-name': 'node0', 232 'driver': iotests.imgfmt, 233 'file': { 234 'driver': 'file', 235 'filename': disk_a 236 }, 237 'backing': { 238 'node-name': 'node0-base', 239 'driver': iotests.imgfmt, 240 'file': { 241 'driver': 'file', 242 'filename': base_a 243 } 244 } 245 } 246 247 self.vm = iotests.VM() 248 self.vm.launch() 249 250 self.vm.cmd('blockdev-add', blockdev) 251 252 # Check that the bitmaps are there 253 nodes = self.vm.qmp('query-named-block-nodes', flat=True)['return'] 254 for node in nodes: 255 if 'node0' in node['node-name']: 256 self.assert_qmp(node, 'dirty-bitmaps[0]/name', 'bmap0') 257 258 caps = [{'capability': 'events', 'state': True}] 259 self.vm.cmd('migrate-set-capabilities', capabilities=caps) 260 261 def tearDown(self): 262 self.vm.shutdown() 263 for f in (disk_a, base_a): 264 os.remove(f) 265 266 def test_cont_on_source(self): 267 """ 268 Continue the source after migration. 269 """ 270 self.vm.cmd('migrate', uri='exec: cat > /dev/null') 271 272 with Timeout(10, 'Migration timeout'): 273 self.vm.wait_migration('postmigrate') 274 275 self.vm.cmd('cont') 276 277 278def main() -> None: 279 for cmb in list(itertools.product((True, False), repeat=5)): 280 name = ('_' if cmb[0] else '_not_') + 'persistent_' 281 name += ('_' if cmb[1] else '_not_') + 'migbitmap_' 282 name += '_online' if cmb[2] else '_offline' 283 name += '_shared' if cmb[3] else '_nonshared' 284 if cmb[4]: 285 name += '__pre_shutdown' 286 287 inject_test_case(TestDirtyBitmapMigration, name, 'do_test_migration', 288 *list(cmb)) 289 290 for cmb in list(itertools.product((True, False), repeat=2)): 291 name = ('_' if cmb[0] else '_not_') + 'persistent_' 292 name += ('_' if cmb[1] else '_not_') + 'migbitmap' 293 294 inject_test_case(TestDirtyBitmapMigration, name, 295 'do_test_migration_resume_source', *list(cmb)) 296 297 iotests.main( 298 supported_fmts=['qcow2'], 299 supported_protocols=['file'], 300 unsupported_imgopts=['compat'] 301 ) 302 303 304if __name__ == '__main__': 305 main() 306