1#!/usr/bin/env python3
2# group: rw migration
3#
4# Migrate a VM with a BDS with backing nodes, which runs
5# bdrv_invalidate_cache(), which for qcow2 and qed triggers reading the
6# backing file string from the image header.  Check whether this
7# interferes with bdrv_backing_overridden().
8#
9# Copyright (C) 2022 Red Hat, Inc.
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program.  If not, see <http://www.gnu.org/licenses/>.
23#
24
25import json
26import os
27from typing import Optional
28
29import iotests
30from iotests import qemu_img_create, qemu_img_info
31
32
33image_size = 1 * 1024 * 1024
34imgs = [os.path.join(iotests.test_dir, f'{i}.img') for i in range(0, 4)]
35
36mig_sock = os.path.join(iotests.sock_dir, 'mig.sock')
37
38
39class TestPostMigrateFilename(iotests.QMPTestCase):
40    vm_s: Optional[iotests.VM] = None
41    vm_d: Optional[iotests.VM] = None
42
43    def setUp(self) -> None:
44        # Create backing chain of three images, where the backing file strings
45        # are json:{} filenames
46        qemu_img_create('-f', iotests.imgfmt, imgs[0], str(image_size))
47        for i in range(1, 3):
48            backing = {
49                'driver': iotests.imgfmt,
50                'file': {
51                    'driver': 'file',
52                    'filename': imgs[i - 1]
53                }
54            }
55            qemu_img_create('-f', iotests.imgfmt, '-F', iotests.imgfmt,
56                            '-b', 'json:' + json.dumps(backing),
57                            imgs[i], str(image_size))
58
59    def tearDown(self) -> None:
60        if self.vm_s is not None:
61            self.vm_s.shutdown()
62        if self.vm_d is not None:
63            self.vm_d.shutdown()
64
65        for img in imgs:
66            try:
67                os.remove(img)
68            except OSError:
69                pass
70        try:
71            os.remove(mig_sock)
72        except OSError:
73            pass
74
75    def test_migration(self) -> None:
76        """
77        Migrate a VM with the backing chain created in setUp() attached.  At
78        the end of the migration process, the destination will run
79        bdrv_invalidate_cache(), which for some image formats (qcow2 and qed)
80        means the backing file string is re-read from the image header.  If
81        this overwrites bs->auto_backing_file, doing so may cause
82        bdrv_backing_overridden() to become true: The image header reports a
83        json:{} filename, but when opening it, bdrv_refresh_filename() will
84        simplify it to a plain simple filename; and when bs->auto_backing_file
85        and bs->backing->bs->filename differ, bdrv_backing_overridden() becomes
86        true.
87        If bdrv_backing_overridden() is true, the BDS will be forced to get a
88        json:{} filename, which in general is not the end of the world, but not
89        great.  Check whether that happens, i.e. whether migration changes the
90        node's filename.
91        """
92
93        blockdev = {
94            'node-name': 'node0',
95            'driver': iotests.imgfmt,
96            'file': {
97                'driver': 'file',
98                'filename': imgs[2]
99            }
100        }
101
102        self.vm_s = iotests.VM(path_suffix='a') \
103                           .add_blockdev(json.dumps(blockdev))
104        self.vm_d = iotests.VM(path_suffix='b') \
105                           .add_blockdev(json.dumps(blockdev)) \
106                           .add_incoming(f'unix:{mig_sock}')
107
108        assert self.vm_s is not None
109        assert self.vm_d is not None
110
111        self.vm_s.launch()
112        self.vm_d.launch()
113
114        pre_mig_filename = self.vm_s.node_info('node0')['file']
115
116        self.vm_s.qmp('migrate', uri=f'unix:{mig_sock}')
117
118        # Wait for migration to be done
119        self.vm_s.event_wait('STOP')
120        self.vm_d.event_wait('RESUME')
121
122        post_mig_filename = self.vm_d.node_info('node0')['file']
123
124        # Verify that the filename hasn't changed from before the migration
125        self.assertEqual(pre_mig_filename, post_mig_filename)
126
127        self.vm_s.shutdown()
128        self.vm_s = None
129
130        # For good measure, try creating an overlay and check its backing
131        # chain below.  This is how the issue was originally found.
132        self.vm_d.cmd('blockdev-snapshot-sync',
133                      format=iotests.imgfmt,
134                      snapshot_file=imgs[3],
135                      node_name='node0',
136                      snapshot_node_name='node0-overlay')
137
138        self.vm_d.shutdown()
139        self.vm_d = None
140
141        # Check the newly created overlay's backing chain
142        chain = qemu_img_info('--backing-chain', imgs[3])
143        for index, image in enumerate(chain):
144            self.assertEqual(image['filename'], imgs[3 - index])
145
146
147if __name__ == '__main__':
148    # These are the image formats that run their open() function from their
149    # .bdrv_co_invaliate_cache() implementations, so test them
150    iotests.main(supported_fmts=['qcow2', 'qed'],
151                 supported_protocols=['file'])
152