1ff2ebff0SFam Zheng# 2ff2ebff0SFam Zheng# VM testing base class 3ff2ebff0SFam Zheng# 48dd38334SGerd Hoffmann# Copyright 2017-2019 Red Hat Inc. 5ff2ebff0SFam Zheng# 6ff2ebff0SFam Zheng# Authors: 7ff2ebff0SFam Zheng# Fam Zheng <famz@redhat.com> 88dd38334SGerd Hoffmann# Gerd Hoffmann <kraxel@redhat.com> 9ff2ebff0SFam Zheng# 10ff2ebff0SFam Zheng# This code is licensed under the GPL version 2 or later. See 11ff2ebff0SFam Zheng# the COPYING file in the top-level directory. 12ff2ebff0SFam Zheng# 13ff2ebff0SFam Zheng 14ff2ebff0SFam Zhengimport os 158dd38334SGerd Hoffmannimport re 16ff2ebff0SFam Zhengimport sys 178dd38334SGerd Hoffmannimport socket 18ff2ebff0SFam Zhengimport logging 19ff2ebff0SFam Zhengimport time 20ff2ebff0SFam Zhengimport datetime 218f8fd9edSCleber Rosasys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 228b272e00SWainer dos Santos Moschettafrom qemu.accel import kvm_available 23abf0bf99SJohn Snowfrom qemu.machine import QEMUMachine 24ff2ebff0SFam Zhengimport subprocess 25ff2ebff0SFam Zhengimport hashlib 26ff2ebff0SFam Zhengimport optparse 27ff2ebff0SFam Zhengimport atexit 28ff2ebff0SFam Zhengimport tempfile 29ff2ebff0SFam Zhengimport shutil 30ff2ebff0SFam Zhengimport multiprocessing 31ff2ebff0SFam Zhengimport traceback 325d676197SRobert Foleyimport shlex 33ff2ebff0SFam Zheng 345d676197SRobert FoleySSH_KEY_FILE = os.path.join(os.path.dirname(__file__), 355d676197SRobert Foley "..", "keys", "id_rsa") 365d676197SRobert FoleySSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__), 375d676197SRobert Foley "..", "keys", "id_rsa.pub") 38ff2ebff0SFam Zheng 395d676197SRobert Foley# This is the standard configuration. 405d676197SRobert Foley# Any or all of these can be overridden by 415d676197SRobert Foley# passing in a config argument to the VM constructor. 425d676197SRobert FoleyDEFAULT_CONFIG = { 435d676197SRobert Foley 'cpu' : "max", 445d676197SRobert Foley 'machine' : 'pc', 455d676197SRobert Foley 'guest_user' : "qemu", 465d676197SRobert Foley 'guest_pass' : "qemupass", 475d676197SRobert Foley 'root_pass' : "qemupass", 485d676197SRobert Foley 'ssh_key_file' : SSH_KEY_FILE, 495d676197SRobert Foley 'ssh_pub_key_file': SSH_PUB_KEY_FILE, 505d676197SRobert Foley 'memory' : "4G", 515d676197SRobert Foley 'extra_args' : [], 525d676197SRobert Foley 'qemu_args' : "", 535d676197SRobert Foley 'dns' : "", 545d676197SRobert Foley 'ssh_port' : 0, 555d676197SRobert Foley 'install_cmds' : "", 565d676197SRobert Foley 'boot_dev_type' : "block", 575d676197SRobert Foley 'ssh_timeout' : 1, 585d676197SRobert Foley} 595d676197SRobert FoleyBOOT_DEVICE = { 605d676197SRobert Foley 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\ 615d676197SRobert Foley "-device virtio-blk,drive=drive0,bootindex=0", 625d676197SRobert Foley 'scsi' : "-device virtio-scsi-device,id=scsi "\ 635d676197SRobert Foley "-drive file={},format=raw,if=none,id=hd0 "\ 645d676197SRobert Foley "-device scsi-hd,drive=hd0,bootindex=0", 655d676197SRobert Foley} 66ff2ebff0SFam Zhengclass BaseVM(object): 67ff2ebff0SFam Zheng 68b08ba163SGerd Hoffmann envvars = [ 69b08ba163SGerd Hoffmann "https_proxy", 70b08ba163SGerd Hoffmann "http_proxy", 71b08ba163SGerd Hoffmann "ftp_proxy", 72b08ba163SGerd Hoffmann "no_proxy", 73b08ba163SGerd Hoffmann ] 74b08ba163SGerd Hoffmann 75ff2ebff0SFam Zheng # The script to run in the guest that builds QEMU 76ff2ebff0SFam Zheng BUILD_SCRIPT = "" 77ff2ebff0SFam Zheng # The guest name, to be overridden by subclasses 78ff2ebff0SFam Zheng name = "#base" 7931719c37SPhilippe Mathieu-Daudé # The guest architecture, to be overridden by subclasses 8031719c37SPhilippe Mathieu-Daudé arch = "#arch" 81b3f94b2fSGerd Hoffmann # command to halt the guest, can be overridden by subclasses 82b3f94b2fSGerd Hoffmann poweroff = "poweroff" 835b790481SEduardo Habkost # enable IPv6 networking 845b790481SEduardo Habkost ipv6 = True 855d676197SRobert Foley # This is the timeout on the wait for console bytes. 865d676197SRobert Foley socket_timeout = 120 87c9de3935SRobert Foley # Scale up some timeouts under TCG. 88c9de3935SRobert Foley # 4 is arbitrary, but greater than 2, 89c9de3935SRobert Foley # since we found we need to wait more than twice as long. 90c9de3935SRobert Foley tcg_ssh_timeout_multiplier = 4 915d676197SRobert Foley def __init__(self, args, config=None): 92ff2ebff0SFam Zheng self._guest = None 931f335d18SRobert Foley self._genisoimage = args.genisoimage 941f335d18SRobert Foley self._build_path = args.build_path 95*13336606SRobert Foley self._efi_aarch64 = args.efi_aarch64 965d676197SRobert Foley # Allow input config to override defaults. 975d676197SRobert Foley self._config = DEFAULT_CONFIG.copy() 985d676197SRobert Foley if config != None: 995d676197SRobert Foley self._config.update(config) 1005d676197SRobert Foley self.validate_ssh_keys() 101ff2ebff0SFam Zheng self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", 102ff2ebff0SFam Zheng suffix=".tmp", 103ff2ebff0SFam Zheng dir=".")) 104ff2ebff0SFam Zheng atexit.register(shutil.rmtree, self._tmpdir) 1055d676197SRobert Foley # Copy the key files to a temporary directory. 1065d676197SRobert Foley # Also chmod the key file to agree with ssh requirements. 1075d676197SRobert Foley self._config['ssh_key'] = \ 1085d676197SRobert Foley open(self._config['ssh_key_file']).read().rstrip() 1095d676197SRobert Foley self._config['ssh_pub_key'] = \ 1105d676197SRobert Foley open(self._config['ssh_pub_key_file']).read().rstrip() 1115d676197SRobert Foley self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa") 1125d676197SRobert Foley open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key']) 1135d676197SRobert Foley subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file]) 114ff2ebff0SFam Zheng 1155d676197SRobert Foley self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") 1165d676197SRobert Foley open(self._ssh_tmp_pub_key_file, 1175d676197SRobert Foley "w").write(self._config['ssh_pub_key']) 118ff2ebff0SFam Zheng 1191f335d18SRobert Foley self.debug = args.debug 120ff2ebff0SFam Zheng self._stderr = sys.stderr 121ff2ebff0SFam Zheng self._devnull = open(os.devnull, "w") 122ff2ebff0SFam Zheng if self.debug: 123ff2ebff0SFam Zheng self._stdout = sys.stdout 124ff2ebff0SFam Zheng else: 125ff2ebff0SFam Zheng self._stdout = self._devnull 1265d676197SRobert Foley netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22" 127ff2ebff0SFam Zheng self._args = [ \ 1285d676197SRobert Foley "-nodefaults", "-m", self._config['memory'], 1295d676197SRobert Foley "-cpu", self._config['cpu'], 1305d676197SRobert Foley "-netdev", 1315d676197SRobert Foley netdev.format(self._config['ssh_port']) + 1325d676197SRobert Foley (",ipv6=no" if not self.ipv6 else "") + 1335d676197SRobert Foley (",dns=" + self._config['dns'] if self._config['dns'] else ""), 134ff2ebff0SFam Zheng "-device", "virtio-net-pci,netdev=vnet", 1358dd38334SGerd Hoffmann "-vnc", "127.0.0.1:0,to=20"] 1361f335d18SRobert Foley if args.jobs and args.jobs > 1: 1371f335d18SRobert Foley self._args += ["-smp", "%d" % args.jobs] 13871531bb5SPhilippe Mathieu-Daudé if kvm_available(self.arch): 139ff2ebff0SFam Zheng self._args += ["-enable-kvm"] 140ff2ebff0SFam Zheng else: 141ff2ebff0SFam Zheng logging.info("KVM not available, not using -enable-kvm") 142ff2ebff0SFam Zheng self._data_args = [] 143ff2ebff0SFam Zheng 1445d676197SRobert Foley if self._config['qemu_args'] != None: 1455d676197SRobert Foley qemu_args = self._config['qemu_args'] 1465d676197SRobert Foley qemu_args = qemu_args.replace('\n',' ').replace('\r','') 1475d676197SRobert Foley # shlex groups quoted arguments together 1485d676197SRobert Foley # we need this to keep the quoted args together for when 1495d676197SRobert Foley # the QEMU command is issued later. 1505d676197SRobert Foley args = shlex.split(qemu_args) 1515d676197SRobert Foley self._config['extra_args'] = [] 1525d676197SRobert Foley for arg in args: 1535d676197SRobert Foley if arg: 1545d676197SRobert Foley # Preserve quotes around arguments. 1555d676197SRobert Foley # shlex above takes them out, so add them in. 1565d676197SRobert Foley if " " in arg: 1575d676197SRobert Foley arg = '"{}"'.format(arg) 1585d676197SRobert Foley self._config['extra_args'].append(arg) 1595d676197SRobert Foley 1605d676197SRobert Foley def validate_ssh_keys(self): 1615d676197SRobert Foley """Check to see if the ssh key files exist.""" 1625d676197SRobert Foley if 'ssh_key_file' not in self._config or\ 1635d676197SRobert Foley not os.path.exists(self._config['ssh_key_file']): 1645d676197SRobert Foley raise Exception("ssh key file not found.") 1655d676197SRobert Foley if 'ssh_pub_key_file' not in self._config or\ 1665d676197SRobert Foley not os.path.exists(self._config['ssh_pub_key_file']): 1675d676197SRobert Foley raise Exception("ssh pub key file not found.") 1685d676197SRobert Foley 1695d676197SRobert Foley def wait_boot(self, wait_string=None): 1705d676197SRobert Foley """Wait for the standard string we expect 1715d676197SRobert Foley on completion of a normal boot. 1725d676197SRobert Foley The user can also choose to override with an 1735d676197SRobert Foley alternate string to wait for.""" 1745d676197SRobert Foley if wait_string is None: 1755d676197SRobert Foley if self.login_prompt is None: 1765d676197SRobert Foley raise Exception("self.login_prompt not defined") 1775d676197SRobert Foley wait_string = self.login_prompt 1785d676197SRobert Foley # Intentionally bump up the default timeout under TCG, 1795d676197SRobert Foley # since the console wait below takes longer. 1805d676197SRobert Foley timeout = self.socket_timeout 1815d676197SRobert Foley if not kvm_available(self.arch): 1825d676197SRobert Foley timeout *= 8 1835d676197SRobert Foley self.console_init(timeout=timeout) 1845d676197SRobert Foley self.console_wait(wait_string) 1855d676197SRobert Foley 1865d676197SRobert Foley def __getattr__(self, name): 1875d676197SRobert Foley # Support direct access to config by key. 1885d676197SRobert Foley # for example, access self._config['cpu'] by self.cpu 1895d676197SRobert Foley if name.lower() in self._config.keys(): 1905d676197SRobert Foley return self._config[name.lower()] 1915d676197SRobert Foley return object.__getattribute__(self, name) 1925d676197SRobert Foley 1935b4b4865SAlex Bennée def _download_with_cache(self, url, sha256sum=None, sha512sum=None): 194ff2ebff0SFam Zheng def check_sha256sum(fname): 195ff2ebff0SFam Zheng if not sha256sum: 196ff2ebff0SFam Zheng return True 197ff2ebff0SFam Zheng checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 1983ace9be6SGerd Hoffmann return sha256sum == checksum.decode("utf-8") 199ff2ebff0SFam Zheng 2005b4b4865SAlex Bennée def check_sha512sum(fname): 2015b4b4865SAlex Bennée if not sha512sum: 2025b4b4865SAlex Bennée return True 2035b4b4865SAlex Bennée checksum = subprocess.check_output(["sha512sum", fname]).split()[0] 2045b4b4865SAlex Bennée return sha512sum == checksum.decode("utf-8") 2055b4b4865SAlex Bennée 206ff2ebff0SFam Zheng cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 207ff2ebff0SFam Zheng if not os.path.exists(cache_dir): 208ff2ebff0SFam Zheng os.makedirs(cache_dir) 2093ace9be6SGerd Hoffmann fname = os.path.join(cache_dir, 2103ace9be6SGerd Hoffmann hashlib.sha1(url.encode("utf-8")).hexdigest()) 2115b4b4865SAlex Bennée if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname): 212ff2ebff0SFam Zheng return fname 213ff2ebff0SFam Zheng logging.debug("Downloading %s to %s...", url, fname) 214ff2ebff0SFam Zheng subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 215ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 216ff2ebff0SFam Zheng os.rename(fname + ".download", fname) 217ff2ebff0SFam Zheng return fname 218ff2ebff0SFam Zheng 219796471e9SGerd Hoffmann def _ssh_do(self, user, cmd, check): 22089adc5b9SRobert Foley ssh_cmd = ["ssh", 22189adc5b9SRobert Foley "-t", 222ff2ebff0SFam Zheng "-o", "StrictHostKeyChecking=no", 223ff2ebff0SFam Zheng "-o", "UserKnownHostsFile=" + os.devnull, 2245d676197SRobert Foley "-o", 2255d676197SRobert Foley "ConnectTimeout={}".format(self._config["ssh_timeout"]), 2265d676197SRobert Foley "-p", self.ssh_port, "-i", self._ssh_tmp_key_file] 22789adc5b9SRobert Foley # If not in debug mode, set ssh to quiet mode to 22889adc5b9SRobert Foley # avoid printing the results of commands. 22989adc5b9SRobert Foley if not self.debug: 23089adc5b9SRobert Foley ssh_cmd.append("-q") 231b08ba163SGerd Hoffmann for var in self.envvars: 232b08ba163SGerd Hoffmann ssh_cmd += ['-o', "SendEnv=%s" % var ] 233ff2ebff0SFam Zheng assert not isinstance(cmd, str) 234ff2ebff0SFam Zheng ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 235ff2ebff0SFam Zheng logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 236726c9a3bSFam Zheng r = subprocess.call(ssh_cmd) 237ff2ebff0SFam Zheng if check and r != 0: 238ff2ebff0SFam Zheng raise Exception("SSH command failed: %s" % cmd) 239ff2ebff0SFam Zheng return r 240ff2ebff0SFam Zheng 241ff2ebff0SFam Zheng def ssh(self, *cmd): 242ff2ebff0SFam Zheng return self._ssh_do(self.GUEST_USER, cmd, False) 243ff2ebff0SFam Zheng 244ff2ebff0SFam Zheng def ssh_root(self, *cmd): 245ff2ebff0SFam Zheng return self._ssh_do("root", cmd, False) 246ff2ebff0SFam Zheng 247ff2ebff0SFam Zheng def ssh_check(self, *cmd): 248ff2ebff0SFam Zheng self._ssh_do(self.GUEST_USER, cmd, True) 249ff2ebff0SFam Zheng 250ff2ebff0SFam Zheng def ssh_root_check(self, *cmd): 251ff2ebff0SFam Zheng self._ssh_do("root", cmd, True) 252ff2ebff0SFam Zheng 253ff2ebff0SFam Zheng def build_image(self, img): 254ff2ebff0SFam Zheng raise NotImplementedError 255ff2ebff0SFam Zheng 2561e48931cSWainer dos Santos Moschetta def exec_qemu_img(self, *args): 2571e48931cSWainer dos Santos Moschetta cmd = [os.environ.get("QEMU_IMG", "qemu-img")] 2581e48931cSWainer dos Santos Moschetta cmd.extend(list(args)) 2591e48931cSWainer dos Santos Moschetta subprocess.check_call(cmd) 2601e48931cSWainer dos Santos Moschetta 261ff2ebff0SFam Zheng def add_source_dir(self, src_dir): 2623ace9be6SGerd Hoffmann name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 263ff2ebff0SFam Zheng tarfile = os.path.join(self._tmpdir, name + ".tar") 264ff2ebff0SFam Zheng logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 265ff2ebff0SFam Zheng subprocess.check_call(["./scripts/archive-source.sh", tarfile], 266ff2ebff0SFam Zheng cwd=src_dir, stdin=self._devnull, 267ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 268ff2ebff0SFam Zheng self._data_args += ["-drive", 269ff2ebff0SFam Zheng "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 270ff2ebff0SFam Zheng (tarfile, name), 271ff2ebff0SFam Zheng "-device", 272ff2ebff0SFam Zheng "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 273ff2ebff0SFam Zheng 274ff2ebff0SFam Zheng def boot(self, img, extra_args=[]): 2755d676197SRobert Foley boot_dev = BOOT_DEVICE[self._config['boot_dev_type']] 2765d676197SRobert Foley boot_params = boot_dev.format(img) 2775d676197SRobert Foley args = self._args + boot_params.split(' ') 2785d676197SRobert Foley args += self._data_args + extra_args + self._config['extra_args'] 279ff2ebff0SFam Zheng logging.debug("QEMU args: %s", " ".join(args)) 280e56c4504SRobert Foley qemu_path = get_qemu_path(self.arch, self._build_path) 281e56c4504SRobert Foley guest = QEMUMachine(binary=qemu_path, args=args) 2825d676197SRobert Foley guest.set_machine(self._config['machine']) 2838dd38334SGerd Hoffmann guest.set_console() 284ff2ebff0SFam Zheng try: 285ff2ebff0SFam Zheng guest.launch() 286ff2ebff0SFam Zheng except: 287ff2ebff0SFam Zheng logging.error("Failed to launch QEMU, command line:") 288e56c4504SRobert Foley logging.error(" ".join([qemu_path] + args)) 289ff2ebff0SFam Zheng logging.error("Log:") 290ff2ebff0SFam Zheng logging.error(guest.get_log()) 291ff2ebff0SFam Zheng logging.error("QEMU version >= 2.10 is required") 292ff2ebff0SFam Zheng raise 293ff2ebff0SFam Zheng atexit.register(self.shutdown) 294ff2ebff0SFam Zheng self._guest = guest 295ff2ebff0SFam Zheng usernet_info = guest.qmp("human-monitor-command", 296ff2ebff0SFam Zheng command_line="info usernet") 297ff2ebff0SFam Zheng self.ssh_port = None 298ff2ebff0SFam Zheng for l in usernet_info["return"].splitlines(): 299ff2ebff0SFam Zheng fields = l.split() 300ff2ebff0SFam Zheng if "TCP[HOST_FORWARD]" in fields and "22" in fields: 301ff2ebff0SFam Zheng self.ssh_port = l.split()[3] 302ff2ebff0SFam Zheng if not self.ssh_port: 303ff2ebff0SFam Zheng raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 304ff2ebff0SFam Zheng usernet_info) 305ff2ebff0SFam Zheng 3068dd38334SGerd Hoffmann def console_init(self, timeout = 120): 3078dd38334SGerd Hoffmann vm = self._guest 3088dd38334SGerd Hoffmann vm.console_socket.settimeout(timeout) 309698a64f9SGerd Hoffmann self.console_raw_path = os.path.join(vm._temp_dir, 310698a64f9SGerd Hoffmann vm._name + "-console.raw") 311698a64f9SGerd Hoffmann self.console_raw_file = open(self.console_raw_path, 'wb') 3128dd38334SGerd Hoffmann 3138dd38334SGerd Hoffmann def console_log(self, text): 3148dd38334SGerd Hoffmann for line in re.split("[\r\n]", text): 3158dd38334SGerd Hoffmann # filter out terminal escape sequences 3168dd38334SGerd Hoffmann line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line) 3178dd38334SGerd Hoffmann line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line) 3188dd38334SGerd Hoffmann # replace unprintable chars 3198dd38334SGerd Hoffmann line = re.sub("\x1b", "<esc>", line) 3208dd38334SGerd Hoffmann line = re.sub("[\x00-\x1f]", ".", line) 3218dd38334SGerd Hoffmann line = re.sub("[\x80-\xff]", ".", line) 3228dd38334SGerd Hoffmann if line == "": 3238dd38334SGerd Hoffmann continue 3248dd38334SGerd Hoffmann # log console line 3258dd38334SGerd Hoffmann sys.stderr.write("con recv: %s\n" % line) 3268dd38334SGerd Hoffmann 32760136e06SGerd Hoffmann def console_wait(self, expect, expectalt = None): 3288dd38334SGerd Hoffmann vm = self._guest 3298dd38334SGerd Hoffmann output = "" 3308dd38334SGerd Hoffmann while True: 3318dd38334SGerd Hoffmann try: 3328dd38334SGerd Hoffmann chars = vm.console_socket.recv(1) 333698a64f9SGerd Hoffmann if self.console_raw_file: 334698a64f9SGerd Hoffmann self.console_raw_file.write(chars) 335698a64f9SGerd Hoffmann self.console_raw_file.flush() 3368dd38334SGerd Hoffmann except socket.timeout: 3378dd38334SGerd Hoffmann sys.stderr.write("console: *** read timeout ***\n") 3388dd38334SGerd Hoffmann sys.stderr.write("console: waiting for: '%s'\n" % expect) 33960136e06SGerd Hoffmann if not expectalt is None: 34060136e06SGerd Hoffmann sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt) 3418dd38334SGerd Hoffmann sys.stderr.write("console: line buffer:\n") 3428dd38334SGerd Hoffmann sys.stderr.write("\n") 3438dd38334SGerd Hoffmann self.console_log(output.rstrip()) 3448dd38334SGerd Hoffmann sys.stderr.write("\n") 3458dd38334SGerd Hoffmann raise 3468dd38334SGerd Hoffmann output += chars.decode("latin1") 3478dd38334SGerd Hoffmann if expect in output: 3488dd38334SGerd Hoffmann break 34960136e06SGerd Hoffmann if not expectalt is None and expectalt in output: 35060136e06SGerd Hoffmann break 3518dd38334SGerd Hoffmann if "\r" in output or "\n" in output: 3528dd38334SGerd Hoffmann lines = re.split("[\r\n]", output) 3538dd38334SGerd Hoffmann output = lines.pop() 3548dd38334SGerd Hoffmann if self.debug: 3558dd38334SGerd Hoffmann self.console_log("\n".join(lines)) 3568dd38334SGerd Hoffmann if self.debug: 3578dd38334SGerd Hoffmann self.console_log(output) 35860136e06SGerd Hoffmann if not expectalt is None and expectalt in output: 35960136e06SGerd Hoffmann return False 36060136e06SGerd Hoffmann return True 3618dd38334SGerd Hoffmann 3626c4f0416SGerd Hoffmann def console_consume(self): 3636c4f0416SGerd Hoffmann vm = self._guest 3646c4f0416SGerd Hoffmann output = "" 3656c4f0416SGerd Hoffmann vm.console_socket.setblocking(0) 3666c4f0416SGerd Hoffmann while True: 3676c4f0416SGerd Hoffmann try: 3686c4f0416SGerd Hoffmann chars = vm.console_socket.recv(1) 3696c4f0416SGerd Hoffmann except: 3706c4f0416SGerd Hoffmann break 3716c4f0416SGerd Hoffmann output += chars.decode("latin1") 3726c4f0416SGerd Hoffmann if "\r" in output or "\n" in output: 3736c4f0416SGerd Hoffmann lines = re.split("[\r\n]", output) 3746c4f0416SGerd Hoffmann output = lines.pop() 3756c4f0416SGerd Hoffmann if self.debug: 3766c4f0416SGerd Hoffmann self.console_log("\n".join(lines)) 3776c4f0416SGerd Hoffmann if self.debug: 3786c4f0416SGerd Hoffmann self.console_log(output) 3796c4f0416SGerd Hoffmann vm.console_socket.setblocking(1) 3806c4f0416SGerd Hoffmann 3818dd38334SGerd Hoffmann def console_send(self, command): 3828dd38334SGerd Hoffmann vm = self._guest 3838dd38334SGerd Hoffmann if self.debug: 3848dd38334SGerd Hoffmann logline = re.sub("\n", "<enter>", command) 3858dd38334SGerd Hoffmann logline = re.sub("[\x00-\x1f]", ".", logline) 3868dd38334SGerd Hoffmann sys.stderr.write("con send: %s\n" % logline) 3878dd38334SGerd Hoffmann for char in list(command): 3888dd38334SGerd Hoffmann vm.console_socket.send(char.encode("utf-8")) 3898dd38334SGerd Hoffmann time.sleep(0.01) 3908dd38334SGerd Hoffmann 3918dd38334SGerd Hoffmann def console_wait_send(self, wait, command): 3928dd38334SGerd Hoffmann self.console_wait(wait) 3938dd38334SGerd Hoffmann self.console_send(command) 3948dd38334SGerd Hoffmann 3958dd38334SGerd Hoffmann def console_ssh_init(self, prompt, user, pw): 3965d676197SRobert Foley sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \ 3975d676197SRobert Foley % self._config['ssh_pub_key'].rstrip() 3988dd38334SGerd Hoffmann self.console_wait_send("login:", "%s\n" % user) 3998dd38334SGerd Hoffmann self.console_wait_send("Password:", "%s\n" % pw) 4008dd38334SGerd Hoffmann self.console_wait_send(prompt, "mkdir .ssh\n") 4018dd38334SGerd Hoffmann self.console_wait_send(prompt, sshkey_cmd) 4028dd38334SGerd Hoffmann self.console_wait_send(prompt, "chmod 755 .ssh\n") 4038dd38334SGerd Hoffmann self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n") 4048dd38334SGerd Hoffmann 4058dd38334SGerd Hoffmann def console_sshd_config(self, prompt): 4068dd38334SGerd Hoffmann self.console_wait(prompt) 4078dd38334SGerd Hoffmann self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n") 4088dd38334SGerd Hoffmann for var in self.envvars: 4098dd38334SGerd Hoffmann self.console_wait(prompt) 4108dd38334SGerd Hoffmann self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var) 4118dd38334SGerd Hoffmann 4128dd38334SGerd Hoffmann def print_step(self, text): 4138dd38334SGerd Hoffmann sys.stderr.write("### %s ...\n" % text) 4148dd38334SGerd Hoffmann 4156ee982c9SRobert Foley def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"): 416c9de3935SRobert Foley # Allow more time for VM to boot under TCG. 417c9de3935SRobert Foley if not kvm_available(self.arch): 418c9de3935SRobert Foley seconds *= self.tcg_ssh_timeout_multiplier 419ff2ebff0SFam Zheng starttime = datetime.datetime.now() 420f5d3d218SPhilippe Mathieu-Daudé endtime = starttime + datetime.timedelta(seconds=seconds) 4216ee982c9SRobert Foley cmd_success = False 422f5d3d218SPhilippe Mathieu-Daudé while datetime.datetime.now() < endtime: 4236ee982c9SRobert Foley if wait_root and self.ssh_root(cmd) == 0: 4246ee982c9SRobert Foley cmd_success = True 425fbb3aa29SRobert Foley break 4266ee982c9SRobert Foley elif self.ssh(cmd) == 0: 4276ee982c9SRobert Foley cmd_success = True 428ff2ebff0SFam Zheng break 429f5d3d218SPhilippe Mathieu-Daudé seconds = (endtime - datetime.datetime.now()).total_seconds() 430f5d3d218SPhilippe Mathieu-Daudé logging.debug("%ds before timeout", seconds) 431ff2ebff0SFam Zheng time.sleep(1) 4326ee982c9SRobert Foley if not cmd_success: 433ff2ebff0SFam Zheng raise Exception("Timeout while waiting for guest ssh") 434ff2ebff0SFam Zheng 435ff2ebff0SFam Zheng def shutdown(self): 436ff2ebff0SFam Zheng self._guest.shutdown() 437ff2ebff0SFam Zheng 438ff2ebff0SFam Zheng def wait(self): 439ff2ebff0SFam Zheng self._guest.wait() 440ff2ebff0SFam Zheng 441b3f94b2fSGerd Hoffmann def graceful_shutdown(self): 442b3f94b2fSGerd Hoffmann self.ssh_root(self.poweroff) 443b3f94b2fSGerd Hoffmann self._guest.wait() 444b3f94b2fSGerd Hoffmann 445ff2ebff0SFam Zheng def qmp(self, *args, **kwargs): 446ff2ebff0SFam Zheng return self._guest.qmp(*args, **kwargs) 447ff2ebff0SFam Zheng 448b081986cSRobert Foley def gen_cloud_init_iso(self): 449b081986cSRobert Foley cidir = self._tmpdir 450b081986cSRobert Foley mdata = open(os.path.join(cidir, "meta-data"), "w") 451b081986cSRobert Foley name = self.name.replace(".","-") 452b081986cSRobert Foley mdata.writelines(["instance-id: {}-vm-0\n".format(name), 453b081986cSRobert Foley "local-hostname: {}-guest\n".format(name)]) 454b081986cSRobert Foley mdata.close() 455b081986cSRobert Foley udata = open(os.path.join(cidir, "user-data"), "w") 4565d676197SRobert Foley print("guest user:pw {}:{}".format(self._config['guest_user'], 4575d676197SRobert Foley self._config['guest_pass'])) 458b081986cSRobert Foley udata.writelines(["#cloud-config\n", 459b081986cSRobert Foley "chpasswd:\n", 460b081986cSRobert Foley " list: |\n", 4615d676197SRobert Foley " root:%s\n" % self._config['root_pass'], 4625d676197SRobert Foley " %s:%s\n" % (self._config['guest_user'], 4635d676197SRobert Foley self._config['guest_pass']), 464b081986cSRobert Foley " expire: False\n", 465b081986cSRobert Foley "users:\n", 4665d676197SRobert Foley " - name: %s\n" % self._config['guest_user'], 467b081986cSRobert Foley " sudo: ALL=(ALL) NOPASSWD:ALL\n", 468b081986cSRobert Foley " ssh-authorized-keys:\n", 4695d676197SRobert Foley " - %s\n" % self._config['ssh_pub_key'], 470b081986cSRobert Foley " - name: root\n", 471b081986cSRobert Foley " ssh-authorized-keys:\n", 4725d676197SRobert Foley " - %s\n" % self._config['ssh_pub_key'], 473b081986cSRobert Foley "locale: en_US.UTF-8\n"]) 474b081986cSRobert Foley proxy = os.environ.get("http_proxy") 475b081986cSRobert Foley if not proxy is None: 476b081986cSRobert Foley udata.writelines(["apt:\n", 477b081986cSRobert Foley " proxy: %s" % proxy]) 478b081986cSRobert Foley udata.close() 47992fecad3SAlex Bennée subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso", 480b081986cSRobert Foley "-volid", "cidata", "-joliet", "-rock", 481b081986cSRobert Foley "user-data", "meta-data"], 482b081986cSRobert Foley cwd=cidir, 483b081986cSRobert Foley stdin=self._devnull, stdout=self._stdout, 484b081986cSRobert Foley stderr=self._stdout) 485b081986cSRobert Foley return os.path.join(cidir, "cloud-init.iso") 486b081986cSRobert Foley 487e56c4504SRobert Foleydef get_qemu_path(arch, build_path=None): 488e56c4504SRobert Foley """Fetch the path to the qemu binary.""" 489e56c4504SRobert Foley # If QEMU environment variable set, it takes precedence 490e56c4504SRobert Foley if "QEMU" in os.environ: 491e56c4504SRobert Foley qemu_path = os.environ["QEMU"] 492e56c4504SRobert Foley elif build_path: 493e56c4504SRobert Foley qemu_path = os.path.join(build_path, arch + "-softmmu") 494e56c4504SRobert Foley qemu_path = os.path.join(qemu_path, "qemu-system-" + arch) 495e56c4504SRobert Foley else: 496e56c4504SRobert Foley # Default is to use system path for qemu. 497e56c4504SRobert Foley qemu_path = "qemu-system-" + arch 498e56c4504SRobert Foley return qemu_path 499e56c4504SRobert Foley 500*13336606SRobert Foleydef get_qemu_version(qemu_path): 501*13336606SRobert Foley """Get the version number from the current QEMU, 502*13336606SRobert Foley and return the major number.""" 503*13336606SRobert Foley output = subprocess.check_output([qemu_path, '--version']) 504*13336606SRobert Foley version_line = output.decode("utf-8") 505*13336606SRobert Foley version_num = re.split(' |\(', version_line)[3].split('.')[0] 506*13336606SRobert Foley return int(version_num) 507*13336606SRobert Foley 5083f1e8137SRobert Foleydef parse_config(config, args): 5093f1e8137SRobert Foley """ Parse yaml config and populate our config structure. 5103f1e8137SRobert Foley The yaml config allows the user to override the 5113f1e8137SRobert Foley defaults for VM parameters. In many cases these 5123f1e8137SRobert Foley defaults can be overridden without rebuilding the VM.""" 5133f1e8137SRobert Foley if args.config: 5143f1e8137SRobert Foley config_file = args.config 5153f1e8137SRobert Foley elif 'QEMU_CONFIG' in os.environ: 5163f1e8137SRobert Foley config_file = os.environ['QEMU_CONFIG'] 5173f1e8137SRobert Foley else: 5183f1e8137SRobert Foley return config 5193f1e8137SRobert Foley if not os.path.exists(config_file): 5203f1e8137SRobert Foley raise Exception("config file {} does not exist".format(config_file)) 5213f1e8137SRobert Foley # We gracefully handle importing the yaml module 5223f1e8137SRobert Foley # since it might not be installed. 5233f1e8137SRobert Foley # If we are here it means the user supplied a .yml file, 5243f1e8137SRobert Foley # so if the yaml module is not installed we will exit with error. 5253f1e8137SRobert Foley try: 5263f1e8137SRobert Foley import yaml 5273f1e8137SRobert Foley except ImportError: 5283f1e8137SRobert Foley print("The python3-yaml package is needed "\ 5293f1e8137SRobert Foley "to support config.yaml files") 5303f1e8137SRobert Foley # Instead of raising an exception we exit to avoid 5313f1e8137SRobert Foley # a raft of messy (expected) errors to stdout. 5323f1e8137SRobert Foley exit(1) 5333f1e8137SRobert Foley with open(config_file) as f: 5343f1e8137SRobert Foley yaml_dict = yaml.safe_load(f) 5353f1e8137SRobert Foley 5363f1e8137SRobert Foley if 'qemu-conf' in yaml_dict: 5373f1e8137SRobert Foley config.update(yaml_dict['qemu-conf']) 5383f1e8137SRobert Foley else: 5393f1e8137SRobert Foley raise Exception("config file {} is not valid"\ 5403f1e8137SRobert Foley " missing qemu-conf".format(config_file)) 5413f1e8137SRobert Foley return config 5423f1e8137SRobert Foley 54363a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls): 5448a6e007eSPhilippe Mathieu-Daudé 5458a6e007eSPhilippe Mathieu-Daudé def get_default_jobs(): 54663a24c5eSPhilippe Mathieu-Daudé if kvm_available(vmcls.arch): 5473ad3e36eSWainer dos Santos Moschetta return multiprocessing.cpu_count() // 2 5488a6e007eSPhilippe Mathieu-Daudé else: 5498a6e007eSPhilippe Mathieu-Daudé return 1 5508a6e007eSPhilippe Mathieu-Daudé 551ff2ebff0SFam Zheng parser = optparse.OptionParser( 552ff2ebff0SFam Zheng description="VM test utility. Exit codes: " 553ff2ebff0SFam Zheng "0 = success, " 554ff2ebff0SFam Zheng "1 = command line error, " 555ff2ebff0SFam Zheng "2 = environment initialization failed, " 556ff2ebff0SFam Zheng "3 = test command failed") 557ff2ebff0SFam Zheng parser.add_option("--debug", "-D", action="store_true", 558ff2ebff0SFam Zheng help="enable debug output") 55963a24c5eSPhilippe Mathieu-Daudé parser.add_option("--image", "-i", default="%s.img" % vmcls.name, 560ff2ebff0SFam Zheng help="image file name") 561ff2ebff0SFam Zheng parser.add_option("--force", "-f", action="store_true", 562ff2ebff0SFam Zheng help="force build image even if image exists") 5638a6e007eSPhilippe Mathieu-Daudé parser.add_option("--jobs", type=int, default=get_default_jobs(), 564ff2ebff0SFam Zheng help="number of virtual CPUs") 56541e3340aSPeter Maydell parser.add_option("--verbose", "-V", action="store_true", 56641e3340aSPeter Maydell help="Pass V=1 to builds within the guest") 567ff2ebff0SFam Zheng parser.add_option("--build-image", "-b", action="store_true", 568ff2ebff0SFam Zheng help="build image") 569ff2ebff0SFam Zheng parser.add_option("--build-qemu", 570ff2ebff0SFam Zheng help="build QEMU from source in guest") 5715c2ec9b6SAlex Bennée parser.add_option("--build-target", 5725c2ec9b6SAlex Bennée help="QEMU build target", default="check") 573e56c4504SRobert Foley parser.add_option("--build-path", default=None, 574e56c4504SRobert Foley help="Path of build directory, "\ 575e56c4504SRobert Foley "for using build tree QEMU binary. ") 576ff2ebff0SFam Zheng parser.add_option("--interactive", "-I", action="store_true", 577ff2ebff0SFam Zheng help="Interactively run command") 578983c2a77SFam Zheng parser.add_option("--snapshot", "-s", action="store_true", 579983c2a77SFam Zheng help="run tests with a snapshot") 58092fecad3SAlex Bennée parser.add_option("--genisoimage", default="genisoimage", 58192fecad3SAlex Bennée help="iso imaging tool") 5823f1e8137SRobert Foley parser.add_option("--config", "-c", default=None, 5833f1e8137SRobert Foley help="Provide config yaml for configuration. "\ 5843f1e8137SRobert Foley "See config_example.yaml for example.") 585*13336606SRobert Foley parser.add_option("--efi-aarch64", 586*13336606SRobert Foley default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd", 587*13336606SRobert Foley help="Path to efi image for aarch64 VMs.") 588ff2ebff0SFam Zheng parser.disable_interspersed_args() 589ff2ebff0SFam Zheng return parser.parse_args() 590ff2ebff0SFam Zheng 5915d676197SRobert Foleydef main(vmcls, config=None): 592ff2ebff0SFam Zheng try: 5935d676197SRobert Foley if config == None: 5945d676197SRobert Foley config = DEFAULT_CONFIG 59563a24c5eSPhilippe Mathieu-Daudé args, argv = parse_args(vmcls) 596ff2ebff0SFam Zheng if not argv and not args.build_qemu and not args.build_image: 597f03868bdSEduardo Habkost print("Nothing to do?") 598ff2ebff0SFam Zheng return 1 5993f1e8137SRobert Foley config = parse_config(config, args) 600fb3b4e6dSEduardo Habkost logging.basicConfig(level=(logging.DEBUG if args.debug 601fb3b4e6dSEduardo Habkost else logging.WARN)) 6025d676197SRobert Foley vm = vmcls(args, config=config) 603ff2ebff0SFam Zheng if args.build_image: 604ff2ebff0SFam Zheng if os.path.exists(args.image) and not args.force: 605ff2ebff0SFam Zheng sys.stderr.writelines(["Image file exists: %s\n" % args.image, 606ff2ebff0SFam Zheng "Use --force option to overwrite\n"]) 607ff2ebff0SFam Zheng return 1 608ff2ebff0SFam Zheng return vm.build_image(args.image) 609ff2ebff0SFam Zheng if args.build_qemu: 610ff2ebff0SFam Zheng vm.add_source_dir(args.build_qemu) 611ff2ebff0SFam Zheng cmd = [vm.BUILD_SCRIPT.format( 612ff2ebff0SFam Zheng configure_opts = " ".join(argv), 6133ace9be6SGerd Hoffmann jobs=int(args.jobs), 6145c2ec9b6SAlex Bennée target=args.build_target, 61541e3340aSPeter Maydell verbose = "V=1" if args.verbose else "")] 616ff2ebff0SFam Zheng else: 617ff2ebff0SFam Zheng cmd = argv 618983c2a77SFam Zheng img = args.image 619983c2a77SFam Zheng if args.snapshot: 620983c2a77SFam Zheng img += ",snapshot=on" 621983c2a77SFam Zheng vm.boot(img) 622ff2ebff0SFam Zheng vm.wait_ssh() 623ff2ebff0SFam Zheng except Exception as e: 624ff2ebff0SFam Zheng if isinstance(e, SystemExit) and e.code == 0: 625ff2ebff0SFam Zheng return 0 626ff2ebff0SFam Zheng sys.stderr.write("Failed to prepare guest environment\n") 627ff2ebff0SFam Zheng traceback.print_exc() 628ff2ebff0SFam Zheng return 2 629ff2ebff0SFam Zheng 630b3f94b2fSGerd Hoffmann exitcode = 0 631ff2ebff0SFam Zheng if vm.ssh(*cmd) != 0: 632b3f94b2fSGerd Hoffmann exitcode = 3 633bcc388dfSAlex Bennée if args.interactive: 634b3f94b2fSGerd Hoffmann vm.ssh() 635b3f94b2fSGerd Hoffmann 636b3f94b2fSGerd Hoffmann if not args.snapshot: 637b3f94b2fSGerd Hoffmann vm.graceful_shutdown() 638b3f94b2fSGerd Hoffmann 639b3f94b2fSGerd Hoffmann return exitcode 640