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