tests/vm: serial console support helpers
[qemu/kevin.git] / tests / vm / basevm.py
blob494d62e1bc10caeccef3096c068cdc3b546665a2
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):
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 sys.stderr.write("console: line buffer:\n")
220 sys.stderr.write("\n")
221 self.console_log(output.rstrip())
222 sys.stderr.write("\n")
223 raise
224 output += chars.decode("latin1")
225 if expect in output:
226 break
227 if "\r" in output or "\n" in output:
228 lines = re.split("[\r\n]", output)
229 output = lines.pop()
230 if self.debug:
231 self.console_log("\n".join(lines))
232 if self.debug:
233 self.console_log(output)
235 def console_send(self, command):
236 vm = self._guest
237 if self.debug:
238 logline = re.sub("\n", "<enter>", command)
239 logline = re.sub("[\x00-\x1f]", ".", logline)
240 sys.stderr.write("con send: %s\n" % logline)
241 for char in list(command):
242 vm.console_socket.send(char.encode("utf-8"))
243 time.sleep(0.01)
245 def console_wait_send(self, wait, command):
246 self.console_wait(wait)
247 self.console_send(command)
249 def console_ssh_init(self, prompt, user, pw):
250 sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" % SSH_PUB_KEY.rstrip()
251 self.console_wait_send("login:", "%s\n" % user)
252 self.console_wait_send("Password:", "%s\n" % pw)
253 self.console_wait_send(prompt, "mkdir .ssh\n")
254 self.console_wait_send(prompt, sshkey_cmd)
255 self.console_wait_send(prompt, "chmod 755 .ssh\n")
256 self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n")
258 def console_sshd_config(self, prompt):
259 self.console_wait(prompt)
260 self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
261 for var in self.envvars:
262 self.console_wait(prompt)
263 self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
265 def print_step(self, text):
266 sys.stderr.write("### %s ...\n" % text)
268 def wait_ssh(self, seconds=300):
269 starttime = datetime.datetime.now()
270 endtime = starttime + datetime.timedelta(seconds=seconds)
271 guest_up = False
272 while datetime.datetime.now() < endtime:
273 if self.ssh("exit 0") == 0:
274 guest_up = True
275 break
276 seconds = (endtime - datetime.datetime.now()).total_seconds()
277 logging.debug("%ds before timeout", seconds)
278 time.sleep(1)
279 if not guest_up:
280 raise Exception("Timeout while waiting for guest ssh")
282 def shutdown(self):
283 self._guest.shutdown()
285 def wait(self):
286 self._guest.wait()
288 def graceful_shutdown(self):
289 self.ssh_root(self.poweroff)
290 self._guest.wait()
292 def qmp(self, *args, **kwargs):
293 return self._guest.qmp(*args, **kwargs)
295 def parse_args(vmcls):
297 def get_default_jobs():
298 if kvm_available(vmcls.arch):
299 return multiprocessing.cpu_count() // 2
300 else:
301 return 1
303 parser = optparse.OptionParser(
304 description="VM test utility. Exit codes: "
305 "0 = success, "
306 "1 = command line error, "
307 "2 = environment initialization failed, "
308 "3 = test command failed")
309 parser.add_option("--debug", "-D", action="store_true",
310 help="enable debug output")
311 parser.add_option("--image", "-i", default="%s.img" % vmcls.name,
312 help="image file name")
313 parser.add_option("--force", "-f", action="store_true",
314 help="force build image even if image exists")
315 parser.add_option("--jobs", type=int, default=get_default_jobs(),
316 help="number of virtual CPUs")
317 parser.add_option("--verbose", "-V", action="store_true",
318 help="Pass V=1 to builds within the guest")
319 parser.add_option("--build-image", "-b", action="store_true",
320 help="build image")
321 parser.add_option("--build-qemu",
322 help="build QEMU from source in guest")
323 parser.add_option("--build-target",
324 help="QEMU build target", default="check")
325 parser.add_option("--interactive", "-I", action="store_true",
326 help="Interactively run command")
327 parser.add_option("--snapshot", "-s", action="store_true",
328 help="run tests with a snapshot")
329 parser.disable_interspersed_args()
330 return parser.parse_args()
332 def main(vmcls):
333 try:
334 args, argv = parse_args(vmcls)
335 if not argv and not args.build_qemu and not args.build_image:
336 print("Nothing to do?")
337 return 1
338 logging.basicConfig(level=(logging.DEBUG if args.debug
339 else logging.WARN))
340 vm = vmcls(debug=args.debug, vcpus=args.jobs)
341 if args.build_image:
342 if os.path.exists(args.image) and not args.force:
343 sys.stderr.writelines(["Image file exists: %s\n" % args.image,
344 "Use --force option to overwrite\n"])
345 return 1
346 return vm.build_image(args.image)
347 if args.build_qemu:
348 vm.add_source_dir(args.build_qemu)
349 cmd = [vm.BUILD_SCRIPT.format(
350 configure_opts = " ".join(argv),
351 jobs=int(args.jobs),
352 target=args.build_target,
353 verbose = "V=1" if args.verbose else "")]
354 else:
355 cmd = argv
356 img = args.image
357 if args.snapshot:
358 img += ",snapshot=on"
359 vm.boot(img)
360 vm.wait_ssh()
361 except Exception as e:
362 if isinstance(e, SystemExit) and e.code == 0:
363 return 0
364 sys.stderr.write("Failed to prepare guest environment\n")
365 traceback.print_exc()
366 return 2
368 exitcode = 0
369 if vm.ssh(*cmd) != 0:
370 exitcode = 3
371 if exitcode != 0 and args.interactive:
372 vm.ssh()
374 if not args.snapshot:
375 vm.graceful_shutdown()
377 return exitcode