]> git.proxmox.com Git - mirror_qemu.git/blob - tests/avocado/avocado_qemu/__init__.py
target/riscv: Fix vslide1up.vf and vslide1down.vf
[mirror_qemu.git] / tests / avocado / avocado_qemu / __init__.py
1 # Test class and utilities for functional tests
2 #
3 # Copyright (c) 2018 Red Hat, Inc.
4 #
5 # Author:
6 # Cleber Rosa <crosa@redhat.com>
7 #
8 # This work is licensed under the terms of the GNU GPL, version 2 or
9 # later. See the COPYING file in the top-level directory.
10
11 import logging
12 import os
13 import shutil
14 import subprocess
15 import sys
16 import tempfile
17 import time
18 import uuid
19
20 import avocado
21 from avocado.utils import cloudinit, datadrainer, process, ssh, vmimage
22 from avocado.utils.path import find_command
23
24 from qemu.machine import QEMUMachine
25 from qemu.utils import (get_info_usernet_hostfwd_port, kvm_available,
26 tcg_available)
27
28
29 #: The QEMU build root directory. It may also be the source directory
30 #: if building from the source dir, but it's safer to use BUILD_DIR for
31 #: that purpose. Be aware that if this code is moved outside of a source
32 #: and build tree, it will not be accurate.
33 BUILD_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
34
35 if os.path.islink(os.path.dirname(os.path.dirname(__file__))):
36 # The link to the avocado tests dir in the source code directory
37 lnk = os.path.dirname(os.path.dirname(__file__))
38 #: The QEMU root source directory
39 SOURCE_DIR = os.path.dirname(os.path.dirname(os.readlink(lnk)))
40 else:
41 SOURCE_DIR = BUILD_DIR
42
43
44 def has_cmd(name, args=None):
45 """
46 This function is for use in a @avocado.skipUnless decorator, e.g.:
47
48 @skipUnless(*has_cmd('sudo -n', ('sudo', '-n', 'true')))
49 def test_something_that_needs_sudo(self):
50 ...
51 """
52
53 if args is None:
54 args = ('which', name)
55
56 try:
57 _, stderr, exitcode = run_cmd(args)
58 except Exception as e:
59 exitcode = -1
60 stderr = str(e)
61
62 if exitcode != 0:
63 cmd_line = ' '.join(args)
64 err = f'{name} required, but "{cmd_line}" failed: {stderr.strip()}'
65 return (False, err)
66 else:
67 return (True, '')
68
69 def has_cmds(*cmds):
70 """
71 This function is for use in a @avocado.skipUnless decorator and
72 allows checking for the availability of multiple commands, e.g.:
73
74 @skipUnless(*has_cmds(('cmd1', ('cmd1', '--some-parameter')),
75 'cmd2', 'cmd3'))
76 def test_something_that_needs_cmd1_and_cmd2(self):
77 ...
78 """
79
80 for cmd in cmds:
81 if isinstance(cmd, str):
82 cmd = (cmd,)
83
84 ok, errstr = has_cmd(*cmd)
85 if not ok:
86 return (False, errstr)
87
88 return (True, '')
89
90 def run_cmd(args):
91 subp = subprocess.Popen(args,
92 stdout=subprocess.PIPE,
93 stderr=subprocess.PIPE,
94 universal_newlines=True)
95 stdout, stderr = subp.communicate()
96 ret = subp.returncode
97
98 return (stdout, stderr, ret)
99
100 def is_readable_executable_file(path):
101 return os.path.isfile(path) and os.access(path, os.R_OK | os.X_OK)
102
103
104 def pick_default_qemu_bin(bin_prefix='qemu-system-', arch=None):
105 """
106 Picks the path of a QEMU binary, starting either in the current working
107 directory or in the source tree root directory.
108
109 :param arch: the arch to use when looking for a QEMU binary (the target
110 will match the arch given). If None (the default), arch
111 will be the current host system arch (as given by
112 :func:`os.uname`).
113 :type arch: str
114 :returns: the path to the default QEMU binary or None if one could not
115 be found
116 :rtype: str or None
117 """
118 if arch is None:
119 arch = os.uname()[4]
120 # qemu binary path does not match arch for powerpc, handle it
121 if 'ppc64le' in arch:
122 arch = 'ppc64'
123 qemu_bin_name = bin_prefix + arch
124 qemu_bin_paths = [
125 os.path.join(".", qemu_bin_name),
126 os.path.join(BUILD_DIR, qemu_bin_name),
127 os.path.join(BUILD_DIR, "build", qemu_bin_name),
128 ]
129 for path in qemu_bin_paths:
130 if is_readable_executable_file(path):
131 return path
132 return None
133
134
135 def _console_interaction(test, success_message, failure_message,
136 send_string, keep_sending=False, vm=None):
137 assert not keep_sending or send_string
138 if vm is None:
139 vm = test.vm
140 console = vm.console_socket.makefile(mode='rb', encoding='utf-8')
141 console_logger = logging.getLogger('console')
142 while True:
143 if send_string:
144 vm.console_socket.sendall(send_string.encode())
145 if not keep_sending:
146 send_string = None # send only once
147 try:
148 msg = console.readline().decode().strip()
149 except UnicodeDecodeError:
150 msg = None
151 if not msg:
152 continue
153 console_logger.debug(msg)
154 if success_message is None or success_message in msg:
155 break
156 if failure_message and failure_message in msg:
157 console.close()
158 fail = 'Failure message found in console: "%s". Expected: "%s"' % \
159 (failure_message, success_message)
160 test.fail(fail)
161
162 def interrupt_interactive_console_until_pattern(test, success_message,
163 failure_message=None,
164 interrupt_string='\r'):
165 """
166 Keep sending a string to interrupt a console prompt, while logging the
167 console output. Typical use case is to break a boot loader prompt, such:
168
169 Press a key within 5 seconds to interrupt boot process.
170 5
171 4
172 3
173 2
174 1
175 Booting default image...
176
177 :param test: an Avocado test containing a VM that will have its console
178 read and probed for a success or failure message
179 :type test: :class:`avocado_qemu.QemuSystemTest`
180 :param success_message: if this message appears, test succeeds
181 :param failure_message: if this message appears, test fails
182 :param interrupt_string: a string to send to the console before trying
183 to read a new line
184 """
185 _console_interaction(test, success_message, failure_message,
186 interrupt_string, True)
187
188 def wait_for_console_pattern(test, success_message, failure_message=None,
189 vm=None):
190 """
191 Waits for messages to appear on the console, while logging the content
192
193 :param test: an Avocado test containing a VM that will have its console
194 read and probed for a success or failure message
195 :type test: :class:`avocado_qemu.QemuSystemTest`
196 :param success_message: if this message appears, test succeeds
197 :param failure_message: if this message appears, test fails
198 """
199 _console_interaction(test, success_message, failure_message, None, vm=vm)
200
201 def exec_command(test, command):
202 """
203 Send a command to a console (appending CRLF characters), while logging
204 the content.
205
206 :param test: an Avocado test containing a VM.
207 :type test: :class:`avocado_qemu.QemuSystemTest`
208 :param command: the command to send
209 :type command: str
210 """
211 _console_interaction(test, None, None, command + '\r')
212
213 def exec_command_and_wait_for_pattern(test, command,
214 success_message, failure_message=None):
215 """
216 Send a command to a console (appending CRLF characters), then wait
217 for success_message to appear on the console, while logging the.
218 content. Mark the test as failed if failure_message is found instead.
219
220 :param test: an Avocado test containing a VM that will have its console
221 read and probed for a success or failure message
222 :type test: :class:`avocado_qemu.QemuSystemTest`
223 :param command: the command to send
224 :param success_message: if this message appears, test succeeds
225 :param failure_message: if this message appears, test fails
226 """
227 _console_interaction(test, success_message, failure_message, command + '\r')
228
229 class QemuBaseTest(avocado.Test):
230
231 # default timeout for all tests, can be overridden
232 timeout = 120
233
234 def _get_unique_tag_val(self, tag_name):
235 """
236 Gets a tag value, if unique for a key
237 """
238 vals = self.tags.get(tag_name, [])
239 if len(vals) == 1:
240 return vals.pop()
241 return None
242
243 def setUp(self, bin_prefix):
244 self.arch = self.params.get('arch',
245 default=self._get_unique_tag_val('arch'))
246
247 self.cpu = self.params.get('cpu',
248 default=self._get_unique_tag_val('cpu'))
249
250 default_qemu_bin = pick_default_qemu_bin(bin_prefix, arch=self.arch)
251 self.qemu_bin = self.params.get('qemu_bin',
252 default=default_qemu_bin)
253 if self.qemu_bin is None:
254 self.cancel("No QEMU binary defined or found in the build tree")
255
256 def fetch_asset(self, name,
257 asset_hash=None, algorithm=None,
258 locations=None, expire=None,
259 find_only=False, cancel_on_missing=True):
260 return super().fetch_asset(name,
261 asset_hash=asset_hash,
262 algorithm=algorithm,
263 locations=locations,
264 expire=expire,
265 find_only=find_only,
266 cancel_on_missing=cancel_on_missing)
267
268
269 class QemuSystemTest(QemuBaseTest):
270 """Facilitates system emulation tests."""
271
272 def setUp(self):
273 self._vms = {}
274
275 super().setUp('qemu-system-')
276
277 self.machine = self.params.get('machine',
278 default=self._get_unique_tag_val('machine'))
279
280 def require_accelerator(self, accelerator):
281 """
282 Requires an accelerator to be available for the test to continue
283
284 It takes into account the currently set qemu binary.
285
286 If the check fails, the test is canceled. If the check itself
287 for the given accelerator is not available, the test is also
288 canceled.
289
290 :param accelerator: name of the accelerator, such as "kvm" or "tcg"
291 :type accelerator: str
292 """
293 checker = {'tcg': tcg_available,
294 'kvm': kvm_available}.get(accelerator)
295 if checker is None:
296 self.cancel("Don't know how to check for the presence "
297 "of accelerator %s" % accelerator)
298 if not checker(qemu_bin=self.qemu_bin):
299 self.cancel("%s accelerator does not seem to be "
300 "available" % accelerator)
301
302 def require_netdev(self, netdevname):
303 netdevhelp = run_cmd([self.qemu_bin,
304 '-M', 'none', '-netdev', 'help'])[0];
305 if netdevhelp.find('\n' + netdevname + '\n') < 0:
306 self.cancel('no support for user networking')
307
308 def _new_vm(self, name, *args):
309 self._sd = tempfile.TemporaryDirectory(prefix="qemu_")
310 vm = QEMUMachine(self.qemu_bin, base_temp_dir=self.workdir,
311 sock_dir=self._sd.name, log_dir=self.logdir)
312 self.log.debug('QEMUMachine "%s" created', name)
313 self.log.debug('QEMUMachine "%s" temp_dir: %s', name, vm.temp_dir)
314 self.log.debug('QEMUMachine "%s" log_dir: %s', name, vm.log_dir)
315 if args:
316 vm.add_args(*args)
317 return vm
318
319 @property
320 def vm(self):
321 return self.get_vm(name='default')
322
323 def get_vm(self, *args, name=None):
324 if not name:
325 name = str(uuid.uuid4())
326 if self._vms.get(name) is None:
327 self._vms[name] = self._new_vm(name, *args)
328 if self.cpu is not None:
329 self._vms[name].add_args('-cpu', self.cpu)
330 if self.machine is not None:
331 self._vms[name].set_machine(self.machine)
332 return self._vms[name]
333
334 def set_vm_arg(self, arg, value):
335 """
336 Set an argument to list of extra arguments to be given to the QEMU
337 binary. If the argument already exists then its value is replaced.
338
339 :param arg: the QEMU argument, such as "-cpu" in "-cpu host"
340 :type arg: str
341 :param value: the argument value, such as "host" in "-cpu host"
342 :type value: str
343 """
344 if not arg or not value:
345 return
346 if arg not in self.vm.args:
347 self.vm.args.extend([arg, value])
348 else:
349 idx = self.vm.args.index(arg) + 1
350 if idx < len(self.vm.args):
351 self.vm.args[idx] = value
352 else:
353 self.vm.args.append(value)
354
355 def tearDown(self):
356 for vm in self._vms.values():
357 vm.shutdown()
358 self._sd = None
359 super().tearDown()
360
361
362 class QemuUserTest(QemuBaseTest):
363 """Facilitates user-mode emulation tests."""
364
365 def setUp(self):
366 self._ldpath = []
367 super().setUp('qemu-')
368
369 def add_ldpath(self, ldpath):
370 self._ldpath.append(os.path.abspath(ldpath))
371
372 def run(self, bin_path, args=[]):
373 qemu_args = " ".join(["-L %s" % ldpath for ldpath in self._ldpath])
374 bin_args = " ".join(args)
375 return process.run("%s %s %s %s" % (self.qemu_bin, qemu_args,
376 bin_path, bin_args))
377
378
379 class LinuxSSHMixIn:
380 """Contains utility methods for interacting with a guest via SSH."""
381
382 def ssh_connect(self, username, credential, credential_is_key=True):
383 self.ssh_logger = logging.getLogger('ssh')
384 res = self.vm.command('human-monitor-command',
385 command_line='info usernet')
386 port = get_info_usernet_hostfwd_port(res)
387 self.assertIsNotNone(port)
388 self.assertGreater(port, 0)
389 self.log.debug('sshd listening on port: %d', port)
390 if credential_is_key:
391 self.ssh_session = ssh.Session('127.0.0.1', port=port,
392 user=username, key=credential)
393 else:
394 self.ssh_session = ssh.Session('127.0.0.1', port=port,
395 user=username, password=credential)
396 for i in range(10):
397 try:
398 self.ssh_session.connect()
399 return
400 except:
401 time.sleep(i)
402 self.fail('ssh connection timeout')
403
404 def ssh_command(self, command):
405 self.ssh_logger.info(command)
406 result = self.ssh_session.cmd(command)
407 stdout_lines = [line.rstrip() for line
408 in result.stdout_text.splitlines()]
409 for line in stdout_lines:
410 self.ssh_logger.info(line)
411 stderr_lines = [line.rstrip() for line
412 in result.stderr_text.splitlines()]
413 for line in stderr_lines:
414 self.ssh_logger.warning(line)
415
416 self.assertEqual(result.exit_status, 0,
417 f'Guest command failed: {command}')
418 return stdout_lines, stderr_lines
419
420 class LinuxDistro:
421 """Represents a Linux distribution
422
423 Holds information of known distros.
424 """
425 #: A collection of known distros and their respective image checksum
426 KNOWN_DISTROS = {
427 'fedora': {
428 '31': {
429 'x86_64':
430 {'checksum': ('e3c1b309d9203604922d6e255c2c5d09'
431 '8a309c2d46215d8fc026954f3c5c27a0'),
432 'pxeboot_url': ('https://archives.fedoraproject.org/'
433 'pub/archive/fedora/linux/releases/31/'
434 'Everything/x86_64/os/images/pxeboot/'),
435 'kernel_params': ('root=UUID=b1438b9b-2cab-4065-a99a-'
436 '08a96687f73c ro no_timer_check '
437 'net.ifnames=0 console=tty1 '
438 'console=ttyS0,115200n8'),
439 },
440 'aarch64':
441 {'checksum': ('1e18d9c0cf734940c4b5d5ec592facae'
442 'd2af0ad0329383d5639c997fdf16fe49'),
443 'pxeboot_url': 'https://archives.fedoraproject.org/'
444 'pub/archive/fedora/linux/releases/31/'
445 'Everything/aarch64/os/images/pxeboot/',
446 'kernel_params': ('root=UUID=b6950a44-9f3c-4076-a9c2-'
447 '355e8475b0a7 ro earlyprintk=pl011,0x9000000'
448 ' ignore_loglevel no_timer_check'
449 ' printk.time=1 rd_NO_PLYMOUTH'
450 ' console=ttyAMA0'),
451 },
452 'ppc64':
453 {'checksum': ('7c3528b85a3df4b2306e892199a9e1e4'
454 '3f991c506f2cc390dc4efa2026ad2f58')},
455 's390x':
456 {'checksum': ('4caaab5a434fd4d1079149a072fdc789'
457 '1e354f834d355069ca982fdcaf5a122d')},
458 },
459 '32': {
460 'aarch64':
461 {'checksum': ('b367755c664a2d7a26955bbfff985855'
462 'adfa2ca15e908baf15b4b176d68d3967'),
463 'pxeboot_url': ('http://dl.fedoraproject.org/pub/fedora/linux/'
464 'releases/32/Server/aarch64/os/images/'
465 'pxeboot/'),
466 'kernel_params': ('root=UUID=3df75b65-be8d-4db4-8655-'
467 '14d95c0e90c5 ro no_timer_check net.ifnames=0'
468 ' console=tty1 console=ttyS0,115200n8'),
469 },
470 },
471 '33': {
472 'aarch64':
473 {'checksum': ('e7f75cdfd523fe5ac2ca9eeece68edc1'
474 'a81f386a17f969c1d1c7c87031008a6b'),
475 'pxeboot_url': ('http://dl.fedoraproject.org/pub/fedora/linux/'
476 'releases/33/Server/aarch64/os/images/'
477 'pxeboot/'),
478 'kernel_params': ('root=UUID=d20b3ffa-6397-4a63-a734-'
479 '1126a0208f8a ro no_timer_check net.ifnames=0'
480 ' console=tty1 console=ttyS0,115200n8'
481 ' console=tty0'),
482 },
483 },
484 }
485 }
486
487 def __init__(self, name, version, arch):
488 self.name = name
489 self.version = version
490 self.arch = arch
491 try:
492 info = self.KNOWN_DISTROS.get(name).get(version).get(arch)
493 except AttributeError:
494 # Unknown distro
495 info = None
496 self._info = info or {}
497
498 @property
499 def checksum(self):
500 """Gets the cloud-image file checksum"""
501 return self._info.get('checksum', None)
502
503 @checksum.setter
504 def checksum(self, value):
505 self._info['checksum'] = value
506
507 @property
508 def pxeboot_url(self):
509 """Gets the repository url where pxeboot files can be found"""
510 return self._info.get('pxeboot_url', None)
511
512 @property
513 def default_kernel_params(self):
514 """Gets the default kernel parameters"""
515 return self._info.get('kernel_params', None)
516
517
518 class LinuxTest(LinuxSSHMixIn, QemuSystemTest):
519 """Facilitates having a cloud-image Linux based available.
520
521 For tests that intend to interact with guests, this is a better choice
522 to start with than the more vanilla `QemuSystemTest` class.
523 """
524
525 distro = None
526 username = 'root'
527 password = 'password'
528 smp = '2'
529 memory = '1024'
530
531 def _set_distro(self):
532 distro_name = self.params.get(
533 'distro',
534 default=self._get_unique_tag_val('distro'))
535 if not distro_name:
536 distro_name = 'fedora'
537
538 distro_version = self.params.get(
539 'distro_version',
540 default=self._get_unique_tag_val('distro_version'))
541 if not distro_version:
542 distro_version = '31'
543
544 self.distro = LinuxDistro(distro_name, distro_version, self.arch)
545
546 # The distro checksum behaves differently than distro name and
547 # version. First, it does not respect a tag with the same
548 # name, given that it's not expected to be used for filtering
549 # (distro name versions are the natural choice). Second, the
550 # order of precedence is: parameter, attribute and then value
551 # from KNOWN_DISTROS.
552 distro_checksum = self.params.get('distro_checksum',
553 default=None)
554 if distro_checksum:
555 self.distro.checksum = distro_checksum
556
557 def setUp(self, ssh_pubkey=None, network_device_type='virtio-net'):
558 super().setUp()
559 self.require_netdev('user')
560 self._set_distro()
561 self.vm.add_args('-smp', self.smp)
562 self.vm.add_args('-m', self.memory)
563 # The following network device allows for SSH connections
564 self.vm.add_args('-netdev', 'user,id=vnet,hostfwd=:127.0.0.1:0-:22',
565 '-device', '%s,netdev=vnet' % network_device_type)
566 self.set_up_boot()
567 if ssh_pubkey is None:
568 ssh_pubkey, self.ssh_key = self.set_up_existing_ssh_keys()
569 self.set_up_cloudinit(ssh_pubkey)
570
571 def set_up_existing_ssh_keys(self):
572 ssh_public_key = os.path.join(SOURCE_DIR, 'tests', 'keys', 'id_rsa.pub')
573 source_private_key = os.path.join(SOURCE_DIR, 'tests', 'keys', 'id_rsa')
574 ssh_dir = os.path.join(self.workdir, '.ssh')
575 os.mkdir(ssh_dir, mode=0o700)
576 ssh_private_key = os.path.join(ssh_dir,
577 os.path.basename(source_private_key))
578 shutil.copyfile(source_private_key, ssh_private_key)
579 os.chmod(ssh_private_key, 0o600)
580 return (ssh_public_key, ssh_private_key)
581
582 def download_boot(self):
583 self.log.debug('Looking for and selecting a qemu-img binary to be '
584 'used to create the bootable snapshot image')
585 # If qemu-img has been built, use it, otherwise the system wide one
586 # will be used. If none is available, the test will cancel.
587 qemu_img = os.path.join(BUILD_DIR, 'qemu-img')
588 if not os.path.exists(qemu_img):
589 qemu_img = find_command('qemu-img', False)
590 if qemu_img is False:
591 self.cancel('Could not find "qemu-img", which is required to '
592 'create the bootable image')
593 vmimage.QEMU_IMG = qemu_img
594
595 self.log.info('Downloading/preparing boot image')
596 # Fedora 31 only provides ppc64le images
597 image_arch = self.arch
598 if self.distro.name == 'fedora':
599 if image_arch == 'ppc64':
600 image_arch = 'ppc64le'
601
602 try:
603 boot = vmimage.get(
604 self.distro.name, arch=image_arch, version=self.distro.version,
605 checksum=self.distro.checksum,
606 algorithm='sha256',
607 cache_dir=self.cache_dirs[0],
608 snapshot_dir=self.workdir)
609 except:
610 self.cancel('Failed to download/prepare boot image')
611 return boot.path
612
613 def prepare_cloudinit(self, ssh_pubkey=None):
614 self.log.info('Preparing cloudinit image')
615 try:
616 cloudinit_iso = os.path.join(self.workdir, 'cloudinit.iso')
617 pubkey_content = None
618 if ssh_pubkey:
619 with open(ssh_pubkey) as pubkey:
620 pubkey_content = pubkey.read()
621 cloudinit.iso(cloudinit_iso, self.name,
622 username=self.username,
623 password=self.password,
624 # QEMU's hard coded usermode router address
625 phone_home_host='10.0.2.2',
626 phone_home_port=self.phone_server.server_port,
627 authorized_key=pubkey_content)
628 except Exception:
629 self.cancel('Failed to prepare the cloudinit image')
630 return cloudinit_iso
631
632 def set_up_boot(self):
633 path = self.download_boot()
634 self.vm.add_args('-drive', 'file=%s' % path)
635
636 def set_up_cloudinit(self, ssh_pubkey=None):
637 self.phone_server = cloudinit.PhoneHomeServer(('0.0.0.0', 0),
638 self.name)
639 cloudinit_iso = self.prepare_cloudinit(ssh_pubkey)
640 self.vm.add_args('-drive', 'file=%s,format=raw' % cloudinit_iso)
641
642 def launch_and_wait(self, set_up_ssh_connection=True):
643 self.vm.set_console()
644 self.vm.launch()
645 console_drainer = datadrainer.LineLogger(self.vm.console_socket.fileno(),
646 logger=self.log.getChild('console'))
647 console_drainer.start()
648 self.log.info('VM launched, waiting for boot confirmation from guest')
649 while not self.phone_server.instance_phoned_back:
650 self.phone_server.handle_request()
651
652 if set_up_ssh_connection:
653 self.log.info('Setting up the SSH connection')
654 self.ssh_connect(self.username, self.ssh_key)