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