xref: /openbmc/qemu/tests/qemu-iotests/056 (revision 6e0bc06e)
1#!/usr/bin/env python3
2# group: rw backing
3#
4# Tests for drive-backup
5#
6# Copyright (C) 2013 Red Hat, Inc.
7#
8# Based on 041.
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
24import time
25import os
26import iotests
27from iotests import qemu_img, qemu_io, create_image
28
29backing_img = os.path.join(iotests.test_dir, 'backing.img')
30test_img = os.path.join(iotests.test_dir, 'test.img')
31target_img = os.path.join(iotests.test_dir, 'target.img')
32
33def img_create(img, fmt=iotests.imgfmt, size='64M', **kwargs):
34    fullname = os.path.join(iotests.test_dir, '%s.%s' % (img, fmt))
35    optargs = []
36    for k,v in kwargs.items():
37        optargs = optargs + ['-o', '%s=%s' % (k,v)]
38    args = ['create', '-f', fmt] + optargs + [fullname, size]
39    iotests.qemu_img(*args)
40    return fullname
41
42def try_remove(img):
43    try:
44        os.remove(img)
45    except OSError:
46        pass
47
48def io_write_patterns(img, patterns):
49    for pattern in patterns:
50        iotests.qemu_io('-c', 'write -P%s %s %s' % pattern, img)
51
52
53class TestSyncModesNoneAndTop(iotests.QMPTestCase):
54    image_len = 64 * 1024 * 1024 # MB
55
56    def setUp(self):
57        create_image(backing_img, TestSyncModesNoneAndTop.image_len)
58        qemu_img('create', '-f', iotests.imgfmt,
59                 '-o', 'backing_file=%s' % backing_img, '-F', 'raw', test_img)
60        qemu_io('-c', 'write -P0x41 0 512', test_img)
61        qemu_io('-c', 'write -P0xd5 1M 32k', test_img)
62        qemu_io('-c', 'write -P0xdc 32M 124k', test_img)
63        qemu_io('-c', 'write -P0xdc 67043328 64k', test_img)
64        self.vm = iotests.VM().add_drive(test_img)
65        self.vm.launch()
66
67    def tearDown(self):
68        self.vm.shutdown()
69        os.remove(test_img)
70        os.remove(backing_img)
71        try:
72            os.remove(target_img)
73        except OSError:
74            pass
75
76    def test_complete_top(self):
77        self.assert_no_active_block_jobs()
78        result = self.vm.qmp('drive-backup', device='drive0', sync='top',
79                             format=iotests.imgfmt, target=target_img)
80        self.assert_qmp(result, 'return', {})
81
82        self.wait_until_completed(check_offset=False)
83
84        self.assert_no_active_block_jobs()
85        self.vm.shutdown()
86        self.assertTrue(iotests.compare_images(test_img, target_img),
87                        'target image does not match source after backup')
88
89    def test_cancel_sync_none(self):
90        self.assert_no_active_block_jobs()
91
92        result = self.vm.qmp('drive-backup', device='drive0',
93                             sync='none', target=target_img)
94        self.assert_qmp(result, 'return', {})
95        time.sleep(1)
96        self.vm.hmp_qemu_io('drive0', 'write -P0x5e 0 512')
97        self.vm.hmp_qemu_io('drive0', 'aio_flush')
98        # Verify that the original contents exist in the target image.
99
100        event = self.cancel_and_wait()
101        self.assert_qmp(event, 'data/type', 'backup')
102
103        self.vm.shutdown()
104        time.sleep(1)
105        self.assertEqual(-1, qemu_io('-c', 'read -P0x41 0 512', target_img).find("verification failed"))
106
107class TestBeforeWriteNotifier(iotests.QMPTestCase):
108    def setUp(self):
109        self.vm = iotests.VM().add_drive_raw("file=blkdebug::null-co://,id=drive0,align=65536,driver=blkdebug")
110        self.vm.launch()
111
112    def tearDown(self):
113        self.vm.shutdown()
114        os.remove(target_img)
115
116    def test_before_write_notifier(self):
117        self.vm.pause_drive("drive0")
118        result = self.vm.qmp('drive-backup', device='drive0',
119                             sync='full', target=target_img,
120                             format="file", speed=1)
121        self.assert_qmp(result, 'return', {})
122        result = self.vm.qmp('block-job-pause', device="drive0")
123        self.assert_qmp(result, 'return', {})
124        # Speed is low enough that this must be an uncopied range, which will
125        # trigger the before write notifier
126        self.vm.hmp_qemu_io('drive0', 'aio_write -P 1 512512 512')
127        self.vm.resume_drive("drive0")
128        result = self.vm.qmp('block-job-resume', device="drive0")
129        self.assert_qmp(result, 'return', {})
130        event = self.cancel_and_wait()
131        self.assert_qmp(event, 'data/type', 'backup')
132
133class BackupTest(iotests.QMPTestCase):
134    def setUp(self):
135        self.vm = iotests.VM()
136        self.test_img = img_create('test')
137        self.dest_img = img_create('dest')
138        self.dest_img2 = img_create('dest2')
139        self.ref_img = img_create('ref')
140        self.vm.add_drive(self.test_img)
141        self.vm.launch()
142
143    def tearDown(self):
144        self.vm.shutdown()
145        try_remove(self.test_img)
146        try_remove(self.dest_img)
147        try_remove(self.dest_img2)
148        try_remove(self.ref_img)
149
150    def hmp_io_writes(self, drive, patterns):
151        for pattern in patterns:
152            self.vm.hmp_qemu_io(drive, 'write -P%s %s %s' % pattern)
153        self.vm.hmp_qemu_io(drive, 'flush')
154
155    def qmp_backup_and_wait(self, cmd='drive-backup', serror=None,
156                            aerror=None, **kwargs):
157        if not self.qmp_backup(cmd, serror, **kwargs):
158            return False
159        return self.qmp_backup_wait(kwargs['device'], aerror)
160
161    def qmp_backup(self, cmd='drive-backup',
162                   error=None, **kwargs):
163        self.assertTrue('device' in kwargs)
164        res = self.vm.qmp(cmd, **kwargs)
165        if error:
166            self.assert_qmp(res, 'error/desc', error)
167            return False
168        self.assert_qmp(res, 'return', {})
169        return True
170
171    def qmp_backup_wait(self, device, error=None):
172        event = self.vm.event_wait(name="BLOCK_JOB_COMPLETED",
173                                   match={'data': {'device': device}})
174        self.assertNotEqual(event, None)
175        try:
176            failure = self.dictpath(event, 'data/error')
177        except AssertionError:
178            # Backup succeeded.
179            self.assert_qmp(event, 'data/offset', event['data']['len'])
180            return True
181        else:
182            # Failure.
183            self.assert_qmp(event, 'data/error', qerror)
184            return False
185
186    def test_overlapping_writes(self):
187        # Write something to back up
188        self.hmp_io_writes('drive0', [('42', '0M', '2M')])
189
190        # Create a reference backup
191        self.qmp_backup_and_wait(device='drive0', format=iotests.imgfmt,
192                                 sync='full', target=self.ref_img,
193                                 auto_dismiss=False)
194        res = self.vm.qmp('block-job-dismiss', id='drive0')
195        self.assert_qmp(res, 'return', {})
196
197        # Now to the test backup: We simulate the following guest
198        # writes:
199        # (1) [1M + 64k, 1M + 128k): Afterwards, everything in that
200        #     area should be in the target image, and we must not copy
201        #     it again (because the source image has changed now)
202        #     (64k is the job's cluster size)
203        # (2) [1M, 2M): The backup job must not get overeager.  It
204        #     must copy [1M, 1M + 64k) and [1M + 128k, 2M) separately,
205        #     but not the area in between.
206
207        self.qmp_backup(device='drive0', format=iotests.imgfmt, sync='full',
208                        target=self.dest_img, speed=1, auto_dismiss=False)
209
210        self.hmp_io_writes('drive0', [('23', '%ik' % (1024 + 64), '64k'),
211                                      ('66', '1M', '1M')])
212
213        # Let the job complete
214        res = self.vm.qmp('block-job-set-speed', device='drive0', speed=0)
215        self.assert_qmp(res, 'return', {})
216        self.qmp_backup_wait('drive0')
217        res = self.vm.qmp('block-job-dismiss', id='drive0')
218        self.assert_qmp(res, 'return', {})
219
220        self.assertTrue(iotests.compare_images(self.ref_img, self.dest_img),
221                        'target image does not match reference image')
222
223    def test_dismiss_false(self):
224        res = self.vm.qmp('query-block-jobs')
225        self.assert_qmp(res, 'return', [])
226        self.qmp_backup_and_wait(device='drive0', format=iotests.imgfmt,
227                                 sync='full', target=self.dest_img,
228                                 auto_dismiss=True)
229        res = self.vm.qmp('query-block-jobs')
230        self.assert_qmp(res, 'return', [])
231
232    def test_dismiss_true(self):
233        res = self.vm.qmp('query-block-jobs')
234        self.assert_qmp(res, 'return', [])
235        self.qmp_backup_and_wait(device='drive0', format=iotests.imgfmt,
236                                 sync='full', target=self.dest_img,
237                                 auto_dismiss=False)
238        res = self.vm.qmp('query-block-jobs')
239        self.assert_qmp(res, 'return[0]/status', 'concluded')
240        res = self.vm.qmp('block-job-dismiss', id='drive0')
241        self.assert_qmp(res, 'return', {})
242        res = self.vm.qmp('query-block-jobs')
243        self.assert_qmp(res, 'return', [])
244
245    def test_dismiss_bad_id(self):
246        res = self.vm.qmp('query-block-jobs')
247        self.assert_qmp(res, 'return', [])
248        res = self.vm.qmp('block-job-dismiss', id='foobar')
249        self.assert_qmp(res, 'error/class', 'DeviceNotActive')
250
251    def test_dismiss_collision(self):
252        res = self.vm.qmp('query-block-jobs')
253        self.assert_qmp(res, 'return', [])
254        self.qmp_backup_and_wait(device='drive0', format=iotests.imgfmt,
255                                 sync='full', target=self.dest_img,
256                                 auto_dismiss=False)
257        res = self.vm.qmp('query-block-jobs')
258        self.assert_qmp(res, 'return[0]/status', 'concluded')
259        # Leave zombie job un-dismissed, observe a failure:
260        res = self.qmp_backup_and_wait(serror="Job ID 'drive0' already in use",
261                                       device='drive0', format=iotests.imgfmt,
262                                       sync='full', target=self.dest_img2,
263                                       auto_dismiss=False)
264        self.assertEqual(res, False)
265        # OK, dismiss the zombie.
266        res = self.vm.qmp('block-job-dismiss', id='drive0')
267        self.assert_qmp(res, 'return', {})
268        res = self.vm.qmp('query-block-jobs')
269        self.assert_qmp(res, 'return', [])
270        # Ensure it's really gone.
271        self.qmp_backup_and_wait(device='drive0', format=iotests.imgfmt,
272                                 sync='full', target=self.dest_img2,
273                                 auto_dismiss=False)
274
275    def dismissal_failure(self, dismissal_opt):
276        res = self.vm.qmp('query-block-jobs')
277        self.assert_qmp(res, 'return', [])
278        # Give blkdebug something to chew on
279        self.hmp_io_writes('drive0',
280                           (('0x9a', 0, 512),
281                           ('0x55', '8M', '352k'),
282                           ('0x78', '15872k', '1M')))
283        # Add destination node via blkdebug
284        res = self.vm.qmp('blockdev-add',
285                          node_name='target0',
286                          driver=iotests.imgfmt,
287                          file={
288                              'driver': 'blkdebug',
289                              'image': {
290                                  'driver': 'file',
291                                  'filename': self.dest_img
292                              },
293                              'inject-error': [{
294                                  'event': 'write_aio',
295                                  'errno': 5,
296                                  'immediately': False,
297                                  'once': True
298                              }],
299                          })
300        self.assert_qmp(res, 'return', {})
301
302        res = self.qmp_backup(cmd='blockdev-backup',
303                              device='drive0', target='target0',
304                              on_target_error='stop',
305                              sync='full',
306                              auto_dismiss=dismissal_opt)
307        self.assertTrue(res)
308        event = self.vm.event_wait(name="BLOCK_JOB_ERROR",
309                                   match={'data': {'device': 'drive0'}})
310        self.assertNotEqual(event, None)
311        # OK, job should pause, but it can't do it immediately, as it can't
312        # cancel other parallel requests (which didn't fail)
313        with iotests.Timeout(60, "Timeout waiting for backup actually paused"):
314            while True:
315                res = self.vm.qmp('query-block-jobs')
316                if res['return'][0]['status'] == 'paused':
317                    break
318        self.assert_qmp(res, 'return[0]/status', 'paused')
319        res = self.vm.qmp('block-job-dismiss', id='drive0')
320        self.assert_qmp(res, 'error/desc',
321                        "Job 'drive0' in state 'paused' cannot accept"
322                        " command verb 'dismiss'")
323        res = self.vm.qmp('query-block-jobs')
324        self.assert_qmp(res, 'return[0]/status', 'paused')
325        # OK, unstick job and move forward.
326        res = self.vm.qmp('block-job-resume', device='drive0')
327        self.assert_qmp(res, 'return', {})
328        # And now we need to wait for it to conclude;
329        res = self.qmp_backup_wait(device='drive0')
330        self.assertTrue(res)
331        if not dismissal_opt:
332            # Job should now be languishing:
333            res = self.vm.qmp('query-block-jobs')
334            self.assert_qmp(res, 'return[0]/status', 'concluded')
335            res = self.vm.qmp('block-job-dismiss', id='drive0')
336            self.assert_qmp(res, 'return', {})
337            res = self.vm.qmp('query-block-jobs')
338            self.assert_qmp(res, 'return', [])
339
340    def test_dismiss_premature(self):
341        self.dismissal_failure(False)
342
343    def test_dismiss_erroneous(self):
344        self.dismissal_failure(True)
345
346if __name__ == '__main__':
347    iotests.main(supported_fmts=['qcow2', 'qed'],
348                 supported_protocols=['file'])
349