master
py 1,766 lines 61.5 KB
Raw
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
19 import argparse
20 import atexit
21 import bz2
22 from collections import OrderedDict
23 import faulthandler
24 import json
25 import logging
26 import os
27 import re
28 import shutil
29 import signal
30 import struct
31 import subprocess
32 import sys
33 import time
34 from typing import (Any, Callable, Dict, Iterable, Iterator,
35 List, Optional, Sequence, TextIO, Tuple, Type, TypeVar)
36 import unittest
37
38 from contextlib import contextmanager
39
40 from qemu.machine import qtest
41 from qemu.qmp.legacy import QMPMessage, QMPReturnValue, QEMUMonitorProtocol
42 from qemu.utils import VerboseProcessError
43
44 # Use this logger for logging messages directly from the iotests module
45 logger = logging.getLogger('qemu.iotests')
46 logger.addHandler(logging.NullHandler())
47
48 # Use this logger for messages that ought to be used for diff output.
49 test_logger = logging.getLogger('qemu.iotests.diff_io')
50
51
52 faulthandler.enable()
53
54 # This will not work if arguments contain spaces but is necessary if we
55 # want to support the override options that ./check supports.
56 qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')]
57 if os.environ.get('QEMU_IMG_OPTIONS'):
58 qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ')
59
60 qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
61 if os.environ.get('QEMU_IO_OPTIONS'):
62 qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ')
63
64 qemu_io_args_no_fmt = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
65 if os.environ.get('QEMU_IO_OPTIONS_NO_FMT'):
66 qemu_io_args_no_fmt += \
67 os.environ['QEMU_IO_OPTIONS_NO_FMT'].strip().split(' ')
68
69 qemu_nbd_prog = os.environ.get('QEMU_NBD_PROG', 'qemu-nbd')
70 qemu_nbd_args = [qemu_nbd_prog]
71 if os.environ.get('QEMU_NBD_OPTIONS'):
72 qemu_nbd_args += os.environ['QEMU_NBD_OPTIONS'].strip().split(' ')
73
74 qemu_prog = os.environ.get('QEMU_PROG', 'qemu')
75 qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ')
76
77 qsd_prog = os.environ.get('QSD_PROG', 'qemu-storage-daemon')
78
79 gdb_qemu_env = os.environ.get('GDB_OPTIONS')
80 qemu_gdb = []
81 if gdb_qemu_env:
82 qemu_gdb = ['gdbserver'] + gdb_qemu_env.strip().split(' ')
83
84 qemu_print = os.environ.get('PRINT_QEMU', False)
85
86 imgfmt = os.environ.get('IMGFMT', 'raw')
87 imgproto = os.environ.get('IMGPROTO', 'file')
88
89 try:
90 test_dir = os.environ['TEST_DIR']
91 sock_dir = os.environ['SOCK_DIR']
92 cachemode = os.environ['CACHEMODE']
93 aiomode = os.environ['AIOMODE']
94 qemu_default_machine = os.environ['QEMU_DEFAULT_MACHINE']
95 except KeyError:
96 # We are using these variables as proxies to indicate that we're
97 # not being run via "check". There may be other things set up by
98 # "check" that individual test cases rely on.
99 sys.stderr.write('Please run this test via the "check" script\n')
100 sys.exit(os.EX_USAGE)
101
102 qemu_valgrind = []
103 if os.environ.get('VALGRIND_QEMU') == "y" and \
104 os.environ.get('NO_VALGRIND') != "y":
105 valgrind_logfile = "--log-file=" + test_dir
106 # %p allows to put the valgrind process PID, since
107 # we don't know it a priori (subprocess.Popen is
108 # not yet invoked)
109 valgrind_logfile += "/%p.valgrind"
110
111 qemu_valgrind = ['valgrind', valgrind_logfile, '--error-exitcode=99']
112
113 luks_default_secret_object = 'secret,id=keysec0,data=' + \
114 os.environ.get('IMGKEYSECRET', '')
115 luks_default_key_secret_opt = 'key-secret=keysec0'
116
117 sample_img_dir = os.environ['SAMPLE_IMG_DIR']
118
119
120 @contextmanager
121 def change_log_level(
122 logger_name: str, level: int = logging.CRITICAL) -> Iterator[None]:
123 """
124 Utility function for temporarily changing the log level of a logger.
125
126 This can be used to silence errors that are expected or uninteresting.
127 """
128 _logger = logging.getLogger(logger_name)
129 current_level = _logger.level
130 _logger.setLevel(level)
131
132 try:
133 yield
134 finally:
135 _logger.setLevel(current_level)
136
137
138 def unarchive_sample_image(sample, fname):
139 sample_fname = os.path.join(sample_img_dir, sample + '.bz2')
140 with bz2.open(sample_fname) as f_in, open(fname, 'wb') as f_out:
141 shutil.copyfileobj(f_in, f_out)
142
143
144 def qemu_tool_popen(args: Sequence[str],
145 connect_stderr: bool = True) -> 'subprocess.Popen[str]':
146 stderr = subprocess.STDOUT if connect_stderr else None
147 # pylint: disable=consider-using-with
148 return subprocess.Popen(args,
149 stdout=subprocess.PIPE,
150 stderr=stderr,
151 universal_newlines=True)
152
153
154 def qemu_tool_pipe_and_status(tool: str, args: Sequence[str],
155 connect_stderr: bool = True,
156 drop_successful_output: bool = False) \
157 -> Tuple[str, int]:
158 """
159 Run a tool and return both its output and its exit code
160 """
161 with qemu_tool_popen(args, connect_stderr) as subp:
162 output = subp.communicate()[0]
163 if subp.returncode < 0:
164 cmd = ' '.join(args)
165 sys.stderr.write(f'{tool} received signal \
166 {-subp.returncode}: {cmd}\n')
167 if drop_successful_output and subp.returncode == 0:
168 output = ''
169 return (output, subp.returncode)
170
171 def qemu_img_create_prepare_args(args: List[str]) -> List[str]:
172 if not args or args[0] != 'create':
173 return list(args)
174 args = args[1:]
175
176 p = argparse.ArgumentParser(allow_abbrev=False)
177 # -o option may be specified several times
178 p.add_argument('-o', action='append', default=[])
179 p.add_argument('-f')
180 parsed, remaining = p.parse_known_args(args)
181
182 opts_list = parsed.o
183
184 result = ['create']
185 if parsed.f is not None:
186 result += ['-f', parsed.f]
187
188 # IMGOPTS most probably contain options specific for the selected format,
189 # like extended_l2 or compression_type for qcow2. Test may want to create
190 # additional images in other formats that doesn't support these options.
191 # So, use IMGOPTS only for images created in imgfmt format.
192 imgopts = os.environ.get('IMGOPTS')
193 if imgopts and parsed.f == imgfmt:
194 opts_list.insert(0, imgopts)
195
196 # default luks support
197 if parsed.f == 'luks' and \
198 all('key-secret' not in opts for opts in opts_list):
199 result += ['--object', luks_default_secret_object]
200 opts_list.append(luks_default_key_secret_opt)
201
202 for opts in opts_list:
203 result += ['-o', opts]
204
205 result += remaining
206
207 return result
208
209
210 def qemu_tool(*args: str, check: bool = True, combine_stdio: bool = True
211 ) -> 'subprocess.CompletedProcess[str]':
212 """
213 Run a qemu tool and return its status code and console output.
214
215 :param args: full command line to run.
216 :param check: Enforce a return code of zero.
217 :param combine_stdio: set to False to keep stdout/stderr separated.
218
219 :raise VerboseProcessError:
220 When the return code is negative, or on any non-zero exit code
221 when 'check=True' was provided (the default). This exception has
222 'stdout', 'stderr', and 'returncode' properties that may be
223 inspected to show greater detail. If this exception is not
224 handled, the command-line, return code, and all console output
225 will be included at the bottom of the stack trace.
226
227 :return:
228 a CompletedProcess. This object has args, returncode, and stdout
229 properties. If streams are not combined, it will also have a
230 stderr property.
231 """
232 subp = subprocess.run(
233 args,
234 stdout=subprocess.PIPE,
235 stderr=subprocess.STDOUT if combine_stdio else subprocess.PIPE,
236 universal_newlines=True,
237 check=False
238 )
239
240 if check and subp.returncode or (subp.returncode < 0):
241 raise VerboseProcessError(
242 subp.returncode, args,
243 output=subp.stdout,
244 stderr=subp.stderr,
245 )
246
247 return subp
248
249
250 def qemu_img(*args: str, check: bool = True, combine_stdio: bool = True
251 ) -> 'subprocess.CompletedProcess[str]':
252 """
253 Run QEMU_IMG_PROG and return its status code and console output.
254
255 This function always prepends QEMU_IMG_OPTIONS and may further alter
256 the args for 'create' commands.
257
258 See `qemu_tool()` for greater detail.
259 """
260 full_args = qemu_img_args + qemu_img_create_prepare_args(list(args))
261 return qemu_tool(*full_args, check=check, combine_stdio=combine_stdio)
262
263
264 def ordered_qmp(qmsg, conv_keys=True):
265 # Dictionaries are not ordered prior to 3.6, therefore:
266 if isinstance(qmsg, list):
267 return [ordered_qmp(atom) for atom in qmsg]
268 if isinstance(qmsg, dict):
269 od = OrderedDict()
270 for k, v in sorted(qmsg.items()):
271 if conv_keys:
272 k = k.replace('_', '-')
273 od[k] = ordered_qmp(v, conv_keys=False)
274 return od
275 return qmsg
276
277 def qemu_img_create(*args: str) -> 'subprocess.CompletedProcess[str]':
278 return qemu_img('create', *args)
279
280 def qemu_img_json(*args: str) -> Any:
281 """
282 Run qemu-img and return its output as deserialized JSON.
283
284 :raise CalledProcessError:
285 When qemu-img crashes, or returns a non-zero exit code without
286 producing a valid JSON document to stdout.
287 :raise JSONDecoderError:
288 When qemu-img returns 0, but failed to produce a valid JSON document.
289
290 :return: A deserialized JSON object; probably a dict[str, Any].
291 """
292 try:
293 res = qemu_img(*args, combine_stdio=False)
294 except subprocess.CalledProcessError as exc:
295 # Terminated due to signal. Don't bother.
296 if exc.returncode < 0:
297 raise
298
299 # Commands like 'check' can return failure (exit codes 2 and 3)
300 # to indicate command completion, but with errors found. For
301 # multi-command flexibility, ignore the exact error codes and
302 # *try* to load JSON.
303 try:
304 return json.loads(exc.stdout)
305 except json.JSONDecodeError:
306 # Nope. This thing is toast. Raise the /process/ error.
307 pass
308 raise
309
310 return json.loads(res.stdout)
311
312 def qemu_img_measure(*args: str) -> Any:
313 return qemu_img_json("measure", "--output", "json", *args)
314
315 def qemu_img_check(*args: str) -> Any:
316 return qemu_img_json("check", "--output", "json", *args)
317
318 def qemu_img_info(*args: str) -> Any:
319 return qemu_img_json('info', "--output", "json", *args)
320
321 def qemu_img_map(*args: str) -> Any:
322 return qemu_img_json('map', "--output", "json", *args)
323
324 def qemu_img_log(*args: str, check: bool = True
325 ) -> 'subprocess.CompletedProcess[str]':
326 result = qemu_img(*args, check=check)
327 log(result.stdout, filters=[filter_testfiles])
328 return result
329
330 def img_info_log(filename: str, filter_path: Optional[str] = None,
331 use_image_opts: bool = False, extra_args: Sequence[str] = (),
332 check: bool = True, drop_child_info: bool = True,
333 ) -> None:
334 args = ['info']
335 if use_image_opts:
336 args.append('--image-opts')
337 else:
338 args += ['-f', imgfmt]
339 args += extra_args
340 args.append(filename)
341
342 output = qemu_img(*args, check=check).stdout
343 if not filter_path:
344 filter_path = filename
345 log(filter_img_info(output, filter_path, drop_child_info))
346
347 def qemu_io_wrap_args(args: Sequence[str]) -> List[str]:
348 if '-f' in args or '--image-opts' in args:
349 return qemu_io_args_no_fmt + list(args)
350 else:
351 return qemu_io_args + list(args)
352
353 def qemu_io_popen(*args):
354 return qemu_tool_popen(qemu_io_wrap_args(args))
355
356 def qemu_io(*args: str, check: bool = True, combine_stdio: bool = True
357 ) -> 'subprocess.CompletedProcess[str]':
358 """
359 Run QEMU_IO_PROG and return the status code and console output.
360
361 This function always prepends either QEMU_IO_OPTIONS or
362 QEMU_IO_OPTIONS_NO_FMT.
363 """
364 return qemu_tool(*qemu_io_wrap_args(args),
365 check=check, combine_stdio=combine_stdio)
366
367 def qemu_io_log(*args: str, check: bool = True
368 ) -> 'subprocess.CompletedProcess[str]':
369 result = qemu_io(*args, check=check)
370 log(result.stdout, filters=[filter_testfiles, filter_qemu_io])
371 return result
372
373 class QemuIoInteractive:
374 def __init__(self, *args):
375 self.args = qemu_io_wrap_args(args)
376 # We need to keep the Popen objext around, and not
377 # close it immediately. Therefore, disable the pylint check:
378 # pylint: disable=consider-using-with
379 self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE,
380 stdout=subprocess.PIPE,
381 stderr=subprocess.STDOUT,
382 universal_newlines=True)
383 out = self._p.stdout.read(9)
384 if out != 'qemu-io> ':
385 # Most probably qemu-io just failed to start.
386 # Let's collect the whole output and exit.
387 out += self._p.stdout.read()
388 self._p.wait(timeout=1)
389 raise ValueError(out)
390
391 def close(self):
392 self._p.communicate('q\n')
393
394 def _read_output(self):
395 pattern = 'qemu-io> '
396 n = len(pattern)
397 pos = 0
398 s = []
399 while pos != n:
400 c = self._p.stdout.read(1)
401 # check unexpected EOF
402 assert c != ''
403 s.append(c)
404 if c == pattern[pos]:
405 pos += 1
406 else:
407 pos = 0
408
409 return ''.join(s[:-n])
410
411 def cmd(self, cmd):
412 # quit command is in close(), '\n' is added automatically
413 assert '\n' not in cmd
414 cmd = cmd.strip()
415 assert cmd not in ('q', 'quit')
416 self._p.stdin.write(cmd + '\n')
417 self._p.stdin.flush()
418 return self._read_output()
419
420
421 class QemuStorageDaemon:
422 _qmp: Optional[QEMUMonitorProtocol] = None
423 _qmpsock: Optional[str] = None
424 # Python < 3.8 would complain if this type were not a string literal
425 # (importing `annotations` from `__future__` would work; but not on <= 3.6)
426 _p: 'Optional[subprocess.Popen[bytes]]' = None
427
428 def __init__(self, *args: str, instance_id: str = 'a', qmp: bool = False):
429 assert '--pidfile' not in args
430 self.pidfile = os.path.join(test_dir, f'qsd-{instance_id}-pid')
431 all_args = [qsd_prog] + list(args) + ['--pidfile', self.pidfile]
432
433 if qmp:
434 self._qmpsock = os.path.join(sock_dir, f'qsd-{instance_id}.sock')
435 all_args += ['--chardev',
436 f'socket,id=qmp-sock,path={self._qmpsock}',
437 '--monitor', 'qmp-sock']
438
439 self._qmp = QEMUMonitorProtocol(self._qmpsock, server=True)
440
441 # Cannot use with here, we want the subprocess to stay around
442 # pylint: disable=consider-using-with
443 self._p = subprocess.Popen(all_args)
444 if self._qmp is not None:
445 self._qmp.accept()
446 while not os.path.exists(self.pidfile):
447 if self._p.poll() is not None:
448 cmd = ' '.join(all_args)
449 raise RuntimeError(
450 'qemu-storage-daemon terminated with exit code ' +
451 f'{self._p.returncode}: {cmd}')
452
453 time.sleep(0.01)
454
455 with open(self.pidfile, encoding='utf-8') as f:
456 self._pid = int(f.read().strip())
457
458 assert self._pid == self._p.pid
459
460 def qmp(self, cmd: str, args: Optional[Dict[str, object]] = None) \
461 -> QMPMessage:
462 assert self._qmp is not None
463 return self._qmp.cmd_raw(cmd, args)
464
465 def get_qmp(self) -> QEMUMonitorProtocol:
466 assert self._qmp is not None
467 return self._qmp
468
469 def cmd(self, cmd: str, args: Optional[Dict[str, object]] = None) \
470 -> QMPReturnValue:
471 assert self._qmp is not None
472 return self._qmp.cmd(cmd, **(args or {}))
473
474 def stop(self, kill_signal=15):
475 self._p.send_signal(kill_signal)
476 self._p.wait()
477 self._p = None
478
479 if self._qmp:
480 self._qmp.close()
481
482 if self._qmpsock is not None:
483 try:
484 os.remove(self._qmpsock)
485 except OSError:
486 pass
487 try:
488 os.remove(self.pidfile)
489 except OSError:
490 pass
491
492 def __del__(self):
493 if self._p is not None:
494 self.stop(kill_signal=9)
495
496
497 def qemu_nbd(*args):
498 '''Run qemu-nbd in daemon mode and return the parent's exit code'''
499 return subprocess.call(qemu_nbd_args + ['--fork'] + list(args))
500
501 def qemu_nbd_early_pipe(*args: str) -> Tuple[int, str]:
502 '''Run qemu-nbd in daemon mode and return both the parent's exit code
503 and its output in case of an error'''
504 full_args = qemu_nbd_args + ['--fork'] + list(args)
505 output, returncode = qemu_tool_pipe_and_status('qemu-nbd', full_args,
506 connect_stderr=False)
507 return returncode, output if returncode else ''
508
509 def qemu_nbd_list_log(*args: str) -> str:
510 '''Run qemu-nbd to list remote exports'''
511 full_args = [qemu_nbd_prog, '-L'] + list(args)
512 output, _ = qemu_tool_pipe_and_status('qemu-nbd', full_args)
513 log(output, filters=[filter_testfiles, filter_nbd_exports])
514 return output
515
516 @contextmanager
517 def qemu_nbd_popen(*args):
518 '''Context manager running qemu-nbd within the context'''
519 pid_file = file_path("qemu_nbd_popen-nbd-pid-file")
520
521 assert not os.path.exists(pid_file)
522
523 cmd = list(qemu_nbd_args)
524 cmd.extend(('--persistent', '--pid-file', pid_file))
525 cmd.extend(args)
526
527 log('Start NBD server')
528 with subprocess.Popen(cmd) as p:
529 try:
530 while not os.path.exists(pid_file):
531 if p.poll() is not None:
532 raise RuntimeError(
533 "qemu-nbd terminated with exit code {}: {}"
534 .format(p.returncode, ' '.join(cmd)))
535
536 time.sleep(0.01)
537 yield
538 finally:
539 if os.path.exists(pid_file):
540 os.remove(pid_file)
541 log('Kill NBD server')
542 p.kill()
543 p.wait()
544
545 def compare_images(img1: str, img2: str,
546 fmt1: str = imgfmt, fmt2: str = imgfmt) -> bool:
547 """
548 Compare two images with QEMU_IMG; return True if they are identical.
549
550 :raise CalledProcessError:
551 when qemu-img crashes or returns a status code of anything other
552 than 0 (identical) or 1 (different).
553 """
554 try:
555 qemu_img('compare', '-f', fmt1, '-F', fmt2, img1, img2)
556 return True
557 except subprocess.CalledProcessError as exc:
558 if exc.returncode == 1:
559 return False
560 raise
561
562 def create_image(name, size):
563 '''Create a fully-allocated raw image with sector markers'''
564 with open(name, 'wb') as file:
565 i = 0
566 while i < size:
567 sector = struct.pack('>l504xl', i // 512, i // 512)
568 file.write(sector)
569 i = i + 512
570
571 def image_size(img: str) -> int:
572 """Return image's virtual size"""
573 value = qemu_img_info('-f', imgfmt, img)['virtual-size']
574 if not isinstance(value, int):
575 type_name = type(value).__name__
576 raise TypeError("Expected 'int' for 'virtual-size', "
577 f"got '{value}' of type '{type_name}'")
578 return value
579
580 def is_str(val):
581 return isinstance(val, str)
582
583 test_dir_re = re.compile(r"%s" % test_dir)
584 def filter_test_dir(msg):
585 return test_dir_re.sub("TEST_DIR", msg)
586
587 win32_re = re.compile(r"\r")
588 def filter_win32(msg):
589 return win32_re.sub("", msg)
590
591 qemu_io_re = re.compile(r"[0-9]* ops; [0-9\/:. sec]* "
592 r"\([0-9\/.inf]* [EPTGMKiBbytes]*\/sec "
593 r"and [0-9\/.inf]* ops\/sec\)")
594 def filter_qemu_io(msg):
595 msg = filter_win32(msg)
596 return qemu_io_re.sub("X ops; XX:XX:XX.X "
597 "(XXX YYY/sec and XXX ops/sec)", msg)
598
599 chown_re = re.compile(r"chown [0-9]+:[0-9]+")
600 def filter_chown(msg):
601 return chown_re.sub("chown UID:GID", msg)
602
603 def filter_qmp_event(event):
604 '''Filter the timestamp of a QMP event dict'''
605 event = dict(event)
606 if 'timestamp' in event:
607 event['timestamp']['seconds'] = 'SECS'
608 event['timestamp']['microseconds'] = 'USECS'
609 return event
610
611 def filter_block_job(event):
612 '''Filter the offset and length of a QMP block job event dict'''
613 event = dict(event)
614 if 'data' in event:
615 if 'offset' in event['data']:
616 event['data']['offset'] = 'OFFSET'
617 if 'len' in event['data']:
618 event['data']['len'] = 'LEN'
619 return event
620
621 def filter_qmp(qmsg, filter_fn):
622 '''Given a string filter, filter a QMP object's values.
623 filter_fn takes a (key, value) pair.'''
624 # Iterate through either lists or dicts;
625 if isinstance(qmsg, list):
626 items = enumerate(qmsg)
627 elif isinstance(qmsg, dict):
628 items = qmsg.items()
629 else:
630 return filter_fn(None, qmsg)
631
632 for k, v in items:
633 if isinstance(v, (dict, list)):
634 qmsg[k] = filter_qmp(v, filter_fn)
635 else:
636 qmsg[k] = filter_fn(k, v)
637 return qmsg
638
639 def filter_testfiles(msg):
640 pref1 = os.path.join(test_dir, "%s-" % (os.getpid()))
641 pref2 = os.path.join(sock_dir, "%s-" % (os.getpid()))
642 return msg.replace(pref1, 'TEST_DIR/PID-').replace(pref2, 'SOCK_DIR/PID-')
643
644 def filter_qmp_testfiles(qmsg):
645 def _filter(_key, value):
646 if is_str(value):
647 return filter_testfiles(value)
648 return value
649 return filter_qmp(qmsg, _filter)
650
651 def filter_virtio_scsi(output: str) -> str:
652 return re.sub(r'(virtio-scsi)-(ccw|pci)', r'\1', output)
653
654 def filter_qmp_virtio_scsi(qmsg):
655 def _filter(_key, value):
656 if is_str(value):
657 return filter_virtio_scsi(value)
658 return value
659 return filter_qmp(qmsg, _filter)
660
661 def filter_generated_node_ids(msg):
662 return re.sub("#block[0-9]+", "NODE_NAME", msg)
663
664 def filter_qmp_generated_node_ids(qmsg):
665 def _filter(_key, value):
666 if is_str(value):
667 return filter_generated_node_ids(value)
668 return value
669 return filter_qmp(qmsg, _filter)
670
671 def filter_img_info(output: str, filename: str,
672 drop_child_info: bool = True) -> str:
673 lines = []
674 drop_indented = False
675 for line in output.split('\n'):
676 if 'disk size' in line or 'actual-size' in line:
677 continue
678
679 # Drop child node info
680 if drop_indented:
681 if line.startswith(' '):
682 continue
683 drop_indented = False
684 if drop_child_info and "Child node '/" in line:
685 drop_indented = True
686 continue
687
688 line = line.replace(filename, 'TEST_IMG')
689 line = filter_testfiles(line)
690 line = line.replace(imgfmt, 'IMGFMT')
691 line = re.sub('iters: [0-9]+', 'iters: XXX', line)
692 line = re.sub('uuid: [-a-f0-9]+',
693 'uuid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
694 line)
695 line = re.sub('cid: [0-9]+', 'cid: XXXXXXXXXX', line)
696 line = re.sub('(compression type: )(zlib|zstd)', r'\1COMPRESSION_TYPE',
697 line)
698 lines.append(line)
699 return '\n'.join(lines)
700
701 def filter_imgfmt(msg):
702 return msg.replace(imgfmt, 'IMGFMT')
703
704 def filter_qmp_imgfmt(qmsg):
705 def _filter(_key, value):
706 if is_str(value):
707 return filter_imgfmt(value)
708 return value
709 return filter_qmp(qmsg, _filter)
710
711 def filter_nbd_exports(output: str) -> str:
712 return re.sub(r'((min|opt|max) block): [0-9]+', r'\1: XXX', output)
713
714 def filter_qtest(output: str) -> str:
715 output = re.sub(r'^\[I \d+\.\d+\] OPENED\n', '', output)
716 output = re.sub(r'\n?\[I \+\d+\.\d+\] CLOSED\n?$', '', output)
717 return output
718
719 Msg = TypeVar('Msg', Dict[str, Any], List[Any], str)
720
721 def log(msg: Msg,
722 filters: Iterable[Callable[[Msg], Msg]] = (),
723 indent: Optional[int] = None) -> None:
724 """
725 Logs either a string message or a JSON serializable message (like QMP).
726 If indent is provided, JSON serializable messages are pretty-printed.
727 """
728 for flt in filters:
729 msg = flt(msg)
730 if isinstance(msg, (dict, list)):
731 # Don't sort if it's already sorted
732 do_sort = not isinstance(msg, OrderedDict)
733 test_logger.info(json.dumps(msg, sort_keys=do_sort, indent=indent))
734 else:
735 test_logger.info(msg)
736
737 class Timeout:
738 def __init__(self, seconds, errmsg="Timeout"):
739 self.seconds = seconds
740 self.errmsg = errmsg
741 def __enter__(self):
742 if qemu_gdb or qemu_valgrind:
743 return self
744 signal.signal(signal.SIGALRM, self.timeout)
745 signal.setitimer(signal.ITIMER_REAL, self.seconds)
746 return self
747 def __exit__(self, exc_type, value, traceback):
748 if qemu_gdb or qemu_valgrind:
749 return False
750 signal.setitimer(signal.ITIMER_REAL, 0)
751 return False
752 def timeout(self, signum, frame):
753 raise TimeoutError(self.errmsg)
754
755 def file_pattern(name):
756 return "{0}-{1}".format(os.getpid(), name)
757
758 class FilePath:
759 """
760 Context manager generating multiple file names. The generated files are
761 removed when exiting the context.
762
763 Example usage:
764
765 with FilePath('a.img', 'b.img') as (img_a, img_b):
766 # Use img_a and img_b here...
767
768 # a.img and b.img are automatically removed here.
769
770 By default images are created in iotests.test_dir. To create sockets use
771 iotests.sock_dir:
772
773 with FilePath('a.sock', base_dir=iotests.sock_dir) as sock:
774
775 For convenience, calling with one argument yields a single file instead of
776 a tuple with one item.
777
778 """
779 def __init__(self, *names, base_dir=test_dir):
780 self.paths = [os.path.join(base_dir, file_pattern(name))
781 for name in names]
782
783 def __enter__(self):
784 if len(self.paths) == 1:
785 return self.paths[0]
786 else:
787 return self.paths
788
789 def __exit__(self, exc_type, exc_val, exc_tb):
790 for path in self.paths:
791 try:
792 os.remove(path)
793 except OSError:
794 pass
795 return False
796
797
798 def try_remove(img):
799 try:
800 os.remove(img)
801 except OSError:
802 pass
803
804 def file_path_remover():
805 for path in reversed(file_path_remover.paths):
806 try_remove(path)
807
808
809 def file_path(*names, base_dir=test_dir):
810 ''' Another way to get auto-generated filename that cleans itself up.
811
812 Use is as simple as:
813
814 img_a, img_b = file_path('a.img', 'b.img')
815 sock = file_path('socket')
816 '''
817
818 if not hasattr(file_path_remover, 'paths'):
819 file_path_remover.paths = []
820 atexit.register(file_path_remover)
821
822 paths = []
823 for name in names:
824 filename = file_pattern(name)
825 path = os.path.join(base_dir, filename)
826 file_path_remover.paths.append(path)
827 paths.append(path)
828
829 return paths[0] if len(paths) == 1 else paths
830
831 def remote_filename(path):
832 if imgproto == 'file':
833 return path
834 elif imgproto == 'ssh':
835 return "ssh://%s@127.0.0.1:22%s" % (os.environ.get('USER'), path)
836 else:
837 raise ValueError("Protocol %s not supported" % (imgproto))
838
839 class VM(qtest.QEMUQtestMachine):
840 '''A QEMU VM'''
841
842 def __init__(self, path_suffix=''):
843 name = "qemu%s-%d" % (path_suffix, os.getpid())
844 timer = 15.0 if not (qemu_gdb or qemu_valgrind) else None
845 if qemu_gdb and qemu_valgrind:
846 sys.stderr.write('gdb and valgrind are mutually exclusive\n')
847 sys.exit(1)
848 wrapper = qemu_gdb if qemu_gdb else qemu_valgrind
849 super().__init__(qemu_prog, qemu_opts, wrapper=wrapper,
850 name=name,
851 base_temp_dir=test_dir,
852 qmp_timer=timer)
853 self._num_drives = 0
854
855 def _post_shutdown(self) -> None:
856 super()._post_shutdown()
857 if not qemu_valgrind or not self._popen:
858 return
859 valgrind_filename = f"{test_dir}/{self._popen.pid}.valgrind"
860 if self.exitcode() == 99:
861 with open(valgrind_filename, encoding='utf-8') as f:
862 print(f.read())
863 else:
864 os.remove(valgrind_filename)
865
866 def _pre_launch(self) -> None:
867 super()._pre_launch()
868 if qemu_print:
869 # set QEMU binary output to stdout
870 self._close_qemu_log_file()
871
872 def add_object(self, opts):
873 self._args.append('-object')
874 self._args.append(opts)
875 return self
876
877 def add_device(self, opts):
878 self._args.append('-device')
879 self._args.append(opts)
880 return self
881
882 def add_drive_raw(self, opts):
883 self._args.append('-drive')
884 self._args.append(opts)
885 return self
886
887 def add_drive(self, path, opts='', interface='virtio', img_format=imgfmt):
888 '''Add a virtio-blk drive to the VM'''
889 options = ['if=%s' % interface,
890 'id=drive%d' % self._num_drives]
891
892 if path is not None:
893 options.append('file=%s' % path)
894 options.append('format=%s' % img_format)
895 options.append('cache=%s' % cachemode)
896 options.append('aio=%s' % aiomode)
897
898 if opts:
899 options.append(opts)
900
901 if img_format == 'luks' and 'key-secret' not in opts:
902 # default luks support
903 if luks_default_secret_object not in self._args:
904 self.add_object(luks_default_secret_object)
905
906 options.append(luks_default_key_secret_opt)
907
908 self._args.append('-drive')
909 self._args.append(','.join(options))
910 self._num_drives += 1
911 return self
912
913 def add_blockdev(self, opts):
914 self._args.append('-blockdev')
915 if isinstance(opts, str):
916 self._args.append(opts)
917 else:
918 self._args.append(','.join(opts))
919 return self
920
921 def add_incoming(self, addr):
922 self._args.append('-incoming')
923 self._args.append(addr)
924 return self
925
926 def add_paused(self):
927 self._args.append('-S')
928 return self
929
930 def hmp(self, command_line: str, use_log: bool = False) -> QMPMessage:
931 cmd = 'human-monitor-command'
932 kwargs: Dict[str, Any] = {'command-line': command_line}
933 if use_log:
934 return self.qmp_log(cmd, **kwargs)
935 else:
936 return self.qmp(cmd, **kwargs)
937
938 def pause_drive(self, drive: str, event: Optional[str] = None) -> None:
939 """Pause drive r/w operations"""
940 if not event:
941 self.pause_drive(drive, "read_aio")
942 self.pause_drive(drive, "write_aio")
943 return
944 self.qmp_qemu_io(drive, f'break {event} bp_{drive}')
945
946 def resume_drive(self, drive: str) -> None:
947 """Resume drive r/w operations"""
948 self.qmp_qemu_io(drive, f'remove_break bp_{drive}')
949
950 def hmp_qemu_io(self, drive: str, cmd: str,
951 use_log: bool = False, qdev: bool = False) -> QMPMessage:
952 """Write to a given drive using an HMP command"""
953 d = '-d ' if qdev else ''
954 return self.hmp(f'qemu-io {d}{drive} "{cmd}"', use_log=use_log)
955
956 def qmp_qemu_io(self, drive: str, cmd: str,
957 use_log: bool = False, qdev: bool = False) -> str:
958 """Write to a given drive using the x-qemu-io QMP command"""
959 kwargs: Dict[str, Any] = {'command': cmd}
960 if qdev:
961 kwargs['qdev'] = drive
962 else:
963 kwargs['device'] = drive
964 if use_log:
965 res = self.qmp_log('x-qemu-io', **kwargs)
966 else:
967 res = self.qmp('x-qemu-io', **kwargs)
968 return res.get('error', {}).get('desc', '')
969
970 def flatten_qmp_object(self, obj, output=None, basestr=''):
971 if output is None:
972 output = {}
973 if isinstance(obj, list):
974 for i, item in enumerate(obj):
975 self.flatten_qmp_object(item, output, basestr + str(i) + '.')
976 elif isinstance(obj, dict):
977 for key in obj:
978 self.flatten_qmp_object(obj[key], output, basestr + key + '.')
979 else:
980 output[basestr[:-1]] = obj # Strip trailing '.'
981 return output
982
983 def qmp_to_opts(self, obj):
984 obj = self.flatten_qmp_object(obj)
985 output_list = []
986 for key in obj:
987 output_list += [key + '=' + obj[key]]
988 return ','.join(output_list)
989
990 def get_qmp_events_filtered(self, wait=60.0):
991 result = []
992 for ev in self.get_qmp_events(wait=wait):
993 result.append(filter_qmp_event(ev))
994 return result
995
996 def qmp_log(self, cmd, filters=(), indent=None, **kwargs):
997 full_cmd = OrderedDict((
998 ("execute", cmd),
999 ("arguments", ordered_qmp(kwargs))
1000 ))
1001 log(full_cmd, filters, indent=indent)
1002 result = self.qmp(cmd, **kwargs)
1003 log(result, filters, indent=indent)
1004 return result
1005
1006 # Returns None on success, and an error string on failure
1007 def run_job(self, job: str, auto_finalize: bool = True,
1008 auto_dismiss: bool = False,
1009 pre_finalize: Optional[Callable[[], None]] = None,
1010 cancel: bool = False, wait: float = 60.0,
1011 filters: Iterable[Callable[[Any], Any]] = (),
1012 ) -> Optional[str]:
1013 """
1014 run_job moves a job from creation through to dismissal.
1015
1016 :param job: String. ID of recently-launched job
1017 :param auto_finalize: Bool. True if the job was launched with
1018 auto_finalize. Defaults to True.
1019 :param auto_dismiss: Bool. True if the job was launched with
1020 auto_dismiss=True. Defaults to False.
1021 :param pre_finalize: Callback. A callable that takes no arguments to be
1022 invoked prior to issuing job-finalize, if any.
1023 :param cancel: Bool. When true, cancels the job after the pre_finalize
1024 callback.
1025 :param wait: Float. Timeout value specifying how long to wait for any
1026 event, in seconds. Defaults to 60.0.
1027 """
1028 match_device = {'data': {'device': job}}
1029 match_id = {'data': {'id': job}}
1030 events = [
1031 ('BLOCK_JOB_COMPLETED', match_device),
1032 ('BLOCK_JOB_CANCELLED', match_device),
1033 ('BLOCK_JOB_ERROR', match_device),
1034 ('BLOCK_JOB_READY', match_device),
1035 ('BLOCK_JOB_PENDING', match_id),
1036 ('JOB_STATUS_CHANGE', match_id)
1037 ]
1038 error = None
1039 while True:
1040 ev = filter_qmp_event(self.events_wait(events, timeout=wait))
1041 if ev['event'] != 'JOB_STATUS_CHANGE':
1042 log(ev, filters=filters)
1043 continue
1044 status = ev['data']['status']
1045 if status == 'aborting':
1046 result = self.qmp('query-jobs')
1047 for j in result['return']:
1048 if j['id'] == job:
1049 error = j['error']
1050 log('Job failed: %s' % (j['error']), filters=filters)
1051 elif status == 'ready':
1052 self.qmp_log('job-complete', id=job, filters=filters)
1053 elif status == 'pending' and not auto_finalize:
1054 if pre_finalize:
1055 pre_finalize()
1056 if cancel:
1057 self.qmp_log('job-cancel', id=job, filters=filters)
1058 else:
1059 self.qmp_log('job-finalize', id=job, filters=filters)
1060 elif status == 'concluded' and not auto_dismiss:
1061 self.qmp_log('job-dismiss', id=job, filters=filters)
1062 elif status == 'null':
1063 return error
1064
1065 # Returns None on success, and an error string on failure
1066 def blockdev_create(self, options, job_id='job0', filters=None):
1067 if filters is None:
1068 filters = [filter_qmp_testfiles]
1069 result = self.qmp_log('blockdev-create', filters=filters,
1070 job_id=job_id, options=options)
1071
1072 if 'return' in result:
1073 assert result['return'] == {}
1074 job_result = self.run_job(job_id, filters=filters)
1075 else:
1076 job_result = result['error']
1077
1078 log("")
1079 return job_result
1080
1081 def enable_migration_events(self, name):
1082 log('Enabling migration QMP events on %s...' % name)
1083 log(self.qmp('migrate-set-capabilities', capabilities=[
1084 {
1085 'capability': 'events',
1086 'state': True
1087 }
1088 ]))
1089
1090 def wait_migration(self, expect_runstate: Optional[str]) -> bool:
1091 while True:
1092 event = self.event_wait('MIGRATION')
1093 # We use the default timeout, and with a timeout, event_wait()
1094 # never returns None
1095 assert event
1096
1097 log(event, filters=[filter_qmp_event])
1098 if event['data']['status'] in ('completed', 'failed'):
1099 break
1100
1101 if event['data']['status'] == 'completed':
1102 # The event may occur in finish-migrate, so wait for the expected
1103 # post-migration runstate
1104 runstate = None
1105 while runstate != expect_runstate:
1106 runstate = self.qmp('query-status')['return']['status']
1107 return True
1108 else:
1109 return False
1110
1111 def node_info(self, node_name):
1112 nodes = self.qmp('query-named-block-nodes')
1113 for x in nodes['return']:
1114 if x['node-name'] == node_name:
1115 return x
1116 return None
1117
1118 def query_bitmaps(self):
1119 res = self.qmp("query-named-block-nodes")
1120 return {device['node-name']: device['dirty-bitmaps']
1121 for device in res['return'] if 'dirty-bitmaps' in device}
1122
1123 def get_bitmap(self, node_name, bitmap_name, recording=None, bitmaps=None):
1124 """
1125 get a specific bitmap from the object returned by query_bitmaps.
1126 :param recording: If specified, filter results by the specified value.
1127 :param bitmaps: If specified, use it instead of call query_bitmaps()
1128 """
1129 if bitmaps is None:
1130 bitmaps = self.query_bitmaps()
1131
1132 for bitmap in bitmaps[node_name]:
1133 if bitmap.get('name', '') == bitmap_name:
1134 if recording is None or bitmap.get('recording') == recording:
1135 return bitmap
1136 return None
1137
1138 def check_bitmap_status(self, node_name, bitmap_name, fields):
1139 ret = self.get_bitmap(node_name, bitmap_name)
1140
1141 return fields.items() <= ret.items()
1142
1143 def assert_block_path(self, root, path, expected_node, graph=None):
1144 """
1145 Check whether the node under the given path in the block graph
1146 is @expected_node.
1147
1148 @root is the node name of the node where the @path is rooted.
1149
1150 @path is a string that consists of child names separated by
1151 slashes. It must begin with a slash.
1152
1153 Examples for @root + @path:
1154 - root="qcow2-node", path="/backing/file"
1155 - root="quorum-node", path="/children.2/file"
1156
1157 Hypothetically, @path could be empty, in which case it would
1158 point to @root. However, in practice this case is not useful
1159 and hence not allowed.
1160
1161 @expected_node may be None. (All elements of the path but the
1162 leaf must still exist.)
1163
1164 @graph may be None or the result of an x-debug-query-block-graph
1165 call that has already been performed.
1166 """
1167 if graph is None:
1168 graph = self.qmp('x-debug-query-block-graph')['return']
1169
1170 iter_path = iter(path.split('/'))
1171
1172 # Must start with a /
1173 assert next(iter_path) == ''
1174
1175 node = next((node for node in graph['nodes'] if node['name'] == root),
1176 None)
1177
1178 # An empty @path is not allowed, so the root node must be present
1179 assert node is not None, 'Root node %s not found' % root
1180
1181 for child_name in iter_path:
1182 assert node is not None, 'Cannot follow path %s%s' % (root, path)
1183
1184 try:
1185 node_id = next(edge['child'] for edge in graph['edges']
1186 if (edge['parent'] == node['id'] and
1187 edge['name'] == child_name))
1188
1189 node = next(node for node in graph['nodes']
1190 if node['id'] == node_id)
1191
1192 except StopIteration:
1193 node = None
1194
1195 if node is None:
1196 assert expected_node is None, \
1197 'No node found under %s (but expected %s)' % \
1198 (path, expected_node)
1199 else:
1200 assert node['name'] == expected_node, \
1201 'Found node %s under %s (but expected %s)' % \
1202 (node['name'], path, expected_node)
1203
1204 index_re = re.compile(r'([^\[]+)\[([^\]]+)\]')
1205
1206 class QMPTestCase(unittest.TestCase):
1207 '''Abstract base class for QMP test cases'''
1208
1209 def __init__(self, *args, **kwargs):
1210 super().__init__(*args, **kwargs)
1211 # Many users of this class set a VM property we rely on heavily
1212 # in the methods below.
1213 self.vm = None
1214
1215 def dictpath(self, d, path):
1216 '''Traverse a path in a nested dict'''
1217 for component in path.split('/'):
1218 m = index_re.match(component)
1219 if m:
1220 component, idx = m.groups()
1221 idx = int(idx)
1222
1223 if not isinstance(d, dict) or component not in d:
1224 self.fail(f'failed path traversal for "{path}" in "{d}"')
1225 d = d[component]
1226
1227 if m:
1228 if not isinstance(d, list):
1229 self.fail(f'path component "{component}" in "{path}" '
1230 f'is not a list in "{d}"')
1231 try:
1232 d = d[idx]
1233 except IndexError:
1234 self.fail(f'invalid index "{idx}" in path "{path}" '
1235 f'in "{d}"')
1236 return d
1237
1238 def assert_qmp_absent(self, d, path):
1239 try:
1240 result = self.dictpath(d, path)
1241 except AssertionError:
1242 return
1243 self.fail('path "%s" has value "%s"' % (path, str(result)))
1244
1245 def assert_qmp(self, d, path, value):
1246 '''Assert that the value for a specific path in a QMP dict
1247 matches. When given a list of values, assert that any of
1248 them matches.'''
1249
1250 result = self.dictpath(d, path)
1251
1252 # [] makes no sense as a list of valid values, so treat it as
1253 # an actual single value.
1254 if isinstance(value, list) and value != []:
1255 for v in value:
1256 if result == v:
1257 return
1258 self.fail('no match for "%s" in %s' % (str(result), str(value)))
1259 else:
1260 self.assertEqual(result, value,
1261 '"%s" is "%s", expected "%s"'
1262 % (path, str(result), str(value)))
1263
1264 def assert_no_active_block_jobs(self):
1265 result = self.vm.qmp('query-block-jobs')
1266 self.assert_qmp(result, 'return', [])
1267
1268 def assert_has_block_node(self, node_name=None, file_name=None):
1269 """Issue a query-named-block-nodes and assert node_name and/or
1270 file_name is present in the result"""
1271 def check_equal_or_none(a, b):
1272 return a is None or b is None or a == b
1273 assert node_name or file_name
1274 result = self.vm.qmp('query-named-block-nodes')
1275 for x in result["return"]:
1276 if check_equal_or_none(x.get("node-name"), node_name) and \
1277 check_equal_or_none(x.get("file"), file_name):
1278 return
1279 self.fail("Cannot find %s %s in result:\n%s" %
1280 (node_name, file_name, result))
1281
1282 def assert_json_filename_equal(self, json_filename, reference):
1283 '''Asserts that the given filename is a json: filename and that its
1284 content is equal to the given reference object'''
1285 self.assertEqual(json_filename[:5], 'json:')
1286 self.assertEqual(
1287 self.vm.flatten_qmp_object(json.loads(json_filename[5:])),
1288 self.vm.flatten_qmp_object(reference)
1289 )
1290
1291 def cancel_and_wait(self, drive='drive0', force=False,
1292 resume=False, wait=60.0):
1293 '''Cancel a block job and wait for it to finish, returning the event'''
1294 self.vm.cmd('block-job-cancel', device=drive, force=force)
1295
1296 if resume:
1297 self.vm.resume_drive(drive)
1298
1299 cancelled = False
1300 result = None
1301 while not cancelled:
1302 for event in self.vm.get_qmp_events(wait=wait):
1303 if event['event'] == 'BLOCK_JOB_COMPLETED' or \
1304 event['event'] == 'BLOCK_JOB_CANCELLED':
1305 self.assert_qmp(event, 'data/device', drive)
1306 result = event
1307 cancelled = True
1308 elif event['event'] == 'JOB_STATUS_CHANGE':
1309 self.assert_qmp(event, 'data/id', drive)
1310
1311
1312 self.assert_no_active_block_jobs()
1313 return result
1314
1315 def wait_until_completed(self, drive='drive0', check_offset=True,
1316 wait=60.0, error=None):
1317 '''Wait for a block job to finish, returning the event'''
1318 while True:
1319 for event in self.vm.get_qmp_events(wait=wait):
1320 if event['event'] == 'BLOCK_JOB_COMPLETED':
1321 self.assert_qmp(event, 'data/device', drive)
1322 if error is None:
1323 self.assert_qmp_absent(event, 'data/error')
1324 if check_offset:
1325 self.assert_qmp(event, 'data/offset',
1326 event['data']['len'])
1327 else:
1328 self.assert_qmp(event, 'data/error', error)
1329 self.assert_no_active_block_jobs()
1330 return event
1331 if event['event'] == 'JOB_STATUS_CHANGE':
1332 self.assert_qmp(event, 'data/id', drive)
1333
1334 def wait_ready(self, drive='drive0'):
1335 """Wait until a BLOCK_JOB_READY event, and return the event."""
1336 return self.vm.events_wait([
1337 ('BLOCK_JOB_READY',
1338 {'data': {'type': 'mirror', 'device': drive}}),
1339 ('BLOCK_JOB_READY',
1340 {'data': {'type': 'commit', 'device': drive}})
1341 ])
1342
1343 def wait_ready_and_cancel(self, drive='drive0'):
1344 self.wait_ready(drive=drive)
1345 event = self.cancel_and_wait(drive=drive)
1346 self.assertEqual(event['event'], 'BLOCK_JOB_COMPLETED')
1347 self.assert_qmp(event, 'data/type', 'mirror')
1348 self.assert_qmp(event, 'data/offset', event['data']['len'])
1349
1350 def complete_and_wait(self, drive='drive0', wait_ready=True,
1351 completion_error=None):
1352 '''Complete a block job and wait for it to finish'''
1353 if wait_ready:
1354 self.wait_ready(drive=drive)
1355
1356 self.vm.cmd('block-job-complete', device=drive)
1357
1358 event = self.wait_until_completed(drive=drive, error=completion_error)
1359 self.assertTrue(event['data']['type'] in ['mirror', 'commit'])
1360
1361 def pause_wait(self, job_id='job0'):
1362 with Timeout(3, "Timeout waiting for job to pause"):
1363 while True:
1364 result = self.vm.qmp('query-block-jobs')
1365 found = False
1366 for job in result['return']:
1367 if job['device'] == job_id:
1368 found = True
1369 if job['paused'] and not job['busy']:
1370 return job
1371 break
1372 assert found
1373
1374 def pause_job(self, job_id='job0', wait=True):
1375 self.vm.cmd('block-job-pause', device=job_id)
1376 if wait:
1377 self.pause_wait(job_id)
1378
1379 def case_skip(self, reason):
1380 '''Skip this test case'''
1381 case_notrun(reason)
1382 self.skipTest(reason)
1383
1384
1385 def notrun(reason):
1386 '''Skip this test suite'''
1387 # Each test in qemu-iotests has a number ("seq")
1388 seq = os.path.basename(sys.argv[0])
1389
1390 with open('%s/%s.notrun' % (test_dir, seq), 'w', encoding='utf-8') \
1391 as outfile:
1392 outfile.write(reason + '\n')
1393 logger.warning("%s not run: %s", seq, reason)
1394 sys.exit(0)
1395
1396 def case_notrun(reason):
1397 '''Mark this test case as not having been run (without actually
1398 skipping it, that is left to the caller). See
1399 QMPTestCase.case_skip() for a variant that actually skips the
1400 current test case.'''
1401
1402 # Each test in qemu-iotests has a number ("seq")
1403 seq = os.path.basename(sys.argv[0])
1404
1405 with open('%s/%s.casenotrun' % (test_dir, seq), 'a', encoding='utf-8') \
1406 as outfile:
1407 outfile.write(' [case not run] ' + reason + '\n')
1408
1409 def _verify_image_format(supported_fmts: Sequence[str] = (),
1410 unsupported_fmts: Sequence[str] = ()) -> None:
1411 if 'generic' in supported_fmts and \
1412 os.environ.get('IMGFMT_GENERIC', 'true') == 'true':
1413 # similar to
1414 # _supported_fmt generic
1415 # for bash tests
1416 supported_fmts = ()
1417
1418 not_sup = supported_fmts and (imgfmt not in supported_fmts)
1419 if not_sup or (imgfmt in unsupported_fmts):
1420 notrun('not suitable for this image format: %s' % imgfmt)
1421
1422 if imgfmt == 'luks':
1423 verify_working_luks()
1424
1425 def _verify_protocol(supported: Sequence[str] = (),
1426 unsupported: Sequence[str] = ()) -> None:
1427 assert not (supported and unsupported)
1428
1429 if 'generic' in supported:
1430 return
1431
1432 not_sup = supported and (imgproto not in supported)
1433 if not_sup or (imgproto in unsupported):
1434 notrun('not suitable for this protocol: %s' % imgproto)
1435
1436 def _verify_platform(supported: Sequence[str] = (),
1437 unsupported: Sequence[str] = ()) -> None:
1438 if any((sys.platform.startswith(x) for x in unsupported)):
1439 notrun('not suitable for this OS: %s' % sys.platform)
1440
1441 if supported:
1442 if not any((sys.platform.startswith(x) for x in supported)):
1443 notrun('not suitable for this OS: %s' % sys.platform)
1444
1445 def _verify_cache_mode(supported_cache_modes: Sequence[str] = ()) -> None:
1446 if supported_cache_modes and (cachemode not in supported_cache_modes):
1447 notrun('not suitable for this cache mode: %s' % cachemode)
1448
1449 def _verify_aio_mode(supported_aio_modes: Sequence[str] = ()) -> None:
1450 if supported_aio_modes and (aiomode not in supported_aio_modes):
1451 notrun('not suitable for this aio mode: %s' % aiomode)
1452
1453 def _verify_formats(required_formats: Sequence[str] = ()) -> None:
1454 usf_list = list(set(required_formats) - set(supported_formats()))
1455 if usf_list:
1456 notrun(f'formats {usf_list} are not whitelisted')
1457
1458
1459 def _verify_hmp() -> None:
1460 args = [qemu_prog] + qemu_opts + ['-M', 'none', '-monitor', 'stdio']
1461 with subprocess.Popen(args, stdin=subprocess.PIPE,
1462 stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
1463 universal_newlines=True) as subp:
1464 out, _ = subp.communicate('quit\n')
1465 if 'HMP monitor is not available' in out:
1466 notrun('HMP monitor not available')
1467
1468 def _verify_virtio_blk() -> None:
1469 out = qemu_pipe('-M', 'none', '-device', 'help')
1470 if 'virtio-blk' not in out:
1471 notrun('Missing virtio-blk in QEMU binary')
1472
1473 def verify_virtio_scsi_pci_or_ccw() -> None:
1474 out = qemu_pipe('-M', 'none', '-device', 'help')
1475 if 'virtio-scsi-pci' not in out and 'virtio-scsi-ccw' not in out:
1476 notrun('Missing virtio-scsi-pci or virtio-scsi-ccw in QEMU binary')
1477
1478
1479 def _verify_imgopts(unsupported: Sequence[str] = ()) -> None:
1480 imgopts = os.environ.get('IMGOPTS')
1481 # One of usage examples for IMGOPTS is "data_file=$TEST_IMG.ext_data_file"
1482 # but it supported only for bash tests. We don't have a concept of global
1483 # TEST_IMG in iotests.py, not saying about somehow parsing $variables.
1484 # So, for simplicity let's just not support any IMGOPTS with '$' inside.
1485 unsup = list(unsupported) + ['$']
1486 if imgopts and any(x in imgopts for x in unsup):
1487 notrun(f'not suitable for this imgopts: {imgopts}')
1488
1489
1490 def supports_quorum() -> bool:
1491 return 'quorum' in qemu_img('--help').stdout
1492
1493 def verify_quorum():
1494 '''Skip test suite if quorum support is not available'''
1495 if not supports_quorum():
1496 notrun('quorum support missing')
1497
1498 def has_working_luks() -> Tuple[bool, str]:
1499 """
1500 Check whether our LUKS driver can actually create images
1501 (this extends to LUKS encryption for qcow2).
1502
1503 If not, return the reason why.
1504 """
1505
1506 img_file = f'{test_dir}/luks-test.luks'
1507 res = qemu_img('create', '-f', 'luks',
1508 '--object', luks_default_secret_object,
1509 '-o', luks_default_key_secret_opt,
1510 '-o', 'iter-time=10',
1511 img_file, '1G',
1512 check=False)
1513 try:
1514 os.remove(img_file)
1515 except OSError:
1516 pass
1517
1518 if res.returncode:
1519 reason = res.stdout
1520 for line in res.stdout.splitlines():
1521 if img_file + ':' in line:
1522 reason = line.split(img_file + ':', 1)[1].strip()
1523 break
1524
1525 return (False, reason)
1526 else:
1527 return (True, '')
1528
1529 def verify_working_luks():
1530 """
1531 Skip test suite if LUKS does not work
1532 """
1533 (working, reason) = has_working_luks()
1534 if not working:
1535 notrun(reason)
1536
1537 def supports_qcow2_zstd_compression() -> bool:
1538 img_file = f'{test_dir}/qcow2-zstd-test.qcow2'
1539 res = qemu_img('create', '-f', 'qcow2', '-o', 'compression_type=zstd',
1540 img_file, '0',
1541 check=False)
1542 try:
1543 os.remove(img_file)
1544 except OSError:
1545 pass
1546
1547 if res.returncode == 1 and \
1548 "'compression-type' does not accept value 'zstd'" in res.stdout:
1549 return False
1550 else:
1551 return True
1552
1553 def verify_qcow2_zstd_compression():
1554 if not supports_qcow2_zstd_compression():
1555 notrun('zstd compression not supported')
1556
1557 def qemu_pipe(*args: str) -> str:
1558 """
1559 Run qemu with an option to print something and exit (e.g. a help option).
1560
1561 :return: QEMU's stdout output.
1562 """
1563 full_args = [qemu_prog] + qemu_opts + list(args)
1564 output, _ = qemu_tool_pipe_and_status('qemu', full_args)
1565 return output
1566
1567 def supported_formats(read_only=False):
1568 '''Set 'read_only' to True to check ro-whitelist
1569 Otherwise, rw-whitelist is checked'''
1570
1571 if not hasattr(supported_formats, "formats"):
1572 supported_formats.formats = {}
1573
1574 if read_only not in supported_formats.formats:
1575 format_message = qemu_pipe("-drive", "format=help")
1576 line = 1 if read_only else 0
1577 supported_formats.formats[read_only] = \
1578 format_message.splitlines()[line].split(":")[1].split()
1579
1580 return supported_formats.formats[read_only]
1581
1582 def skip_if_unsupported(required_formats=(), read_only=False):
1583 '''Skip Test Decorator
1584 Runs the test if all the required formats are whitelisted'''
1585 def skip_test_decorator(func):
1586 def func_wrapper(test_case: QMPTestCase, *args: List[Any],
1587 **kwargs: Dict[str, Any]) -> None:
1588 if callable(required_formats):
1589 fmts = required_formats(test_case)
1590 else:
1591 fmts = required_formats
1592
1593 usf_list = list(set(fmts) - set(supported_formats(read_only)))
1594 if usf_list:
1595 msg = f'{test_case}: formats {usf_list} are not whitelisted'
1596 test_case.case_skip(msg)
1597 else:
1598 func(test_case, *args, **kwargs)
1599 return func_wrapper
1600 return skip_test_decorator
1601
1602 def skip_for_formats(formats: Sequence[str] = ()) \
1603 -> Callable[[Callable[[QMPTestCase, List[Any], Dict[str, Any]], None]],
1604 Callable[[QMPTestCase, List[Any], Dict[str, Any]], None]]:
1605 '''Skip Test Decorator
1606 Skips the test for the given formats'''
1607 def skip_test_decorator(func):
1608 def func_wrapper(test_case: QMPTestCase, *args: List[Any],
1609 **kwargs: Dict[str, Any]) -> None:
1610 if imgfmt in formats:
1611 msg = f'{test_case}: Skipped for format {imgfmt}'
1612 test_case.case_skip(msg)
1613 else:
1614 func(test_case, *args, **kwargs)
1615 return func_wrapper
1616 return skip_test_decorator
1617
1618 def skip_if_user_is_root(func):
1619 '''Skip Test Decorator
1620 Runs the test only without root permissions'''
1621 def func_wrapper(*args, **kwargs):
1622 if os.getuid() == 0:
1623 case_notrun('{}: cannot be run as root'.format(args[0]))
1624 return None
1625 else:
1626 return func(*args, **kwargs)
1627 return func_wrapper
1628
1629 def skip_flaky(bugurl):
1630 '''Skip Test Decorator
1631 Always skips test due to unreliable design.
1632 Requires a bug report URL for historical record.'''
1633 def skip_test_decorator(func):
1634 def func_wrapper(*args, **kwargs):
1635 if os.environ.get("QEMU_TEST_FLAKY_TESTS", None) is None:
1636 case_notrun(
1637 ('{}: test is flaky (see {}) and $QEMU_TEST_FLAKY_TESTS ' +
1638 'is not set').format(args[0], bugurl))
1639 return None
1640 else:
1641 return func(*args, **kwargs)
1642 return func_wrapper
1643 return skip_test_decorator
1644
1645 # We need to filter out the time taken from the output so that
1646 # qemu-iotest can reliably diff the results against master output,
1647 # and hide skipped tests from the reference output.
1648
1649 class ReproducibleTestResult(unittest.TextTestResult):
1650 def addSkip(self, test, reason):
1651 # Same as TextTestResult, but print dot instead of "s"
1652 unittest.TestResult.addSkip(self, test, reason)
1653 if self.showAll:
1654 self.stream.writeln("skipped {0!r}".format(reason))
1655 elif self.dots:
1656 self.stream.write(".")
1657 self.stream.flush()
1658
1659 class ReproducibleStreamWrapper:
1660 def __init__(self, stream: TextIO):
1661 self.stream = stream
1662
1663 def __getattr__(self, attr):
1664 if attr in ('stream', '__getstate__'):
1665 raise AttributeError(attr)
1666 return getattr(self.stream, attr)
1667
1668 def write(self, arg=None):
1669 arg = re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', arg)
1670 arg = re.sub(r' \(skipped=\d+\)', r'', arg)
1671 self.stream.write(arg)
1672
1673 class ReproducibleTestRunner(unittest.TextTestRunner):
1674 def __init__(
1675 self,
1676 stream: Optional[TextIO] = None,
1677 resultclass: Type[unittest.TextTestResult] =
1678 ReproducibleTestResult,
1679 **kwargs: Any
1680 ) -> None:
1681 rstream = ReproducibleStreamWrapper(stream or sys.stdout)
1682 super().__init__(stream=rstream, # type: ignore
1683 descriptions=True,
1684 resultclass=resultclass,
1685 **kwargs)
1686
1687 def execute_unittest(argv: List[str], debug: bool = False) -> None:
1688 """Executes unittests within the calling module."""
1689
1690 # Some tests have warnings, especially ResourceWarnings for unclosed
1691 # files and sockets. Ignore them for now to ensure reproducibility of
1692 # the test output.
1693 unittest.main(argv=argv,
1694 testRunner=ReproducibleTestRunner,
1695 verbosity=2 if debug else 1,
1696 warnings=None if sys.warnoptions else 'ignore')
1697
1698 def execute_setup_common(supported_fmts: Sequence[str] = (),
1699 supported_platforms: Sequence[str] = (),
1700 supported_cache_modes: Sequence[str] = (),
1701 supported_aio_modes: Sequence[str] = (),
1702 unsupported_fmts: Sequence[str] = (),
1703 supported_protocols: Sequence[str] = (),
1704 unsupported_protocols: Sequence[str] = (),
1705 required_fmts: Sequence[str] = (),
1706 unsupported_imgopts: Sequence[str] = (),
1707 require_hmp: bool = False) -> bool:
1708 """
1709 Perform necessary setup for either script-style or unittest-style tests.
1710
1711 :return: Bool; Whether or not debug mode has been requested via the CLI.
1712 """
1713 # Note: Python 3.6 and pylint do not like 'Collection' so use 'Sequence'.
1714
1715 debug = '-d' in sys.argv
1716 if debug:
1717 sys.argv.remove('-d')
1718 logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN))
1719
1720 _verify_image_format(supported_fmts, unsupported_fmts)
1721 _verify_protocol(supported_protocols, unsupported_protocols)
1722 _verify_platform(supported=supported_platforms)
1723 _verify_cache_mode(supported_cache_modes)
1724 _verify_aio_mode(supported_aio_modes)
1725 _verify_formats(required_fmts)
1726 _verify_virtio_blk()
1727 _verify_imgopts(unsupported_imgopts)
1728 if require_hmp:
1729 _verify_hmp()
1730
1731 return debug
1732
1733 def execute_test(*args, test_function=None, **kwargs):
1734 """Run either unittest or script-style tests."""
1735
1736 debug = execute_setup_common(*args, **kwargs)
1737 if not test_function:
1738 execute_unittest(sys.argv, debug)
1739 else:
1740 test_function()
1741
1742 def activate_logging():
1743 """Activate iotests.log() output to stdout for script-style tests."""
1744 handler = logging.StreamHandler(stream=sys.stdout)
1745 formatter = logging.Formatter('%(message)s')
1746 handler.setFormatter(formatter)
1747 test_logger.addHandler(handler)
1748 test_logger.setLevel(logging.INFO)
1749 test_logger.propagate = False
1750
1751 # This is called from script-style iotests without a single point of entry
1752 def script_initialize(*args, **kwargs):
1753 """Initialize script-style tests without running any tests."""
1754 activate_logging()
1755 execute_setup_common(*args, **kwargs)
1756
1757 # This is called from script-style iotests with a single point of entry
1758 def script_main(test_function, *args, **kwargs):
1759 """Run script-style tests outside of the unittest framework"""
1760 activate_logging()
1761 execute_test(*args, test_function=test_function, **kwargs)
1762
1763 # This is called from unittest style iotests
1764 def main(*args, **kwargs):
1765 """Run tests using the unittest framework"""
1766 execute_test(*args, **kwargs)