]> Git Repo - qemu.git/blob - tests/vm/basevm.py
tests/vm: Allow to set qemu-img path
[qemu.git] / tests / vm / basevm.py
1 #!/usr/bin/env python
2 #
3 # VM testing base class
4 #
5 # Copyright 2017-2019 Red Hat Inc.
6 #
7 # Authors:
8 #  Fam Zheng <[email protected]>
9 #  Gerd Hoffmann <[email protected]>
10 #
11 # This code is licensed under the GPL version 2 or later.  See
12 # the COPYING file in the top-level directory.
13 #
14
15 from __future__ import print_function
16 import os
17 import re
18 import sys
19 import socket
20 import logging
21 import time
22 import datetime
23 sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
24 from qemu.accel import kvm_available
25 from qemu.machine import QEMUMachine
26 import subprocess
27 import hashlib
28 import optparse
29 import atexit
30 import tempfile
31 import shutil
32 import multiprocessing
33 import traceback
34
35 SSH_KEY = open(os.path.join(os.path.dirname(__file__),
36                "..", "keys", "id_rsa")).read()
37 SSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__),
38                    "..", "keys", "id_rsa.pub")).read()
39
40 class BaseVM(object):
41     GUEST_USER = "qemu"
42     GUEST_PASS = "qemupass"
43     ROOT_PASS = "qemupass"
44
45     envvars = [
46         "https_proxy",
47         "http_proxy",
48         "ftp_proxy",
49         "no_proxy",
50     ]
51
52     # The script to run in the guest that builds QEMU
53     BUILD_SCRIPT = ""
54     # The guest name, to be overridden by subclasses
55     name = "#base"
56     # The guest architecture, to be overridden by subclasses
57     arch = "#arch"
58     # command to halt the guest, can be overridden by subclasses
59     poweroff = "poweroff"
60     # enable IPv6 networking
61     ipv6 = True
62     def __init__(self, debug=False, vcpus=None):
63         self._guest = None
64         self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
65                                                          suffix=".tmp",
66                                                          dir="."))
67         atexit.register(shutil.rmtree, self._tmpdir)
68
69         self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa")
70         open(self._ssh_key_file, "w").write(SSH_KEY)
71         subprocess.check_call(["chmod", "600", self._ssh_key_file])
72
73         self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
74         open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY)
75
76         self.debug = debug
77         self._stderr = sys.stderr
78         self._devnull = open(os.devnull, "w")
79         if self.debug:
80             self._stdout = sys.stdout
81         else:
82             self._stdout = self._devnull
83         self._args = [ \
84             "-nodefaults", "-m", "4G",
85             "-cpu", "max",
86             "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22" +
87                        (",ipv6=no" if not self.ipv6 else ""),
88             "-device", "virtio-net-pci,netdev=vnet",
89             "-vnc", "127.0.0.1:0,to=20"]
90         if vcpus and vcpus > 1:
91             self._args += ["-smp", "%d" % vcpus]
92         if kvm_available(self.arch):
93             self._args += ["-enable-kvm"]
94         else:
95             logging.info("KVM not available, not using -enable-kvm")
96         self._data_args = []
97
98     def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
99         def check_sha256sum(fname):
100             if not sha256sum:
101                 return True
102             checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
103             return sha256sum == checksum.decode("utf-8")
104
105         def check_sha512sum(fname):
106             if not sha512sum:
107                 return True
108             checksum = subprocess.check_output(["sha512sum", fname]).split()[0]
109             return sha512sum == checksum.decode("utf-8")
110
111         cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
112         if not os.path.exists(cache_dir):
113             os.makedirs(cache_dir)
114         fname = os.path.join(cache_dir,
115                              hashlib.sha1(url.encode("utf-8")).hexdigest())
116         if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname):
117             return fname
118         logging.debug("Downloading %s to %s...", url, fname)
119         subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
120                               stdout=self._stdout, stderr=self._stderr)
121         os.rename(fname + ".download", fname)
122         return fname
123
124     def _ssh_do(self, user, cmd, check):
125         ssh_cmd = ["ssh", "-q", "-t",
126                    "-o", "StrictHostKeyChecking=no",
127                    "-o", "UserKnownHostsFile=" + os.devnull,
128                    "-o", "ConnectTimeout=1",
129                    "-p", self.ssh_port, "-i", self._ssh_key_file]
130         for var in self.envvars:
131             ssh_cmd += ['-o', "SendEnv=%s" % var ]
132         assert not isinstance(cmd, str)
133         ssh_cmd += ["%[email protected]" % user] + list(cmd)
134         logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
135         r = subprocess.call(ssh_cmd)
136         if check and r != 0:
137             raise Exception("SSH command failed: %s" % cmd)
138         return r
139
140     def ssh(self, *cmd):
141         return self._ssh_do(self.GUEST_USER, cmd, False)
142
143     def ssh_root(self, *cmd):
144         return self._ssh_do("root", cmd, False)
145
146     def ssh_check(self, *cmd):
147         self._ssh_do(self.GUEST_USER, cmd, True)
148
149     def ssh_root_check(self, *cmd):
150         self._ssh_do("root", cmd, True)
151
152     def build_image(self, img):
153         raise NotImplementedError
154
155     def exec_qemu_img(self, *args):
156         cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
157         cmd.extend(list(args))
158         subprocess.check_call(cmd)
159
160     def add_source_dir(self, src_dir):
161         name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
162         tarfile = os.path.join(self._tmpdir, name + ".tar")
163         logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
164         subprocess.check_call(["./scripts/archive-source.sh", tarfile],
165                               cwd=src_dir, stdin=self._devnull,
166                               stdout=self._stdout, stderr=self._stderr)
167         self._data_args += ["-drive",
168                             "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
169                                     (tarfile, name),
170                             "-device",
171                             "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
172
173     def boot(self, img, extra_args=[]):
174         args = self._args + [
175             "-device", "VGA",
176             "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img,
177             "-device", "virtio-blk,drive=drive0,bootindex=0"]
178         args += self._data_args + extra_args
179         logging.debug("QEMU args: %s", " ".join(args))
180         qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch)
181         guest = QEMUMachine(binary=qemu_bin, args=args)
182         guest.set_machine('pc')
183         guest.set_console()
184         try:
185             guest.launch()
186         except:
187             logging.error("Failed to launch QEMU, command line:")
188             logging.error(" ".join([qemu_bin] + args))
189             logging.error("Log:")
190             logging.error(guest.get_log())
191             logging.error("QEMU version >= 2.10 is required")
192             raise
193         atexit.register(self.shutdown)
194         self._guest = guest
195         usernet_info = guest.qmp("human-monitor-command",
196                                  command_line="info usernet")
197         self.ssh_port = None
198         for l in usernet_info["return"].splitlines():
199             fields = l.split()
200             if "TCP[HOST_FORWARD]" in fields and "22" in fields:
201                 self.ssh_port = l.split()[3]
202         if not self.ssh_port:
203             raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
204                             usernet_info)
205
206     def console_init(self, timeout = 120):
207         vm = self._guest
208         vm.console_socket.settimeout(timeout)
209
210     def console_log(self, text):
211         for line in re.split("[\r\n]", text):
212             # filter out terminal escape sequences
213             line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
214             line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
215             # replace unprintable chars
216             line = re.sub("\x1b", "<esc>", line)
217             line = re.sub("[\x00-\x1f]", ".", line)
218             line = re.sub("[\x80-\xff]", ".", line)
219             if line == "":
220                 continue
221             # log console line
222             sys.stderr.write("con recv: %s\n" % line)
223
224     def console_wait(self, expect, expectalt = None):
225         vm = self._guest
226         output = ""
227         while True:
228             try:
229                 chars = vm.console_socket.recv(1)
230             except socket.timeout:
231                 sys.stderr.write("console: *** read timeout ***\n")
232                 sys.stderr.write("console: waiting for: '%s'\n" % expect)
233                 if not expectalt is None:
234                     sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
235                 sys.stderr.write("console: line buffer:\n")
236                 sys.stderr.write("\n")
237                 self.console_log(output.rstrip())
238                 sys.stderr.write("\n")
239                 raise
240             output += chars.decode("latin1")
241             if expect in output:
242                 break
243             if not expectalt is None and expectalt in output:
244                 break
245             if "\r" in output or "\n" in output:
246                 lines = re.split("[\r\n]", output)
247                 output = lines.pop()
248                 if self.debug:
249                     self.console_log("\n".join(lines))
250         if self.debug:
251             self.console_log(output)
252         if not expectalt is None and expectalt in output:
253             return False
254         return True
255
256     def console_consume(self):
257         vm = self._guest
258         output = ""
259         vm.console_socket.setblocking(0)
260         while True:
261             try:
262                 chars = vm.console_socket.recv(1)
263             except:
264                 break
265             output += chars.decode("latin1")
266             if "\r" in output or "\n" in output:
267                 lines = re.split("[\r\n]", output)
268                 output = lines.pop()
269                 if self.debug:
270                     self.console_log("\n".join(lines))
271         if self.debug:
272             self.console_log(output)
273         vm.console_socket.setblocking(1)
274
275     def console_send(self, command):
276         vm = self._guest
277         if self.debug:
278             logline = re.sub("\n", "<enter>", command)
279             logline = re.sub("[\x00-\x1f]", ".", logline)
280             sys.stderr.write("con send: %s\n" % logline)
281         for char in list(command):
282             vm.console_socket.send(char.encode("utf-8"))
283             time.sleep(0.01)
284
285     def console_wait_send(self, wait, command):
286         self.console_wait(wait)
287         self.console_send(command)
288
289     def console_ssh_init(self, prompt, user, pw):
290         sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" % SSH_PUB_KEY.rstrip()
291         self.console_wait_send("login:",    "%s\n" % user)
292         self.console_wait_send("Password:", "%s\n" % pw)
293         self.console_wait_send(prompt,      "mkdir .ssh\n")
294         self.console_wait_send(prompt,      sshkey_cmd)
295         self.console_wait_send(prompt,      "chmod 755 .ssh\n")
296         self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
297
298     def console_sshd_config(self, prompt):
299         self.console_wait(prompt)
300         self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
301         for var in self.envvars:
302             self.console_wait(prompt)
303             self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
304
305     def print_step(self, text):
306         sys.stderr.write("### %s ...\n" % text)
307
308     def wait_ssh(self, seconds=300):
309         starttime = datetime.datetime.now()
310         endtime = starttime + datetime.timedelta(seconds=seconds)
311         guest_up = False
312         while datetime.datetime.now() < endtime:
313             if self.ssh("exit 0") == 0:
314                 guest_up = True
315                 break
316             seconds = (endtime - datetime.datetime.now()).total_seconds()
317             logging.debug("%ds before timeout", seconds)
318             time.sleep(1)
319         if not guest_up:
320             raise Exception("Timeout while waiting for guest ssh")
321
322     def shutdown(self):
323         self._guest.shutdown()
324
325     def wait(self):
326         self._guest.wait()
327
328     def graceful_shutdown(self):
329         self.ssh_root(self.poweroff)
330         self._guest.wait()
331
332     def qmp(self, *args, **kwargs):
333         return self._guest.qmp(*args, **kwargs)
334
335 def parse_args(vmcls):
336
337     def get_default_jobs():
338         if kvm_available(vmcls.arch):
339             return multiprocessing.cpu_count() // 2
340         else:
341             return 1
342
343     parser = optparse.OptionParser(
344         description="VM test utility.  Exit codes: "
345                     "0 = success, "
346                     "1 = command line error, "
347                     "2 = environment initialization failed, "
348                     "3 = test command failed")
349     parser.add_option("--debug", "-D", action="store_true",
350                       help="enable debug output")
351     parser.add_option("--image", "-i", default="%s.img" % vmcls.name,
352                       help="image file name")
353     parser.add_option("--force", "-f", action="store_true",
354                       help="force build image even if image exists")
355     parser.add_option("--jobs", type=int, default=get_default_jobs(),
356                       help="number of virtual CPUs")
357     parser.add_option("--verbose", "-V", action="store_true",
358                       help="Pass V=1 to builds within the guest")
359     parser.add_option("--build-image", "-b", action="store_true",
360                       help="build image")
361     parser.add_option("--build-qemu",
362                       help="build QEMU from source in guest")
363     parser.add_option("--build-target",
364                       help="QEMU build target", default="check")
365     parser.add_option("--interactive", "-I", action="store_true",
366                       help="Interactively run command")
367     parser.add_option("--snapshot", "-s", action="store_true",
368                       help="run tests with a snapshot")
369     parser.disable_interspersed_args()
370     return parser.parse_args()
371
372 def main(vmcls):
373     try:
374         args, argv = parse_args(vmcls)
375         if not argv and not args.build_qemu and not args.build_image:
376             print("Nothing to do?")
377             return 1
378         logging.basicConfig(level=(logging.DEBUG if args.debug
379                                    else logging.WARN))
380         vm = vmcls(debug=args.debug, vcpus=args.jobs)
381         if args.build_image:
382             if os.path.exists(args.image) and not args.force:
383                 sys.stderr.writelines(["Image file exists: %s\n" % args.image,
384                                       "Use --force option to overwrite\n"])
385                 return 1
386             return vm.build_image(args.image)
387         if args.build_qemu:
388             vm.add_source_dir(args.build_qemu)
389             cmd = [vm.BUILD_SCRIPT.format(
390                    configure_opts = " ".join(argv),
391                    jobs=int(args.jobs),
392                    target=args.build_target,
393                    verbose = "V=1" if args.verbose else "")]
394         else:
395             cmd = argv
396         img = args.image
397         if args.snapshot:
398             img += ",snapshot=on"
399         vm.boot(img)
400         vm.wait_ssh()
401     except Exception as e:
402         if isinstance(e, SystemExit) and e.code == 0:
403             return 0
404         sys.stderr.write("Failed to prepare guest environment\n")
405         traceback.print_exc()
406         return 2
407
408     exitcode = 0
409     if vm.ssh(*cmd) != 0:
410         exitcode = 3
411     if args.interactive:
412         vm.ssh()
413
414     if not args.snapshot:
415         vm.graceful_shutdown()
416
417     return exitcode
This page took 0.045953 seconds and 4 git commands to generate.