]> Git Repo - qemu.git/blob - tests/vm/basevm.py
Merge remote-tracking branch 'remotes/vivier2/tags/trivial-branch-pull-request' into...
[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, 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
241
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)
251
252     def console_wait_send(self, wait, command):
253         self.console_wait(wait)
254         self.console_send(command)
255
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")
264
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)
271
272     def print_step(self, text):
273         sys.stderr.write("### %s ...\n" % text)
274
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")
288
289     def shutdown(self):
290         self._guest.shutdown()
291
292     def wait(self):
293         self._guest.wait()
294
295     def graceful_shutdown(self):
296         self.ssh_root(self.poweroff)
297         self._guest.wait()
298
299     def qmp(self, *args, **kwargs):
300         return self._guest.qmp(*args, **kwargs)
301
302 def parse_args(vmcls):
303
304     def get_default_jobs():
305         if kvm_available(vmcls.arch):
306             return multiprocessing.cpu_count() // 2
307         else:
308             return 1
309
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()
338
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
374
375     exitcode = 0
376     if vm.ssh(*cmd) != 0:
377         exitcode = 3
378     if exitcode != 0 and args.interactive:
379         vm.ssh()
380
381     if not args.snapshot:
382         vm.graceful_shutdown()
383
384     return exitcode
This page took 0.046439 seconds and 4 git commands to generate.