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