1# Common utilities and Python wrappers for qemu-iotests 2# 3# Copyright (C) 2012 IBM Corp. 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <http://www.gnu.org/licenses/>. 17# 18 19import errno 20import os 21import re 22import subprocess 23import string 24import unittest 25import sys 26import struct 27import json 28import signal 29import logging 30import atexit 31import io 32from collections import OrderedDict 33import faulthandler 34 35sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 36from qemu import qtest 37 38assert sys.version_info >= (3,6) 39 40faulthandler.enable() 41 42# This will not work if arguments contain spaces but is necessary if we 43# want to support the override options that ./check supports. 44qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')] 45if os.environ.get('QEMU_IMG_OPTIONS'): 46 qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ') 47 48qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')] 49if os.environ.get('QEMU_IO_OPTIONS'): 50 qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ') 51 52qemu_io_args_no_fmt = [os.environ.get('QEMU_IO_PROG', 'qemu-io')] 53if os.environ.get('QEMU_IO_OPTIONS_NO_FMT'): 54 qemu_io_args_no_fmt += \ 55 os.environ['QEMU_IO_OPTIONS_NO_FMT'].strip().split(' ') 56 57qemu_nbd_args = [os.environ.get('QEMU_NBD_PROG', 'qemu-nbd')] 58if os.environ.get('QEMU_NBD_OPTIONS'): 59 qemu_nbd_args += os.environ['QEMU_NBD_OPTIONS'].strip().split(' ') 60 61qemu_prog = os.environ.get('QEMU_PROG', 'qemu') 62qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ') 63 64imgfmt = os.environ.get('IMGFMT', 'raw') 65imgproto = os.environ.get('IMGPROTO', 'file') 66test_dir = os.environ.get('TEST_DIR') 67sock_dir = os.environ.get('SOCK_DIR') 68output_dir = os.environ.get('OUTPUT_DIR', '.') 69cachemode = os.environ.get('CACHEMODE') 70aiomode = os.environ.get('AIOMODE') 71qemu_default_machine = os.environ.get('QEMU_DEFAULT_MACHINE') 72 73socket_scm_helper = os.environ.get('SOCKET_SCM_HELPER', 'socket_scm_helper') 74 75luks_default_secret_object = 'secret,id=keysec0,data=' + \ 76 os.environ.get('IMGKEYSECRET', '') 77luks_default_key_secret_opt = 'key-secret=keysec0' 78 79 80def qemu_img(*args): 81 '''Run qemu-img and return the exit code''' 82 devnull = open('/dev/null', 'r+') 83 exitcode = subprocess.call(qemu_img_args + list(args), stdin=devnull, stdout=devnull) 84 if exitcode < 0: 85 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args)))) 86 return exitcode 87 88def ordered_qmp(qmsg, conv_keys=True): 89 # Dictionaries are not ordered prior to 3.6, therefore: 90 if isinstance(qmsg, list): 91 return [ordered_qmp(atom) for atom in qmsg] 92 if isinstance(qmsg, dict): 93 od = OrderedDict() 94 for k, v in sorted(qmsg.items()): 95 if conv_keys: 96 k = k.replace('_', '-') 97 od[k] = ordered_qmp(v, conv_keys=False) 98 return od 99 return qmsg 100 101def qemu_img_create(*args): 102 args = list(args) 103 104 # default luks support 105 if '-f' in args and args[args.index('-f') + 1] == 'luks': 106 if '-o' in args: 107 i = args.index('-o') 108 if 'key-secret' not in args[i + 1]: 109 args[i + 1].append(luks_default_key_secret_opt) 110 args.insert(i + 2, '--object') 111 args.insert(i + 3, luks_default_secret_object) 112 else: 113 args = ['-o', luks_default_key_secret_opt, 114 '--object', luks_default_secret_object] + args 115 116 args.insert(0, 'create') 117 118 return qemu_img(*args) 119 120def qemu_img_verbose(*args): 121 '''Run qemu-img without suppressing its output and return the exit code''' 122 exitcode = subprocess.call(qemu_img_args + list(args)) 123 if exitcode < 0: 124 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args)))) 125 return exitcode 126 127def qemu_img_pipe(*args): 128 '''Run qemu-img and return its output''' 129 subp = subprocess.Popen(qemu_img_args + list(args), 130 stdout=subprocess.PIPE, 131 stderr=subprocess.STDOUT, 132 universal_newlines=True) 133 exitcode = subp.wait() 134 if exitcode < 0: 135 sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args)))) 136 return subp.communicate()[0] 137 138def qemu_img_log(*args): 139 result = qemu_img_pipe(*args) 140 log(result, filters=[filter_testfiles]) 141 return result 142 143def img_info_log(filename, filter_path=None, imgopts=False, extra_args=[]): 144 args = [ 'info' ] 145 if imgopts: 146 args.append('--image-opts') 147 else: 148 args += [ '-f', imgfmt ] 149 args += extra_args 150 args.append(filename) 151 152 output = qemu_img_pipe(*args) 153 if not filter_path: 154 filter_path = filename 155 log(filter_img_info(output, filter_path)) 156 157def qemu_io(*args): 158 '''Run qemu-io and return the stdout data''' 159 args = qemu_io_args + list(args) 160 subp = subprocess.Popen(args, stdout=subprocess.PIPE, 161 stderr=subprocess.STDOUT, 162 universal_newlines=True) 163 exitcode = subp.wait() 164 if exitcode < 0: 165 sys.stderr.write('qemu-io received signal %i: %s\n' % (-exitcode, ' '.join(args))) 166 return subp.communicate()[0] 167 168def qemu_io_log(*args): 169 result = qemu_io(*args) 170 log(result, filters=[filter_testfiles, filter_qemu_io]) 171 return result 172 173def qemu_io_silent(*args): 174 '''Run qemu-io and return the exit code, suppressing stdout''' 175 args = qemu_io_args + list(args) 176 exitcode = subprocess.call(args, stdout=open('/dev/null', 'w')) 177 if exitcode < 0: 178 sys.stderr.write('qemu-io received signal %i: %s\n' % 179 (-exitcode, ' '.join(args))) 180 return exitcode 181 182def qemu_io_silent_check(*args): 183 '''Run qemu-io and return the true if subprocess returned 0''' 184 args = qemu_io_args + list(args) 185 exitcode = subprocess.call(args, stdout=open('/dev/null', 'w'), 186 stderr=subprocess.STDOUT) 187 return exitcode == 0 188 189def get_virtio_scsi_device(): 190 if qemu_default_machine == 's390-ccw-virtio': 191 return 'virtio-scsi-ccw' 192 return 'virtio-scsi-pci' 193 194class QemuIoInteractive: 195 def __init__(self, *args): 196 self.args = qemu_io_args + list(args) 197 self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE, 198 stdout=subprocess.PIPE, 199 stderr=subprocess.STDOUT, 200 universal_newlines=True) 201 assert self._p.stdout.read(9) == 'qemu-io> ' 202 203 def close(self): 204 self._p.communicate('q\n') 205 206 def _read_output(self): 207 pattern = 'qemu-io> ' 208 n = len(pattern) 209 pos = 0 210 s = [] 211 while pos != n: 212 c = self._p.stdout.read(1) 213 # check unexpected EOF 214 assert c != '' 215 s.append(c) 216 if c == pattern[pos]: 217 pos += 1 218 else: 219 pos = 0 220 221 return ''.join(s[:-n]) 222 223 def cmd(self, cmd): 224 # quit command is in close(), '\n' is added automatically 225 assert '\n' not in cmd 226 cmd = cmd.strip() 227 assert cmd != 'q' and cmd != 'quit' 228 self._p.stdin.write(cmd + '\n') 229 self._p.stdin.flush() 230 return self._read_output() 231 232 233def qemu_nbd(*args): 234 '''Run qemu-nbd in daemon mode and return the parent's exit code''' 235 return subprocess.call(qemu_nbd_args + ['--fork'] + list(args)) 236 237def qemu_nbd_early_pipe(*args): 238 '''Run qemu-nbd in daemon mode and return both the parent's exit code 239 and its output in case of an error''' 240 subp = subprocess.Popen(qemu_nbd_args + ['--fork'] + list(args), 241 stdout=subprocess.PIPE, 242 stderr=subprocess.STDOUT, 243 universal_newlines=True) 244 exitcode = subp.wait() 245 if exitcode < 0: 246 sys.stderr.write('qemu-nbd received signal %i: %s\n' % 247 (-exitcode, 248 ' '.join(qemu_nbd_args + ['--fork'] + list(args)))) 249 if exitcode == 0: 250 return exitcode, '' 251 else: 252 return exitcode, subp.communicate()[0] 253 254def qemu_nbd_popen(*args): 255 '''Run qemu-nbd in daemon mode and return the parent's exit code''' 256 return subprocess.Popen(qemu_nbd_args + ['--persistent'] + list(args)) 257 258def compare_images(img1, img2, fmt1=imgfmt, fmt2=imgfmt): 259 '''Return True if two image files are identical''' 260 return qemu_img('compare', '-f', fmt1, 261 '-F', fmt2, img1, img2) == 0 262 263def create_image(name, size): 264 '''Create a fully-allocated raw image with sector markers''' 265 file = open(name, 'wb') 266 i = 0 267 while i < size: 268 sector = struct.pack('>l504xl', i // 512, i // 512) 269 file.write(sector) 270 i = i + 512 271 file.close() 272 273def image_size(img): 274 '''Return image's virtual size''' 275 r = qemu_img_pipe('info', '--output=json', '-f', imgfmt, img) 276 return json.loads(r)['virtual-size'] 277 278def is_str(val): 279 return isinstance(val, str) 280 281test_dir_re = re.compile(r"%s" % test_dir) 282def filter_test_dir(msg): 283 return test_dir_re.sub("TEST_DIR", msg) 284 285win32_re = re.compile(r"\r") 286def filter_win32(msg): 287 return win32_re.sub("", msg) 288 289qemu_io_re = re.compile(r"[0-9]* ops; [0-9\/:. sec]* \([0-9\/.inf]* [EPTGMKiBbytes]*\/sec and [0-9\/.inf]* ops\/sec\)") 290def filter_qemu_io(msg): 291 msg = filter_win32(msg) 292 return qemu_io_re.sub("X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)", msg) 293 294chown_re = re.compile(r"chown [0-9]+:[0-9]+") 295def filter_chown(msg): 296 return chown_re.sub("chown UID:GID", msg) 297 298def filter_qmp_event(event): 299 '''Filter a QMP event dict''' 300 event = dict(event) 301 if 'timestamp' in event: 302 event['timestamp']['seconds'] = 'SECS' 303 event['timestamp']['microseconds'] = 'USECS' 304 return event 305 306def filter_qmp(qmsg, filter_fn): 307 '''Given a string filter, filter a QMP object's values. 308 filter_fn takes a (key, value) pair.''' 309 # Iterate through either lists or dicts; 310 if isinstance(qmsg, list): 311 items = enumerate(qmsg) 312 else: 313 items = qmsg.items() 314 315 for k, v in items: 316 if isinstance(v, list) or isinstance(v, dict): 317 qmsg[k] = filter_qmp(v, filter_fn) 318 else: 319 qmsg[k] = filter_fn(k, v) 320 return qmsg 321 322def filter_testfiles(msg): 323 prefix = os.path.join(test_dir, "%s-" % (os.getpid())) 324 return msg.replace(prefix, 'TEST_DIR/PID-') 325 326def filter_qmp_testfiles(qmsg): 327 def _filter(key, value): 328 if is_str(value): 329 return filter_testfiles(value) 330 return value 331 return filter_qmp(qmsg, _filter) 332 333def filter_generated_node_ids(msg): 334 return re.sub("#block[0-9]+", "NODE_NAME", msg) 335 336def filter_img_info(output, filename): 337 lines = [] 338 for line in output.split('\n'): 339 if 'disk size' in line or 'actual-size' in line: 340 continue 341 line = line.replace(filename, 'TEST_IMG') \ 342 .replace(imgfmt, 'IMGFMT') 343 line = re.sub('iters: [0-9]+', 'iters: XXX', line) 344 line = re.sub('uuid: [-a-f0-9]+', 'uuid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', line) 345 line = re.sub('cid: [0-9]+', 'cid: XXXXXXXXXX', line) 346 lines.append(line) 347 return '\n'.join(lines) 348 349def filter_imgfmt(msg): 350 return msg.replace(imgfmt, 'IMGFMT') 351 352def filter_qmp_imgfmt(qmsg): 353 def _filter(key, value): 354 if is_str(value): 355 return filter_imgfmt(value) 356 return value 357 return filter_qmp(qmsg, _filter) 358 359def log(msg, filters=[], indent=None): 360 '''Logs either a string message or a JSON serializable message (like QMP). 361 If indent is provided, JSON serializable messages are pretty-printed.''' 362 for flt in filters: 363 msg = flt(msg) 364 if isinstance(msg, dict) or isinstance(msg, list): 365 # Python < 3.4 needs to know not to add whitespace when pretty-printing: 366 separators = (', ', ': ') if indent is None else (',', ': ') 367 # Don't sort if it's already sorted 368 do_sort = not isinstance(msg, OrderedDict) 369 print(json.dumps(msg, sort_keys=do_sort, 370 indent=indent, separators=separators)) 371 else: 372 print(msg) 373 374class Timeout: 375 def __init__(self, seconds, errmsg = "Timeout"): 376 self.seconds = seconds 377 self.errmsg = errmsg 378 def __enter__(self): 379 signal.signal(signal.SIGALRM, self.timeout) 380 signal.setitimer(signal.ITIMER_REAL, self.seconds) 381 return self 382 def __exit__(self, type, value, traceback): 383 signal.setitimer(signal.ITIMER_REAL, 0) 384 return False 385 def timeout(self, signum, frame): 386 raise Exception(self.errmsg) 387 388def file_pattern(name): 389 return "{0}-{1}".format(os.getpid(), name) 390 391class FilePaths(object): 392 """ 393 FilePaths is an auto-generated filename that cleans itself up. 394 395 Use this context manager to generate filenames and ensure that the file 396 gets deleted:: 397 398 with FilePaths(['test.img']) as img_path: 399 qemu_img('create', img_path, '1G') 400 # migration_sock_path is automatically deleted 401 """ 402 def __init__(self, names, base_dir=test_dir): 403 self.paths = [] 404 for name in names: 405 self.paths.append(os.path.join(base_dir, file_pattern(name))) 406 407 def __enter__(self): 408 return self.paths 409 410 def __exit__(self, exc_type, exc_val, exc_tb): 411 try: 412 for path in self.paths: 413 os.remove(path) 414 except OSError: 415 pass 416 return False 417 418class FilePath(FilePaths): 419 """ 420 FilePath is a specialization of FilePaths that takes a single filename. 421 """ 422 def __init__(self, name, base_dir=test_dir): 423 super(FilePath, self).__init__([name], base_dir) 424 425 def __enter__(self): 426 return self.paths[0] 427 428def file_path_remover(): 429 for path in reversed(file_path_remover.paths): 430 try: 431 os.remove(path) 432 except OSError: 433 pass 434 435 436def file_path(*names, base_dir=test_dir): 437 ''' Another way to get auto-generated filename that cleans itself up. 438 439 Use is as simple as: 440 441 img_a, img_b = file_path('a.img', 'b.img') 442 sock = file_path('socket') 443 ''' 444 445 if not hasattr(file_path_remover, 'paths'): 446 file_path_remover.paths = [] 447 atexit.register(file_path_remover) 448 449 paths = [] 450 for name in names: 451 filename = file_pattern(name) 452 path = os.path.join(base_dir, filename) 453 file_path_remover.paths.append(path) 454 paths.append(path) 455 456 return paths[0] if len(paths) == 1 else paths 457 458def remote_filename(path): 459 if imgproto == 'file': 460 return path 461 elif imgproto == 'ssh': 462 return "ssh://%s@127.0.0.1:22%s" % (os.environ.get('USER'), path) 463 else: 464 raise Exception("Protocol %s not supported" % (imgproto)) 465 466class VM(qtest.QEMUQtestMachine): 467 '''A QEMU VM''' 468 469 def __init__(self, path_suffix=''): 470 name = "qemu%s-%d" % (path_suffix, os.getpid()) 471 super(VM, self).__init__(qemu_prog, qemu_opts, name=name, 472 test_dir=test_dir, 473 socket_scm_helper=socket_scm_helper, 474 sock_dir=sock_dir) 475 self._num_drives = 0 476 477 def add_object(self, opts): 478 self._args.append('-object') 479 self._args.append(opts) 480 return self 481 482 def add_device(self, opts): 483 self._args.append('-device') 484 self._args.append(opts) 485 return self 486 487 def add_drive_raw(self, opts): 488 self._args.append('-drive') 489 self._args.append(opts) 490 return self 491 492 def add_drive(self, path, opts='', interface='virtio', format=imgfmt): 493 '''Add a virtio-blk drive to the VM''' 494 options = ['if=%s' % interface, 495 'id=drive%d' % self._num_drives] 496 497 if path is not None: 498 options.append('file=%s' % path) 499 options.append('format=%s' % format) 500 options.append('cache=%s' % cachemode) 501 options.append('aio=%s' % aiomode) 502 503 if opts: 504 options.append(opts) 505 506 if format == 'luks' and 'key-secret' not in opts: 507 # default luks support 508 if luks_default_secret_object not in self._args: 509 self.add_object(luks_default_secret_object) 510 511 options.append(luks_default_key_secret_opt) 512 513 self._args.append('-drive') 514 self._args.append(','.join(options)) 515 self._num_drives += 1 516 return self 517 518 def add_blockdev(self, opts): 519 self._args.append('-blockdev') 520 if isinstance(opts, str): 521 self._args.append(opts) 522 else: 523 self._args.append(','.join(opts)) 524 return self 525 526 def add_incoming(self, addr): 527 self._args.append('-incoming') 528 self._args.append(addr) 529 return self 530 531 def pause_drive(self, drive, event=None): 532 '''Pause drive r/w operations''' 533 if not event: 534 self.pause_drive(drive, "read_aio") 535 self.pause_drive(drive, "write_aio") 536 return 537 self.qmp('human-monitor-command', 538 command_line='qemu-io %s "break %s bp_%s"' % (drive, event, drive)) 539 540 def resume_drive(self, drive): 541 self.qmp('human-monitor-command', 542 command_line='qemu-io %s "remove_break bp_%s"' % (drive, drive)) 543 544 def hmp_qemu_io(self, drive, cmd): 545 '''Write to a given drive using an HMP command''' 546 return self.qmp('human-monitor-command', 547 command_line='qemu-io %s "%s"' % (drive, cmd)) 548 549 def flatten_qmp_object(self, obj, output=None, basestr=''): 550 if output is None: 551 output = dict() 552 if isinstance(obj, list): 553 for i in range(len(obj)): 554 self.flatten_qmp_object(obj[i], output, basestr + str(i) + '.') 555 elif isinstance(obj, dict): 556 for key in obj: 557 self.flatten_qmp_object(obj[key], output, basestr + key + '.') 558 else: 559 output[basestr[:-1]] = obj # Strip trailing '.' 560 return output 561 562 def qmp_to_opts(self, obj): 563 obj = self.flatten_qmp_object(obj) 564 output_list = list() 565 for key in obj: 566 output_list += [key + '=' + obj[key]] 567 return ','.join(output_list) 568 569 def get_qmp_events_filtered(self, wait=60.0): 570 result = [] 571 for ev in self.get_qmp_events(wait=wait): 572 result.append(filter_qmp_event(ev)) 573 return result 574 575 def qmp_log(self, cmd, filters=[], indent=None, **kwargs): 576 full_cmd = OrderedDict(( 577 ("execute", cmd), 578 ("arguments", ordered_qmp(kwargs)) 579 )) 580 log(full_cmd, filters, indent=indent) 581 result = self.qmp(cmd, **kwargs) 582 log(result, filters, indent=indent) 583 return result 584 585 # Returns None on success, and an error string on failure 586 def run_job(self, job, auto_finalize=True, auto_dismiss=False, 587 pre_finalize=None, cancel=False, use_log=True, wait=60.0): 588 """ 589 run_job moves a job from creation through to dismissal. 590 591 :param job: String. ID of recently-launched job 592 :param auto_finalize: Bool. True if the job was launched with 593 auto_finalize. Defaults to True. 594 :param auto_dismiss: Bool. True if the job was launched with 595 auto_dismiss=True. Defaults to False. 596 :param pre_finalize: Callback. A callable that takes no arguments to be 597 invoked prior to issuing job-finalize, if any. 598 :param cancel: Bool. When true, cancels the job after the pre_finalize 599 callback. 600 :param use_log: Bool. When false, does not log QMP messages. 601 :param wait: Float. Timeout value specifying how long to wait for any 602 event, in seconds. Defaults to 60.0. 603 """ 604 match_device = {'data': {'device': job}} 605 match_id = {'data': {'id': job}} 606 events = [ 607 ('BLOCK_JOB_COMPLETED', match_device), 608 ('BLOCK_JOB_CANCELLED', match_device), 609 ('BLOCK_JOB_ERROR', match_device), 610 ('BLOCK_JOB_READY', match_device), 611 ('BLOCK_JOB_PENDING', match_id), 612 ('JOB_STATUS_CHANGE', match_id) 613 ] 614 error = None 615 while True: 616 ev = filter_qmp_event(self.events_wait(events, timeout=wait)) 617 if ev['event'] != 'JOB_STATUS_CHANGE': 618 if use_log: 619 log(ev) 620 continue 621 status = ev['data']['status'] 622 if status == 'aborting': 623 result = self.qmp('query-jobs') 624 for j in result['return']: 625 if j['id'] == job: 626 error = j['error'] 627 if use_log: 628 log('Job failed: %s' % (j['error'])) 629 elif status == 'ready': 630 if use_log: 631 self.qmp_log('job-complete', id=job) 632 else: 633 self.qmp('job-complete', id=job) 634 elif status == 'pending' and not auto_finalize: 635 if pre_finalize: 636 pre_finalize() 637 if cancel and use_log: 638 self.qmp_log('job-cancel', id=job) 639 elif cancel: 640 self.qmp('job-cancel', id=job) 641 elif use_log: 642 self.qmp_log('job-finalize', id=job) 643 else: 644 self.qmp('job-finalize', id=job) 645 elif status == 'concluded' and not auto_dismiss: 646 if use_log: 647 self.qmp_log('job-dismiss', id=job) 648 else: 649 self.qmp('job-dismiss', id=job) 650 elif status == 'null': 651 return error 652 653 # Returns None on success, and an error string on failure 654 def blockdev_create(self, options, job_id='job0', filters=None): 655 if filters is None: 656 filters = [filter_qmp_testfiles] 657 result = self.qmp_log('blockdev-create', filters=filters, 658 job_id=job_id, options=options) 659 660 if 'return' in result: 661 assert result['return'] == {} 662 job_result = self.run_job(job_id) 663 else: 664 job_result = result['error'] 665 666 log("") 667 return job_result 668 669 def enable_migration_events(self, name): 670 log('Enabling migration QMP events on %s...' % name) 671 log(self.qmp('migrate-set-capabilities', capabilities=[ 672 { 673 'capability': 'events', 674 'state': True 675 } 676 ])) 677 678 def wait_migration(self, expect_runstate): 679 while True: 680 event = self.event_wait('MIGRATION') 681 log(event, filters=[filter_qmp_event]) 682 if event['data']['status'] == 'completed': 683 break 684 # The event may occur in finish-migrate, so wait for the expected 685 # post-migration runstate 686 while self.qmp('query-status')['return']['status'] != expect_runstate: 687 pass 688 689 def node_info(self, node_name): 690 nodes = self.qmp('query-named-block-nodes') 691 for x in nodes['return']: 692 if x['node-name'] == node_name: 693 return x 694 return None 695 696 def query_bitmaps(self): 697 res = self.qmp("query-named-block-nodes") 698 return {device['node-name']: device['dirty-bitmaps'] 699 for device in res['return'] if 'dirty-bitmaps' in device} 700 701 def get_bitmap(self, node_name, bitmap_name, recording=None, bitmaps=None): 702 """ 703 get a specific bitmap from the object returned by query_bitmaps. 704 :param recording: If specified, filter results by the specified value. 705 :param bitmaps: If specified, use it instead of call query_bitmaps() 706 """ 707 if bitmaps is None: 708 bitmaps = self.query_bitmaps() 709 710 for bitmap in bitmaps[node_name]: 711 if bitmap.get('name', '') == bitmap_name: 712 if recording is None: 713 return bitmap 714 elif bitmap.get('recording') == recording: 715 return bitmap 716 return None 717 718 def check_bitmap_status(self, node_name, bitmap_name, fields): 719 ret = self.get_bitmap(node_name, bitmap_name) 720 721 return fields.items() <= ret.items() 722 723 def assert_block_path(self, root, path, expected_node, graph=None): 724 """ 725 Check whether the node under the given path in the block graph 726 is @expected_node. 727 728 @root is the node name of the node where the @path is rooted. 729 730 @path is a string that consists of child names separated by 731 slashes. It must begin with a slash. 732 733 Examples for @root + @path: 734 - root="qcow2-node", path="/backing/file" 735 - root="quorum-node", path="/children.2/file" 736 737 Hypothetically, @path could be empty, in which case it would 738 point to @root. However, in practice this case is not useful 739 and hence not allowed. 740 741 @expected_node may be None. (All elements of the path but the 742 leaf must still exist.) 743 744 @graph may be None or the result of an x-debug-query-block-graph 745 call that has already been performed. 746 """ 747 if graph is None: 748 graph = self.qmp('x-debug-query-block-graph')['return'] 749 750 iter_path = iter(path.split('/')) 751 752 # Must start with a / 753 assert next(iter_path) == '' 754 755 node = next((node for node in graph['nodes'] if node['name'] == root), 756 None) 757 758 # An empty @path is not allowed, so the root node must be present 759 assert node is not None, 'Root node %s not found' % root 760 761 for child_name in iter_path: 762 assert node is not None, 'Cannot follow path %s%s' % (root, path) 763 764 try: 765 node_id = next(edge['child'] for edge in graph['edges'] \ 766 if edge['parent'] == node['id'] and 767 edge['name'] == child_name) 768 769 node = next(node for node in graph['nodes'] \ 770 if node['id'] == node_id) 771 except StopIteration: 772 node = None 773 774 if node is None: 775 assert expected_node is None, \ 776 'No node found under %s (but expected %s)' % \ 777 (path, expected_node) 778 else: 779 assert node['name'] == expected_node, \ 780 'Found node %s under %s (but expected %s)' % \ 781 (node['name'], path, expected_node) 782 783index_re = re.compile(r'([^\[]+)\[([^\]]+)\]') 784 785class QMPTestCase(unittest.TestCase): 786 '''Abstract base class for QMP test cases''' 787 788 def dictpath(self, d, path): 789 '''Traverse a path in a nested dict''' 790 for component in path.split('/'): 791 m = index_re.match(component) 792 if m: 793 component, idx = m.groups() 794 idx = int(idx) 795 796 if not isinstance(d, dict) or component not in d: 797 self.fail('failed path traversal for "%s" in "%s"' % (path, str(d))) 798 d = d[component] 799 800 if m: 801 if not isinstance(d, list): 802 self.fail('path component "%s" in "%s" is not a list in "%s"' % (component, path, str(d))) 803 try: 804 d = d[idx] 805 except IndexError: 806 self.fail('invalid index "%s" in path "%s" in "%s"' % (idx, path, str(d))) 807 return d 808 809 def assert_qmp_absent(self, d, path): 810 try: 811 result = self.dictpath(d, path) 812 except AssertionError: 813 return 814 self.fail('path "%s" has value "%s"' % (path, str(result))) 815 816 def assert_qmp(self, d, path, value): 817 '''Assert that the value for a specific path in a QMP dict 818 matches. When given a list of values, assert that any of 819 them matches.''' 820 821 result = self.dictpath(d, path) 822 823 # [] makes no sense as a list of valid values, so treat it as 824 # an actual single value. 825 if isinstance(value, list) and value != []: 826 for v in value: 827 if result == v: 828 return 829 self.fail('no match for "%s" in %s' % (str(result), str(value))) 830 else: 831 self.assertEqual(result, value, 832 '"%s" is "%s", expected "%s"' 833 % (path, str(result), str(value))) 834 835 def assert_no_active_block_jobs(self): 836 result = self.vm.qmp('query-block-jobs') 837 self.assert_qmp(result, 'return', []) 838 839 def assert_has_block_node(self, node_name=None, file_name=None): 840 """Issue a query-named-block-nodes and assert node_name and/or 841 file_name is present in the result""" 842 def check_equal_or_none(a, b): 843 return a == None or b == None or a == b 844 assert node_name or file_name 845 result = self.vm.qmp('query-named-block-nodes') 846 for x in result["return"]: 847 if check_equal_or_none(x.get("node-name"), node_name) and \ 848 check_equal_or_none(x.get("file"), file_name): 849 return 850 self.assertTrue(False, "Cannot find %s %s in result:\n%s" % \ 851 (node_name, file_name, result)) 852 853 def assert_json_filename_equal(self, json_filename, reference): 854 '''Asserts that the given filename is a json: filename and that its 855 content is equal to the given reference object''' 856 self.assertEqual(json_filename[:5], 'json:') 857 self.assertEqual(self.vm.flatten_qmp_object(json.loads(json_filename[5:])), 858 self.vm.flatten_qmp_object(reference)) 859 860 def cancel_and_wait(self, drive='drive0', force=False, resume=False, wait=60.0): 861 '''Cancel a block job and wait for it to finish, returning the event''' 862 result = self.vm.qmp('block-job-cancel', device=drive, force=force) 863 self.assert_qmp(result, 'return', {}) 864 865 if resume: 866 self.vm.resume_drive(drive) 867 868 cancelled = False 869 result = None 870 while not cancelled: 871 for event in self.vm.get_qmp_events(wait=wait): 872 if event['event'] == 'BLOCK_JOB_COMPLETED' or \ 873 event['event'] == 'BLOCK_JOB_CANCELLED': 874 self.assert_qmp(event, 'data/device', drive) 875 result = event 876 cancelled = True 877 elif event['event'] == 'JOB_STATUS_CHANGE': 878 self.assert_qmp(event, 'data/id', drive) 879 880 881 self.assert_no_active_block_jobs() 882 return result 883 884 def wait_until_completed(self, drive='drive0', check_offset=True, wait=60.0, 885 error=None): 886 '''Wait for a block job to finish, returning the event''' 887 while True: 888 for event in self.vm.get_qmp_events(wait=wait): 889 if event['event'] == 'BLOCK_JOB_COMPLETED': 890 self.assert_qmp(event, 'data/device', drive) 891 if error is None: 892 self.assert_qmp_absent(event, 'data/error') 893 if check_offset: 894 self.assert_qmp(event, 'data/offset', 895 event['data']['len']) 896 else: 897 self.assert_qmp(event, 'data/error', error) 898 self.assert_no_active_block_jobs() 899 return event 900 elif event['event'] == 'JOB_STATUS_CHANGE': 901 self.assert_qmp(event, 'data/id', drive) 902 903 def wait_ready(self, drive='drive0'): 904 '''Wait until a block job BLOCK_JOB_READY event''' 905 f = {'data': {'type': 'mirror', 'device': drive } } 906 event = self.vm.event_wait(name='BLOCK_JOB_READY', match=f) 907 908 def wait_ready_and_cancel(self, drive='drive0'): 909 self.wait_ready(drive=drive) 910 event = self.cancel_and_wait(drive=drive) 911 self.assertEqual(event['event'], 'BLOCK_JOB_COMPLETED') 912 self.assert_qmp(event, 'data/type', 'mirror') 913 self.assert_qmp(event, 'data/offset', event['data']['len']) 914 915 def complete_and_wait(self, drive='drive0', wait_ready=True, 916 completion_error=None): 917 '''Complete a block job and wait for it to finish''' 918 if wait_ready: 919 self.wait_ready(drive=drive) 920 921 result = self.vm.qmp('block-job-complete', device=drive) 922 self.assert_qmp(result, 'return', {}) 923 924 event = self.wait_until_completed(drive=drive, error=completion_error) 925 self.assert_qmp(event, 'data/type', 'mirror') 926 927 def pause_wait(self, job_id='job0'): 928 with Timeout(3, "Timeout waiting for job to pause"): 929 while True: 930 result = self.vm.qmp('query-block-jobs') 931 found = False 932 for job in result['return']: 933 if job['device'] == job_id: 934 found = True 935 if job['paused'] == True and job['busy'] == False: 936 return job 937 break 938 assert found 939 940 def pause_job(self, job_id='job0', wait=True): 941 result = self.vm.qmp('block-job-pause', device=job_id) 942 self.assert_qmp(result, 'return', {}) 943 if wait: 944 return self.pause_wait(job_id) 945 return result 946 947 def case_skip(self, reason): 948 '''Skip this test case''' 949 case_notrun(reason) 950 self.skipTest(reason) 951 952 953def notrun(reason): 954 '''Skip this test suite''' 955 # Each test in qemu-iotests has a number ("seq") 956 seq = os.path.basename(sys.argv[0]) 957 958 open('%s/%s.notrun' % (output_dir, seq), 'w').write(reason + '\n') 959 print('%s not run: %s' % (seq, reason)) 960 sys.exit(0) 961 962def case_notrun(reason): 963 '''Mark this test case as not having been run (without actually 964 skipping it, that is left to the caller). See 965 QMPTestCase.case_skip() for a variant that actually skips the 966 current test case.''' 967 968 # Each test in qemu-iotests has a number ("seq") 969 seq = os.path.basename(sys.argv[0]) 970 971 open('%s/%s.casenotrun' % (output_dir, seq), 'a').write( 972 ' [case not run] ' + reason + '\n') 973 974def verify_image_format(supported_fmts=[], unsupported_fmts=[]): 975 assert not (supported_fmts and unsupported_fmts) 976 977 if 'generic' in supported_fmts and \ 978 os.environ.get('IMGFMT_GENERIC', 'true') == 'true': 979 # similar to 980 # _supported_fmt generic 981 # for bash tests 982 return 983 984 not_sup = supported_fmts and (imgfmt not in supported_fmts) 985 if not_sup or (imgfmt in unsupported_fmts): 986 notrun('not suitable for this image format: %s' % imgfmt) 987 988def verify_protocol(supported=[], unsupported=[]): 989 assert not (supported and unsupported) 990 991 if 'generic' in supported: 992 return 993 994 not_sup = supported and (imgproto not in supported) 995 if not_sup or (imgproto in unsupported): 996 notrun('not suitable for this protocol: %s' % imgproto) 997 998def verify_platform(supported=None, unsupported=None): 999 if unsupported is not None: 1000 if any((sys.platform.startswith(x) for x in unsupported)): 1001 notrun('not suitable for this OS: %s' % sys.platform) 1002 1003 if supported is not None: 1004 if not any((sys.platform.startswith(x) for x in supported)): 1005 notrun('not suitable for this OS: %s' % sys.platform) 1006 1007def verify_cache_mode(supported_cache_modes=[]): 1008 if supported_cache_modes and (cachemode not in supported_cache_modes): 1009 notrun('not suitable for this cache mode: %s' % cachemode) 1010 1011def verify_aio_mode(supported_aio_modes=[]): 1012 if supported_aio_modes and (aiomode not in supported_aio_modes): 1013 notrun('not suitable for this aio mode: %s' % aiomode) 1014 1015def supports_quorum(): 1016 return 'quorum' in qemu_img_pipe('--help') 1017 1018def verify_quorum(): 1019 '''Skip test suite if quorum support is not available''' 1020 if not supports_quorum(): 1021 notrun('quorum support missing') 1022 1023def qemu_pipe(*args): 1024 '''Run qemu with an option to print something and exit (e.g. a help option), 1025 and return its output''' 1026 args = [qemu_prog] + qemu_opts + list(args) 1027 subp = subprocess.Popen(args, stdout=subprocess.PIPE, 1028 stderr=subprocess.STDOUT, 1029 universal_newlines=True) 1030 exitcode = subp.wait() 1031 if exitcode < 0: 1032 sys.stderr.write('qemu received signal %i: %s\n' % (-exitcode, 1033 ' '.join(args))) 1034 return subp.communicate()[0] 1035 1036def supported_formats(read_only=False): 1037 '''Set 'read_only' to True to check ro-whitelist 1038 Otherwise, rw-whitelist is checked''' 1039 1040 if not hasattr(supported_formats, "formats"): 1041 supported_formats.formats = {} 1042 1043 if read_only not in supported_formats.formats: 1044 format_message = qemu_pipe("-drive", "format=help") 1045 line = 1 if read_only else 0 1046 supported_formats.formats[read_only] = \ 1047 format_message.splitlines()[line].split(":")[1].split() 1048 1049 return supported_formats.formats[read_only] 1050 1051def skip_if_unsupported(required_formats=[], read_only=False): 1052 '''Skip Test Decorator 1053 Runs the test if all the required formats are whitelisted''' 1054 def skip_test_decorator(func): 1055 def func_wrapper(test_case: QMPTestCase, *args, **kwargs): 1056 if callable(required_formats): 1057 fmts = required_formats(test_case) 1058 else: 1059 fmts = required_formats 1060 1061 usf_list = list(set(fmts) - set(supported_formats(read_only))) 1062 if usf_list: 1063 test_case.case_skip('{}: formats {} are not whitelisted'.format( 1064 test_case, usf_list)) 1065 else: 1066 return func(test_case, *args, **kwargs) 1067 return func_wrapper 1068 return skip_test_decorator 1069 1070def skip_if_user_is_root(func): 1071 '''Skip Test Decorator 1072 Runs the test only without root permissions''' 1073 def func_wrapper(*args, **kwargs): 1074 if os.getuid() == 0: 1075 case_notrun('{}: cannot be run as root'.format(args[0])) 1076 else: 1077 return func(*args, **kwargs) 1078 return func_wrapper 1079 1080def execute_unittest(output, verbosity, debug): 1081 runner = unittest.TextTestRunner(stream=output, descriptions=True, 1082 verbosity=verbosity) 1083 try: 1084 # unittest.main() will use sys.exit(); so expect a SystemExit 1085 # exception 1086 unittest.main(testRunner=runner) 1087 finally: 1088 if not debug: 1089 out = output.getvalue() 1090 out = re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', out) 1091 1092 # Hide skipped tests from the reference output 1093 out = re.sub(r'OK \(skipped=\d+\)', 'OK', out) 1094 out_first_line, out_rest = out.split('\n', 1) 1095 out = out_first_line.replace('s', '.') + '\n' + out_rest 1096 1097 sys.stderr.write(out) 1098 1099def execute_test(test_function=None, 1100 supported_fmts=[], 1101 supported_platforms=None, 1102 supported_cache_modes=[], supported_aio_modes={}, 1103 unsupported_fmts=[], supported_protocols=[], 1104 unsupported_protocols=[]): 1105 """Run either unittest or script-style tests.""" 1106 1107 # We are using TEST_DIR and QEMU_DEFAULT_MACHINE as proxies to 1108 # indicate that we're not being run via "check". There may be 1109 # other things set up by "check" that individual test cases rely 1110 # on. 1111 if test_dir is None or qemu_default_machine is None: 1112 sys.stderr.write('Please run this test via the "check" script\n') 1113 sys.exit(os.EX_USAGE) 1114 1115 debug = '-d' in sys.argv 1116 verbosity = 1 1117 verify_image_format(supported_fmts, unsupported_fmts) 1118 verify_protocol(supported_protocols, unsupported_protocols) 1119 verify_platform(supported=supported_platforms) 1120 verify_cache_mode(supported_cache_modes) 1121 verify_aio_mode(supported_aio_modes) 1122 1123 if debug: 1124 output = sys.stdout 1125 verbosity = 2 1126 sys.argv.remove('-d') 1127 else: 1128 # We need to filter out the time taken from the output so that 1129 # qemu-iotest can reliably diff the results against master output. 1130 output = io.StringIO() 1131 1132 logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN)) 1133 1134 if not test_function: 1135 execute_unittest(output, verbosity, debug) 1136 else: 1137 test_function() 1138 1139def script_main(test_function, *args, **kwargs): 1140 """Run script-style tests outside of the unittest framework""" 1141 execute_test(test_function, *args, **kwargs) 1142 1143def main(*args, **kwargs): 1144 """Run tests using the unittest framework""" 1145 execute_test(None, *args, **kwargs) 1146