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