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