| #!/usr/bin/env python3 |
| # group: rw migration |
| # |
| # Tests for dirty bitmaps migration. |
| # |
| # Copyright (c) 2016-2017 Virtuozzo International GmbH. All rights reserved. |
| # |
| # 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 itertools |
| import operator |
| import os |
| import re |
| |
| import iotests |
| from iotests import qemu_img, qemu_img_create, Timeout |
| |
| |
| disk_a = os.path.join(iotests.test_dir, 'disk_a') |
| disk_b = os.path.join(iotests.test_dir, 'disk_b') |
| base_a = os.path.join(iotests.test_dir, 'base_a') |
| size = '1M' |
| mig_file = os.path.join(iotests.test_dir, 'mig_file') |
| mig_cmd = 'exec: cat > ' + mig_file |
| incoming_cmd = 'exec: cat ' + mig_file |
| |
| |
| def get_bitmap_hash(vm): |
| result = vm.qmp('x-debug-block-dirty-bitmap-sha256', |
| node='drive0', name='bitmap0') |
| return result['return']['sha256'] |
| |
| |
| class TestDirtyBitmapMigration(iotests.QMPTestCase): |
| def tearDown(self): |
| self.vm_a.shutdown() |
| self.vm_b.shutdown() |
| os.remove(disk_a) |
| os.remove(disk_b) |
| os.remove(mig_file) |
| |
| def setUp(self): |
| qemu_img('create', '-f', iotests.imgfmt, disk_a, size) |
| qemu_img('create', '-f', iotests.imgfmt, disk_b, size) |
| |
| self.vm_a = iotests.VM(path_suffix='a').add_drive(disk_a) |
| self.vm_a.launch() |
| |
| self.vm_b = iotests.VM(path_suffix='b') |
| |
| def add_bitmap(self, vm, granularity, persistent): |
| params = {'node': 'drive0', |
| 'name': 'bitmap0', |
| 'granularity': granularity} |
| if persistent: |
| params['persistent'] = True |
| |
| result = vm.qmp('block-dirty-bitmap-add', **params) |
| self.assert_qmp(result, 'return', {}) |
| |
| def check_bitmap(self, vm, sha256): |
| result = vm.qmp('x-debug-block-dirty-bitmap-sha256', |
| node='drive0', name='bitmap0') |
| if sha256: |
| self.assert_qmp(result, 'return/sha256', sha256) |
| else: |
| self.assert_qmp(result, 'error/desc', |
| "Dirty bitmap 'bitmap0' not found") |
| |
| def do_test_migration_resume_source(self, persistent, migrate_bitmaps): |
| granularity = 512 |
| |
| # regions = ((start, count), ...) |
| regions = ((0, 0x10000), |
| (0xf0000, 0x10000), |
| (0xa0201, 0x1000)) |
| |
| mig_caps = [{'capability': 'events', 'state': True}] |
| if migrate_bitmaps: |
| mig_caps.append({'capability': 'dirty-bitmaps', 'state': True}) |
| |
| result = self.vm_a.qmp('migrate-set-capabilities', |
| capabilities=mig_caps) |
| self.assert_qmp(result, 'return', {}) |
| |
| self.add_bitmap(self.vm_a, granularity, persistent) |
| for r in regions: |
| self.vm_a.hmp_qemu_io('drive0', 'write %d %d' % r) |
| sha256 = get_bitmap_hash(self.vm_a) |
| |
| result = self.vm_a.qmp('migrate', uri=mig_cmd) |
| while True: |
| event = self.vm_a.event_wait('MIGRATION') |
| if event['data']['status'] == 'completed': |
| break |
| while True: |
| result = self.vm_a.qmp('query-status') |
| if result['return']['status'] == 'postmigrate': |
| break |
| |
| # test that bitmap is still here |
| removed = (not migrate_bitmaps) and persistent |
| self.check_bitmap(self.vm_a, False if removed else sha256) |
| |
| result = self.vm_a.qmp('cont') |
| self.assert_qmp(result, 'return', {}) |
| |
| # test that bitmap is still here after invalidation |
| self.check_bitmap(self.vm_a, sha256) |
| |
| # shutdown and check that invalidation didn't fail |
| self.vm_a.shutdown() |
| |
| # catch 'Could not reopen qcow2 layer: Bitmap already exists' |
| # possible error |
| log = self.vm_a.get_log() |
| log = re.sub(r'^\[I \d+\.\d+\] OPENED\n', '', log) |
| log = re.sub(r'^(wrote .* bytes at offset .*\n.*KiB.*ops.*sec.*\n){3}', |
| '', log) |
| log = re.sub(r'\[I \+\d+\.\d+\] CLOSED\n?$', '', log) |
| self.assertEqual(log, '') |
| |
| # test that bitmap is still persistent |
| self.vm_a.launch() |
| self.check_bitmap(self.vm_a, sha256 if persistent else False) |
| |
| def do_test_migration(self, persistent, migrate_bitmaps, online, |
| shared_storage, pre_shutdown): |
| granularity = 512 |
| |
| # regions = ((start, count), ...) |
| regions = ((0, 0x10000), |
| (0xf0000, 0x10000), |
| (0xa0201, 0x1000)) |
| |
| should_migrate = \ |
| (migrate_bitmaps and (persistent or not pre_shutdown)) or \ |
| (persistent and shared_storage) |
| mig_caps = [{'capability': 'events', 'state': True}] |
| if migrate_bitmaps: |
| mig_caps.append({'capability': 'dirty-bitmaps', 'state': True}) |
| |
| self.vm_b.add_incoming(incoming_cmd if online else "defer") |
| self.vm_b.add_drive(disk_a if shared_storage else disk_b) |
| |
| if online: |
| os.mkfifo(mig_file) |
| self.vm_b.launch() |
| result = self.vm_b.qmp('migrate-set-capabilities', |
| capabilities=mig_caps) |
| self.assert_qmp(result, 'return', {}) |
| |
| self.add_bitmap(self.vm_a, granularity, persistent) |
| for r in regions: |
| self.vm_a.hmp_qemu_io('drive0', 'write %d %d' % r) |
| sha256 = get_bitmap_hash(self.vm_a) |
| |
| if pre_shutdown: |
| self.vm_a.shutdown() |
| self.vm_a.launch() |
| |
| result = self.vm_a.qmp('migrate-set-capabilities', |
| capabilities=mig_caps) |
| self.assert_qmp(result, 'return', {}) |
| |
| result = self.vm_a.qmp('migrate', uri=mig_cmd) |
| while True: |
| event = self.vm_a.event_wait('MIGRATION') |
| if event['data']['status'] == 'completed': |
| break |
| |
| if not online: |
| self.vm_a.shutdown() |
| self.vm_b.launch() |
| result = self.vm_b.qmp('migrate-set-capabilities', |
| capabilities=mig_caps) |
| self.assert_qmp(result, 'return', {}) |
| result = self.vm_b.qmp('migrate-incoming', uri=incoming_cmd) |
| self.assert_qmp(result, 'return', {}) |
| |
| while True: |
| event = self.vm_b.event_wait('MIGRATION') |
| if event['data']['status'] == 'completed': |
| break |
| |
| self.check_bitmap(self.vm_b, sha256 if should_migrate else False) |
| |
| if should_migrate: |
| self.vm_b.shutdown() |
| |
| # catch 'Could not reopen qcow2 layer: Bitmap already exists' |
| # possible error |
| log = self.vm_b.get_log() |
| log = re.sub(r'^\[I \d+\.\d+\] OPENED\n', '', log) |
| log = re.sub(r'\[I \+\d+\.\d+\] CLOSED\n?$', '', log) |
| self.assertEqual(log, '') |
| |
| # recreate vm_b, as we don't want -incoming option (this will lead |
| # to "cat" process left alive after test finish) |
| self.vm_b = iotests.VM(path_suffix='b') |
| self.vm_b.add_drive(disk_a if shared_storage else disk_b) |
| self.vm_b.launch() |
| self.check_bitmap(self.vm_b, sha256 if persistent else False) |
| |
| |
| def inject_test_case(klass, suffix, method, *args, **kwargs): |
| mc = operator.methodcaller(method, *args, **kwargs) |
| # We want to add a function attribute to `klass`, so that it is |
| # correctly converted to a method on instantiation. The |
| # methodcaller object `mc` is a callable, not a function, so we |
| # need the lambda to turn it into a function. |
| # pylint: disable=unnecessary-lambda |
| setattr(klass, 'test_' + method + suffix, lambda self: mc(self)) |
| |
| |
| class TestDirtyBitmapBackingMigration(iotests.QMPTestCase): |
| def setUp(self): |
| qemu_img_create('-f', iotests.imgfmt, base_a, size) |
| qemu_img_create('-f', iotests.imgfmt, '-F', iotests.imgfmt, |
| '-b', base_a, disk_a, size) |
| |
| for f in (disk_a, base_a): |
| qemu_img('bitmap', '--add', f, 'bmap0') |
| |
| blockdev = { |
| 'node-name': 'node0', |
| 'driver': iotests.imgfmt, |
| 'file': { |
| 'driver': 'file', |
| 'filename': disk_a |
| }, |
| 'backing': { |
| 'node-name': 'node0-base', |
| 'driver': iotests.imgfmt, |
| 'file': { |
| 'driver': 'file', |
| 'filename': base_a |
| } |
| } |
| } |
| |
| self.vm = iotests.VM() |
| self.vm.launch() |
| |
| result = self.vm.qmp('blockdev-add', **blockdev) |
| self.assert_qmp(result, 'return', {}) |
| |
| # Check that the bitmaps are there |
| nodes = self.vm.qmp('query-named-block-nodes', flat=True)['return'] |
| for node in nodes: |
| if 'node0' in node['node-name']: |
| self.assert_qmp(node, 'dirty-bitmaps[0]/name', 'bmap0') |
| |
| caps = [{'capability': 'events', 'state': True}] |
| result = self.vm.qmp('migrate-set-capabilities', capabilities=caps) |
| self.assert_qmp(result, 'return', {}) |
| |
| def tearDown(self): |
| self.vm.shutdown() |
| for f in (disk_a, base_a): |
| os.remove(f) |
| |
| def test_cont_on_source(self): |
| """ |
| Continue the source after migration. |
| """ |
| result = self.vm.qmp('migrate', uri='exec: cat > /dev/null') |
| self.assert_qmp(result, 'return', {}) |
| |
| with Timeout(10, 'Migration timeout'): |
| self.vm.wait_migration('postmigrate') |
| |
| result = self.vm.qmp('cont') |
| self.assert_qmp(result, 'return', {}) |
| |
| |
| def main() -> None: |
| for cmb in list(itertools.product((True, False), repeat=5)): |
| name = ('_' if cmb[0] else '_not_') + 'persistent_' |
| name += ('_' if cmb[1] else '_not_') + 'migbitmap_' |
| name += '_online' if cmb[2] else '_offline' |
| name += '_shared' if cmb[3] else '_nonshared' |
| if cmb[4]: |
| name += '__pre_shutdown' |
| |
| inject_test_case(TestDirtyBitmapMigration, name, 'do_test_migration', |
| *list(cmb)) |
| |
| for cmb in list(itertools.product((True, False), repeat=2)): |
| name = ('_' if cmb[0] else '_not_') + 'persistent_' |
| name += ('_' if cmb[1] else '_not_') + 'migbitmap' |
| |
| inject_test_case(TestDirtyBitmapMigration, name, |
| 'do_test_migration_resume_source', *list(cmb)) |
| |
| iotests.main( |
| supported_fmts=['qcow2'], |
| supported_protocols=['file'], |
| unsupported_imgopts=['compat'] |
| ) |
| |
| |
| if __name__ == '__main__': |
| main() |