1# 2# VM testing base class 3# 4# Copyright 2017-2019 Red Hat Inc. 5# 6# Authors: 7# Fam Zheng <famz@redhat.com> 8# Gerd Hoffmann <kraxel@redhat.com> 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 15import re 16import sys 17import socket 18import logging 19import time 20import datetime 21sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 22from qemu.accel import kvm_available 23from qemu.machine import QEMUMachine 24import subprocess 25import hashlib 26import optparse 27import atexit 28import tempfile 29import shutil 30import multiprocessing 31import traceback 32 33SSH_KEY = open(os.path.join(os.path.dirname(__file__), 34 "..", "keys", "id_rsa")).read() 35SSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__), 36 "..", "keys", "id_rsa.pub")).read() 37 38class BaseVM(object): 39 GUEST_USER = "qemu" 40 GUEST_PASS = "qemupass" 41 ROOT_PASS = "qemupass" 42 43 envvars = [ 44 "https_proxy", 45 "http_proxy", 46 "ftp_proxy", 47 "no_proxy", 48 ] 49 50 # The script to run in the guest that builds QEMU 51 BUILD_SCRIPT = "" 52 # The guest name, to be overridden by subclasses 53 name = "#base" 54 # The guest architecture, to be overridden by subclasses 55 arch = "#arch" 56 # command to halt the guest, can be overridden by subclasses 57 poweroff = "poweroff" 58 # enable IPv6 networking 59 ipv6 = True 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 (",ipv6=no" if not self.ipv6 else ""), 86 "-device", "virtio-net-pci,netdev=vnet", 87 "-vnc", "127.0.0.1:0,to=20"] 88 if vcpus and vcpus > 1: 89 self._args += ["-smp", "%d" % vcpus] 90 if kvm_available(self.arch): 91 self._args += ["-enable-kvm"] 92 else: 93 logging.info("KVM not available, not using -enable-kvm") 94 self._data_args = [] 95 96 def _download_with_cache(self, url, sha256sum=None, sha512sum=None): 97 def check_sha256sum(fname): 98 if not sha256sum: 99 return True 100 checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 101 return sha256sum == checksum.decode("utf-8") 102 103 def check_sha512sum(fname): 104 if not sha512sum: 105 return True 106 checksum = subprocess.check_output(["sha512sum", fname]).split()[0] 107 return sha512sum == checksum.decode("utf-8") 108 109 cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 110 if not os.path.exists(cache_dir): 111 os.makedirs(cache_dir) 112 fname = os.path.join(cache_dir, 113 hashlib.sha1(url.encode("utf-8")).hexdigest()) 114 if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname): 115 return fname 116 logging.debug("Downloading %s to %s...", url, fname) 117 subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 118 stdout=self._stdout, stderr=self._stderr) 119 os.rename(fname + ".download", fname) 120 return fname 121 122 def _ssh_do(self, user, cmd, check): 123 ssh_cmd = ["ssh", "-q", "-t", 124 "-o", "StrictHostKeyChecking=no", 125 "-o", "UserKnownHostsFile=" + os.devnull, 126 "-o", "ConnectTimeout=1", 127 "-p", self.ssh_port, "-i", self._ssh_key_file] 128 for var in self.envvars: 129 ssh_cmd += ['-o', "SendEnv=%s" % var ] 130 assert not isinstance(cmd, str) 131 ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 132 logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 133 r = subprocess.call(ssh_cmd) 134 if check and r != 0: 135 raise Exception("SSH command failed: %s" % cmd) 136 return r 137 138 def ssh(self, *cmd): 139 return self._ssh_do(self.GUEST_USER, cmd, False) 140 141 def ssh_root(self, *cmd): 142 return self._ssh_do("root", cmd, False) 143 144 def ssh_check(self, *cmd): 145 self._ssh_do(self.GUEST_USER, cmd, True) 146 147 def ssh_root_check(self, *cmd): 148 self._ssh_do("root", cmd, True) 149 150 def build_image(self, img): 151 raise NotImplementedError 152 153 def exec_qemu_img(self, *args): 154 cmd = [os.environ.get("QEMU_IMG", "qemu-img")] 155 cmd.extend(list(args)) 156 subprocess.check_call(cmd) 157 158 def add_source_dir(self, src_dir): 159 name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 160 tarfile = os.path.join(self._tmpdir, name + ".tar") 161 logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 162 subprocess.check_call(["./scripts/archive-source.sh", tarfile], 163 cwd=src_dir, stdin=self._devnull, 164 stdout=self._stdout, stderr=self._stderr) 165 self._data_args += ["-drive", 166 "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 167 (tarfile, name), 168 "-device", 169 "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 170 171 def boot(self, img, extra_args=[]): 172 args = self._args + [ 173 "-device", "VGA", 174 "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img, 175 "-device", "virtio-blk,drive=drive0,bootindex=0"] 176 args += self._data_args + extra_args 177 logging.debug("QEMU args: %s", " ".join(args)) 178 qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch) 179 guest = QEMUMachine(binary=qemu_bin, args=args) 180 guest.set_machine('pc') 181 guest.set_console() 182 try: 183 guest.launch() 184 except: 185 logging.error("Failed to launch QEMU, command line:") 186 logging.error(" ".join([qemu_bin] + args)) 187 logging.error("Log:") 188 logging.error(guest.get_log()) 189 logging.error("QEMU version >= 2.10 is required") 190 raise 191 atexit.register(self.shutdown) 192 self._guest = guest 193 usernet_info = guest.qmp("human-monitor-command", 194 command_line="info usernet") 195 self.ssh_port = None 196 for l in usernet_info["return"].splitlines(): 197 fields = l.split() 198 if "TCP[HOST_FORWARD]" in fields and "22" in fields: 199 self.ssh_port = l.split()[3] 200 if not self.ssh_port: 201 raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 202 usernet_info) 203 204 def console_init(self, timeout = 120): 205 vm = self._guest 206 vm.console_socket.settimeout(timeout) 207 208 def console_log(self, text): 209 for line in re.split("[\r\n]", text): 210 # filter out terminal escape sequences 211 line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line) 212 line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line) 213 # replace unprintable chars 214 line = re.sub("\x1b", "<esc>", line) 215 line = re.sub("[\x00-\x1f]", ".", line) 216 line = re.sub("[\x80-\xff]", ".", line) 217 if line == "": 218 continue 219 # log console line 220 sys.stderr.write("con recv: %s\n" % line) 221 222 def console_wait(self, expect, expectalt = None): 223 vm = self._guest 224 output = "" 225 while True: 226 try: 227 chars = vm.console_socket.recv(1) 228 except socket.timeout: 229 sys.stderr.write("console: *** read timeout ***\n") 230 sys.stderr.write("console: waiting for: '%s'\n" % expect) 231 if not expectalt is None: 232 sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt) 233 sys.stderr.write("console: line buffer:\n") 234 sys.stderr.write("\n") 235 self.console_log(output.rstrip()) 236 sys.stderr.write("\n") 237 raise 238 output += chars.decode("latin1") 239 if expect in output: 240 break 241 if not expectalt is None and expectalt in output: 242 break 243 if "\r" in output or "\n" in output: 244 lines = re.split("[\r\n]", output) 245 output = lines.pop() 246 if self.debug: 247 self.console_log("\n".join(lines)) 248 if self.debug: 249 self.console_log(output) 250 if not expectalt is None and expectalt in output: 251 return False 252 return True 253 254 def console_consume(self): 255 vm = self._guest 256 output = "" 257 vm.console_socket.setblocking(0) 258 while True: 259 try: 260 chars = vm.console_socket.recv(1) 261 except: 262 break 263 output += chars.decode("latin1") 264 if "\r" in output or "\n" in output: 265 lines = re.split("[\r\n]", output) 266 output = lines.pop() 267 if self.debug: 268 self.console_log("\n".join(lines)) 269 if self.debug: 270 self.console_log(output) 271 vm.console_socket.setblocking(1) 272 273 def console_send(self, command): 274 vm = self._guest 275 if self.debug: 276 logline = re.sub("\n", "<enter>", command) 277 logline = re.sub("[\x00-\x1f]", ".", logline) 278 sys.stderr.write("con send: %s\n" % logline) 279 for char in list(command): 280 vm.console_socket.send(char.encode("utf-8")) 281 time.sleep(0.01) 282 283 def console_wait_send(self, wait, command): 284 self.console_wait(wait) 285 self.console_send(command) 286 287 def console_ssh_init(self, prompt, user, pw): 288 sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" % SSH_PUB_KEY.rstrip() 289 self.console_wait_send("login:", "%s\n" % user) 290 self.console_wait_send("Password:", "%s\n" % pw) 291 self.console_wait_send(prompt, "mkdir .ssh\n") 292 self.console_wait_send(prompt, sshkey_cmd) 293 self.console_wait_send(prompt, "chmod 755 .ssh\n") 294 self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n") 295 296 def console_sshd_config(self, prompt): 297 self.console_wait(prompt) 298 self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n") 299 for var in self.envvars: 300 self.console_wait(prompt) 301 self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var) 302 303 def print_step(self, text): 304 sys.stderr.write("### %s ...\n" % text) 305 306 def wait_ssh(self, seconds=300): 307 starttime = datetime.datetime.now() 308 endtime = starttime + datetime.timedelta(seconds=seconds) 309 guest_up = False 310 while datetime.datetime.now() < endtime: 311 if self.ssh("exit 0") == 0: 312 guest_up = True 313 break 314 seconds = (endtime - datetime.datetime.now()).total_seconds() 315 logging.debug("%ds before timeout", seconds) 316 time.sleep(1) 317 if not guest_up: 318 raise Exception("Timeout while waiting for guest ssh") 319 320 def shutdown(self): 321 self._guest.shutdown() 322 323 def wait(self): 324 self._guest.wait() 325 326 def graceful_shutdown(self): 327 self.ssh_root(self.poweroff) 328 self._guest.wait() 329 330 def qmp(self, *args, **kwargs): 331 return self._guest.qmp(*args, **kwargs) 332 333def parse_args(vmcls): 334 335 def get_default_jobs(): 336 if kvm_available(vmcls.arch): 337 return multiprocessing.cpu_count() // 2 338 else: 339 return 1 340 341 parser = optparse.OptionParser( 342 description="VM test utility. Exit codes: " 343 "0 = success, " 344 "1 = command line error, " 345 "2 = environment initialization failed, " 346 "3 = test command failed") 347 parser.add_option("--debug", "-D", action="store_true", 348 help="enable debug output") 349 parser.add_option("--image", "-i", default="%s.img" % vmcls.name, 350 help="image file name") 351 parser.add_option("--force", "-f", action="store_true", 352 help="force build image even if image exists") 353 parser.add_option("--jobs", type=int, default=get_default_jobs(), 354 help="number of virtual CPUs") 355 parser.add_option("--verbose", "-V", action="store_true", 356 help="Pass V=1 to builds within the guest") 357 parser.add_option("--build-image", "-b", action="store_true", 358 help="build image") 359 parser.add_option("--build-qemu", 360 help="build QEMU from source in guest") 361 parser.add_option("--build-target", 362 help="QEMU build target", default="check") 363 parser.add_option("--interactive", "-I", action="store_true", 364 help="Interactively run command") 365 parser.add_option("--snapshot", "-s", action="store_true", 366 help="run tests with a snapshot") 367 parser.disable_interspersed_args() 368 return parser.parse_args() 369 370def main(vmcls): 371 try: 372 args, argv = parse_args(vmcls) 373 if not argv and not args.build_qemu and not args.build_image: 374 print("Nothing to do?") 375 return 1 376 logging.basicConfig(level=(logging.DEBUG if args.debug 377 else logging.WARN)) 378 vm = vmcls(debug=args.debug, vcpus=args.jobs) 379 if args.build_image: 380 if os.path.exists(args.image) and not args.force: 381 sys.stderr.writelines(["Image file exists: %s\n" % args.image, 382 "Use --force option to overwrite\n"]) 383 return 1 384 return vm.build_image(args.image) 385 if args.build_qemu: 386 vm.add_source_dir(args.build_qemu) 387 cmd = [vm.BUILD_SCRIPT.format( 388 configure_opts = " ".join(argv), 389 jobs=int(args.jobs), 390 target=args.build_target, 391 verbose = "V=1" if args.verbose else "")] 392 else: 393 cmd = argv 394 img = args.image 395 if args.snapshot: 396 img += ",snapshot=on" 397 vm.boot(img) 398 vm.wait_ssh() 399 except Exception as e: 400 if isinstance(e, SystemExit) and e.code == 0: 401 return 0 402 sys.stderr.write("Failed to prepare guest environment\n") 403 traceback.print_exc() 404 return 2 405 406 exitcode = 0 407 if vm.ssh(*cmd) != 0: 408 exitcode = 3 409 if args.interactive: 410 vm.ssh() 411 412 if not args.snapshot: 413 vm.graceful_shutdown() 414 415 return exitcode 416