1#!/usr/bin/env python3
2# group: rw quick
3#
4# This test covers the basic fleecing workflow, which provides a
5# point-in-time snapshot of a node that can be queried over NBD.
6#
7# Copyright (C) 2018 Red Hat, Inc.
8# John helped, too.
9#
10# This program is free software; you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation; either version 2 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program.  If not, see <http://www.gnu.org/licenses/>.
22#
23# Creator/Owner: John Snow <jsnow@redhat.com>
24
25from subprocess import CalledProcessError
26
27import iotests
28from iotests import log, qemu_img, qemu_io
29
30iotests.script_initialize(
31    supported_fmts=['qcow2'],
32    supported_platforms=['linux'],
33    required_fmts=['copy-before-write'],
34    unsupported_imgopts=['compat']
35)
36
37patterns = [('0x5d', '0',         '64k'),
38            ('0xd5', '1M',        '64k'),
39            ('0xdc', '32M',       '64k'),
40            ('0xcd', '0x3ff0000', '64k')]  # 64M - 64K
41
42overwrite = [('0xab', '0',         '64k'), # Full overwrite
43             ('0xad', '0x00f8000', '64k'), # Partial-left (1M-32K)
44             ('0x1d', '0x2008000', '64k'), # Partial-right (32M+32K)
45             ('0xea', '0x3fe0000', '64k')] # Adjacent-left (64M - 128K)
46
47zeroes = [('0', '0x00f8000', '32k'), # Left-end of partial-left (1M-32K)
48          ('0', '0x2010000', '32k'), # Right-end of partial-right (32M+64K)
49          ('0', '0x3fe0000', '64k')] # overwrite[3]
50
51remainder = [('0xd5', '0x108000',  '32k'), # Right-end of partial-left [1]
52             ('0xdc', '32M',       '32k'), # Left-end of partial-right [2]
53             ('0xcd', '0x3ff0000', '64k')] # patterns[3]
54
55def do_test(vm, use_cbw, use_snapshot_access_filter, base_img_path,
56            fleece_img_path, nbd_sock_path=None,
57            target_img_path=None,
58            bitmap=False):
59    push_backup = target_img_path is not None
60    assert (nbd_sock_path is not None) != push_backup
61    if push_backup:
62        assert use_cbw
63
64    log('--- Setting up images ---')
65    log('')
66
67    qemu_img('create', '-f', iotests.imgfmt, base_img_path, '64M')
68    if bitmap:
69        qemu_img('bitmap', '--add', base_img_path, 'bitmap0')
70
71    if use_snapshot_access_filter:
72        assert use_cbw
73        qemu_img('create', '-f', 'raw', fleece_img_path, '64M')
74    else:
75        qemu_img('create', '-f', 'qcow2', fleece_img_path, '64M')
76
77    if push_backup:
78        qemu_img('create', '-f', 'qcow2', target_img_path, '64M')
79
80    for p in patterns:
81        qemu_io('-f', iotests.imgfmt,
82                '-c', 'write -P%s %s %s' % p, base_img_path)
83
84    log('Done')
85
86    log('')
87    log('--- Launching VM ---')
88    log('')
89
90    src_node = 'source'
91    tmp_node = 'temp'
92    qom_path = '/machine/peripheral/sda'
93    vm.add_blockdev(f'driver={iotests.imgfmt},file.driver=file,'
94                    f'file.filename={base_img_path},node-name={src_node}')
95    vm.add_device('virtio-scsi')
96    vm.add_device(f'scsi-hd,id=sda,drive={src_node}')
97    vm.launch()
98    log('Done')
99
100    log('')
101    log('--- Setting up Fleecing Graph ---')
102    log('')
103
104
105    if use_snapshot_access_filter:
106        log(vm.qmp('blockdev-add', {
107            'node-name': tmp_node,
108            'driver': 'file',
109            'filename': fleece_img_path,
110        }))
111    else:
112        # create tmp_node backed by src_node
113        log(vm.qmp('blockdev-add', {
114            'driver': 'qcow2',
115            'node-name': tmp_node,
116            'file': {
117                'driver': 'file',
118                'filename': fleece_img_path,
119            },
120            'backing': src_node,
121        }))
122
123    # Establish CBW from source to fleecing node
124    if use_cbw:
125        fl_cbw = {
126            'driver': 'copy-before-write',
127            'node-name': 'fl-cbw',
128            'file': src_node,
129            'target': tmp_node
130        }
131
132        if bitmap:
133            fl_cbw['bitmap'] = {'node': src_node, 'name': 'bitmap0'}
134
135        log(vm.qmp('blockdev-add', fl_cbw))
136
137        log(vm.qmp('qom-set', path=qom_path, property='drive', value='fl-cbw'))
138
139        if use_snapshot_access_filter:
140            log(vm.qmp('blockdev-add', {
141                'driver': 'snapshot-access',
142                'node-name': 'fl-access',
143                'file': 'fl-cbw',
144            }))
145    else:
146        log(vm.qmp('blockdev-backup',
147                   job_id='fleecing',
148                   device=src_node,
149                   target=tmp_node,
150                   sync='none'))
151
152    export_node = 'fl-access' if use_snapshot_access_filter else tmp_node
153
154    if push_backup:
155        log('')
156        log('--- Starting actual backup ---')
157        log('')
158
159        log(vm.qmp('blockdev-add', **{
160            'driver': iotests.imgfmt,
161            'node-name': 'target',
162            'file': {
163                'driver': 'file',
164                'filename': target_img_path
165            }
166        }))
167        log(vm.qmp('blockdev-backup', device=export_node,
168                   sync='full', target='target',
169                   job_id='push-backup', speed=1))
170    else:
171        log('')
172        log('--- Setting up NBD Export ---')
173        log('')
174
175        nbd_uri = 'nbd+unix:///%s?socket=%s' % (export_node, nbd_sock_path)
176        log(vm.qmp('nbd-server-start',
177                   {'addr': { 'type': 'unix',
178                              'data': { 'path': nbd_sock_path } } }))
179
180        log(vm.qmp('nbd-server-add', device=export_node))
181
182        log('')
183        log('--- Sanity Check ---')
184        log('')
185
186        for p in patterns + zeroes:
187            cmd = 'read -P%s %s %s' % p
188            log(cmd)
189
190            try:
191                qemu_io('-r', '-f', 'raw', '-c', cmd, nbd_uri)
192            except CalledProcessError as exc:
193                if bitmap and p in zeroes:
194                    log(exc.stdout)
195                else:
196                    raise
197
198    log('')
199    log('--- Testing COW ---')
200    log('')
201
202    for p in overwrite:
203        cmd = 'write -P%s %s %s' % p
204        log(cmd)
205        log(vm.hmp_qemu_io(qom_path, cmd, qdev=True))
206
207    if push_backup:
208        # Check that previous operations were done during backup, not after
209        # If backup is already finished, it's possible that it was finished
210        # even before hmp qemu_io write, and we didn't actually test
211        # copy-before-write operation. This should not happen, as we use
212        # speed=1. But worth checking.
213        result = vm.qmp('query-block-jobs')
214        assert len(result['return']) == 1
215
216        vm.cmd('block-job-set-speed', device='push-backup', speed=0)
217
218        log(vm.event_wait(name='BLOCK_JOB_COMPLETED',
219                          match={'data': {'device': 'push-backup'}}),
220            filters=[iotests.filter_qmp_event])
221        log(vm.qmp('blockdev-del', node_name='target'))
222
223    log('')
224    log('--- Verifying Data ---')
225    log('')
226
227    for p in patterns + zeroes:
228        cmd = 'read -P%s %s %s' % p
229        log(cmd)
230        args = ['-r', '-c', cmd]
231        if push_backup:
232            args += [target_img_path]
233        else:
234            args += ['-f', 'raw', nbd_uri]
235
236        try:
237            qemu_io(*args)
238        except CalledProcessError as exc:
239            if bitmap and p in zeroes:
240                log(exc.stdout)
241            else:
242                raise
243
244    log('')
245    log('--- Cleanup ---')
246    log('')
247
248    if not push_backup:
249        log(vm.qmp('nbd-server-stop'))
250
251    if use_cbw:
252        if use_snapshot_access_filter:
253            log(vm.qmp('blockdev-del', node_name='fl-access'))
254        log(vm.qmp('qom-set', path=qom_path, property='drive', value=src_node))
255        log(vm.qmp('blockdev-del', node_name='fl-cbw'))
256    else:
257        log(vm.qmp('block-job-cancel', device='fleecing'))
258        e = vm.event_wait('BLOCK_JOB_CANCELLED')
259        assert e is not None
260        log(e, filters=[iotests.filter_qmp_event])
261
262    log(vm.qmp('blockdev-del', node_name=tmp_node))
263    vm.shutdown()
264
265    log('')
266    log('--- Confirming writes ---')
267    log('')
268
269    for p in overwrite + remainder:
270        cmd = 'read -P%s %s %s' % p
271        log(cmd)
272        qemu_io(base_img_path, '-c', cmd)
273
274    log('')
275    log('Done')
276
277
278def test(use_cbw, use_snapshot_access_filter,
279         nbd_sock_path=None, target_img_path=None, bitmap=False):
280    with iotests.FilePath('base.img') as base_img_path, \
281         iotests.FilePath('fleece.img') as fleece_img_path, \
282         iotests.VM() as vm:
283        do_test(vm, use_cbw, use_snapshot_access_filter, base_img_path,
284                fleece_img_path, nbd_sock_path, target_img_path,
285                bitmap=bitmap)
286
287def test_pull(use_cbw, use_snapshot_access_filter, bitmap=False):
288    with iotests.FilePath('nbd.sock',
289                          base_dir=iotests.sock_dir) as nbd_sock_path:
290        test(use_cbw, use_snapshot_access_filter, nbd_sock_path, None,
291             bitmap=bitmap)
292
293def test_push():
294    with iotests.FilePath('target.img') as target_img_path:
295        test(True, True, None, target_img_path)
296
297
298log('=== Test backup(sync=none) based fleecing ===\n')
299test_pull(False, False)
300
301log('=== Test cbw-filter based fleecing ===\n')
302test_pull(True, False)
303
304log('=== Test fleecing-format based fleecing ===\n')
305test_pull(True, True)
306
307log('=== Test fleecing-format based fleecing with bitmap ===\n')
308test_pull(True, True, bitmap=True)
309
310log('=== Test push backup with fleecing ===\n')
311test_push()
312