xref: /qemu/tests/qemu-iotests/124 (revision bf957284)
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
39class Bitmap:
40    def __init__(self, name, drive):
41        self.name = name
42        self.drive = drive
43        self.num = 0
44        self.backups = list()
45
46    def base_target(self):
47        return (self.drive['backup'], None)
48
49    def new_target(self, num=None):
50        if num is None:
51            num = self.num
52        self.num = num + 1
53        base = os.path.join(iotests.test_dir,
54                            "%s.%s." % (self.drive['id'], self.name))
55        suff = "%i.%s" % (num, self.drive['fmt'])
56        target = base + "inc" + suff
57        reference = base + "ref" + suff
58        self.backups.append((target, reference))
59        return (target, reference)
60
61    def last_target(self):
62        if self.backups:
63            return self.backups[-1]
64        return self.base_target()
65
66    def del_target(self):
67        for image in self.backups.pop():
68            try_remove(image)
69        self.num -= 1
70
71    def cleanup(self):
72        for backup in self.backups:
73            for image in backup:
74                try_remove(image)
75
76
77class TestIncrementalBackup(iotests.QMPTestCase):
78    def setUp(self):
79        self.bitmaps = list()
80        self.files = list()
81        self.drives = list()
82        self.vm = iotests.VM()
83        self.err_img = os.path.join(iotests.test_dir, 'err.%s' % iotests.imgfmt)
84
85        # Create a base image with a distinctive patterning
86        drive0 = self.add_node('drive0')
87        self.img_create(drive0['file'], drive0['fmt'])
88        self.vm.add_drive(drive0['file'])
89        io_write_patterns(drive0['file'], (('0x41', 0, 512),
90                                           ('0xd5', '1M', '32k'),
91                                           ('0xdc', '32M', '124k')))
92        self.vm.launch()
93
94
95    def add_node(self, node_id, fmt=iotests.imgfmt, path=None, backup=None):
96        if path is None:
97            path = os.path.join(iotests.test_dir, '%s.%s' % (node_id, fmt))
98        if backup is None:
99            backup = os.path.join(iotests.test_dir,
100                                  '%s.full.backup.%s' % (node_id, fmt))
101
102        self.drives.append({
103            'id': node_id,
104            'file': path,
105            'backup': backup,
106            'fmt': fmt })
107        return self.drives[-1]
108
109
110    def img_create(self, img, fmt=iotests.imgfmt, size='64M',
111                   parent=None, parentFormat=None):
112        if parent:
113            if parentFormat is None:
114                parentFormat = fmt
115            iotests.qemu_img('create', '-f', fmt, img, size,
116                             '-b', parent, '-F', parentFormat)
117        else:
118            iotests.qemu_img('create', '-f', fmt, img, size)
119        self.files.append(img)
120
121
122    def do_qmp_backup(self, error='Input/output error', **kwargs):
123        res = self.vm.qmp('drive-backup', **kwargs)
124        self.assert_qmp(res, 'return', {})
125
126        event = self.vm.event_wait(name="BLOCK_JOB_COMPLETED",
127                                   match={'data': {'device': kwargs['device']}})
128        self.assertNotEqual(event, None)
129
130        try:
131            failure = self.dictpath(event, 'data/error')
132        except AssertionError:
133            # Backup succeeded.
134            self.assert_qmp(event, 'data/offset', event['data']['len'])
135            return True
136        else:
137            # Backup failed.
138            self.assert_qmp(event, 'data/error', error)
139            return False
140
141
142    def create_anchor_backup(self, drive=None):
143        if drive is None:
144            drive = self.drives[-1]
145        res = self.do_qmp_backup(device=drive['id'], sync='full',
146                                 format=drive['fmt'], target=drive['backup'])
147        self.assertTrue(res)
148        self.files.append(drive['backup'])
149        return drive['backup']
150
151
152    def make_reference_backup(self, bitmap=None):
153        if bitmap is None:
154            bitmap = self.bitmaps[-1]
155        _, reference = bitmap.last_target()
156        res = self.do_qmp_backup(device=bitmap.drive['id'], sync='full',
157                                 format=bitmap.drive['fmt'], target=reference)
158        self.assertTrue(res)
159
160
161    def add_bitmap(self, name, drive, **kwargs):
162        bitmap = Bitmap(name, drive)
163        self.bitmaps.append(bitmap)
164        result = self.vm.qmp('block-dirty-bitmap-add', node=drive['id'],
165                             name=bitmap.name, **kwargs)
166        self.assert_qmp(result, 'return', {})
167        return bitmap
168
169
170    def prepare_backup(self, bitmap=None, parent=None):
171        if bitmap is None:
172            bitmap = self.bitmaps[-1]
173        if parent is None:
174            parent, _ = bitmap.last_target()
175
176        target, _ = bitmap.new_target()
177        self.img_create(target, bitmap.drive['fmt'], parent=parent)
178        return target
179
180
181    def create_incremental(self, bitmap=None, parent=None,
182                           parentFormat=None, validate=True):
183        if bitmap is None:
184            bitmap = self.bitmaps[-1]
185        if parent is None:
186            parent, _ = bitmap.last_target()
187
188        target = self.prepare_backup(bitmap, parent)
189        res = self.do_qmp_backup(device=bitmap.drive['id'],
190                                 sync='incremental', bitmap=bitmap.name,
191                                 format=bitmap.drive['fmt'], target=target,
192                                 mode='existing')
193        if not res:
194            bitmap.del_target();
195            self.assertFalse(validate)
196        else:
197            self.make_reference_backup(bitmap)
198        return res
199
200
201    def check_backups(self):
202        for bitmap in self.bitmaps:
203            for incremental, reference in bitmap.backups:
204                self.assertTrue(iotests.compare_images(incremental, reference))
205            last = bitmap.last_target()[0]
206            self.assertTrue(iotests.compare_images(last, bitmap.drive['file']))
207
208
209    def hmp_io_writes(self, drive, patterns):
210        for pattern in patterns:
211            self.vm.hmp_qemu_io(drive, 'write -P%s %s %s' % pattern)
212        self.vm.hmp_qemu_io(drive, 'flush')
213
214
215    def do_incremental_simple(self, **kwargs):
216        self.create_anchor_backup()
217        self.add_bitmap('bitmap0', self.drives[0], **kwargs)
218
219        # Sanity: Create a "hollow" incremental backup
220        self.create_incremental()
221        # Three writes: One complete overwrite, one new segment,
222        # and one partial overlap.
223        self.hmp_io_writes(self.drives[0]['id'], (('0xab', 0, 512),
224                                                  ('0xfe', '16M', '256k'),
225                                                  ('0x64', '32736k', '64k')))
226        self.create_incremental()
227        # Three more writes, one of each kind, like above
228        self.hmp_io_writes(self.drives[0]['id'], (('0x9a', 0, 512),
229                                                  ('0x55', '8M', '352k'),
230                                                  ('0x78', '15872k', '1M')))
231        self.create_incremental()
232        self.vm.shutdown()
233        self.check_backups()
234
235
236    def test_incremental_simple(self):
237        '''
238        Test: Create and verify three incremental backups.
239
240        Create a bitmap and a full backup before VM execution begins,
241        then create a series of three incremental backups "during execution,"
242        i.e.; after IO requests begin modifying the drive.
243        '''
244        return self.do_incremental_simple()
245
246
247    def test_small_granularity(self):
248        '''
249        Test: Create and verify backups made with a small granularity bitmap.
250
251        Perform the same test as test_incremental_simple, but with a granularity
252        of only 32KiB instead of the present default of 64KiB.
253        '''
254        return self.do_incremental_simple(granularity=32768)
255
256
257    def test_large_granularity(self):
258        '''
259        Test: Create and verify backups made with a large granularity bitmap.
260
261        Perform the same test as test_incremental_simple, but with a granularity
262        of 128KiB instead of the present default of 64KiB.
263        '''
264        return self.do_incremental_simple(granularity=131072)
265
266
267    def test_incremental_failure(self):
268        '''Test: Verify backups made after a failure are correct.
269
270        Simulate a failure during an incremental backup block job,
271        emulate additional writes, then create another incremental backup
272        afterwards and verify that the backup created is correct.
273        '''
274
275        # Create a blkdebug interface to this img as 'drive1',
276        # but don't actually create a new image.
277        drive1 = self.add_node('drive1', self.drives[0]['fmt'],
278                               path=self.drives[0]['file'],
279                               backup=self.drives[0]['backup'])
280        result = self.vm.qmp('blockdev-add', options={
281            'id': drive1['id'],
282            'driver': drive1['fmt'],
283            'file': {
284                'driver': 'blkdebug',
285                'image': {
286                    'driver': 'file',
287                    'filename': drive1['file']
288                },
289                'set-state': [{
290                    'event': 'flush_to_disk',
291                    'state': 1,
292                    'new_state': 2
293                }],
294                'inject-error': [{
295                    'event': 'read_aio',
296                    'errno': 5,
297                    'state': 2,
298                    'immediately': False,
299                    'once': True
300                }],
301            }
302        })
303        self.assert_qmp(result, 'return', {})
304
305        self.create_anchor_backup(self.drives[0])
306        self.add_bitmap('bitmap0', drive1)
307        # Note: at this point, during a normal execution,
308        # Assume that the VM resumes and begins issuing IO requests here.
309
310        self.hmp_io_writes(drive1['id'], (('0xab', 0, 512),
311                                          ('0xfe', '16M', '256k'),
312                                          ('0x64', '32736k', '64k')))
313
314        result = self.create_incremental(validate=False)
315        self.assertFalse(result)
316        self.hmp_io_writes(drive1['id'], (('0x9a', 0, 512),
317                                          ('0x55', '8M', '352k'),
318                                          ('0x78', '15872k', '1M')))
319        self.create_incremental()
320        self.vm.shutdown()
321        self.check_backups()
322
323
324    def test_sync_dirty_bitmap_missing(self):
325        self.assert_no_active_block_jobs()
326        self.files.append(self.err_img)
327        result = self.vm.qmp('drive-backup', device=self.drives[0]['id'],
328                             sync='incremental', format=self.drives[0]['fmt'],
329                             target=self.err_img)
330        self.assert_qmp(result, 'error/class', 'GenericError')
331
332
333    def test_sync_dirty_bitmap_not_found(self):
334        self.assert_no_active_block_jobs()
335        self.files.append(self.err_img)
336        result = self.vm.qmp('drive-backup', device=self.drives[0]['id'],
337                             sync='incremental', bitmap='unknown',
338                             format=self.drives[0]['fmt'], target=self.err_img)
339        self.assert_qmp(result, 'error/class', 'GenericError')
340
341
342    def test_sync_dirty_bitmap_bad_granularity(self):
343        '''
344        Test: Test what happens if we provide an improper granularity.
345
346        The granularity must always be a power of 2.
347        '''
348        self.assert_no_active_block_jobs()
349        self.assertRaises(AssertionError, self.add_bitmap,
350                          'bitmap0', self.drives[0],
351                          granularity=64000)
352
353
354    def tearDown(self):
355        self.vm.shutdown()
356        for bitmap in self.bitmaps:
357            bitmap.cleanup()
358        for filename in self.files:
359            try_remove(filename)
360
361
362if __name__ == '__main__':
363    iotests.main(supported_fmts=['qcow2'])
364