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