]> Git Repo - qemu.git/blob - tests/vm/basevm.py
tests/vm: serial console support helpers
[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 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     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)
66
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])
70
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)
73
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 = []
94
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")
101
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
114
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 += ["%[email protected]" % 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
130
131     def ssh(self, *cmd):
132         return self._ssh_do(self.GUEST_USER, cmd, False)
133
134     def ssh_root(self, *cmd):
135         return self._ssh_do("root", cmd, False)
136
137     def ssh_check(self, *cmd):
138         self._ssh_do(self.GUEST_USER, cmd, True)
139
140     def ssh_root_check(self, *cmd):
141         self._ssh_do("root", cmd, True)
142
143     def build_image(self, img):
144         raise NotImplementedError
145
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)]
158
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)
191
192     def console_init(self, timeout = 120):
193         vm = self._guest
194         vm.console_socket.settimeout(timeout)
195
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)
209
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)
234
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)
244
245     def console_wait_send(self, wait, command):
246         self.console_wait(wait)
247         self.console_send(command)
248
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")
257
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)
264
265     def print_step(self, text):
266         sys.stderr.write("### %s ...\n" % text)
267
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")
281
282     def shutdown(self):
283         self._guest.shutdown()
284
285     def wait(self):
286         self._guest.wait()
287
288     def graceful_shutdown(self):
289         self.ssh_root(self.poweroff)
290         self._guest.wait()
291
292     def qmp(self, *args, **kwargs):
293         return self._guest.qmp(*args, **kwargs)
294
295 def parse_args(vmcls):
296
297     def get_default_jobs():
298         if kvm_available(vmcls.arch):
299             return multiprocessing.cpu_count() // 2
300         else:
301             return 1
302
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()
331
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
367
368     exitcode = 0
369     if vm.ssh(*cmd) != 0:
370         exitcode = 3
371     if exitcode != 0 and args.interactive:
372         vm.ssh()
373
374     if not args.snapshot:
375         vm.graceful_shutdown()
376
377     return exitcode
This page took 0.046952 seconds and 4 git commands to generate.