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