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 21ff2ebff0SFam Zhengimport subprocess 22ff2ebff0SFam Zhengimport hashlib 232fea3a12SAlex Bennéeimport argparse 24ff2ebff0SFam Zhengimport atexit 25ff2ebff0SFam Zhengimport tempfile 26ff2ebff0SFam Zhengimport shutil 27ff2ebff0SFam Zhengimport multiprocessing 28ff2ebff0SFam Zhengimport traceback 295d676197SRobert Foleyimport shlex 304cd57671SPhilippe Mathieu-Daudéimport json 31ff2ebff0SFam Zheng 32f4c66f17SJohn Snowfrom qemu.machine import QEMUMachine 33f4c66f17SJohn Snowfrom qemu.utils import get_info_usernet_hostfwd_port, kvm_available 34f4c66f17SJohn Snow 355d676197SRobert FoleySSH_KEY_FILE = os.path.join(os.path.dirname(__file__), 365d676197SRobert Foley "..", "keys", "id_rsa") 375d676197SRobert FoleySSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__), 385d676197SRobert Foley "..", "keys", "id_rsa.pub") 39ff2ebff0SFam Zheng 405d676197SRobert Foley# This is the standard configuration. 415d676197SRobert Foley# Any or all of these can be overridden by 425d676197SRobert Foley# passing in a config argument to the VM constructor. 435d676197SRobert FoleyDEFAULT_CONFIG = { 445d676197SRobert Foley 'cpu' : "max", 455d676197SRobert Foley 'machine' : 'pc', 465d676197SRobert Foley 'guest_user' : "qemu", 475d676197SRobert Foley 'guest_pass' : "qemupass", 489fc33bf4SAlexander von Gluck IV 'root_user' : "root", 495d676197SRobert Foley 'root_pass' : "qemupass", 505d676197SRobert Foley 'ssh_key_file' : SSH_KEY_FILE, 515d676197SRobert Foley 'ssh_pub_key_file': SSH_PUB_KEY_FILE, 525d676197SRobert Foley 'memory' : "4G", 535d676197SRobert Foley 'extra_args' : [], 545d676197SRobert Foley 'qemu_args' : "", 555d676197SRobert Foley 'dns' : "", 565d676197SRobert Foley 'ssh_port' : 0, 575d676197SRobert Foley 'install_cmds' : "", 585d676197SRobert Foley 'boot_dev_type' : "block", 595d676197SRobert Foley 'ssh_timeout' : 1, 605d676197SRobert Foley} 615d676197SRobert FoleyBOOT_DEVICE = { 625d676197SRobert Foley 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\ 635d676197SRobert Foley "-device virtio-blk,drive=drive0,bootindex=0", 645d676197SRobert Foley 'scsi' : "-device virtio-scsi-device,id=scsi "\ 655d676197SRobert Foley "-drive file={},format=raw,if=none,id=hd0 "\ 665d676197SRobert Foley "-device scsi-hd,drive=hd0,bootindex=0", 675d676197SRobert Foley} 68ff2ebff0SFam Zhengclass BaseVM(object): 69ff2ebff0SFam Zheng 70b08ba163SGerd Hoffmann envvars = [ 71b08ba163SGerd Hoffmann "https_proxy", 72b08ba163SGerd Hoffmann "http_proxy", 73b08ba163SGerd Hoffmann "ftp_proxy", 74b08ba163SGerd Hoffmann "no_proxy", 75b08ba163SGerd Hoffmann ] 76b08ba163SGerd Hoffmann 77ff2ebff0SFam Zheng # The script to run in the guest that builds QEMU 78ff2ebff0SFam Zheng BUILD_SCRIPT = "" 79ff2ebff0SFam Zheng # The guest name, to be overridden by subclasses 80ff2ebff0SFam Zheng name = "#base" 8131719c37SPhilippe Mathieu-Daudé # The guest architecture, to be overridden by subclasses 8231719c37SPhilippe Mathieu-Daudé arch = "#arch" 83b3f94b2fSGerd Hoffmann # command to halt the guest, can be overridden by subclasses 84b3f94b2fSGerd Hoffmann poweroff = "poweroff" 854a70232bSRobert Foley # Time to wait for shutdown to finish. 864a70232bSRobert Foley shutdown_timeout_default = 30 875b790481SEduardo Habkost # enable IPv6 networking 885b790481SEduardo Habkost ipv6 = True 895d676197SRobert Foley # This is the timeout on the wait for console bytes. 905d676197SRobert Foley socket_timeout = 120 91c9de3935SRobert Foley # Scale up some timeouts under TCG. 92c9de3935SRobert Foley # 4 is arbitrary, but greater than 2, 93c9de3935SRobert Foley # since we found we need to wait more than twice as long. 944a70232bSRobert Foley tcg_timeout_multiplier = 4 955d676197SRobert Foley def __init__(self, args, config=None): 96ff2ebff0SFam Zheng self._guest = None 971f335d18SRobert Foley self._genisoimage = args.genisoimage 981f335d18SRobert Foley self._build_path = args.build_path 9913336606SRobert Foley self._efi_aarch64 = args.efi_aarch64 1007bb17a92SAlex Bennée self._source_path = args.source_path 1015d676197SRobert Foley # Allow input config to override defaults. 1025d676197SRobert Foley self._config = DEFAULT_CONFIG.copy() 103eaf46a65SJohn Snow 104eaf46a65SJohn Snow # 1GB per core, minimum of 4. This is only a default. 105eaf46a65SJohn Snow mem = max(4, args.jobs) 106eaf46a65SJohn Snow self._config['memory'] = f"{mem}G" 107eaf46a65SJohn Snow 1085d676197SRobert Foley if config != None: 1095d676197SRobert Foley self._config.update(config) 1105d676197SRobert Foley self.validate_ssh_keys() 111ff2ebff0SFam Zheng self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", 112ff2ebff0SFam Zheng suffix=".tmp", 113ff2ebff0SFam Zheng dir=".")) 114ff2ebff0SFam Zheng atexit.register(shutil.rmtree, self._tmpdir) 1155d676197SRobert Foley # Copy the key files to a temporary directory. 1165d676197SRobert Foley # Also chmod the key file to agree with ssh requirements. 1175d676197SRobert Foley self._config['ssh_key'] = \ 1185d676197SRobert Foley open(self._config['ssh_key_file']).read().rstrip() 1195d676197SRobert Foley self._config['ssh_pub_key'] = \ 1205d676197SRobert Foley open(self._config['ssh_pub_key_file']).read().rstrip() 1215d676197SRobert Foley self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa") 1225d676197SRobert Foley open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key']) 1235d676197SRobert Foley subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file]) 124ff2ebff0SFam Zheng 1255d676197SRobert Foley self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") 1265d676197SRobert Foley open(self._ssh_tmp_pub_key_file, 1275d676197SRobert Foley "w").write(self._config['ssh_pub_key']) 128ff2ebff0SFam Zheng 1291f335d18SRobert Foley self.debug = args.debug 130ff14ab0cSRobert Foley self._console_log_path = None 131ff14ab0cSRobert Foley if args.log_console: 132ff14ab0cSRobert Foley self._console_log_path = \ 133ff14ab0cSRobert Foley os.path.join(os.path.expanduser("~/.cache/qemu-vm"), 134ff14ab0cSRobert Foley "{}.install.log".format(self.name)) 135ff2ebff0SFam Zheng self._stderr = sys.stderr 136ff2ebff0SFam Zheng self._devnull = open(os.devnull, "w") 137ff2ebff0SFam Zheng if self.debug: 138ff2ebff0SFam Zheng self._stdout = sys.stdout 139ff2ebff0SFam Zheng else: 140ff2ebff0SFam Zheng self._stdout = self._devnull 1415d676197SRobert Foley netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22" 142ff2ebff0SFam Zheng self._args = [ \ 1435d676197SRobert Foley "-nodefaults", "-m", self._config['memory'], 1445d676197SRobert Foley "-cpu", self._config['cpu'], 1455d676197SRobert Foley "-netdev", 1465d676197SRobert Foley netdev.format(self._config['ssh_port']) + 1475d676197SRobert Foley (",ipv6=no" if not self.ipv6 else "") + 1485d676197SRobert Foley (",dns=" + self._config['dns'] if self._config['dns'] else ""), 149ff2ebff0SFam Zheng "-device", "virtio-net-pci,netdev=vnet", 1508dd38334SGerd Hoffmann "-vnc", "127.0.0.1:0,to=20"] 1511f335d18SRobert Foley if args.jobs and args.jobs > 1: 1521f335d18SRobert Foley self._args += ["-smp", "%d" % args.jobs] 15371531bb5SPhilippe Mathieu-Daudé if kvm_available(self.arch): 1544a70232bSRobert Foley self._shutdown_timeout = self.shutdown_timeout_default 155ff2ebff0SFam Zheng self._args += ["-enable-kvm"] 156ff2ebff0SFam Zheng else: 157ff2ebff0SFam Zheng logging.info("KVM not available, not using -enable-kvm") 1584a70232bSRobert Foley self._shutdown_timeout = \ 1594a70232bSRobert Foley self.shutdown_timeout_default * self.tcg_timeout_multiplier 160ff2ebff0SFam Zheng self._data_args = [] 161ff2ebff0SFam Zheng 1625d676197SRobert Foley if self._config['qemu_args'] != None: 1635d676197SRobert Foley qemu_args = self._config['qemu_args'] 1645d676197SRobert Foley qemu_args = qemu_args.replace('\n',' ').replace('\r','') 1655d676197SRobert Foley # shlex groups quoted arguments together 1665d676197SRobert Foley # we need this to keep the quoted args together for when 1675d676197SRobert Foley # the QEMU command is issued later. 1685d676197SRobert Foley args = shlex.split(qemu_args) 1695d676197SRobert Foley self._config['extra_args'] = [] 1705d676197SRobert Foley for arg in args: 1715d676197SRobert Foley if arg: 1725d676197SRobert Foley # Preserve quotes around arguments. 1735d676197SRobert Foley # shlex above takes them out, so add them in. 1745d676197SRobert Foley if " " in arg: 1755d676197SRobert Foley arg = '"{}"'.format(arg) 1765d676197SRobert Foley self._config['extra_args'].append(arg) 1775d676197SRobert Foley 1785d676197SRobert Foley def validate_ssh_keys(self): 1795d676197SRobert Foley """Check to see if the ssh key files exist.""" 1805d676197SRobert Foley if 'ssh_key_file' not in self._config or\ 1815d676197SRobert Foley not os.path.exists(self._config['ssh_key_file']): 1825d676197SRobert Foley raise Exception("ssh key file not found.") 1835d676197SRobert Foley if 'ssh_pub_key_file' not in self._config or\ 1845d676197SRobert Foley not os.path.exists(self._config['ssh_pub_key_file']): 1855d676197SRobert Foley raise Exception("ssh pub key file not found.") 1865d676197SRobert Foley 1875d676197SRobert Foley def wait_boot(self, wait_string=None): 1885d676197SRobert Foley """Wait for the standard string we expect 1895d676197SRobert Foley on completion of a normal boot. 1905d676197SRobert Foley The user can also choose to override with an 1915d676197SRobert Foley alternate string to wait for.""" 1925d676197SRobert Foley if wait_string is None: 1935d676197SRobert Foley if self.login_prompt is None: 1945d676197SRobert Foley raise Exception("self.login_prompt not defined") 1955d676197SRobert Foley wait_string = self.login_prompt 1965d676197SRobert Foley # Intentionally bump up the default timeout under TCG, 1975d676197SRobert Foley # since the console wait below takes longer. 1985d676197SRobert Foley timeout = self.socket_timeout 1995d676197SRobert Foley if not kvm_available(self.arch): 2005d676197SRobert Foley timeout *= 8 2015d676197SRobert Foley self.console_init(timeout=timeout) 2025d676197SRobert Foley self.console_wait(wait_string) 2035d676197SRobert Foley 2045b4b4865SAlex Bennée def _download_with_cache(self, url, sha256sum=None, sha512sum=None): 205ff2ebff0SFam Zheng def check_sha256sum(fname): 206ff2ebff0SFam Zheng if not sha256sum: 207ff2ebff0SFam Zheng return True 208ff2ebff0SFam Zheng checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 2093ace9be6SGerd Hoffmann return sha256sum == checksum.decode("utf-8") 210ff2ebff0SFam Zheng 2115b4b4865SAlex Bennée def check_sha512sum(fname): 2125b4b4865SAlex Bennée if not sha512sum: 2135b4b4865SAlex Bennée return True 2145b4b4865SAlex Bennée checksum = subprocess.check_output(["sha512sum", fname]).split()[0] 2155b4b4865SAlex Bennée return sha512sum == checksum.decode("utf-8") 2165b4b4865SAlex Bennée 217ff2ebff0SFam Zheng cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 218ff2ebff0SFam Zheng if not os.path.exists(cache_dir): 219ff2ebff0SFam Zheng os.makedirs(cache_dir) 2203ace9be6SGerd Hoffmann fname = os.path.join(cache_dir, 2213ace9be6SGerd Hoffmann hashlib.sha1(url.encode("utf-8")).hexdigest()) 2225b4b4865SAlex Bennée if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname): 223ff2ebff0SFam Zheng return fname 224ff2ebff0SFam Zheng logging.debug("Downloading %s to %s...", url, fname) 225ff2ebff0SFam Zheng subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 226ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 227ff2ebff0SFam Zheng os.rename(fname + ".download", fname) 228ff2ebff0SFam Zheng return fname 229ff2ebff0SFam Zheng 230796471e9SGerd Hoffmann def _ssh_do(self, user, cmd, check): 23189adc5b9SRobert Foley ssh_cmd = ["ssh", 23289adc5b9SRobert Foley "-t", 233ff2ebff0SFam Zheng "-o", "StrictHostKeyChecking=no", 234ff2ebff0SFam Zheng "-o", "UserKnownHostsFile=" + os.devnull, 2355d676197SRobert Foley "-o", 2365d676197SRobert Foley "ConnectTimeout={}".format(self._config["ssh_timeout"]), 237339bf0c0SIlya Leoshkevich "-p", str(self.ssh_port), "-i", self._ssh_tmp_key_file, 238339bf0c0SIlya Leoshkevich "-o", "IdentitiesOnly=yes"] 23989adc5b9SRobert Foley # If not in debug mode, set ssh to quiet mode to 24089adc5b9SRobert Foley # avoid printing the results of commands. 24189adc5b9SRobert Foley if not self.debug: 24289adc5b9SRobert Foley ssh_cmd.append("-q") 243b08ba163SGerd Hoffmann for var in self.envvars: 244b08ba163SGerd Hoffmann ssh_cmd += ['-o', "SendEnv=%s" % var ] 245ff2ebff0SFam Zheng assert not isinstance(cmd, str) 246ff2ebff0SFam Zheng ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 247ff2ebff0SFam Zheng logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 248726c9a3bSFam Zheng r = subprocess.call(ssh_cmd) 249ff2ebff0SFam Zheng if check and r != 0: 250ff2ebff0SFam Zheng raise Exception("SSH command failed: %s" % cmd) 251ff2ebff0SFam Zheng return r 252ff2ebff0SFam Zheng 253ff2ebff0SFam Zheng def ssh(self, *cmd): 254df001680SRobert Foley return self._ssh_do(self._config["guest_user"], cmd, False) 255ff2ebff0SFam Zheng 256ff2ebff0SFam Zheng def ssh_root(self, *cmd): 2579fc33bf4SAlexander von Gluck IV return self._ssh_do(self._config["root_user"], cmd, False) 258ff2ebff0SFam Zheng 259ff2ebff0SFam Zheng def ssh_check(self, *cmd): 260df001680SRobert Foley self._ssh_do(self._config["guest_user"], cmd, True) 261ff2ebff0SFam Zheng 262ff2ebff0SFam Zheng def ssh_root_check(self, *cmd): 2639fc33bf4SAlexander von Gluck IV self._ssh_do(self._config["root_user"], cmd, True) 264ff2ebff0SFam Zheng 265ff2ebff0SFam Zheng def build_image(self, img): 266ff2ebff0SFam Zheng raise NotImplementedError 267ff2ebff0SFam Zheng 2681e48931cSWainer dos Santos Moschetta def exec_qemu_img(self, *args): 2691e48931cSWainer dos Santos Moschetta cmd = [os.environ.get("QEMU_IMG", "qemu-img")] 2701e48931cSWainer dos Santos Moschetta cmd.extend(list(args)) 2711e48931cSWainer dos Santos Moschetta subprocess.check_call(cmd) 2721e48931cSWainer dos Santos Moschetta 273ff2ebff0SFam Zheng def add_source_dir(self, src_dir): 2743ace9be6SGerd Hoffmann name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 275ff2ebff0SFam Zheng tarfile = os.path.join(self._tmpdir, name + ".tar") 276ff2ebff0SFam Zheng logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 277ff2ebff0SFam Zheng subprocess.check_call(["./scripts/archive-source.sh", tarfile], 278ff2ebff0SFam Zheng cwd=src_dir, stdin=self._devnull, 279ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 280ff2ebff0SFam Zheng self._data_args += ["-drive", 281ff2ebff0SFam Zheng "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 282ff2ebff0SFam Zheng (tarfile, name), 283ff2ebff0SFam Zheng "-device", 284ff2ebff0SFam Zheng "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 285ff2ebff0SFam Zheng 286ff2ebff0SFam Zheng def boot(self, img, extra_args=[]): 2875d676197SRobert Foley boot_dev = BOOT_DEVICE[self._config['boot_dev_type']] 2885d676197SRobert Foley boot_params = boot_dev.format(img) 2895d676197SRobert Foley args = self._args + boot_params.split(' ') 2905d676197SRobert Foley args += self._data_args + extra_args + self._config['extra_args'] 291ff2ebff0SFam Zheng logging.debug("QEMU args: %s", " ".join(args)) 292e56c4504SRobert Foley qemu_path = get_qemu_path(self.arch, self._build_path) 293ff14ab0cSRobert Foley 294ff14ab0cSRobert Foley # Since console_log_path is only set when the user provides the 295ff14ab0cSRobert Foley # log_console option, we will set drain_console=True so the 296ff14ab0cSRobert Foley # console is always drained. 297ff14ab0cSRobert Foley guest = QEMUMachine(binary=qemu_path, args=args, 298ff14ab0cSRobert Foley console_log=self._console_log_path, 299ff14ab0cSRobert Foley drain_console=True) 3005d676197SRobert Foley guest.set_machine(self._config['machine']) 3018dd38334SGerd Hoffmann guest.set_console() 302ff2ebff0SFam Zheng try: 303ff2ebff0SFam Zheng guest.launch() 304ff2ebff0SFam Zheng except: 305ff2ebff0SFam Zheng logging.error("Failed to launch QEMU, command line:") 306e56c4504SRobert Foley logging.error(" ".join([qemu_path] + args)) 307ff2ebff0SFam Zheng logging.error("Log:") 308ff2ebff0SFam Zheng logging.error(guest.get_log()) 309ff2ebff0SFam Zheng logging.error("QEMU version >= 2.10 is required") 310ff2ebff0SFam Zheng raise 311ff2ebff0SFam Zheng atexit.register(self.shutdown) 312ff2ebff0SFam Zheng self._guest = guest 313ff14ab0cSRobert Foley # Init console so we can start consuming the chars. 314ff14ab0cSRobert Foley self.console_init() 3159acd49e2SVladimir Sementsov-Ogievskiy usernet_info = guest.cmd("human-monitor-command", 3169acd49e2SVladimir Sementsov-Ogievskiy command_line="info usernet") 317976218cbSCleber Rosa self.ssh_port = get_info_usernet_hostfwd_port(usernet_info) 318ff2ebff0SFam Zheng if not self.ssh_port: 319ff2ebff0SFam Zheng raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 320ff2ebff0SFam Zheng usernet_info) 321ff2ebff0SFam Zheng 322ff14ab0cSRobert Foley def console_init(self, timeout = None): 323ff14ab0cSRobert Foley if timeout == None: 324ff14ab0cSRobert Foley timeout = self.socket_timeout 3258dd38334SGerd Hoffmann vm = self._guest 3268dd38334SGerd Hoffmann vm.console_socket.settimeout(timeout) 327698a64f9SGerd Hoffmann self.console_raw_path = os.path.join(vm._temp_dir, 328698a64f9SGerd Hoffmann vm._name + "-console.raw") 329698a64f9SGerd Hoffmann self.console_raw_file = open(self.console_raw_path, 'wb') 3308dd38334SGerd Hoffmann 3318dd38334SGerd Hoffmann def console_log(self, text): 3328dd38334SGerd Hoffmann for line in re.split("[\r\n]", text): 3338dd38334SGerd Hoffmann # filter out terminal escape sequences 334*86a8989dSPaolo Bonzini line = re.sub("\x1b\\[[0-9;?]*[a-zA-Z]", "", line) 335*86a8989dSPaolo Bonzini line = re.sub("\x1b\\([0-9;?]*[a-zA-Z]", "", line) 3368dd38334SGerd Hoffmann # replace unprintable chars 3378dd38334SGerd Hoffmann line = re.sub("\x1b", "<esc>", line) 3388dd38334SGerd Hoffmann line = re.sub("[\x00-\x1f]", ".", line) 3398dd38334SGerd Hoffmann line = re.sub("[\x80-\xff]", ".", line) 3408dd38334SGerd Hoffmann if line == "": 3418dd38334SGerd Hoffmann continue 3428dd38334SGerd Hoffmann # log console line 3438dd38334SGerd Hoffmann sys.stderr.write("con recv: %s\n" % line) 3448dd38334SGerd Hoffmann 34560136e06SGerd Hoffmann def console_wait(self, expect, expectalt = None): 3468dd38334SGerd Hoffmann vm = self._guest 3478dd38334SGerd Hoffmann output = "" 3488dd38334SGerd Hoffmann while True: 3498dd38334SGerd Hoffmann try: 3508dd38334SGerd Hoffmann chars = vm.console_socket.recv(1) 351698a64f9SGerd Hoffmann if self.console_raw_file: 352698a64f9SGerd Hoffmann self.console_raw_file.write(chars) 353698a64f9SGerd Hoffmann self.console_raw_file.flush() 3548dd38334SGerd Hoffmann except socket.timeout: 3558dd38334SGerd Hoffmann sys.stderr.write("console: *** read timeout ***\n") 3568dd38334SGerd Hoffmann sys.stderr.write("console: waiting for: '%s'\n" % expect) 35760136e06SGerd Hoffmann if not expectalt is None: 35860136e06SGerd Hoffmann sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt) 3598dd38334SGerd Hoffmann sys.stderr.write("console: line buffer:\n") 3608dd38334SGerd Hoffmann sys.stderr.write("\n") 3618dd38334SGerd Hoffmann self.console_log(output.rstrip()) 3628dd38334SGerd Hoffmann sys.stderr.write("\n") 3638dd38334SGerd Hoffmann raise 3648dd38334SGerd Hoffmann output += chars.decode("latin1") 3658dd38334SGerd Hoffmann if expect in output: 3668dd38334SGerd Hoffmann break 36760136e06SGerd Hoffmann if not expectalt is None and expectalt in output: 36860136e06SGerd Hoffmann break 3698dd38334SGerd Hoffmann if "\r" in output or "\n" in output: 3708dd38334SGerd Hoffmann lines = re.split("[\r\n]", output) 3718dd38334SGerd Hoffmann output = lines.pop() 3728dd38334SGerd Hoffmann if self.debug: 3738dd38334SGerd Hoffmann self.console_log("\n".join(lines)) 3748dd38334SGerd Hoffmann if self.debug: 3758dd38334SGerd Hoffmann self.console_log(output) 37660136e06SGerd Hoffmann if not expectalt is None and expectalt in output: 37760136e06SGerd Hoffmann return False 37860136e06SGerd Hoffmann return True 3798dd38334SGerd Hoffmann 3806c4f0416SGerd Hoffmann def console_consume(self): 3816c4f0416SGerd Hoffmann vm = self._guest 3826c4f0416SGerd Hoffmann output = "" 3836c4f0416SGerd Hoffmann vm.console_socket.setblocking(0) 3846c4f0416SGerd Hoffmann while True: 3856c4f0416SGerd Hoffmann try: 3866c4f0416SGerd Hoffmann chars = vm.console_socket.recv(1) 3876c4f0416SGerd Hoffmann except: 3886c4f0416SGerd Hoffmann break 3896c4f0416SGerd Hoffmann output += chars.decode("latin1") 3906c4f0416SGerd Hoffmann if "\r" in output or "\n" in output: 3916c4f0416SGerd Hoffmann lines = re.split("[\r\n]", output) 3926c4f0416SGerd Hoffmann output = lines.pop() 3936c4f0416SGerd Hoffmann if self.debug: 3946c4f0416SGerd Hoffmann self.console_log("\n".join(lines)) 3956c4f0416SGerd Hoffmann if self.debug: 3966c4f0416SGerd Hoffmann self.console_log(output) 3976c4f0416SGerd Hoffmann vm.console_socket.setblocking(1) 3986c4f0416SGerd Hoffmann 3998dd38334SGerd Hoffmann def console_send(self, command): 4008dd38334SGerd Hoffmann vm = self._guest 4018dd38334SGerd Hoffmann if self.debug: 4028dd38334SGerd Hoffmann logline = re.sub("\n", "<enter>", command) 4038dd38334SGerd Hoffmann logline = re.sub("[\x00-\x1f]", ".", logline) 4048dd38334SGerd Hoffmann sys.stderr.write("con send: %s\n" % logline) 4058dd38334SGerd Hoffmann for char in list(command): 4068dd38334SGerd Hoffmann vm.console_socket.send(char.encode("utf-8")) 4078dd38334SGerd Hoffmann time.sleep(0.01) 4088dd38334SGerd Hoffmann 4098dd38334SGerd Hoffmann def console_wait_send(self, wait, command): 4108dd38334SGerd Hoffmann self.console_wait(wait) 4118dd38334SGerd Hoffmann self.console_send(command) 4128dd38334SGerd Hoffmann 4138dd38334SGerd Hoffmann def console_ssh_init(self, prompt, user, pw): 4145d676197SRobert Foley sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \ 4155d676197SRobert Foley % self._config['ssh_pub_key'].rstrip() 4168dd38334SGerd Hoffmann self.console_wait_send("login:", "%s\n" % user) 4178dd38334SGerd Hoffmann self.console_wait_send("Password:", "%s\n" % pw) 4188dd38334SGerd Hoffmann self.console_wait_send(prompt, "mkdir .ssh\n") 4198dd38334SGerd Hoffmann self.console_wait_send(prompt, sshkey_cmd) 4208dd38334SGerd Hoffmann self.console_wait_send(prompt, "chmod 755 .ssh\n") 4218dd38334SGerd Hoffmann self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n") 4228dd38334SGerd Hoffmann 4238dd38334SGerd Hoffmann def console_sshd_config(self, prompt): 4248dd38334SGerd Hoffmann self.console_wait(prompt) 4258dd38334SGerd Hoffmann self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n") 4268dd38334SGerd Hoffmann for var in self.envvars: 4278dd38334SGerd Hoffmann self.console_wait(prompt) 4288dd38334SGerd Hoffmann self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var) 4298dd38334SGerd Hoffmann 4308dd38334SGerd Hoffmann def print_step(self, text): 4318dd38334SGerd Hoffmann sys.stderr.write("### %s ...\n" % text) 4328dd38334SGerd Hoffmann 4336ee982c9SRobert Foley def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"): 434c9de3935SRobert Foley # Allow more time for VM to boot under TCG. 435c9de3935SRobert Foley if not kvm_available(self.arch): 4364a70232bSRobert Foley seconds *= self.tcg_timeout_multiplier 437ff2ebff0SFam Zheng starttime = datetime.datetime.now() 438f5d3d218SPhilippe Mathieu-Daudé endtime = starttime + datetime.timedelta(seconds=seconds) 4396ee982c9SRobert Foley cmd_success = False 440f5d3d218SPhilippe Mathieu-Daudé while datetime.datetime.now() < endtime: 4416ee982c9SRobert Foley if wait_root and self.ssh_root(cmd) == 0: 4426ee982c9SRobert Foley cmd_success = True 443fbb3aa29SRobert Foley break 4446ee982c9SRobert Foley elif self.ssh(cmd) == 0: 4456ee982c9SRobert Foley cmd_success = True 446ff2ebff0SFam Zheng break 447f5d3d218SPhilippe Mathieu-Daudé seconds = (endtime - datetime.datetime.now()).total_seconds() 448f5d3d218SPhilippe Mathieu-Daudé logging.debug("%ds before timeout", seconds) 449ff2ebff0SFam Zheng time.sleep(1) 4506ee982c9SRobert Foley if not cmd_success: 451ff2ebff0SFam Zheng raise Exception("Timeout while waiting for guest ssh") 452ff2ebff0SFam Zheng 453ff2ebff0SFam Zheng def shutdown(self): 4544a70232bSRobert Foley self._guest.shutdown(timeout=self._shutdown_timeout) 455ff2ebff0SFam Zheng 456ff2ebff0SFam Zheng def wait(self): 4574a70232bSRobert Foley self._guest.wait(timeout=self._shutdown_timeout) 458ff2ebff0SFam Zheng 459b3f94b2fSGerd Hoffmann def graceful_shutdown(self): 460b3f94b2fSGerd Hoffmann self.ssh_root(self.poweroff) 4614a70232bSRobert Foley self._guest.wait(timeout=self._shutdown_timeout) 462b3f94b2fSGerd Hoffmann 463ff2ebff0SFam Zheng def qmp(self, *args, **kwargs): 464ff2ebff0SFam Zheng return self._guest.qmp(*args, **kwargs) 465ff2ebff0SFam Zheng 466b081986cSRobert Foley def gen_cloud_init_iso(self): 467b081986cSRobert Foley cidir = self._tmpdir 468b081986cSRobert Foley mdata = open(os.path.join(cidir, "meta-data"), "w") 469b081986cSRobert Foley name = self.name.replace(".","-") 470b081986cSRobert Foley mdata.writelines(["instance-id: {}-vm-0\n".format(name), 471b081986cSRobert Foley "local-hostname: {}-guest\n".format(name)]) 472b081986cSRobert Foley mdata.close() 473b081986cSRobert Foley udata = open(os.path.join(cidir, "user-data"), "w") 4745d676197SRobert Foley print("guest user:pw {}:{}".format(self._config['guest_user'], 4755d676197SRobert Foley self._config['guest_pass'])) 476b081986cSRobert Foley udata.writelines(["#cloud-config\n", 477b081986cSRobert Foley "chpasswd:\n", 478b081986cSRobert Foley " list: |\n", 4795d676197SRobert Foley " root:%s\n" % self._config['root_pass'], 4805d676197SRobert Foley " %s:%s\n" % (self._config['guest_user'], 4815d676197SRobert Foley self._config['guest_pass']), 482b081986cSRobert Foley " expire: False\n", 483b081986cSRobert Foley "users:\n", 4845d676197SRobert Foley " - name: %s\n" % self._config['guest_user'], 485b081986cSRobert Foley " sudo: ALL=(ALL) NOPASSWD:ALL\n", 486b081986cSRobert Foley " ssh-authorized-keys:\n", 4875d676197SRobert Foley " - %s\n" % self._config['ssh_pub_key'], 488b081986cSRobert Foley " - name: root\n", 489b081986cSRobert Foley " ssh-authorized-keys:\n", 4905d676197SRobert Foley " - %s\n" % self._config['ssh_pub_key'], 491b081986cSRobert Foley "locale: en_US.UTF-8\n"]) 492b081986cSRobert Foley proxy = os.environ.get("http_proxy") 493b081986cSRobert Foley if not proxy is None: 494b081986cSRobert Foley udata.writelines(["apt:\n", 495b081986cSRobert Foley " proxy: %s" % proxy]) 496b081986cSRobert Foley udata.close() 49792fecad3SAlex Bennée subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso", 498b081986cSRobert Foley "-volid", "cidata", "-joliet", "-rock", 499b081986cSRobert Foley "user-data", "meta-data"], 500b081986cSRobert Foley cwd=cidir, 501b081986cSRobert Foley stdin=self._devnull, stdout=self._stdout, 502b081986cSRobert Foley stderr=self._stdout) 503b081986cSRobert Foley return os.path.join(cidir, "cloud-init.iso") 504b081986cSRobert Foley 5054cd57671SPhilippe Mathieu-Daudé def get_qemu_packages_from_lcitool_json(self, json_path=None): 5064cd57671SPhilippe Mathieu-Daudé """Parse a lcitool variables json file and return the PKGS list.""" 5074cd57671SPhilippe Mathieu-Daudé if json_path is None: 5084cd57671SPhilippe Mathieu-Daudé json_path = os.path.join( 5094cd57671SPhilippe Mathieu-Daudé os.path.dirname(__file__), "generated", self.name + ".json" 5104cd57671SPhilippe Mathieu-Daudé ) 5114cd57671SPhilippe Mathieu-Daudé with open(json_path, "r") as fh: 5124cd57671SPhilippe Mathieu-Daudé return json.load(fh)["pkgs"] 5134cd57671SPhilippe Mathieu-Daudé 5144cd57671SPhilippe Mathieu-Daudé 515e56c4504SRobert Foleydef get_qemu_path(arch, build_path=None): 516e56c4504SRobert Foley """Fetch the path to the qemu binary.""" 517e56c4504SRobert Foley # If QEMU environment variable set, it takes precedence 518e56c4504SRobert Foley if "QEMU" in os.environ: 519e56c4504SRobert Foley qemu_path = os.environ["QEMU"] 520e56c4504SRobert Foley elif build_path: 521e56c4504SRobert Foley qemu_path = os.path.join(build_path, arch + "-softmmu") 522e56c4504SRobert Foley qemu_path = os.path.join(qemu_path, "qemu-system-" + arch) 523e56c4504SRobert Foley else: 524e56c4504SRobert Foley # Default is to use system path for qemu. 525e56c4504SRobert Foley qemu_path = "qemu-system-" + arch 526e56c4504SRobert Foley return qemu_path 527e56c4504SRobert Foley 52813336606SRobert Foleydef get_qemu_version(qemu_path): 52913336606SRobert Foley """Get the version number from the current QEMU, 53013336606SRobert Foley and return the major number.""" 53113336606SRobert Foley output = subprocess.check_output([qemu_path, '--version']) 53213336606SRobert Foley version_line = output.decode("utf-8") 533*86a8989dSPaolo Bonzini version_num = re.split(r' |\(', version_line)[3].split('.')[0] 53413336606SRobert Foley return int(version_num) 53513336606SRobert Foley 5363f1e8137SRobert Foleydef parse_config(config, args): 5373f1e8137SRobert Foley """ Parse yaml config and populate our config structure. 5383f1e8137SRobert Foley The yaml config allows the user to override the 5393f1e8137SRobert Foley defaults for VM parameters. In many cases these 5403f1e8137SRobert Foley defaults can be overridden without rebuilding the VM.""" 5413f1e8137SRobert Foley if args.config: 5423f1e8137SRobert Foley config_file = args.config 5433f1e8137SRobert Foley elif 'QEMU_CONFIG' in os.environ: 5443f1e8137SRobert Foley config_file = os.environ['QEMU_CONFIG'] 5453f1e8137SRobert Foley else: 5463f1e8137SRobert Foley return config 5473f1e8137SRobert Foley if not os.path.exists(config_file): 5483f1e8137SRobert Foley raise Exception("config file {} does not exist".format(config_file)) 5493f1e8137SRobert Foley # We gracefully handle importing the yaml module 5503f1e8137SRobert Foley # since it might not be installed. 5513f1e8137SRobert Foley # If we are here it means the user supplied a .yml file, 5523f1e8137SRobert Foley # so if the yaml module is not installed we will exit with error. 5533f1e8137SRobert Foley try: 5543f1e8137SRobert Foley import yaml 5553f1e8137SRobert Foley except ImportError: 5563f1e8137SRobert Foley print("The python3-yaml package is needed "\ 5573f1e8137SRobert Foley "to support config.yaml files") 5583f1e8137SRobert Foley # Instead of raising an exception we exit to avoid 5593f1e8137SRobert Foley # a raft of messy (expected) errors to stdout. 5603f1e8137SRobert Foley exit(1) 5613f1e8137SRobert Foley with open(config_file) as f: 5623f1e8137SRobert Foley yaml_dict = yaml.safe_load(f) 5633f1e8137SRobert Foley 5643f1e8137SRobert Foley if 'qemu-conf' in yaml_dict: 5653f1e8137SRobert Foley config.update(yaml_dict['qemu-conf']) 5663f1e8137SRobert Foley else: 5673f1e8137SRobert Foley raise Exception("config file {} is not valid"\ 5683f1e8137SRobert Foley " missing qemu-conf".format(config_file)) 5693f1e8137SRobert Foley return config 5703f1e8137SRobert Foley 57163a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls): 5728a6e007eSPhilippe Mathieu-Daudé 5738a6e007eSPhilippe Mathieu-Daudé def get_default_jobs(): 574b0953944SAlex Bennée if multiprocessing.cpu_count() > 1: 57563a24c5eSPhilippe Mathieu-Daudé if kvm_available(vmcls.arch): 5763ad3e36eSWainer dos Santos Moschetta return multiprocessing.cpu_count() // 2 577b0953944SAlex Bennée elif os.uname().machine == "x86_64" and \ 578b0953944SAlex Bennée vmcls.arch in ["aarch64", "x86_64", "i386"]: 579b0953944SAlex Bennée # MTTCG is available on these arches and we can allow 580b0953944SAlex Bennée # more cores. but only up to a reasonable limit. User 581b0953944SAlex Bennée # can always override these limits with --jobs. 582b0953944SAlex Bennée return min(multiprocessing.cpu_count() // 2, 8) 5838a6e007eSPhilippe Mathieu-Daudé return 1 5848a6e007eSPhilippe Mathieu-Daudé 5852fea3a12SAlex Bennée parser = argparse.ArgumentParser( 5862fea3a12SAlex Bennée formatter_class=argparse.ArgumentDefaultsHelpFormatter, 5872fea3a12SAlex Bennée description="Utility for provisioning VMs and running builds", 5882fea3a12SAlex Bennée epilog="""Remaining arguments are passed to the command. 5892fea3a12SAlex Bennée Exit codes: 0 = success, 1 = command line error, 5902fea3a12SAlex Bennée 2 = environment initialization failed, 5912fea3a12SAlex Bennée 3 = test command failed""") 5922fea3a12SAlex Bennée parser.add_argument("--debug", "-D", action="store_true", 593ff2ebff0SFam Zheng help="enable debug output") 5942fea3a12SAlex Bennée parser.add_argument("--image", "-i", default="%s.img" % vmcls.name, 595ff2ebff0SFam Zheng help="image file name") 5962fea3a12SAlex Bennée parser.add_argument("--force", "-f", action="store_true", 597ff2ebff0SFam Zheng help="force build image even if image exists") 5982fea3a12SAlex Bennée parser.add_argument("--jobs", type=int, default=get_default_jobs(), 599ff2ebff0SFam Zheng help="number of virtual CPUs") 6002fea3a12SAlex Bennée parser.add_argument("--verbose", "-V", action="store_true", 60141e3340aSPeter Maydell help="Pass V=1 to builds within the guest") 6022fea3a12SAlex Bennée parser.add_argument("--build-image", "-b", action="store_true", 603ff2ebff0SFam Zheng help="build image") 6042fea3a12SAlex Bennée parser.add_argument("--build-qemu", 605ff2ebff0SFam Zheng help="build QEMU from source in guest") 6062fea3a12SAlex Bennée parser.add_argument("--build-target", 6075c2ec9b6SAlex Bennée help="QEMU build target", default="check") 6082fea3a12SAlex Bennée parser.add_argument("--build-path", default=None, 609e56c4504SRobert Foley help="Path of build directory, "\ 610e56c4504SRobert Foley "for using build tree QEMU binary. ") 6117bb17a92SAlex Bennée parser.add_argument("--source-path", default=None, 6127bb17a92SAlex Bennée help="Path of source directory, "\ 6137bb17a92SAlex Bennée "for finding additional files. ") 6142fea3a12SAlex Bennée parser.add_argument("--interactive", "-I", action="store_true", 615ff2ebff0SFam Zheng help="Interactively run command") 6162fea3a12SAlex Bennée parser.add_argument("--snapshot", "-s", action="store_true", 617983c2a77SFam Zheng help="run tests with a snapshot") 6182fea3a12SAlex Bennée parser.add_argument("--genisoimage", default="genisoimage", 61992fecad3SAlex Bennée help="iso imaging tool") 6202fea3a12SAlex Bennée parser.add_argument("--config", "-c", default=None, 6213f1e8137SRobert Foley help="Provide config yaml for configuration. "\ 6223f1e8137SRobert Foley "See config_example.yaml for example.") 6232fea3a12SAlex Bennée parser.add_argument("--efi-aarch64", 62413336606SRobert Foley default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd", 62513336606SRobert Foley help="Path to efi image for aarch64 VMs.") 6262fea3a12SAlex Bennée parser.add_argument("--log-console", action="store_true", 627ff14ab0cSRobert Foley help="Log console to file.") 6282fea3a12SAlex Bennée parser.add_argument("commands", nargs="*", help="""Remaining 6292fea3a12SAlex Bennée commands after -- are passed to command inside the VM""") 6302fea3a12SAlex Bennée 631ff2ebff0SFam Zheng return parser.parse_args() 632ff2ebff0SFam Zheng 6335d676197SRobert Foleydef main(vmcls, config=None): 634ff2ebff0SFam Zheng try: 6355d676197SRobert Foley if config == None: 6365d676197SRobert Foley config = DEFAULT_CONFIG 6372fea3a12SAlex Bennée args = parse_args(vmcls) 6382fea3a12SAlex Bennée if not args.commands and not args.build_qemu and not args.build_image: 639f03868bdSEduardo Habkost print("Nothing to do?") 640ff2ebff0SFam Zheng return 1 6413f1e8137SRobert Foley config = parse_config(config, args) 642fb3b4e6dSEduardo Habkost logging.basicConfig(level=(logging.DEBUG if args.debug 643fb3b4e6dSEduardo Habkost else logging.WARN)) 6445d676197SRobert Foley vm = vmcls(args, config=config) 645ff2ebff0SFam Zheng if args.build_image: 646ff2ebff0SFam Zheng if os.path.exists(args.image) and not args.force: 647ff2ebff0SFam Zheng sys.stderr.writelines(["Image file exists: %s\n" % args.image, 648ff2ebff0SFam Zheng "Use --force option to overwrite\n"]) 649ff2ebff0SFam Zheng return 1 650ff2ebff0SFam Zheng return vm.build_image(args.image) 651ff2ebff0SFam Zheng if args.build_qemu: 652ff2ebff0SFam Zheng vm.add_source_dir(args.build_qemu) 653ff2ebff0SFam Zheng cmd = [vm.BUILD_SCRIPT.format( 6542fea3a12SAlex Bennée configure_opts = " ".join(args.commands), 6553ace9be6SGerd Hoffmann jobs=int(args.jobs), 6565c2ec9b6SAlex Bennée target=args.build_target, 65741e3340aSPeter Maydell verbose = "V=1" if args.verbose else "")] 658ff2ebff0SFam Zheng else: 6592fea3a12SAlex Bennée cmd = args.commands 660983c2a77SFam Zheng img = args.image 661983c2a77SFam Zheng if args.snapshot: 662983c2a77SFam Zheng img += ",snapshot=on" 663983c2a77SFam Zheng vm.boot(img) 664ff2ebff0SFam Zheng vm.wait_ssh() 665ff2ebff0SFam Zheng except Exception as e: 666ff2ebff0SFam Zheng if isinstance(e, SystemExit) and e.code == 0: 667ff2ebff0SFam Zheng return 0 668ff2ebff0SFam Zheng sys.stderr.write("Failed to prepare guest environment\n") 669ff2ebff0SFam Zheng traceback.print_exc() 670ff2ebff0SFam Zheng return 2 671ff2ebff0SFam Zheng 672b3f94b2fSGerd Hoffmann exitcode = 0 673ff2ebff0SFam Zheng if vm.ssh(*cmd) != 0: 674b3f94b2fSGerd Hoffmann exitcode = 3 675bcc388dfSAlex Bennée if args.interactive: 676b3f94b2fSGerd Hoffmann vm.ssh() 677b3f94b2fSGerd Hoffmann 678b3f94b2fSGerd Hoffmann if not args.snapshot: 679b3f94b2fSGerd Hoffmann vm.graceful_shutdown() 680b3f94b2fSGerd Hoffmann 681b3f94b2fSGerd Hoffmann return exitcode 682