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