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", str(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 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, hashlib.sha1(url).hexdigest()) 94 if os.path.exists(fname) and check_sha256sum(fname): 95 return fname 96 logging.debug("Downloading %s to %s...", url, fname) 97 subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 98 stdout=self._stdout, stderr=self._stderr) 99 os.rename(fname + ".download", fname) 100 return fname 101 102 def _ssh_do(self, user, cmd, check, interactive=False): 103 ssh_cmd = ["ssh", "-q", 104 "-o", "StrictHostKeyChecking=no", 105 "-o", "UserKnownHostsFile=" + os.devnull, 106 "-o", "ConnectTimeout=1", 107 "-p", self.ssh_port, "-i", self._ssh_key_file] 108 if interactive: 109 ssh_cmd += ['-t'] 110 assert not isinstance(cmd, str) 111 ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 112 logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 113 r = subprocess.call(ssh_cmd) 114 if check and r != 0: 115 raise Exception("SSH command failed: %s" % cmd) 116 return r 117 118 def ssh(self, *cmd): 119 return self._ssh_do(self.GUEST_USER, cmd, False) 120 121 def ssh_interactive(self, *cmd): 122 return self._ssh_do(self.GUEST_USER, cmd, False, True) 123 124 def ssh_root(self, *cmd): 125 return self._ssh_do("root", cmd, False) 126 127 def ssh_check(self, *cmd): 128 self._ssh_do(self.GUEST_USER, cmd, True) 129 130 def ssh_root_check(self, *cmd): 131 self._ssh_do("root", cmd, True) 132 133 def build_image(self, img): 134 raise NotImplementedError 135 136 def add_source_dir(self, src_dir): 137 name = "data-" + hashlib.sha1(src_dir).hexdigest()[:5] 138 tarfile = os.path.join(self._tmpdir, name + ".tar") 139 logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 140 subprocess.check_call(["./scripts/archive-source.sh", tarfile], 141 cwd=src_dir, stdin=self._devnull, 142 stdout=self._stdout, stderr=self._stderr) 143 self._data_args += ["-drive", 144 "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 145 (tarfile, name), 146 "-device", 147 "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 148 149 def boot(self, img, extra_args=[]): 150 args = self._args + [ 151 "-device", "VGA", 152 "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img, 153 "-device", "virtio-blk,drive=drive0,bootindex=0"] 154 args += self._data_args + extra_args 155 logging.debug("QEMU args: %s", " ".join(args)) 156 qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch) 157 guest = QEMUMachine(binary=qemu_bin, args=args) 158 try: 159 guest.launch() 160 except: 161 logging.error("Failed to launch QEMU, command line:") 162 logging.error(" ".join([qemu_bin] + args)) 163 logging.error("Log:") 164 logging.error(guest.get_log()) 165 logging.error("QEMU version >= 2.10 is required") 166 raise 167 atexit.register(self.shutdown) 168 self._guest = guest 169 usernet_info = guest.qmp("human-monitor-command", 170 command_line="info usernet") 171 self.ssh_port = None 172 for l in usernet_info["return"].splitlines(): 173 fields = l.split() 174 if "TCP[HOST_FORWARD]" in fields and "22" in fields: 175 self.ssh_port = l.split()[3] 176 if not self.ssh_port: 177 raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 178 usernet_info) 179 180 def wait_ssh(self, seconds=300): 181 starttime = datetime.datetime.now() 182 endtime = starttime + datetime.timedelta(seconds=seconds) 183 guest_up = False 184 while datetime.datetime.now() < endtime: 185 if self.ssh("exit 0") == 0: 186 guest_up = True 187 break 188 seconds = (endtime - datetime.datetime.now()).total_seconds() 189 logging.debug("%ds before timeout", seconds) 190 time.sleep(1) 191 if not guest_up: 192 raise Exception("Timeout while waiting for guest ssh") 193 194 def shutdown(self): 195 self._guest.shutdown() 196 197 def wait(self): 198 self._guest.wait() 199 200 def qmp(self, *args, **kwargs): 201 return self._guest.qmp(*args, **kwargs) 202 203def parse_args(vmcls): 204 205 def get_default_jobs(): 206 if kvm_available(vmcls.arch): 207 return multiprocessing.cpu_count() / 2 208 else: 209 return 1 210 211 parser = optparse.OptionParser( 212 description="VM test utility. Exit codes: " 213 "0 = success, " 214 "1 = command line error, " 215 "2 = environment initialization failed, " 216 "3 = test command failed") 217 parser.add_option("--debug", "-D", action="store_true", 218 help="enable debug output") 219 parser.add_option("--image", "-i", default="%s.img" % vmcls.name, 220 help="image file name") 221 parser.add_option("--force", "-f", action="store_true", 222 help="force build image even if image exists") 223 parser.add_option("--jobs", type=int, default=get_default_jobs(), 224 help="number of virtual CPUs") 225 parser.add_option("--verbose", "-V", action="store_true", 226 help="Pass V=1 to builds within the guest") 227 parser.add_option("--build-image", "-b", action="store_true", 228 help="build image") 229 parser.add_option("--build-qemu", 230 help="build QEMU from source in guest") 231 parser.add_option("--build-target", 232 help="QEMU build target", default="check") 233 parser.add_option("--interactive", "-I", action="store_true", 234 help="Interactively run command") 235 parser.add_option("--snapshot", "-s", action="store_true", 236 help="run tests with a snapshot") 237 parser.disable_interspersed_args() 238 return parser.parse_args() 239 240def main(vmcls): 241 try: 242 args, argv = parse_args(vmcls) 243 if not argv and not args.build_qemu and not args.build_image: 244 print("Nothing to do?") 245 return 1 246 logging.basicConfig(level=(logging.DEBUG if args.debug 247 else logging.WARN)) 248 vm = vmcls(debug=args.debug, vcpus=args.jobs) 249 if args.build_image: 250 if os.path.exists(args.image) and not args.force: 251 sys.stderr.writelines(["Image file exists: %s\n" % args.image, 252 "Use --force option to overwrite\n"]) 253 return 1 254 return vm.build_image(args.image) 255 if args.build_qemu: 256 vm.add_source_dir(args.build_qemu) 257 cmd = [vm.BUILD_SCRIPT.format( 258 configure_opts = " ".join(argv), 259 jobs=args.jobs, 260 target=args.build_target, 261 verbose = "V=1" if args.verbose else "")] 262 else: 263 cmd = argv 264 img = args.image 265 if args.snapshot: 266 img += ",snapshot=on" 267 vm.boot(img) 268 vm.wait_ssh() 269 except Exception as e: 270 if isinstance(e, SystemExit) and e.code == 0: 271 return 0 272 sys.stderr.write("Failed to prepare guest environment\n") 273 traceback.print_exc() 274 return 2 275 276 if args.interactive: 277 if vm.ssh_interactive(*cmd) == 0: 278 return 0 279 vm.ssh_interactive() 280 return 3 281 else: 282 if vm.ssh(*cmd) != 0: 283 return 3 284