xref: /openbmc/qemu/tests/vm/basevm.py (revision 13336606a5ef9d6beeb8c0763ac0a9d11c249cac)
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
95*13336606SRobert 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
120ff2ebff0SFam Zheng        self._stderr = sys.stderr
121ff2ebff0SFam Zheng        self._devnull = open(os.devnull, "w")
122ff2ebff0SFam Zheng        if self.debug:
123ff2ebff0SFam Zheng            self._stdout = sys.stdout
124ff2ebff0SFam Zheng        else:
125ff2ebff0SFam Zheng            self._stdout = self._devnull
1265d676197SRobert Foley        netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
127ff2ebff0SFam Zheng        self._args = [ \
1285d676197SRobert Foley            "-nodefaults", "-m", self._config['memory'],
1295d676197SRobert Foley            "-cpu", self._config['cpu'],
1305d676197SRobert Foley            "-netdev",
1315d676197SRobert Foley            netdev.format(self._config['ssh_port']) +
1325d676197SRobert Foley            (",ipv6=no" if not self.ipv6 else "") +
1335d676197SRobert Foley            (",dns=" + self._config['dns'] if self._config['dns'] else ""),
134ff2ebff0SFam Zheng            "-device", "virtio-net-pci,netdev=vnet",
1358dd38334SGerd Hoffmann            "-vnc", "127.0.0.1:0,to=20"]
1361f335d18SRobert Foley        if args.jobs and args.jobs > 1:
1371f335d18SRobert Foley            self._args += ["-smp", "%d" % args.jobs]
13871531bb5SPhilippe Mathieu-Daudé        if kvm_available(self.arch):
139ff2ebff0SFam Zheng            self._args += ["-enable-kvm"]
140ff2ebff0SFam Zheng        else:
141ff2ebff0SFam Zheng            logging.info("KVM not available, not using -enable-kvm")
142ff2ebff0SFam Zheng        self._data_args = []
143ff2ebff0SFam Zheng
1445d676197SRobert Foley        if self._config['qemu_args'] != None:
1455d676197SRobert Foley            qemu_args = self._config['qemu_args']
1465d676197SRobert Foley            qemu_args = qemu_args.replace('\n',' ').replace('\r','')
1475d676197SRobert Foley            # shlex groups quoted arguments together
1485d676197SRobert Foley            # we need this to keep the quoted args together for when
1495d676197SRobert Foley            # the QEMU command is issued later.
1505d676197SRobert Foley            args = shlex.split(qemu_args)
1515d676197SRobert Foley            self._config['extra_args'] = []
1525d676197SRobert Foley            for arg in args:
1535d676197SRobert Foley                if arg:
1545d676197SRobert Foley                    # Preserve quotes around arguments.
1555d676197SRobert Foley                    # shlex above takes them out, so add them in.
1565d676197SRobert Foley                    if " " in arg:
1575d676197SRobert Foley                        arg = '"{}"'.format(arg)
1585d676197SRobert Foley                    self._config['extra_args'].append(arg)
1595d676197SRobert Foley
1605d676197SRobert Foley    def validate_ssh_keys(self):
1615d676197SRobert Foley        """Check to see if the ssh key files exist."""
1625d676197SRobert Foley        if 'ssh_key_file' not in self._config or\
1635d676197SRobert Foley           not os.path.exists(self._config['ssh_key_file']):
1645d676197SRobert Foley            raise Exception("ssh key file not found.")
1655d676197SRobert Foley        if 'ssh_pub_key_file' not in self._config or\
1665d676197SRobert Foley           not os.path.exists(self._config['ssh_pub_key_file']):
1675d676197SRobert Foley               raise Exception("ssh pub key file not found.")
1685d676197SRobert Foley
1695d676197SRobert Foley    def wait_boot(self, wait_string=None):
1705d676197SRobert Foley        """Wait for the standard string we expect
1715d676197SRobert Foley           on completion of a normal boot.
1725d676197SRobert Foley           The user can also choose to override with an
1735d676197SRobert Foley           alternate string to wait for."""
1745d676197SRobert Foley        if wait_string is None:
1755d676197SRobert Foley            if self.login_prompt is None:
1765d676197SRobert Foley                raise Exception("self.login_prompt not defined")
1775d676197SRobert Foley            wait_string = self.login_prompt
1785d676197SRobert Foley        # Intentionally bump up the default timeout under TCG,
1795d676197SRobert Foley        # since the console wait below takes longer.
1805d676197SRobert Foley        timeout = self.socket_timeout
1815d676197SRobert Foley        if not kvm_available(self.arch):
1825d676197SRobert Foley            timeout *= 8
1835d676197SRobert Foley        self.console_init(timeout=timeout)
1845d676197SRobert Foley        self.console_wait(wait_string)
1855d676197SRobert Foley
1865d676197SRobert Foley    def __getattr__(self, name):
1875d676197SRobert Foley        # Support direct access to config by key.
1885d676197SRobert Foley        # for example, access self._config['cpu'] by self.cpu
1895d676197SRobert Foley        if name.lower() in self._config.keys():
1905d676197SRobert Foley            return self._config[name.lower()]
1915d676197SRobert Foley        return object.__getattribute__(self, name)
1925d676197SRobert Foley
1935b4b4865SAlex Bennée    def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
194ff2ebff0SFam Zheng        def check_sha256sum(fname):
195ff2ebff0SFam Zheng            if not sha256sum:
196ff2ebff0SFam Zheng                return True
197ff2ebff0SFam Zheng            checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
1983ace9be6SGerd Hoffmann            return sha256sum == checksum.decode("utf-8")
199ff2ebff0SFam Zheng
2005b4b4865SAlex Bennée        def check_sha512sum(fname):
2015b4b4865SAlex Bennée            if not sha512sum:
2025b4b4865SAlex Bennée                return True
2035b4b4865SAlex Bennée            checksum = subprocess.check_output(["sha512sum", fname]).split()[0]
2045b4b4865SAlex Bennée            return sha512sum == checksum.decode("utf-8")
2055b4b4865SAlex Bennée
206ff2ebff0SFam Zheng        cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
207ff2ebff0SFam Zheng        if not os.path.exists(cache_dir):
208ff2ebff0SFam Zheng            os.makedirs(cache_dir)
2093ace9be6SGerd Hoffmann        fname = os.path.join(cache_dir,
2103ace9be6SGerd Hoffmann                             hashlib.sha1(url.encode("utf-8")).hexdigest())
2115b4b4865SAlex Bennée        if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname):
212ff2ebff0SFam Zheng            return fname
213ff2ebff0SFam Zheng        logging.debug("Downloading %s to %s...", url, fname)
214ff2ebff0SFam Zheng        subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
215ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
216ff2ebff0SFam Zheng        os.rename(fname + ".download", fname)
217ff2ebff0SFam Zheng        return fname
218ff2ebff0SFam Zheng
219796471e9SGerd Hoffmann    def _ssh_do(self, user, cmd, check):
22089adc5b9SRobert Foley        ssh_cmd = ["ssh",
22189adc5b9SRobert Foley                   "-t",
222ff2ebff0SFam Zheng                   "-o", "StrictHostKeyChecking=no",
223ff2ebff0SFam Zheng                   "-o", "UserKnownHostsFile=" + os.devnull,
2245d676197SRobert Foley                   "-o",
2255d676197SRobert Foley                   "ConnectTimeout={}".format(self._config["ssh_timeout"]),
2265d676197SRobert Foley                   "-p", self.ssh_port, "-i", self._ssh_tmp_key_file]
22789adc5b9SRobert Foley        # If not in debug mode, set ssh to quiet mode to
22889adc5b9SRobert Foley        # avoid printing the results of commands.
22989adc5b9SRobert Foley        if not self.debug:
23089adc5b9SRobert Foley            ssh_cmd.append("-q")
231b08ba163SGerd Hoffmann        for var in self.envvars:
232b08ba163SGerd Hoffmann            ssh_cmd += ['-o', "SendEnv=%s" % var ]
233ff2ebff0SFam Zheng        assert not isinstance(cmd, str)
234ff2ebff0SFam Zheng        ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
235ff2ebff0SFam Zheng        logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
236726c9a3bSFam Zheng        r = subprocess.call(ssh_cmd)
237ff2ebff0SFam Zheng        if check and r != 0:
238ff2ebff0SFam Zheng            raise Exception("SSH command failed: %s" % cmd)
239ff2ebff0SFam Zheng        return r
240ff2ebff0SFam Zheng
241ff2ebff0SFam Zheng    def ssh(self, *cmd):
242ff2ebff0SFam Zheng        return self._ssh_do(self.GUEST_USER, cmd, False)
243ff2ebff0SFam Zheng
244ff2ebff0SFam Zheng    def ssh_root(self, *cmd):
245ff2ebff0SFam Zheng        return self._ssh_do("root", cmd, False)
246ff2ebff0SFam Zheng
247ff2ebff0SFam Zheng    def ssh_check(self, *cmd):
248ff2ebff0SFam Zheng        self._ssh_do(self.GUEST_USER, cmd, True)
249ff2ebff0SFam Zheng
250ff2ebff0SFam Zheng    def ssh_root_check(self, *cmd):
251ff2ebff0SFam Zheng        self._ssh_do("root", cmd, True)
252ff2ebff0SFam Zheng
253ff2ebff0SFam Zheng    def build_image(self, img):
254ff2ebff0SFam Zheng        raise NotImplementedError
255ff2ebff0SFam Zheng
2561e48931cSWainer dos Santos Moschetta    def exec_qemu_img(self, *args):
2571e48931cSWainer dos Santos Moschetta        cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
2581e48931cSWainer dos Santos Moschetta        cmd.extend(list(args))
2591e48931cSWainer dos Santos Moschetta        subprocess.check_call(cmd)
2601e48931cSWainer dos Santos Moschetta
261ff2ebff0SFam Zheng    def add_source_dir(self, src_dir):
2623ace9be6SGerd Hoffmann        name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
263ff2ebff0SFam Zheng        tarfile = os.path.join(self._tmpdir, name + ".tar")
264ff2ebff0SFam Zheng        logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
265ff2ebff0SFam Zheng        subprocess.check_call(["./scripts/archive-source.sh", tarfile],
266ff2ebff0SFam Zheng                              cwd=src_dir, stdin=self._devnull,
267ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
268ff2ebff0SFam Zheng        self._data_args += ["-drive",
269ff2ebff0SFam Zheng                            "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
270ff2ebff0SFam Zheng                                    (tarfile, name),
271ff2ebff0SFam Zheng                            "-device",
272ff2ebff0SFam Zheng                            "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
273ff2ebff0SFam Zheng
274ff2ebff0SFam Zheng    def boot(self, img, extra_args=[]):
2755d676197SRobert Foley        boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
2765d676197SRobert Foley        boot_params = boot_dev.format(img)
2775d676197SRobert Foley        args = self._args + boot_params.split(' ')
2785d676197SRobert Foley        args += self._data_args + extra_args + self._config['extra_args']
279ff2ebff0SFam Zheng        logging.debug("QEMU args: %s", " ".join(args))
280e56c4504SRobert Foley        qemu_path = get_qemu_path(self.arch, self._build_path)
281e56c4504SRobert Foley        guest = QEMUMachine(binary=qemu_path, args=args)
2825d676197SRobert Foley        guest.set_machine(self._config['machine'])
2838dd38334SGerd Hoffmann        guest.set_console()
284ff2ebff0SFam Zheng        try:
285ff2ebff0SFam Zheng            guest.launch()
286ff2ebff0SFam Zheng        except:
287ff2ebff0SFam Zheng            logging.error("Failed to launch QEMU, command line:")
288e56c4504SRobert Foley            logging.error(" ".join([qemu_path] + args))
289ff2ebff0SFam Zheng            logging.error("Log:")
290ff2ebff0SFam Zheng            logging.error(guest.get_log())
291ff2ebff0SFam Zheng            logging.error("QEMU version >= 2.10 is required")
292ff2ebff0SFam Zheng            raise
293ff2ebff0SFam Zheng        atexit.register(self.shutdown)
294ff2ebff0SFam Zheng        self._guest = guest
295ff2ebff0SFam Zheng        usernet_info = guest.qmp("human-monitor-command",
296ff2ebff0SFam Zheng                                 command_line="info usernet")
297ff2ebff0SFam Zheng        self.ssh_port = None
298ff2ebff0SFam Zheng        for l in usernet_info["return"].splitlines():
299ff2ebff0SFam Zheng            fields = l.split()
300ff2ebff0SFam Zheng            if "TCP[HOST_FORWARD]" in fields and "22" in fields:
301ff2ebff0SFam Zheng                self.ssh_port = l.split()[3]
302ff2ebff0SFam Zheng        if not self.ssh_port:
303ff2ebff0SFam Zheng            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
304ff2ebff0SFam Zheng                            usernet_info)
305ff2ebff0SFam Zheng
3068dd38334SGerd Hoffmann    def console_init(self, timeout = 120):
3078dd38334SGerd Hoffmann        vm = self._guest
3088dd38334SGerd Hoffmann        vm.console_socket.settimeout(timeout)
309698a64f9SGerd Hoffmann        self.console_raw_path = os.path.join(vm._temp_dir,
310698a64f9SGerd Hoffmann                                             vm._name + "-console.raw")
311698a64f9SGerd Hoffmann        self.console_raw_file = open(self.console_raw_path, 'wb')
3128dd38334SGerd Hoffmann
3138dd38334SGerd Hoffmann    def console_log(self, text):
3148dd38334SGerd Hoffmann        for line in re.split("[\r\n]", text):
3158dd38334SGerd Hoffmann            # filter out terminal escape sequences
3168dd38334SGerd Hoffmann            line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
3178dd38334SGerd Hoffmann            line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
3188dd38334SGerd Hoffmann            # replace unprintable chars
3198dd38334SGerd Hoffmann            line = re.sub("\x1b", "<esc>", line)
3208dd38334SGerd Hoffmann            line = re.sub("[\x00-\x1f]", ".", line)
3218dd38334SGerd Hoffmann            line = re.sub("[\x80-\xff]", ".", line)
3228dd38334SGerd Hoffmann            if line == "":
3238dd38334SGerd Hoffmann                continue
3248dd38334SGerd Hoffmann            # log console line
3258dd38334SGerd Hoffmann            sys.stderr.write("con recv: %s\n" % line)
3268dd38334SGerd Hoffmann
32760136e06SGerd Hoffmann    def console_wait(self, expect, expectalt = None):
3288dd38334SGerd Hoffmann        vm = self._guest
3298dd38334SGerd Hoffmann        output = ""
3308dd38334SGerd Hoffmann        while True:
3318dd38334SGerd Hoffmann            try:
3328dd38334SGerd Hoffmann                chars = vm.console_socket.recv(1)
333698a64f9SGerd Hoffmann                if self.console_raw_file:
334698a64f9SGerd Hoffmann                    self.console_raw_file.write(chars)
335698a64f9SGerd Hoffmann                    self.console_raw_file.flush()
3368dd38334SGerd Hoffmann            except socket.timeout:
3378dd38334SGerd Hoffmann                sys.stderr.write("console: *** read timeout ***\n")
3388dd38334SGerd Hoffmann                sys.stderr.write("console: waiting for: '%s'\n" % expect)
33960136e06SGerd Hoffmann                if not expectalt is None:
34060136e06SGerd Hoffmann                    sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
3418dd38334SGerd Hoffmann                sys.stderr.write("console: line buffer:\n")
3428dd38334SGerd Hoffmann                sys.stderr.write("\n")
3438dd38334SGerd Hoffmann                self.console_log(output.rstrip())
3448dd38334SGerd Hoffmann                sys.stderr.write("\n")
3458dd38334SGerd Hoffmann                raise
3468dd38334SGerd Hoffmann            output += chars.decode("latin1")
3478dd38334SGerd Hoffmann            if expect in output:
3488dd38334SGerd Hoffmann                break
34960136e06SGerd Hoffmann            if not expectalt is None and expectalt in output:
35060136e06SGerd Hoffmann                break
3518dd38334SGerd Hoffmann            if "\r" in output or "\n" in output:
3528dd38334SGerd Hoffmann                lines = re.split("[\r\n]", output)
3538dd38334SGerd Hoffmann                output = lines.pop()
3548dd38334SGerd Hoffmann                if self.debug:
3558dd38334SGerd Hoffmann                    self.console_log("\n".join(lines))
3568dd38334SGerd Hoffmann        if self.debug:
3578dd38334SGerd Hoffmann            self.console_log(output)
35860136e06SGerd Hoffmann        if not expectalt is None and expectalt in output:
35960136e06SGerd Hoffmann            return False
36060136e06SGerd Hoffmann        return True
3618dd38334SGerd Hoffmann
3626c4f0416SGerd Hoffmann    def console_consume(self):
3636c4f0416SGerd Hoffmann        vm = self._guest
3646c4f0416SGerd Hoffmann        output = ""
3656c4f0416SGerd Hoffmann        vm.console_socket.setblocking(0)
3666c4f0416SGerd Hoffmann        while True:
3676c4f0416SGerd Hoffmann            try:
3686c4f0416SGerd Hoffmann                chars = vm.console_socket.recv(1)
3696c4f0416SGerd Hoffmann            except:
3706c4f0416SGerd Hoffmann                break
3716c4f0416SGerd Hoffmann            output += chars.decode("latin1")
3726c4f0416SGerd Hoffmann            if "\r" in output or "\n" in output:
3736c4f0416SGerd Hoffmann                lines = re.split("[\r\n]", output)
3746c4f0416SGerd Hoffmann                output = lines.pop()
3756c4f0416SGerd Hoffmann                if self.debug:
3766c4f0416SGerd Hoffmann                    self.console_log("\n".join(lines))
3776c4f0416SGerd Hoffmann        if self.debug:
3786c4f0416SGerd Hoffmann            self.console_log(output)
3796c4f0416SGerd Hoffmann        vm.console_socket.setblocking(1)
3806c4f0416SGerd Hoffmann
3818dd38334SGerd Hoffmann    def console_send(self, command):
3828dd38334SGerd Hoffmann        vm = self._guest
3838dd38334SGerd Hoffmann        if self.debug:
3848dd38334SGerd Hoffmann            logline = re.sub("\n", "<enter>", command)
3858dd38334SGerd Hoffmann            logline = re.sub("[\x00-\x1f]", ".", logline)
3868dd38334SGerd Hoffmann            sys.stderr.write("con send: %s\n" % logline)
3878dd38334SGerd Hoffmann        for char in list(command):
3888dd38334SGerd Hoffmann            vm.console_socket.send(char.encode("utf-8"))
3898dd38334SGerd Hoffmann            time.sleep(0.01)
3908dd38334SGerd Hoffmann
3918dd38334SGerd Hoffmann    def console_wait_send(self, wait, command):
3928dd38334SGerd Hoffmann        self.console_wait(wait)
3938dd38334SGerd Hoffmann        self.console_send(command)
3948dd38334SGerd Hoffmann
3958dd38334SGerd Hoffmann    def console_ssh_init(self, prompt, user, pw):
3965d676197SRobert Foley        sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
3975d676197SRobert Foley                     % self._config['ssh_pub_key'].rstrip()
3988dd38334SGerd Hoffmann        self.console_wait_send("login:",    "%s\n" % user)
3998dd38334SGerd Hoffmann        self.console_wait_send("Password:", "%s\n" % pw)
4008dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "mkdir .ssh\n")
4018dd38334SGerd Hoffmann        self.console_wait_send(prompt,      sshkey_cmd)
4028dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 755 .ssh\n")
4038dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
4048dd38334SGerd Hoffmann
4058dd38334SGerd Hoffmann    def console_sshd_config(self, prompt):
4068dd38334SGerd Hoffmann        self.console_wait(prompt)
4078dd38334SGerd Hoffmann        self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
4088dd38334SGerd Hoffmann        for var in self.envvars:
4098dd38334SGerd Hoffmann            self.console_wait(prompt)
4108dd38334SGerd Hoffmann            self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
4118dd38334SGerd Hoffmann
4128dd38334SGerd Hoffmann    def print_step(self, text):
4138dd38334SGerd Hoffmann        sys.stderr.write("### %s ...\n" % text)
4148dd38334SGerd Hoffmann
4156ee982c9SRobert Foley    def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
416c9de3935SRobert Foley        # Allow more time for VM to boot under TCG.
417c9de3935SRobert Foley        if not kvm_available(self.arch):
418c9de3935SRobert Foley            seconds *= self.tcg_ssh_timeout_multiplier
419ff2ebff0SFam Zheng        starttime = datetime.datetime.now()
420f5d3d218SPhilippe Mathieu-Daudé        endtime = starttime + datetime.timedelta(seconds=seconds)
4216ee982c9SRobert Foley        cmd_success = False
422f5d3d218SPhilippe Mathieu-Daudé        while datetime.datetime.now() < endtime:
4236ee982c9SRobert Foley            if wait_root and self.ssh_root(cmd) == 0:
4246ee982c9SRobert Foley                cmd_success = True
425fbb3aa29SRobert Foley                break
4266ee982c9SRobert Foley            elif self.ssh(cmd) == 0:
4276ee982c9SRobert Foley                cmd_success = True
428ff2ebff0SFam Zheng                break
429f5d3d218SPhilippe Mathieu-Daudé            seconds = (endtime - datetime.datetime.now()).total_seconds()
430f5d3d218SPhilippe Mathieu-Daudé            logging.debug("%ds before timeout", seconds)
431ff2ebff0SFam Zheng            time.sleep(1)
4326ee982c9SRobert Foley        if not cmd_success:
433ff2ebff0SFam Zheng            raise Exception("Timeout while waiting for guest ssh")
434ff2ebff0SFam Zheng
435ff2ebff0SFam Zheng    def shutdown(self):
436ff2ebff0SFam Zheng        self._guest.shutdown()
437ff2ebff0SFam Zheng
438ff2ebff0SFam Zheng    def wait(self):
439ff2ebff0SFam Zheng        self._guest.wait()
440ff2ebff0SFam Zheng
441b3f94b2fSGerd Hoffmann    def graceful_shutdown(self):
442b3f94b2fSGerd Hoffmann        self.ssh_root(self.poweroff)
443b3f94b2fSGerd Hoffmann        self._guest.wait()
444b3f94b2fSGerd Hoffmann
445ff2ebff0SFam Zheng    def qmp(self, *args, **kwargs):
446ff2ebff0SFam Zheng        return self._guest.qmp(*args, **kwargs)
447ff2ebff0SFam Zheng
448b081986cSRobert Foley    def gen_cloud_init_iso(self):
449b081986cSRobert Foley        cidir = self._tmpdir
450b081986cSRobert Foley        mdata = open(os.path.join(cidir, "meta-data"), "w")
451b081986cSRobert Foley        name = self.name.replace(".","-")
452b081986cSRobert Foley        mdata.writelines(["instance-id: {}-vm-0\n".format(name),
453b081986cSRobert Foley                          "local-hostname: {}-guest\n".format(name)])
454b081986cSRobert Foley        mdata.close()
455b081986cSRobert Foley        udata = open(os.path.join(cidir, "user-data"), "w")
4565d676197SRobert Foley        print("guest user:pw {}:{}".format(self._config['guest_user'],
4575d676197SRobert Foley                                           self._config['guest_pass']))
458b081986cSRobert Foley        udata.writelines(["#cloud-config\n",
459b081986cSRobert Foley                          "chpasswd:\n",
460b081986cSRobert Foley                          "  list: |\n",
4615d676197SRobert Foley                          "    root:%s\n" % self._config['root_pass'],
4625d676197SRobert Foley                          "    %s:%s\n" % (self._config['guest_user'],
4635d676197SRobert Foley                                           self._config['guest_pass']),
464b081986cSRobert Foley                          "  expire: False\n",
465b081986cSRobert Foley                          "users:\n",
4665d676197SRobert Foley                          "  - name: %s\n" % self._config['guest_user'],
467b081986cSRobert Foley                          "    sudo: ALL=(ALL) NOPASSWD:ALL\n",
468b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
4695d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
470b081986cSRobert Foley                          "  - name: root\n",
471b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
4725d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
473b081986cSRobert Foley                          "locale: en_US.UTF-8\n"])
474b081986cSRobert Foley        proxy = os.environ.get("http_proxy")
475b081986cSRobert Foley        if not proxy is None:
476b081986cSRobert Foley            udata.writelines(["apt:\n",
477b081986cSRobert Foley                              "  proxy: %s" % proxy])
478b081986cSRobert Foley        udata.close()
47992fecad3SAlex Bennée        subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
480b081986cSRobert Foley                               "-volid", "cidata", "-joliet", "-rock",
481b081986cSRobert Foley                               "user-data", "meta-data"],
482b081986cSRobert Foley                              cwd=cidir,
483b081986cSRobert Foley                              stdin=self._devnull, stdout=self._stdout,
484b081986cSRobert Foley                              stderr=self._stdout)
485b081986cSRobert Foley        return os.path.join(cidir, "cloud-init.iso")
486b081986cSRobert Foley
487e56c4504SRobert Foleydef get_qemu_path(arch, build_path=None):
488e56c4504SRobert Foley    """Fetch the path to the qemu binary."""
489e56c4504SRobert Foley    # If QEMU environment variable set, it takes precedence
490e56c4504SRobert Foley    if "QEMU" in os.environ:
491e56c4504SRobert Foley        qemu_path = os.environ["QEMU"]
492e56c4504SRobert Foley    elif build_path:
493e56c4504SRobert Foley        qemu_path = os.path.join(build_path, arch + "-softmmu")
494e56c4504SRobert Foley        qemu_path = os.path.join(qemu_path, "qemu-system-" + arch)
495e56c4504SRobert Foley    else:
496e56c4504SRobert Foley        # Default is to use system path for qemu.
497e56c4504SRobert Foley        qemu_path = "qemu-system-" + arch
498e56c4504SRobert Foley    return qemu_path
499e56c4504SRobert Foley
500*13336606SRobert Foleydef get_qemu_version(qemu_path):
501*13336606SRobert Foley    """Get the version number from the current QEMU,
502*13336606SRobert Foley       and return the major number."""
503*13336606SRobert Foley    output = subprocess.check_output([qemu_path, '--version'])
504*13336606SRobert Foley    version_line = output.decode("utf-8")
505*13336606SRobert Foley    version_num = re.split(' |\(', version_line)[3].split('.')[0]
506*13336606SRobert Foley    return int(version_num)
507*13336606SRobert Foley
5083f1e8137SRobert Foleydef parse_config(config, args):
5093f1e8137SRobert Foley    """ Parse yaml config and populate our config structure.
5103f1e8137SRobert Foley        The yaml config allows the user to override the
5113f1e8137SRobert Foley        defaults for VM parameters.  In many cases these
5123f1e8137SRobert Foley        defaults can be overridden without rebuilding the VM."""
5133f1e8137SRobert Foley    if args.config:
5143f1e8137SRobert Foley        config_file = args.config
5153f1e8137SRobert Foley    elif 'QEMU_CONFIG' in os.environ:
5163f1e8137SRobert Foley        config_file = os.environ['QEMU_CONFIG']
5173f1e8137SRobert Foley    else:
5183f1e8137SRobert Foley        return config
5193f1e8137SRobert Foley    if not os.path.exists(config_file):
5203f1e8137SRobert Foley        raise Exception("config file {} does not exist".format(config_file))
5213f1e8137SRobert Foley    # We gracefully handle importing the yaml module
5223f1e8137SRobert Foley    # since it might not be installed.
5233f1e8137SRobert Foley    # If we are here it means the user supplied a .yml file,
5243f1e8137SRobert Foley    # so if the yaml module is not installed we will exit with error.
5253f1e8137SRobert Foley    try:
5263f1e8137SRobert Foley        import yaml
5273f1e8137SRobert Foley    except ImportError:
5283f1e8137SRobert Foley        print("The python3-yaml package is needed "\
5293f1e8137SRobert Foley              "to support config.yaml files")
5303f1e8137SRobert Foley        # Instead of raising an exception we exit to avoid
5313f1e8137SRobert Foley        # a raft of messy (expected) errors to stdout.
5323f1e8137SRobert Foley        exit(1)
5333f1e8137SRobert Foley    with open(config_file) as f:
5343f1e8137SRobert Foley        yaml_dict = yaml.safe_load(f)
5353f1e8137SRobert Foley
5363f1e8137SRobert Foley    if 'qemu-conf' in yaml_dict:
5373f1e8137SRobert Foley        config.update(yaml_dict['qemu-conf'])
5383f1e8137SRobert Foley    else:
5393f1e8137SRobert Foley        raise Exception("config file {} is not valid"\
5403f1e8137SRobert Foley                        " missing qemu-conf".format(config_file))
5413f1e8137SRobert Foley    return config
5423f1e8137SRobert Foley
54363a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls):
5448a6e007eSPhilippe Mathieu-Daudé
5458a6e007eSPhilippe Mathieu-Daudé    def get_default_jobs():
54663a24c5eSPhilippe Mathieu-Daudé        if kvm_available(vmcls.arch):
5473ad3e36eSWainer dos Santos Moschetta            return multiprocessing.cpu_count() // 2
5488a6e007eSPhilippe Mathieu-Daudé        else:
5498a6e007eSPhilippe Mathieu-Daudé            return 1
5508a6e007eSPhilippe Mathieu-Daudé
551ff2ebff0SFam Zheng    parser = optparse.OptionParser(
552ff2ebff0SFam Zheng        description="VM test utility.  Exit codes: "
553ff2ebff0SFam Zheng                    "0 = success, "
554ff2ebff0SFam Zheng                    "1 = command line error, "
555ff2ebff0SFam Zheng                    "2 = environment initialization failed, "
556ff2ebff0SFam Zheng                    "3 = test command failed")
557ff2ebff0SFam Zheng    parser.add_option("--debug", "-D", action="store_true",
558ff2ebff0SFam Zheng                      help="enable debug output")
55963a24c5eSPhilippe Mathieu-Daudé    parser.add_option("--image", "-i", default="%s.img" % vmcls.name,
560ff2ebff0SFam Zheng                      help="image file name")
561ff2ebff0SFam Zheng    parser.add_option("--force", "-f", action="store_true",
562ff2ebff0SFam Zheng                      help="force build image even if image exists")
5638a6e007eSPhilippe Mathieu-Daudé    parser.add_option("--jobs", type=int, default=get_default_jobs(),
564ff2ebff0SFam Zheng                      help="number of virtual CPUs")
56541e3340aSPeter Maydell    parser.add_option("--verbose", "-V", action="store_true",
56641e3340aSPeter Maydell                      help="Pass V=1 to builds within the guest")
567ff2ebff0SFam Zheng    parser.add_option("--build-image", "-b", action="store_true",
568ff2ebff0SFam Zheng                      help="build image")
569ff2ebff0SFam Zheng    parser.add_option("--build-qemu",
570ff2ebff0SFam Zheng                      help="build QEMU from source in guest")
5715c2ec9b6SAlex Bennée    parser.add_option("--build-target",
5725c2ec9b6SAlex Bennée                      help="QEMU build target", default="check")
573e56c4504SRobert Foley    parser.add_option("--build-path", default=None,
574e56c4504SRobert Foley                      help="Path of build directory, "\
575e56c4504SRobert Foley                           "for using build tree QEMU binary. ")
576ff2ebff0SFam Zheng    parser.add_option("--interactive", "-I", action="store_true",
577ff2ebff0SFam Zheng                      help="Interactively run command")
578983c2a77SFam Zheng    parser.add_option("--snapshot", "-s", action="store_true",
579983c2a77SFam Zheng                      help="run tests with a snapshot")
58092fecad3SAlex Bennée    parser.add_option("--genisoimage", default="genisoimage",
58192fecad3SAlex Bennée                      help="iso imaging tool")
5823f1e8137SRobert Foley    parser.add_option("--config", "-c", default=None,
5833f1e8137SRobert Foley                      help="Provide config yaml for configuration. "\
5843f1e8137SRobert Foley                           "See config_example.yaml for example.")
585*13336606SRobert Foley    parser.add_option("--efi-aarch64",
586*13336606SRobert Foley                      default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
587*13336606SRobert Foley                      help="Path to efi image for aarch64 VMs.")
588ff2ebff0SFam Zheng    parser.disable_interspersed_args()
589ff2ebff0SFam Zheng    return parser.parse_args()
590ff2ebff0SFam Zheng
5915d676197SRobert Foleydef main(vmcls, config=None):
592ff2ebff0SFam Zheng    try:
5935d676197SRobert Foley        if config == None:
5945d676197SRobert Foley            config = DEFAULT_CONFIG
59563a24c5eSPhilippe Mathieu-Daudé        args, argv = parse_args(vmcls)
596ff2ebff0SFam Zheng        if not argv and not args.build_qemu and not args.build_image:
597f03868bdSEduardo Habkost            print("Nothing to do?")
598ff2ebff0SFam Zheng            return 1
5993f1e8137SRobert Foley        config = parse_config(config, args)
600fb3b4e6dSEduardo Habkost        logging.basicConfig(level=(logging.DEBUG if args.debug
601fb3b4e6dSEduardo Habkost                                   else logging.WARN))
6025d676197SRobert Foley        vm = vmcls(args, config=config)
603ff2ebff0SFam Zheng        if args.build_image:
604ff2ebff0SFam Zheng            if os.path.exists(args.image) and not args.force:
605ff2ebff0SFam Zheng                sys.stderr.writelines(["Image file exists: %s\n" % args.image,
606ff2ebff0SFam Zheng                                      "Use --force option to overwrite\n"])
607ff2ebff0SFam Zheng                return 1
608ff2ebff0SFam Zheng            return vm.build_image(args.image)
609ff2ebff0SFam Zheng        if args.build_qemu:
610ff2ebff0SFam Zheng            vm.add_source_dir(args.build_qemu)
611ff2ebff0SFam Zheng            cmd = [vm.BUILD_SCRIPT.format(
612ff2ebff0SFam Zheng                   configure_opts = " ".join(argv),
6133ace9be6SGerd Hoffmann                   jobs=int(args.jobs),
6145c2ec9b6SAlex Bennée                   target=args.build_target,
61541e3340aSPeter Maydell                   verbose = "V=1" if args.verbose else "")]
616ff2ebff0SFam Zheng        else:
617ff2ebff0SFam Zheng            cmd = argv
618983c2a77SFam Zheng        img = args.image
619983c2a77SFam Zheng        if args.snapshot:
620983c2a77SFam Zheng            img += ",snapshot=on"
621983c2a77SFam Zheng        vm.boot(img)
622ff2ebff0SFam Zheng        vm.wait_ssh()
623ff2ebff0SFam Zheng    except Exception as e:
624ff2ebff0SFam Zheng        if isinstance(e, SystemExit) and e.code == 0:
625ff2ebff0SFam Zheng            return 0
626ff2ebff0SFam Zheng        sys.stderr.write("Failed to prepare guest environment\n")
627ff2ebff0SFam Zheng        traceback.print_exc()
628ff2ebff0SFam Zheng        return 2
629ff2ebff0SFam Zheng
630b3f94b2fSGerd Hoffmann    exitcode = 0
631ff2ebff0SFam Zheng    if vm.ssh(*cmd) != 0:
632b3f94b2fSGerd Hoffmann        exitcode = 3
633bcc388dfSAlex Bennée    if args.interactive:
634b3f94b2fSGerd Hoffmann        vm.ssh()
635b3f94b2fSGerd Hoffmann
636b3f94b2fSGerd Hoffmann    if not args.snapshot:
637b3f94b2fSGerd Hoffmann        vm.graceful_shutdown()
638b3f94b2fSGerd Hoffmann
639b3f94b2fSGerd Hoffmann    return exitcode
640