xref: /openbmc/qemu/tests/vm/basevm.py (revision 5d676197ebe9bfe0ecc466c5375bcdbc7a7e48f5)
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
32*5d676197SRobert Foleyimport shlex
33ff2ebff0SFam Zheng
34*5d676197SRobert FoleySSH_KEY_FILE = os.path.join(os.path.dirname(__file__),
35*5d676197SRobert Foley               "..", "keys", "id_rsa")
36*5d676197SRobert FoleySSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__),
37*5d676197SRobert Foley                   "..", "keys", "id_rsa.pub")
38ff2ebff0SFam Zheng
39*5d676197SRobert Foley# This is the standard configuration.
40*5d676197SRobert Foley# Any or all of these can be overridden by
41*5d676197SRobert Foley# passing in a config argument to the VM constructor.
42*5d676197SRobert FoleyDEFAULT_CONFIG = {
43*5d676197SRobert Foley    'cpu'             : "max",
44*5d676197SRobert Foley    'machine'         : 'pc',
45*5d676197SRobert Foley    'guest_user'      : "qemu",
46*5d676197SRobert Foley    'guest_pass'      : "qemupass",
47*5d676197SRobert Foley    'root_pass'       : "qemupass",
48*5d676197SRobert Foley    'ssh_key_file'    : SSH_KEY_FILE,
49*5d676197SRobert Foley    'ssh_pub_key_file': SSH_PUB_KEY_FILE,
50*5d676197SRobert Foley    'memory'          : "4G",
51*5d676197SRobert Foley    'extra_args'      : [],
52*5d676197SRobert Foley    'qemu_args'       : "",
53*5d676197SRobert Foley    'dns'             : "",
54*5d676197SRobert Foley    'ssh_port'        : 0,
55*5d676197SRobert Foley    'install_cmds'    : "",
56*5d676197SRobert Foley    'boot_dev_type'   : "block",
57*5d676197SRobert Foley    'ssh_timeout'     : 1,
58*5d676197SRobert Foley}
59*5d676197SRobert FoleyBOOT_DEVICE = {
60*5d676197SRobert Foley    'block' :  "-drive file={},if=none,id=drive0,cache=writeback "\
61*5d676197SRobert Foley               "-device virtio-blk,drive=drive0,bootindex=0",
62*5d676197SRobert Foley    'scsi'  :  "-device virtio-scsi-device,id=scsi "\
63*5d676197SRobert Foley               "-drive file={},format=raw,if=none,id=hd0 "\
64*5d676197SRobert Foley               "-device scsi-hd,drive=hd0,bootindex=0",
65*5d676197SRobert 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
85*5d676197SRobert Foley    # This is the timeout on the wait for console bytes.
86*5d676197SRobert 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
91*5d676197SRobert 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*5d676197SRobert Foley        # Allow input config to override defaults.
96*5d676197SRobert Foley        self._config = DEFAULT_CONFIG.copy()
97*5d676197SRobert Foley        if config != None:
98*5d676197SRobert Foley            self._config.update(config)
99*5d676197SRobert Foley        self.validate_ssh_keys()
100ff2ebff0SFam Zheng        self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
101ff2ebff0SFam Zheng                                                         suffix=".tmp",
102ff2ebff0SFam Zheng                                                         dir="."))
103ff2ebff0SFam Zheng        atexit.register(shutil.rmtree, self._tmpdir)
104*5d676197SRobert Foley        # Copy the key files to a temporary directory.
105*5d676197SRobert Foley        # Also chmod the key file to agree with ssh requirements.
106*5d676197SRobert Foley        self._config['ssh_key'] = \
107*5d676197SRobert Foley            open(self._config['ssh_key_file']).read().rstrip()
108*5d676197SRobert Foley        self._config['ssh_pub_key'] = \
109*5d676197SRobert Foley            open(self._config['ssh_pub_key_file']).read().rstrip()
110*5d676197SRobert Foley        self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa")
111*5d676197SRobert Foley        open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key'])
112*5d676197SRobert Foley        subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file])
113ff2ebff0SFam Zheng
114*5d676197SRobert Foley        self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
115*5d676197SRobert Foley        open(self._ssh_tmp_pub_key_file,
116*5d676197SRobert Foley             "w").write(self._config['ssh_pub_key'])
117ff2ebff0SFam Zheng
1181f335d18SRobert Foley        self.debug = args.debug
119ff2ebff0SFam Zheng        self._stderr = sys.stderr
120ff2ebff0SFam Zheng        self._devnull = open(os.devnull, "w")
121ff2ebff0SFam Zheng        if self.debug:
122ff2ebff0SFam Zheng            self._stdout = sys.stdout
123ff2ebff0SFam Zheng        else:
124ff2ebff0SFam Zheng            self._stdout = self._devnull
125*5d676197SRobert Foley        netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
126ff2ebff0SFam Zheng        self._args = [ \
127*5d676197SRobert Foley            "-nodefaults", "-m", self._config['memory'],
128*5d676197SRobert Foley            "-cpu", self._config['cpu'],
129*5d676197SRobert Foley            "-netdev",
130*5d676197SRobert Foley            netdev.format(self._config['ssh_port']) +
131*5d676197SRobert Foley            (",ipv6=no" if not self.ipv6 else "") +
132*5d676197SRobert Foley            (",dns=" + self._config['dns'] if self._config['dns'] else ""),
133ff2ebff0SFam Zheng            "-device", "virtio-net-pci,netdev=vnet",
1348dd38334SGerd Hoffmann            "-vnc", "127.0.0.1:0,to=20"]
1351f335d18SRobert Foley        if args.jobs and args.jobs > 1:
1361f335d18SRobert Foley            self._args += ["-smp", "%d" % args.jobs]
13771531bb5SPhilippe Mathieu-Daudé        if kvm_available(self.arch):
138ff2ebff0SFam Zheng            self._args += ["-enable-kvm"]
139ff2ebff0SFam Zheng        else:
140ff2ebff0SFam Zheng            logging.info("KVM not available, not using -enable-kvm")
141ff2ebff0SFam Zheng        self._data_args = []
142ff2ebff0SFam Zheng
143*5d676197SRobert Foley        if self._config['qemu_args'] != None:
144*5d676197SRobert Foley            qemu_args = self._config['qemu_args']
145*5d676197SRobert Foley            qemu_args = qemu_args.replace('\n',' ').replace('\r','')
146*5d676197SRobert Foley            # shlex groups quoted arguments together
147*5d676197SRobert Foley            # we need this to keep the quoted args together for when
148*5d676197SRobert Foley            # the QEMU command is issued later.
149*5d676197SRobert Foley            args = shlex.split(qemu_args)
150*5d676197SRobert Foley            self._config['extra_args'] = []
151*5d676197SRobert Foley            for arg in args:
152*5d676197SRobert Foley                if arg:
153*5d676197SRobert Foley                    # Preserve quotes around arguments.
154*5d676197SRobert Foley                    # shlex above takes them out, so add them in.
155*5d676197SRobert Foley                    if " " in arg:
156*5d676197SRobert Foley                        arg = '"{}"'.format(arg)
157*5d676197SRobert Foley                    self._config['extra_args'].append(arg)
158*5d676197SRobert Foley
159*5d676197SRobert Foley    def validate_ssh_keys(self):
160*5d676197SRobert Foley        """Check to see if the ssh key files exist."""
161*5d676197SRobert Foley        if 'ssh_key_file' not in self._config or\
162*5d676197SRobert Foley           not os.path.exists(self._config['ssh_key_file']):
163*5d676197SRobert Foley            raise Exception("ssh key file not found.")
164*5d676197SRobert Foley        if 'ssh_pub_key_file' not in self._config or\
165*5d676197SRobert Foley           not os.path.exists(self._config['ssh_pub_key_file']):
166*5d676197SRobert Foley               raise Exception("ssh pub key file not found.")
167*5d676197SRobert Foley
168*5d676197SRobert Foley    def wait_boot(self, wait_string=None):
169*5d676197SRobert Foley        """Wait for the standard string we expect
170*5d676197SRobert Foley           on completion of a normal boot.
171*5d676197SRobert Foley           The user can also choose to override with an
172*5d676197SRobert Foley           alternate string to wait for."""
173*5d676197SRobert Foley        if wait_string is None:
174*5d676197SRobert Foley            if self.login_prompt is None:
175*5d676197SRobert Foley                raise Exception("self.login_prompt not defined")
176*5d676197SRobert Foley            wait_string = self.login_prompt
177*5d676197SRobert Foley        # Intentionally bump up the default timeout under TCG,
178*5d676197SRobert Foley        # since the console wait below takes longer.
179*5d676197SRobert Foley        timeout = self.socket_timeout
180*5d676197SRobert Foley        if not kvm_available(self.arch):
181*5d676197SRobert Foley            timeout *= 8
182*5d676197SRobert Foley        self.console_init(timeout=timeout)
183*5d676197SRobert Foley        self.console_wait(wait_string)
184*5d676197SRobert Foley
185*5d676197SRobert Foley    def __getattr__(self, name):
186*5d676197SRobert Foley        # Support direct access to config by key.
187*5d676197SRobert Foley        # for example, access self._config['cpu'] by self.cpu
188*5d676197SRobert Foley        if name.lower() in self._config.keys():
189*5d676197SRobert Foley            return self._config[name.lower()]
190*5d676197SRobert Foley        return object.__getattribute__(self, name)
191*5d676197SRobert Foley
1925b4b4865SAlex Bennée    def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
193ff2ebff0SFam Zheng        def check_sha256sum(fname):
194ff2ebff0SFam Zheng            if not sha256sum:
195ff2ebff0SFam Zheng                return True
196ff2ebff0SFam Zheng            checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
1973ace9be6SGerd Hoffmann            return sha256sum == checksum.decode("utf-8")
198ff2ebff0SFam Zheng
1995b4b4865SAlex Bennée        def check_sha512sum(fname):
2005b4b4865SAlex Bennée            if not sha512sum:
2015b4b4865SAlex Bennée                return True
2025b4b4865SAlex Bennée            checksum = subprocess.check_output(["sha512sum", fname]).split()[0]
2035b4b4865SAlex Bennée            return sha512sum == checksum.decode("utf-8")
2045b4b4865SAlex Bennée
205ff2ebff0SFam Zheng        cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
206ff2ebff0SFam Zheng        if not os.path.exists(cache_dir):
207ff2ebff0SFam Zheng            os.makedirs(cache_dir)
2083ace9be6SGerd Hoffmann        fname = os.path.join(cache_dir,
2093ace9be6SGerd Hoffmann                             hashlib.sha1(url.encode("utf-8")).hexdigest())
2105b4b4865SAlex Bennée        if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname):
211ff2ebff0SFam Zheng            return fname
212ff2ebff0SFam Zheng        logging.debug("Downloading %s to %s...", url, fname)
213ff2ebff0SFam Zheng        subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
214ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
215ff2ebff0SFam Zheng        os.rename(fname + ".download", fname)
216ff2ebff0SFam Zheng        return fname
217ff2ebff0SFam Zheng
218796471e9SGerd Hoffmann    def _ssh_do(self, user, cmd, check):
21989adc5b9SRobert Foley        ssh_cmd = ["ssh",
22089adc5b9SRobert Foley                   "-t",
221ff2ebff0SFam Zheng                   "-o", "StrictHostKeyChecking=no",
222ff2ebff0SFam Zheng                   "-o", "UserKnownHostsFile=" + os.devnull,
223*5d676197SRobert Foley                   "-o",
224*5d676197SRobert Foley                   "ConnectTimeout={}".format(self._config["ssh_timeout"]),
225*5d676197SRobert Foley                   "-p", self.ssh_port, "-i", self._ssh_tmp_key_file]
22689adc5b9SRobert Foley        # If not in debug mode, set ssh to quiet mode to
22789adc5b9SRobert Foley        # avoid printing the results of commands.
22889adc5b9SRobert Foley        if not self.debug:
22989adc5b9SRobert Foley            ssh_cmd.append("-q")
230b08ba163SGerd Hoffmann        for var in self.envvars:
231b08ba163SGerd Hoffmann            ssh_cmd += ['-o', "SendEnv=%s" % var ]
232ff2ebff0SFam Zheng        assert not isinstance(cmd, str)
233ff2ebff0SFam Zheng        ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
234ff2ebff0SFam Zheng        logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
235726c9a3bSFam Zheng        r = subprocess.call(ssh_cmd)
236ff2ebff0SFam Zheng        if check and r != 0:
237ff2ebff0SFam Zheng            raise Exception("SSH command failed: %s" % cmd)
238ff2ebff0SFam Zheng        return r
239ff2ebff0SFam Zheng
240ff2ebff0SFam Zheng    def ssh(self, *cmd):
241ff2ebff0SFam Zheng        return self._ssh_do(self.GUEST_USER, cmd, False)
242ff2ebff0SFam Zheng
243ff2ebff0SFam Zheng    def ssh_root(self, *cmd):
244ff2ebff0SFam Zheng        return self._ssh_do("root", cmd, False)
245ff2ebff0SFam Zheng
246ff2ebff0SFam Zheng    def ssh_check(self, *cmd):
247ff2ebff0SFam Zheng        self._ssh_do(self.GUEST_USER, cmd, True)
248ff2ebff0SFam Zheng
249ff2ebff0SFam Zheng    def ssh_root_check(self, *cmd):
250ff2ebff0SFam Zheng        self._ssh_do("root", cmd, True)
251ff2ebff0SFam Zheng
252ff2ebff0SFam Zheng    def build_image(self, img):
253ff2ebff0SFam Zheng        raise NotImplementedError
254ff2ebff0SFam Zheng
2551e48931cSWainer dos Santos Moschetta    def exec_qemu_img(self, *args):
2561e48931cSWainer dos Santos Moschetta        cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
2571e48931cSWainer dos Santos Moschetta        cmd.extend(list(args))
2581e48931cSWainer dos Santos Moschetta        subprocess.check_call(cmd)
2591e48931cSWainer dos Santos Moschetta
260ff2ebff0SFam Zheng    def add_source_dir(self, src_dir):
2613ace9be6SGerd Hoffmann        name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
262ff2ebff0SFam Zheng        tarfile = os.path.join(self._tmpdir, name + ".tar")
263ff2ebff0SFam Zheng        logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
264ff2ebff0SFam Zheng        subprocess.check_call(["./scripts/archive-source.sh", tarfile],
265ff2ebff0SFam Zheng                              cwd=src_dir, stdin=self._devnull,
266ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
267ff2ebff0SFam Zheng        self._data_args += ["-drive",
268ff2ebff0SFam Zheng                            "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
269ff2ebff0SFam Zheng                                    (tarfile, name),
270ff2ebff0SFam Zheng                            "-device",
271ff2ebff0SFam Zheng                            "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
272ff2ebff0SFam Zheng
273ff2ebff0SFam Zheng    def boot(self, img, extra_args=[]):
274*5d676197SRobert Foley        boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
275*5d676197SRobert Foley        boot_params = boot_dev.format(img)
276*5d676197SRobert Foley        args = self._args + boot_params.split(' ')
277*5d676197SRobert Foley        args += self._data_args + extra_args + self._config['extra_args']
278ff2ebff0SFam Zheng        logging.debug("QEMU args: %s", " ".join(args))
279e56c4504SRobert Foley        qemu_path = get_qemu_path(self.arch, self._build_path)
280e56c4504SRobert Foley        guest = QEMUMachine(binary=qemu_path, args=args)
281*5d676197SRobert Foley        guest.set_machine(self._config['machine'])
2828dd38334SGerd Hoffmann        guest.set_console()
283ff2ebff0SFam Zheng        try:
284ff2ebff0SFam Zheng            guest.launch()
285ff2ebff0SFam Zheng        except:
286ff2ebff0SFam Zheng            logging.error("Failed to launch QEMU, command line:")
287e56c4504SRobert Foley            logging.error(" ".join([qemu_path] + args))
288ff2ebff0SFam Zheng            logging.error("Log:")
289ff2ebff0SFam Zheng            logging.error(guest.get_log())
290ff2ebff0SFam Zheng            logging.error("QEMU version >= 2.10 is required")
291ff2ebff0SFam Zheng            raise
292ff2ebff0SFam Zheng        atexit.register(self.shutdown)
293ff2ebff0SFam Zheng        self._guest = guest
294ff2ebff0SFam Zheng        usernet_info = guest.qmp("human-monitor-command",
295ff2ebff0SFam Zheng                                 command_line="info usernet")
296ff2ebff0SFam Zheng        self.ssh_port = None
297ff2ebff0SFam Zheng        for l in usernet_info["return"].splitlines():
298ff2ebff0SFam Zheng            fields = l.split()
299ff2ebff0SFam Zheng            if "TCP[HOST_FORWARD]" in fields and "22" in fields:
300ff2ebff0SFam Zheng                self.ssh_port = l.split()[3]
301ff2ebff0SFam Zheng        if not self.ssh_port:
302ff2ebff0SFam Zheng            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
303ff2ebff0SFam Zheng                            usernet_info)
304ff2ebff0SFam Zheng
3058dd38334SGerd Hoffmann    def console_init(self, timeout = 120):
3068dd38334SGerd Hoffmann        vm = self._guest
3078dd38334SGerd Hoffmann        vm.console_socket.settimeout(timeout)
308698a64f9SGerd Hoffmann        self.console_raw_path = os.path.join(vm._temp_dir,
309698a64f9SGerd Hoffmann                                             vm._name + "-console.raw")
310698a64f9SGerd Hoffmann        self.console_raw_file = open(self.console_raw_path, 'wb')
3118dd38334SGerd Hoffmann
3128dd38334SGerd Hoffmann    def console_log(self, text):
3138dd38334SGerd Hoffmann        for line in re.split("[\r\n]", text):
3148dd38334SGerd Hoffmann            # filter out terminal escape sequences
3158dd38334SGerd Hoffmann            line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
3168dd38334SGerd Hoffmann            line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
3178dd38334SGerd Hoffmann            # replace unprintable chars
3188dd38334SGerd Hoffmann            line = re.sub("\x1b", "<esc>", line)
3198dd38334SGerd Hoffmann            line = re.sub("[\x00-\x1f]", ".", line)
3208dd38334SGerd Hoffmann            line = re.sub("[\x80-\xff]", ".", line)
3218dd38334SGerd Hoffmann            if line == "":
3228dd38334SGerd Hoffmann                continue
3238dd38334SGerd Hoffmann            # log console line
3248dd38334SGerd Hoffmann            sys.stderr.write("con recv: %s\n" % line)
3258dd38334SGerd Hoffmann
32660136e06SGerd Hoffmann    def console_wait(self, expect, expectalt = None):
3278dd38334SGerd Hoffmann        vm = self._guest
3288dd38334SGerd Hoffmann        output = ""
3298dd38334SGerd Hoffmann        while True:
3308dd38334SGerd Hoffmann            try:
3318dd38334SGerd Hoffmann                chars = vm.console_socket.recv(1)
332698a64f9SGerd Hoffmann                if self.console_raw_file:
333698a64f9SGerd Hoffmann                    self.console_raw_file.write(chars)
334698a64f9SGerd Hoffmann                    self.console_raw_file.flush()
3358dd38334SGerd Hoffmann            except socket.timeout:
3368dd38334SGerd Hoffmann                sys.stderr.write("console: *** read timeout ***\n")
3378dd38334SGerd Hoffmann                sys.stderr.write("console: waiting for: '%s'\n" % expect)
33860136e06SGerd Hoffmann                if not expectalt is None:
33960136e06SGerd Hoffmann                    sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
3408dd38334SGerd Hoffmann                sys.stderr.write("console: line buffer:\n")
3418dd38334SGerd Hoffmann                sys.stderr.write("\n")
3428dd38334SGerd Hoffmann                self.console_log(output.rstrip())
3438dd38334SGerd Hoffmann                sys.stderr.write("\n")
3448dd38334SGerd Hoffmann                raise
3458dd38334SGerd Hoffmann            output += chars.decode("latin1")
3468dd38334SGerd Hoffmann            if expect in output:
3478dd38334SGerd Hoffmann                break
34860136e06SGerd Hoffmann            if not expectalt is None and expectalt in output:
34960136e06SGerd Hoffmann                break
3508dd38334SGerd Hoffmann            if "\r" in output or "\n" in output:
3518dd38334SGerd Hoffmann                lines = re.split("[\r\n]", output)
3528dd38334SGerd Hoffmann                output = lines.pop()
3538dd38334SGerd Hoffmann                if self.debug:
3548dd38334SGerd Hoffmann                    self.console_log("\n".join(lines))
3558dd38334SGerd Hoffmann        if self.debug:
3568dd38334SGerd Hoffmann            self.console_log(output)
35760136e06SGerd Hoffmann        if not expectalt is None and expectalt in output:
35860136e06SGerd Hoffmann            return False
35960136e06SGerd Hoffmann        return True
3608dd38334SGerd Hoffmann
3616c4f0416SGerd Hoffmann    def console_consume(self):
3626c4f0416SGerd Hoffmann        vm = self._guest
3636c4f0416SGerd Hoffmann        output = ""
3646c4f0416SGerd Hoffmann        vm.console_socket.setblocking(0)
3656c4f0416SGerd Hoffmann        while True:
3666c4f0416SGerd Hoffmann            try:
3676c4f0416SGerd Hoffmann                chars = vm.console_socket.recv(1)
3686c4f0416SGerd Hoffmann            except:
3696c4f0416SGerd Hoffmann                break
3706c4f0416SGerd Hoffmann            output += chars.decode("latin1")
3716c4f0416SGerd Hoffmann            if "\r" in output or "\n" in output:
3726c4f0416SGerd Hoffmann                lines = re.split("[\r\n]", output)
3736c4f0416SGerd Hoffmann                output = lines.pop()
3746c4f0416SGerd Hoffmann                if self.debug:
3756c4f0416SGerd Hoffmann                    self.console_log("\n".join(lines))
3766c4f0416SGerd Hoffmann        if self.debug:
3776c4f0416SGerd Hoffmann            self.console_log(output)
3786c4f0416SGerd Hoffmann        vm.console_socket.setblocking(1)
3796c4f0416SGerd Hoffmann
3808dd38334SGerd Hoffmann    def console_send(self, command):
3818dd38334SGerd Hoffmann        vm = self._guest
3828dd38334SGerd Hoffmann        if self.debug:
3838dd38334SGerd Hoffmann            logline = re.sub("\n", "<enter>", command)
3848dd38334SGerd Hoffmann            logline = re.sub("[\x00-\x1f]", ".", logline)
3858dd38334SGerd Hoffmann            sys.stderr.write("con send: %s\n" % logline)
3868dd38334SGerd Hoffmann        for char in list(command):
3878dd38334SGerd Hoffmann            vm.console_socket.send(char.encode("utf-8"))
3888dd38334SGerd Hoffmann            time.sleep(0.01)
3898dd38334SGerd Hoffmann
3908dd38334SGerd Hoffmann    def console_wait_send(self, wait, command):
3918dd38334SGerd Hoffmann        self.console_wait(wait)
3928dd38334SGerd Hoffmann        self.console_send(command)
3938dd38334SGerd Hoffmann
3948dd38334SGerd Hoffmann    def console_ssh_init(self, prompt, user, pw):
395*5d676197SRobert Foley        sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
396*5d676197SRobert Foley                     % self._config['ssh_pub_key'].rstrip()
3978dd38334SGerd Hoffmann        self.console_wait_send("login:",    "%s\n" % user)
3988dd38334SGerd Hoffmann        self.console_wait_send("Password:", "%s\n" % pw)
3998dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "mkdir .ssh\n")
4008dd38334SGerd Hoffmann        self.console_wait_send(prompt,      sshkey_cmd)
4018dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 755 .ssh\n")
4028dd38334SGerd Hoffmann        self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
4038dd38334SGerd Hoffmann
4048dd38334SGerd Hoffmann    def console_sshd_config(self, prompt):
4058dd38334SGerd Hoffmann        self.console_wait(prompt)
4068dd38334SGerd Hoffmann        self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
4078dd38334SGerd Hoffmann        for var in self.envvars:
4088dd38334SGerd Hoffmann            self.console_wait(prompt)
4098dd38334SGerd Hoffmann            self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
4108dd38334SGerd Hoffmann
4118dd38334SGerd Hoffmann    def print_step(self, text):
4128dd38334SGerd Hoffmann        sys.stderr.write("### %s ...\n" % text)
4138dd38334SGerd Hoffmann
4146ee982c9SRobert Foley    def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
415c9de3935SRobert Foley        # Allow more time for VM to boot under TCG.
416c9de3935SRobert Foley        if not kvm_available(self.arch):
417c9de3935SRobert Foley            seconds *= self.tcg_ssh_timeout_multiplier
418ff2ebff0SFam Zheng        starttime = datetime.datetime.now()
419f5d3d218SPhilippe Mathieu-Daudé        endtime = starttime + datetime.timedelta(seconds=seconds)
4206ee982c9SRobert Foley        cmd_success = False
421f5d3d218SPhilippe Mathieu-Daudé        while datetime.datetime.now() < endtime:
4226ee982c9SRobert Foley            if wait_root and self.ssh_root(cmd) == 0:
4236ee982c9SRobert Foley                cmd_success = True
424fbb3aa29SRobert Foley                break
4256ee982c9SRobert Foley            elif self.ssh(cmd) == 0:
4266ee982c9SRobert Foley                cmd_success = True
427ff2ebff0SFam Zheng                break
428f5d3d218SPhilippe Mathieu-Daudé            seconds = (endtime - datetime.datetime.now()).total_seconds()
429f5d3d218SPhilippe Mathieu-Daudé            logging.debug("%ds before timeout", seconds)
430ff2ebff0SFam Zheng            time.sleep(1)
4316ee982c9SRobert Foley        if not cmd_success:
432ff2ebff0SFam Zheng            raise Exception("Timeout while waiting for guest ssh")
433ff2ebff0SFam Zheng
434ff2ebff0SFam Zheng    def shutdown(self):
435ff2ebff0SFam Zheng        self._guest.shutdown()
436ff2ebff0SFam Zheng
437ff2ebff0SFam Zheng    def wait(self):
438ff2ebff0SFam Zheng        self._guest.wait()
439ff2ebff0SFam Zheng
440b3f94b2fSGerd Hoffmann    def graceful_shutdown(self):
441b3f94b2fSGerd Hoffmann        self.ssh_root(self.poweroff)
442b3f94b2fSGerd Hoffmann        self._guest.wait()
443b3f94b2fSGerd Hoffmann
444ff2ebff0SFam Zheng    def qmp(self, *args, **kwargs):
445ff2ebff0SFam Zheng        return self._guest.qmp(*args, **kwargs)
446ff2ebff0SFam Zheng
447b081986cSRobert Foley    def gen_cloud_init_iso(self):
448b081986cSRobert Foley        cidir = self._tmpdir
449b081986cSRobert Foley        mdata = open(os.path.join(cidir, "meta-data"), "w")
450b081986cSRobert Foley        name = self.name.replace(".","-")
451b081986cSRobert Foley        mdata.writelines(["instance-id: {}-vm-0\n".format(name),
452b081986cSRobert Foley                          "local-hostname: {}-guest\n".format(name)])
453b081986cSRobert Foley        mdata.close()
454b081986cSRobert Foley        udata = open(os.path.join(cidir, "user-data"), "w")
455*5d676197SRobert Foley        print("guest user:pw {}:{}".format(self._config['guest_user'],
456*5d676197SRobert Foley                                           self._config['guest_pass']))
457b081986cSRobert Foley        udata.writelines(["#cloud-config\n",
458b081986cSRobert Foley                          "chpasswd:\n",
459b081986cSRobert Foley                          "  list: |\n",
460*5d676197SRobert Foley                          "    root:%s\n" % self._config['root_pass'],
461*5d676197SRobert Foley                          "    %s:%s\n" % (self._config['guest_user'],
462*5d676197SRobert Foley                                           self._config['guest_pass']),
463b081986cSRobert Foley                          "  expire: False\n",
464b081986cSRobert Foley                          "users:\n",
465*5d676197SRobert Foley                          "  - name: %s\n" % self._config['guest_user'],
466b081986cSRobert Foley                          "    sudo: ALL=(ALL) NOPASSWD:ALL\n",
467b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
468*5d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
469b081986cSRobert Foley                          "  - name: root\n",
470b081986cSRobert Foley                          "    ssh-authorized-keys:\n",
471*5d676197SRobert Foley                          "    - %s\n" % self._config['ssh_pub_key'],
472b081986cSRobert Foley                          "locale: en_US.UTF-8\n"])
473b081986cSRobert Foley        proxy = os.environ.get("http_proxy")
474b081986cSRobert Foley        if not proxy is None:
475b081986cSRobert Foley            udata.writelines(["apt:\n",
476b081986cSRobert Foley                              "  proxy: %s" % proxy])
477b081986cSRobert Foley        udata.close()
47892fecad3SAlex Bennée        subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
479b081986cSRobert Foley                               "-volid", "cidata", "-joliet", "-rock",
480b081986cSRobert Foley                               "user-data", "meta-data"],
481b081986cSRobert Foley                              cwd=cidir,
482b081986cSRobert Foley                              stdin=self._devnull, stdout=self._stdout,
483b081986cSRobert Foley                              stderr=self._stdout)
484b081986cSRobert Foley
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
50063a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls):
5018a6e007eSPhilippe Mathieu-Daudé
5028a6e007eSPhilippe Mathieu-Daudé    def get_default_jobs():
50363a24c5eSPhilippe Mathieu-Daudé        if kvm_available(vmcls.arch):
5043ad3e36eSWainer dos Santos Moschetta            return multiprocessing.cpu_count() // 2
5058a6e007eSPhilippe Mathieu-Daudé        else:
5068a6e007eSPhilippe Mathieu-Daudé            return 1
5078a6e007eSPhilippe Mathieu-Daudé
508ff2ebff0SFam Zheng    parser = optparse.OptionParser(
509ff2ebff0SFam Zheng        description="VM test utility.  Exit codes: "
510ff2ebff0SFam Zheng                    "0 = success, "
511ff2ebff0SFam Zheng                    "1 = command line error, "
512ff2ebff0SFam Zheng                    "2 = environment initialization failed, "
513ff2ebff0SFam Zheng                    "3 = test command failed")
514ff2ebff0SFam Zheng    parser.add_option("--debug", "-D", action="store_true",
515ff2ebff0SFam Zheng                      help="enable debug output")
51663a24c5eSPhilippe Mathieu-Daudé    parser.add_option("--image", "-i", default="%s.img" % vmcls.name,
517ff2ebff0SFam Zheng                      help="image file name")
518ff2ebff0SFam Zheng    parser.add_option("--force", "-f", action="store_true",
519ff2ebff0SFam Zheng                      help="force build image even if image exists")
5208a6e007eSPhilippe Mathieu-Daudé    parser.add_option("--jobs", type=int, default=get_default_jobs(),
521ff2ebff0SFam Zheng                      help="number of virtual CPUs")
52241e3340aSPeter Maydell    parser.add_option("--verbose", "-V", action="store_true",
52341e3340aSPeter Maydell                      help="Pass V=1 to builds within the guest")
524ff2ebff0SFam Zheng    parser.add_option("--build-image", "-b", action="store_true",
525ff2ebff0SFam Zheng                      help="build image")
526ff2ebff0SFam Zheng    parser.add_option("--build-qemu",
527ff2ebff0SFam Zheng                      help="build QEMU from source in guest")
5285c2ec9b6SAlex Bennée    parser.add_option("--build-target",
5295c2ec9b6SAlex Bennée                      help="QEMU build target", default="check")
530e56c4504SRobert Foley    parser.add_option("--build-path", default=None,
531e56c4504SRobert Foley                      help="Path of build directory, "\
532e56c4504SRobert Foley                           "for using build tree QEMU binary. ")
533ff2ebff0SFam Zheng    parser.add_option("--interactive", "-I", action="store_true",
534ff2ebff0SFam Zheng                      help="Interactively run command")
535983c2a77SFam Zheng    parser.add_option("--snapshot", "-s", action="store_true",
536983c2a77SFam Zheng                      help="run tests with a snapshot")
53792fecad3SAlex Bennée    parser.add_option("--genisoimage", default="genisoimage",
53892fecad3SAlex Bennée                      help="iso imaging tool")
539ff2ebff0SFam Zheng    parser.disable_interspersed_args()
540ff2ebff0SFam Zheng    return parser.parse_args()
541ff2ebff0SFam Zheng
542*5d676197SRobert Foleydef main(vmcls, config=None):
543ff2ebff0SFam Zheng    try:
544*5d676197SRobert Foley        if config == None:
545*5d676197SRobert Foley            config = DEFAULT_CONFIG
54663a24c5eSPhilippe Mathieu-Daudé        args, argv = parse_args(vmcls)
547ff2ebff0SFam Zheng        if not argv and not args.build_qemu and not args.build_image:
548f03868bdSEduardo Habkost            print("Nothing to do?")
549ff2ebff0SFam Zheng            return 1
550fb3b4e6dSEduardo Habkost        logging.basicConfig(level=(logging.DEBUG if args.debug
551fb3b4e6dSEduardo Habkost                                   else logging.WARN))
552*5d676197SRobert Foley        vm = vmcls(args, config=config)
553ff2ebff0SFam Zheng        if args.build_image:
554ff2ebff0SFam Zheng            if os.path.exists(args.image) and not args.force:
555ff2ebff0SFam Zheng                sys.stderr.writelines(["Image file exists: %s\n" % args.image,
556ff2ebff0SFam Zheng                                      "Use --force option to overwrite\n"])
557ff2ebff0SFam Zheng                return 1
558ff2ebff0SFam Zheng            return vm.build_image(args.image)
559ff2ebff0SFam Zheng        if args.build_qemu:
560ff2ebff0SFam Zheng            vm.add_source_dir(args.build_qemu)
561ff2ebff0SFam Zheng            cmd = [vm.BUILD_SCRIPT.format(
562ff2ebff0SFam Zheng                   configure_opts = " ".join(argv),
5633ace9be6SGerd Hoffmann                   jobs=int(args.jobs),
5645c2ec9b6SAlex Bennée                   target=args.build_target,
56541e3340aSPeter Maydell                   verbose = "V=1" if args.verbose else "")]
566ff2ebff0SFam Zheng        else:
567ff2ebff0SFam Zheng            cmd = argv
568983c2a77SFam Zheng        img = args.image
569983c2a77SFam Zheng        if args.snapshot:
570983c2a77SFam Zheng            img += ",snapshot=on"
571983c2a77SFam Zheng        vm.boot(img)
572ff2ebff0SFam Zheng        vm.wait_ssh()
573ff2ebff0SFam Zheng    except Exception as e:
574ff2ebff0SFam Zheng        if isinstance(e, SystemExit) and e.code == 0:
575ff2ebff0SFam Zheng            return 0
576ff2ebff0SFam Zheng        sys.stderr.write("Failed to prepare guest environment\n")
577ff2ebff0SFam Zheng        traceback.print_exc()
578ff2ebff0SFam Zheng        return 2
579ff2ebff0SFam Zheng
580b3f94b2fSGerd Hoffmann    exitcode = 0
581ff2ebff0SFam Zheng    if vm.ssh(*cmd) != 0:
582b3f94b2fSGerd Hoffmann        exitcode = 3
583bcc388dfSAlex Bennée    if args.interactive:
584b3f94b2fSGerd Hoffmann        vm.ssh()
585b3f94b2fSGerd Hoffmann
586b3f94b2fSGerd Hoffmann    if not args.snapshot:
587b3f94b2fSGerd Hoffmann        vm.graceful_shutdown()
588b3f94b2fSGerd Hoffmann
589b3f94b2fSGerd Hoffmann    return exitcode
590