tests/vm: proper guest shutdown
[qemu/ar7.git] / tests / vm / basevm.py
blob896e9d07c10dadbce79a499191a5ce7bba44607c
1 #!/usr/bin/env python
3 # VM testing base class
5 # Copyright 2017 Red Hat Inc.
7 # Authors:
8 # Fam Zheng <famz@redhat.com>
10 # This code is licensed under the GPL version 2 or later. See
11 # the COPYING file in the top-level directory.
14 from __future__ import print_function
15 import os
16 import sys
17 import logging
18 import time
19 import datetime
20 sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
21 from qemu import kvm_available
22 from qemu.machine import QEMUMachine
23 import subprocess
24 import hashlib
25 import optparse
26 import atexit
27 import tempfile
28 import shutil
29 import multiprocessing
30 import traceback
32 SSH_KEY = open(os.path.join(os.path.dirname(__file__),
33 "..", "keys", "id_rsa")).read()
34 SSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__),
35 "..", "keys", "id_rsa.pub")).read()
37 class BaseVM(object):
38 GUEST_USER = "qemu"
39 GUEST_PASS = "qemupass"
40 ROOT_PASS = "qemupass"
42 envvars = [
43 "https_proxy",
44 "http_proxy",
45 "ftp_proxy",
46 "no_proxy",
49 # The script to run in the guest that builds QEMU
50 BUILD_SCRIPT = ""
51 # The guest name, to be overridden by subclasses
52 name = "#base"
53 # The guest architecture, to be overridden by subclasses
54 arch = "#arch"
55 # command to halt the guest, can be overridden by subclasses
56 poweroff = "poweroff"
57 def __init__(self, debug=False, vcpus=None):
58 self._guest = None
59 self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
60 suffix=".tmp",
61 dir="."))
62 atexit.register(shutil.rmtree, self._tmpdir)
64 self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa")
65 open(self._ssh_key_file, "w").write(SSH_KEY)
66 subprocess.check_call(["chmod", "600", self._ssh_key_file])
68 self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
69 open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY)
71 self.debug = debug
72 self._stderr = sys.stderr
73 self._devnull = open(os.devnull, "w")
74 if self.debug:
75 self._stdout = sys.stdout
76 else:
77 self._stdout = self._devnull
78 self._args = [ \
79 "-nodefaults", "-m", "4G",
80 "-cpu", "max",
81 "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22",
82 "-device", "virtio-net-pci,netdev=vnet",
83 "-vnc", "127.0.0.1:0,to=20",
84 "-serial", "file:%s" % os.path.join(self._tmpdir, "serial.out")]
85 if vcpus and vcpus > 1:
86 self._args += ["-smp", "%d" % vcpus]
87 if kvm_available(self.arch):
88 self._args += ["-enable-kvm"]
89 else:
90 logging.info("KVM not available, not using -enable-kvm")
91 self._data_args = []
93 def _download_with_cache(self, url, sha256sum=None):
94 def check_sha256sum(fname):
95 if not sha256sum:
96 return True
97 checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
98 return sha256sum == checksum.decode("utf-8")
100 cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
101 if not os.path.exists(cache_dir):
102 os.makedirs(cache_dir)
103 fname = os.path.join(cache_dir,
104 hashlib.sha1(url.encode("utf-8")).hexdigest())
105 if os.path.exists(fname) and check_sha256sum(fname):
106 return fname
107 logging.debug("Downloading %s to %s...", url, fname)
108 subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
109 stdout=self._stdout, stderr=self._stderr)
110 os.rename(fname + ".download", fname)
111 return fname
113 def _ssh_do(self, user, cmd, check):
114 ssh_cmd = ["ssh", "-q", "-t",
115 "-o", "StrictHostKeyChecking=no",
116 "-o", "UserKnownHostsFile=" + os.devnull,
117 "-o", "ConnectTimeout=1",
118 "-p", self.ssh_port, "-i", self._ssh_key_file]
119 for var in self.envvars:
120 ssh_cmd += ['-o', "SendEnv=%s" % var ]
121 assert not isinstance(cmd, str)
122 ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
123 logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
124 r = subprocess.call(ssh_cmd)
125 if check and r != 0:
126 raise Exception("SSH command failed: %s" % cmd)
127 return r
129 def ssh(self, *cmd):
130 return self._ssh_do(self.GUEST_USER, cmd, False)
132 def ssh_root(self, *cmd):
133 return self._ssh_do("root", cmd, False)
135 def ssh_check(self, *cmd):
136 self._ssh_do(self.GUEST_USER, cmd, True)
138 def ssh_root_check(self, *cmd):
139 self._ssh_do("root", cmd, True)
141 def build_image(self, img):
142 raise NotImplementedError
144 def add_source_dir(self, src_dir):
145 name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
146 tarfile = os.path.join(self._tmpdir, name + ".tar")
147 logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
148 subprocess.check_call(["./scripts/archive-source.sh", tarfile],
149 cwd=src_dir, stdin=self._devnull,
150 stdout=self._stdout, stderr=self._stderr)
151 self._data_args += ["-drive",
152 "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
153 (tarfile, name),
154 "-device",
155 "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
157 def boot(self, img, extra_args=[]):
158 args = self._args + [
159 "-device", "VGA",
160 "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img,
161 "-device", "virtio-blk,drive=drive0,bootindex=0"]
162 args += self._data_args + extra_args
163 logging.debug("QEMU args: %s", " ".join(args))
164 qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch)
165 guest = QEMUMachine(binary=qemu_bin, args=args)
166 try:
167 guest.launch()
168 except:
169 logging.error("Failed to launch QEMU, command line:")
170 logging.error(" ".join([qemu_bin] + args))
171 logging.error("Log:")
172 logging.error(guest.get_log())
173 logging.error("QEMU version >= 2.10 is required")
174 raise
175 atexit.register(self.shutdown)
176 self._guest = guest
177 usernet_info = guest.qmp("human-monitor-command",
178 command_line="info usernet")
179 self.ssh_port = None
180 for l in usernet_info["return"].splitlines():
181 fields = l.split()
182 if "TCP[HOST_FORWARD]" in fields and "22" in fields:
183 self.ssh_port = l.split()[3]
184 if not self.ssh_port:
185 raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
186 usernet_info)
188 def wait_ssh(self, seconds=300):
189 starttime = datetime.datetime.now()
190 endtime = starttime + datetime.timedelta(seconds=seconds)
191 guest_up = False
192 while datetime.datetime.now() < endtime:
193 if self.ssh("exit 0") == 0:
194 guest_up = True
195 break
196 seconds = (endtime - datetime.datetime.now()).total_seconds()
197 logging.debug("%ds before timeout", seconds)
198 time.sleep(1)
199 if not guest_up:
200 raise Exception("Timeout while waiting for guest ssh")
202 def shutdown(self):
203 self._guest.shutdown()
205 def wait(self):
206 self._guest.wait()
208 def graceful_shutdown(self):
209 self.ssh_root(self.poweroff)
210 self._guest.wait()
212 def qmp(self, *args, **kwargs):
213 return self._guest.qmp(*args, **kwargs)
215 def parse_args(vmcls):
217 def get_default_jobs():
218 if kvm_available(vmcls.arch):
219 return multiprocessing.cpu_count() // 2
220 else:
221 return 1
223 parser = optparse.OptionParser(
224 description="VM test utility. Exit codes: "
225 "0 = success, "
226 "1 = command line error, "
227 "2 = environment initialization failed, "
228 "3 = test command failed")
229 parser.add_option("--debug", "-D", action="store_true",
230 help="enable debug output")
231 parser.add_option("--image", "-i", default="%s.img" % vmcls.name,
232 help="image file name")
233 parser.add_option("--force", "-f", action="store_true",
234 help="force build image even if image exists")
235 parser.add_option("--jobs", type=int, default=get_default_jobs(),
236 help="number of virtual CPUs")
237 parser.add_option("--verbose", "-V", action="store_true",
238 help="Pass V=1 to builds within the guest")
239 parser.add_option("--build-image", "-b", action="store_true",
240 help="build image")
241 parser.add_option("--build-qemu",
242 help="build QEMU from source in guest")
243 parser.add_option("--build-target",
244 help="QEMU build target", default="check")
245 parser.add_option("--interactive", "-I", action="store_true",
246 help="Interactively run command")
247 parser.add_option("--snapshot", "-s", action="store_true",
248 help="run tests with a snapshot")
249 parser.disable_interspersed_args()
250 return parser.parse_args()
252 def main(vmcls):
253 try:
254 args, argv = parse_args(vmcls)
255 if not argv and not args.build_qemu and not args.build_image:
256 print("Nothing to do?")
257 return 1
258 logging.basicConfig(level=(logging.DEBUG if args.debug
259 else logging.WARN))
260 vm = vmcls(debug=args.debug, vcpus=args.jobs)
261 if args.build_image:
262 if os.path.exists(args.image) and not args.force:
263 sys.stderr.writelines(["Image file exists: %s\n" % args.image,
264 "Use --force option to overwrite\n"])
265 return 1
266 return vm.build_image(args.image)
267 if args.build_qemu:
268 vm.add_source_dir(args.build_qemu)
269 cmd = [vm.BUILD_SCRIPT.format(
270 configure_opts = " ".join(argv),
271 jobs=int(args.jobs),
272 target=args.build_target,
273 verbose = "V=1" if args.verbose else "")]
274 else:
275 cmd = argv
276 img = args.image
277 if args.snapshot:
278 img += ",snapshot=on"
279 vm.boot(img)
280 vm.wait_ssh()
281 except Exception as e:
282 if isinstance(e, SystemExit) and e.code == 0:
283 return 0
284 sys.stderr.write("Failed to prepare guest environment\n")
285 traceback.print_exc()
286 return 2
288 exitcode = 0
289 if vm.ssh(*cmd) != 0:
290 exitcode = 3
291 if exitcode != 0 and args.interactive:
292 vm.ssh()
294 if not args.snapshot:
295 vm.graceful_shutdown()
297 return exitcode