xref: /openbmc/qemu/tests/vm/basevm.py (revision ba49d760eb04630e7b15f423ebecf6c871b8f77b)
1ff2ebff0SFam Zheng#
2ff2ebff0SFam Zheng# VM testing base class
3ff2ebff0SFam Zheng#
48dd38334SGerd Hoffmann# Copyright 2017-2019 Red Hat Inc.
5ff2ebff0SFam Zheng#
6ff2ebff0SFam Zheng# Authors:
7ff2ebff0SFam Zheng#  Fam Zheng <famz@redhat.com>
88dd38334SGerd Hoffmann#  Gerd Hoffmann <kraxel@redhat.com>
9ff2ebff0SFam Zheng#
10ff2ebff0SFam Zheng# This code is licensed under the GPL version 2 or later.  See
11ff2ebff0SFam Zheng# the COPYING file in the top-level directory.
12ff2ebff0SFam Zheng#
13ff2ebff0SFam Zheng
14ff2ebff0SFam Zhengimport os
158dd38334SGerd Hoffmannimport re
16ff2ebff0SFam Zhengimport sys
178dd38334SGerd Hoffmannimport socket
18ff2ebff0SFam Zhengimport logging
19ff2ebff0SFam Zhengimport time
20ff2ebff0SFam Zhengimport datetime
21ff2ebff0SFam Zhengimport subprocess
22ff2ebff0SFam Zhengimport hashlib
232fea3a12SAlex Bennéeimport argparse
24ff2ebff0SFam Zhengimport atexit
25ff2ebff0SFam Zhengimport tempfile
26ff2ebff0SFam Zhengimport shutil
27ff2ebff0SFam Zhengimport multiprocessing
28ff2ebff0SFam Zhengimport traceback
295d676197SRobert Foleyimport shlex
304cd57671SPhilippe Mathieu-Daudéimport json
31ff2ebff0SFam Zheng
32f4c66f17SJohn Snowfrom qemu.machine import QEMUMachine
33f4c66f17SJohn Snowfrom qemu.utils import get_info_usernet_hostfwd_port, kvm_available
34f4c66f17SJohn Snow
355d676197SRobert FoleySSH_KEY_FILE = os.path.join(os.path.dirname(__file__),
365d676197SRobert Foley               "..", "keys", "id_rsa")
375d676197SRobert FoleySSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__),
385d676197SRobert Foley                   "..", "keys", "id_rsa.pub")
39ff2ebff0SFam Zheng
405d676197SRobert Foley# This is the standard configuration.
415d676197SRobert Foley# Any or all of these can be overridden by
425d676197SRobert Foley# passing in a config argument to the VM constructor.
435d676197SRobert FoleyDEFAULT_CONFIG = {
445d676197SRobert Foley    'cpu'             : "max",
455d676197SRobert Foley    'machine'         : 'pc',
465d676197SRobert Foley    'guest_user'      : "qemu",
475d676197SRobert Foley    'guest_pass'      : "qemupass",
489fc33bf4SAlexander von Gluck IV    'root_user'       : "root",
495d676197SRobert Foley    'root_pass'       : "qemupass",
505d676197SRobert Foley    'ssh_key_file'    : SSH_KEY_FILE,
515d676197SRobert Foley    'ssh_pub_key_file': SSH_PUB_KEY_FILE,
525d676197SRobert Foley    'memory'          : "4G",
535d676197SRobert Foley    'extra_args'      : [],
545d676197SRobert Foley    'qemu_args'       : "",
555d676197SRobert Foley    'dns'             : "",
565d676197SRobert Foley    'ssh_port'        : 0,
575d676197SRobert Foley    'install_cmds'    : "",
585d676197SRobert Foley    'boot_dev_type'   : "block",
595d676197SRobert Foley    'ssh_timeout'     : 1,
605d676197SRobert Foley}
615d676197SRobert FoleyBOOT_DEVICE = {
625d676197SRobert Foley    'block' :  "-drive file={},if=none,id=drive0,cache=writeback "\
635d676197SRobert Foley               "-device virtio-blk,drive=drive0,bootindex=0",
645d676197SRobert Foley    'scsi'  :  "-device virtio-scsi-device,id=scsi "\
655d676197SRobert Foley               "-drive file={},format=raw,if=none,id=hd0 "\
665d676197SRobert Foley               "-device scsi-hd,drive=hd0,bootindex=0",
675d676197SRobert Foley}
68ff2ebff0SFam Zhengclass BaseVM(object):
69ff2ebff0SFam Zheng
70b08ba163SGerd Hoffmann    envvars = [
71b08ba163SGerd Hoffmann        "https_proxy",
72b08ba163SGerd Hoffmann        "http_proxy",
73b08ba163SGerd Hoffmann        "ftp_proxy",
74b08ba163SGerd Hoffmann        "no_proxy",
75b08ba163SGerd Hoffmann    ]
76b08ba163SGerd Hoffmann
77ff2ebff0SFam Zheng    # The script to run in the guest that builds QEMU
78ff2ebff0SFam Zheng    BUILD_SCRIPT = ""
79ff2ebff0SFam Zheng    # The guest name, to be overridden by subclasses
80ff2ebff0SFam Zheng    name = "#base"
8131719c37SPhilippe Mathieu-Daudé    # The guest architecture, to be overridden by subclasses
8231719c37SPhilippe Mathieu-Daudé    arch = "#arch"
83b3f94b2fSGerd Hoffmann    # command to halt the guest, can be overridden by subclasses
84b3f94b2fSGerd Hoffmann    poweroff = "poweroff"
854a70232bSRobert Foley    # Time to wait for shutdown to finish.
864a70232bSRobert Foley    shutdown_timeout_default = 30
875b790481SEduardo Habkost    # enable IPv6 networking
885b790481SEduardo Habkost    ipv6 = True
895d676197SRobert Foley    # This is the timeout on the wait for console bytes.
905d676197SRobert Foley    socket_timeout = 120
91c9de3935SRobert Foley    # Scale up some timeouts under TCG.
92c9de3935SRobert Foley    # 4 is arbitrary, but greater than 2,
93c9de3935SRobert Foley    # since we found we need to wait more than twice as long.
944a70232bSRobert Foley    tcg_timeout_multiplier = 4
955d676197SRobert Foley    def __init__(self, args, config=None):
96ff2ebff0SFam Zheng        self._guest = None
971f335d18SRobert Foley        self._genisoimage = args.genisoimage
981f335d18SRobert Foley        self._build_path = args.build_path
9913336606SRobert Foley        self._efi_aarch64 = args.efi_aarch64
1007bb17a92SAlex Bennée        self._source_path = args.source_path
1015d676197SRobert Foley        # Allow input config to override defaults.
1025d676197SRobert Foley        self._config = DEFAULT_CONFIG.copy()
103eaf46a65SJohn Snow
104eaf46a65SJohn Snow        # 1GB per core, minimum of 4. This is only a default.
105eaf46a65SJohn Snow        mem = max(4, args.jobs)
106eaf46a65SJohn Snow        self._config['memory'] = f"{mem}G"
107eaf46a65SJohn Snow
1085d676197SRobert Foley        if config != None:
1095d676197SRobert Foley            self._config.update(config)
1105d676197SRobert Foley        self.validate_ssh_keys()
111ff2ebff0SFam Zheng        self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
112ff2ebff0SFam Zheng                                                         suffix=".tmp",
113ff2ebff0SFam Zheng                                                         dir="."))
114ff2ebff0SFam Zheng        atexit.register(shutil.rmtree, self._tmpdir)
1155d676197SRobert Foley        # Copy the key files to a temporary directory.
1165d676197SRobert Foley        # Also chmod the key file to agree with ssh requirements.
1175d676197SRobert Foley        self._config['ssh_key'] = \
1185d676197SRobert Foley            open(self._config['ssh_key_file']).read().rstrip()
1195d676197SRobert Foley        self._config['ssh_pub_key'] = \
1205d676197SRobert Foley            open(self._config['ssh_pub_key_file']).read().rstrip()
1215d676197SRobert Foley        self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa")
1225d676197SRobert Foley        open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key'])
1235d676197SRobert Foley        subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file])
124ff2ebff0SFam Zheng
1255d676197SRobert Foley        self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
1265d676197SRobert Foley        open(self._ssh_tmp_pub_key_file,
1275d676197SRobert Foley             "w").write(self._config['ssh_pub_key'])
128ff2ebff0SFam Zheng
1291f335d18SRobert Foley        self.debug = args.debug
130ff14ab0cSRobert Foley        self._console_log_path = None
131ff14ab0cSRobert Foley        if args.log_console:
132ff14ab0cSRobert Foley                self._console_log_path = \
133ff14ab0cSRobert Foley                         os.path.join(os.path.expanduser("~/.cache/qemu-vm"),
134ff14ab0cSRobert Foley                                      "{}.install.log".format(self.name))
135ff2ebff0SFam Zheng        self._stderr = sys.stderr
136ff2ebff0SFam Zheng        self._devnull = open(os.devnull, "w")
137ff2ebff0SFam Zheng        if self.debug:
138ff2ebff0SFam Zheng            self._stdout = sys.stdout
139ff2ebff0SFam Zheng        else:
140ff2ebff0SFam Zheng            self._stdout = self._devnull
1415d676197SRobert Foley        netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
142ff2ebff0SFam Zheng        self._args = [ \
1435d676197SRobert Foley            "-nodefaults", "-m", self._config['memory'],
1445d676197SRobert Foley            "-cpu", self._config['cpu'],
1455d676197SRobert Foley            "-netdev",
1465d676197SRobert Foley            netdev.format(self._config['ssh_port']) +
1475d676197SRobert Foley            (",ipv6=no" if not self.ipv6 else "") +
1485d676197SRobert Foley            (",dns=" + self._config['dns'] if self._config['dns'] else ""),
149ff2ebff0SFam Zheng            "-device", "virtio-net-pci,netdev=vnet",
1508dd38334SGerd Hoffmann            "-vnc", "127.0.0.1:0,to=20"]
1511f335d18SRobert Foley        if args.jobs and args.jobs > 1:
1521f335d18SRobert Foley            self._args += ["-smp", "%d" % args.jobs]
15371531bb5SPhilippe Mathieu-Daudé        if kvm_available(self.arch):
1544a70232bSRobert Foley            self._shutdown_timeout = self.shutdown_timeout_default
155ff2ebff0SFam Zheng            self._args += ["-enable-kvm"]
156ff2ebff0SFam Zheng        else:
157ff2ebff0SFam Zheng            logging.info("KVM not available, not using -enable-kvm")
1584a70232bSRobert Foley            self._shutdown_timeout = \
1594a70232bSRobert Foley                self.shutdown_timeout_default * self.tcg_timeout_multiplier
160ff2ebff0SFam Zheng        self._data_args = []
161ff2ebff0SFam Zheng
1625d676197SRobert Foley        if self._config['qemu_args'] != None:
1635d676197SRobert Foley            qemu_args = self._config['qemu_args']
1645d676197SRobert Foley            qemu_args = qemu_args.replace('\n',' ').replace('\r','')
1655d676197SRobert Foley            # shlex groups quoted arguments together
1665d676197SRobert Foley            # we need this to keep the quoted args together for when
1675d676197SRobert Foley            # the QEMU command is issued later.
1685d676197SRobert Foley            args = shlex.split(qemu_args)
1695d676197SRobert Foley            self._config['extra_args'] = []
1705d676197SRobert Foley            for arg in args:
1715d676197SRobert Foley                if arg:
1725d676197SRobert Foley                    # Preserve quotes around arguments.
1735d676197SRobert Foley                    # shlex above takes them out, so add them in.
1745d676197SRobert Foley                    if " " in arg:
1755d676197SRobert Foley                        arg = '"{}"'.format(arg)
1765d676197SRobert Foley                    self._config['extra_args'].append(arg)
1775d676197SRobert Foley
1785d676197SRobert Foley    def validate_ssh_keys(self):
1795d676197SRobert Foley        """Check to see if the ssh key files exist."""
1805d676197SRobert Foley        if 'ssh_key_file' not in self._config or\
1815d676197SRobert Foley           not os.path.exists(self._config['ssh_key_file']):
1825d676197SRobert Foley            raise Exception("ssh key file not found.")
1835d676197SRobert Foley        if 'ssh_pub_key_file' not in self._config or\
1845d676197SRobert Foley           not os.path.exists(self._config['ssh_pub_key_file']):
1855d676197SRobert Foley               raise Exception("ssh pub key file not found.")
1865d676197SRobert Foley
1875d676197SRobert Foley    def wait_boot(self, wait_string=None):
1885d676197SRobert Foley        """Wait for the standard string we expect
1895d676197SRobert Foley           on completion of a normal boot.
1905d676197SRobert Foley           The user can also choose to override with an
1915d676197SRobert Foley           alternate string to wait for."""
1925d676197SRobert Foley        if wait_string is None:
1935d676197SRobert Foley            if self.login_prompt is None:
1945d676197SRobert Foley                raise Exception("self.login_prompt not defined")
1955d676197SRobert Foley            wait_string = self.login_prompt
1965d676197SRobert Foley        # Intentionally bump up the default timeout under TCG,
1975d676197SRobert Foley        # since the console wait below takes longer.
1985d676197SRobert Foley        timeout = self.socket_timeout
1995d676197SRobert Foley        if not kvm_available(self.arch):
2005d676197SRobert Foley            timeout *= 8
2015d676197SRobert Foley        self.console_init(timeout=timeout)
2025d676197SRobert Foley        self.console_wait(wait_string)
2035d676197SRobert Foley
2045b4b4865SAlex Bennée    def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
205ff2ebff0SFam Zheng        def check_sha256sum(fname):
206ff2ebff0SFam Zheng            if not sha256sum:
207ff2ebff0SFam Zheng                return True
208ff2ebff0SFam Zheng            checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
2093ace9be6SGerd Hoffmann            return sha256sum == checksum.decode("utf-8")
210ff2ebff0SFam Zheng
2115b4b4865SAlex Bennée        def check_sha512sum(fname):
2125b4b4865SAlex Bennée            if not sha512sum:
2135b4b4865SAlex Bennée                return True
2145b4b4865SAlex Bennée            checksum = subprocess.check_output(["sha512sum", fname]).split()[0]
2155b4b4865SAlex Bennée            return sha512sum == checksum.decode("utf-8")
2165b4b4865SAlex Bennée
217ff2ebff0SFam Zheng        cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
218ff2ebff0SFam Zheng        if not os.path.exists(cache_dir):
219ff2ebff0SFam Zheng            os.makedirs(cache_dir)
2203ace9be6SGerd Hoffmann        fname = os.path.join(cache_dir,
2213ace9be6SGerd Hoffmann                             hashlib.sha1(url.encode("utf-8")).hexdigest())
2225b4b4865SAlex Bennée        if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname):
223ff2ebff0SFam Zheng            return fname
224ff2ebff0SFam Zheng        logging.debug("Downloading %s to %s...", url, fname)
225ff2ebff0SFam Zheng        subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
226ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
227ff2ebff0SFam Zheng        os.rename(fname + ".download", fname)
228ff2ebff0SFam Zheng        return fname
229ff2ebff0SFam Zheng
230796471e9SGerd Hoffmann    def _ssh_do(self, user, cmd, check):
23189adc5b9SRobert Foley        ssh_cmd = ["ssh",
23289adc5b9SRobert Foley                   "-t",
233ff2ebff0SFam Zheng                   "-o", "StrictHostKeyChecking=no",
234ff2ebff0SFam Zheng                   "-o", "UserKnownHostsFile=" + os.devnull,
2355d676197SRobert Foley                   "-o",
2365d676197SRobert Foley                   "ConnectTimeout={}".format(self._config["ssh_timeout"]),
237339bf0c0SIlya Leoshkevich                   "-p", str(self.ssh_port), "-i", self._ssh_tmp_key_file,
238339bf0c0SIlya Leoshkevich                   "-o", "IdentitiesOnly=yes"]
23989adc5b9SRobert Foley        # If not in debug mode, set ssh to quiet mode to
24089adc5b9SRobert Foley        # avoid printing the results of commands.
24189adc5b9SRobert Foley        if not self.debug:
24289adc5b9SRobert Foley            ssh_cmd.append("-q")
243b08ba163SGerd Hoffmann        for var in self.envvars:
244b08ba163SGerd Hoffmann            ssh_cmd += ['-o', "SendEnv=%s" % var ]
245ff2ebff0SFam Zheng        assert not isinstance(cmd, str)
246ff2ebff0SFam Zheng        ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
247ff2ebff0SFam Zheng        logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
248726c9a3bSFam Zheng        r = subprocess.call(ssh_cmd)
249ff2ebff0SFam Zheng        if check and r != 0:
250ff2ebff0SFam Zheng            raise Exception("SSH command failed: %s" % cmd)
251ff2ebff0SFam Zheng        return r
252ff2ebff0SFam Zheng
253ff2ebff0SFam Zheng    def ssh(self, *cmd):
254df001680SRobert Foley        return self._ssh_do(self._config["guest_user"], cmd, False)
255ff2ebff0SFam Zheng
256ff2ebff0SFam Zheng    def ssh_root(self, *cmd):
2579fc33bf4SAlexander von Gluck IV        return self._ssh_do(self._config["root_user"], cmd, False)
258ff2ebff0SFam Zheng
259ff2ebff0SFam Zheng    def ssh_check(self, *cmd):
260df001680SRobert Foley        self._ssh_do(self._config["guest_user"], cmd, True)
261ff2ebff0SFam Zheng
262ff2ebff0SFam Zheng    def ssh_root_check(self, *cmd):
2639fc33bf4SAlexander von Gluck IV        self._ssh_do(self._config["root_user"], cmd, True)
264ff2ebff0SFam Zheng
265ff2ebff0SFam Zheng    def build_image(self, img):
266ff2ebff0SFam Zheng        raise NotImplementedError
267ff2ebff0SFam Zheng
2681e48931cSWainer dos Santos Moschetta    def exec_qemu_img(self, *args):
2691e48931cSWainer dos Santos Moschetta        cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
2701e48931cSWainer dos Santos Moschetta        cmd.extend(list(args))
2711e48931cSWainer dos Santos Moschetta        subprocess.check_call(cmd)
2721e48931cSWainer dos Santos Moschetta
273ff2ebff0SFam Zheng    def add_source_dir(self, src_dir):
2743ace9be6SGerd Hoffmann        name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
275ff2ebff0SFam Zheng        tarfile = os.path.join(self._tmpdir, name + ".tar")
276ff2ebff0SFam Zheng        logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
277ff2ebff0SFam Zheng        subprocess.check_call(["./scripts/archive-source.sh", tarfile],
278ff2ebff0SFam Zheng                              cwd=src_dir, stdin=self._devnull,
279ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
280ff2ebff0SFam Zheng        self._data_args += ["-drive",
281ff2ebff0SFam Zheng                            "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
282ff2ebff0SFam Zheng                                    (tarfile, name),
283ff2ebff0SFam Zheng                            "-device",
284ff2ebff0SFam Zheng                            "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
285ff2ebff0SFam Zheng
286ff2ebff0SFam Zheng    def boot(self, img, extra_args=[]):
2875d676197SRobert Foley        boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
2885d676197SRobert Foley        boot_params = boot_dev.format(img)
2895d676197SRobert Foley        args = self._args + boot_params.split(' ')
2905d676197SRobert Foley        args += self._data_args + extra_args + self._config['extra_args']
291ff2ebff0SFam Zheng        logging.debug("QEMU args: %s", " ".join(args))
292e56c4504SRobert Foley        qemu_path = get_qemu_path(self.arch, self._build_path)
293ff14ab0cSRobert Foley
294ff14ab0cSRobert Foley        # Since console_log_path is only set when the user provides the
295ff14ab0cSRobert Foley        # log_console option, we will set drain_console=True so the
296ff14ab0cSRobert Foley        # console is always drained.
297ff14ab0cSRobert Foley        guest = QEMUMachine(binary=qemu_path, args=args,
298ff14ab0cSRobert Foley                            console_log=self._console_log_path,
299ff14ab0cSRobert Foley                            drain_console=True)
3005d676197SRobert Foley        guest.set_machine(self._config['machine'])
3018dd38334SGerd Hoffmann        guest.set_console()
302ff2ebff0SFam Zheng        try:
303ff2ebff0SFam Zheng            guest.launch()
304ff2ebff0SFam Zheng        except:
305ff2ebff0SFam Zheng            logging.error("Failed to launch QEMU, command line:")
306e56c4504SRobert Foley            logging.error(" ".join([qemu_path] + args))
307ff2ebff0SFam Zheng            logging.error("Log:")
308ff2ebff0SFam Zheng            logging.error(guest.get_log())
309ff2ebff0SFam Zheng            logging.error("QEMU version >= 2.10 is required")
310ff2ebff0SFam Zheng            raise
311ff2ebff0SFam Zheng        atexit.register(self.shutdown)
312ff2ebff0SFam Zheng        self._guest = guest
313ff14ab0cSRobert Foley        # Init console so we can start consuming the chars.
314ff14ab0cSRobert Foley        self.console_init()
3159acd49e2SVladimir Sementsov-Ogievskiy        usernet_info = guest.cmd("human-monitor-command",
3169acd49e2SVladimir Sementsov-Ogievskiy                                 command_line="info usernet")
317976218cbSCleber Rosa        self.ssh_port = get_info_usernet_hostfwd_port(usernet_info)
318ff2ebff0SFam Zheng        if not self.ssh_port:
319ff2ebff0SFam Zheng            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
320ff2ebff0SFam Zheng                            usernet_info)
321ff2ebff0SFam Zheng
322ff14ab0cSRobert Foley    def console_init(self, timeout = None):
323ff14ab0cSRobert Foley        if timeout == None:
324ff14ab0cSRobert Foley            timeout = self.socket_timeout
3258dd38334SGerd Hoffmann        vm = self._guest
3268dd38334SGerd Hoffmann        vm.console_socket.settimeout(timeout)
327698a64f9SGerd Hoffmann        self.console_raw_path = os.path.join(vm._temp_dir,
328698a64f9SGerd Hoffmann                                             vm._name + "-console.raw")
329698a64f9SGerd Hoffmann        self.console_raw_file = open(self.console_raw_path, 'wb')
3308dd38334SGerd Hoffmann
3318dd38334SGerd Hoffmann    def console_log(self, text):
3328dd38334SGerd Hoffmann        for line in re.split("[\r\n]", text):
3338dd38334SGerd Hoffmann            # filter out terminal escape sequences
33486a8989dSPaolo Bonzini            line = re.sub("\x1b\\[[0-9;?]*[a-zA-Z]", "", line)
33586a8989dSPaolo Bonzini            line = re.sub("\x1b\\([0-9;?]*[a-zA-Z]", "", line)
3368dd38334SGerd Hoffmann            # replace unprintable chars
3378dd38334SGerd Hoffmann            line = re.sub("\x1b", "<esc>", line)
3388dd38334SGerd Hoffmann            line = re.sub("[\x00-\x1f]", ".", line)
3398dd38334SGerd Hoffmann            line = re.sub("[\x80-\xff]", ".", line)
3408dd38334SGerd Hoffmann            if line == "":
3418dd38334SGerd Hoffmann                continue
3428dd38334SGerd Hoffmann            # log console line
3438dd38334SGerd Hoffmann            sys.stderr.write("con recv: %s\n" % line)
3448dd38334SGerd Hoffmann
34560136e06SGerd Hoffmann    def console_wait(self, expect, expectalt = None):
3468dd38334SGerd Hoffmann        vm = self._guest
3478dd38334SGerd Hoffmann        output = ""
3488dd38334SGerd Hoffmann        while True:
3498dd38334SGerd Hoffmann            try:
3508dd38334SGerd Hoffmann                chars = vm.console_socket.recv(1)
351698a64f9SGerd Hoffmann                if self.console_raw_file:
352698a64f9SGerd Hoffmann                    self.console_raw_file.write(chars)
353698a64f9SGerd Hoffmann                    self.console_raw_file.flush()
3548dd38334SGerd Hoffmann            except socket.timeout:
3558dd38334SGerd Hoffmann                sys.stderr.write("console: *** read timeout ***\n")
3568dd38334SGerd Hoffmann                sys.stderr.write("console: waiting for: '%s'\n" % expect)
35760136e06SGerd Hoffmann                if not expectalt is None:
35860136e06SGerd Hoffmann                    sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
3598dd38334SGerd Hoffmann                sys.stderr.write("console: line buffer:\n")
3608dd38334SGerd Hoffmann                sys.stderr.write("\n")
3618dd38334SGerd Hoffmann                self.console_log(output.rstrip())
3628dd38334SGerd Hoffmann                sys.stderr.write("\n")
3638dd38334SGerd Hoffmann                raise
3648dd38334SGerd Hoffmann            output += chars.decode("latin1")
3658dd38334SGerd Hoffmann            if expect in output:
3668dd38334SGerd Hoffmann                break
36760136e06SGerd Hoffmann            if not expectalt is None and expectalt in output:
36860136e06SGerd Hoffmann                break
3698dd38334SGerd Hoffmann            if "\r" in output or "\n" in output:
3708dd38334SGerd Hoffmann                lines = re.split("[\r\n]", output)
3718dd38334SGerd Hoffmann                output = lines.pop()
3728dd38334SGerd Hoffmann                if self.debug:
3738dd38334SGerd Hoffmann                    self.console_log("\n".join(lines))
3748dd38334SGerd Hoffmann        if self.debug:
3758dd38334SGerd Hoffmann            self.console_log(output)
37660136e06SGerd Hoffmann        if not expectalt is None and expectalt in output:
37760136e06SGerd Hoffmann            return False
37860136e06SGerd Hoffmann        return True
3798dd38334SGerd Hoffmann
3806c4f0416SGerd Hoffmann    def console_consume(self):
3816c4f0416SGerd Hoffmann        vm = self._guest
3826c4f0416SGerd Hoffmann        output = ""
3836c4f0416SGerd Hoffmann        vm.console_socket.setblocking(0)
3846c4f0416SGerd Hoffmann        while True:
3856c4f0416SGerd Hoffmann            try:
3866c4f0416SGerd Hoffmann                chars = vm.console_socket.recv(1)
3876c4f0416SGerd Hoffmann            except:
3886c4f0416SGerd Hoffmann                break
3896c4f0416SGerd Hoffmann            output += chars.decode("latin1")
3906c4f0416SGerd Hoffmann            if "\r" in output or "\n" in output:
3916c4f0416SGerd Hoffmann                lines = re.split("[\r\n]", output)
3926c4f0416SGerd Hoffmann                output = lines.pop()
3936c4f0416SGerd Hoffmann                if self.debug:
3946c4f0416SGerd Hoffmann                    self.console_log("\n".join(lines))
3956c4f0416SGerd Hoffmann        if self.debug:
3966c4f0416SGerd Hoffmann            self.console_log(output)
3976c4f0416SGerd Hoffmann        vm.console_socket.setblocking(1)
3986c4f0416SGerd Hoffmann
3998dd38334SGerd Hoffmann    def console_send(self, command):
4008dd38334SGerd Hoffmann        vm = self._guest
4018dd38334SGerd Hoffmann        if self.debug:
4028dd38334SGerd Hoffmann            logline = re.sub("\n", "<enter>", command)
4038dd38334SGerd Hoffmann            logline = re.sub("[\x00-\x1f]", ".", logline)
4048dd38334SGerd Hoffmann            sys.stderr.write("con send: %s\n" % logline)
4058dd38334SGerd Hoffmann        for char in list(command):
4068dd38334SGerd Hoffmann            vm.console_socket.send(char.encode("utf-8"))
4078dd38334SGerd Hoffmann            time.sleep(0.01)
4088dd38334SGerd Hoffmann
4098dd38334SGerd Hoffmann    def console_wait_send(self, wait, command):
4108dd38334SGerd Hoffmann        self.console_wait(wait)
4118dd38334SGerd Hoffmann        self.console_send(command)
4128dd38334SGerd Hoffmann
4138dd38334SGerd Hoffmann    def console_ssh_init(self, prompt, user, pw):
4145d676197SRobert Foley        sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
4155d676197SRobert Foley                     % self._config['ssh_pub_key'].rstrip()
4168dd38334SGerd Hoffmann        self.console_wait_send("login:",    "%s\n" % user)
4178dd38334SGerd Hoffmann        self.console_wait_send("Password:", "%s\n" % pw)
4188dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "mkdir .ssh\n")
4198dd38334SGerd Hoffmann        self.console_wait_send(prompt,      sshkey_cmd)
4208dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 755 .ssh\n")
4218dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
4228dd38334SGerd Hoffmann
4238dd38334SGerd Hoffmann    def console_sshd_config(self, prompt):
4248dd38334SGerd Hoffmann        self.console_wait(prompt)
4258dd38334SGerd Hoffmann        self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
4267f0bf5eaSIlya Leoshkevich        self.console_wait(prompt)
4277f0bf5eaSIlya Leoshkevich        self.console_send("echo 'UseDNS no' >> /etc/ssh/sshd_config\n")
4288dd38334SGerd Hoffmann        for var in self.envvars:
4298dd38334SGerd Hoffmann            self.console_wait(prompt)
4308dd38334SGerd Hoffmann            self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
4318dd38334SGerd Hoffmann
4328dd38334SGerd Hoffmann    def print_step(self, text):
4338dd38334SGerd Hoffmann        sys.stderr.write("### %s ...\n" % text)
4348dd38334SGerd Hoffmann
4356ee982c9SRobert Foley    def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
436c9de3935SRobert Foley        # Allow more time for VM to boot under TCG.
437c9de3935SRobert Foley        if not kvm_available(self.arch):
4384a70232bSRobert Foley            seconds *= self.tcg_timeout_multiplier
439ff2ebff0SFam Zheng        starttime = datetime.datetime.now()
440f5d3d218SPhilippe Mathieu-Daudé        endtime = starttime + datetime.timedelta(seconds=seconds)
4416ee982c9SRobert Foley        cmd_success = False
442f5d3d218SPhilippe Mathieu-Daudé        while datetime.datetime.now() < endtime:
4436ee982c9SRobert Foley            if wait_root and self.ssh_root(cmd) == 0:
4446ee982c9SRobert Foley                cmd_success = True
445fbb3aa29SRobert Foley                break
4466ee982c9SRobert Foley            elif self.ssh(cmd) == 0:
4476ee982c9SRobert Foley                cmd_success = True
448ff2ebff0SFam Zheng                break
449f5d3d218SPhilippe Mathieu-Daudé            seconds = (endtime - datetime.datetime.now()).total_seconds()
450f5d3d218SPhilippe Mathieu-Daudé            logging.debug("%ds before timeout", seconds)
451ff2ebff0SFam Zheng            time.sleep(1)
4526ee982c9SRobert Foley        if not cmd_success:
453ff2ebff0SFam Zheng            raise Exception("Timeout while waiting for guest ssh")
454ff2ebff0SFam Zheng
455ff2ebff0SFam Zheng    def shutdown(self):
4564a70232bSRobert Foley        self._guest.shutdown(timeout=self._shutdown_timeout)
457ff2ebff0SFam Zheng
458ff2ebff0SFam Zheng    def wait(self):
4594a70232bSRobert Foley        self._guest.wait(timeout=self._shutdown_timeout)
460ff2ebff0SFam Zheng
461b3f94b2fSGerd Hoffmann    def graceful_shutdown(self):
462b3f94b2fSGerd Hoffmann        self.ssh_root(self.poweroff)
4634a70232bSRobert Foley        self._guest.wait(timeout=self._shutdown_timeout)
464b3f94b2fSGerd Hoffmann
465ff2ebff0SFam Zheng    def qmp(self, *args, **kwargs):
466ff2ebff0SFam Zheng        return self._guest.qmp(*args, **kwargs)
467ff2ebff0SFam Zheng
468b081986cSRobert Foley    def gen_cloud_init_iso(self):
469b081986cSRobert Foley        cidir = self._tmpdir
470b081986cSRobert Foley        mdata = open(os.path.join(cidir, "meta-data"), "w")
471b081986cSRobert Foley        name = self.name.replace(".","-")
472b081986cSRobert Foley        mdata.writelines(["instance-id: {}-vm-0\n".format(name),
473b081986cSRobert Foley                          "local-hostname: {}-guest\n".format(name)])
474b081986cSRobert Foley        mdata.close()
475b081986cSRobert Foley        udata = open(os.path.join(cidir, "user-data"), "w")
4765d676197SRobert Foley        print("guest user:pw {}:{}".format(self._config['guest_user'],
4775d676197SRobert Foley                                           self._config['guest_pass']))
478b081986cSRobert Foley        udata.writelines(["#cloud-config\n",
479b081986cSRobert Foley                          "chpasswd:\n",
480b081986cSRobert Foley                          "  list: |\n",
4815d676197SRobert Foley                          "    root:%s\n" % self._config['root_pass'],
4825d676197SRobert Foley                          "    %s:%s\n" % (self._config['guest_user'],
4835d676197SRobert Foley                                           self._config['guest_pass']),
484b081986cSRobert Foley                          "  expire: False\n",
485b081986cSRobert Foley                          "users:\n",
4865d676197SRobert Foley                          "  - name: %s\n" % self._config['guest_user'],
487b081986cSRobert Foley                          "    sudo: ALL=(ALL) NOPASSWD:ALL\n",
488b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
4895d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
490b081986cSRobert Foley                          "  - name: root\n",
491b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
4925d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
493b081986cSRobert Foley                          "locale: en_US.UTF-8\n"])
494b081986cSRobert Foley        proxy = os.environ.get("http_proxy")
495b081986cSRobert Foley        if not proxy is None:
496b081986cSRobert Foley            udata.writelines(["apt:\n",
497b081986cSRobert Foley                              "  proxy: %s" % proxy])
498b081986cSRobert Foley        udata.close()
49992fecad3SAlex Bennée        subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
500b081986cSRobert Foley                               "-volid", "cidata", "-joliet", "-rock",
501b081986cSRobert Foley                               "user-data", "meta-data"],
502b081986cSRobert Foley                              cwd=cidir,
503b081986cSRobert Foley                              stdin=self._devnull, stdout=self._stdout,
504b081986cSRobert Foley                              stderr=self._stdout)
505b081986cSRobert Foley        return os.path.join(cidir, "cloud-init.iso")
506b081986cSRobert Foley
5074cd57671SPhilippe Mathieu-Daudé    def get_qemu_packages_from_lcitool_json(self, json_path=None):
5084cd57671SPhilippe Mathieu-Daudé        """Parse a lcitool variables json file and return the PKGS list."""
5094cd57671SPhilippe Mathieu-Daudé        if json_path is None:
5104cd57671SPhilippe Mathieu-Daudé            json_path = os.path.join(
5114cd57671SPhilippe Mathieu-Daudé                os.path.dirname(__file__), "generated", self.name + ".json"
5124cd57671SPhilippe Mathieu-Daudé            )
5134cd57671SPhilippe Mathieu-Daudé        with open(json_path, "r") as fh:
5144cd57671SPhilippe Mathieu-Daudé            return json.load(fh)["pkgs"]
5154cd57671SPhilippe Mathieu-Daudé
5164cd57671SPhilippe Mathieu-Daudé
517e56c4504SRobert Foleydef get_qemu_path(arch, build_path=None):
518e56c4504SRobert Foley    """Fetch the path to the qemu binary."""
519e56c4504SRobert Foley    # If QEMU environment variable set, it takes precedence
520e56c4504SRobert Foley    if "QEMU" in os.environ:
521e56c4504SRobert Foley        qemu_path = os.environ["QEMU"]
522e56c4504SRobert Foley    elif build_path:
523e56c4504SRobert Foley        qemu_path = os.path.join(build_path, arch + "-softmmu")
524e56c4504SRobert Foley        qemu_path = os.path.join(qemu_path, "qemu-system-" + arch)
525e56c4504SRobert Foley    else:
526e56c4504SRobert Foley        # Default is to use system path for qemu.
527e56c4504SRobert Foley        qemu_path = "qemu-system-" + arch
528e56c4504SRobert Foley    return qemu_path
529e56c4504SRobert Foley
53013336606SRobert Foleydef get_qemu_version(qemu_path):
53113336606SRobert Foley    """Get the version number from the current QEMU,
53213336606SRobert Foley       and return the major number."""
53313336606SRobert Foley    output = subprocess.check_output([qemu_path, '--version'])
53413336606SRobert Foley    version_line = output.decode("utf-8")
53586a8989dSPaolo Bonzini    version_num = re.split(r' |\(', version_line)[3].split('.')[0]
53613336606SRobert Foley    return int(version_num)
53713336606SRobert Foley
5383f1e8137SRobert Foleydef parse_config(config, args):
5393f1e8137SRobert Foley    """ Parse yaml config and populate our config structure.
5403f1e8137SRobert Foley        The yaml config allows the user to override the
5413f1e8137SRobert Foley        defaults for VM parameters.  In many cases these
5423f1e8137SRobert Foley        defaults can be overridden without rebuilding the VM."""
5433f1e8137SRobert Foley    if args.config:
5443f1e8137SRobert Foley        config_file = args.config
5453f1e8137SRobert Foley    elif 'QEMU_CONFIG' in os.environ:
5463f1e8137SRobert Foley        config_file = os.environ['QEMU_CONFIG']
5473f1e8137SRobert Foley    else:
5483f1e8137SRobert Foley        return config
5493f1e8137SRobert Foley    if not os.path.exists(config_file):
5503f1e8137SRobert Foley        raise Exception("config file {} does not exist".format(config_file))
5513f1e8137SRobert Foley    # We gracefully handle importing the yaml module
5523f1e8137SRobert Foley    # since it might not be installed.
5533f1e8137SRobert Foley    # If we are here it means the user supplied a .yml file,
5543f1e8137SRobert Foley    # so if the yaml module is not installed we will exit with error.
5553f1e8137SRobert Foley    try:
5563f1e8137SRobert Foley        import yaml
5573f1e8137SRobert Foley    except ImportError:
5583f1e8137SRobert Foley        print("The python3-yaml package is needed "\
5593f1e8137SRobert Foley              "to support config.yaml files")
5603f1e8137SRobert Foley        # Instead of raising an exception we exit to avoid
5613f1e8137SRobert Foley        # a raft of messy (expected) errors to stdout.
5623f1e8137SRobert Foley        exit(1)
5633f1e8137SRobert Foley    with open(config_file) as f:
5643f1e8137SRobert Foley        yaml_dict = yaml.safe_load(f)
5653f1e8137SRobert Foley
5663f1e8137SRobert Foley    if 'qemu-conf' in yaml_dict:
5673f1e8137SRobert Foley        config.update(yaml_dict['qemu-conf'])
5683f1e8137SRobert Foley    else:
5693f1e8137SRobert Foley        raise Exception("config file {} is not valid"\
5703f1e8137SRobert Foley                        " missing qemu-conf".format(config_file))
5713f1e8137SRobert Foley    return config
5723f1e8137SRobert Foley
57363a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls):
5748a6e007eSPhilippe Mathieu-Daudé
5758a6e007eSPhilippe Mathieu-Daudé    def get_default_jobs():
576b0953944SAlex Bennée        if multiprocessing.cpu_count() > 1:
57763a24c5eSPhilippe Mathieu-Daudé            if kvm_available(vmcls.arch):
5783ad3e36eSWainer dos Santos Moschetta                return multiprocessing.cpu_count() // 2
579b0953944SAlex Bennée            elif os.uname().machine == "x86_64" and \
580b0953944SAlex Bennée                 vmcls.arch in ["aarch64", "x86_64", "i386"]:
581b0953944SAlex Bennée                # MTTCG is available on these arches and we can allow
582b0953944SAlex Bennée                # more cores. but only up to a reasonable limit. User
583b0953944SAlex Bennée                # can always override these limits with --jobs.
584b0953944SAlex Bennée                return min(multiprocessing.cpu_count() // 2, 8)
5858a6e007eSPhilippe Mathieu-Daudé        return 1
5868a6e007eSPhilippe Mathieu-Daudé
5872fea3a12SAlex Bennée    parser = argparse.ArgumentParser(
5882fea3a12SAlex Bennée        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
5892fea3a12SAlex Bennée        description="Utility for provisioning VMs and running builds",
5902fea3a12SAlex Bennée        epilog="""Remaining arguments are passed to the command.
5912fea3a12SAlex Bennée        Exit codes: 0 = success, 1 = command line error,
5922fea3a12SAlex Bennée        2 = environment initialization failed,
5932fea3a12SAlex Bennée        3 = test command failed""")
5942fea3a12SAlex Bennée    parser.add_argument("--debug", "-D", action="store_true",
595ff2ebff0SFam Zheng                        help="enable debug output")
5962fea3a12SAlex Bennée    parser.add_argument("--image", "-i", default="%s.img" % vmcls.name,
597ff2ebff0SFam Zheng                        help="image file name")
5982fea3a12SAlex Bennée    parser.add_argument("--force", "-f", action="store_true",
599ff2ebff0SFam Zheng                        help="force build image even if image exists")
6002fea3a12SAlex Bennée    parser.add_argument("--jobs", type=int, default=get_default_jobs(),
601ff2ebff0SFam Zheng                        help="number of virtual CPUs")
6022fea3a12SAlex Bennée    parser.add_argument("--verbose", "-V", action="store_true",
60341e3340aSPeter Maydell                        help="Pass V=1 to builds within the guest")
6042fea3a12SAlex Bennée    parser.add_argument("--build-image", "-b", action="store_true",
605ff2ebff0SFam Zheng                        help="build image")
6062fea3a12SAlex Bennée    parser.add_argument("--build-qemu",
607ff2ebff0SFam Zheng                        help="build QEMU from source in guest")
6082fea3a12SAlex Bennée    parser.add_argument("--build-target",
609*578774c0SAlex Bennée                        help="QEMU build target", default="all check")
6102fea3a12SAlex Bennée    parser.add_argument("--build-path", default=None,
611e56c4504SRobert Foley                        help="Path of build directory, "\
612e56c4504SRobert Foley                        "for using build tree QEMU binary. ")
6137bb17a92SAlex Bennée    parser.add_argument("--source-path", default=None,
6147bb17a92SAlex Bennée                        help="Path of source directory, "\
6157bb17a92SAlex Bennée                        "for finding additional files. ")
6162fea3a12SAlex Bennée    parser.add_argument("--interactive", "-I", action="store_true",
617ff2ebff0SFam Zheng                        help="Interactively run command")
6182fea3a12SAlex Bennée    parser.add_argument("--snapshot", "-s", action="store_true",
619983c2a77SFam Zheng                        help="run tests with a snapshot")
6202fea3a12SAlex Bennée    parser.add_argument("--genisoimage", default="genisoimage",
62192fecad3SAlex Bennée                        help="iso imaging tool")
6222fea3a12SAlex Bennée    parser.add_argument("--config", "-c", default=None,
6233f1e8137SRobert Foley                        help="Provide config yaml for configuration. "\
6243f1e8137SRobert Foley                        "See config_example.yaml for example.")
6252fea3a12SAlex Bennée    parser.add_argument("--efi-aarch64",
62613336606SRobert Foley                        default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
62713336606SRobert Foley                        help="Path to efi image for aarch64 VMs.")
6282fea3a12SAlex Bennée    parser.add_argument("--log-console", action="store_true",
629ff14ab0cSRobert Foley                        help="Log console to file.")
6302fea3a12SAlex Bennée    parser.add_argument("commands", nargs="*", help="""Remaining
6312fea3a12SAlex Bennée        commands after -- are passed to command inside the VM""")
6322fea3a12SAlex Bennée
633ff2ebff0SFam Zheng    return parser.parse_args()
634ff2ebff0SFam Zheng
6355d676197SRobert Foleydef main(vmcls, config=None):
636ff2ebff0SFam Zheng    try:
6375d676197SRobert Foley        if config == None:
6385d676197SRobert Foley            config = DEFAULT_CONFIG
6392fea3a12SAlex Bennée        args = parse_args(vmcls)
6402fea3a12SAlex Bennée        if not args.commands and not args.build_qemu and not args.build_image:
641f03868bdSEduardo Habkost            print("Nothing to do?")
642ff2ebff0SFam Zheng            return 1
6433f1e8137SRobert Foley        config = parse_config(config, args)
644fb3b4e6dSEduardo Habkost        logging.basicConfig(level=(logging.DEBUG if args.debug
645fb3b4e6dSEduardo Habkost                                   else logging.WARN))
6465d676197SRobert Foley        vm = vmcls(args, config=config)
647ff2ebff0SFam Zheng        if args.build_image:
648ff2ebff0SFam Zheng            if os.path.exists(args.image) and not args.force:
649151b7dbaSAlex Bennée                sys.stderr.writelines(["Image file exists, skipping build: %s\n" % args.image,
650ff2ebff0SFam Zheng                                      "Use --force option to overwrite\n"])
651151b7dbaSAlex Bennée                return 0
652ff2ebff0SFam Zheng            return vm.build_image(args.image)
653ff2ebff0SFam Zheng        if args.build_qemu:
654ff2ebff0SFam Zheng            vm.add_source_dir(args.build_qemu)
655ff2ebff0SFam Zheng            cmd = [vm.BUILD_SCRIPT.format(
6562fea3a12SAlex Bennée                   configure_opts = " ".join(args.commands),
6573ace9be6SGerd Hoffmann                   jobs=int(args.jobs),
6585c2ec9b6SAlex Bennée                   target=args.build_target,
65941e3340aSPeter Maydell                   verbose = "V=1" if args.verbose else "")]
660ff2ebff0SFam Zheng        else:
6612fea3a12SAlex Bennée            cmd = args.commands
662983c2a77SFam Zheng        img = args.image
663983c2a77SFam Zheng        if args.snapshot:
664983c2a77SFam Zheng            img += ",snapshot=on"
665983c2a77SFam Zheng        vm.boot(img)
666ff2ebff0SFam Zheng        vm.wait_ssh()
667ff2ebff0SFam Zheng    except Exception as e:
668ff2ebff0SFam Zheng        if isinstance(e, SystemExit) and e.code == 0:
669ff2ebff0SFam Zheng            return 0
670ff2ebff0SFam Zheng        sys.stderr.write("Failed to prepare guest environment\n")
671ff2ebff0SFam Zheng        traceback.print_exc()
672ff2ebff0SFam Zheng        return 2
673ff2ebff0SFam Zheng
674b3f94b2fSGerd Hoffmann    exitcode = 0
675ff2ebff0SFam Zheng    if vm.ssh(*cmd) != 0:
676b3f94b2fSGerd Hoffmann        exitcode = 3
677bcc388dfSAlex Bennée    if args.interactive:
678b3f94b2fSGerd Hoffmann        vm.ssh()
679b3f94b2fSGerd Hoffmann
680b3f94b2fSGerd Hoffmann    if not args.snapshot:
681b3f94b2fSGerd Hoffmann        vm.graceful_shutdown()
682b3f94b2fSGerd Hoffmann
683b3f94b2fSGerd Hoffmann    return exitcode
684