1from __future__ import print_function 2# Common utilities and Python wrappers for qemu-iotests 3# 4# Copyright (C) 2012 IBM Corp. 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18# 19 20import errno 21import os 22import re 23import subprocess 24import string 25import unittest 26import sys 27import struct 28import json 29import signal 30import logging 31import atexit 32import io 33from collections import OrderedDict 34 35sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 36from qemu import qtest 37 38 39# This will not work if arguments contain spaces but is necessary if we 40# want to support the override options that ./check supports. 41qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')] 42if os.environ.get('QEMU_IMG_OPTIONS'): 43 qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ') 44 45qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')] 46if os.environ.get('QEMU_IO_OPTIONS'): 47 qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ') 48 49qemu_nbd_args = [os.environ.get('QEMU_NBD_PROG', 'qemu-nbd')] 50if os.environ.get('QEMU_NBD_OPTIONS'): 51 qemu_nbd_args += os.environ['QEMU_NBD_OPTIONS'].strip().split(' ') 52 53qemu_prog = os.environ.get('QEMU_PROG', 'qemu') 54qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ') 55 56imgfmt = os.environ.get('IMGFMT', 'raw') 57imgproto = os.environ.get('IMGPROTO', 'file') 58test_dir = os.environ.get('TEST_DIR') 59output_dir = os.environ.get('OUTPUT_DIR', '.') 60cachemode = os.environ.get('CACHEMODE') 61qemu_default_machine = os.environ.get('QEMU_DEFAULT_MACHINE') 62 63socket_scm_helper = os.environ.get('SOCKET_SCM_HELPER', 'socket_scm_helper') 64debug = False 65 66luks_default_secret_object = 'secret,id=keysec0,data=' + \ 67 os.environ.get('IMGKEYSECRET', '') 68luks_default_key_secret_opt = 'key-secret=keysec0' 69 70 71def qemu_img(*args): 72 '''Run qemu-img and return the exit code''' 73 devnull = open('/dev/null', 'r+') 74 exitcode = subprocess.call(qemu_img_args + list(args), stdin=devnull, stdout=devnull) 75 if exitcode < 0: 76 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args)))) 77 return exitcode 78 79def ordered_qmp(qmsg, conv_keys=True): 80 # Dictionaries are not ordered prior to 3.6, therefore: 81 if isinstance(qmsg, list): 82 return [ordered_qmp(atom) for atom in qmsg] 83 if isinstance(qmsg, dict): 84 od = OrderedDict() 85 for k, v in sorted(qmsg.items()): 86 if conv_keys: 87 k = k.replace('_', '-') 88 od[k] = ordered_qmp(v, conv_keys=False) 89 return od 90 return qmsg 91 92def qemu_img_create(*args): 93 args = list(args) 94 95 # default luks support 96 if '-f' in args and args[args.index('-f') + 1] == 'luks': 97 if '-o' in args: 98 i = args.index('-o') 99 if 'key-secret' not in args[i + 1]: 100 args[i + 1].append(luks_default_key_secret_opt) 101 args.insert(i + 2, '--object') 102 args.insert(i + 3, luks_default_secret_object) 103 else: 104 args = ['-o', luks_default_key_secret_opt, 105 '--object', luks_default_secret_object] + args 106 107 args.insert(0, 'create') 108 109 return qemu_img(*args) 110 111def qemu_img_verbose(*args): 112 '''Run qemu-img without suppressing its output and return the exit code''' 113 exitcode = subprocess.call(qemu_img_args + list(args)) 114 if exitcode < 0: 115 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args)))) 116 return exitcode 117 118def qemu_img_pipe(*args): 119 '''Run qemu-img and return its output''' 120 subp = subprocess.Popen(qemu_img_args + list(args), 121 stdout=subprocess.PIPE, 122 stderr=subprocess.STDOUT, 123 universal_newlines=True) 124 exitcode = subp.wait() 125 if exitcode < 0: 126 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args)))) 127 return subp.communicate()[0] 128 129def img_info_log(filename, filter_path=None, imgopts=False, extra_args=[]): 130 args = [ 'info' ] 131 if imgopts: 132 args.append('--image-opts') 133 else: 134 args += [ '-f', imgfmt ] 135 args += extra_args 136 args.append(filename) 137 138 output = qemu_img_pipe(*args) 139 if not filter_path: 140 filter_path = filename 141 log(filter_img_info(output, filter_path)) 142 143def qemu_io(*args): 144 '''Run qemu-io and return the stdout data''' 145 args = qemu_io_args + list(args) 146 subp = subprocess.Popen(args, stdout=subprocess.PIPE, 147 stderr=subprocess.STDOUT, 148 universal_newlines=True) 149 exitcode = subp.wait() 150 if exitcode < 0: 151 sys.stderr.write('qemu-io received signal %i: %s\n' % (-exitcode, ' '.join(args))) 152 return subp.communicate()[0] 153 154def qemu_io_silent(*args): 155 '''Run qemu-io and return the exit code, suppressing stdout''' 156 args = qemu_io_args + list(args) 157 exitcode = subprocess.call(args, stdout=open('/dev/null', 'w')) 158 if exitcode < 0: 159 sys.stderr.write('qemu-io received signal %i: %s\n' % 160 (-exitcode, ' '.join(args))) 161 return exitcode 162 163 164class QemuIoInteractive: 165 def __init__(self, *args): 166 self.args = qemu_io_args + list(args) 167 self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE, 168 stdout=subprocess.PIPE, 169 stderr=subprocess.STDOUT, 170 universal_newlines=True) 171 assert self._p.stdout.read(9) == 'qemu-io> ' 172 173 def close(self): 174 self._p.communicate('q\n') 175 176 def _read_output(self): 177 pattern = 'qemu-io> ' 178 n = len(pattern) 179 pos = 0 180 s = [] 181 while pos != n: 182 c = self._p.stdout.read(1) 183 # check unexpected EOF 184 assert c != '' 185 s.append(c) 186 if c == pattern[pos]: 187 pos += 1 188 else: 189 pos = 0 190 191 return ''.join(s[:-n]) 192 193 def cmd(self, cmd): 194 # quit command is in close(), '\n' is added automatically 195 assert '\n' not in cmd 196 cmd = cmd.strip() 197 assert cmd != 'q' and cmd != 'quit' 198 self._p.stdin.write(cmd + '\n') 199 self._p.stdin.flush() 200 return self._read_output() 201 202 203def qemu_nbd(*args): 204 '''Run qemu-nbd in daemon mode and return the parent's exit code''' 205 return subprocess.call(qemu_nbd_args + ['--fork'] + list(args)) 206 207def qemu_nbd_pipe(*args): 208 '''Run qemu-nbd in daemon mode and return both the parent's exit code 209 and its output''' 210 subp = subprocess.Popen(qemu_nbd_args + ['--fork'] + list(args), 211 stdout=subprocess.PIPE, 212 stderr=subprocess.STDOUT, 213 universal_newlines=True) 214 exitcode = subp.wait() 215 if exitcode < 0: 216 sys.stderr.write('qemu-nbd received signal %i: %s\n' % 217 (-exitcode, 218 ' '.join(qemu_nbd_args + ['--fork'] + list(args)))) 219 return exitcode, subp.communicate()[0] 220 221def compare_images(img1, img2, fmt1=imgfmt, fmt2=imgfmt): 222 '''Return True if two image files are identical''' 223 return qemu_img('compare', '-f', fmt1, 224 '-F', fmt2, img1, img2) == 0 225 226def create_image(name, size): 227 '''Create a fully-allocated raw image with sector markers''' 228 file = open(name, 'wb') 229 i = 0 230 while i < size: 231 sector = struct.pack('>l504xl', i // 512, i // 512) 232 file.write(sector) 233 i = i + 512 234 file.close() 235 236def image_size(img): 237 '''Return image's virtual size''' 238 r = qemu_img_pipe('info', '--output=json', '-f', imgfmt, img) 239 return json.loads(r)['virtual-size'] 240 241def is_str(val): 242 if sys.version_info.major >= 3: 243 return isinstance(val, str) 244 else: 245 return isinstance(val, str) or isinstance(val, unicode) 246 247test_dir_re = re.compile(r"%s" % test_dir) 248def filter_test_dir(msg): 249 return test_dir_re.sub("TEST_DIR", msg) 250 251win32_re = re.compile(r"\r") 252def filter_win32(msg): 253 return win32_re.sub("", msg) 254 255qemu_io_re = re.compile(r"[0-9]* ops; [0-9\/:. sec]* \([0-9\/.inf]* [EPTGMKiBbytes]*\/sec and [0-9\/.inf]* ops\/sec\)") 256def filter_qemu_io(msg): 257 msg = filter_win32(msg) 258 return qemu_io_re.sub("X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)", msg) 259 260chown_re = re.compile(r"chown [0-9]+:[0-9]+") 261def filter_chown(msg): 262 return chown_re.sub("chown UID:GID", msg) 263 264def filter_qmp_event(event): 265 '''Filter a QMP event dict''' 266 event = dict(event) 267 if 'timestamp' in event: 268 event['timestamp']['seconds'] = 'SECS' 269 event['timestamp']['microseconds'] = 'USECS' 270 return event 271 272def filter_qmp(qmsg, filter_fn): 273 '''Given a string filter, filter a QMP object's values. 274 filter_fn takes a (key, value) pair.''' 275 # Iterate through either lists or dicts; 276 if isinstance(qmsg, list): 277 items = enumerate(qmsg) 278 else: 279 items = qmsg.items() 280 281 for k, v in items: 282 if isinstance(v, list) or isinstance(v, dict): 283 qmsg[k] = filter_qmp(v, filter_fn) 284 else: 285 qmsg[k] = filter_fn(k, v) 286 return qmsg 287 288def filter_testfiles(msg): 289 prefix = os.path.join(test_dir, "%s-" % (os.getpid())) 290 return msg.replace(prefix, 'TEST_DIR/PID-') 291 292def filter_qmp_testfiles(qmsg): 293 def _filter(key, value): 294 if is_str(value): 295 return filter_testfiles(value) 296 return value 297 return filter_qmp(qmsg, _filter) 298 299def filter_generated_node_ids(msg): 300 return re.sub("#block[0-9]+", "NODE_NAME", msg) 301 302def filter_img_info(output, filename): 303 lines = [] 304 for line in output.split('\n'): 305 if 'disk size' in line or 'actual-size' in line: 306 continue 307 line = line.replace(filename, 'TEST_IMG') \ 308 .replace(imgfmt, 'IMGFMT') 309 line = re.sub('iters: [0-9]+', 'iters: XXX', line) 310 line = re.sub('uuid: [-a-f0-9]+', 'uuid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', line) 311 line = re.sub('cid: [0-9]+', 'cid: XXXXXXXXXX', line) 312 lines.append(line) 313 return '\n'.join(lines) 314 315def filter_imgfmt(msg): 316 return msg.replace(imgfmt, 'IMGFMT') 317 318def filter_qmp_imgfmt(qmsg): 319 def _filter(key, value): 320 if is_str(value): 321 return filter_imgfmt(value) 322 return value 323 return filter_qmp(qmsg, _filter) 324 325def log(msg, filters=[], indent=None): 326 '''Logs either a string message or a JSON serializable message (like QMP). 327 If indent is provided, JSON serializable messages are pretty-printed.''' 328 for flt in filters: 329 msg = flt(msg) 330 if isinstance(msg, dict) or isinstance(msg, list): 331 # Python < 3.4 needs to know not to add whitespace when pretty-printing: 332 separators = (', ', ': ') if indent is None else (',', ': ') 333 # Don't sort if it's already sorted 334 do_sort = not isinstance(msg, OrderedDict) 335 print(json.dumps(msg, sort_keys=do_sort, 336 indent=indent, separators=separators)) 337 else: 338 print(msg) 339 340class Timeout: 341 def __init__(self, seconds, errmsg = "Timeout"): 342 self.seconds = seconds 343 self.errmsg = errmsg 344 def __enter__(self): 345 signal.signal(signal.SIGALRM, self.timeout) 346 signal.setitimer(signal.ITIMER_REAL, self.seconds) 347 return self 348 def __exit__(self, type, value, traceback): 349 signal.setitimer(signal.ITIMER_REAL, 0) 350 return False 351 def timeout(self, signum, frame): 352 raise Exception(self.errmsg) 353 354 355class FilePath(object): 356 '''An auto-generated filename that cleans itself up. 357 358 Use this context manager to generate filenames and ensure that the file 359 gets deleted:: 360 361 with TestFilePath('test.img') as img_path: 362 qemu_img('create', img_path, '1G') 363 # migration_sock_path is automatically deleted 364 ''' 365 def __init__(self, name): 366 filename = '{0}-{1}'.format(os.getpid(), name) 367 self.path = os.path.join(test_dir, filename) 368 369 def __enter__(self): 370 return self.path 371 372 def __exit__(self, exc_type, exc_val, exc_tb): 373 try: 374 os.remove(self.path) 375 except OSError: 376 pass 377 return False 378 379 380def file_path_remover(): 381 for path in reversed(file_path_remover.paths): 382 try: 383 os.remove(path) 384 except OSError: 385 pass 386 387 388def file_path(*names): 389 ''' Another way to get auto-generated filename that cleans itself up. 390 391 Use is as simple as: 392 393 img_a, img_b = file_path('a.img', 'b.img') 394 sock = file_path('socket') 395 ''' 396 397 if not hasattr(file_path_remover, 'paths'): 398 file_path_remover.paths = [] 399 atexit.register(file_path_remover) 400 401 paths = [] 402 for name in names: 403 filename = '{0}-{1}'.format(os.getpid(), name) 404 path = os.path.join(test_dir, filename) 405 file_path_remover.paths.append(path) 406 paths.append(path) 407 408 return paths[0] if len(paths) == 1 else paths 409 410def remote_filename(path): 411 if imgproto == 'file': 412 return path 413 elif imgproto == 'ssh': 414 return "ssh://%s@127.0.0.1:22%s" % (os.environ.get('USER'), path) 415 else: 416 raise Exception("Protocol %s not supported" % (imgproto)) 417 418class VM(qtest.QEMUQtestMachine): 419 '''A QEMU VM''' 420 421 def __init__(self, path_suffix=''): 422 name = "qemu%s-%d" % (path_suffix, os.getpid()) 423 super(VM, self).__init__(qemu_prog, qemu_opts, name=name, 424 test_dir=test_dir, 425 socket_scm_helper=socket_scm_helper) 426 self._num_drives = 0 427 428 def add_object(self, opts): 429 self._args.append('-object') 430 self._args.append(opts) 431 return self 432 433 def add_device(self, opts): 434 self._args.append('-device') 435 self._args.append(opts) 436 return self 437 438 def add_drive_raw(self, opts): 439 self._args.append('-drive') 440 self._args.append(opts) 441 return self 442 443 def add_drive(self, path, opts='', interface='virtio', format=imgfmt): 444 '''Add a virtio-blk drive to the VM''' 445 options = ['if=%s' % interface, 446 'id=drive%d' % self._num_drives] 447 448 if path is not None: 449 options.append('file=%s' % path) 450 options.append('format=%s' % format) 451 options.append('cache=%s' % cachemode) 452 453 if opts: 454 options.append(opts) 455 456 if format == 'luks' and 'key-secret' not in opts: 457 # default luks support 458 if luks_default_secret_object not in self._args: 459 self.add_object(luks_default_secret_object) 460 461 options.append(luks_default_key_secret_opt) 462 463 self._args.append('-drive') 464 self._args.append(','.join(options)) 465 self._num_drives += 1 466 return self 467 468 def add_blockdev(self, opts): 469 self._args.append('-blockdev') 470 if isinstance(opts, str): 471 self._args.append(opts) 472 else: 473 self._args.append(','.join(opts)) 474 return self 475 476 def add_incoming(self, addr): 477 self._args.append('-incoming') 478 self._args.append(addr) 479 return self 480 481 def pause_drive(self, drive, event=None): 482 '''Pause drive r/w operations''' 483 if not event: 484 self.pause_drive(drive, "read_aio") 485 self.pause_drive(drive, "write_aio") 486 return 487 self.qmp('human-monitor-command', 488 command_line='qemu-io %s "break %s bp_%s"' % (drive, event, drive)) 489 490 def resume_drive(self, drive): 491 self.qmp('human-monitor-command', 492 command_line='qemu-io %s "remove_break bp_%s"' % (drive, drive)) 493 494 def hmp_qemu_io(self, drive, cmd): 495 '''Write to a given drive using an HMP command''' 496 return self.qmp('human-monitor-command', 497 command_line='qemu-io %s "%s"' % (drive, cmd)) 498 499 def flatten_qmp_object(self, obj, output=None, basestr=''): 500 if output is None: 501 output = dict() 502 if isinstance(obj, list): 503 for i in range(len(obj)): 504 self.flatten_qmp_object(obj[i], output, basestr + str(i) + '.') 505 elif isinstance(obj, dict): 506 for key in obj: 507 self.flatten_qmp_object(obj[key], output, basestr + key + '.') 508 else: 509 output[basestr[:-1]] = obj # Strip trailing '.' 510 return output 511 512 def qmp_to_opts(self, obj): 513 obj = self.flatten_qmp_object(obj) 514 output_list = list() 515 for key in obj: 516 output_list += [key + '=' + obj[key]] 517 return ','.join(output_list) 518 519 def get_qmp_events_filtered(self, wait=True): 520 result = [] 521 for ev in self.get_qmp_events(wait=wait): 522 result.append(filter_qmp_event(ev)) 523 return result 524 525 def qmp_log(self, cmd, filters=[], indent=None, **kwargs): 526 full_cmd = OrderedDict(( 527 ("execute", cmd), 528 ("arguments", ordered_qmp(kwargs)) 529 )) 530 log(full_cmd, filters, indent=indent) 531 result = self.qmp(cmd, **kwargs) 532 log(result, filters, indent=indent) 533 return result 534 535 # Returns None on success, and an error string on failure 536 def run_job(self, job, auto_finalize=True, auto_dismiss=False): 537 error = None 538 while True: 539 for ev in self.get_qmp_events_filtered(wait=True): 540 if ev['event'] == 'JOB_STATUS_CHANGE': 541 status = ev['data']['status'] 542 if status == 'aborting': 543 result = self.qmp('query-jobs') 544 for j in result['return']: 545 if j['id'] == job: 546 error = j['error'] 547 log('Job failed: %s' % (j['error'])) 548 elif status == 'pending' and not auto_finalize: 549 self.qmp_log('job-finalize', id=job) 550 elif status == 'concluded' and not auto_dismiss: 551 self.qmp_log('job-dismiss', id=job) 552 elif status == 'null': 553 return error 554 else: 555 log(ev) 556 557 def node_info(self, node_name): 558 nodes = self.qmp('query-named-block-nodes') 559 for x in nodes['return']: 560 if x['node-name'] == node_name: 561 return x 562 return None 563 564 565index_re = re.compile(r'([^\[]+)\[([^\]]+)\]') 566 567class QMPTestCase(unittest.TestCase): 568 '''Abstract base class for QMP test cases''' 569 570 def dictpath(self, d, path): 571 '''Traverse a path in a nested dict''' 572 for component in path.split('/'): 573 m = index_re.match(component) 574 if m: 575 component, idx = m.groups() 576 idx = int(idx) 577 578 if not isinstance(d, dict) or component not in d: 579 self.fail('failed path traversal for "%s" in "%s"' % (path, str(d))) 580 d = d[component] 581 582 if m: 583 if not isinstance(d, list): 584 self.fail('path component "%s" in "%s" is not a list in "%s"' % (component, path, str(d))) 585 try: 586 d = d[idx] 587 except IndexError: 588 self.fail('invalid index "%s" in path "%s" in "%s"' % (idx, path, str(d))) 589 return d 590 591 def assert_qmp_absent(self, d, path): 592 try: 593 result = self.dictpath(d, path) 594 except AssertionError: 595 return 596 self.fail('path "%s" has value "%s"' % (path, str(result))) 597 598 def assert_qmp(self, d, path, value): 599 '''Assert that the value for a specific path in a QMP dict 600 matches. When given a list of values, assert that any of 601 them matches.''' 602 603 result = self.dictpath(d, path) 604 605 # [] makes no sense as a list of valid values, so treat it as 606 # an actual single value. 607 if isinstance(value, list) and value != []: 608 for v in value: 609 if result == v: 610 return 611 self.fail('no match for "%s" in %s' % (str(result), str(value))) 612 else: 613 self.assertEqual(result, value, 614 'values not equal "%s" and "%s"' 615 % (str(result), str(value))) 616 617 def assert_no_active_block_jobs(self): 618 result = self.vm.qmp('query-block-jobs') 619 self.assert_qmp(result, 'return', []) 620 621 def assert_has_block_node(self, node_name=None, file_name=None): 622 """Issue a query-named-block-nodes and assert node_name and/or 623 file_name is present in the result""" 624 def check_equal_or_none(a, b): 625 return a == None or b == None or a == b 626 assert node_name or file_name 627 result = self.vm.qmp('query-named-block-nodes') 628 for x in result["return"]: 629 if check_equal_or_none(x.get("node-name"), node_name) and \ 630 check_equal_or_none(x.get("file"), file_name): 631 return 632 self.assertTrue(False, "Cannot find %s %s in result:\n%s" % \ 633 (node_name, file_name, result)) 634 635 def assert_json_filename_equal(self, json_filename, reference): 636 '''Asserts that the given filename is a json: filename and that its 637 content is equal to the given reference object''' 638 self.assertEqual(json_filename[:5], 'json:') 639 self.assertEqual(self.vm.flatten_qmp_object(json.loads(json_filename[5:])), 640 self.vm.flatten_qmp_object(reference)) 641 642 def cancel_and_wait(self, drive='drive0', force=False, resume=False): 643 '''Cancel a block job and wait for it to finish, returning the event''' 644 result = self.vm.qmp('block-job-cancel', device=drive, force=force) 645 self.assert_qmp(result, 'return', {}) 646 647 if resume: 648 self.vm.resume_drive(drive) 649 650 cancelled = False 651 result = None 652 while not cancelled: 653 for event in self.vm.get_qmp_events(wait=True): 654 if event['event'] == 'BLOCK_JOB_COMPLETED' or \ 655 event['event'] == 'BLOCK_JOB_CANCELLED': 656 self.assert_qmp(event, 'data/device', drive) 657 result = event 658 cancelled = True 659 elif event['event'] == 'JOB_STATUS_CHANGE': 660 self.assert_qmp(event, 'data/id', drive) 661 662 663 self.assert_no_active_block_jobs() 664 return result 665 666 def wait_until_completed(self, drive='drive0', check_offset=True): 667 '''Wait for a block job to finish, returning the event''' 668 while True: 669 for event in self.vm.get_qmp_events(wait=True): 670 if event['event'] == 'BLOCK_JOB_COMPLETED': 671 self.assert_qmp(event, 'data/device', drive) 672 self.assert_qmp_absent(event, 'data/error') 673 if check_offset: 674 self.assert_qmp(event, 'data/offset', event['data']['len']) 675 self.assert_no_active_block_jobs() 676 return event 677 elif event['event'] == 'JOB_STATUS_CHANGE': 678 self.assert_qmp(event, 'data/id', drive) 679 680 def wait_ready(self, drive='drive0'): 681 '''Wait until a block job BLOCK_JOB_READY event''' 682 f = {'data': {'type': 'mirror', 'device': drive } } 683 event = self.vm.event_wait(name='BLOCK_JOB_READY', match=f) 684 685 def wait_ready_and_cancel(self, drive='drive0'): 686 self.wait_ready(drive=drive) 687 event = self.cancel_and_wait(drive=drive) 688 self.assertEqual(event['event'], 'BLOCK_JOB_COMPLETED') 689 self.assert_qmp(event, 'data/type', 'mirror') 690 self.assert_qmp(event, 'data/offset', event['data']['len']) 691 692 def complete_and_wait(self, drive='drive0', wait_ready=True): 693 '''Complete a block job and wait for it to finish''' 694 if wait_ready: 695 self.wait_ready(drive=drive) 696 697 result = self.vm.qmp('block-job-complete', device=drive) 698 self.assert_qmp(result, 'return', {}) 699 700 event = self.wait_until_completed(drive=drive) 701 self.assert_qmp(event, 'data/type', 'mirror') 702 703 def pause_wait(self, job_id='job0'): 704 with Timeout(1, "Timeout waiting for job to pause"): 705 while True: 706 result = self.vm.qmp('query-block-jobs') 707 found = False 708 for job in result['return']: 709 if job['device'] == job_id: 710 found = True 711 if job['paused'] == True and job['busy'] == False: 712 return job 713 break 714 assert found 715 716 def pause_job(self, job_id='job0', wait=True): 717 result = self.vm.qmp('block-job-pause', device=job_id) 718 self.assert_qmp(result, 'return', {}) 719 if wait: 720 return self.pause_wait(job_id) 721 return result 722 723 724def notrun(reason): 725 '''Skip this test suite''' 726 # Each test in qemu-iotests has a number ("seq") 727 seq = os.path.basename(sys.argv[0]) 728 729 open('%s/%s.notrun' % (output_dir, seq), 'w').write(reason + '\n') 730 print('%s not run: %s' % (seq, reason)) 731 sys.exit(0) 732 733def case_notrun(reason): 734 '''Skip this test case''' 735 # Each test in qemu-iotests has a number ("seq") 736 seq = os.path.basename(sys.argv[0]) 737 738 open('%s/%s.casenotrun' % (output_dir, seq), 'a').write( 739 ' [case not run] ' + reason + '\n') 740 741def verify_image_format(supported_fmts=[], unsupported_fmts=[]): 742 assert not (supported_fmts and unsupported_fmts) 743 744 if 'generic' in supported_fmts and \ 745 os.environ.get('IMGFMT_GENERIC', 'true') == 'true': 746 # similar to 747 # _supported_fmt generic 748 # for bash tests 749 return 750 751 not_sup = supported_fmts and (imgfmt not in supported_fmts) 752 if not_sup or (imgfmt in unsupported_fmts): 753 notrun('not suitable for this image format: %s' % imgfmt) 754 755def verify_protocol(supported=[], unsupported=[]): 756 assert not (supported and unsupported) 757 758 if 'generic' in supported: 759 return 760 761 not_sup = supported and (imgproto not in supported) 762 if not_sup or (imgproto in unsupported): 763 notrun('not suitable for this protocol: %s' % imgproto) 764 765def verify_platform(supported_oses=['linux']): 766 if True not in [sys.platform.startswith(x) for x in supported_oses]: 767 notrun('not suitable for this OS: %s' % sys.platform) 768 769def verify_cache_mode(supported_cache_modes=[]): 770 if supported_cache_modes and (cachemode not in supported_cache_modes): 771 notrun('not suitable for this cache mode: %s' % cachemode) 772 773def supports_quorum(): 774 return 'quorum' in qemu_img_pipe('--help') 775 776def verify_quorum(): 777 '''Skip test suite if quorum support is not available''' 778 if not supports_quorum(): 779 notrun('quorum support missing') 780 781def qemu_pipe(*args): 782 '''Run qemu with an option to print something and exit (e.g. a help option), 783 and return its output''' 784 args = [qemu_prog] + qemu_opts + list(args) 785 subp = subprocess.Popen(args, stdout=subprocess.PIPE, 786 stderr=subprocess.STDOUT, 787 universal_newlines=True) 788 exitcode = subp.wait() 789 if exitcode < 0: 790 sys.stderr.write('qemu received signal %i: %s\n' % (-exitcode, 791 ' '.join(args))) 792 return subp.communicate()[0] 793 794def supported_formats(read_only=False): 795 '''Set 'read_only' to True to check ro-whitelist 796 Otherwise, rw-whitelist is checked''' 797 format_message = qemu_pipe("-drive", "format=help") 798 line = 1 if read_only else 0 799 return format_message.splitlines()[line].split(":")[1].split() 800 801def skip_if_unsupported(required_formats=[], read_only=False): 802 '''Skip Test Decorator 803 Runs the test if all the required formats are whitelisted''' 804 def skip_test_decorator(func): 805 def func_wrapper(*args, **kwargs): 806 usf_list = list(set(required_formats) - 807 set(supported_formats(read_only))) 808 if usf_list: 809 case_notrun('{}: formats {} are not whitelisted'.format( 810 args[0], usf_list)) 811 else: 812 return func(*args, **kwargs) 813 return func_wrapper 814 return skip_test_decorator 815 816def main(supported_fmts=[], supported_oses=['linux'], supported_cache_modes=[], 817 unsupported_fmts=[]): 818 '''Run tests''' 819 820 global debug 821 822 # We are using TEST_DIR and QEMU_DEFAULT_MACHINE as proxies to 823 # indicate that we're not being run via "check". There may be 824 # other things set up by "check" that individual test cases rely 825 # on. 826 if test_dir is None or qemu_default_machine is None: 827 sys.stderr.write('Please run this test via the "check" script\n') 828 sys.exit(os.EX_USAGE) 829 830 debug = '-d' in sys.argv 831 verbosity = 1 832 verify_image_format(supported_fmts, unsupported_fmts) 833 verify_platform(supported_oses) 834 verify_cache_mode(supported_cache_modes) 835 836 if debug: 837 output = sys.stdout 838 verbosity = 2 839 sys.argv.remove('-d') 840 else: 841 # We need to filter out the time taken from the output so that 842 # qemu-iotest can reliably diff the results against master output. 843 if sys.version_info.major >= 3: 844 output = io.StringIO() 845 else: 846 # io.StringIO is for unicode strings, which is not what 847 # 2.x's test runner emits. 848 output = io.BytesIO() 849 850 logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN)) 851 852 class MyTestRunner(unittest.TextTestRunner): 853 def __init__(self, stream=output, descriptions=True, verbosity=verbosity): 854 unittest.TextTestRunner.__init__(self, stream, descriptions, verbosity) 855 856 # unittest.main() will use sys.exit() so expect a SystemExit exception 857 try: 858 unittest.main(testRunner=MyTestRunner) 859 finally: 860 if not debug: 861 sys.stderr.write(re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', output.getvalue())) 862