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