xref: /openbmc/qemu/tests/qemu-iotests/040 (revision e9206163)
1#!/usr/bin/env python3
2# group: rw auto
3#
4# Tests for image block commit.
5#
6# Copyright (C) 2012 IBM, Corp.
7# Copyright (C) 2012 Red Hat, Inc.
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program.  If not, see <http://www.gnu.org/licenses/>.
21#
22# Test for live block commit
23# Derived from Image Streaming Test 030
24
25import time
26import os
27import iotests
28from iotests import qemu_img, qemu_io
29import struct
30import errno
31
32backing_img = os.path.join(iotests.test_dir, 'backing.img')
33mid_img = os.path.join(iotests.test_dir, 'mid.img')
34test_img = os.path.join(iotests.test_dir, 'test.img')
35
36class ImageCommitTestCase(iotests.QMPTestCase):
37    '''Abstract base class for image commit test cases'''
38
39    def wait_for_complete(self, need_ready=False):
40        completed = False
41        ready = False
42        while not completed:
43            for event in self.vm.get_qmp_events(wait=True):
44                if event['event'] == 'BLOCK_JOB_COMPLETED':
45                    self.assert_qmp_absent(event, 'data/error')
46                    self.assert_qmp(event, 'data/type', 'commit')
47                    self.assert_qmp(event, 'data/device', 'drive0')
48                    self.assert_qmp(event, 'data/offset', event['data']['len'])
49                    if need_ready:
50                        self.assertTrue(ready, "Expecting BLOCK_JOB_COMPLETED event")
51                    completed = True
52                elif event['event'] == 'BLOCK_JOB_READY':
53                    ready = True
54                    self.assert_qmp(event, 'data/type', 'commit')
55                    self.assert_qmp(event, 'data/device', 'drive0')
56                    self.vm.qmp('block-job-complete', device='drive0')
57
58        self.assert_no_active_block_jobs()
59        self.vm.shutdown()
60
61    def run_commit_test(self, top, base, need_ready=False, node_names=False):
62        self.assert_no_active_block_jobs()
63        if node_names:
64            self.vm.cmd('block-commit', device='drive0', top_node=top, base_node=base)
65        else:
66            self.vm.cmd('block-commit', device='drive0', top=top, base=base)
67        self.wait_for_complete(need_ready)
68
69    def run_default_commit_test(self):
70        self.assert_no_active_block_jobs()
71        self.vm.cmd('block-commit', device='drive0')
72        self.wait_for_complete()
73
74class TestSingleDrive(ImageCommitTestCase):
75    # Need some space after the copied data so that throttling is effective in
76    # tests that use it rather than just completing the job immediately
77    image_len = 2 * 1024 * 1024
78    test_len = 1 * 1024 * 256
79
80    def setUp(self):
81        iotests.create_image(backing_img, self.image_len)
82        qemu_img('create', '-f', iotests.imgfmt,
83                 '-o', 'backing_file=%s' % backing_img, '-F', 'raw', mid_img)
84        qemu_img('create', '-f', iotests.imgfmt,
85                 '-o', 'backing_file=%s' % mid_img,
86                 '-F', iotests.imgfmt, test_img)
87        if self.image_len:
88            qemu_io('-f', 'raw', '-c', 'write -P 0xab 0 524288', backing_img)
89            qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0xef 524288 524288',
90                    mid_img)
91        self.vm = iotests.VM().add_drive(test_img, "node-name=top,backing.node-name=mid,backing.backing.node-name=base", interface="none")
92        self.vm.add_device('virtio-scsi')
93        self.vm.add_device("scsi-hd,id=scsi0,drive=drive0")
94        self.vm.launch()
95
96    def tearDown(self):
97        self.vm.shutdown()
98        os.remove(test_img)
99        os.remove(mid_img)
100        os.remove(backing_img)
101
102    def test_commit(self):
103        self.run_commit_test(mid_img, backing_img)
104        if not self.image_len:
105            return
106        qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img)
107        qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img)
108
109    def test_commit_node(self):
110        self.run_commit_test("mid", "base", node_names=True)
111        if not self.image_len:
112            return
113        qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img)
114        qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img)
115
116    @iotests.skip_if_unsupported(['throttle'])
117    def test_commit_with_filter_and_quit(self):
118        self.vm.cmd('object-add', qom_type='throttle-group', id='tg')
119
120        # Add a filter outside of the backing chain
121        self.vm.cmd('blockdev-add', driver='throttle', node_name='filter', throttle_group='tg', file='mid')
122
123        self.vm.cmd('block-commit', device='drive0')
124
125        # Quit immediately, thus forcing a simultaneous cancel of the
126        # block job and a bdrv_drain_all()
127        self.vm.cmd('quit')
128
129    # Same as above, but this time we add the filter after starting the job
130    @iotests.skip_if_unsupported(['throttle'])
131    def test_commit_plus_filter_and_quit(self):
132        self.vm.cmd('object-add', qom_type='throttle-group', id='tg')
133
134        self.vm.cmd('block-commit', device='drive0')
135
136        # Add a filter outside of the backing chain
137        self.vm.cmd('blockdev-add', driver='throttle', node_name='filter', throttle_group='tg', file='mid')
138
139        # Quit immediately, thus forcing a simultaneous cancel of the
140        # block job and a bdrv_drain_all()
141        self.vm.cmd('quit')
142
143    def test_device_not_found(self):
144        result = self.vm.qmp('block-commit', device='nonexistent', top='%s' % mid_img)
145        self.assert_qmp(result, 'error/class', 'DeviceNotFound')
146
147    def test_top_same_base(self):
148        self.assert_no_active_block_jobs()
149        result = self.vm.qmp('block-commit', device='drive0', top='%s' % backing_img, base='%s' % backing_img)
150        self.assert_qmp(result, 'error/class', 'GenericError')
151        self.assert_qmp(result, 'error/desc', "Can't find '%s' in the backing chain" % backing_img)
152
153    def test_top_invalid(self):
154        self.assert_no_active_block_jobs()
155        result = self.vm.qmp('block-commit', device='drive0', top='badfile', base='%s' % backing_img)
156        self.assert_qmp(result, 'error/class', 'GenericError')
157        self.assert_qmp(result, 'error/desc', 'Top image file badfile not found')
158
159    def test_base_invalid(self):
160        self.assert_no_active_block_jobs()
161        result = self.vm.qmp('block-commit', device='drive0', top='%s' % mid_img, base='badfile')
162        self.assert_qmp(result, 'error/class', 'GenericError')
163        self.assert_qmp(result, 'error/desc', "Can't find 'badfile' in the backing chain")
164
165    def test_top_node_invalid(self):
166        self.assert_no_active_block_jobs()
167        result = self.vm.qmp('block-commit', device='drive0', top_node='badfile', base_node='base')
168        self.assert_qmp(result, 'error/class', 'GenericError')
169        self.assert_qmp(result, 'error/desc', "Cannot find device='' nor node-name='badfile'")
170
171    def test_base_node_invalid(self):
172        self.assert_no_active_block_jobs()
173        result = self.vm.qmp('block-commit', device='drive0', top_node='mid', base_node='badfile')
174        self.assert_qmp(result, 'error/class', 'GenericError')
175        self.assert_qmp(result, 'error/desc', "Cannot find device='' nor node-name='badfile'")
176
177    def test_top_path_and_node(self):
178        self.assert_no_active_block_jobs()
179        result = self.vm.qmp('block-commit', device='drive0', top_node='mid', base_node='base', top='%s' % mid_img)
180        self.assert_qmp(result, 'error/class', 'GenericError')
181        self.assert_qmp(result, 'error/desc', "'top-node' and 'top' are mutually exclusive")
182
183    def test_base_path_and_node(self):
184        self.assert_no_active_block_jobs()
185        result = self.vm.qmp('block-commit', device='drive0', top_node='mid', base_node='base', base='%s' % backing_img)
186        self.assert_qmp(result, 'error/class', 'GenericError')
187        self.assert_qmp(result, 'error/desc', "'base-node' and 'base' are mutually exclusive")
188
189    def test_top_is_active(self):
190        self.run_commit_test(test_img, backing_img, need_ready=True)
191        if not self.image_len:
192            return
193        qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img)
194        qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img)
195
196    def test_top_is_default_active(self):
197        self.run_default_commit_test()
198        if not self.image_len:
199            return
200        qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img)
201        qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img)
202
203    def test_top_and_base_reversed(self):
204        self.assert_no_active_block_jobs()
205        result = self.vm.qmp('block-commit', device='drive0', top='%s' % backing_img, base='%s' % mid_img)
206        self.assert_qmp(result, 'error/class', 'GenericError')
207        self.assert_qmp(result, 'error/desc', "Can't find '%s' in the backing chain" % mid_img)
208
209    def test_top_and_base_node_reversed(self):
210        self.assert_no_active_block_jobs()
211        result = self.vm.qmp('block-commit', device='drive0', top_node='base', base_node='top')
212        self.assert_qmp(result, 'error/class', 'GenericError')
213        self.assert_qmp(result, 'error/desc', "'top' is not in this backing file chain")
214
215    def test_top_node_in_wrong_chain(self):
216        self.assert_no_active_block_jobs()
217
218        self.vm.cmd('blockdev-add', driver='null-co', node_name='null')
219
220        result = self.vm.qmp('block-commit', device='drive0', top_node='null', base_node='base')
221        self.assert_qmp(result, 'error/class', 'GenericError')
222        self.assert_qmp(result, 'error/desc', "'null' is not in this backing file chain")
223
224    # When the job is running on a BB that is automatically deleted on hot
225    # unplug, the job is cancelled when the device disappears
226    def test_hot_unplug(self):
227        if self.image_len == 0:
228            return
229
230        self.assert_no_active_block_jobs()
231        self.vm.cmd('block-commit', device='drive0', top=mid_img,
232                    base=backing_img, speed=(self.image_len // 4))
233        self.vm.cmd('device_del', id='scsi0')
234
235        cancelled = False
236        deleted = False
237        while not cancelled or not deleted:
238            for event in self.vm.get_qmp_events(wait=True):
239                if event['event'] == 'DEVICE_DELETED':
240                    self.assert_qmp(event, 'data/device', 'scsi0')
241                    deleted = True
242                elif event['event'] == 'BLOCK_JOB_CANCELLED':
243                    self.assert_qmp(event, 'data/device', 'drive0')
244                    cancelled = True
245                elif event['event'] == 'JOB_STATUS_CHANGE':
246                    self.assert_qmp(event, 'data/id', 'drive0')
247                else:
248                    self.fail("Unexpected event %s" % (event['event']))
249
250        self.assert_no_active_block_jobs()
251
252    # Tests that the insertion of the commit_top filter node doesn't make a
253    # difference to query-blockstat
254    def test_implicit_node(self):
255        if self.image_len == 0:
256            return
257
258        self.assert_no_active_block_jobs()
259        self.vm.cmd('block-commit', device='drive0', top=mid_img,
260                    base=backing_img, speed=(self.image_len // 4))
261
262        result = self.vm.qmp('query-block')
263        self.assert_qmp(result, 'return[0]/inserted/file', test_img)
264        self.assert_qmp(result, 'return[0]/inserted/drv', iotests.imgfmt)
265        self.assert_qmp(result, 'return[0]/inserted/backing_file', mid_img)
266        self.assert_qmp(result, 'return[0]/inserted/backing_file_depth', 2)
267        self.assert_qmp(result, 'return[0]/inserted/image/filename', test_img)
268        self.assert_qmp(result, 'return[0]/inserted/image/backing-image/filename', mid_img)
269        self.assert_qmp(result, 'return[0]/inserted/image/backing-image/backing-image/filename', backing_img)
270
271        result = self.vm.qmp('query-blockstats')
272        self.assert_qmp(result, 'return[0]/node-name', 'top')
273        self.assert_qmp(result, 'return[0]/backing/node-name', 'mid')
274        self.assert_qmp(result, 'return[0]/backing/backing/node-name', 'base')
275
276        self.cancel_and_wait()
277        self.assert_no_active_block_jobs()
278
279class TestRelativePaths(ImageCommitTestCase):
280    image_len = 1 * 1024 * 1024
281    test_len = 1 * 1024 * 256
282
283    dir1 = "dir1"
284    dir2 = "dir2/"
285    dir3 = "dir2/dir3/"
286
287    test_img = os.path.join(iotests.test_dir, dir3, 'test.img')
288    mid_img = "../mid.img"
289    backing_img = "../dir1/backing.img"
290
291    backing_img_abs = os.path.join(iotests.test_dir, dir1, 'backing.img')
292    mid_img_abs = os.path.join(iotests.test_dir, dir2, 'mid.img')
293
294    def setUp(self):
295        try:
296            os.mkdir(os.path.join(iotests.test_dir, self.dir1))
297            os.mkdir(os.path.join(iotests.test_dir, self.dir2))
298            os.mkdir(os.path.join(iotests.test_dir, self.dir3))
299        except OSError as exception:
300            if exception.errno != errno.EEXIST:
301                raise
302        iotests.create_image(self.backing_img_abs, TestRelativePaths.image_len)
303        qemu_img('create', '-f', iotests.imgfmt,
304                 '-o', 'backing_file=%s' % self.backing_img_abs,
305                 '-F', 'raw', self.mid_img_abs)
306        qemu_img('create', '-f', iotests.imgfmt,
307                 '-o', 'backing_file=%s' % self.mid_img_abs,
308                 '-F', iotests.imgfmt, self.test_img)
309        qemu_img('rebase', '-u', '-b', self.backing_img,
310                 '-F', 'raw', self.mid_img_abs)
311        qemu_img('rebase', '-u', '-b', self.mid_img,
312                 '-F', iotests.imgfmt, self.test_img)
313        qemu_io('-f', 'raw', '-c', 'write -P 0xab 0 524288', self.backing_img_abs)
314        qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0xef 524288 524288', self.mid_img_abs)
315        self.vm = iotests.VM().add_drive(self.test_img)
316        self.vm.launch()
317
318    def tearDown(self):
319        self.vm.shutdown()
320        os.remove(self.test_img)
321        os.remove(self.mid_img_abs)
322        os.remove(self.backing_img_abs)
323        try:
324            os.rmdir(os.path.join(iotests.test_dir, self.dir1))
325            os.rmdir(os.path.join(iotests.test_dir, self.dir3))
326            os.rmdir(os.path.join(iotests.test_dir, self.dir2))
327        except OSError as exception:
328            if exception.errno != errno.EEXIST and exception.errno != errno.ENOTEMPTY:
329                raise
330
331    def test_commit(self):
332        self.run_commit_test(self.mid_img, self.backing_img)
333        qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', self.backing_img_abs)
334        qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', self.backing_img_abs)
335
336    def test_device_not_found(self):
337        result = self.vm.qmp('block-commit', device='nonexistent', top='%s' % self.mid_img)
338        self.assert_qmp(result, 'error/class', 'DeviceNotFound')
339
340    def test_top_same_base(self):
341        self.assert_no_active_block_jobs()
342        result = self.vm.qmp('block-commit', device='drive0', top='%s' % self.mid_img, base='%s' % self.mid_img)
343        self.assert_qmp(result, 'error/class', 'GenericError')
344        self.assert_qmp(result, 'error/desc', "Can't find '%s' in the backing chain" % self.mid_img)
345
346    def test_top_invalid(self):
347        self.assert_no_active_block_jobs()
348        result = self.vm.qmp('block-commit', device='drive0', top='badfile', base='%s' % self.backing_img)
349        self.assert_qmp(result, 'error/class', 'GenericError')
350        self.assert_qmp(result, 'error/desc', 'Top image file badfile not found')
351
352    def test_base_invalid(self):
353        self.assert_no_active_block_jobs()
354        result = self.vm.qmp('block-commit', device='drive0', top='%s' % self.mid_img, base='badfile')
355        self.assert_qmp(result, 'error/class', 'GenericError')
356        self.assert_qmp(result, 'error/desc', "Can't find 'badfile' in the backing chain")
357
358    def test_top_is_active(self):
359        self.run_commit_test(self.test_img, self.backing_img)
360        qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', self.backing_img_abs)
361        qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', self.backing_img_abs)
362
363    def test_top_and_base_reversed(self):
364        self.assert_no_active_block_jobs()
365        result = self.vm.qmp('block-commit', device='drive0', top='%s' % self.backing_img, base='%s' % self.mid_img)
366        self.assert_qmp(result, 'error/class', 'GenericError')
367        self.assert_qmp(result, 'error/desc', "Can't find '%s' in the backing chain" % self.mid_img)
368
369
370class TestSetSpeed(ImageCommitTestCase):
371    image_len = 80 * 1024 * 1024 # MB
372
373    def setUp(self):
374        qemu_img('create', backing_img, str(TestSetSpeed.image_len))
375        qemu_img('create', '-f', iotests.imgfmt,
376                 '-o', 'backing_file=%s' % backing_img, '-F', 'raw', mid_img)
377        qemu_img('create', '-f', iotests.imgfmt,
378                 '-o', 'backing_file=%s' % mid_img,
379                 '-F', iotests.imgfmt, test_img)
380        qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x1 0 512', test_img)
381        qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0xef 524288 524288', mid_img)
382        self.vm = iotests.VM().add_drive('blkdebug::' + test_img)
383        self.vm.launch()
384
385    def tearDown(self):
386        self.vm.shutdown()
387        os.remove(test_img)
388        os.remove(mid_img)
389        os.remove(backing_img)
390
391    def test_set_speed(self):
392        self.assert_no_active_block_jobs()
393
394        self.vm.pause_drive('drive0')
395        self.vm.cmd('block-commit', device='drive0', top=mid_img, speed=1024 * 1024)
396
397        # Ensure the speed we set was accepted
398        result = self.vm.qmp('query-block-jobs')
399        self.assert_qmp(result, 'return[0]/device', 'drive0')
400        self.assert_qmp(result, 'return[0]/speed', 1024 * 1024)
401
402        self.cancel_and_wait(resume=True)
403
404class TestActiveZeroLengthImage(TestSingleDrive):
405    image_len = 0
406
407class TestReopenOverlay(ImageCommitTestCase):
408    image_len = 1024 * 1024
409    img0 = os.path.join(iotests.test_dir, '0.img')
410    img1 = os.path.join(iotests.test_dir, '1.img')
411    img2 = os.path.join(iotests.test_dir, '2.img')
412    img3 = os.path.join(iotests.test_dir, '3.img')
413
414    def setUp(self):
415        iotests.create_image(self.img0, self.image_len)
416        qemu_img('create', '-f', iotests.imgfmt,
417                 '-o', 'backing_file=%s' % self.img0, '-F', 'raw', self.img1)
418        qemu_img('create', '-f', iotests.imgfmt,
419                 '-o', 'backing_file=%s' % self.img1,
420                 '-F', iotests.imgfmt, self.img2)
421        qemu_img('create', '-f', iotests.imgfmt,
422                 '-o', 'backing_file=%s' % self.img2,
423                 '-F', iotests.imgfmt, self.img3)
424        qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0xab 0 128K', self.img1)
425        self.vm = iotests.VM().add_drive(self.img3)
426        self.vm.launch()
427
428    def tearDown(self):
429        self.vm.shutdown()
430        os.remove(self.img0)
431        os.remove(self.img1)
432        os.remove(self.img2)
433        os.remove(self.img3)
434
435    # This tests what happens when the overlay image of the 'top' node
436    # needs to be reopened in read-write mode in order to update the
437    # backing image string.
438    def test_reopen_overlay(self):
439        self.run_commit_test(self.img1, self.img0)
440
441class TestErrorHandling(iotests.QMPTestCase):
442    image_len = 2 * 1024 * 1024
443
444    def setUp(self):
445        iotests.create_image(backing_img, self.image_len)
446        qemu_img('create', '-f', iotests.imgfmt,
447                 '-o', 'backing_file=%s' % backing_img,
448                 '-F', 'raw', mid_img)
449        qemu_img('create', '-f', iotests.imgfmt,
450                 '-o', 'backing_file=%s' % mid_img,
451                 '-F', iotests.imgfmt, test_img)
452
453        qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x11 0 512k', mid_img)
454        qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x22 0 512k', test_img)
455
456        self.vm = iotests.VM()
457        self.vm.launch()
458
459        self.blkdebug_file = iotests.file_path("blkdebug.conf")
460
461    def tearDown(self):
462        self.vm.shutdown()
463        os.remove(test_img)
464        os.remove(mid_img)
465        os.remove(backing_img)
466
467    def blockdev_add(self, **kwargs):
468        self.vm.cmd('blockdev-add', **kwargs)
469
470    def add_block_nodes(self, base_debug=None, mid_debug=None, top_debug=None):
471        self.blockdev_add(node_name='base-file', driver='file',
472                          filename=backing_img)
473        self.blockdev_add(node_name='mid-file', driver='file',
474                          filename=mid_img)
475        self.blockdev_add(node_name='top-file', driver='file',
476                          filename=test_img)
477
478        if base_debug:
479            self.blockdev_add(node_name='base-dbg', driver='blkdebug',
480                              image='base-file', inject_error=base_debug)
481        if mid_debug:
482            self.blockdev_add(node_name='mid-dbg', driver='blkdebug',
483                              image='mid-file', inject_error=mid_debug)
484        if top_debug:
485            self.blockdev_add(node_name='top-dbg', driver='blkdebug',
486                              image='top-file', inject_error=top_debug)
487
488        self.blockdev_add(node_name='base-fmt', driver='raw',
489                          file=('base-dbg' if base_debug else 'base-file'))
490        self.blockdev_add(node_name='mid-fmt', driver=iotests.imgfmt,
491                          file=('mid-dbg' if mid_debug else 'mid-file'),
492                          backing='base-fmt')
493        self.blockdev_add(node_name='top-fmt', driver=iotests.imgfmt,
494                          file=('top-dbg' if top_debug else 'top-file'),
495                          backing='mid-fmt')
496
497    def run_job(self, expected_events, error_pauses_job=False):
498        match_device = {'data': {'device': 'job0'}}
499        events = [
500            ('BLOCK_JOB_COMPLETED', match_device),
501            ('BLOCK_JOB_CANCELLED', match_device),
502            ('BLOCK_JOB_ERROR', match_device),
503            ('BLOCK_JOB_READY', match_device),
504        ]
505
506        completed = False
507        log = []
508        while not completed:
509            ev = self.vm.events_wait(events, timeout=5.0)
510            if ev['event'] == 'BLOCK_JOB_COMPLETED':
511                completed = True
512            elif ev['event'] == 'BLOCK_JOB_ERROR':
513                if error_pauses_job:
514                    self.vm.cmd('block-job-resume', device='job0')
515            elif ev['event'] == 'BLOCK_JOB_READY':
516                self.vm.cmd('block-job-complete', device='job0')
517            else:
518                self.fail("Unexpected event: %s" % ev)
519            log.append(iotests.filter_qmp_event(ev))
520
521        self.maxDiff = None
522        self.assertEqual(expected_events, log)
523
524    def event_error(self, op, action):
525        return {
526            'event': 'BLOCK_JOB_ERROR',
527            'data': {'action': action, 'device': 'job0', 'operation': op},
528            'timestamp': {'microseconds': 'USECS', 'seconds': 'SECS'}
529        }
530
531    def event_ready(self):
532        return {
533            'event': 'BLOCK_JOB_READY',
534            'data': {'device': 'job0',
535                     'len': 524288,
536                     'offset': 524288,
537                     'speed': 0,
538                     'type': 'commit'},
539            'timestamp': {'microseconds': 'USECS', 'seconds': 'SECS'},
540        }
541
542    def event_completed(self, errmsg=None, active=True):
543        max_len = 524288 if active else self.image_len
544        data = {
545            'device': 'job0',
546            'len': max_len,
547            'offset': 0 if errmsg else max_len,
548            'speed': 0,
549            'type': 'commit'
550        }
551        if errmsg:
552            data['error'] = errmsg
553
554        return {
555            'event': 'BLOCK_JOB_COMPLETED',
556            'data': data,
557            'timestamp': {'microseconds': 'USECS', 'seconds': 'SECS'},
558        }
559
560    def blkdebug_event(self, event, is_raw=False):
561        if event:
562            return [{
563                'event': event,
564                'sector': 512 if is_raw else 1024,
565                'once': True,
566            }]
567        return None
568
569    def prepare_and_start_job(self, on_error, active=True,
570                              top_event=None, mid_event=None, base_event=None):
571
572        top_debug = self.blkdebug_event(top_event)
573        mid_debug = self.blkdebug_event(mid_event)
574        base_debug = self.blkdebug_event(base_event, True)
575
576        self.add_block_nodes(top_debug=top_debug, mid_debug=mid_debug,
577                             base_debug=base_debug)
578
579        self.vm.cmd('block-commit', job_id='job0', device='top-fmt',
580                    top_node='top-fmt' if active else 'mid-fmt',
581                    base_node='mid-fmt' if active else 'base-fmt',
582                    on_error=on_error)
583
584    def testActiveReadErrorReport(self):
585        self.prepare_and_start_job('report', top_event='read_aio')
586        self.run_job([
587            self.event_error('read', 'report'),
588            self.event_completed('Input/output error')
589        ])
590
591        self.vm.shutdown()
592        self.assertFalse(iotests.compare_images(test_img, mid_img),
593                         'target image matches source after error')
594
595    def testActiveReadErrorStop(self):
596        self.prepare_and_start_job('stop', top_event='read_aio')
597        self.run_job([
598            self.event_error('read', 'stop'),
599            self.event_ready(),
600            self.event_completed()
601        ], error_pauses_job=True)
602
603        self.vm.shutdown()
604        self.assertTrue(iotests.compare_images(test_img, mid_img),
605                        'target image does not match source after commit')
606
607    def testActiveReadErrorIgnore(self):
608        self.prepare_and_start_job('ignore', top_event='read_aio')
609        self.run_job([
610            self.event_error('read', 'ignore'),
611            self.event_ready(),
612            self.event_completed()
613        ])
614
615        # For commit, 'ignore' actually means retry, so this will succeed
616        self.vm.shutdown()
617        self.assertTrue(iotests.compare_images(test_img, mid_img),
618                        'target image does not match source after commit')
619
620    def testActiveWriteErrorReport(self):
621        self.prepare_and_start_job('report', mid_event='write_aio')
622        self.run_job([
623            self.event_error('write', 'report'),
624            self.event_completed('Input/output error')
625        ])
626
627        self.vm.shutdown()
628        self.assertFalse(iotests.compare_images(test_img, mid_img),
629                         'target image matches source after error')
630
631    def testActiveWriteErrorStop(self):
632        self.prepare_and_start_job('stop', mid_event='write_aio')
633        self.run_job([
634            self.event_error('write', 'stop'),
635            self.event_ready(),
636            self.event_completed()
637        ], error_pauses_job=True)
638
639        self.vm.shutdown()
640        self.assertTrue(iotests.compare_images(test_img, mid_img),
641                        'target image does not match source after commit')
642
643    def testActiveWriteErrorIgnore(self):
644        self.prepare_and_start_job('ignore', mid_event='write_aio')
645        self.run_job([
646            self.event_error('write', 'ignore'),
647            self.event_ready(),
648            self.event_completed()
649        ])
650
651        # For commit, 'ignore' actually means retry, so this will succeed
652        self.vm.shutdown()
653        self.assertTrue(iotests.compare_images(test_img, mid_img),
654                        'target image does not match source after commit')
655
656    def testIntermediateReadErrorReport(self):
657        self.prepare_and_start_job('report', active=False, mid_event='read_aio')
658        self.run_job([
659            self.event_error('read', 'report'),
660            self.event_completed('Input/output error', active=False)
661        ])
662
663        self.vm.shutdown()
664        self.assertFalse(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
665                         'target image matches source after error')
666
667    def testIntermediateReadErrorStop(self):
668        self.prepare_and_start_job('stop', active=False, mid_event='read_aio')
669        self.run_job([
670            self.event_error('read', 'stop'),
671            self.event_completed(active=False)
672        ], error_pauses_job=True)
673
674        self.vm.shutdown()
675        self.assertTrue(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
676                        'target image does not match source after commit')
677
678    def testIntermediateReadErrorIgnore(self):
679        self.prepare_and_start_job('ignore', active=False, mid_event='read_aio')
680        self.run_job([
681            self.event_error('read', 'ignore'),
682            self.event_completed(active=False)
683        ])
684
685        # For commit, 'ignore' actually means retry, so this will succeed
686        self.vm.shutdown()
687        self.assertTrue(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
688                        'target image does not match source after commit')
689
690    def testIntermediateWriteErrorReport(self):
691        self.prepare_and_start_job('report', active=False, base_event='write_aio')
692        self.run_job([
693            self.event_error('write', 'report'),
694            self.event_completed('Input/output error', active=False)
695        ])
696
697        self.vm.shutdown()
698        self.assertFalse(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
699                         'target image matches source after error')
700
701    def testIntermediateWriteErrorStop(self):
702        self.prepare_and_start_job('stop', active=False, base_event='write_aio')
703        self.run_job([
704            self.event_error('write', 'stop'),
705            self.event_completed(active=False)
706        ], error_pauses_job=True)
707
708        self.vm.shutdown()
709        self.assertTrue(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
710                        'target image does not match source after commit')
711
712    def testIntermediateWriteErrorIgnore(self):
713        self.prepare_and_start_job('ignore', active=False, base_event='write_aio')
714        self.run_job([
715            self.event_error('write', 'ignore'),
716            self.event_completed(active=False)
717        ])
718
719        # For commit, 'ignore' actually means retry, so this will succeed
720        self.vm.shutdown()
721        self.assertTrue(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
722                        'target image does not match source after commit')
723
724class TestCommitWithFilters(iotests.QMPTestCase):
725    img0 = os.path.join(iotests.test_dir, '0.img')
726    img1 = os.path.join(iotests.test_dir, '1.img')
727    img2 = os.path.join(iotests.test_dir, '2.img')
728    img3 = os.path.join(iotests.test_dir, '3.img')
729
730    def do_test_io(self, read_or_write):
731        for index, pattern_file in enumerate(self.pattern_files):
732            qemu_io('-f', iotests.imgfmt,
733                    '-c',
734                    f'{read_or_write} -P {index + 1} {index}M 1M',
735                    pattern_file)
736
737    @iotests.skip_if_unsupported(['throttle'])
738    def setUp(self):
739        qemu_img('create', '-f', iotests.imgfmt, self.img0, '64M')
740        qemu_img('create', '-f', iotests.imgfmt, self.img1, '64M')
741        qemu_img('create', '-f', iotests.imgfmt, self.img2, '64M')
742        qemu_img('create', '-f', iotests.imgfmt, self.img3, '64M')
743
744        # Distributions of the patterns in the files; this is checked
745        # by tearDown() and should be changed by the test cases as is
746        # necessary
747        self.pattern_files = [self.img0, self.img1, self.img2, self.img3]
748
749        self.do_test_io('write')
750
751        self.vm = iotests.VM().add_device('virtio-scsi,id=vio-scsi')
752        self.vm.launch()
753
754        self.vm.cmd('object-add', qom_type='throttle-group', id='tg')
755
756        self.vm.cmd('blockdev-add', {
757                'node-name': 'top-filter',
758                'driver': 'throttle',
759                'throttle-group': 'tg',
760                'file': {
761                    'node-name': 'cow-3',
762                    'driver': iotests.imgfmt,
763                    'file': {
764                        'driver': 'file',
765                        'filename': self.img3
766                    },
767                    'backing': {
768                        'node-name': 'cow-2',
769                        'driver': iotests.imgfmt,
770                        'file': {
771                            'driver': 'file',
772                            'filename': self.img2
773                        },
774                        'backing': {
775                            'node-name': 'cow-1',
776                            'driver': iotests.imgfmt,
777                            'file': {
778                                'driver': 'file',
779                                'filename': self.img1
780                            },
781                            'backing': {
782                                'node-name': 'bottom-filter',
783                                'driver': 'throttle',
784                                'throttle-group': 'tg',
785                                'file': {
786                                    'node-name': 'cow-0',
787                                    'driver': iotests.imgfmt,
788                                    'file': {
789                                        'driver': 'file',
790                                        'filename': self.img0
791                                    }
792                                }
793                            }
794                        }
795                    }
796                }
797            })
798
799    def tearDown(self):
800        self.vm.shutdown()
801        self.do_test_io('read')
802
803        os.remove(self.img3)
804        os.remove(self.img2)
805        os.remove(self.img1)
806        os.remove(self.img0)
807
808    # Filters make for funny filenames, so we cannot just use
809    # self.imgX to get them
810    def get_filename(self, node):
811        return self.vm.node_info(node)['image']['filename']
812
813    def test_filterless_commit(self):
814        self.vm.cmd('block-commit',
815                    job_id='commit',
816                    device='top-filter',
817                    top_node='cow-2',
818                    base_node='cow-1',
819                    backing_file=self.img1)
820        self.wait_until_completed(drive='commit')
821
822        self.assertIsNotNone(self.vm.node_info('cow-3'))
823        self.assertIsNone(self.vm.node_info('cow-2'))
824        self.assertIsNotNone(self.vm.node_info('cow-1'))
825
826        # 2 has been committed into 1
827        self.pattern_files[2] = self.img1
828
829    def test_commit_through_filter(self):
830        self.vm.cmd('block-commit',
831                    job_id='commit',
832                    device='top-filter',
833                    top_node='cow-1',
834                    base_node='cow-0',
835                    backing_file=self.img0)
836        self.wait_until_completed(drive='commit')
837
838        self.assertIsNotNone(self.vm.node_info('cow-2'))
839        self.assertIsNone(self.vm.node_info('cow-1'))
840        self.assertIsNone(self.vm.node_info('bottom-filter'))
841        self.assertIsNotNone(self.vm.node_info('cow-0'))
842
843        # 1 has been committed into 0
844        self.pattern_files[1] = self.img0
845
846    def test_filtered_active_commit_with_filter(self):
847        # Add a device, so the commit job finds a parent it can change
848        # to point to the base node (so we can test that top-filter is
849        # dropped from the graph)
850        self.vm.cmd('device_add', id='drv0', driver='scsi-hd',
851                    bus='vio-scsi.0', drive='top-filter')
852
853        # Try to release our reference to top-filter; that should not
854        # work because drv0 uses it
855        result = self.vm.qmp('blockdev-del', node_name='top-filter')
856        self.assert_qmp(result, 'error/class', 'GenericError')
857        self.assert_qmp(result, 'error/desc', 'Node top-filter is in use')
858
859        self.vm.cmd('block-commit',
860                    job_id='commit',
861                    device='top-filter',
862                    base_node='cow-2')
863        self.complete_and_wait(drive='commit')
864
865        # Try to release our reference to top-filter again
866        self.vm.cmd('blockdev-del', node_name='top-filter')
867
868        self.assertIsNone(self.vm.node_info('top-filter'))
869        self.assertIsNone(self.vm.node_info('cow-3'))
870        self.assertIsNotNone(self.vm.node_info('cow-2'))
871
872        # Check that drv0 is now connected to cow-2
873        blockdevs = self.vm.qmp('query-block')['return']
874        drv0 = next(dev for dev in blockdevs if dev['qdev'] == 'drv0')
875        self.assertEqual(drv0['inserted']['node-name'], 'cow-2')
876
877        # 3 has been committed into 2
878        self.pattern_files[3] = self.img2
879
880    def test_filtered_active_commit_without_filter(self):
881        self.vm.cmd('block-commit',
882                    job_id='commit',
883                    device='top-filter',
884                    top_node='cow-3',
885                    base_node='cow-2')
886        self.complete_and_wait(drive='commit')
887
888        self.assertIsNotNone(self.vm.node_info('top-filter'))
889        self.assertIsNone(self.vm.node_info('cow-3'))
890        self.assertIsNotNone(self.vm.node_info('cow-2'))
891
892        # 3 has been committed into 2
893        self.pattern_files[3] = self.img2
894
895class TestCommitWithOverriddenBacking(iotests.QMPTestCase):
896    img_base_a = os.path.join(iotests.test_dir, 'base_a.img')
897    img_base_b = os.path.join(iotests.test_dir, 'base_b.img')
898    img_top = os.path.join(iotests.test_dir, 'top.img')
899
900    def setUp(self):
901        qemu_img('create', '-f', iotests.imgfmt, self.img_base_a, '1M')
902        qemu_img('create', '-f', iotests.imgfmt, self.img_base_b, '1M')
903        qemu_img('create', '-f', iotests.imgfmt, '-b', self.img_base_a,
904                 '-F', iotests.imgfmt, self.img_top)
905
906        self.vm = iotests.VM()
907        self.vm.launch()
908
909        # Use base_b instead of base_a as the backing of top
910        self.vm.cmd('blockdev-add', {
911                        'node-name': 'top',
912                        'driver': iotests.imgfmt,
913                        'file': {
914                            'driver': 'file',
915                            'filename': self.img_top
916                        },
917                        'backing': {
918                            'node-name': 'base',
919                            'driver': iotests.imgfmt,
920                            'file': {
921                                'driver': 'file',
922                                'filename': self.img_base_b
923                            }
924                        }
925                    })
926
927    def tearDown(self):
928        self.vm.shutdown()
929        os.remove(self.img_top)
930        os.remove(self.img_base_a)
931        os.remove(self.img_base_b)
932
933    def test_commit_to_a(self):
934        # Try committing to base_a (which should fail, as top's
935        # backing image is base_b instead)
936        result = self.vm.qmp('block-commit',
937                             job_id='commit',
938                             device='top',
939                             base=self.img_base_a)
940        self.assert_qmp(result, 'error/class', 'GenericError')
941
942    def test_commit_to_b(self):
943        # Try committing to base_b (which should work, since that is
944        # actually top's backing image)
945        self.vm.cmd('block-commit',
946                    job_id='commit',
947                    device='top',
948                    base=self.img_base_b)
949
950        self.vm.event_wait('BLOCK_JOB_READY')
951        self.vm.qmp('block-job-complete', device='commit')
952        self.vm.event_wait('BLOCK_JOB_COMPLETED')
953
954if __name__ == '__main__':
955    iotests.main(supported_fmts=['qcow2', 'qed'],
956                 supported_protocols=['file'])
957