]> git.proxmox.com Git - mirror_qemu.git/blame - tests/vm/basevm.py
Merge remote-tracking branch 'remotes/kraxel/tags/seabios-20200819-pull-request'...
[mirror_qemu.git] / tests / vm / basevm.py
CommitLineData
ff2ebff0
FZ
1#
2# VM testing base class
3#
8dd38334 4# Copyright 2017-2019 Red Hat Inc.
ff2ebff0
FZ
5#
6# Authors:
7# Fam Zheng <famz@redhat.com>
8dd38334 8# Gerd Hoffmann <kraxel@redhat.com>
ff2ebff0
FZ
9#
10# This code is licensed under the GPL version 2 or later. See
11# the COPYING file in the top-level directory.
12#
13
14import os
8dd38334 15import re
ff2ebff0 16import sys
8dd38334 17import socket
ff2ebff0
FZ
18import logging
19import time
20import datetime
8f8fd9ed 21sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
8b272e00 22from qemu.accel import kvm_available
abf0bf99 23from qemu.machine import QEMUMachine
ff2ebff0
FZ
24import subprocess
25import hashlib
2fea3a12 26import argparse
ff2ebff0
FZ
27import atexit
28import tempfile
29import shutil
30import multiprocessing
31import traceback
5d676197
RF
32import shlex
33
34SSH_KEY_FILE = os.path.join(os.path.dirname(__file__),
35 "..", "keys", "id_rsa")
36SSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__),
37 "..", "keys", "id_rsa.pub")
38
39# This is the standard configuration.
40# Any or all of these can be overridden by
41# passing in a config argument to the VM constructor.
42DEFAULT_CONFIG = {
43 'cpu' : "max",
44 'machine' : 'pc',
45 'guest_user' : "qemu",
46 'guest_pass' : "qemupass",
47 'root_pass' : "qemupass",
48 'ssh_key_file' : SSH_KEY_FILE,
49 'ssh_pub_key_file': SSH_PUB_KEY_FILE,
50 'memory' : "4G",
51 'extra_args' : [],
52 'qemu_args' : "",
53 'dns' : "",
54 'ssh_port' : 0,
55 'install_cmds' : "",
56 'boot_dev_type' : "block",
57 'ssh_timeout' : 1,
58}
59BOOT_DEVICE = {
60 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\
61 "-device virtio-blk,drive=drive0,bootindex=0",
62 'scsi' : "-device virtio-scsi-device,id=scsi "\
63 "-drive file={},format=raw,if=none,id=hd0 "\
64 "-device scsi-hd,drive=hd0,bootindex=0",
65}
ff2ebff0 66class BaseVM(object):
ff2ebff0 67
b08ba163
GH
68 envvars = [
69 "https_proxy",
70 "http_proxy",
71 "ftp_proxy",
72 "no_proxy",
73 ]
74
ff2ebff0
FZ
75 # The script to run in the guest that builds QEMU
76 BUILD_SCRIPT = ""
77 # The guest name, to be overridden by subclasses
78 name = "#base"
31719c37
PMD
79 # The guest architecture, to be overridden by subclasses
80 arch = "#arch"
b3f94b2f
GH
81 # command to halt the guest, can be overridden by subclasses
82 poweroff = "poweroff"
4a70232b
RF
83 # Time to wait for shutdown to finish.
84 shutdown_timeout_default = 30
5b790481
EH
85 # enable IPv6 networking
86 ipv6 = True
5d676197
RF
87 # This is the timeout on the wait for console bytes.
88 socket_timeout = 120
c9de3935
RF
89 # Scale up some timeouts under TCG.
90 # 4 is arbitrary, but greater than 2,
91 # since we found we need to wait more than twice as long.
4a70232b 92 tcg_timeout_multiplier = 4
5d676197 93 def __init__(self, args, config=None):
ff2ebff0 94 self._guest = None
1f335d18
RF
95 self._genisoimage = args.genisoimage
96 self._build_path = args.build_path
13336606 97 self._efi_aarch64 = args.efi_aarch64
5d676197
RF
98 # Allow input config to override defaults.
99 self._config = DEFAULT_CONFIG.copy()
100 if config != None:
101 self._config.update(config)
102 self.validate_ssh_keys()
ff2ebff0
FZ
103 self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
104 suffix=".tmp",
105 dir="."))
106 atexit.register(shutil.rmtree, self._tmpdir)
5d676197
RF
107 # Copy the key files to a temporary directory.
108 # Also chmod the key file to agree with ssh requirements.
109 self._config['ssh_key'] = \
110 open(self._config['ssh_key_file']).read().rstrip()
111 self._config['ssh_pub_key'] = \
112 open(self._config['ssh_pub_key_file']).read().rstrip()
113 self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa")
114 open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key'])
115 subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file])
116
117 self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
118 open(self._ssh_tmp_pub_key_file,
119 "w").write(self._config['ssh_pub_key'])
ff2ebff0 120
1f335d18 121 self.debug = args.debug
ff14ab0c
RF
122 self._console_log_path = None
123 if args.log_console:
124 self._console_log_path = \
125 os.path.join(os.path.expanduser("~/.cache/qemu-vm"),
126 "{}.install.log".format(self.name))
ff2ebff0
FZ
127 self._stderr = sys.stderr
128 self._devnull = open(os.devnull, "w")
129 if self.debug:
130 self._stdout = sys.stdout
131 else:
132 self._stdout = self._devnull
5d676197 133 netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
ff2ebff0 134 self._args = [ \
5d676197
RF
135 "-nodefaults", "-m", self._config['memory'],
136 "-cpu", self._config['cpu'],
137 "-netdev",
138 netdev.format(self._config['ssh_port']) +
139 (",ipv6=no" if not self.ipv6 else "") +
140 (",dns=" + self._config['dns'] if self._config['dns'] else ""),
ff2ebff0 141 "-device", "virtio-net-pci,netdev=vnet",
8dd38334 142 "-vnc", "127.0.0.1:0,to=20"]
1f335d18
RF
143 if args.jobs and args.jobs > 1:
144 self._args += ["-smp", "%d" % args.jobs]
71531bb5 145 if kvm_available(self.arch):
4a70232b 146 self._shutdown_timeout = self.shutdown_timeout_default
ff2ebff0
FZ
147 self._args += ["-enable-kvm"]
148 else:
149 logging.info("KVM not available, not using -enable-kvm")
4a70232b
RF
150 self._shutdown_timeout = \
151 self.shutdown_timeout_default * self.tcg_timeout_multiplier
ff2ebff0
FZ
152 self._data_args = []
153
5d676197
RF
154 if self._config['qemu_args'] != None:
155 qemu_args = self._config['qemu_args']
156 qemu_args = qemu_args.replace('\n',' ').replace('\r','')
157 # shlex groups quoted arguments together
158 # we need this to keep the quoted args together for when
159 # the QEMU command is issued later.
160 args = shlex.split(qemu_args)
161 self._config['extra_args'] = []
162 for arg in args:
163 if arg:
164 # Preserve quotes around arguments.
165 # shlex above takes them out, so add them in.
166 if " " in arg:
167 arg = '"{}"'.format(arg)
168 self._config['extra_args'].append(arg)
169
170 def validate_ssh_keys(self):
171 """Check to see if the ssh key files exist."""
172 if 'ssh_key_file' not in self._config or\
173 not os.path.exists(self._config['ssh_key_file']):
174 raise Exception("ssh key file not found.")
175 if 'ssh_pub_key_file' not in self._config or\
176 not os.path.exists(self._config['ssh_pub_key_file']):
177 raise Exception("ssh pub key file not found.")
178
179 def wait_boot(self, wait_string=None):
180 """Wait for the standard string we expect
181 on completion of a normal boot.
182 The user can also choose to override with an
183 alternate string to wait for."""
184 if wait_string is None:
185 if self.login_prompt is None:
186 raise Exception("self.login_prompt not defined")
187 wait_string = self.login_prompt
188 # Intentionally bump up the default timeout under TCG,
189 # since the console wait below takes longer.
190 timeout = self.socket_timeout
191 if not kvm_available(self.arch):
192 timeout *= 8
193 self.console_init(timeout=timeout)
194 self.console_wait(wait_string)
195
5b4b4865 196 def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
ff2ebff0
FZ
197 def check_sha256sum(fname):
198 if not sha256sum:
199 return True
200 checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
3ace9be6 201 return sha256sum == checksum.decode("utf-8")
ff2ebff0 202
5b4b4865
AB
203 def check_sha512sum(fname):
204 if not sha512sum:
205 return True
206 checksum = subprocess.check_output(["sha512sum", fname]).split()[0]
207 return sha512sum == checksum.decode("utf-8")
208
ff2ebff0
FZ
209 cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
210 if not os.path.exists(cache_dir):
211 os.makedirs(cache_dir)
3ace9be6
GH
212 fname = os.path.join(cache_dir,
213 hashlib.sha1(url.encode("utf-8")).hexdigest())
5b4b4865 214 if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname):
ff2ebff0
FZ
215 return fname
216 logging.debug("Downloading %s to %s...", url, fname)
217 subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
218 stdout=self._stdout, stderr=self._stderr)
219 os.rename(fname + ".download", fname)
220 return fname
221
796471e9 222 def _ssh_do(self, user, cmd, check):
89adc5b9
RF
223 ssh_cmd = ["ssh",
224 "-t",
ff2ebff0
FZ
225 "-o", "StrictHostKeyChecking=no",
226 "-o", "UserKnownHostsFile=" + os.devnull,
5d676197
RF
227 "-o",
228 "ConnectTimeout={}".format(self._config["ssh_timeout"]),
229 "-p", self.ssh_port, "-i", self._ssh_tmp_key_file]
89adc5b9
RF
230 # If not in debug mode, set ssh to quiet mode to
231 # avoid printing the results of commands.
232 if not self.debug:
233 ssh_cmd.append("-q")
b08ba163
GH
234 for var in self.envvars:
235 ssh_cmd += ['-o', "SendEnv=%s" % var ]
ff2ebff0
FZ
236 assert not isinstance(cmd, str)
237 ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
238 logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
726c9a3b 239 r = subprocess.call(ssh_cmd)
ff2ebff0
FZ
240 if check and r != 0:
241 raise Exception("SSH command failed: %s" % cmd)
242 return r
243
244 def ssh(self, *cmd):
df001680 245 return self._ssh_do(self._config["guest_user"], cmd, False)
ff2ebff0 246
ff2ebff0
FZ
247 def ssh_root(self, *cmd):
248 return self._ssh_do("root", cmd, False)
249
250 def ssh_check(self, *cmd):
df001680 251 self._ssh_do(self._config["guest_user"], cmd, True)
ff2ebff0
FZ
252
253 def ssh_root_check(self, *cmd):
254 self._ssh_do("root", cmd, True)
255
256 def build_image(self, img):
257 raise NotImplementedError
258
1e48931c
WSM
259 def exec_qemu_img(self, *args):
260 cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
261 cmd.extend(list(args))
262 subprocess.check_call(cmd)
263
ff2ebff0 264 def add_source_dir(self, src_dir):
3ace9be6 265 name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
ff2ebff0
FZ
266 tarfile = os.path.join(self._tmpdir, name + ".tar")
267 logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
268 subprocess.check_call(["./scripts/archive-source.sh", tarfile],
269 cwd=src_dir, stdin=self._devnull,
270 stdout=self._stdout, stderr=self._stderr)
271 self._data_args += ["-drive",
272 "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
273 (tarfile, name),
274 "-device",
275 "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
276
277 def boot(self, img, extra_args=[]):
5d676197
RF
278 boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
279 boot_params = boot_dev.format(img)
280 args = self._args + boot_params.split(' ')
281 args += self._data_args + extra_args + self._config['extra_args']
ff2ebff0 282 logging.debug("QEMU args: %s", " ".join(args))
e56c4504 283 qemu_path = get_qemu_path(self.arch, self._build_path)
ff14ab0c
RF
284
285 # Since console_log_path is only set when the user provides the
286 # log_console option, we will set drain_console=True so the
287 # console is always drained.
288 guest = QEMUMachine(binary=qemu_path, args=args,
289 console_log=self._console_log_path,
290 drain_console=True)
5d676197 291 guest.set_machine(self._config['machine'])
8dd38334 292 guest.set_console()
ff2ebff0
FZ
293 try:
294 guest.launch()
295 except:
296 logging.error("Failed to launch QEMU, command line:")
e56c4504 297 logging.error(" ".join([qemu_path] + args))
ff2ebff0
FZ
298 logging.error("Log:")
299 logging.error(guest.get_log())
300 logging.error("QEMU version >= 2.10 is required")
301 raise
302 atexit.register(self.shutdown)
303 self._guest = guest
ff14ab0c
RF
304 # Init console so we can start consuming the chars.
305 self.console_init()
ff2ebff0
FZ
306 usernet_info = guest.qmp("human-monitor-command",
307 command_line="info usernet")
308 self.ssh_port = None
309 for l in usernet_info["return"].splitlines():
310 fields = l.split()
311 if "TCP[HOST_FORWARD]" in fields and "22" in fields:
312 self.ssh_port = l.split()[3]
313 if not self.ssh_port:
314 raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
315 usernet_info)
316
ff14ab0c
RF
317 def console_init(self, timeout = None):
318 if timeout == None:
319 timeout = self.socket_timeout
8dd38334
GH
320 vm = self._guest
321 vm.console_socket.settimeout(timeout)
698a64f9
GH
322 self.console_raw_path = os.path.join(vm._temp_dir,
323 vm._name + "-console.raw")
324 self.console_raw_file = open(self.console_raw_path, 'wb')
8dd38334
GH
325
326 def console_log(self, text):
327 for line in re.split("[\r\n]", text):
328 # filter out terminal escape sequences
329 line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
330 line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
331 # replace unprintable chars
332 line = re.sub("\x1b", "<esc>", line)
333 line = re.sub("[\x00-\x1f]", ".", line)
334 line = re.sub("[\x80-\xff]", ".", line)
335 if line == "":
336 continue
337 # log console line
338 sys.stderr.write("con recv: %s\n" % line)
339
60136e06 340 def console_wait(self, expect, expectalt = None):
8dd38334
GH
341 vm = self._guest
342 output = ""
343 while True:
344 try:
345 chars = vm.console_socket.recv(1)
698a64f9
GH
346 if self.console_raw_file:
347 self.console_raw_file.write(chars)
348 self.console_raw_file.flush()
8dd38334
GH
349 except socket.timeout:
350 sys.stderr.write("console: *** read timeout ***\n")
351 sys.stderr.write("console: waiting for: '%s'\n" % expect)
60136e06
GH
352 if not expectalt is None:
353 sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
8dd38334
GH
354 sys.stderr.write("console: line buffer:\n")
355 sys.stderr.write("\n")
356 self.console_log(output.rstrip())
357 sys.stderr.write("\n")
358 raise
359 output += chars.decode("latin1")
360 if expect in output:
361 break
60136e06
GH
362 if not expectalt is None and expectalt in output:
363 break
8dd38334
GH
364 if "\r" in output or "\n" in output:
365 lines = re.split("[\r\n]", output)
366 output = lines.pop()
367 if self.debug:
368 self.console_log("\n".join(lines))
369 if self.debug:
370 self.console_log(output)
60136e06
GH
371 if not expectalt is None and expectalt in output:
372 return False
373 return True
8dd38334 374
6c4f0416
GH
375 def console_consume(self):
376 vm = self._guest
377 output = ""
378 vm.console_socket.setblocking(0)
379 while True:
380 try:
381 chars = vm.console_socket.recv(1)
382 except:
383 break
384 output += chars.decode("latin1")
385 if "\r" in output or "\n" in output:
386 lines = re.split("[\r\n]", output)
387 output = lines.pop()
388 if self.debug:
389 self.console_log("\n".join(lines))
390 if self.debug:
391 self.console_log(output)
392 vm.console_socket.setblocking(1)
393
8dd38334
GH
394 def console_send(self, command):
395 vm = self._guest
396 if self.debug:
397 logline = re.sub("\n", "<enter>", command)
398 logline = re.sub("[\x00-\x1f]", ".", logline)
399 sys.stderr.write("con send: %s\n" % logline)
400 for char in list(command):
401 vm.console_socket.send(char.encode("utf-8"))
402 time.sleep(0.01)
403
404 def console_wait_send(self, wait, command):
405 self.console_wait(wait)
406 self.console_send(command)
407
408 def console_ssh_init(self, prompt, user, pw):
5d676197
RF
409 sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
410 % self._config['ssh_pub_key'].rstrip()
8dd38334
GH
411 self.console_wait_send("login:", "%s\n" % user)
412 self.console_wait_send("Password:", "%s\n" % pw)
413 self.console_wait_send(prompt, "mkdir .ssh\n")
414 self.console_wait_send(prompt, sshkey_cmd)
415 self.console_wait_send(prompt, "chmod 755 .ssh\n")
416 self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n")
417
418 def console_sshd_config(self, prompt):
419 self.console_wait(prompt)
420 self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
421 for var in self.envvars:
422 self.console_wait(prompt)
423 self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
424
425 def print_step(self, text):
426 sys.stderr.write("### %s ...\n" % text)
427
6ee982c9 428 def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
c9de3935
RF
429 # Allow more time for VM to boot under TCG.
430 if not kvm_available(self.arch):
4a70232b 431 seconds *= self.tcg_timeout_multiplier
ff2ebff0 432 starttime = datetime.datetime.now()
f5d3d218 433 endtime = starttime + datetime.timedelta(seconds=seconds)
6ee982c9 434 cmd_success = False
f5d3d218 435 while datetime.datetime.now() < endtime:
6ee982c9
RF
436 if wait_root and self.ssh_root(cmd) == 0:
437 cmd_success = True
fbb3aa29 438 break
6ee982c9
RF
439 elif self.ssh(cmd) == 0:
440 cmd_success = True
ff2ebff0 441 break
f5d3d218
PMD
442 seconds = (endtime - datetime.datetime.now()).total_seconds()
443 logging.debug("%ds before timeout", seconds)
ff2ebff0 444 time.sleep(1)
6ee982c9 445 if not cmd_success:
ff2ebff0
FZ
446 raise Exception("Timeout while waiting for guest ssh")
447
448 def shutdown(self):
4a70232b 449 self._guest.shutdown(timeout=self._shutdown_timeout)
ff2ebff0
FZ
450
451 def wait(self):
4a70232b 452 self._guest.wait(timeout=self._shutdown_timeout)
ff2ebff0 453
b3f94b2f
GH
454 def graceful_shutdown(self):
455 self.ssh_root(self.poweroff)
4a70232b 456 self._guest.wait(timeout=self._shutdown_timeout)
b3f94b2f 457
ff2ebff0
FZ
458 def qmp(self, *args, **kwargs):
459 return self._guest.qmp(*args, **kwargs)
460
b081986c
RF
461 def gen_cloud_init_iso(self):
462 cidir = self._tmpdir
463 mdata = open(os.path.join(cidir, "meta-data"), "w")
464 name = self.name.replace(".","-")
465 mdata.writelines(["instance-id: {}-vm-0\n".format(name),
466 "local-hostname: {}-guest\n".format(name)])
467 mdata.close()
468 udata = open(os.path.join(cidir, "user-data"), "w")
5d676197
RF
469 print("guest user:pw {}:{}".format(self._config['guest_user'],
470 self._config['guest_pass']))
b081986c
RF
471 udata.writelines(["#cloud-config\n",
472 "chpasswd:\n",
473 " list: |\n",
5d676197
RF
474 " root:%s\n" % self._config['root_pass'],
475 " %s:%s\n" % (self._config['guest_user'],
476 self._config['guest_pass']),
b081986c
RF
477 " expire: False\n",
478 "users:\n",
5d676197 479 " - name: %s\n" % self._config['guest_user'],
b081986c
RF
480 " sudo: ALL=(ALL) NOPASSWD:ALL\n",
481 " ssh-authorized-keys:\n",
5d676197 482 " - %s\n" % self._config['ssh_pub_key'],
b081986c
RF
483 " - name: root\n",
484 " ssh-authorized-keys:\n",
5d676197 485 " - %s\n" % self._config['ssh_pub_key'],
b081986c
RF
486 "locale: en_US.UTF-8\n"])
487 proxy = os.environ.get("http_proxy")
488 if not proxy is None:
489 udata.writelines(["apt:\n",
490 " proxy: %s" % proxy])
491 udata.close()
92fecad3 492 subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
b081986c
RF
493 "-volid", "cidata", "-joliet", "-rock",
494 "user-data", "meta-data"],
92fecad3
AB
495 cwd=cidir,
496 stdin=self._devnull, stdout=self._stdout,
497 stderr=self._stdout)
b081986c
RF
498 return os.path.join(cidir, "cloud-init.iso")
499
e56c4504
RF
500def get_qemu_path(arch, build_path=None):
501 """Fetch the path to the qemu binary."""
502 # If QEMU environment variable set, it takes precedence
503 if "QEMU" in os.environ:
504 qemu_path = os.environ["QEMU"]
505 elif build_path:
506 qemu_path = os.path.join(build_path, arch + "-softmmu")
507 qemu_path = os.path.join(qemu_path, "qemu-system-" + arch)
508 else:
509 # Default is to use system path for qemu.
510 qemu_path = "qemu-system-" + arch
511 return qemu_path
512
13336606
RF
513def get_qemu_version(qemu_path):
514 """Get the version number from the current QEMU,
515 and return the major number."""
516 output = subprocess.check_output([qemu_path, '--version'])
517 version_line = output.decode("utf-8")
518 version_num = re.split(' |\(', version_line)[3].split('.')[0]
519 return int(version_num)
520
3f1e8137
RF
521def parse_config(config, args):
522 """ Parse yaml config and populate our config structure.
523 The yaml config allows the user to override the
524 defaults for VM parameters. In many cases these
525 defaults can be overridden without rebuilding the VM."""
526 if args.config:
527 config_file = args.config
528 elif 'QEMU_CONFIG' in os.environ:
529 config_file = os.environ['QEMU_CONFIG']
530 else:
531 return config
532 if not os.path.exists(config_file):
533 raise Exception("config file {} does not exist".format(config_file))
534 # We gracefully handle importing the yaml module
535 # since it might not be installed.
536 # If we are here it means the user supplied a .yml file,
537 # so if the yaml module is not installed we will exit with error.
538 try:
539 import yaml
540 except ImportError:
541 print("The python3-yaml package is needed "\
542 "to support config.yaml files")
543 # Instead of raising an exception we exit to avoid
544 # a raft of messy (expected) errors to stdout.
545 exit(1)
546 with open(config_file) as f:
547 yaml_dict = yaml.safe_load(f)
548
549 if 'qemu-conf' in yaml_dict:
550 config.update(yaml_dict['qemu-conf'])
551 else:
552 raise Exception("config file {} is not valid"\
553 " missing qemu-conf".format(config_file))
554 return config
555
63a24c5e 556def parse_args(vmcls):
8a6e007e
PMD
557
558 def get_default_jobs():
b0953944
AB
559 if multiprocessing.cpu_count() > 1:
560 if kvm_available(vmcls.arch):
561 return multiprocessing.cpu_count() // 2
562 elif os.uname().machine == "x86_64" and \
563 vmcls.arch in ["aarch64", "x86_64", "i386"]:
564 # MTTCG is available on these arches and we can allow
565 # more cores. but only up to a reasonable limit. User
566 # can always override these limits with --jobs.
567 return min(multiprocessing.cpu_count() // 2, 8)
8a6e007e
PMD
568 else:
569 return 1
570
2fea3a12
AB
571 parser = argparse.ArgumentParser(
572 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
573 description="Utility for provisioning VMs and running builds",
574 epilog="""Remaining arguments are passed to the command.
575 Exit codes: 0 = success, 1 = command line error,
576 2 = environment initialization failed,
577 3 = test command failed""")
578 parser.add_argument("--debug", "-D", action="store_true",
579 help="enable debug output")
580 parser.add_argument("--image", "-i", default="%s.img" % vmcls.name,
581 help="image file name")
582 parser.add_argument("--force", "-f", action="store_true",
583 help="force build image even if image exists")
584 parser.add_argument("--jobs", type=int, default=get_default_jobs(),
585 help="number of virtual CPUs")
586 parser.add_argument("--verbose", "-V", action="store_true",
587 help="Pass V=1 to builds within the guest")
588 parser.add_argument("--build-image", "-b", action="store_true",
589 help="build image")
590 parser.add_argument("--build-qemu",
591 help="build QEMU from source in guest")
592 parser.add_argument("--build-target",
593 help="QEMU build target", default="check")
594 parser.add_argument("--build-path", default=None,
595 help="Path of build directory, "\
596 "for using build tree QEMU binary. ")
597 parser.add_argument("--interactive", "-I", action="store_true",
598 help="Interactively run command")
599 parser.add_argument("--snapshot", "-s", action="store_true",
600 help="run tests with a snapshot")
601 parser.add_argument("--genisoimage", default="genisoimage",
602 help="iso imaging tool")
603 parser.add_argument("--config", "-c", default=None,
604 help="Provide config yaml for configuration. "\
605 "See config_example.yaml for example.")
606 parser.add_argument("--efi-aarch64",
607 default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
608 help="Path to efi image for aarch64 VMs.")
609 parser.add_argument("--log-console", action="store_true",
610 help="Log console to file.")
611 parser.add_argument("commands", nargs="*", help="""Remaining
612 commands after -- are passed to command inside the VM""")
613
ff2ebff0
FZ
614 return parser.parse_args()
615
5d676197 616def main(vmcls, config=None):
ff2ebff0 617 try:
5d676197
RF
618 if config == None:
619 config = DEFAULT_CONFIG
2fea3a12
AB
620 args = parse_args(vmcls)
621 if not args.commands and not args.build_qemu and not args.build_image:
f03868bd 622 print("Nothing to do?")
ff2ebff0 623 return 1
3f1e8137 624 config = parse_config(config, args)
fb3b4e6d
EH
625 logging.basicConfig(level=(logging.DEBUG if args.debug
626 else logging.WARN))
5d676197 627 vm = vmcls(args, config=config)
ff2ebff0
FZ
628 if args.build_image:
629 if os.path.exists(args.image) and not args.force:
630 sys.stderr.writelines(["Image file exists: %s\n" % args.image,
631 "Use --force option to overwrite\n"])
632 return 1
633 return vm.build_image(args.image)
634 if args.build_qemu:
635 vm.add_source_dir(args.build_qemu)
636 cmd = [vm.BUILD_SCRIPT.format(
2fea3a12 637 configure_opts = " ".join(args.commands),
3ace9be6 638 jobs=int(args.jobs),
5c2ec9b6 639 target=args.build_target,
41e3340a 640 verbose = "V=1" if args.verbose else "")]
ff2ebff0 641 else:
2fea3a12 642 cmd = args.commands
983c2a77
FZ
643 img = args.image
644 if args.snapshot:
645 img += ",snapshot=on"
646 vm.boot(img)
ff2ebff0
FZ
647 vm.wait_ssh()
648 except Exception as e:
649 if isinstance(e, SystemExit) and e.code == 0:
650 return 0
651 sys.stderr.write("Failed to prepare guest environment\n")
652 traceback.print_exc()
653 return 2
654
b3f94b2f
GH
655 exitcode = 0
656 if vm.ssh(*cmd) != 0:
657 exitcode = 3
bcc388df 658 if args.interactive:
796471e9 659 vm.ssh()
b3f94b2f
GH
660
661 if not args.snapshot:
662 vm.graceful_shutdown()
663
664 return exitcode