xref: /openbmc/qemu/tests/vm/basevm.py (revision b3f94b2f46222f088eef898a8eb51553e25be3fa)
1ff2ebff0SFam Zheng#!/usr/bin/env python
2ff2ebff0SFam Zheng#
3ff2ebff0SFam Zheng# VM testing base class
4ff2ebff0SFam Zheng#
5ff2ebff0SFam Zheng# Copyright 2017 Red Hat Inc.
6ff2ebff0SFam Zheng#
7ff2ebff0SFam Zheng# Authors:
8ff2ebff0SFam Zheng#  Fam Zheng <famz@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
14f03868bdSEduardo Habkostfrom __future__ import print_function
15ff2ebff0SFam Zhengimport os
16ff2ebff0SFam Zhengimport sys
17ff2ebff0SFam Zhengimport logging
18ff2ebff0SFam Zhengimport time
19ff2ebff0SFam Zhengimport datetime
208f8fd9edSCleber Rosasys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
21abf0bf99SJohn Snowfrom qemu import kvm_available
22abf0bf99SJohn Snowfrom qemu.machine import QEMUMachine
23ff2ebff0SFam Zhengimport subprocess
24ff2ebff0SFam Zhengimport hashlib
25ff2ebff0SFam Zhengimport optparse
26ff2ebff0SFam Zhengimport atexit
27ff2ebff0SFam Zhengimport tempfile
28ff2ebff0SFam Zhengimport shutil
29ff2ebff0SFam Zhengimport multiprocessing
30ff2ebff0SFam Zhengimport traceback
31ff2ebff0SFam Zheng
32ff2ebff0SFam ZhengSSH_KEY = open(os.path.join(os.path.dirname(__file__),
33ff2ebff0SFam Zheng               "..", "keys", "id_rsa")).read()
34ff2ebff0SFam ZhengSSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__),
35ff2ebff0SFam Zheng                   "..", "keys", "id_rsa.pub")).read()
36ff2ebff0SFam Zheng
37ff2ebff0SFam Zhengclass BaseVM(object):
38ff2ebff0SFam Zheng    GUEST_USER = "qemu"
39ff2ebff0SFam Zheng    GUEST_PASS = "qemupass"
40ff2ebff0SFam Zheng    ROOT_PASS = "qemupass"
41ff2ebff0SFam Zheng
42b08ba163SGerd Hoffmann    envvars = [
43b08ba163SGerd Hoffmann        "https_proxy",
44b08ba163SGerd Hoffmann        "http_proxy",
45b08ba163SGerd Hoffmann        "ftp_proxy",
46b08ba163SGerd Hoffmann        "no_proxy",
47b08ba163SGerd Hoffmann    ]
48b08ba163SGerd Hoffmann
49ff2ebff0SFam Zheng    # The script to run in the guest that builds QEMU
50ff2ebff0SFam Zheng    BUILD_SCRIPT = ""
51ff2ebff0SFam Zheng    # The guest name, to be overridden by subclasses
52ff2ebff0SFam Zheng    name = "#base"
5331719c37SPhilippe Mathieu-Daudé    # The guest architecture, to be overridden by subclasses
5431719c37SPhilippe Mathieu-Daudé    arch = "#arch"
55*b3f94b2fSGerd Hoffmann    # command to halt the guest, can be overridden by subclasses
56*b3f94b2fSGerd Hoffmann    poweroff = "poweroff"
57ff2ebff0SFam Zheng    def __init__(self, debug=False, vcpus=None):
58ff2ebff0SFam Zheng        self._guest = None
59ff2ebff0SFam Zheng        self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
60ff2ebff0SFam Zheng                                                         suffix=".tmp",
61ff2ebff0SFam Zheng                                                         dir="."))
62ff2ebff0SFam Zheng        atexit.register(shutil.rmtree, self._tmpdir)
63ff2ebff0SFam Zheng
64ff2ebff0SFam Zheng        self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa")
65ff2ebff0SFam Zheng        open(self._ssh_key_file, "w").write(SSH_KEY)
66ff2ebff0SFam Zheng        subprocess.check_call(["chmod", "600", self._ssh_key_file])
67ff2ebff0SFam Zheng
68ff2ebff0SFam Zheng        self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
69ff2ebff0SFam Zheng        open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY)
70ff2ebff0SFam Zheng
71ff2ebff0SFam Zheng        self.debug = debug
72ff2ebff0SFam Zheng        self._stderr = sys.stderr
73ff2ebff0SFam Zheng        self._devnull = open(os.devnull, "w")
74ff2ebff0SFam Zheng        if self.debug:
75ff2ebff0SFam Zheng            self._stdout = sys.stdout
76ff2ebff0SFam Zheng        else:
77ff2ebff0SFam Zheng            self._stdout = self._devnull
78ff2ebff0SFam Zheng        self._args = [ \
79eb2712f5SPeter Maydell            "-nodefaults", "-m", "4G",
80b33bd859SPeter Maydell            "-cpu", "max",
81ff2ebff0SFam Zheng            "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22",
82ff2ebff0SFam Zheng            "-device", "virtio-net-pci,netdev=vnet",
83ff2ebff0SFam Zheng            "-vnc", "127.0.0.1:0,to=20",
84ff2ebff0SFam Zheng            "-serial", "file:%s" % os.path.join(self._tmpdir, "serial.out")]
85071cf5a4SPhilippe Mathieu-Daudé        if vcpus and vcpus > 1:
863ace9be6SGerd Hoffmann            self._args += ["-smp", "%d" % vcpus]
8771531bb5SPhilippe Mathieu-Daudé        if kvm_available(self.arch):
88ff2ebff0SFam Zheng            self._args += ["-enable-kvm"]
89ff2ebff0SFam Zheng        else:
90ff2ebff0SFam Zheng            logging.info("KVM not available, not using -enable-kvm")
91ff2ebff0SFam Zheng        self._data_args = []
92ff2ebff0SFam Zheng
93ff2ebff0SFam Zheng    def _download_with_cache(self, url, sha256sum=None):
94ff2ebff0SFam Zheng        def check_sha256sum(fname):
95ff2ebff0SFam Zheng            if not sha256sum:
96ff2ebff0SFam Zheng                return True
97ff2ebff0SFam Zheng            checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
983ace9be6SGerd Hoffmann            return sha256sum == checksum.decode("utf-8")
99ff2ebff0SFam Zheng
100ff2ebff0SFam Zheng        cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
101ff2ebff0SFam Zheng        if not os.path.exists(cache_dir):
102ff2ebff0SFam Zheng            os.makedirs(cache_dir)
1033ace9be6SGerd Hoffmann        fname = os.path.join(cache_dir,
1043ace9be6SGerd Hoffmann                             hashlib.sha1(url.encode("utf-8")).hexdigest())
105ff2ebff0SFam Zheng        if os.path.exists(fname) and check_sha256sum(fname):
106ff2ebff0SFam Zheng            return fname
107ff2ebff0SFam Zheng        logging.debug("Downloading %s to %s...", url, fname)
108ff2ebff0SFam Zheng        subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
109ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
110ff2ebff0SFam Zheng        os.rename(fname + ".download", fname)
111ff2ebff0SFam Zheng        return fname
112ff2ebff0SFam Zheng
113796471e9SGerd Hoffmann    def _ssh_do(self, user, cmd, check):
114796471e9SGerd Hoffmann        ssh_cmd = ["ssh", "-q", "-t",
115ff2ebff0SFam Zheng                   "-o", "StrictHostKeyChecking=no",
116ff2ebff0SFam Zheng                   "-o", "UserKnownHostsFile=" + os.devnull,
117ff2ebff0SFam Zheng                   "-o", "ConnectTimeout=1",
118ff2ebff0SFam Zheng                   "-p", self.ssh_port, "-i", self._ssh_key_file]
119b08ba163SGerd Hoffmann        for var in self.envvars:
120b08ba163SGerd Hoffmann            ssh_cmd += ['-o', "SendEnv=%s" % var ]
121ff2ebff0SFam Zheng        assert not isinstance(cmd, str)
122ff2ebff0SFam Zheng        ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
123ff2ebff0SFam Zheng        logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
124726c9a3bSFam Zheng        r = subprocess.call(ssh_cmd)
125ff2ebff0SFam Zheng        if check and r != 0:
126ff2ebff0SFam Zheng            raise Exception("SSH command failed: %s" % cmd)
127ff2ebff0SFam Zheng        return r
128ff2ebff0SFam Zheng
129ff2ebff0SFam Zheng    def ssh(self, *cmd):
130ff2ebff0SFam Zheng        return self._ssh_do(self.GUEST_USER, cmd, False)
131ff2ebff0SFam Zheng
132ff2ebff0SFam Zheng    def ssh_root(self, *cmd):
133ff2ebff0SFam Zheng        return self._ssh_do("root", cmd, False)
134ff2ebff0SFam Zheng
135ff2ebff0SFam Zheng    def ssh_check(self, *cmd):
136ff2ebff0SFam Zheng        self._ssh_do(self.GUEST_USER, cmd, True)
137ff2ebff0SFam Zheng
138ff2ebff0SFam Zheng    def ssh_root_check(self, *cmd):
139ff2ebff0SFam Zheng        self._ssh_do("root", cmd, True)
140ff2ebff0SFam Zheng
141ff2ebff0SFam Zheng    def build_image(self, img):
142ff2ebff0SFam Zheng        raise NotImplementedError
143ff2ebff0SFam Zheng
144ff2ebff0SFam Zheng    def add_source_dir(self, src_dir):
1453ace9be6SGerd Hoffmann        name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
146ff2ebff0SFam Zheng        tarfile = os.path.join(self._tmpdir, name + ".tar")
147ff2ebff0SFam Zheng        logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
148ff2ebff0SFam Zheng        subprocess.check_call(["./scripts/archive-source.sh", tarfile],
149ff2ebff0SFam Zheng                              cwd=src_dir, stdin=self._devnull,
150ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
151ff2ebff0SFam Zheng        self._data_args += ["-drive",
152ff2ebff0SFam Zheng                            "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
153ff2ebff0SFam Zheng                                    (tarfile, name),
154ff2ebff0SFam Zheng                            "-device",
155ff2ebff0SFam Zheng                            "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
156ff2ebff0SFam Zheng
157ff2ebff0SFam Zheng    def boot(self, img, extra_args=[]):
158ff2ebff0SFam Zheng        args = self._args + [
159ff2ebff0SFam Zheng            "-device", "VGA",
160ff2ebff0SFam Zheng            "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img,
161ff2ebff0SFam Zheng            "-device", "virtio-blk,drive=drive0,bootindex=0"]
162ff2ebff0SFam Zheng        args += self._data_args + extra_args
163ff2ebff0SFam Zheng        logging.debug("QEMU args: %s", " ".join(args))
16431719c37SPhilippe Mathieu-Daudé        qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch)
165ff2ebff0SFam Zheng        guest = QEMUMachine(binary=qemu_bin, args=args)
166ff2ebff0SFam Zheng        try:
167ff2ebff0SFam Zheng            guest.launch()
168ff2ebff0SFam Zheng        except:
169ff2ebff0SFam Zheng            logging.error("Failed to launch QEMU, command line:")
170ff2ebff0SFam Zheng            logging.error(" ".join([qemu_bin] + args))
171ff2ebff0SFam Zheng            logging.error("Log:")
172ff2ebff0SFam Zheng            logging.error(guest.get_log())
173ff2ebff0SFam Zheng            logging.error("QEMU version >= 2.10 is required")
174ff2ebff0SFam Zheng            raise
175ff2ebff0SFam Zheng        atexit.register(self.shutdown)
176ff2ebff0SFam Zheng        self._guest = guest
177ff2ebff0SFam Zheng        usernet_info = guest.qmp("human-monitor-command",
178ff2ebff0SFam Zheng                                 command_line="info usernet")
179ff2ebff0SFam Zheng        self.ssh_port = None
180ff2ebff0SFam Zheng        for l in usernet_info["return"].splitlines():
181ff2ebff0SFam Zheng            fields = l.split()
182ff2ebff0SFam Zheng            if "TCP[HOST_FORWARD]" in fields and "22" in fields:
183ff2ebff0SFam Zheng                self.ssh_port = l.split()[3]
184ff2ebff0SFam Zheng        if not self.ssh_port:
185ff2ebff0SFam Zheng            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
186ff2ebff0SFam Zheng                            usernet_info)
187ff2ebff0SFam Zheng
1886b699ae1SPeter Maydell    def wait_ssh(self, seconds=300):
189ff2ebff0SFam Zheng        starttime = datetime.datetime.now()
190f5d3d218SPhilippe Mathieu-Daudé        endtime = starttime + datetime.timedelta(seconds=seconds)
191ff2ebff0SFam Zheng        guest_up = False
192f5d3d218SPhilippe Mathieu-Daudé        while datetime.datetime.now() < endtime:
193ff2ebff0SFam Zheng            if self.ssh("exit 0") == 0:
194ff2ebff0SFam Zheng                guest_up = True
195ff2ebff0SFam Zheng                break
196f5d3d218SPhilippe Mathieu-Daudé            seconds = (endtime - datetime.datetime.now()).total_seconds()
197f5d3d218SPhilippe Mathieu-Daudé            logging.debug("%ds before timeout", seconds)
198ff2ebff0SFam Zheng            time.sleep(1)
199ff2ebff0SFam Zheng        if not guest_up:
200ff2ebff0SFam Zheng            raise Exception("Timeout while waiting for guest ssh")
201ff2ebff0SFam Zheng
202ff2ebff0SFam Zheng    def shutdown(self):
203ff2ebff0SFam Zheng        self._guest.shutdown()
204ff2ebff0SFam Zheng
205ff2ebff0SFam Zheng    def wait(self):
206ff2ebff0SFam Zheng        self._guest.wait()
207ff2ebff0SFam Zheng
208*b3f94b2fSGerd Hoffmann    def graceful_shutdown(self):
209*b3f94b2fSGerd Hoffmann        self.ssh_root(self.poweroff)
210*b3f94b2fSGerd Hoffmann        self._guest.wait()
211*b3f94b2fSGerd Hoffmann
212ff2ebff0SFam Zheng    def qmp(self, *args, **kwargs):
213ff2ebff0SFam Zheng        return self._guest.qmp(*args, **kwargs)
214ff2ebff0SFam Zheng
21563a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls):
2168a6e007eSPhilippe Mathieu-Daudé
2178a6e007eSPhilippe Mathieu-Daudé    def get_default_jobs():
21863a24c5eSPhilippe Mathieu-Daudé        if kvm_available(vmcls.arch):
2193ad3e36eSWainer dos Santos Moschetta            return multiprocessing.cpu_count() // 2
2208a6e007eSPhilippe Mathieu-Daudé        else:
2218a6e007eSPhilippe Mathieu-Daudé            return 1
2228a6e007eSPhilippe Mathieu-Daudé
223ff2ebff0SFam Zheng    parser = optparse.OptionParser(
224ff2ebff0SFam Zheng        description="VM test utility.  Exit codes: "
225ff2ebff0SFam Zheng                    "0 = success, "
226ff2ebff0SFam Zheng                    "1 = command line error, "
227ff2ebff0SFam Zheng                    "2 = environment initialization failed, "
228ff2ebff0SFam Zheng                    "3 = test command failed")
229ff2ebff0SFam Zheng    parser.add_option("--debug", "-D", action="store_true",
230ff2ebff0SFam Zheng                      help="enable debug output")
23163a24c5eSPhilippe Mathieu-Daudé    parser.add_option("--image", "-i", default="%s.img" % vmcls.name,
232ff2ebff0SFam Zheng                      help="image file name")
233ff2ebff0SFam Zheng    parser.add_option("--force", "-f", action="store_true",
234ff2ebff0SFam Zheng                      help="force build image even if image exists")
2358a6e007eSPhilippe Mathieu-Daudé    parser.add_option("--jobs", type=int, default=get_default_jobs(),
236ff2ebff0SFam Zheng                      help="number of virtual CPUs")
23741e3340aSPeter Maydell    parser.add_option("--verbose", "-V", action="store_true",
23841e3340aSPeter Maydell                      help="Pass V=1 to builds within the guest")
239ff2ebff0SFam Zheng    parser.add_option("--build-image", "-b", action="store_true",
240ff2ebff0SFam Zheng                      help="build image")
241ff2ebff0SFam Zheng    parser.add_option("--build-qemu",
242ff2ebff0SFam Zheng                      help="build QEMU from source in guest")
2435c2ec9b6SAlex Bennée    parser.add_option("--build-target",
2445c2ec9b6SAlex Bennée                      help="QEMU build target", default="check")
245ff2ebff0SFam Zheng    parser.add_option("--interactive", "-I", action="store_true",
246ff2ebff0SFam Zheng                      help="Interactively run command")
247983c2a77SFam Zheng    parser.add_option("--snapshot", "-s", action="store_true",
248983c2a77SFam Zheng                      help="run tests with a snapshot")
249ff2ebff0SFam Zheng    parser.disable_interspersed_args()
250ff2ebff0SFam Zheng    return parser.parse_args()
251ff2ebff0SFam Zheng
252ff2ebff0SFam Zhengdef main(vmcls):
253ff2ebff0SFam Zheng    try:
25463a24c5eSPhilippe Mathieu-Daudé        args, argv = parse_args(vmcls)
255ff2ebff0SFam Zheng        if not argv and not args.build_qemu and not args.build_image:
256f03868bdSEduardo Habkost            print("Nothing to do?")
257ff2ebff0SFam Zheng            return 1
258fb3b4e6dSEduardo Habkost        logging.basicConfig(level=(logging.DEBUG if args.debug
259fb3b4e6dSEduardo Habkost                                   else logging.WARN))
260ff2ebff0SFam Zheng        vm = vmcls(debug=args.debug, vcpus=args.jobs)
261ff2ebff0SFam Zheng        if args.build_image:
262ff2ebff0SFam Zheng            if os.path.exists(args.image) and not args.force:
263ff2ebff0SFam Zheng                sys.stderr.writelines(["Image file exists: %s\n" % args.image,
264ff2ebff0SFam Zheng                                      "Use --force option to overwrite\n"])
265ff2ebff0SFam Zheng                return 1
266ff2ebff0SFam Zheng            return vm.build_image(args.image)
267ff2ebff0SFam Zheng        if args.build_qemu:
268ff2ebff0SFam Zheng            vm.add_source_dir(args.build_qemu)
269ff2ebff0SFam Zheng            cmd = [vm.BUILD_SCRIPT.format(
270ff2ebff0SFam Zheng                   configure_opts = " ".join(argv),
2713ace9be6SGerd Hoffmann                   jobs=int(args.jobs),
2725c2ec9b6SAlex Bennée                   target=args.build_target,
27341e3340aSPeter Maydell                   verbose = "V=1" if args.verbose else "")]
274ff2ebff0SFam Zheng        else:
275ff2ebff0SFam Zheng            cmd = argv
276983c2a77SFam Zheng        img = args.image
277983c2a77SFam Zheng        if args.snapshot:
278983c2a77SFam Zheng            img += ",snapshot=on"
279983c2a77SFam Zheng        vm.boot(img)
280ff2ebff0SFam Zheng        vm.wait_ssh()
281ff2ebff0SFam Zheng    except Exception as e:
282ff2ebff0SFam Zheng        if isinstance(e, SystemExit) and e.code == 0:
283ff2ebff0SFam Zheng            return 0
284ff2ebff0SFam Zheng        sys.stderr.write("Failed to prepare guest environment\n")
285ff2ebff0SFam Zheng        traceback.print_exc()
286ff2ebff0SFam Zheng        return 2
287ff2ebff0SFam Zheng
288*b3f94b2fSGerd Hoffmann    exitcode = 0
289ff2ebff0SFam Zheng    if vm.ssh(*cmd) != 0:
290*b3f94b2fSGerd Hoffmann        exitcode = 3
291*b3f94b2fSGerd Hoffmann    if exitcode != 0 and args.interactive:
292*b3f94b2fSGerd Hoffmann        vm.ssh()
293*b3f94b2fSGerd Hoffmann
294*b3f94b2fSGerd Hoffmann    if not args.snapshot:
295*b3f94b2fSGerd Hoffmann        vm.graceful_shutdown()
296*b3f94b2fSGerd Hoffmann
297*b3f94b2fSGerd Hoffmann    return exitcode
298