xref: /openbmc/qemu/tests/vm/basevm.py (revision ff14ab0c13ee470f8f3fdcdea14104cef3d9fe2f)
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
218f8fd9edSCleber Rosasys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
228b272e00SWainer dos Santos Moschettafrom qemu.accel import kvm_available
23abf0bf99SJohn Snowfrom qemu.machine import QEMUMachine
24ff2ebff0SFam Zhengimport subprocess
25ff2ebff0SFam Zhengimport hashlib
26ff2ebff0SFam Zhengimport optparse
27ff2ebff0SFam Zhengimport atexit
28ff2ebff0SFam Zhengimport tempfile
29ff2ebff0SFam Zhengimport shutil
30ff2ebff0SFam Zhengimport multiprocessing
31ff2ebff0SFam Zhengimport traceback
325d676197SRobert Foleyimport shlex
33ff2ebff0SFam Zheng
345d676197SRobert FoleySSH_KEY_FILE = os.path.join(os.path.dirname(__file__),
355d676197SRobert Foley               "..", "keys", "id_rsa")
365d676197SRobert FoleySSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__),
375d676197SRobert Foley                   "..", "keys", "id_rsa.pub")
38ff2ebff0SFam Zheng
395d676197SRobert Foley# This is the standard configuration.
405d676197SRobert Foley# Any or all of these can be overridden by
415d676197SRobert Foley# passing in a config argument to the VM constructor.
425d676197SRobert FoleyDEFAULT_CONFIG = {
435d676197SRobert Foley    'cpu'             : "max",
445d676197SRobert Foley    'machine'         : 'pc',
455d676197SRobert Foley    'guest_user'      : "qemu",
465d676197SRobert Foley    'guest_pass'      : "qemupass",
475d676197SRobert Foley    'root_pass'       : "qemupass",
485d676197SRobert Foley    'ssh_key_file'    : SSH_KEY_FILE,
495d676197SRobert Foley    'ssh_pub_key_file': SSH_PUB_KEY_FILE,
505d676197SRobert Foley    'memory'          : "4G",
515d676197SRobert Foley    'extra_args'      : [],
525d676197SRobert Foley    'qemu_args'       : "",
535d676197SRobert Foley    'dns'             : "",
545d676197SRobert Foley    'ssh_port'        : 0,
555d676197SRobert Foley    'install_cmds'    : "",
565d676197SRobert Foley    'boot_dev_type'   : "block",
575d676197SRobert Foley    'ssh_timeout'     : 1,
585d676197SRobert Foley}
595d676197SRobert FoleyBOOT_DEVICE = {
605d676197SRobert Foley    'block' :  "-drive file={},if=none,id=drive0,cache=writeback "\
615d676197SRobert Foley               "-device virtio-blk,drive=drive0,bootindex=0",
625d676197SRobert Foley    'scsi'  :  "-device virtio-scsi-device,id=scsi "\
635d676197SRobert Foley               "-drive file={},format=raw,if=none,id=hd0 "\
645d676197SRobert Foley               "-device scsi-hd,drive=hd0,bootindex=0",
655d676197SRobert Foley}
66ff2ebff0SFam Zhengclass BaseVM(object):
67ff2ebff0SFam Zheng
68b08ba163SGerd Hoffmann    envvars = [
69b08ba163SGerd Hoffmann        "https_proxy",
70b08ba163SGerd Hoffmann        "http_proxy",
71b08ba163SGerd Hoffmann        "ftp_proxy",
72b08ba163SGerd Hoffmann        "no_proxy",
73b08ba163SGerd Hoffmann    ]
74b08ba163SGerd Hoffmann
75ff2ebff0SFam Zheng    # The script to run in the guest that builds QEMU
76ff2ebff0SFam Zheng    BUILD_SCRIPT = ""
77ff2ebff0SFam Zheng    # The guest name, to be overridden by subclasses
78ff2ebff0SFam Zheng    name = "#base"
7931719c37SPhilippe Mathieu-Daudé    # The guest architecture, to be overridden by subclasses
8031719c37SPhilippe Mathieu-Daudé    arch = "#arch"
81b3f94b2fSGerd Hoffmann    # command to halt the guest, can be overridden by subclasses
82b3f94b2fSGerd Hoffmann    poweroff = "poweroff"
835b790481SEduardo Habkost    # enable IPv6 networking
845b790481SEduardo Habkost    ipv6 = True
855d676197SRobert Foley    # This is the timeout on the wait for console bytes.
865d676197SRobert Foley    socket_timeout = 120
87c9de3935SRobert Foley    # Scale up some timeouts under TCG.
88c9de3935SRobert Foley    # 4 is arbitrary, but greater than 2,
89c9de3935SRobert Foley    # since we found we need to wait more than twice as long.
90c9de3935SRobert Foley    tcg_ssh_timeout_multiplier = 4
915d676197SRobert Foley    def __init__(self, args, config=None):
92ff2ebff0SFam Zheng        self._guest = None
931f335d18SRobert Foley        self._genisoimage = args.genisoimage
941f335d18SRobert Foley        self._build_path = args.build_path
9513336606SRobert Foley        self._efi_aarch64 = args.efi_aarch64
965d676197SRobert Foley        # Allow input config to override defaults.
975d676197SRobert Foley        self._config = DEFAULT_CONFIG.copy()
985d676197SRobert Foley        if config != None:
995d676197SRobert Foley            self._config.update(config)
1005d676197SRobert Foley        self.validate_ssh_keys()
101ff2ebff0SFam Zheng        self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
102ff2ebff0SFam Zheng                                                         suffix=".tmp",
103ff2ebff0SFam Zheng                                                         dir="."))
104ff2ebff0SFam Zheng        atexit.register(shutil.rmtree, self._tmpdir)
1055d676197SRobert Foley        # Copy the key files to a temporary directory.
1065d676197SRobert Foley        # Also chmod the key file to agree with ssh requirements.
1075d676197SRobert Foley        self._config['ssh_key'] = \
1085d676197SRobert Foley            open(self._config['ssh_key_file']).read().rstrip()
1095d676197SRobert Foley        self._config['ssh_pub_key'] = \
1105d676197SRobert Foley            open(self._config['ssh_pub_key_file']).read().rstrip()
1115d676197SRobert Foley        self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa")
1125d676197SRobert Foley        open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key'])
1135d676197SRobert Foley        subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file])
114ff2ebff0SFam Zheng
1155d676197SRobert Foley        self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
1165d676197SRobert Foley        open(self._ssh_tmp_pub_key_file,
1175d676197SRobert Foley             "w").write(self._config['ssh_pub_key'])
118ff2ebff0SFam Zheng
1191f335d18SRobert Foley        self.debug = args.debug
120*ff14ab0cSRobert Foley        self._console_log_path = None
121*ff14ab0cSRobert Foley        if args.log_console:
122*ff14ab0cSRobert Foley                self._console_log_path = \
123*ff14ab0cSRobert Foley                         os.path.join(os.path.expanduser("~/.cache/qemu-vm"),
124*ff14ab0cSRobert Foley                                      "{}.install.log".format(self.name))
125ff2ebff0SFam Zheng        self._stderr = sys.stderr
126ff2ebff0SFam Zheng        self._devnull = open(os.devnull, "w")
127ff2ebff0SFam Zheng        if self.debug:
128ff2ebff0SFam Zheng            self._stdout = sys.stdout
129ff2ebff0SFam Zheng        else:
130ff2ebff0SFam Zheng            self._stdout = self._devnull
1315d676197SRobert Foley        netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
132ff2ebff0SFam Zheng        self._args = [ \
1335d676197SRobert Foley            "-nodefaults", "-m", self._config['memory'],
1345d676197SRobert Foley            "-cpu", self._config['cpu'],
1355d676197SRobert Foley            "-netdev",
1365d676197SRobert Foley            netdev.format(self._config['ssh_port']) +
1375d676197SRobert Foley            (",ipv6=no" if not self.ipv6 else "") +
1385d676197SRobert Foley            (",dns=" + self._config['dns'] if self._config['dns'] else ""),
139ff2ebff0SFam Zheng            "-device", "virtio-net-pci,netdev=vnet",
1408dd38334SGerd Hoffmann            "-vnc", "127.0.0.1:0,to=20"]
1411f335d18SRobert Foley        if args.jobs and args.jobs > 1:
1421f335d18SRobert Foley            self._args += ["-smp", "%d" % args.jobs]
14371531bb5SPhilippe Mathieu-Daudé        if kvm_available(self.arch):
144ff2ebff0SFam Zheng            self._args += ["-enable-kvm"]
145ff2ebff0SFam Zheng        else:
146ff2ebff0SFam Zheng            logging.info("KVM not available, not using -enable-kvm")
147ff2ebff0SFam Zheng        self._data_args = []
148ff2ebff0SFam Zheng
1495d676197SRobert Foley        if self._config['qemu_args'] != None:
1505d676197SRobert Foley            qemu_args = self._config['qemu_args']
1515d676197SRobert Foley            qemu_args = qemu_args.replace('\n',' ').replace('\r','')
1525d676197SRobert Foley            # shlex groups quoted arguments together
1535d676197SRobert Foley            # we need this to keep the quoted args together for when
1545d676197SRobert Foley            # the QEMU command is issued later.
1555d676197SRobert Foley            args = shlex.split(qemu_args)
1565d676197SRobert Foley            self._config['extra_args'] = []
1575d676197SRobert Foley            for arg in args:
1585d676197SRobert Foley                if arg:
1595d676197SRobert Foley                    # Preserve quotes around arguments.
1605d676197SRobert Foley                    # shlex above takes them out, so add them in.
1615d676197SRobert Foley                    if " " in arg:
1625d676197SRobert Foley                        arg = '"{}"'.format(arg)
1635d676197SRobert Foley                    self._config['extra_args'].append(arg)
1645d676197SRobert Foley
1655d676197SRobert Foley    def validate_ssh_keys(self):
1665d676197SRobert Foley        """Check to see if the ssh key files exist."""
1675d676197SRobert Foley        if 'ssh_key_file' not in self._config or\
1685d676197SRobert Foley           not os.path.exists(self._config['ssh_key_file']):
1695d676197SRobert Foley            raise Exception("ssh key file not found.")
1705d676197SRobert Foley        if 'ssh_pub_key_file' not in self._config or\
1715d676197SRobert Foley           not os.path.exists(self._config['ssh_pub_key_file']):
1725d676197SRobert Foley               raise Exception("ssh pub key file not found.")
1735d676197SRobert Foley
1745d676197SRobert Foley    def wait_boot(self, wait_string=None):
1755d676197SRobert Foley        """Wait for the standard string we expect
1765d676197SRobert Foley           on completion of a normal boot.
1775d676197SRobert Foley           The user can also choose to override with an
1785d676197SRobert Foley           alternate string to wait for."""
1795d676197SRobert Foley        if wait_string is None:
1805d676197SRobert Foley            if self.login_prompt is None:
1815d676197SRobert Foley                raise Exception("self.login_prompt not defined")
1825d676197SRobert Foley            wait_string = self.login_prompt
1835d676197SRobert Foley        # Intentionally bump up the default timeout under TCG,
1845d676197SRobert Foley        # since the console wait below takes longer.
1855d676197SRobert Foley        timeout = self.socket_timeout
1865d676197SRobert Foley        if not kvm_available(self.arch):
1875d676197SRobert Foley            timeout *= 8
1885d676197SRobert Foley        self.console_init(timeout=timeout)
1895d676197SRobert Foley        self.console_wait(wait_string)
1905d676197SRobert Foley
1915b4b4865SAlex Bennée    def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
192ff2ebff0SFam Zheng        def check_sha256sum(fname):
193ff2ebff0SFam Zheng            if not sha256sum:
194ff2ebff0SFam Zheng                return True
195ff2ebff0SFam Zheng            checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
1963ace9be6SGerd Hoffmann            return sha256sum == checksum.decode("utf-8")
197ff2ebff0SFam Zheng
1985b4b4865SAlex Bennée        def check_sha512sum(fname):
1995b4b4865SAlex Bennée            if not sha512sum:
2005b4b4865SAlex Bennée                return True
2015b4b4865SAlex Bennée            checksum = subprocess.check_output(["sha512sum", fname]).split()[0]
2025b4b4865SAlex Bennée            return sha512sum == checksum.decode("utf-8")
2035b4b4865SAlex Bennée
204ff2ebff0SFam Zheng        cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
205ff2ebff0SFam Zheng        if not os.path.exists(cache_dir):
206ff2ebff0SFam Zheng            os.makedirs(cache_dir)
2073ace9be6SGerd Hoffmann        fname = os.path.join(cache_dir,
2083ace9be6SGerd Hoffmann                             hashlib.sha1(url.encode("utf-8")).hexdigest())
2095b4b4865SAlex Bennée        if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname):
210ff2ebff0SFam Zheng            return fname
211ff2ebff0SFam Zheng        logging.debug("Downloading %s to %s...", url, fname)
212ff2ebff0SFam Zheng        subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
213ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
214ff2ebff0SFam Zheng        os.rename(fname + ".download", fname)
215ff2ebff0SFam Zheng        return fname
216ff2ebff0SFam Zheng
217796471e9SGerd Hoffmann    def _ssh_do(self, user, cmd, check):
21889adc5b9SRobert Foley        ssh_cmd = ["ssh",
21989adc5b9SRobert Foley                   "-t",
220ff2ebff0SFam Zheng                   "-o", "StrictHostKeyChecking=no",
221ff2ebff0SFam Zheng                   "-o", "UserKnownHostsFile=" + os.devnull,
2225d676197SRobert Foley                   "-o",
2235d676197SRobert Foley                   "ConnectTimeout={}".format(self._config["ssh_timeout"]),
2245d676197SRobert Foley                   "-p", self.ssh_port, "-i", self._ssh_tmp_key_file]
22589adc5b9SRobert Foley        # If not in debug mode, set ssh to quiet mode to
22689adc5b9SRobert Foley        # avoid printing the results of commands.
22789adc5b9SRobert Foley        if not self.debug:
22889adc5b9SRobert Foley            ssh_cmd.append("-q")
229b08ba163SGerd Hoffmann        for var in self.envvars:
230b08ba163SGerd Hoffmann            ssh_cmd += ['-o', "SendEnv=%s" % var ]
231ff2ebff0SFam Zheng        assert not isinstance(cmd, str)
232ff2ebff0SFam Zheng        ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
233ff2ebff0SFam Zheng        logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
234726c9a3bSFam Zheng        r = subprocess.call(ssh_cmd)
235ff2ebff0SFam Zheng        if check and r != 0:
236ff2ebff0SFam Zheng            raise Exception("SSH command failed: %s" % cmd)
237ff2ebff0SFam Zheng        return r
238ff2ebff0SFam Zheng
239ff2ebff0SFam Zheng    def ssh(self, *cmd):
240df001680SRobert Foley        return self._ssh_do(self._config["guest_user"], cmd, False)
241ff2ebff0SFam Zheng
242ff2ebff0SFam Zheng    def ssh_root(self, *cmd):
243ff2ebff0SFam Zheng        return self._ssh_do("root", cmd, False)
244ff2ebff0SFam Zheng
245ff2ebff0SFam Zheng    def ssh_check(self, *cmd):
246df001680SRobert Foley        self._ssh_do(self._config["guest_user"], cmd, True)
247ff2ebff0SFam Zheng
248ff2ebff0SFam Zheng    def ssh_root_check(self, *cmd):
249ff2ebff0SFam Zheng        self._ssh_do("root", cmd, True)
250ff2ebff0SFam Zheng
251ff2ebff0SFam Zheng    def build_image(self, img):
252ff2ebff0SFam Zheng        raise NotImplementedError
253ff2ebff0SFam Zheng
2541e48931cSWainer dos Santos Moschetta    def exec_qemu_img(self, *args):
2551e48931cSWainer dos Santos Moschetta        cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
2561e48931cSWainer dos Santos Moschetta        cmd.extend(list(args))
2571e48931cSWainer dos Santos Moschetta        subprocess.check_call(cmd)
2581e48931cSWainer dos Santos Moschetta
259ff2ebff0SFam Zheng    def add_source_dir(self, src_dir):
2603ace9be6SGerd Hoffmann        name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
261ff2ebff0SFam Zheng        tarfile = os.path.join(self._tmpdir, name + ".tar")
262ff2ebff0SFam Zheng        logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
263ff2ebff0SFam Zheng        subprocess.check_call(["./scripts/archive-source.sh", tarfile],
264ff2ebff0SFam Zheng                              cwd=src_dir, stdin=self._devnull,
265ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
266ff2ebff0SFam Zheng        self._data_args += ["-drive",
267ff2ebff0SFam Zheng                            "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
268ff2ebff0SFam Zheng                                    (tarfile, name),
269ff2ebff0SFam Zheng                            "-device",
270ff2ebff0SFam Zheng                            "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
271ff2ebff0SFam Zheng
272ff2ebff0SFam Zheng    def boot(self, img, extra_args=[]):
2735d676197SRobert Foley        boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
2745d676197SRobert Foley        boot_params = boot_dev.format(img)
2755d676197SRobert Foley        args = self._args + boot_params.split(' ')
2765d676197SRobert Foley        args += self._data_args + extra_args + self._config['extra_args']
277ff2ebff0SFam Zheng        logging.debug("QEMU args: %s", " ".join(args))
278e56c4504SRobert Foley        qemu_path = get_qemu_path(self.arch, self._build_path)
279*ff14ab0cSRobert Foley
280*ff14ab0cSRobert Foley        # Since console_log_path is only set when the user provides the
281*ff14ab0cSRobert Foley        # log_console option, we will set drain_console=True so the
282*ff14ab0cSRobert Foley        # console is always drained.
283*ff14ab0cSRobert Foley        guest = QEMUMachine(binary=qemu_path, args=args,
284*ff14ab0cSRobert Foley                            console_log=self._console_log_path,
285*ff14ab0cSRobert Foley                            drain_console=True)
2865d676197SRobert Foley        guest.set_machine(self._config['machine'])
2878dd38334SGerd Hoffmann        guest.set_console()
288ff2ebff0SFam Zheng        try:
289ff2ebff0SFam Zheng            guest.launch()
290ff2ebff0SFam Zheng        except:
291ff2ebff0SFam Zheng            logging.error("Failed to launch QEMU, command line:")
292e56c4504SRobert Foley            logging.error(" ".join([qemu_path] + args))
293ff2ebff0SFam Zheng            logging.error("Log:")
294ff2ebff0SFam Zheng            logging.error(guest.get_log())
295ff2ebff0SFam Zheng            logging.error("QEMU version >= 2.10 is required")
296ff2ebff0SFam Zheng            raise
297ff2ebff0SFam Zheng        atexit.register(self.shutdown)
298ff2ebff0SFam Zheng        self._guest = guest
299*ff14ab0cSRobert Foley        # Init console so we can start consuming the chars.
300*ff14ab0cSRobert Foley        self.console_init()
301ff2ebff0SFam Zheng        usernet_info = guest.qmp("human-monitor-command",
302ff2ebff0SFam Zheng                                 command_line="info usernet")
303ff2ebff0SFam Zheng        self.ssh_port = None
304ff2ebff0SFam Zheng        for l in usernet_info["return"].splitlines():
305ff2ebff0SFam Zheng            fields = l.split()
306ff2ebff0SFam Zheng            if "TCP[HOST_FORWARD]" in fields and "22" in fields:
307ff2ebff0SFam Zheng                self.ssh_port = l.split()[3]
308ff2ebff0SFam Zheng        if not self.ssh_port:
309ff2ebff0SFam Zheng            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
310ff2ebff0SFam Zheng                            usernet_info)
311ff2ebff0SFam Zheng
312*ff14ab0cSRobert Foley    def console_init(self, timeout = None):
313*ff14ab0cSRobert Foley        if timeout == None:
314*ff14ab0cSRobert Foley            timeout = self.socket_timeout
3158dd38334SGerd Hoffmann        vm = self._guest
3168dd38334SGerd Hoffmann        vm.console_socket.settimeout(timeout)
317698a64f9SGerd Hoffmann        self.console_raw_path = os.path.join(vm._temp_dir,
318698a64f9SGerd Hoffmann                                             vm._name + "-console.raw")
319698a64f9SGerd Hoffmann        self.console_raw_file = open(self.console_raw_path, 'wb')
3208dd38334SGerd Hoffmann
3218dd38334SGerd Hoffmann    def console_log(self, text):
3228dd38334SGerd Hoffmann        for line in re.split("[\r\n]", text):
3238dd38334SGerd Hoffmann            # filter out terminal escape sequences
3248dd38334SGerd Hoffmann            line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
3258dd38334SGerd Hoffmann            line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
3268dd38334SGerd Hoffmann            # replace unprintable chars
3278dd38334SGerd Hoffmann            line = re.sub("\x1b", "<esc>", line)
3288dd38334SGerd Hoffmann            line = re.sub("[\x00-\x1f]", ".", line)
3298dd38334SGerd Hoffmann            line = re.sub("[\x80-\xff]", ".", line)
3308dd38334SGerd Hoffmann            if line == "":
3318dd38334SGerd Hoffmann                continue
3328dd38334SGerd Hoffmann            # log console line
3338dd38334SGerd Hoffmann            sys.stderr.write("con recv: %s\n" % line)
3348dd38334SGerd Hoffmann
33560136e06SGerd Hoffmann    def console_wait(self, expect, expectalt = None):
3368dd38334SGerd Hoffmann        vm = self._guest
3378dd38334SGerd Hoffmann        output = ""
3388dd38334SGerd Hoffmann        while True:
3398dd38334SGerd Hoffmann            try:
3408dd38334SGerd Hoffmann                chars = vm.console_socket.recv(1)
341698a64f9SGerd Hoffmann                if self.console_raw_file:
342698a64f9SGerd Hoffmann                    self.console_raw_file.write(chars)
343698a64f9SGerd Hoffmann                    self.console_raw_file.flush()
3448dd38334SGerd Hoffmann            except socket.timeout:
3458dd38334SGerd Hoffmann                sys.stderr.write("console: *** read timeout ***\n")
3468dd38334SGerd Hoffmann                sys.stderr.write("console: waiting for: '%s'\n" % expect)
34760136e06SGerd Hoffmann                if not expectalt is None:
34860136e06SGerd Hoffmann                    sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
3498dd38334SGerd Hoffmann                sys.stderr.write("console: line buffer:\n")
3508dd38334SGerd Hoffmann                sys.stderr.write("\n")
3518dd38334SGerd Hoffmann                self.console_log(output.rstrip())
3528dd38334SGerd Hoffmann                sys.stderr.write("\n")
3538dd38334SGerd Hoffmann                raise
3548dd38334SGerd Hoffmann            output += chars.decode("latin1")
3558dd38334SGerd Hoffmann            if expect in output:
3568dd38334SGerd Hoffmann                break
35760136e06SGerd Hoffmann            if not expectalt is None and expectalt in output:
35860136e06SGerd Hoffmann                break
3598dd38334SGerd Hoffmann            if "\r" in output or "\n" in output:
3608dd38334SGerd Hoffmann                lines = re.split("[\r\n]", output)
3618dd38334SGerd Hoffmann                output = lines.pop()
3628dd38334SGerd Hoffmann                if self.debug:
3638dd38334SGerd Hoffmann                    self.console_log("\n".join(lines))
3648dd38334SGerd Hoffmann        if self.debug:
3658dd38334SGerd Hoffmann            self.console_log(output)
36660136e06SGerd Hoffmann        if not expectalt is None and expectalt in output:
36760136e06SGerd Hoffmann            return False
36860136e06SGerd Hoffmann        return True
3698dd38334SGerd Hoffmann
3706c4f0416SGerd Hoffmann    def console_consume(self):
3716c4f0416SGerd Hoffmann        vm = self._guest
3726c4f0416SGerd Hoffmann        output = ""
3736c4f0416SGerd Hoffmann        vm.console_socket.setblocking(0)
3746c4f0416SGerd Hoffmann        while True:
3756c4f0416SGerd Hoffmann            try:
3766c4f0416SGerd Hoffmann                chars = vm.console_socket.recv(1)
3776c4f0416SGerd Hoffmann            except:
3786c4f0416SGerd Hoffmann                break
3796c4f0416SGerd Hoffmann            output += chars.decode("latin1")
3806c4f0416SGerd Hoffmann            if "\r" in output or "\n" in output:
3816c4f0416SGerd Hoffmann                lines = re.split("[\r\n]", output)
3826c4f0416SGerd Hoffmann                output = lines.pop()
3836c4f0416SGerd Hoffmann                if self.debug:
3846c4f0416SGerd Hoffmann                    self.console_log("\n".join(lines))
3856c4f0416SGerd Hoffmann        if self.debug:
3866c4f0416SGerd Hoffmann            self.console_log(output)
3876c4f0416SGerd Hoffmann        vm.console_socket.setblocking(1)
3886c4f0416SGerd Hoffmann
3898dd38334SGerd Hoffmann    def console_send(self, command):
3908dd38334SGerd Hoffmann        vm = self._guest
3918dd38334SGerd Hoffmann        if self.debug:
3928dd38334SGerd Hoffmann            logline = re.sub("\n", "<enter>", command)
3938dd38334SGerd Hoffmann            logline = re.sub("[\x00-\x1f]", ".", logline)
3948dd38334SGerd Hoffmann            sys.stderr.write("con send: %s\n" % logline)
3958dd38334SGerd Hoffmann        for char in list(command):
3968dd38334SGerd Hoffmann            vm.console_socket.send(char.encode("utf-8"))
3978dd38334SGerd Hoffmann            time.sleep(0.01)
3988dd38334SGerd Hoffmann
3998dd38334SGerd Hoffmann    def console_wait_send(self, wait, command):
4008dd38334SGerd Hoffmann        self.console_wait(wait)
4018dd38334SGerd Hoffmann        self.console_send(command)
4028dd38334SGerd Hoffmann
4038dd38334SGerd Hoffmann    def console_ssh_init(self, prompt, user, pw):
4045d676197SRobert Foley        sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
4055d676197SRobert Foley                     % self._config['ssh_pub_key'].rstrip()
4068dd38334SGerd Hoffmann        self.console_wait_send("login:",    "%s\n" % user)
4078dd38334SGerd Hoffmann        self.console_wait_send("Password:", "%s\n" % pw)
4088dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "mkdir .ssh\n")
4098dd38334SGerd Hoffmann        self.console_wait_send(prompt,      sshkey_cmd)
4108dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 755 .ssh\n")
4118dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
4128dd38334SGerd Hoffmann
4138dd38334SGerd Hoffmann    def console_sshd_config(self, prompt):
4148dd38334SGerd Hoffmann        self.console_wait(prompt)
4158dd38334SGerd Hoffmann        self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
4168dd38334SGerd Hoffmann        for var in self.envvars:
4178dd38334SGerd Hoffmann            self.console_wait(prompt)
4188dd38334SGerd Hoffmann            self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
4198dd38334SGerd Hoffmann
4208dd38334SGerd Hoffmann    def print_step(self, text):
4218dd38334SGerd Hoffmann        sys.stderr.write("### %s ...\n" % text)
4228dd38334SGerd Hoffmann
4236ee982c9SRobert Foley    def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
424c9de3935SRobert Foley        # Allow more time for VM to boot under TCG.
425c9de3935SRobert Foley        if not kvm_available(self.arch):
426c9de3935SRobert Foley            seconds *= self.tcg_ssh_timeout_multiplier
427ff2ebff0SFam Zheng        starttime = datetime.datetime.now()
428f5d3d218SPhilippe Mathieu-Daudé        endtime = starttime + datetime.timedelta(seconds=seconds)
4296ee982c9SRobert Foley        cmd_success = False
430f5d3d218SPhilippe Mathieu-Daudé        while datetime.datetime.now() < endtime:
4316ee982c9SRobert Foley            if wait_root and self.ssh_root(cmd) == 0:
4326ee982c9SRobert Foley                cmd_success = True
433fbb3aa29SRobert Foley                break
4346ee982c9SRobert Foley            elif self.ssh(cmd) == 0:
4356ee982c9SRobert Foley                cmd_success = True
436ff2ebff0SFam Zheng                break
437f5d3d218SPhilippe Mathieu-Daudé            seconds = (endtime - datetime.datetime.now()).total_seconds()
438f5d3d218SPhilippe Mathieu-Daudé            logging.debug("%ds before timeout", seconds)
439ff2ebff0SFam Zheng            time.sleep(1)
4406ee982c9SRobert Foley        if not cmd_success:
441ff2ebff0SFam Zheng            raise Exception("Timeout while waiting for guest ssh")
442ff2ebff0SFam Zheng
443ff2ebff0SFam Zheng    def shutdown(self):
444ff2ebff0SFam Zheng        self._guest.shutdown()
445ff2ebff0SFam Zheng
446ff2ebff0SFam Zheng    def wait(self):
447ff2ebff0SFam Zheng        self._guest.wait()
448ff2ebff0SFam Zheng
449b3f94b2fSGerd Hoffmann    def graceful_shutdown(self):
450b3f94b2fSGerd Hoffmann        self.ssh_root(self.poweroff)
451b3f94b2fSGerd Hoffmann        self._guest.wait()
452b3f94b2fSGerd Hoffmann
453ff2ebff0SFam Zheng    def qmp(self, *args, **kwargs):
454ff2ebff0SFam Zheng        return self._guest.qmp(*args, **kwargs)
455ff2ebff0SFam Zheng
456b081986cSRobert Foley    def gen_cloud_init_iso(self):
457b081986cSRobert Foley        cidir = self._tmpdir
458b081986cSRobert Foley        mdata = open(os.path.join(cidir, "meta-data"), "w")
459b081986cSRobert Foley        name = self.name.replace(".","-")
460b081986cSRobert Foley        mdata.writelines(["instance-id: {}-vm-0\n".format(name),
461b081986cSRobert Foley                          "local-hostname: {}-guest\n".format(name)])
462b081986cSRobert Foley        mdata.close()
463b081986cSRobert Foley        udata = open(os.path.join(cidir, "user-data"), "w")
4645d676197SRobert Foley        print("guest user:pw {}:{}".format(self._config['guest_user'],
4655d676197SRobert Foley                                           self._config['guest_pass']))
466b081986cSRobert Foley        udata.writelines(["#cloud-config\n",
467b081986cSRobert Foley                          "chpasswd:\n",
468b081986cSRobert Foley                          "  list: |\n",
4695d676197SRobert Foley                          "    root:%s\n" % self._config['root_pass'],
4705d676197SRobert Foley                          "    %s:%s\n" % (self._config['guest_user'],
4715d676197SRobert Foley                                           self._config['guest_pass']),
472b081986cSRobert Foley                          "  expire: False\n",
473b081986cSRobert Foley                          "users:\n",
4745d676197SRobert Foley                          "  - name: %s\n" % self._config['guest_user'],
475b081986cSRobert Foley                          "    sudo: ALL=(ALL) NOPASSWD:ALL\n",
476b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
4775d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
478b081986cSRobert Foley                          "  - name: root\n",
479b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
4805d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
481b081986cSRobert Foley                          "locale: en_US.UTF-8\n"])
482b081986cSRobert Foley        proxy = os.environ.get("http_proxy")
483b081986cSRobert Foley        if not proxy is None:
484b081986cSRobert Foley            udata.writelines(["apt:\n",
485b081986cSRobert Foley                              "  proxy: %s" % proxy])
486b081986cSRobert Foley        udata.close()
48792fecad3SAlex Bennée        subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
488b081986cSRobert Foley                               "-volid", "cidata", "-joliet", "-rock",
489b081986cSRobert Foley                               "user-data", "meta-data"],
490b081986cSRobert Foley                              cwd=cidir,
491b081986cSRobert Foley                              stdin=self._devnull, stdout=self._stdout,
492b081986cSRobert Foley                              stderr=self._stdout)
493b081986cSRobert Foley        return os.path.join(cidir, "cloud-init.iso")
494b081986cSRobert Foley
495e56c4504SRobert Foleydef get_qemu_path(arch, build_path=None):
496e56c4504SRobert Foley    """Fetch the path to the qemu binary."""
497e56c4504SRobert Foley    # If QEMU environment variable set, it takes precedence
498e56c4504SRobert Foley    if "QEMU" in os.environ:
499e56c4504SRobert Foley        qemu_path = os.environ["QEMU"]
500e56c4504SRobert Foley    elif build_path:
501e56c4504SRobert Foley        qemu_path = os.path.join(build_path, arch + "-softmmu")
502e56c4504SRobert Foley        qemu_path = os.path.join(qemu_path, "qemu-system-" + arch)
503e56c4504SRobert Foley    else:
504e56c4504SRobert Foley        # Default is to use system path for qemu.
505e56c4504SRobert Foley        qemu_path = "qemu-system-" + arch
506e56c4504SRobert Foley    return qemu_path
507e56c4504SRobert Foley
50813336606SRobert Foleydef get_qemu_version(qemu_path):
50913336606SRobert Foley    """Get the version number from the current QEMU,
51013336606SRobert Foley       and return the major number."""
51113336606SRobert Foley    output = subprocess.check_output([qemu_path, '--version'])
51213336606SRobert Foley    version_line = output.decode("utf-8")
51313336606SRobert Foley    version_num = re.split(' |\(', version_line)[3].split('.')[0]
51413336606SRobert Foley    return int(version_num)
51513336606SRobert Foley
5163f1e8137SRobert Foleydef parse_config(config, args):
5173f1e8137SRobert Foley    """ Parse yaml config and populate our config structure.
5183f1e8137SRobert Foley        The yaml config allows the user to override the
5193f1e8137SRobert Foley        defaults for VM parameters.  In many cases these
5203f1e8137SRobert Foley        defaults can be overridden without rebuilding the VM."""
5213f1e8137SRobert Foley    if args.config:
5223f1e8137SRobert Foley        config_file = args.config
5233f1e8137SRobert Foley    elif 'QEMU_CONFIG' in os.environ:
5243f1e8137SRobert Foley        config_file = os.environ['QEMU_CONFIG']
5253f1e8137SRobert Foley    else:
5263f1e8137SRobert Foley        return config
5273f1e8137SRobert Foley    if not os.path.exists(config_file):
5283f1e8137SRobert Foley        raise Exception("config file {} does not exist".format(config_file))
5293f1e8137SRobert Foley    # We gracefully handle importing the yaml module
5303f1e8137SRobert Foley    # since it might not be installed.
5313f1e8137SRobert Foley    # If we are here it means the user supplied a .yml file,
5323f1e8137SRobert Foley    # so if the yaml module is not installed we will exit with error.
5333f1e8137SRobert Foley    try:
5343f1e8137SRobert Foley        import yaml
5353f1e8137SRobert Foley    except ImportError:
5363f1e8137SRobert Foley        print("The python3-yaml package is needed "\
5373f1e8137SRobert Foley              "to support config.yaml files")
5383f1e8137SRobert Foley        # Instead of raising an exception we exit to avoid
5393f1e8137SRobert Foley        # a raft of messy (expected) errors to stdout.
5403f1e8137SRobert Foley        exit(1)
5413f1e8137SRobert Foley    with open(config_file) as f:
5423f1e8137SRobert Foley        yaml_dict = yaml.safe_load(f)
5433f1e8137SRobert Foley
5443f1e8137SRobert Foley    if 'qemu-conf' in yaml_dict:
5453f1e8137SRobert Foley        config.update(yaml_dict['qemu-conf'])
5463f1e8137SRobert Foley    else:
5473f1e8137SRobert Foley        raise Exception("config file {} is not valid"\
5483f1e8137SRobert Foley                        " missing qemu-conf".format(config_file))
5493f1e8137SRobert Foley    return config
5503f1e8137SRobert Foley
55163a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls):
5528a6e007eSPhilippe Mathieu-Daudé
5538a6e007eSPhilippe Mathieu-Daudé    def get_default_jobs():
55463a24c5eSPhilippe Mathieu-Daudé        if kvm_available(vmcls.arch):
5553ad3e36eSWainer dos Santos Moschetta            return multiprocessing.cpu_count() // 2
5568a6e007eSPhilippe Mathieu-Daudé        else:
5578a6e007eSPhilippe Mathieu-Daudé            return 1
5588a6e007eSPhilippe Mathieu-Daudé
559ff2ebff0SFam Zheng    parser = optparse.OptionParser(
560ff2ebff0SFam Zheng        description="VM test utility.  Exit codes: "
561ff2ebff0SFam Zheng                    "0 = success, "
562ff2ebff0SFam Zheng                    "1 = command line error, "
563ff2ebff0SFam Zheng                    "2 = environment initialization failed, "
564ff2ebff0SFam Zheng                    "3 = test command failed")
565ff2ebff0SFam Zheng    parser.add_option("--debug", "-D", action="store_true",
566ff2ebff0SFam Zheng                      help="enable debug output")
56763a24c5eSPhilippe Mathieu-Daudé    parser.add_option("--image", "-i", default="%s.img" % vmcls.name,
568ff2ebff0SFam Zheng                      help="image file name")
569ff2ebff0SFam Zheng    parser.add_option("--force", "-f", action="store_true",
570ff2ebff0SFam Zheng                      help="force build image even if image exists")
5718a6e007eSPhilippe Mathieu-Daudé    parser.add_option("--jobs", type=int, default=get_default_jobs(),
572ff2ebff0SFam Zheng                      help="number of virtual CPUs")
57341e3340aSPeter Maydell    parser.add_option("--verbose", "-V", action="store_true",
57441e3340aSPeter Maydell                      help="Pass V=1 to builds within the guest")
575ff2ebff0SFam Zheng    parser.add_option("--build-image", "-b", action="store_true",
576ff2ebff0SFam Zheng                      help="build image")
577ff2ebff0SFam Zheng    parser.add_option("--build-qemu",
578ff2ebff0SFam Zheng                      help="build QEMU from source in guest")
5795c2ec9b6SAlex Bennée    parser.add_option("--build-target",
5805c2ec9b6SAlex Bennée                      help="QEMU build target", default="check")
581e56c4504SRobert Foley    parser.add_option("--build-path", default=None,
582e56c4504SRobert Foley                      help="Path of build directory, "\
583e56c4504SRobert Foley                           "for using build tree QEMU binary. ")
584ff2ebff0SFam Zheng    parser.add_option("--interactive", "-I", action="store_true",
585ff2ebff0SFam Zheng                      help="Interactively run command")
586983c2a77SFam Zheng    parser.add_option("--snapshot", "-s", action="store_true",
587983c2a77SFam Zheng                      help="run tests with a snapshot")
58892fecad3SAlex Bennée    parser.add_option("--genisoimage", default="genisoimage",
58992fecad3SAlex Bennée                      help="iso imaging tool")
5903f1e8137SRobert Foley    parser.add_option("--config", "-c", default=None,
5913f1e8137SRobert Foley                      help="Provide config yaml for configuration. "\
5923f1e8137SRobert Foley                           "See config_example.yaml for example.")
59313336606SRobert Foley    parser.add_option("--efi-aarch64",
59413336606SRobert Foley                      default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
59513336606SRobert Foley                      help="Path to efi image for aarch64 VMs.")
596*ff14ab0cSRobert Foley    parser.add_option("--log-console", action="store_true",
597*ff14ab0cSRobert Foley                      help="Log console to file.")
598ff2ebff0SFam Zheng    parser.disable_interspersed_args()
599ff2ebff0SFam Zheng    return parser.parse_args()
600ff2ebff0SFam Zheng
6015d676197SRobert Foleydef main(vmcls, config=None):
602ff2ebff0SFam Zheng    try:
6035d676197SRobert Foley        if config == None:
6045d676197SRobert Foley            config = DEFAULT_CONFIG
60563a24c5eSPhilippe Mathieu-Daudé        args, argv = parse_args(vmcls)
606ff2ebff0SFam Zheng        if not argv and not args.build_qemu and not args.build_image:
607f03868bdSEduardo Habkost            print("Nothing to do?")
608ff2ebff0SFam Zheng            return 1
6093f1e8137SRobert Foley        config = parse_config(config, args)
610fb3b4e6dSEduardo Habkost        logging.basicConfig(level=(logging.DEBUG if args.debug
611fb3b4e6dSEduardo Habkost                                   else logging.WARN))
6125d676197SRobert Foley        vm = vmcls(args, config=config)
613ff2ebff0SFam Zheng        if args.build_image:
614ff2ebff0SFam Zheng            if os.path.exists(args.image) and not args.force:
615ff2ebff0SFam Zheng                sys.stderr.writelines(["Image file exists: %s\n" % args.image,
616ff2ebff0SFam Zheng                                      "Use --force option to overwrite\n"])
617ff2ebff0SFam Zheng                return 1
618ff2ebff0SFam Zheng            return vm.build_image(args.image)
619ff2ebff0SFam Zheng        if args.build_qemu:
620ff2ebff0SFam Zheng            vm.add_source_dir(args.build_qemu)
621ff2ebff0SFam Zheng            cmd = [vm.BUILD_SCRIPT.format(
622ff2ebff0SFam Zheng                   configure_opts = " ".join(argv),
6233ace9be6SGerd Hoffmann                   jobs=int(args.jobs),
6245c2ec9b6SAlex Bennée                   target=args.build_target,
62541e3340aSPeter Maydell                   verbose = "V=1" if args.verbose else "")]
626ff2ebff0SFam Zheng        else:
627ff2ebff0SFam Zheng            cmd = argv
628983c2a77SFam Zheng        img = args.image
629983c2a77SFam Zheng        if args.snapshot:
630983c2a77SFam Zheng            img += ",snapshot=on"
631983c2a77SFam Zheng        vm.boot(img)
632ff2ebff0SFam Zheng        vm.wait_ssh()
633ff2ebff0SFam Zheng    except Exception as e:
634ff2ebff0SFam Zheng        if isinstance(e, SystemExit) and e.code == 0:
635ff2ebff0SFam Zheng            return 0
636ff2ebff0SFam Zheng        sys.stderr.write("Failed to prepare guest environment\n")
637ff2ebff0SFam Zheng        traceback.print_exc()
638ff2ebff0SFam Zheng        return 2
639ff2ebff0SFam Zheng
640b3f94b2fSGerd Hoffmann    exitcode = 0
641ff2ebff0SFam Zheng    if vm.ssh(*cmd) != 0:
642b3f94b2fSGerd Hoffmann        exitcode = 3
643bcc388dfSAlex Bennée    if args.interactive:
644b3f94b2fSGerd Hoffmann        vm.ssh()
645b3f94b2fSGerd Hoffmann
646b3f94b2fSGerd Hoffmann    if not args.snapshot:
647b3f94b2fSGerd Hoffmann        vm.graceful_shutdown()
648b3f94b2fSGerd Hoffmann
649b3f94b2fSGerd Hoffmann    return exitcode
650