]> Git Repo - qemu.git/blob - tests/vm/basevm.py
53b9515ee22afb25e4a8963d92d86baa588b4f9a
[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 add_source_dir(self, src_dir):
156         name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
157         tarfile = os.path.join(self._tmpdir, name + ".tar")
158         logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
159         subprocess.check_call(["./scripts/archive-source.sh", tarfile],
160                               cwd=src_dir, stdin=self._devnull,
161                               stdout=self._stdout, stderr=self._stderr)
162         self._data_args += ["-drive",
163                             "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
164                                     (tarfile, name),
165                             "-device",
166                             "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
167
168     def boot(self, img, extra_args=[]):
169         args = self._args + [
170             "-device", "VGA",
171             "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img,
172             "-device", "virtio-blk,drive=drive0,bootindex=0"]
173         args += self._data_args + extra_args
174         logging.debug("QEMU args: %s", " ".join(args))
175         qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch)
176         guest = QEMUMachine(binary=qemu_bin, args=args)
177         guest.set_machine('pc')
178         guest.set_console()
179         try:
180             guest.launch()
181         except:
182             logging.error("Failed to launch QEMU, command line:")
183             logging.error(" ".join([qemu_bin] + args))
184             logging.error("Log:")
185             logging.error(guest.get_log())
186             logging.error("QEMU version >= 2.10 is required")
187             raise
188         atexit.register(self.shutdown)
189         self._guest = guest
190         usernet_info = guest.qmp("human-monitor-command",
191                                  command_line="info usernet")
192         self.ssh_port = None
193         for l in usernet_info["return"].splitlines():
194             fields = l.split()
195             if "TCP[HOST_FORWARD]" in fields and "22" in fields:
196                 self.ssh_port = l.split()[3]
197         if not self.ssh_port:
198             raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
199                             usernet_info)
200
201     def console_init(self, timeout = 120):
202         vm = self._guest
203         vm.console_socket.settimeout(timeout)
204
205     def console_log(self, text):
206         for line in re.split("[\r\n]", text):
207             # filter out terminal escape sequences
208             line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
209             line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
210             # replace unprintable chars
211             line = re.sub("\x1b", "<esc>", line)
212             line = re.sub("[\x00-\x1f]", ".", line)
213             line = re.sub("[\x80-\xff]", ".", line)
214             if line == "":
215                 continue
216             # log console line
217             sys.stderr.write("con recv: %s\n" % line)
218
219     def console_wait(self, expect, expectalt = None):
220         vm = self._guest
221         output = ""
222         while True:
223             try:
224                 chars = vm.console_socket.recv(1)
225             except socket.timeout:
226                 sys.stderr.write("console: *** read timeout ***\n")
227                 sys.stderr.write("console: waiting for: '%s'\n" % expect)
228                 if not expectalt is None:
229                     sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
230                 sys.stderr.write("console: line buffer:\n")
231                 sys.stderr.write("\n")
232                 self.console_log(output.rstrip())
233                 sys.stderr.write("\n")
234                 raise
235             output += chars.decode("latin1")
236             if expect in output:
237                 break
238             if not expectalt is None and expectalt in output:
239                 break
240             if "\r" in output or "\n" in output:
241                 lines = re.split("[\r\n]", output)
242                 output = lines.pop()
243                 if self.debug:
244                     self.console_log("\n".join(lines))
245         if self.debug:
246             self.console_log(output)
247         if not expectalt is None and expectalt in output:
248             return False
249         return True
250
251     def console_consume(self):
252         vm = self._guest
253         output = ""
254         vm.console_socket.setblocking(0)
255         while True:
256             try:
257                 chars = vm.console_socket.recv(1)
258             except:
259                 break
260             output += chars.decode("latin1")
261             if "\r" in output or "\n" in output:
262                 lines = re.split("[\r\n]", output)
263                 output = lines.pop()
264                 if self.debug:
265                     self.console_log("\n".join(lines))
266         if self.debug:
267             self.console_log(output)
268         vm.console_socket.setblocking(1)
269
270     def console_send(self, command):
271         vm = self._guest
272         if self.debug:
273             logline = re.sub("\n", "<enter>", command)
274             logline = re.sub("[\x00-\x1f]", ".", logline)
275             sys.stderr.write("con send: %s\n" % logline)
276         for char in list(command):
277             vm.console_socket.send(char.encode("utf-8"))
278             time.sleep(0.01)
279
280     def console_wait_send(self, wait, command):
281         self.console_wait(wait)
282         self.console_send(command)
283
284     def console_ssh_init(self, prompt, user, pw):
285         sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" % SSH_PUB_KEY.rstrip()
286         self.console_wait_send("login:",    "%s\n" % user)
287         self.console_wait_send("Password:", "%s\n" % pw)
288         self.console_wait_send(prompt,      "mkdir .ssh\n")
289         self.console_wait_send(prompt,      sshkey_cmd)
290         self.console_wait_send(prompt,      "chmod 755 .ssh\n")
291         self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
292
293     def console_sshd_config(self, prompt):
294         self.console_wait(prompt)
295         self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
296         for var in self.envvars:
297             self.console_wait(prompt)
298             self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
299
300     def print_step(self, text):
301         sys.stderr.write("### %s ...\n" % text)
302
303     def wait_ssh(self, seconds=300):
304         starttime = datetime.datetime.now()
305         endtime = starttime + datetime.timedelta(seconds=seconds)
306         guest_up = False
307         while datetime.datetime.now() < endtime:
308             if self.ssh("exit 0") == 0:
309                 guest_up = True
310                 break
311             seconds = (endtime - datetime.datetime.now()).total_seconds()
312             logging.debug("%ds before timeout", seconds)
313             time.sleep(1)
314         if not guest_up:
315             raise Exception("Timeout while waiting for guest ssh")
316
317     def shutdown(self):
318         self._guest.shutdown()
319
320     def wait(self):
321         self._guest.wait()
322
323     def graceful_shutdown(self):
324         self.ssh_root(self.poweroff)
325         self._guest.wait()
326
327     def qmp(self, *args, **kwargs):
328         return self._guest.qmp(*args, **kwargs)
329
330 def parse_args(vmcls):
331
332     def get_default_jobs():
333         if kvm_available(vmcls.arch):
334             return multiprocessing.cpu_count() // 2
335         else:
336             return 1
337
338     parser = optparse.OptionParser(
339         description="VM test utility.  Exit codes: "
340                     "0 = success, "
341                     "1 = command line error, "
342                     "2 = environment initialization failed, "
343                     "3 = test command failed")
344     parser.add_option("--debug", "-D", action="store_true",
345                       help="enable debug output")
346     parser.add_option("--image", "-i", default="%s.img" % vmcls.name,
347                       help="image file name")
348     parser.add_option("--force", "-f", action="store_true",
349                       help="force build image even if image exists")
350     parser.add_option("--jobs", type=int, default=get_default_jobs(),
351                       help="number of virtual CPUs")
352     parser.add_option("--verbose", "-V", action="store_true",
353                       help="Pass V=1 to builds within the guest")
354     parser.add_option("--build-image", "-b", action="store_true",
355                       help="build image")
356     parser.add_option("--build-qemu",
357                       help="build QEMU from source in guest")
358     parser.add_option("--build-target",
359                       help="QEMU build target", default="check")
360     parser.add_option("--interactive", "-I", action="store_true",
361                       help="Interactively run command")
362     parser.add_option("--snapshot", "-s", action="store_true",
363                       help="run tests with a snapshot")
364     parser.disable_interspersed_args()
365     return parser.parse_args()
366
367 def main(vmcls):
368     try:
369         args, argv = parse_args(vmcls)
370         if not argv and not args.build_qemu and not args.build_image:
371             print("Nothing to do?")
372             return 1
373         logging.basicConfig(level=(logging.DEBUG if args.debug
374                                    else logging.WARN))
375         vm = vmcls(debug=args.debug, vcpus=args.jobs)
376         if args.build_image:
377             if os.path.exists(args.image) and not args.force:
378                 sys.stderr.writelines(["Image file exists: %s\n" % args.image,
379                                       "Use --force option to overwrite\n"])
380                 return 1
381             return vm.build_image(args.image)
382         if args.build_qemu:
383             vm.add_source_dir(args.build_qemu)
384             cmd = [vm.BUILD_SCRIPT.format(
385                    configure_opts = " ".join(argv),
386                    jobs=int(args.jobs),
387                    target=args.build_target,
388                    verbose = "V=1" if args.verbose else "")]
389         else:
390             cmd = argv
391         img = args.image
392         if args.snapshot:
393             img += ",snapshot=on"
394         vm.boot(img)
395         vm.wait_ssh()
396     except Exception as e:
397         if isinstance(e, SystemExit) and e.code == 0:
398             return 0
399         sys.stderr.write("Failed to prepare guest environment\n")
400         traceback.print_exc()
401         return 2
402
403     exitcode = 0
404     if vm.ssh(*cmd) != 0:
405         exitcode = 3
406     if args.interactive:
407         vm.ssh()
408
409     if not args.snapshot:
410         vm.graceful_shutdown()
411
412     return exitcode
This page took 0.037636 seconds and 2 git commands to generate.