xref: /openbmc/qemu/tests/vm/basevm.py (revision 6b699ae1be9f257478d5eca7ef65dcea270a2796)
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
20ff2ebff0SFam Zhengsys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "scripts"))
21ff2ebff0SFam Zhengfrom qemu import QEMUMachine
22ff2ebff0SFam Zhengimport subprocess
23ff2ebff0SFam Zhengimport hashlib
24ff2ebff0SFam Zhengimport optparse
25ff2ebff0SFam Zhengimport atexit
26ff2ebff0SFam Zhengimport tempfile
27ff2ebff0SFam Zhengimport shutil
28ff2ebff0SFam Zhengimport multiprocessing
29ff2ebff0SFam Zhengimport traceback
30ff2ebff0SFam Zheng
31ff2ebff0SFam ZhengSSH_KEY = open(os.path.join(os.path.dirname(__file__),
32ff2ebff0SFam Zheng               "..", "keys", "id_rsa")).read()
33ff2ebff0SFam ZhengSSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__),
34ff2ebff0SFam Zheng                   "..", "keys", "id_rsa.pub")).read()
35ff2ebff0SFam Zheng
36ff2ebff0SFam Zhengclass BaseVM(object):
37ff2ebff0SFam Zheng    GUEST_USER = "qemu"
38ff2ebff0SFam Zheng    GUEST_PASS = "qemupass"
39ff2ebff0SFam Zheng    ROOT_PASS = "qemupass"
40ff2ebff0SFam Zheng
41ff2ebff0SFam Zheng    # The script to run in the guest that builds QEMU
42ff2ebff0SFam Zheng    BUILD_SCRIPT = ""
43ff2ebff0SFam Zheng    # The guest name, to be overridden by subclasses
44ff2ebff0SFam Zheng    name = "#base"
45ff2ebff0SFam Zheng    def __init__(self, debug=False, vcpus=None):
46ff2ebff0SFam Zheng        self._guest = None
47ff2ebff0SFam Zheng        self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
48ff2ebff0SFam Zheng                                                         suffix=".tmp",
49ff2ebff0SFam Zheng                                                         dir="."))
50ff2ebff0SFam Zheng        atexit.register(shutil.rmtree, self._tmpdir)
51ff2ebff0SFam Zheng
52ff2ebff0SFam Zheng        self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa")
53ff2ebff0SFam Zheng        open(self._ssh_key_file, "w").write(SSH_KEY)
54ff2ebff0SFam Zheng        subprocess.check_call(["chmod", "600", self._ssh_key_file])
55ff2ebff0SFam Zheng
56ff2ebff0SFam Zheng        self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
57ff2ebff0SFam Zheng        open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY)
58ff2ebff0SFam Zheng
59ff2ebff0SFam Zheng        self.debug = debug
60ff2ebff0SFam Zheng        self._stderr = sys.stderr
61ff2ebff0SFam Zheng        self._devnull = open(os.devnull, "w")
62ff2ebff0SFam Zheng        if self.debug:
63ff2ebff0SFam Zheng            self._stdout = sys.stdout
64ff2ebff0SFam Zheng        else:
65ff2ebff0SFam Zheng            self._stdout = self._devnull
66ff2ebff0SFam Zheng        self._args = [ \
67eb2712f5SPeter Maydell            "-nodefaults", "-m", "4G",
68ff2ebff0SFam Zheng            "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22",
69ff2ebff0SFam Zheng            "-device", "virtio-net-pci,netdev=vnet",
70ff2ebff0SFam Zheng            "-vnc", "127.0.0.1:0,to=20",
71ff2ebff0SFam Zheng            "-serial", "file:%s" % os.path.join(self._tmpdir, "serial.out")]
72ff2ebff0SFam Zheng        if vcpus:
73ff2ebff0SFam Zheng            self._args += ["-smp", str(vcpus)]
74ff2ebff0SFam Zheng        if os.access("/dev/kvm", os.R_OK | os.W_OK):
75dcf7ea4aSPhilippe Mathieu-Daudé            self._args += ["-cpu", "host"]
76ff2ebff0SFam Zheng            self._args += ["-enable-kvm"]
77ff2ebff0SFam Zheng        else:
78ff2ebff0SFam Zheng            logging.info("KVM not available, not using -enable-kvm")
79dcf7ea4aSPhilippe Mathieu-Daudé            self._args += ["-cpu", "max"]
80ff2ebff0SFam Zheng        self._data_args = []
81ff2ebff0SFam Zheng
82ff2ebff0SFam Zheng    def _download_with_cache(self, url, sha256sum=None):
83ff2ebff0SFam Zheng        def check_sha256sum(fname):
84ff2ebff0SFam Zheng            if not sha256sum:
85ff2ebff0SFam Zheng                return True
86ff2ebff0SFam Zheng            checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
87ff2ebff0SFam Zheng            return sha256sum == checksum
88ff2ebff0SFam Zheng
89ff2ebff0SFam Zheng        cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
90ff2ebff0SFam Zheng        if not os.path.exists(cache_dir):
91ff2ebff0SFam Zheng            os.makedirs(cache_dir)
92ff2ebff0SFam Zheng        fname = os.path.join(cache_dir, hashlib.sha1(url).hexdigest())
93ff2ebff0SFam Zheng        if os.path.exists(fname) and check_sha256sum(fname):
94ff2ebff0SFam Zheng            return fname
95ff2ebff0SFam Zheng        logging.debug("Downloading %s to %s...", url, fname)
96ff2ebff0SFam Zheng        subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
97ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
98ff2ebff0SFam Zheng        os.rename(fname + ".download", fname)
99ff2ebff0SFam Zheng        return fname
100ff2ebff0SFam Zheng
101ff2ebff0SFam Zheng    def _ssh_do(self, user, cmd, check, interactive=False):
102ff2ebff0SFam Zheng        ssh_cmd = ["ssh", "-q",
103ff2ebff0SFam Zheng                   "-o", "StrictHostKeyChecking=no",
104ff2ebff0SFam Zheng                   "-o", "UserKnownHostsFile=" + os.devnull,
105ff2ebff0SFam Zheng                   "-o", "ConnectTimeout=1",
106ff2ebff0SFam Zheng                   "-p", self.ssh_port, "-i", self._ssh_key_file]
107ff2ebff0SFam Zheng        if interactive:
108ff2ebff0SFam Zheng            ssh_cmd += ['-t']
109ff2ebff0SFam Zheng        assert not isinstance(cmd, str)
110ff2ebff0SFam Zheng        ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
111ff2ebff0SFam Zheng        logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
112726c9a3bSFam Zheng        r = subprocess.call(ssh_cmd)
113ff2ebff0SFam Zheng        if check and r != 0:
114ff2ebff0SFam Zheng            raise Exception("SSH command failed: %s" % cmd)
115ff2ebff0SFam Zheng        return r
116ff2ebff0SFam Zheng
117ff2ebff0SFam Zheng    def ssh(self, *cmd):
118ff2ebff0SFam Zheng        return self._ssh_do(self.GUEST_USER, cmd, False)
119ff2ebff0SFam Zheng
120ff2ebff0SFam Zheng    def ssh_interactive(self, *cmd):
121ff2ebff0SFam Zheng        return self._ssh_do(self.GUEST_USER, cmd, False, True)
122ff2ebff0SFam Zheng
123ff2ebff0SFam Zheng    def ssh_root(self, *cmd):
124ff2ebff0SFam Zheng        return self._ssh_do("root", cmd, False)
125ff2ebff0SFam Zheng
126ff2ebff0SFam Zheng    def ssh_check(self, *cmd):
127ff2ebff0SFam Zheng        self._ssh_do(self.GUEST_USER, cmd, True)
128ff2ebff0SFam Zheng
129ff2ebff0SFam Zheng    def ssh_root_check(self, *cmd):
130ff2ebff0SFam Zheng        self._ssh_do("root", cmd, True)
131ff2ebff0SFam Zheng
132ff2ebff0SFam Zheng    def build_image(self, img):
133ff2ebff0SFam Zheng        raise NotImplementedError
134ff2ebff0SFam Zheng
135ff2ebff0SFam Zheng    def add_source_dir(self, src_dir):
136ff2ebff0SFam Zheng        name = "data-" + hashlib.sha1(src_dir).hexdigest()[:5]
137ff2ebff0SFam Zheng        tarfile = os.path.join(self._tmpdir, name + ".tar")
138ff2ebff0SFam Zheng        logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
139ff2ebff0SFam Zheng        subprocess.check_call(["./scripts/archive-source.sh", tarfile],
140ff2ebff0SFam Zheng                              cwd=src_dir, stdin=self._devnull,
141ff2ebff0SFam Zheng                              stdout=self._stdout, stderr=self._stderr)
142ff2ebff0SFam Zheng        self._data_args += ["-drive",
143ff2ebff0SFam Zheng                            "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
144ff2ebff0SFam Zheng                                    (tarfile, name),
145ff2ebff0SFam Zheng                            "-device",
146ff2ebff0SFam Zheng                            "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
147ff2ebff0SFam Zheng
148ff2ebff0SFam Zheng    def boot(self, img, extra_args=[]):
149ff2ebff0SFam Zheng        args = self._args + [
150ff2ebff0SFam Zheng            "-device", "VGA",
151ff2ebff0SFam Zheng            "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img,
152ff2ebff0SFam Zheng            "-device", "virtio-blk,drive=drive0,bootindex=0"]
153ff2ebff0SFam Zheng        args += self._data_args + extra_args
154ff2ebff0SFam Zheng        logging.debug("QEMU args: %s", " ".join(args))
155ff2ebff0SFam Zheng        qemu_bin = os.environ.get("QEMU", "qemu-system-x86_64")
156ff2ebff0SFam Zheng        guest = QEMUMachine(binary=qemu_bin, args=args)
157ff2ebff0SFam Zheng        try:
158ff2ebff0SFam Zheng            guest.launch()
159ff2ebff0SFam Zheng        except:
160ff2ebff0SFam Zheng            logging.error("Failed to launch QEMU, command line:")
161ff2ebff0SFam Zheng            logging.error(" ".join([qemu_bin] + args))
162ff2ebff0SFam Zheng            logging.error("Log:")
163ff2ebff0SFam Zheng            logging.error(guest.get_log())
164ff2ebff0SFam Zheng            logging.error("QEMU version >= 2.10 is required")
165ff2ebff0SFam Zheng            raise
166ff2ebff0SFam Zheng        atexit.register(self.shutdown)
167ff2ebff0SFam Zheng        self._guest = guest
168ff2ebff0SFam Zheng        usernet_info = guest.qmp("human-monitor-command",
169ff2ebff0SFam Zheng                                 command_line="info usernet")
170ff2ebff0SFam Zheng        self.ssh_port = None
171ff2ebff0SFam Zheng        for l in usernet_info["return"].splitlines():
172ff2ebff0SFam Zheng            fields = l.split()
173ff2ebff0SFam Zheng            if "TCP[HOST_FORWARD]" in fields and "22" in fields:
174ff2ebff0SFam Zheng                self.ssh_port = l.split()[3]
175ff2ebff0SFam Zheng        if not self.ssh_port:
176ff2ebff0SFam Zheng            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
177ff2ebff0SFam Zheng                            usernet_info)
178ff2ebff0SFam Zheng
179*6b699ae1SPeter Maydell    def wait_ssh(self, seconds=300):
180ff2ebff0SFam Zheng        starttime = datetime.datetime.now()
181ff2ebff0SFam Zheng        guest_up = False
182ff2ebff0SFam Zheng        while (datetime.datetime.now() - starttime).total_seconds() < seconds:
183ff2ebff0SFam Zheng            if self.ssh("exit 0") == 0:
184ff2ebff0SFam Zheng                guest_up = True
185ff2ebff0SFam Zheng                break
186ff2ebff0SFam Zheng            time.sleep(1)
187ff2ebff0SFam Zheng        if not guest_up:
188ff2ebff0SFam Zheng            raise Exception("Timeout while waiting for guest ssh")
189ff2ebff0SFam Zheng
190ff2ebff0SFam Zheng    def shutdown(self):
191ff2ebff0SFam Zheng        self._guest.shutdown()
192ff2ebff0SFam Zheng
193ff2ebff0SFam Zheng    def wait(self):
194ff2ebff0SFam Zheng        self._guest.wait()
195ff2ebff0SFam Zheng
196ff2ebff0SFam Zheng    def qmp(self, *args, **kwargs):
197ff2ebff0SFam Zheng        return self._guest.qmp(*args, **kwargs)
198ff2ebff0SFam Zheng
199ff2ebff0SFam Zhengdef parse_args(vm_name):
200ff2ebff0SFam Zheng    parser = optparse.OptionParser(
201ff2ebff0SFam Zheng        description="VM test utility.  Exit codes: "
202ff2ebff0SFam Zheng                    "0 = success, "
203ff2ebff0SFam Zheng                    "1 = command line error, "
204ff2ebff0SFam Zheng                    "2 = environment initialization failed, "
205ff2ebff0SFam Zheng                    "3 = test command failed")
206ff2ebff0SFam Zheng    parser.add_option("--debug", "-D", action="store_true",
207ff2ebff0SFam Zheng                      help="enable debug output")
208ff2ebff0SFam Zheng    parser.add_option("--image", "-i", default="%s.img" % vm_name,
209ff2ebff0SFam Zheng                      help="image file name")
210ff2ebff0SFam Zheng    parser.add_option("--force", "-f", action="store_true",
211ff2ebff0SFam Zheng                      help="force build image even if image exists")
212ff2ebff0SFam Zheng    parser.add_option("--jobs", type=int, default=multiprocessing.cpu_count() / 2,
213ff2ebff0SFam Zheng                      help="number of virtual CPUs")
21441e3340aSPeter Maydell    parser.add_option("--verbose", "-V", action="store_true",
21541e3340aSPeter Maydell                      help="Pass V=1 to builds within the guest")
216ff2ebff0SFam Zheng    parser.add_option("--build-image", "-b", action="store_true",
217ff2ebff0SFam Zheng                      help="build image")
218ff2ebff0SFam Zheng    parser.add_option("--build-qemu",
219ff2ebff0SFam Zheng                      help="build QEMU from source in guest")
220ff2ebff0SFam Zheng    parser.add_option("--interactive", "-I", action="store_true",
221ff2ebff0SFam Zheng                      help="Interactively run command")
222983c2a77SFam Zheng    parser.add_option("--snapshot", "-s", action="store_true",
223983c2a77SFam Zheng                      help="run tests with a snapshot")
224ff2ebff0SFam Zheng    parser.disable_interspersed_args()
225ff2ebff0SFam Zheng    return parser.parse_args()
226ff2ebff0SFam Zheng
227ff2ebff0SFam Zhengdef main(vmcls):
228ff2ebff0SFam Zheng    try:
229ff2ebff0SFam Zheng        args, argv = parse_args(vmcls.name)
230ff2ebff0SFam Zheng        if not argv and not args.build_qemu and not args.build_image:
231f03868bdSEduardo Habkost            print("Nothing to do?")
232ff2ebff0SFam Zheng            return 1
233fb3b4e6dSEduardo Habkost        logging.basicConfig(level=(logging.DEBUG if args.debug
234fb3b4e6dSEduardo Habkost                                   else logging.WARN))
235ff2ebff0SFam Zheng        vm = vmcls(debug=args.debug, vcpus=args.jobs)
236ff2ebff0SFam Zheng        if args.build_image:
237ff2ebff0SFam Zheng            if os.path.exists(args.image) and not args.force:
238ff2ebff0SFam Zheng                sys.stderr.writelines(["Image file exists: %s\n" % args.image,
239ff2ebff0SFam Zheng                                      "Use --force option to overwrite\n"])
240ff2ebff0SFam Zheng                return 1
241ff2ebff0SFam Zheng            return vm.build_image(args.image)
242ff2ebff0SFam Zheng        if args.build_qemu:
243ff2ebff0SFam Zheng            vm.add_source_dir(args.build_qemu)
244ff2ebff0SFam Zheng            cmd = [vm.BUILD_SCRIPT.format(
245ff2ebff0SFam Zheng                   configure_opts = " ".join(argv),
24641e3340aSPeter Maydell                   jobs=args.jobs,
24741e3340aSPeter Maydell                   verbose = "V=1" if args.verbose else "")]
248ff2ebff0SFam Zheng        else:
249ff2ebff0SFam Zheng            cmd = argv
250983c2a77SFam Zheng        img = args.image
251983c2a77SFam Zheng        if args.snapshot:
252983c2a77SFam Zheng            img += ",snapshot=on"
253983c2a77SFam Zheng        vm.boot(img)
254ff2ebff0SFam Zheng        vm.wait_ssh()
255ff2ebff0SFam Zheng    except Exception as e:
256ff2ebff0SFam Zheng        if isinstance(e, SystemExit) and e.code == 0:
257ff2ebff0SFam Zheng            return 0
258ff2ebff0SFam Zheng        sys.stderr.write("Failed to prepare guest environment\n")
259ff2ebff0SFam Zheng        traceback.print_exc()
260ff2ebff0SFam Zheng        return 2
261ff2ebff0SFam Zheng
262ff2ebff0SFam Zheng    if args.interactive:
263ff2ebff0SFam Zheng        if vm.ssh_interactive(*cmd) == 0:
264ff2ebff0SFam Zheng            return 0
265ff2ebff0SFam Zheng        vm.ssh_interactive()
266ff2ebff0SFam Zheng        return 3
267ff2ebff0SFam Zheng    else:
268ff2ebff0SFam Zheng        if vm.ssh(*cmd) != 0:
269ff2ebff0SFam Zheng            return 3
270