xref: /openbmc/qemu/tests/qemu-iotests/124 (revision 61b9251a)
1#!/usr/bin/env python
2#
3# Tests for incremental drive-backup
4#
5# Copyright (C) 2015 John Snow for Red Hat, Inc.
6#
7# Based on 056.
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
23import os
24import iotests
25
26
27def io_write_patterns(img, patterns):
28    for pattern in patterns:
29        iotests.qemu_io('-c', 'write -P%s %s %s' % pattern, img)
30
31
32def try_remove(img):
33    try:
34        os.remove(img)
35    except OSError:
36        pass
37
38
39def transaction_action(action, **kwargs):
40    return {
41        'type': action,
42        'data': dict((k.replace('_', '-'), v) for k, v in kwargs.iteritems())
43    }
44
45
46def transaction_bitmap_clear(node, name, **kwargs):
47    return transaction_action('block-dirty-bitmap-clear',
48                              node=node, name=name, **kwargs)
49
50
51def transaction_drive_backup(device, target, **kwargs):
52    return transaction_action('drive-backup', device=device, target=target,
53                              **kwargs)
54
55
56class Bitmap:
57    def __init__(self, name, drive):
58        self.name = name
59        self.drive = drive
60        self.num = 0
61        self.backups = list()
62
63    def base_target(self):
64        return (self.drive['backup'], None)
65
66    def new_target(self, num=None):
67        if num is None:
68            num = self.num
69        self.num = num + 1
70        base = os.path.join(iotests.test_dir,
71                            "%s.%s." % (self.drive['id'], self.name))
72        suff = "%i.%s" % (num, self.drive['fmt'])
73        target = base + "inc" + suff
74        reference = base + "ref" + suff
75        self.backups.append((target, reference))
76        return (target, reference)
77
78    def last_target(self):
79        if self.backups:
80            return self.backups[-1]
81        return self.base_target()
82
83    def del_target(self):
84        for image in self.backups.pop():
85            try_remove(image)
86        self.num -= 1
87
88    def cleanup(self):
89        for backup in self.backups:
90            for image in backup:
91                try_remove(image)
92
93
94class TestIncrementalBackup(iotests.QMPTestCase):
95    def setUp(self):
96        self.bitmaps = list()
97        self.files = list()
98        self.drives = list()
99        self.vm = iotests.VM()
100        self.err_img = os.path.join(iotests.test_dir, 'err.%s' % iotests.imgfmt)
101
102        # Create a base image with a distinctive patterning
103        drive0 = self.add_node('drive0')
104        self.img_create(drive0['file'], drive0['fmt'])
105        self.vm.add_drive(drive0['file'])
106        io_write_patterns(drive0['file'], (('0x41', 0, 512),
107                                           ('0xd5', '1M', '32k'),
108                                           ('0xdc', '32M', '124k')))
109        self.vm.launch()
110
111
112    def add_node(self, node_id, fmt=iotests.imgfmt, path=None, backup=None):
113        if path is None:
114            path = os.path.join(iotests.test_dir, '%s.%s' % (node_id, fmt))
115        if backup is None:
116            backup = os.path.join(iotests.test_dir,
117                                  '%s.full.backup.%s' % (node_id, fmt))
118
119        self.drives.append({
120            'id': node_id,
121            'file': path,
122            'backup': backup,
123            'fmt': fmt })
124        return self.drives[-1]
125
126
127    def img_create(self, img, fmt=iotests.imgfmt, size='64M',
128                   parent=None, parentFormat=None):
129        if parent:
130            if parentFormat is None:
131                parentFormat = fmt
132            iotests.qemu_img('create', '-f', fmt, img, size,
133                             '-b', parent, '-F', parentFormat)
134        else:
135            iotests.qemu_img('create', '-f', fmt, img, size)
136        self.files.append(img)
137
138
139    def do_qmp_backup(self, error='Input/output error', **kwargs):
140        res = self.vm.qmp('drive-backup', **kwargs)
141        self.assert_qmp(res, 'return', {})
142        return self.wait_qmp_backup(kwargs['device'], error)
143
144
145    def wait_qmp_backup(self, device, error='Input/output error'):
146        event = self.vm.event_wait(name="BLOCK_JOB_COMPLETED",
147                                   match={'data': {'device': device}})
148        self.assertNotEqual(event, None)
149
150        try:
151            failure = self.dictpath(event, 'data/error')
152        except AssertionError:
153            # Backup succeeded.
154            self.assert_qmp(event, 'data/offset', event['data']['len'])
155            return True
156        else:
157            # Backup failed.
158            self.assert_qmp(event, 'data/error', error)
159            return False
160
161
162    def wait_qmp_backup_cancelled(self, device):
163        event = self.vm.event_wait(name='BLOCK_JOB_CANCELLED',
164                                   match={'data': {'device': device}})
165        self.assertNotEqual(event, None)
166
167
168    def create_anchor_backup(self, drive=None):
169        if drive is None:
170            drive = self.drives[-1]
171        res = self.do_qmp_backup(device=drive['id'], sync='full',
172                                 format=drive['fmt'], target=drive['backup'])
173        self.assertTrue(res)
174        self.files.append(drive['backup'])
175        return drive['backup']
176
177
178    def make_reference_backup(self, bitmap=None):
179        if bitmap is None:
180            bitmap = self.bitmaps[-1]
181        _, reference = bitmap.last_target()
182        res = self.do_qmp_backup(device=bitmap.drive['id'], sync='full',
183                                 format=bitmap.drive['fmt'], target=reference)
184        self.assertTrue(res)
185
186
187    def add_bitmap(self, name, drive, **kwargs):
188        bitmap = Bitmap(name, drive)
189        self.bitmaps.append(bitmap)
190        result = self.vm.qmp('block-dirty-bitmap-add', node=drive['id'],
191                             name=bitmap.name, **kwargs)
192        self.assert_qmp(result, 'return', {})
193        return bitmap
194
195
196    def prepare_backup(self, bitmap=None, parent=None):
197        if bitmap is None:
198            bitmap = self.bitmaps[-1]
199        if parent is None:
200            parent, _ = bitmap.last_target()
201
202        target, _ = bitmap.new_target()
203        self.img_create(target, bitmap.drive['fmt'], parent=parent)
204        return target
205
206
207    def create_incremental(self, bitmap=None, parent=None,
208                           parentFormat=None, validate=True):
209        if bitmap is None:
210            bitmap = self.bitmaps[-1]
211        if parent is None:
212            parent, _ = bitmap.last_target()
213
214        target = self.prepare_backup(bitmap, parent)
215        res = self.do_qmp_backup(device=bitmap.drive['id'],
216                                 sync='incremental', bitmap=bitmap.name,
217                                 format=bitmap.drive['fmt'], target=target,
218                                 mode='existing')
219        if not res:
220            bitmap.del_target();
221            self.assertFalse(validate)
222        else:
223            self.make_reference_backup(bitmap)
224        return res
225
226
227    def check_backups(self):
228        for bitmap in self.bitmaps:
229            for incremental, reference in bitmap.backups:
230                self.assertTrue(iotests.compare_images(incremental, reference))
231            last = bitmap.last_target()[0]
232            self.assertTrue(iotests.compare_images(last, bitmap.drive['file']))
233
234
235    def hmp_io_writes(self, drive, patterns):
236        for pattern in patterns:
237            self.vm.hmp_qemu_io(drive, 'write -P%s %s %s' % pattern)
238        self.vm.hmp_qemu_io(drive, 'flush')
239
240
241    def do_incremental_simple(self, **kwargs):
242        self.create_anchor_backup()
243        self.add_bitmap('bitmap0', self.drives[0], **kwargs)
244
245        # Sanity: Create a "hollow" incremental backup
246        self.create_incremental()
247        # Three writes: One complete overwrite, one new segment,
248        # and one partial overlap.
249        self.hmp_io_writes(self.drives[0]['id'], (('0xab', 0, 512),
250                                                  ('0xfe', '16M', '256k'),
251                                                  ('0x64', '32736k', '64k')))
252        self.create_incremental()
253        # Three more writes, one of each kind, like above
254        self.hmp_io_writes(self.drives[0]['id'], (('0x9a', 0, 512),
255                                                  ('0x55', '8M', '352k'),
256                                                  ('0x78', '15872k', '1M')))
257        self.create_incremental()
258        self.vm.shutdown()
259        self.check_backups()
260
261
262    def test_incremental_simple(self):
263        '''
264        Test: Create and verify three incremental backups.
265
266        Create a bitmap and a full backup before VM execution begins,
267        then create a series of three incremental backups "during execution,"
268        i.e.; after IO requests begin modifying the drive.
269        '''
270        return self.do_incremental_simple()
271
272
273    def test_small_granularity(self):
274        '''
275        Test: Create and verify backups made with a small granularity bitmap.
276
277        Perform the same test as test_incremental_simple, but with a granularity
278        of only 32KiB instead of the present default of 64KiB.
279        '''
280        return self.do_incremental_simple(granularity=32768)
281
282
283    def test_large_granularity(self):
284        '''
285        Test: Create and verify backups made with a large granularity bitmap.
286
287        Perform the same test as test_incremental_simple, but with a granularity
288        of 128KiB instead of the present default of 64KiB.
289        '''
290        return self.do_incremental_simple(granularity=131072)
291
292
293    def test_incremental_transaction(self):
294        '''Test: Verify backups made from transactionally created bitmaps.
295
296        Create a bitmap "before" VM execution begins, then create a second
297        bitmap AFTER writes have already occurred. Use transactions to create
298        a full backup and synchronize both bitmaps to this backup.
299        Create an incremental backup through both bitmaps and verify that
300        both backups match the current drive0 image.
301        '''
302
303        drive0 = self.drives[0]
304        bitmap0 = self.add_bitmap('bitmap0', drive0)
305        self.hmp_io_writes(drive0['id'], (('0xab', 0, 512),
306                                          ('0xfe', '16M', '256k'),
307                                          ('0x64', '32736k', '64k')))
308        bitmap1 = self.add_bitmap('bitmap1', drive0)
309
310        result = self.vm.qmp('transaction', actions=[
311            transaction_bitmap_clear(bitmap0.drive['id'], bitmap0.name),
312            transaction_bitmap_clear(bitmap1.drive['id'], bitmap1.name),
313            transaction_drive_backup(drive0['id'], drive0['backup'],
314                                     sync='full', format=drive0['fmt'])
315        ])
316        self.assert_qmp(result, 'return', {})
317        self.wait_until_completed(drive0['id'])
318        self.files.append(drive0['backup'])
319
320        self.hmp_io_writes(drive0['id'], (('0x9a', 0, 512),
321                                          ('0x55', '8M', '352k'),
322                                          ('0x78', '15872k', '1M')))
323        # Both bitmaps should be correctly in sync.
324        self.create_incremental(bitmap0)
325        self.create_incremental(bitmap1)
326        self.vm.shutdown()
327        self.check_backups()
328
329
330    def test_incremental_failure(self):
331        '''Test: Verify backups made after a failure are correct.
332
333        Simulate a failure during an incremental backup block job,
334        emulate additional writes, then create another incremental backup
335        afterwards and verify that the backup created is correct.
336        '''
337
338        # Create a blkdebug interface to this img as 'drive1',
339        # but don't actually create a new image.
340        drive1 = self.add_node('drive1', self.drives[0]['fmt'],
341                               path=self.drives[0]['file'],
342                               backup=self.drives[0]['backup'])
343        result = self.vm.qmp('blockdev-add', options={
344            'id': drive1['id'],
345            'driver': drive1['fmt'],
346            'file': {
347                'driver': 'blkdebug',
348                'image': {
349                    'driver': 'file',
350                    'filename': drive1['file']
351                },
352                'set-state': [{
353                    'event': 'flush_to_disk',
354                    'state': 1,
355                    'new_state': 2
356                }],
357                'inject-error': [{
358                    'event': 'read_aio',
359                    'errno': 5,
360                    'state': 2,
361                    'immediately': False,
362                    'once': True
363                }],
364            }
365        })
366        self.assert_qmp(result, 'return', {})
367
368        self.create_anchor_backup(self.drives[0])
369        self.add_bitmap('bitmap0', drive1)
370        # Note: at this point, during a normal execution,
371        # Assume that the VM resumes and begins issuing IO requests here.
372
373        self.hmp_io_writes(drive1['id'], (('0xab', 0, 512),
374                                          ('0xfe', '16M', '256k'),
375                                          ('0x64', '32736k', '64k')))
376
377        result = self.create_incremental(validate=False)
378        self.assertFalse(result)
379        self.hmp_io_writes(drive1['id'], (('0x9a', 0, 512),
380                                          ('0x55', '8M', '352k'),
381                                          ('0x78', '15872k', '1M')))
382        self.create_incremental()
383        self.vm.shutdown()
384        self.check_backups()
385
386
387    def test_transaction_failure(self):
388        '''Test: Verify backups made from a transaction that partially fails.
389
390        Add a second drive with its own unique pattern, and add a bitmap to each
391        drive. Use blkdebug to interfere with the backup on just one drive and
392        attempt to create a coherent incremental backup across both drives.
393
394        verify a failure in one but not both, then delete the failed stubs and
395        re-run the same transaction.
396
397        verify that both incrementals are created successfully.
398        '''
399
400        # Create a second drive, with pattern:
401        drive1 = self.add_node('drive1')
402        self.img_create(drive1['file'], drive1['fmt'])
403        io_write_patterns(drive1['file'], (('0x14', 0, 512),
404                                           ('0x5d', '1M', '32k'),
405                                           ('0xcd', '32M', '124k')))
406
407        # Create a blkdebug interface to this img as 'drive1'
408        result = self.vm.qmp('blockdev-add', options={
409            'id': drive1['id'],
410            'driver': drive1['fmt'],
411            'file': {
412                'driver': 'blkdebug',
413                'image': {
414                    'driver': 'file',
415                    'filename': drive1['file']
416                },
417                'set-state': [{
418                    'event': 'flush_to_disk',
419                    'state': 1,
420                    'new_state': 2
421                }],
422                'inject-error': [{
423                    'event': 'read_aio',
424                    'errno': 5,
425                    'state': 2,
426                    'immediately': False,
427                    'once': True
428                }],
429            }
430        })
431        self.assert_qmp(result, 'return', {})
432
433        # Create bitmaps and full backups for both drives
434        drive0 = self.drives[0]
435        dr0bm0 = self.add_bitmap('bitmap0', drive0)
436        dr1bm0 = self.add_bitmap('bitmap0', drive1)
437        self.create_anchor_backup(drive0)
438        self.create_anchor_backup(drive1)
439        self.assert_no_active_block_jobs()
440        self.assertFalse(self.vm.get_qmp_events(wait=False))
441
442        # Emulate some writes
443        self.hmp_io_writes(drive0['id'], (('0xab', 0, 512),
444                                          ('0xfe', '16M', '256k'),
445                                          ('0x64', '32736k', '64k')))
446        self.hmp_io_writes(drive1['id'], (('0xba', 0, 512),
447                                          ('0xef', '16M', '256k'),
448                                          ('0x46', '32736k', '64k')))
449
450        # Create incremental backup targets
451        target0 = self.prepare_backup(dr0bm0)
452        target1 = self.prepare_backup(dr1bm0)
453
454        # Ask for a new incremental backup per-each drive,
455        # expecting drive1's backup to fail:
456        transaction = [
457            transaction_drive_backup(drive0['id'], target0, sync='incremental',
458                                     format=drive0['fmt'], mode='existing',
459                                     bitmap=dr0bm0.name),
460            transaction_drive_backup(drive1['id'], target1, sync='incremental',
461                                     format=drive1['fmt'], mode='existing',
462                                     bitmap=dr1bm0.name)
463        ]
464        result = self.vm.qmp('transaction', actions=transaction,
465                             properties={'completion-mode': 'grouped'} )
466        self.assert_qmp(result, 'return', {})
467
468        # Observe that drive0's backup is cancelled and drive1 completes with
469        # an error.
470        self.wait_qmp_backup_cancelled(drive0['id'])
471        self.assertFalse(self.wait_qmp_backup(drive1['id']))
472        error = self.vm.event_wait('BLOCK_JOB_ERROR')
473        self.assert_qmp(error, 'data', {'device': drive1['id'],
474                                        'action': 'report',
475                                        'operation': 'read'})
476        self.assertFalse(self.vm.get_qmp_events(wait=False))
477        self.assert_no_active_block_jobs()
478
479        # Delete drive0's successful target and eliminate our record of the
480        # unsuccessful drive1 target. Then re-run the same transaction.
481        dr0bm0.del_target()
482        dr1bm0.del_target()
483        target0 = self.prepare_backup(dr0bm0)
484        target1 = self.prepare_backup(dr1bm0)
485
486        # Re-run the exact same transaction.
487        result = self.vm.qmp('transaction', actions=transaction,
488                             properties={'completion-mode':'grouped'})
489        self.assert_qmp(result, 'return', {})
490
491        # Both should complete successfully this time.
492        self.assertTrue(self.wait_qmp_backup(drive0['id']))
493        self.assertTrue(self.wait_qmp_backup(drive1['id']))
494        self.make_reference_backup(dr0bm0)
495        self.make_reference_backup(dr1bm0)
496        self.assertFalse(self.vm.get_qmp_events(wait=False))
497        self.assert_no_active_block_jobs()
498
499        # And the images should of course validate.
500        self.vm.shutdown()
501        self.check_backups()
502
503
504    def test_sync_dirty_bitmap_missing(self):
505        self.assert_no_active_block_jobs()
506        self.files.append(self.err_img)
507        result = self.vm.qmp('drive-backup', device=self.drives[0]['id'],
508                             sync='incremental', format=self.drives[0]['fmt'],
509                             target=self.err_img)
510        self.assert_qmp(result, 'error/class', 'GenericError')
511
512
513    def test_sync_dirty_bitmap_not_found(self):
514        self.assert_no_active_block_jobs()
515        self.files.append(self.err_img)
516        result = self.vm.qmp('drive-backup', device=self.drives[0]['id'],
517                             sync='incremental', bitmap='unknown',
518                             format=self.drives[0]['fmt'], target=self.err_img)
519        self.assert_qmp(result, 'error/class', 'GenericError')
520
521
522    def test_sync_dirty_bitmap_bad_granularity(self):
523        '''
524        Test: Test what happens if we provide an improper granularity.
525
526        The granularity must always be a power of 2.
527        '''
528        self.assert_no_active_block_jobs()
529        self.assertRaises(AssertionError, self.add_bitmap,
530                          'bitmap0', self.drives[0],
531                          granularity=64000)
532
533
534    def tearDown(self):
535        self.vm.shutdown()
536        for bitmap in self.bitmaps:
537            bitmap.cleanup()
538        for filename in self.files:
539            try_remove(filename)
540
541
542if __name__ == '__main__':
543    iotests.main(supported_fmts=['qcow2'])
544