Merge remote-tracking branch 'remotes/dgibson/tags/ppc-for-5.0-20200108' into staging
[qemu/ar7.git] / tests / vm / basevm.py
blobed5dd4f3d0e45bad50fb47dd49de9189c502264a
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.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
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 # 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)
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])
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)
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 = []
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")
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")
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
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 += ["%s@127.0.0.1" % 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
140 def ssh(self, *cmd):
141 return self._ssh_do(self.GUEST_USER, cmd, False)
143 def ssh_root(self, *cmd):
144 return self._ssh_do("root", cmd, False)
146 def ssh_check(self, *cmd):
147 self._ssh_do(self.GUEST_USER, cmd, True)
149 def ssh_root_check(self, *cmd):
150 self._ssh_do("root", cmd, True)
152 def build_image(self, img):
153 raise NotImplementedError
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)
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)]
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)
206 def console_init(self, timeout = 120):
207 vm = self._guest
208 vm.console_socket.settimeout(timeout)
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)
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
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)
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)
285 def console_wait_send(self, wait, command):
286 self.console_wait(wait)
287 self.console_send(command)
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")
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)
305 def print_step(self, text):
306 sys.stderr.write("### %s ...\n" % text)
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")
322 def shutdown(self):
323 self._guest.shutdown()
325 def wait(self):
326 self._guest.wait()
328 def graceful_shutdown(self):
329 self.ssh_root(self.poweroff)
330 self._guest.wait()
332 def qmp(self, *args, **kwargs):
333 return self._guest.qmp(*args, **kwargs)
335 def parse_args(vmcls):
337 def get_default_jobs():
338 if kvm_available(vmcls.arch):
339 return multiprocessing.cpu_count() // 2
340 else:
341 return 1
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()
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
408 exitcode = 0
409 if vm.ssh(*cmd) != 0:
410 exitcode = 3
411 if args.interactive:
412 vm.ssh()
414 if not args.snapshot:
415 vm.graceful_shutdown()
417 return exitcode