xref: /openbmc/qemu/tests/vm/basevm.py (revision 7bb17a9263665c7cb0b93e6889e66bb62b9f71c1)
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'))
22abf0bf99SJohn Snowfrom qemu.machine import QEMUMachine
23beb6b57bSJohn Snowfrom qemu.utils import get_info_usernet_hostfwd_port, kvm_available
24ff2ebff0SFam Zhengimport subprocess
25ff2ebff0SFam Zhengimport hashlib
262fea3a12SAlex Bennéeimport argparse
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",
479fc33bf4SAlexander von Gluck IV    'root_user'       : "root",
485d676197SRobert Foley    'root_pass'       : "qemupass",
495d676197SRobert Foley    'ssh_key_file'    : SSH_KEY_FILE,
505d676197SRobert Foley    'ssh_pub_key_file': SSH_PUB_KEY_FILE,
515d676197SRobert Foley    'memory'          : "4G",
525d676197SRobert Foley    'extra_args'      : [],
535d676197SRobert Foley    'qemu_args'       : "",
545d676197SRobert Foley    'dns'             : "",
555d676197SRobert Foley    'ssh_port'        : 0,
565d676197SRobert Foley    'install_cmds'    : "",
575d676197SRobert Foley    'boot_dev_type'   : "block",
585d676197SRobert Foley    'ssh_timeout'     : 1,
595d676197SRobert Foley}
605d676197SRobert FoleyBOOT_DEVICE = {
615d676197SRobert Foley    'block' :  "-drive file={},if=none,id=drive0,cache=writeback "\
625d676197SRobert Foley               "-device virtio-blk,drive=drive0,bootindex=0",
635d676197SRobert Foley    'scsi'  :  "-device virtio-scsi-device,id=scsi "\
645d676197SRobert Foley               "-drive file={},format=raw,if=none,id=hd0 "\
655d676197SRobert Foley               "-device scsi-hd,drive=hd0,bootindex=0",
665d676197SRobert Foley}
67ff2ebff0SFam Zhengclass BaseVM(object):
68ff2ebff0SFam Zheng
69b08ba163SGerd Hoffmann    envvars = [
70b08ba163SGerd Hoffmann        "https_proxy",
71b08ba163SGerd Hoffmann        "http_proxy",
72b08ba163SGerd Hoffmann        "ftp_proxy",
73b08ba163SGerd Hoffmann        "no_proxy",
74b08ba163SGerd Hoffmann    ]
75b08ba163SGerd Hoffmann
76ff2ebff0SFam Zheng    # The script to run in the guest that builds QEMU
77ff2ebff0SFam Zheng    BUILD_SCRIPT = ""
78ff2ebff0SFam Zheng    # The guest name, to be overridden by subclasses
79ff2ebff0SFam Zheng    name = "#base"
8031719c37SPhilippe Mathieu-Daudé    # The guest architecture, to be overridden by subclasses
8131719c37SPhilippe Mathieu-Daudé    arch = "#arch"
82b3f94b2fSGerd Hoffmann    # command to halt the guest, can be overridden by subclasses
83b3f94b2fSGerd Hoffmann    poweroff = "poweroff"
844a70232bSRobert Foley    # Time to wait for shutdown to finish.
854a70232bSRobert Foley    shutdown_timeout_default = 30
865b790481SEduardo Habkost    # enable IPv6 networking
875b790481SEduardo Habkost    ipv6 = True
885d676197SRobert Foley    # This is the timeout on the wait for console bytes.
895d676197SRobert Foley    socket_timeout = 120
90c9de3935SRobert Foley    # Scale up some timeouts under TCG.
91c9de3935SRobert Foley    # 4 is arbitrary, but greater than 2,
92c9de3935SRobert Foley    # since we found we need to wait more than twice as long.
934a70232bSRobert Foley    tcg_timeout_multiplier = 4
945d676197SRobert Foley    def __init__(self, args, config=None):
95ff2ebff0SFam Zheng        self._guest = None
961f335d18SRobert Foley        self._genisoimage = args.genisoimage
971f335d18SRobert Foley        self._build_path = args.build_path
9813336606SRobert Foley        self._efi_aarch64 = args.efi_aarch64
99*7bb17a92SAlex Bennée        self._source_path = args.source_path
1005d676197SRobert Foley        # Allow input config to override defaults.
1015d676197SRobert Foley        self._config = DEFAULT_CONFIG.copy()
1025d676197SRobert Foley        if config != None:
1035d676197SRobert Foley            self._config.update(config)
1045d676197SRobert Foley        self.validate_ssh_keys()
105ff2ebff0SFam Zheng        self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
106ff2ebff0SFam Zheng                                                         suffix=".tmp",
107ff2ebff0SFam Zheng                                                         dir="."))
108ff2ebff0SFam Zheng        atexit.register(shutil.rmtree, self._tmpdir)
1095d676197SRobert Foley        # Copy the key files to a temporary directory.
1105d676197SRobert Foley        # Also chmod the key file to agree with ssh requirements.
1115d676197SRobert Foley        self._config['ssh_key'] = \
1125d676197SRobert Foley            open(self._config['ssh_key_file']).read().rstrip()
1135d676197SRobert Foley        self._config['ssh_pub_key'] = \
1145d676197SRobert Foley            open(self._config['ssh_pub_key_file']).read().rstrip()
1155d676197SRobert Foley        self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa")
1165d676197SRobert Foley        open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key'])
1175d676197SRobert Foley        subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file])
118ff2ebff0SFam Zheng
1195d676197SRobert Foley        self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
1205d676197SRobert Foley        open(self._ssh_tmp_pub_key_file,
1215d676197SRobert Foley             "w").write(self._config['ssh_pub_key'])
122ff2ebff0SFam Zheng
1231f335d18SRobert Foley        self.debug = args.debug
124ff14ab0cSRobert Foley        self._console_log_path = None
125ff14ab0cSRobert Foley        if args.log_console:
126ff14ab0cSRobert Foley                self._console_log_path = \
127ff14ab0cSRobert Foley                         os.path.join(os.path.expanduser("~/.cache/qemu-vm"),
128ff14ab0cSRobert Foley                                      "{}.install.log".format(self.name))
129ff2ebff0SFam Zheng        self._stderr = sys.stderr
130ff2ebff0SFam Zheng        self._devnull = open(os.devnull, "w")
131ff2ebff0SFam Zheng        if self.debug:
132ff2ebff0SFam Zheng            self._stdout = sys.stdout
133ff2ebff0SFam Zheng        else:
134ff2ebff0SFam Zheng            self._stdout = self._devnull
1355d676197SRobert Foley        netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
136ff2ebff0SFam Zheng        self._args = [ \
1375d676197SRobert Foley            "-nodefaults", "-m", self._config['memory'],
1385d676197SRobert Foley            "-cpu", self._config['cpu'],
1395d676197SRobert Foley            "-netdev",
1405d676197SRobert Foley            netdev.format(self._config['ssh_port']) +
1415d676197SRobert Foley            (",ipv6=no" if not self.ipv6 else "") +
1425d676197SRobert Foley            (",dns=" + self._config['dns'] if self._config['dns'] else ""),
143ff2ebff0SFam Zheng            "-device", "virtio-net-pci,netdev=vnet",
1448dd38334SGerd Hoffmann            "-vnc", "127.0.0.1:0,to=20"]
1451f335d18SRobert Foley        if args.jobs and args.jobs > 1:
1461f335d18SRobert Foley            self._args += ["-smp", "%d" % args.jobs]
14771531bb5SPhilippe Mathieu-Daudé        if kvm_available(self.arch):
1484a70232bSRobert Foley            self._shutdown_timeout = self.shutdown_timeout_default
149ff2ebff0SFam Zheng            self._args += ["-enable-kvm"]
150ff2ebff0SFam Zheng        else:
151ff2ebff0SFam Zheng            logging.info("KVM not available, not using -enable-kvm")
1524a70232bSRobert Foley            self._shutdown_timeout = \
1534a70232bSRobert Foley                self.shutdown_timeout_default * self.tcg_timeout_multiplier
154ff2ebff0SFam Zheng        self._data_args = []
155ff2ebff0SFam Zheng
1565d676197SRobert Foley        if self._config['qemu_args'] != None:
1575d676197SRobert Foley            qemu_args = self._config['qemu_args']
1585d676197SRobert Foley            qemu_args = qemu_args.replace('\n',' ').replace('\r','')
1595d676197SRobert Foley            # shlex groups quoted arguments together
1605d676197SRobert Foley            # we need this to keep the quoted args together for when
1615d676197SRobert Foley            # the QEMU command is issued later.
1625d676197SRobert Foley            args = shlex.split(qemu_args)
1635d676197SRobert Foley            self._config['extra_args'] = []
1645d676197SRobert Foley            for arg in args:
1655d676197SRobert Foley                if arg:
1665d676197SRobert Foley                    # Preserve quotes around arguments.
1675d676197SRobert Foley                    # shlex above takes them out, so add them in.
1685d676197SRobert Foley                    if " " in arg:
1695d676197SRobert Foley                        arg = '"{}"'.format(arg)
1705d676197SRobert Foley                    self._config['extra_args'].append(arg)
1715d676197SRobert Foley
1725d676197SRobert Foley    def validate_ssh_keys(self):
1735d676197SRobert Foley        """Check to see if the ssh key files exist."""
1745d676197SRobert Foley        if 'ssh_key_file' not in self._config or\
1755d676197SRobert Foley           not os.path.exists(self._config['ssh_key_file']):
1765d676197SRobert Foley            raise Exception("ssh key file not found.")
1775d676197SRobert Foley        if 'ssh_pub_key_file' not in self._config or\
1785d676197SRobert Foley           not os.path.exists(self._config['ssh_pub_key_file']):
1795d676197SRobert Foley               raise Exception("ssh pub key file not found.")
1805d676197SRobert Foley
1815d676197SRobert Foley    def wait_boot(self, wait_string=None):
1825d676197SRobert Foley        """Wait for the standard string we expect
1835d676197SRobert Foley           on completion of a normal boot.
1845d676197SRobert Foley           The user can also choose to override with an
1855d676197SRobert Foley           alternate string to wait for."""
1865d676197SRobert Foley        if wait_string is None:
1875d676197SRobert Foley            if self.login_prompt is None:
1885d676197SRobert Foley                raise Exception("self.login_prompt not defined")
1895d676197SRobert Foley            wait_string = self.login_prompt
1905d676197SRobert Foley        # Intentionally bump up the default timeout under TCG,
1915d676197SRobert Foley        # since the console wait below takes longer.
1925d676197SRobert Foley        timeout = self.socket_timeout
1935d676197SRobert Foley        if not kvm_available(self.arch):
1945d676197SRobert Foley            timeout *= 8
1955d676197SRobert Foley        self.console_init(timeout=timeout)
1965d676197SRobert Foley        self.console_wait(wait_string)
1975d676197SRobert Foley
1985b4b4865SAlex Bennée    def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
199ff2ebff0SFam Zheng        def check_sha256sum(fname):
200ff2ebff0SFam Zheng            if not sha256sum:
201ff2ebff0SFam Zheng                return True
202ff2ebff0SFam Zheng            checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
2033ace9be6SGerd Hoffmann            return sha256sum == checksum.decode("utf-8")
204ff2ebff0SFam Zheng
2055b4b4865SAlex Bennée        def check_sha512sum(fname):
2065b4b4865SAlex Bennée            if not sha512sum:
2075b4b4865SAlex Bennée                return True
2085b4b4865SAlex Bennée            checksum = subprocess.check_output(["sha512sum", fname]).split()[0]
2095b4b4865SAlex Bennée            return sha512sum == checksum.decode("utf-8")
2105b4b4865SAlex Bennée
211ff2ebff0SFam Zheng        cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
212ff2ebff0SFam Zheng        if not os.path.exists(cache_dir):
213ff2ebff0SFam Zheng            os.makedirs(cache_dir)
2143ace9be6SGerd Hoffmann        fname = os.path.join(cache_dir,
2153ace9be6SGerd Hoffmann                             hashlib.sha1(url.encode("utf-8")).hexdigest())
2165b4b4865SAlex Bennée        if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname):
217ff2ebff0SFam Zheng            return fname
218ff2ebff0SFam Zheng        logging.debug("Downloading %s to %s...", url, fname)
219ff2ebff0SFam Zheng        subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
220ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
221ff2ebff0SFam Zheng        os.rename(fname + ".download", fname)
222ff2ebff0SFam Zheng        return fname
223ff2ebff0SFam Zheng
224796471e9SGerd Hoffmann    def _ssh_do(self, user, cmd, check):
22589adc5b9SRobert Foley        ssh_cmd = ["ssh",
22689adc5b9SRobert Foley                   "-t",
227ff2ebff0SFam Zheng                   "-o", "StrictHostKeyChecking=no",
228ff2ebff0SFam Zheng                   "-o", "UserKnownHostsFile=" + os.devnull,
2295d676197SRobert Foley                   "-o",
2305d676197SRobert Foley                   "ConnectTimeout={}".format(self._config["ssh_timeout"]),
231976218cbSCleber Rosa                   "-p", str(self.ssh_port), "-i", self._ssh_tmp_key_file]
23289adc5b9SRobert Foley        # If not in debug mode, set ssh to quiet mode to
23389adc5b9SRobert Foley        # avoid printing the results of commands.
23489adc5b9SRobert Foley        if not self.debug:
23589adc5b9SRobert Foley            ssh_cmd.append("-q")
236b08ba163SGerd Hoffmann        for var in self.envvars:
237b08ba163SGerd Hoffmann            ssh_cmd += ['-o', "SendEnv=%s" % var ]
238ff2ebff0SFam Zheng        assert not isinstance(cmd, str)
239ff2ebff0SFam Zheng        ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
240ff2ebff0SFam Zheng        logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
241726c9a3bSFam Zheng        r = subprocess.call(ssh_cmd)
242ff2ebff0SFam Zheng        if check and r != 0:
243ff2ebff0SFam Zheng            raise Exception("SSH command failed: %s" % cmd)
244ff2ebff0SFam Zheng        return r
245ff2ebff0SFam Zheng
246ff2ebff0SFam Zheng    def ssh(self, *cmd):
247df001680SRobert Foley        return self._ssh_do(self._config["guest_user"], cmd, False)
248ff2ebff0SFam Zheng
249ff2ebff0SFam Zheng    def ssh_root(self, *cmd):
2509fc33bf4SAlexander von Gluck IV        return self._ssh_do(self._config["root_user"], cmd, False)
251ff2ebff0SFam Zheng
252ff2ebff0SFam Zheng    def ssh_check(self, *cmd):
253df001680SRobert Foley        self._ssh_do(self._config["guest_user"], cmd, True)
254ff2ebff0SFam Zheng
255ff2ebff0SFam Zheng    def ssh_root_check(self, *cmd):
2569fc33bf4SAlexander von Gluck IV        self._ssh_do(self._config["root_user"], cmd, True)
257ff2ebff0SFam Zheng
258ff2ebff0SFam Zheng    def build_image(self, img):
259ff2ebff0SFam Zheng        raise NotImplementedError
260ff2ebff0SFam Zheng
2611e48931cSWainer dos Santos Moschetta    def exec_qemu_img(self, *args):
2621e48931cSWainer dos Santos Moschetta        cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
2631e48931cSWainer dos Santos Moschetta        cmd.extend(list(args))
2641e48931cSWainer dos Santos Moschetta        subprocess.check_call(cmd)
2651e48931cSWainer dos Santos Moschetta
266ff2ebff0SFam Zheng    def add_source_dir(self, src_dir):
2673ace9be6SGerd Hoffmann        name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
268ff2ebff0SFam Zheng        tarfile = os.path.join(self._tmpdir, name + ".tar")
269ff2ebff0SFam Zheng        logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
270ff2ebff0SFam Zheng        subprocess.check_call(["./scripts/archive-source.sh", tarfile],
271ff2ebff0SFam Zheng                              cwd=src_dir, stdin=self._devnull,
272ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
273ff2ebff0SFam Zheng        self._data_args += ["-drive",
274ff2ebff0SFam Zheng                            "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
275ff2ebff0SFam Zheng                                    (tarfile, name),
276ff2ebff0SFam Zheng                            "-device",
277ff2ebff0SFam Zheng                            "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
278ff2ebff0SFam Zheng
279ff2ebff0SFam Zheng    def boot(self, img, extra_args=[]):
2805d676197SRobert Foley        boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
2815d676197SRobert Foley        boot_params = boot_dev.format(img)
2825d676197SRobert Foley        args = self._args + boot_params.split(' ')
2835d676197SRobert Foley        args += self._data_args + extra_args + self._config['extra_args']
284ff2ebff0SFam Zheng        logging.debug("QEMU args: %s", " ".join(args))
285e56c4504SRobert Foley        qemu_path = get_qemu_path(self.arch, self._build_path)
286ff14ab0cSRobert Foley
287ff14ab0cSRobert Foley        # Since console_log_path is only set when the user provides the
288ff14ab0cSRobert Foley        # log_console option, we will set drain_console=True so the
289ff14ab0cSRobert Foley        # console is always drained.
290ff14ab0cSRobert Foley        guest = QEMUMachine(binary=qemu_path, args=args,
291ff14ab0cSRobert Foley                            console_log=self._console_log_path,
292ff14ab0cSRobert Foley                            drain_console=True)
2935d676197SRobert Foley        guest.set_machine(self._config['machine'])
2948dd38334SGerd Hoffmann        guest.set_console()
295ff2ebff0SFam Zheng        try:
296ff2ebff0SFam Zheng            guest.launch()
297ff2ebff0SFam Zheng        except:
298ff2ebff0SFam Zheng            logging.error("Failed to launch QEMU, command line:")
299e56c4504SRobert Foley            logging.error(" ".join([qemu_path] + args))
300ff2ebff0SFam Zheng            logging.error("Log:")
301ff2ebff0SFam Zheng            logging.error(guest.get_log())
302ff2ebff0SFam Zheng            logging.error("QEMU version >= 2.10 is required")
303ff2ebff0SFam Zheng            raise
304ff2ebff0SFam Zheng        atexit.register(self.shutdown)
305ff2ebff0SFam Zheng        self._guest = guest
306ff14ab0cSRobert Foley        # Init console so we can start consuming the chars.
307ff14ab0cSRobert Foley        self.console_init()
308ff2ebff0SFam Zheng        usernet_info = guest.qmp("human-monitor-command",
309976218cbSCleber Rosa                                 command_line="info usernet").get("return")
310976218cbSCleber Rosa        self.ssh_port = get_info_usernet_hostfwd_port(usernet_info)
311ff2ebff0SFam Zheng        if not self.ssh_port:
312ff2ebff0SFam Zheng            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
313ff2ebff0SFam Zheng                            usernet_info)
314ff2ebff0SFam Zheng
315ff14ab0cSRobert Foley    def console_init(self, timeout = None):
316ff14ab0cSRobert Foley        if timeout == None:
317ff14ab0cSRobert Foley            timeout = self.socket_timeout
3188dd38334SGerd Hoffmann        vm = self._guest
3198dd38334SGerd Hoffmann        vm.console_socket.settimeout(timeout)
320698a64f9SGerd Hoffmann        self.console_raw_path = os.path.join(vm._temp_dir,
321698a64f9SGerd Hoffmann                                             vm._name + "-console.raw")
322698a64f9SGerd Hoffmann        self.console_raw_file = open(self.console_raw_path, 'wb')
3238dd38334SGerd Hoffmann
3248dd38334SGerd Hoffmann    def console_log(self, text):
3258dd38334SGerd Hoffmann        for line in re.split("[\r\n]", text):
3268dd38334SGerd Hoffmann            # filter out terminal escape sequences
3278dd38334SGerd Hoffmann            line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
3288dd38334SGerd Hoffmann            line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
3298dd38334SGerd Hoffmann            # replace unprintable chars
3308dd38334SGerd Hoffmann            line = re.sub("\x1b", "<esc>", line)
3318dd38334SGerd Hoffmann            line = re.sub("[\x00-\x1f]", ".", line)
3328dd38334SGerd Hoffmann            line = re.sub("[\x80-\xff]", ".", line)
3338dd38334SGerd Hoffmann            if line == "":
3348dd38334SGerd Hoffmann                continue
3358dd38334SGerd Hoffmann            # log console line
3368dd38334SGerd Hoffmann            sys.stderr.write("con recv: %s\n" % line)
3378dd38334SGerd Hoffmann
33860136e06SGerd Hoffmann    def console_wait(self, expect, expectalt = None):
3398dd38334SGerd Hoffmann        vm = self._guest
3408dd38334SGerd Hoffmann        output = ""
3418dd38334SGerd Hoffmann        while True:
3428dd38334SGerd Hoffmann            try:
3438dd38334SGerd Hoffmann                chars = vm.console_socket.recv(1)
344698a64f9SGerd Hoffmann                if self.console_raw_file:
345698a64f9SGerd Hoffmann                    self.console_raw_file.write(chars)
346698a64f9SGerd Hoffmann                    self.console_raw_file.flush()
3478dd38334SGerd Hoffmann            except socket.timeout:
3488dd38334SGerd Hoffmann                sys.stderr.write("console: *** read timeout ***\n")
3498dd38334SGerd Hoffmann                sys.stderr.write("console: waiting for: '%s'\n" % expect)
35060136e06SGerd Hoffmann                if not expectalt is None:
35160136e06SGerd Hoffmann                    sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
3528dd38334SGerd Hoffmann                sys.stderr.write("console: line buffer:\n")
3538dd38334SGerd Hoffmann                sys.stderr.write("\n")
3548dd38334SGerd Hoffmann                self.console_log(output.rstrip())
3558dd38334SGerd Hoffmann                sys.stderr.write("\n")
3568dd38334SGerd Hoffmann                raise
3578dd38334SGerd Hoffmann            output += chars.decode("latin1")
3588dd38334SGerd Hoffmann            if expect in output:
3598dd38334SGerd Hoffmann                break
36060136e06SGerd Hoffmann            if not expectalt is None and expectalt in output:
36160136e06SGerd Hoffmann                break
3628dd38334SGerd Hoffmann            if "\r" in output or "\n" in output:
3638dd38334SGerd Hoffmann                lines = re.split("[\r\n]", output)
3648dd38334SGerd Hoffmann                output = lines.pop()
3658dd38334SGerd Hoffmann                if self.debug:
3668dd38334SGerd Hoffmann                    self.console_log("\n".join(lines))
3678dd38334SGerd Hoffmann        if self.debug:
3688dd38334SGerd Hoffmann            self.console_log(output)
36960136e06SGerd Hoffmann        if not expectalt is None and expectalt in output:
37060136e06SGerd Hoffmann            return False
37160136e06SGerd Hoffmann        return True
3728dd38334SGerd Hoffmann
3736c4f0416SGerd Hoffmann    def console_consume(self):
3746c4f0416SGerd Hoffmann        vm = self._guest
3756c4f0416SGerd Hoffmann        output = ""
3766c4f0416SGerd Hoffmann        vm.console_socket.setblocking(0)
3776c4f0416SGerd Hoffmann        while True:
3786c4f0416SGerd Hoffmann            try:
3796c4f0416SGerd Hoffmann                chars = vm.console_socket.recv(1)
3806c4f0416SGerd Hoffmann            except:
3816c4f0416SGerd Hoffmann                break
3826c4f0416SGerd Hoffmann            output += chars.decode("latin1")
3836c4f0416SGerd Hoffmann            if "\r" in output or "\n" in output:
3846c4f0416SGerd Hoffmann                lines = re.split("[\r\n]", output)
3856c4f0416SGerd Hoffmann                output = lines.pop()
3866c4f0416SGerd Hoffmann                if self.debug:
3876c4f0416SGerd Hoffmann                    self.console_log("\n".join(lines))
3886c4f0416SGerd Hoffmann        if self.debug:
3896c4f0416SGerd Hoffmann            self.console_log(output)
3906c4f0416SGerd Hoffmann        vm.console_socket.setblocking(1)
3916c4f0416SGerd Hoffmann
3928dd38334SGerd Hoffmann    def console_send(self, command):
3938dd38334SGerd Hoffmann        vm = self._guest
3948dd38334SGerd Hoffmann        if self.debug:
3958dd38334SGerd Hoffmann            logline = re.sub("\n", "<enter>", command)
3968dd38334SGerd Hoffmann            logline = re.sub("[\x00-\x1f]", ".", logline)
3978dd38334SGerd Hoffmann            sys.stderr.write("con send: %s\n" % logline)
3988dd38334SGerd Hoffmann        for char in list(command):
3998dd38334SGerd Hoffmann            vm.console_socket.send(char.encode("utf-8"))
4008dd38334SGerd Hoffmann            time.sleep(0.01)
4018dd38334SGerd Hoffmann
4028dd38334SGerd Hoffmann    def console_wait_send(self, wait, command):
4038dd38334SGerd Hoffmann        self.console_wait(wait)
4048dd38334SGerd Hoffmann        self.console_send(command)
4058dd38334SGerd Hoffmann
4068dd38334SGerd Hoffmann    def console_ssh_init(self, prompt, user, pw):
4075d676197SRobert Foley        sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
4085d676197SRobert Foley                     % self._config['ssh_pub_key'].rstrip()
4098dd38334SGerd Hoffmann        self.console_wait_send("login:",    "%s\n" % user)
4108dd38334SGerd Hoffmann        self.console_wait_send("Password:", "%s\n" % pw)
4118dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "mkdir .ssh\n")
4128dd38334SGerd Hoffmann        self.console_wait_send(prompt,      sshkey_cmd)
4138dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 755 .ssh\n")
4148dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
4158dd38334SGerd Hoffmann
4168dd38334SGerd Hoffmann    def console_sshd_config(self, prompt):
4178dd38334SGerd Hoffmann        self.console_wait(prompt)
4188dd38334SGerd Hoffmann        self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
4198dd38334SGerd Hoffmann        for var in self.envvars:
4208dd38334SGerd Hoffmann            self.console_wait(prompt)
4218dd38334SGerd Hoffmann            self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
4228dd38334SGerd Hoffmann
4238dd38334SGerd Hoffmann    def print_step(self, text):
4248dd38334SGerd Hoffmann        sys.stderr.write("### %s ...\n" % text)
4258dd38334SGerd Hoffmann
4266ee982c9SRobert Foley    def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
427c9de3935SRobert Foley        # Allow more time for VM to boot under TCG.
428c9de3935SRobert Foley        if not kvm_available(self.arch):
4294a70232bSRobert Foley            seconds *= self.tcg_timeout_multiplier
430ff2ebff0SFam Zheng        starttime = datetime.datetime.now()
431f5d3d218SPhilippe Mathieu-Daudé        endtime = starttime + datetime.timedelta(seconds=seconds)
4326ee982c9SRobert Foley        cmd_success = False
433f5d3d218SPhilippe Mathieu-Daudé        while datetime.datetime.now() < endtime:
4346ee982c9SRobert Foley            if wait_root and self.ssh_root(cmd) == 0:
4356ee982c9SRobert Foley                cmd_success = True
436fbb3aa29SRobert Foley                break
4376ee982c9SRobert Foley            elif self.ssh(cmd) == 0:
4386ee982c9SRobert Foley                cmd_success = True
439ff2ebff0SFam Zheng                break
440f5d3d218SPhilippe Mathieu-Daudé            seconds = (endtime - datetime.datetime.now()).total_seconds()
441f5d3d218SPhilippe Mathieu-Daudé            logging.debug("%ds before timeout", seconds)
442ff2ebff0SFam Zheng            time.sleep(1)
4436ee982c9SRobert Foley        if not cmd_success:
444ff2ebff0SFam Zheng            raise Exception("Timeout while waiting for guest ssh")
445ff2ebff0SFam Zheng
446ff2ebff0SFam Zheng    def shutdown(self):
4474a70232bSRobert Foley        self._guest.shutdown(timeout=self._shutdown_timeout)
448ff2ebff0SFam Zheng
449ff2ebff0SFam Zheng    def wait(self):
4504a70232bSRobert Foley        self._guest.wait(timeout=self._shutdown_timeout)
451ff2ebff0SFam Zheng
452b3f94b2fSGerd Hoffmann    def graceful_shutdown(self):
453b3f94b2fSGerd Hoffmann        self.ssh_root(self.poweroff)
4544a70232bSRobert Foley        self._guest.wait(timeout=self._shutdown_timeout)
455b3f94b2fSGerd Hoffmann
456ff2ebff0SFam Zheng    def qmp(self, *args, **kwargs):
457ff2ebff0SFam Zheng        return self._guest.qmp(*args, **kwargs)
458ff2ebff0SFam Zheng
459b081986cSRobert Foley    def gen_cloud_init_iso(self):
460b081986cSRobert Foley        cidir = self._tmpdir
461b081986cSRobert Foley        mdata = open(os.path.join(cidir, "meta-data"), "w")
462b081986cSRobert Foley        name = self.name.replace(".","-")
463b081986cSRobert Foley        mdata.writelines(["instance-id: {}-vm-0\n".format(name),
464b081986cSRobert Foley                          "local-hostname: {}-guest\n".format(name)])
465b081986cSRobert Foley        mdata.close()
466b081986cSRobert Foley        udata = open(os.path.join(cidir, "user-data"), "w")
4675d676197SRobert Foley        print("guest user:pw {}:{}".format(self._config['guest_user'],
4685d676197SRobert Foley                                           self._config['guest_pass']))
469b081986cSRobert Foley        udata.writelines(["#cloud-config\n",
470b081986cSRobert Foley                          "chpasswd:\n",
471b081986cSRobert Foley                          "  list: |\n",
4725d676197SRobert Foley                          "    root:%s\n" % self._config['root_pass'],
4735d676197SRobert Foley                          "    %s:%s\n" % (self._config['guest_user'],
4745d676197SRobert Foley                                           self._config['guest_pass']),
475b081986cSRobert Foley                          "  expire: False\n",
476b081986cSRobert Foley                          "users:\n",
4775d676197SRobert Foley                          "  - name: %s\n" % self._config['guest_user'],
478b081986cSRobert Foley                          "    sudo: ALL=(ALL) NOPASSWD:ALL\n",
479b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
4805d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
481b081986cSRobert Foley                          "  - name: root\n",
482b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
4835d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
484b081986cSRobert Foley                          "locale: en_US.UTF-8\n"])
485b081986cSRobert Foley        proxy = os.environ.get("http_proxy")
486b081986cSRobert Foley        if not proxy is None:
487b081986cSRobert Foley            udata.writelines(["apt:\n",
488b081986cSRobert Foley                              "  proxy: %s" % proxy])
489b081986cSRobert Foley        udata.close()
49092fecad3SAlex Bennée        subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
491b081986cSRobert Foley                               "-volid", "cidata", "-joliet", "-rock",
492b081986cSRobert Foley                               "user-data", "meta-data"],
493b081986cSRobert Foley                              cwd=cidir,
494b081986cSRobert Foley                              stdin=self._devnull, stdout=self._stdout,
495b081986cSRobert Foley                              stderr=self._stdout)
496b081986cSRobert Foley        return os.path.join(cidir, "cloud-init.iso")
497b081986cSRobert Foley
498e56c4504SRobert Foleydef get_qemu_path(arch, build_path=None):
499e56c4504SRobert Foley    """Fetch the path to the qemu binary."""
500e56c4504SRobert Foley    # If QEMU environment variable set, it takes precedence
501e56c4504SRobert Foley    if "QEMU" in os.environ:
502e56c4504SRobert Foley        qemu_path = os.environ["QEMU"]
503e56c4504SRobert Foley    elif build_path:
504e56c4504SRobert Foley        qemu_path = os.path.join(build_path, arch + "-softmmu")
505e56c4504SRobert Foley        qemu_path = os.path.join(qemu_path, "qemu-system-" + arch)
506e56c4504SRobert Foley    else:
507e56c4504SRobert Foley        # Default is to use system path for qemu.
508e56c4504SRobert Foley        qemu_path = "qemu-system-" + arch
509e56c4504SRobert Foley    return qemu_path
510e56c4504SRobert Foley
51113336606SRobert Foleydef get_qemu_version(qemu_path):
51213336606SRobert Foley    """Get the version number from the current QEMU,
51313336606SRobert Foley       and return the major number."""
51413336606SRobert Foley    output = subprocess.check_output([qemu_path, '--version'])
51513336606SRobert Foley    version_line = output.decode("utf-8")
51613336606SRobert Foley    version_num = re.split(' |\(', version_line)[3].split('.')[0]
51713336606SRobert Foley    return int(version_num)
51813336606SRobert Foley
5193f1e8137SRobert Foleydef parse_config(config, args):
5203f1e8137SRobert Foley    """ Parse yaml config and populate our config structure.
5213f1e8137SRobert Foley        The yaml config allows the user to override the
5223f1e8137SRobert Foley        defaults for VM parameters.  In many cases these
5233f1e8137SRobert Foley        defaults can be overridden without rebuilding the VM."""
5243f1e8137SRobert Foley    if args.config:
5253f1e8137SRobert Foley        config_file = args.config
5263f1e8137SRobert Foley    elif 'QEMU_CONFIG' in os.environ:
5273f1e8137SRobert Foley        config_file = os.environ['QEMU_CONFIG']
5283f1e8137SRobert Foley    else:
5293f1e8137SRobert Foley        return config
5303f1e8137SRobert Foley    if not os.path.exists(config_file):
5313f1e8137SRobert Foley        raise Exception("config file {} does not exist".format(config_file))
5323f1e8137SRobert Foley    # We gracefully handle importing the yaml module
5333f1e8137SRobert Foley    # since it might not be installed.
5343f1e8137SRobert Foley    # If we are here it means the user supplied a .yml file,
5353f1e8137SRobert Foley    # so if the yaml module is not installed we will exit with error.
5363f1e8137SRobert Foley    try:
5373f1e8137SRobert Foley        import yaml
5383f1e8137SRobert Foley    except ImportError:
5393f1e8137SRobert Foley        print("The python3-yaml package is needed "\
5403f1e8137SRobert Foley              "to support config.yaml files")
5413f1e8137SRobert Foley        # Instead of raising an exception we exit to avoid
5423f1e8137SRobert Foley        # a raft of messy (expected) errors to stdout.
5433f1e8137SRobert Foley        exit(1)
5443f1e8137SRobert Foley    with open(config_file) as f:
5453f1e8137SRobert Foley        yaml_dict = yaml.safe_load(f)
5463f1e8137SRobert Foley
5473f1e8137SRobert Foley    if 'qemu-conf' in yaml_dict:
5483f1e8137SRobert Foley        config.update(yaml_dict['qemu-conf'])
5493f1e8137SRobert Foley    else:
5503f1e8137SRobert Foley        raise Exception("config file {} is not valid"\
5513f1e8137SRobert Foley                        " missing qemu-conf".format(config_file))
5523f1e8137SRobert Foley    return config
5533f1e8137SRobert Foley
55463a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls):
5558a6e007eSPhilippe Mathieu-Daudé
5568a6e007eSPhilippe Mathieu-Daudé    def get_default_jobs():
557b0953944SAlex Bennée        if multiprocessing.cpu_count() > 1:
55863a24c5eSPhilippe Mathieu-Daudé            if kvm_available(vmcls.arch):
5593ad3e36eSWainer dos Santos Moschetta                return multiprocessing.cpu_count() // 2
560b0953944SAlex Bennée            elif os.uname().machine == "x86_64" and \
561b0953944SAlex Bennée                 vmcls.arch in ["aarch64", "x86_64", "i386"]:
562b0953944SAlex Bennée                # MTTCG is available on these arches and we can allow
563b0953944SAlex Bennée                # more cores. but only up to a reasonable limit. User
564b0953944SAlex Bennée                # can always override these limits with --jobs.
565b0953944SAlex Bennée                return min(multiprocessing.cpu_count() // 2, 8)
5668a6e007eSPhilippe Mathieu-Daudé        else:
5678a6e007eSPhilippe Mathieu-Daudé            return 1
5688a6e007eSPhilippe Mathieu-Daudé
5692fea3a12SAlex Bennée    parser = argparse.ArgumentParser(
5702fea3a12SAlex Bennée        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
5712fea3a12SAlex Bennée        description="Utility for provisioning VMs and running builds",
5722fea3a12SAlex Bennée        epilog="""Remaining arguments are passed to the command.
5732fea3a12SAlex Bennée        Exit codes: 0 = success, 1 = command line error,
5742fea3a12SAlex Bennée        2 = environment initialization failed,
5752fea3a12SAlex Bennée        3 = test command failed""")
5762fea3a12SAlex Bennée    parser.add_argument("--debug", "-D", action="store_true",
577ff2ebff0SFam Zheng                        help="enable debug output")
5782fea3a12SAlex Bennée    parser.add_argument("--image", "-i", default="%s.img" % vmcls.name,
579ff2ebff0SFam Zheng                        help="image file name")
5802fea3a12SAlex Bennée    parser.add_argument("--force", "-f", action="store_true",
581ff2ebff0SFam Zheng                        help="force build image even if image exists")
5822fea3a12SAlex Bennée    parser.add_argument("--jobs", type=int, default=get_default_jobs(),
583ff2ebff0SFam Zheng                        help="number of virtual CPUs")
5842fea3a12SAlex Bennée    parser.add_argument("--verbose", "-V", action="store_true",
58541e3340aSPeter Maydell                        help="Pass V=1 to builds within the guest")
5862fea3a12SAlex Bennée    parser.add_argument("--build-image", "-b", action="store_true",
587ff2ebff0SFam Zheng                        help="build image")
5882fea3a12SAlex Bennée    parser.add_argument("--build-qemu",
589ff2ebff0SFam Zheng                        help="build QEMU from source in guest")
5902fea3a12SAlex Bennée    parser.add_argument("--build-target",
5915c2ec9b6SAlex Bennée                        help="QEMU build target", default="check")
5922fea3a12SAlex Bennée    parser.add_argument("--build-path", default=None,
593e56c4504SRobert Foley                        help="Path of build directory, "\
594e56c4504SRobert Foley                        "for using build tree QEMU binary. ")
595*7bb17a92SAlex Bennée    parser.add_argument("--source-path", default=None,
596*7bb17a92SAlex Bennée                        help="Path of source directory, "\
597*7bb17a92SAlex Bennée                        "for finding additional files. ")
5982fea3a12SAlex Bennée    parser.add_argument("--interactive", "-I", action="store_true",
599ff2ebff0SFam Zheng                        help="Interactively run command")
6002fea3a12SAlex Bennée    parser.add_argument("--snapshot", "-s", action="store_true",
601983c2a77SFam Zheng                        help="run tests with a snapshot")
6022fea3a12SAlex Bennée    parser.add_argument("--genisoimage", default="genisoimage",
60392fecad3SAlex Bennée                        help="iso imaging tool")
6042fea3a12SAlex Bennée    parser.add_argument("--config", "-c", default=None,
6053f1e8137SRobert Foley                        help="Provide config yaml for configuration. "\
6063f1e8137SRobert Foley                        "See config_example.yaml for example.")
6072fea3a12SAlex Bennée    parser.add_argument("--efi-aarch64",
60813336606SRobert Foley                        default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
60913336606SRobert Foley                        help="Path to efi image for aarch64 VMs.")
6102fea3a12SAlex Bennée    parser.add_argument("--log-console", action="store_true",
611ff14ab0cSRobert Foley                        help="Log console to file.")
6122fea3a12SAlex Bennée    parser.add_argument("commands", nargs="*", help="""Remaining
6132fea3a12SAlex Bennée        commands after -- are passed to command inside the VM""")
6142fea3a12SAlex Bennée
615ff2ebff0SFam Zheng    return parser.parse_args()
616ff2ebff0SFam Zheng
6175d676197SRobert Foleydef main(vmcls, config=None):
618ff2ebff0SFam Zheng    try:
6195d676197SRobert Foley        if config == None:
6205d676197SRobert Foley            config = DEFAULT_CONFIG
6212fea3a12SAlex Bennée        args = parse_args(vmcls)
6222fea3a12SAlex Bennée        if not args.commands and not args.build_qemu and not args.build_image:
623f03868bdSEduardo Habkost            print("Nothing to do?")
624ff2ebff0SFam Zheng            return 1
6253f1e8137SRobert Foley        config = parse_config(config, args)
626fb3b4e6dSEduardo Habkost        logging.basicConfig(level=(logging.DEBUG if args.debug
627fb3b4e6dSEduardo Habkost                                   else logging.WARN))
6285d676197SRobert Foley        vm = vmcls(args, config=config)
629ff2ebff0SFam Zheng        if args.build_image:
630ff2ebff0SFam Zheng            if os.path.exists(args.image) and not args.force:
631ff2ebff0SFam Zheng                sys.stderr.writelines(["Image file exists: %s\n" % args.image,
632ff2ebff0SFam Zheng                                      "Use --force option to overwrite\n"])
633ff2ebff0SFam Zheng                return 1
634ff2ebff0SFam Zheng            return vm.build_image(args.image)
635ff2ebff0SFam Zheng        if args.build_qemu:
636ff2ebff0SFam Zheng            vm.add_source_dir(args.build_qemu)
637ff2ebff0SFam Zheng            cmd = [vm.BUILD_SCRIPT.format(
6382fea3a12SAlex Bennée                   configure_opts = " ".join(args.commands),
6393ace9be6SGerd Hoffmann                   jobs=int(args.jobs),
6405c2ec9b6SAlex Bennée                   target=args.build_target,
64141e3340aSPeter Maydell                   verbose = "V=1" if args.verbose else "")]
642ff2ebff0SFam Zheng        else:
6432fea3a12SAlex Bennée            cmd = args.commands
644983c2a77SFam Zheng        img = args.image
645983c2a77SFam Zheng        if args.snapshot:
646983c2a77SFam Zheng            img += ",snapshot=on"
647983c2a77SFam Zheng        vm.boot(img)
648ff2ebff0SFam Zheng        vm.wait_ssh()
649ff2ebff0SFam Zheng    except Exception as e:
650ff2ebff0SFam Zheng        if isinstance(e, SystemExit) and e.code == 0:
651ff2ebff0SFam Zheng            return 0
652ff2ebff0SFam Zheng        sys.stderr.write("Failed to prepare guest environment\n")
653ff2ebff0SFam Zheng        traceback.print_exc()
654ff2ebff0SFam Zheng        return 2
655ff2ebff0SFam Zheng
656b3f94b2fSGerd Hoffmann    exitcode = 0
657ff2ebff0SFam Zheng    if vm.ssh(*cmd) != 0:
658b3f94b2fSGerd Hoffmann        exitcode = 3
659bcc388dfSAlex Bennée    if args.interactive:
660b3f94b2fSGerd Hoffmann        vm.ssh()
661b3f94b2fSGerd Hoffmann
662b3f94b2fSGerd Hoffmann    if not args.snapshot:
663b3f94b2fSGerd Hoffmann        vm.graceful_shutdown()
664b3f94b2fSGerd Hoffmann
665b3f94b2fSGerd Hoffmann    return exitcode
666