1#!/usr/bin/env python3 2# group: migration 3# 4# Copyright (C) 2020 Red Hat, Inc. 5# 6# Tests for dirty bitmaps migration with node aliases 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 random 24import re 25from typing import Dict, List, Optional, Union 26 27import iotests 28 29# Import qemu after iotests.py has amended sys.path 30# pylint: disable=wrong-import-order 31import qemu 32 33BlockBitmapMapping = List[Dict[str, Union[str, List[Dict[str, str]]]]] 34 35mig_sock = os.path.join(iotests.sock_dir, 'mig_sock') 36 37 38class TestDirtyBitmapMigration(iotests.QMPTestCase): 39 src_node_name: str = '' 40 dst_node_name: str = '' 41 src_bmap_name: str = '' 42 dst_bmap_name: str = '' 43 44 def setUp(self) -> None: 45 self.vm_a = iotests.VM(path_suffix='-a') 46 self.vm_a.add_blockdev(f'node-name={self.src_node_name},' 47 'driver=null-co') 48 self.vm_a.launch() 49 50 self.vm_b = iotests.VM(path_suffix='-b') 51 self.vm_b.add_blockdev(f'node-name={self.dst_node_name},' 52 'driver=null-co') 53 self.vm_b.add_incoming(f'unix:{mig_sock}') 54 self.vm_b.launch() 55 56 result = self.vm_a.qmp('block-dirty-bitmap-add', 57 node=self.src_node_name, 58 name=self.src_bmap_name) 59 self.assert_qmp(result, 'return', {}) 60 61 # Dirty some random megabytes 62 for _ in range(9): 63 mb_ofs = random.randrange(1024) 64 self.vm_a.hmp_qemu_io(self.src_node_name, f'discard {mb_ofs}M 1M') 65 66 result = self.vm_a.qmp('x-debug-block-dirty-bitmap-sha256', 67 node=self.src_node_name, 68 name=self.src_bmap_name) 69 self.bitmap_hash_reference = result['return']['sha256'] 70 71 caps = [{'capability': name, 'state': True} 72 for name in ('dirty-bitmaps', 'events')] 73 74 for vm in (self.vm_a, self.vm_b): 75 result = vm.qmp('migrate-set-capabilities', capabilities=caps) 76 self.assert_qmp(result, 'return', {}) 77 78 def tearDown(self) -> None: 79 self.vm_a.shutdown() 80 self.vm_b.shutdown() 81 try: 82 os.remove(mig_sock) 83 except OSError: 84 pass 85 86 def check_bitmap(self, bitmap_name_valid: bool) -> None: 87 result = self.vm_b.qmp('x-debug-block-dirty-bitmap-sha256', 88 node=self.dst_node_name, 89 name=self.dst_bmap_name) 90 if bitmap_name_valid: 91 self.assert_qmp(result, 'return/sha256', 92 self.bitmap_hash_reference) 93 else: 94 self.assert_qmp(result, 'error/desc', 95 f"Dirty bitmap '{self.dst_bmap_name}' not found") 96 97 def migrate(self, bitmap_name_valid: bool = True, 98 migration_success: bool = True) -> None: 99 result = self.vm_a.qmp('migrate', uri=f'unix:{mig_sock}') 100 self.assert_qmp(result, 'return', {}) 101 102 with iotests.Timeout(5, 'Timeout waiting for migration to complete'): 103 self.assertEqual(self.vm_a.wait_migration('postmigrate'), 104 migration_success) 105 self.assertEqual(self.vm_b.wait_migration('running'), 106 migration_success) 107 108 if migration_success: 109 self.check_bitmap(bitmap_name_valid) 110 111 def verify_dest_error(self, msg: Optional[str]) -> None: 112 """ 113 Check whether the given error message is present in vm_b's log. 114 (vm_b is shut down to do so.) 115 If @msg is None, check that there has not been any error. 116 """ 117 self.vm_b.shutdown() 118 119 log = self.vm_b.get_log() 120 assert log is not None # Loaded after shutdown 121 122 if msg is None: 123 self.assertNotIn('qemu-system-', log) 124 else: 125 self.assertIn(msg, log) 126 127 @staticmethod 128 def mapping(node_name: str, node_alias: str, 129 bitmap_name: str, bitmap_alias: str) -> BlockBitmapMapping: 130 return [{ 131 'node-name': node_name, 132 'alias': node_alias, 133 'bitmaps': [{ 134 'name': bitmap_name, 135 'alias': bitmap_alias 136 }] 137 }] 138 139 def set_mapping(self, vm: iotests.VM, mapping: BlockBitmapMapping, 140 error: Optional[str] = None) -> None: 141 """ 142 Invoke migrate-set-parameters on @vm to set the given @mapping. 143 Check for success if @error is None, or verify the error message 144 if it is not. 145 On success, verify that "info migrate_parameters" on HMP returns 146 our mapping. (Just to check its formatting code.) 147 """ 148 result = vm.qmp('migrate-set-parameters', 149 block_bitmap_mapping=mapping) 150 151 if error is None: 152 self.assert_qmp(result, 'return', {}) 153 154 result = vm.qmp('human-monitor-command', 155 command_line='info migrate_parameters') 156 157 m = re.search(r'^block-bitmap-mapping:\r?(\n .*)*\n', 158 result['return'], flags=re.MULTILINE) 159 hmp_mapping = m.group(0).replace('\r', '') if m else None 160 161 self.assertEqual(hmp_mapping, self.to_hmp_mapping(mapping)) 162 else: 163 self.assert_qmp(result, 'error/desc', error) 164 165 @staticmethod 166 def to_hmp_mapping(mapping: BlockBitmapMapping) -> str: 167 result = 'block-bitmap-mapping:\n' 168 169 for node in mapping: 170 result += f" '{node['node-name']}' -> '{node['alias']}'\n" 171 172 assert isinstance(node['bitmaps'], list) 173 for bitmap in node['bitmaps']: 174 result += f" '{bitmap['name']}' -> '{bitmap['alias']}'\n" 175 176 return result 177 178 179class TestAliasMigration(TestDirtyBitmapMigration): 180 src_node_name = 'node0' 181 dst_node_name = 'node0' 182 src_bmap_name = 'bmap0' 183 dst_bmap_name = 'bmap0' 184 185 def test_migration_without_alias(self) -> None: 186 self.migrate(self.src_node_name == self.dst_node_name and 187 self.src_bmap_name == self.dst_bmap_name) 188 189 # Check for error message on the destination 190 if self.src_node_name != self.dst_node_name: 191 self.verify_dest_error(f"Cannot find " 192 f"device={self.src_node_name} nor " 193 f"node_name={self.src_node_name}") 194 else: 195 self.verify_dest_error(None) 196 197 def test_alias_on_src_migration(self) -> None: 198 mapping = self.mapping(self.src_node_name, self.dst_node_name, 199 self.src_bmap_name, self.dst_bmap_name) 200 201 self.set_mapping(self.vm_a, mapping) 202 self.migrate() 203 self.verify_dest_error(None) 204 205 def test_alias_on_dst_migration(self) -> None: 206 mapping = self.mapping(self.dst_node_name, self.src_node_name, 207 self.dst_bmap_name, self.src_bmap_name) 208 209 self.set_mapping(self.vm_b, mapping) 210 self.migrate() 211 self.verify_dest_error(None) 212 213 def test_alias_on_both_migration(self) -> None: 214 src_map = self.mapping(self.src_node_name, 'node-alias', 215 self.src_bmap_name, 'bmap-alias') 216 217 dst_map = self.mapping(self.dst_node_name, 'node-alias', 218 self.dst_bmap_name, 'bmap-alias') 219 220 self.set_mapping(self.vm_a, src_map) 221 self.set_mapping(self.vm_b, dst_map) 222 self.migrate() 223 self.verify_dest_error(None) 224 225 226class TestNodeAliasMigration(TestAliasMigration): 227 src_node_name = 'node-src' 228 dst_node_name = 'node-dst' 229 230 231class TestBitmapAliasMigration(TestAliasMigration): 232 src_bmap_name = 'bmap-src' 233 dst_bmap_name = 'bmap-dst' 234 235 236class TestFullAliasMigration(TestAliasMigration): 237 src_node_name = 'node-src' 238 dst_node_name = 'node-dst' 239 src_bmap_name = 'bmap-src' 240 dst_bmap_name = 'bmap-dst' 241 242 243class TestLongBitmapNames(TestAliasMigration): 244 # Giving long bitmap names is OK, as long as there is a short alias for 245 # migration 246 src_bmap_name = 'a' * 512 247 dst_bmap_name = 'b' * 512 248 249 # Skip all tests that do not use the intermediate alias 250 def test_migration_without_alias(self) -> None: 251 pass 252 253 def test_alias_on_src_migration(self) -> None: 254 pass 255 256 def test_alias_on_dst_migration(self) -> None: 257 pass 258 259 260class TestBlockBitmapMappingErrors(TestDirtyBitmapMigration): 261 src_node_name = 'node0' 262 dst_node_name = 'node0' 263 src_bmap_name = 'bmap0' 264 dst_bmap_name = 'bmap0' 265 266 """ 267 Note that mapping nodes or bitmaps that do not exist is not an error. 268 """ 269 270 def test_non_injective_node_mapping(self) -> None: 271 mapping: BlockBitmapMapping = [ 272 { 273 'node-name': 'node0', 274 'alias': 'common-alias', 275 'bitmaps': [{ 276 'name': 'bmap0', 277 'alias': 'bmap-alias0' 278 }] 279 }, 280 { 281 'node-name': 'node1', 282 'alias': 'common-alias', 283 'bitmaps': [{ 284 'name': 'bmap1', 285 'alias': 'bmap-alias1' 286 }] 287 } 288 ] 289 290 self.set_mapping(self.vm_a, mapping, 291 "Invalid mapping given for block-bitmap-mapping: " 292 "The node alias 'common-alias' is used twice") 293 294 def test_non_injective_bitmap_mapping(self) -> None: 295 mapping: BlockBitmapMapping = [{ 296 'node-name': 'node0', 297 'alias': 'node-alias0', 298 'bitmaps': [ 299 { 300 'name': 'bmap0', 301 'alias': 'common-alias' 302 }, 303 { 304 'name': 'bmap1', 305 'alias': 'common-alias' 306 } 307 ] 308 }] 309 310 self.set_mapping(self.vm_a, mapping, 311 "Invalid mapping given for block-bitmap-mapping: " 312 "The bitmap alias 'node-alias0'/'common-alias' is " 313 "used twice") 314 315 def test_ambiguous_node_mapping(self) -> None: 316 mapping: BlockBitmapMapping = [ 317 { 318 'node-name': 'node0', 319 'alias': 'node-alias0', 320 'bitmaps': [{ 321 'name': 'bmap0', 322 'alias': 'bmap-alias0' 323 }] 324 }, 325 { 326 'node-name': 'node0', 327 'alias': 'node-alias1', 328 'bitmaps': [{ 329 'name': 'bmap0', 330 'alias': 'bmap-alias0' 331 }] 332 } 333 ] 334 335 self.set_mapping(self.vm_a, mapping, 336 "Invalid mapping given for block-bitmap-mapping: " 337 "The node name 'node0' is mapped twice") 338 339 def test_ambiguous_bitmap_mapping(self) -> None: 340 mapping: BlockBitmapMapping = [{ 341 'node-name': 'node0', 342 'alias': 'node-alias0', 343 'bitmaps': [ 344 { 345 'name': 'bmap0', 346 'alias': 'bmap-alias0' 347 }, 348 { 349 'name': 'bmap0', 350 'alias': 'bmap-alias1' 351 } 352 ] 353 }] 354 355 self.set_mapping(self.vm_a, mapping, 356 "Invalid mapping given for block-bitmap-mapping: " 357 "The bitmap 'node0'/'bmap0' is mapped twice") 358 359 def test_migratee_node_is_not_mapped_on_src(self) -> None: 360 self.set_mapping(self.vm_a, []) 361 # Should just ignore all bitmaps on unmapped nodes 362 self.migrate(False) 363 self.verify_dest_error(None) 364 365 def test_migratee_node_is_not_mapped_on_dst(self) -> None: 366 self.set_mapping(self.vm_b, []) 367 self.migrate(False) 368 self.verify_dest_error(f"Unknown node alias '{self.src_node_name}'") 369 370 def test_migratee_bitmap_is_not_mapped_on_src(self) -> None: 371 mapping: BlockBitmapMapping = [{ 372 'node-name': self.src_node_name, 373 'alias': self.dst_node_name, 374 'bitmaps': [] 375 }] 376 377 self.set_mapping(self.vm_a, mapping) 378 # Should just ignore all unmapped bitmaps 379 self.migrate(False) 380 self.verify_dest_error(None) 381 382 def test_migratee_bitmap_is_not_mapped_on_dst(self) -> None: 383 mapping: BlockBitmapMapping = [{ 384 'node-name': self.dst_node_name, 385 'alias': self.src_node_name, 386 'bitmaps': [] 387 }] 388 389 self.set_mapping(self.vm_b, mapping) 390 self.migrate(False) 391 self.verify_dest_error(f"Unknown bitmap alias " 392 f"'{self.src_bmap_name}' " 393 f"on node '{self.dst_node_name}' " 394 f"(alias '{self.src_node_name}')") 395 396 def test_unused_mapping_on_dst(self) -> None: 397 # Let the source not send any bitmaps 398 self.set_mapping(self.vm_a, []) 399 400 # Establish some mapping on the destination 401 self.set_mapping(self.vm_b, []) 402 403 # The fact that there is a mapping on B without any bitmaps 404 # being received should be fine, not fatal 405 self.migrate(False) 406 self.verify_dest_error(None) 407 408 def test_non_wellformed_node_alias(self) -> None: 409 alias = '123-foo' 410 411 mapping: BlockBitmapMapping = [{ 412 'node-name': self.src_node_name, 413 'alias': alias, 414 'bitmaps': [] 415 }] 416 417 self.set_mapping(self.vm_a, mapping, 418 f"Invalid mapping given for block-bitmap-mapping: " 419 f"The node alias '{alias}' is not well-formed") 420 421 def test_node_alias_too_long(self) -> None: 422 alias = 'a' * 256 423 424 mapping: BlockBitmapMapping = [{ 425 'node-name': self.src_node_name, 426 'alias': alias, 427 'bitmaps': [] 428 }] 429 430 self.set_mapping(self.vm_a, mapping, 431 f"Invalid mapping given for block-bitmap-mapping: " 432 f"The node alias '{alias}' is longer than 255 bytes") 433 434 def test_bitmap_alias_too_long(self) -> None: 435 alias = 'a' * 256 436 437 mapping = self.mapping(self.src_node_name, self.dst_node_name, 438 self.src_bmap_name, alias) 439 440 self.set_mapping(self.vm_a, mapping, 441 f"Invalid mapping given for block-bitmap-mapping: " 442 f"The bitmap alias '{alias}' is longer than 255 " 443 f"bytes") 444 445 def test_bitmap_name_too_long(self) -> None: 446 name = 'a' * 256 447 448 result = self.vm_a.qmp('block-dirty-bitmap-add', 449 node=self.src_node_name, 450 name=name) 451 self.assert_qmp(result, 'return', {}) 452 453 self.migrate(False, False) 454 455 # Check for the error in the source's log 456 self.vm_a.shutdown() 457 458 log = self.vm_a.get_log() 459 assert log is not None # Loaded after shutdown 460 461 self.assertIn(f"Cannot migrate bitmap '{name}' on node " 462 f"'{self.src_node_name}': Name is longer than 255 bytes", 463 log) 464 465 # Expect abnormal shutdown of the destination VM because of 466 # the failed migration 467 try: 468 self.vm_b.shutdown() 469 except qemu.machine.AbnormalShutdown: 470 pass 471 472 def test_aliased_bitmap_name_too_long(self) -> None: 473 # Longer than the maximum for bitmap names 474 self.dst_bmap_name = 'a' * 1024 475 476 mapping = self.mapping(self.dst_node_name, self.src_node_name, 477 self.dst_bmap_name, self.src_bmap_name) 478 479 # We would have to create this bitmap during migration, and 480 # that would fail, because the name is too long. Better to 481 # catch it early. 482 self.set_mapping(self.vm_b, mapping, 483 f"Invalid mapping given for block-bitmap-mapping: " 484 f"The bitmap name '{self.dst_bmap_name}' is longer " 485 f"than 1023 bytes") 486 487 def test_node_name_too_long(self) -> None: 488 # Longer than the maximum for node names 489 self.dst_node_name = 'a' * 32 490 491 mapping = self.mapping(self.dst_node_name, self.src_node_name, 492 self.dst_bmap_name, self.src_bmap_name) 493 494 # During migration, this would appear simply as a node that 495 # cannot be found. Still better to catch impossible node 496 # names early (similar to test_non_wellformed_node_alias). 497 self.set_mapping(self.vm_b, mapping, 498 f"Invalid mapping given for block-bitmap-mapping: " 499 f"The node name '{self.dst_node_name}' is longer " 500 f"than 31 bytes") 501 502 503class TestCrossAliasMigration(TestDirtyBitmapMigration): 504 """ 505 Swap aliases, both to see that qemu does not get confused, and 506 that we can migrate multiple things at once. 507 508 So we migrate this: 509 node-a.bmap-a -> node-b.bmap-b 510 node-a.bmap-b -> node-b.bmap-a 511 node-b.bmap-a -> node-a.bmap-b 512 node-b.bmap-b -> node-a.bmap-a 513 """ 514 515 src_node_name = 'node-a' 516 dst_node_name = 'node-b' 517 src_bmap_name = 'bmap-a' 518 dst_bmap_name = 'bmap-b' 519 520 def setUp(self) -> None: 521 TestDirtyBitmapMigration.setUp(self) 522 523 # Now create another block device and let both have two bitmaps each 524 result = self.vm_a.qmp('blockdev-add', 525 node_name='node-b', driver='null-co') 526 self.assert_qmp(result, 'return', {}) 527 528 result = self.vm_b.qmp('blockdev-add', 529 node_name='node-a', driver='null-co') 530 self.assert_qmp(result, 'return', {}) 531 532 bmaps_to_add = (('node-a', 'bmap-b'), 533 ('node-b', 'bmap-a'), 534 ('node-b', 'bmap-b')) 535 536 for (node, bmap) in bmaps_to_add: 537 result = self.vm_a.qmp('block-dirty-bitmap-add', 538 node=node, name=bmap) 539 self.assert_qmp(result, 'return', {}) 540 541 @staticmethod 542 def cross_mapping() -> BlockBitmapMapping: 543 return [ 544 { 545 'node-name': 'node-a', 546 'alias': 'node-b', 547 'bitmaps': [ 548 { 549 'name': 'bmap-a', 550 'alias': 'bmap-b' 551 }, 552 { 553 'name': 'bmap-b', 554 'alias': 'bmap-a' 555 } 556 ] 557 }, 558 { 559 'node-name': 'node-b', 560 'alias': 'node-a', 561 'bitmaps': [ 562 { 563 'name': 'bmap-b', 564 'alias': 'bmap-a' 565 }, 566 { 567 'name': 'bmap-a', 568 'alias': 'bmap-b' 569 } 570 ] 571 } 572 ] 573 574 def verify_dest_has_all_bitmaps(self) -> None: 575 bitmaps = self.vm_b.query_bitmaps() 576 577 # Extract and sort bitmap names 578 for node in bitmaps: 579 bitmaps[node] = sorted((bmap['name'] for bmap in bitmaps[node])) 580 581 self.assertEqual(bitmaps, 582 {'node-a': ['bmap-a', 'bmap-b'], 583 'node-b': ['bmap-a', 'bmap-b']}) 584 585 def test_alias_on_src(self) -> None: 586 self.set_mapping(self.vm_a, self.cross_mapping()) 587 588 # Checks that node-a.bmap-a was migrated to node-b.bmap-b, and 589 # that is enough 590 self.migrate() 591 self.verify_dest_has_all_bitmaps() 592 self.verify_dest_error(None) 593 594 def test_alias_on_dst(self) -> None: 595 self.set_mapping(self.vm_b, self.cross_mapping()) 596 597 # Checks that node-a.bmap-a was migrated to node-b.bmap-b, and 598 # that is enough 599 self.migrate() 600 self.verify_dest_has_all_bitmaps() 601 self.verify_dest_error(None) 602 603 604if __name__ == '__main__': 605 iotests.main(supported_protocols=['file']) 606