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.assertIsNotNone(event) 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='dirty-bitmap', 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='dirty-bitmap', 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='dirty-bitmap', 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