| # Copyright 2013 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """VM-related helper functions/classes.""" |
| |
| import distutils.version # pylint: disable=import-error,no-name-in-module |
| import errno |
| import fcntl |
| import glob |
| import logging |
| import multiprocessing |
| import os |
| import re |
| import shutil |
| import socket |
| import tempfile |
| import time |
| |
| from chromite.cli.cros import cros_chrome_sdk |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import device |
| from chromite.lib import image_lib |
| from chromite.lib import osutils |
| from chromite.lib import path_util |
| from chromite.lib import qemu |
| from chromite.lib import remote_access |
| from chromite.lib import retry_util |
| from chromite.utils import memoize |
| |
| |
| class VMError(device.DeviceError): |
| """Exception for VM errors.""" |
| |
| |
| def VMIsUpdatable(path): |
| """Check if the existing VM image is updatable. |
| |
| Args: |
| path: Path to the VM image. |
| |
| Returns: |
| True if VM is updatable; False otherwise. |
| """ |
| table = {p.name: p for p in image_lib.GetImageDiskPartitionInfo(path)} |
| # Assume if size of the two root partitions match, the image |
| # is updatable. |
| return table["ROOT-B"].size == table["ROOT-A"].size |
| |
| |
| def CreateVMImage(image=None, board=None, updatable=True, dest_dir=None): |
| """Returns the path of the image built to run in a VM. |
| |
| By default, the returned VM is a test image that can run full update |
| testing on it. If there exists a VM image with the matching |
| |updatable| setting, this method returns the path to the existing |
| image. If |dest_dir| is set, it will copy/create the VM image to the |
| |dest_dir|. |
| |
| Args: |
| image: Path to the (non-VM) image. Defaults to None to use the latest |
| image for the board. |
| board: Board that the image was built with. If None, attempts to use the |
| configured default board. |
| updatable: Create a VM image that supports AU. |
| dest_dir: If set, create/copy the VM image to |dest|; otherwise, |
| use the folder where |image| resides. |
| """ |
| if not image and not board: |
| raise VMError("Cannot create VM when both image and board are None.") |
| |
| image_dir = os.path.dirname(image) |
| src_path = dest_path = os.path.join(image_dir, constants.TEST_IMAGE_BIN) |
| |
| if dest_dir: |
| dest_path = os.path.join(dest_dir, constants.TEST_IMAGE_BIN) |
| |
| exists = False |
| # Do not create a new VM image if a matching image already exists. |
| exists = os.path.exists(src_path) and ( |
| not updatable or VMIsUpdatable(src_path) |
| ) |
| |
| if exists and dest_dir: |
| # Copy the existing VM image to dest_dir. |
| shutil.copyfile(src_path, dest_path) |
| |
| if not exists: |
| # No existing VM image that we can reuse. Create a new VM image. |
| logging.info("Creating %s", dest_path) |
| cmd = ["./image_to_vm.sh", "--test_image"] |
| |
| if image: |
| cmd.append("--from=%s" % path_util.ToChrootPath(image_dir)) |
| |
| if updatable: |
| # image_to_vm.sh default, for clarity. |
| cmd.extend(["--disk_layout", "usb_updatable"]) |
| else: |
| # `cros build-image` default. |
| cmd.extend(["--disk_layout", "usb"]) |
| |
| if board: |
| cmd.extend(["--board", board]) |
| |
| # image_to_vm.sh only runs in chroot, but dest_dir may not be |
| # reachable from chroot. In that case, we copy it to a temporary |
| # directory in chroot, and then move it to dest_dir . |
| tempdir = None |
| if dest_dir: |
| # Create a temporary directory in chroot to store the VM |
| # image. This is to avoid the case where dest_dir is not |
| # reachable within chroot. |
| tempdir = cros_build_lib.run( |
| ["mktemp", "-d"], capture_output=True, enter_chroot=True |
| ).stdout.strip() |
| cmd.append("--to=%s" % tempdir) |
| |
| msg = "Failed to create the VM image" |
| try: |
| # When enter_chroot is true the cwd needs to be src/scripts. |
| cros_build_lib.run( |
| cmd, enter_chroot=True, cwd=constants.CROSUTILS_DIR |
| ) |
| except cros_build_lib.RunCommandError as e: |
| logging.error("%s: %s", msg, e) |
| if tempdir: |
| osutils.RmDir( |
| path_util.FromChrootPath(tempdir), ignore_missing=True |
| ) |
| raise VMError(msg) |
| |
| if dest_dir: |
| # Move VM from tempdir to dest_dir. |
| shutil.move( |
| path_util.FromChrootPath( |
| os.path.join(tempdir, constants.TEST_IMAGE_BIN) |
| ), |
| dest_path, |
| ) |
| osutils.RmDir( |
| path_util.FromChrootPath(tempdir), ignore_missing=True |
| ) |
| |
| if not os.path.exists(dest_path): |
| raise VMError(msg) |
| |
| return dest_path |
| |
| |
| class VM(device.Device): |
| """Class for managing a VM.""" |
| |
| SSH_PORT = 9222 |
| SSH_NON_KVM_CONNECT_TIMEOUT = 120 |
| IMAGE_FORMAT = "raw" |
| # kvm_* should match kvm_intel, kvm_amd, etc. |
| NESTED_KVM_GLOB = "/sys/module/kvm_*/parameters/nested" |
| |
| def __init__(self, opts) -> None: |
| """Initialize VM. |
| |
| Args: |
| opts: command line options. |
| """ |
| super().__init__(opts) |
| |
| self.qemu_path = opts.qemu_path |
| self.qemu_img_path = opts.qemu_img_path |
| self.qemu_bios_path = opts.qemu_bios_path |
| self.qemu_m = opts.qemu_m |
| self.qemu_cpu = opts.qemu_cpu |
| # x86_64 is used by default instead of aarch64 |
| self.is_x86 = True |
| self.qemu_smp = opts.qemu_smp |
| if self.qemu_smp == 0: |
| self.qemu_smp = min(8, multiprocessing.cpu_count()) |
| self.qemu_hostfwd = opts.qemu_hostfwd |
| self.qemu_args = opts.qemu_args |
| |
| if opts.enable_kvm is None: |
| self.enable_kvm = os.path.exists("/dev/kvm") |
| else: |
| self.enable_kvm = opts.enable_kvm |
| self.copy_on_write = opts.copy_on_write |
| # We don't need sudo access for software emulation or if /dev/kvm is |
| # writeable. |
| self.use_sudo = self.enable_kvm and not os.access("/dev/kvm", os.W_OK) |
| self.display = opts.display |
| self.image_path = opts.image_path |
| self.image_format = opts.image_format |
| |
| self.device = remote_access.LOCALHOST |
| self.ssh_port = self.ssh_port or opts.ssh_port or VM.SSH_PORT |
| |
| self.start = opts.start |
| self.stop = opts.stop |
| |
| self.chroot_path = opts.chroot_path |
| |
| self.wait_for_boot = opts.wait_for_boot |
| |
| self.cache_dir = os.path.abspath(opts.cache_dir) |
| assert os.path.isdir(self.cache_dir), "Cache directory doesn't exist" |
| |
| self.vm_dir = opts.vm_dir |
| if not self.vm_dir: |
| self.vm_dir = os.path.join( |
| tempfile.gettempdir(), f"cros_vm_{self.ssh_port}" |
| ) |
| self._CreateVMDir() |
| |
| self.pidfile = os.path.join(self.vm_dir, "kvm.pid") |
| self.kvm_monitor = os.path.join(self.vm_dir, "kvm.monitor") |
| self.kvm_pipe_in = "%s.in" % self.kvm_monitor # to KVM |
| self.kvm_pipe_out = "%s.out" % self.kvm_monitor # from KVM |
| self.kvm_serial = "%s.serial" % self.kvm_monitor |
| |
| self.copy_image_on_shutdown = False |
| self.image_copy_dir = None |
| |
| # Wait 2 min for non-KVM. |
| connect_timeout = ( |
| VM.SSH_CONNECT_TIMEOUT |
| if self.enable_kvm |
| else VM.SSH_NON_KVM_CONNECT_TIMEOUT |
| ) |
| self.InitRemote(connect_timeout=connect_timeout) |
| |
| def _CreateVMDir(self) -> None: |
| """Safely create vm_dir.""" |
| if not osutils.SafeMakedirs(self.vm_dir): |
| # For security, ensure that vm_dir is not a symlink, and is owned by |
| # us. |
| error_str = ( |
| "VM state dir is misconfigured; please recreate: %s" |
| % self.vm_dir |
| ) |
| assert os.path.isdir(self.vm_dir), error_str |
| assert not os.path.islink(self.vm_dir), error_str |
| assert os.stat(self.vm_dir).st_uid == os.getuid(), error_str |
| |
| def _CreateQcow2Image(self): |
| """Creates a qcow2-formatted image in the temporary VM dir. |
| |
| This image will get removed on VM shutdown. |
| |
| Returns: |
| Tuple of (path to qcow2 image, format of qcow2 image) |
| """ |
| cow_image_path = os.path.join(self.vm_dir, "qcow2.img") |
| qemu_img_args = [ |
| self.qemu_img_path, |
| "create", |
| "-f", |
| "qcow2", |
| "-o", |
| "backing_file=%s,backing_fmt=raw" % self.image_path, |
| cow_image_path, |
| ] |
| cros_build_lib.run(qemu_img_args, dryrun=self.dryrun) |
| logging.info("qcow2 image created at %s.", cow_image_path) |
| return cow_image_path, "qcow2" |
| |
| def _RmVMDir(self) -> None: |
| """Cleanup vm_dir.""" |
| osutils.RmDir(self.vm_dir, ignore_missing=True, sudo=self.use_sudo) |
| |
| @memoize.MemoizedSingleCall |
| def QemuVersion(self): |
| """Determine QEMU version. |
| |
| Returns: |
| QEMU version. |
| """ |
| version_str = cros_build_lib.run( |
| [self.qemu_path, "--version"], |
| capture_output=True, |
| dryrun=self.dryrun, |
| encoding="utf-8", |
| ).stdout |
| # version string looks like one of these: |
| # QEMU emulator version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.36), |
| # Copyright (c) 2003-2008 Fabrice Bellard |
| # |
| # QEMU emulator version 2.6.0, Copyright (c) 2003-2008 Fabrice Bellard |
| # |
| # qemu-x86_64 version 2.10.1 |
| # Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project |
| # developers |
| m = re.search(r"version ([0-9.]+)", version_str) |
| if not m: |
| raise VMError( |
| "Unable to determine QEMU version from:\n%s." % version_str |
| ) |
| return m.group(1) |
| |
| def _CheckQemuMinVersion(self) -> None: |
| """Ensure minimum QEMU version.""" |
| if self.dryrun: |
| return |
| min_qemu_version = "2.6.0" |
| logging.info("QEMU version %s", self.QemuVersion()) |
| LooseVersion = distutils.version.LooseVersion |
| if LooseVersion(self.QemuVersion()) < LooseVersion(min_qemu_version): |
| raise VMError( |
| "QEMU %s is the minimum supported version. You have %s." |
| % (min_qemu_version, self.QemuVersion()) |
| ) |
| |
| def _SetQemuPath(self) -> None: |
| """Find a suitable Qemu executable.""" |
| if self.is_x86: |
| qemu_exe = "qemu-system-x86_64" |
| else: |
| qemu_exe = "qemu-system-aarch64" |
| |
| # Pull from CIPD if needed. |
| if not self.qemu_path: |
| self.qemu_path = str( |
| qemu.InstallFromCipd(cache_dir=self.cache_dir) / qemu_exe |
| ) |
| |
| if not os.path.isfile(self.qemu_path): |
| raise VMError("QEMU not found.", self.qemu_path) |
| |
| if self.copy_on_write: |
| if not self.qemu_img_path: |
| # Look for qemu-img right next to qemu-system-x86_64. |
| self.qemu_img_path = os.path.join( |
| os.path.dirname(self.qemu_path), "qemu-img" |
| ) |
| if not os.path.isfile(self.qemu_img_path): |
| raise VMError( |
| "qemu-img not found. (Needed to create qcow2 image)." |
| ) |
| |
| logging.debug("QEMU path: %s", self.qemu_path) |
| self._CheckQemuMinVersion() |
| |
| def _GetBuiltVMImagePath(self): |
| """Get path of a locally built VM image.""" |
| vm_image_path = os.path.join( |
| constants.SOURCE_ROOT, |
| "src/build/images", |
| self.board, |
| "latest", |
| constants.TEST_IMAGE_BIN, |
| ) |
| return vm_image_path if os.path.isfile(vm_image_path) else None |
| |
| def _GetCacheVMImagePath(self): |
| """Get path of a cached VM image.""" |
| cache_path = cros_chrome_sdk.SDKFetcher.GetCachePath( |
| constants.TEST_IMAGE_TAR, self.cache_dir, self.board |
| ) |
| if cache_path: |
| vm_image = os.path.join(cache_path, constants.TEST_IMAGE_BIN) |
| if os.path.isfile(vm_image): |
| return vm_image |
| return None |
| |
| def _SetVMImagePath(self) -> None: |
| """Detect VM image path in SDK and chroot.""" |
| if not self.image_path: |
| self.image_path = ( |
| self._GetCacheVMImagePath() or self._GetBuiltVMImagePath() |
| ) |
| if not self.image_path: |
| raise VMError( |
| "No VM image found. Use cros chrome-sdk --download-vm." |
| ) |
| if not os.path.isfile(self.image_path): |
| # Checks if the image path points to a directory containing the bin |
| # file. |
| image_path = os.path.join(self.image_path, constants.TEST_IMAGE_BIN) |
| if os.path.isfile(image_path): |
| self.image_path = image_path |
| else: |
| raise VMError("VM image does not exist: %s" % self.image_path) |
| logging.debug("VM image path: %s", self.image_path) |
| |
| def _SetBoard(self) -> None: |
| """Sets the board. |
| |
| Picks the first non-None board from the user-specified board, |
| SDK environment variable, cros default board. |
| |
| Raises: |
| DieSystemExit: If a board cannot be found. |
| """ |
| if not self.board: |
| sdk_board_env = os.environ.get( |
| cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV |
| ) |
| self.board = cros_build_lib.GetBoard(sdk_board_env, strict=True) |
| |
| # TODO: b/321778557 - Remove hacking "arm64" check. |
| if self.board.startswith("arm64"): |
| self.is_x86 = False |
| |
| def _WaitForSSHPort(self, sleep=5) -> None: |
| """Wait for SSH port to become available.""" |
| |
| class _SSHPortInUseError(Exception): |
| """Exception for _CheckSSHPortBusy to throw.""" |
| |
| def _CheckSSHPortBusy(ssh_port) -> None: |
| """Check if the SSH port is in use.""" |
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| try: |
| sock.bind((remote_access.LOCALHOST_IP, ssh_port)) |
| except socket.error as e: |
| if e.errno == errno.EADDRINUSE: |
| logging.info("SSH port %d in use...", self.ssh_port) |
| raise _SSHPortInUseError() |
| finally: |
| sock.close() |
| |
| try: |
| retry_util.RetryException( |
| exception=_SSHPortInUseError, |
| max_retry=10, |
| functor=lambda: _CheckSSHPortBusy(self.ssh_port), |
| sleep=sleep, |
| ) |
| except _SSHPortInUseError: |
| raise VMError("SSH port %d in use" % self.ssh_port) |
| |
| def Run(self) -> None: |
| """Perform an action, one of start, stop, or run a command in the VM.""" |
| if not self.start and not self.stop and not self.cmd: |
| raise VMError("Must specify one of start, stop, or cmd.") |
| |
| if self.start: |
| self.Start() |
| if self.cmd: |
| if not self.IsRunning(): |
| raise VMError("VM not running.") |
| self.run(self.cmd, stream_output=True) |
| if self.stop: |
| self.Stop() |
| |
| def _GetQemuArgs(self, image_path, image_format): |
| """Returns the args to qemu used to launch the VM. |
| |
| Args: |
| image_path: Path to QEMU image. |
| image_format: Format of the image. |
| """ |
| if self.is_x86: |
| if not self.qemu_cpu: |
| self.qemu_cpu = ( |
| "Haswell-noTSX,vendor=GenuineIntel,-invpcid,-tsc-deadline" |
| ) |
| |
| # Append 'check' to warn if the requested CPU is not fully |
| # supported. |
| if "check" not in self.qemu_cpu.split(","): |
| self.qemu_cpu += ",check" |
| # Append 'vmx=on' if the host supports nested virtualization. It |
| # can be enabled via 'vmx+' or 'vmx=on' (or similarly disabled) so |
| # just test for the presence of 'vmx'. For more details, see: |
| # https://docs.kernel.org/virt/kvm/x86/nested-vmx.html |
| if "vmx" not in self.qemu_cpu and self.enable_kvm: |
| for f in glob.glob(self.NESTED_KVM_GLOB): |
| if cros_build_lib.BooleanShellValue( |
| osutils.ReadFile(f).strip(), False |
| ): |
| self.qemu_cpu += ",vmx=on,svm=on" |
| break |
| else: |
| if not self.qemu_cpu: |
| self.qemu_cpu = "cortex-a72" |
| |
| qemu_args = [self.qemu_path] |
| if self.qemu_bios_path: |
| if not os.path.isdir(self.qemu_bios_path): |
| raise VMError( |
| "Invalid QEMU bios path: %s" % self.qemu_bios_path |
| ) |
| qemu_args += ["-L", self.qemu_bios_path] |
| |
| qemu_args += [ |
| "-m", |
| self.qemu_m, |
| "-smp", |
| str(self.qemu_smp), |
| "-daemonize", |
| "-pidfile", |
| self.pidfile, |
| "-chardev", |
| "pipe,id=control_pipe,path=%s" % self.kvm_monitor, |
| "-serial", |
| "file:%s" % self.kvm_serial, |
| "-mon", |
| "chardev=control_pipe", |
| "-cpu", |
| self.qemu_cpu, |
| "-usb", |
| "-device", |
| "nec-usb-xhci", |
| "-device", |
| "usb-tablet", |
| "-device", |
| "usb-kbd", |
| "-device", |
| "virtio-net,netdev=eth0", |
| "-device", |
| "virtio-scsi-pci,id=scsi", |
| "-device", |
| "virtio-rng", |
| "-device", |
| # rotation_rate=1 is treated as "non-rotational" |
| "scsi-hd,drive=hd,rotation_rate=1", |
| "-drive", |
| "if=none,id=hd,file=%s,cache=unsafe,format=%s" |
| % (image_path, image_format), |
| ] |
| # netdev args, including hostfwds. |
| netdev_args = "user,id=eth0,net=10.0.2.0/27,hostfwd=tcp:%s:%d-:%d" % ( |
| remote_access.LOCALHOST_IP, |
| self.ssh_port, |
| remote_access.DEFAULT_SSH_PORT, |
| ) |
| if self.qemu_hostfwd: |
| for hostfwd in self.qemu_hostfwd: |
| netdev_args += ",hostfwd=%s" % hostfwd |
| qemu_args += ["-netdev", netdev_args] |
| |
| if self.qemu_args: |
| for arg in self.qemu_args: |
| qemu_args += arg.split() |
| if self.enable_kvm: |
| qemu_args += ["-enable-kvm"] |
| if not self.display: |
| qemu_args += ["-display", "none"] |
| |
| if self.is_x86: |
| qemu_args += [ |
| "-vga", |
| "virtio", |
| ] |
| else: |
| qemu_args += [ |
| "-M", |
| "virt", |
| "-vga", |
| "none", |
| "-bios", |
| "edk2-aarch64-code.fd", |
| ] |
| |
| return qemu_args |
| |
| def Start(self, retries=1) -> None: |
| """Start the VM. |
| |
| Args: |
| retries: Number of times to retry launching the VM if it fails to |
| boot-up. |
| """ |
| self._SetBoard() |
| # KVM is currently not supported on arm64. |
| if not self.is_x86: |
| self.enable_kvm = False |
| if not self.enable_kvm: |
| logging.warning("KVM is not supported; Chrome VM will be slow") |
| self._SetQemuPath() |
| self._SetVMImagePath() |
| logging.info("Pid file: %s", self.pidfile) |
| |
| for attempt in range(0, retries + 1): |
| self.Stop() |
| |
| logging.debug("Start VM, attempt #%d", attempt) |
| |
| self._CreateVMDir() |
| image_path = self.image_path |
| image_format = self.image_format |
| if self.copy_on_write: |
| image_path, image_format = self._CreateQcow2Image() |
| qemu_args = self._GetQemuArgs(image_path, image_format) |
| # Make sure we can read these files later on by creating them as |
| # ourselves. |
| osutils.Touch(self.kvm_serial) |
| for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]: |
| os.mkfifo(pipe, 0o600) |
| osutils.Touch(self.pidfile) |
| |
| # Add use_sudo support to cros_build_lib.run. |
| run = ( |
| cros_build_lib.sudo_run if self.use_sudo else cros_build_lib.run |
| ) |
| run(qemu_args, dryrun=self.dryrun) |
| try: |
| if self.wait_for_boot: |
| self.WaitForBoot() |
| return |
| except device.DeviceError: |
| if attempt == retries: |
| raise |
| else: |
| logging.warning("Error when launching VM. Retrying...") |
| |
| def _GetVMPid(self): |
| """Get the pid of the VM. |
| |
| Returns: |
| pid of the VM. |
| """ |
| if not os.path.exists(self.vm_dir): |
| logging.debug("%s not present.", self.vm_dir) |
| return 0 |
| |
| if not os.path.exists(self.pidfile): |
| logging.info("%s does not exist.", self.pidfile) |
| return 0 |
| |
| pid = osutils.ReadFile(self.pidfile).rstrip() |
| if not pid.isdigit(): |
| # Ignore blank/empty files. |
| if pid: |
| logging.error("%s in %s is not a pid.", pid, self.pidfile) |
| return 0 |
| |
| return int(pid) |
| |
| def IsRunning(self): |
| """Returns True if there's a running VM. |
| |
| Returns: |
| True if there's a running VM. |
| """ |
| pid = self._GetVMPid() |
| if not pid: |
| return False |
| |
| # Make sure the process actually exists. |
| return os.path.isdir("/proc/%i" % pid) |
| |
| def SaveVMImageOnShutdown(self, output_dir) -> None: |
| """Take a VM snapshot via savevm and signal to save the VM image later. |
| |
| Args: |
| output_dir: A path specifying the directory that the VM image should |
| be saved to. |
| """ |
| logging.debug("Taking VM snapshot") |
| self.copy_image_on_shutdown = True |
| self.image_copy_dir = output_dir |
| if not self.copy_on_write: |
| logging.warning( |
| "Attempting to take a VM snapshot without --copy-on-write. " |
| "Saved VM image may not contain the desired snapshot." |
| ) |
| with open(self.kvm_pipe_in, "w", encoding="utf-8") as monitor_pipe: |
| # Saving the snapshot will take an indeterminate amount of time, so |
| # also send a fake command that the monitor will complain about so |
| # we can know when the snapshot saving is done. |
| monitor_pipe.write("savevm chromite_lib_vm_snapshot\n") |
| monitor_pipe.write("thisisafakecommand\n") |
| with open(self.kvm_pipe_out, encoding="utf-8") as monitor_pipe: |
| # Set reads to be non-blocking |
| fd = monitor_pipe.fileno() |
| cur_flags = fcntl.fcntl(fd, fcntl.F_GETFL) |
| fcntl.fcntl(fd, fcntl.F_SETFL, cur_flags | os.O_NONBLOCK) |
| # 30 second timeout. |
| success = False |
| start_time = time.time() |
| while time.time() - start_time < 30: |
| try: |
| line = monitor_pipe.readline() |
| if "thisisafakecommand" in line: |
| logging.debug("Finished attempting to take VM snapshot") |
| success = True |
| break |
| logging.debug("VM monitor output: %s", line) |
| except IOError: |
| time.sleep(1) |
| if not success: |
| logging.warning("Timed out trying to take VM snapshot") |
| |
| def _KillVM(self) -> None: |
| """Kill the VM process.""" |
| pid = self._GetVMPid() |
| if pid: |
| # Add use_sudo support to cros_build_lib.run. |
| run = ( |
| cros_build_lib.sudo_run if self.use_sudo else cros_build_lib.run |
| ) |
| run(["kill", "-9", str(pid)], check=False, dryrun=self.dryrun) |
| |
| def _MaybeCopyVMImage(self) -> None: |
| """Saves the VM image to a location on disk if previously told to.""" |
| if not self.copy_image_on_shutdown: |
| return |
| if not self.image_copy_dir: |
| logging.debug("Told to copy VM image, but no output directory set") |
| return |
| shutil.copy( |
| self.image_path, |
| os.path.join( |
| self.image_copy_dir, os.path.basename(self.image_path) |
| ), |
| ) |
| |
| def Stop(self) -> None: |
| """Stop the VM.""" |
| logging.debug("Stop VM") |
| |
| self._KillVM() |
| self._WaitForSSHPort() |
| self._MaybeCopyVMImage() |
| self._RmVMDir() |
| |
| def _WaitForProcs(self, sleep=2) -> None: |
| """Wait for expected processes to launch.""" |
| |
| class _TooFewPidsException(Exception): |
| """Exception for _GetRunningPids to throw.""" |
| |
| def _GetRunningPids(exe, numpids) -> None: |
| pids = self.remote.GetRunningPids(exe, full_path=False) |
| logging.info("%s pids: %s", exe, repr(pids)) |
| if len(pids) < numpids: |
| raise _TooFewPidsException() |
| |
| def _WaitForProc(exe, numpids) -> None: |
| try: |
| retry_util.RetryException( |
| exception=_TooFewPidsException, |
| max_retry=5, |
| functor=lambda: _GetRunningPids(exe, numpids), |
| sleep=sleep, |
| ) |
| except _TooFewPidsException: |
| raise VMError( |
| "_WaitForProcs failed: timed out while waiting for " |
| "%d %s processes to start." % (numpids, exe) |
| ) |
| |
| # We could also wait for session_manager, nacl_helper, etc., but chrome |
| # is the long pole. We expect the parent, 2 zygotes, gpu-process, |
| # utility-process, 3 renderers. |
| _WaitForProc("chrome", 8) |
| |
| def WaitForBoot(self, max_retry=10, sleep=5) -> None: |
| """Wait for the VM to boot up. |
| |
| Wait for ssh connection to become active, and wait for all expected |
| chrome processes to be launched. Set max_retry to a lower value since we |
| can easily restart the VM if something is stuck and timing out. |
| """ |
| if not os.path.exists(self.vm_dir): |
| self.Start() |
| |
| super().WaitForBoot(sleep=sleep, max_retry=max_retry) |
| |
| # Chrome can take a while to start with software emulation. |
| if not self.enable_kvm: |
| self._WaitForProcs() |
| |
| @staticmethod |
| def GetParser(parser=None): |
| """Parse a list of args. |
| |
| Returns: |
| commandline.ArgumentParser |
| """ |
| parser = device.Device.GetParser(parser) |
| parser.add_argument( |
| "--start", action="store_true", default=False, help="Start the VM." |
| ) |
| parser.add_argument( |
| "--stop", action="store_true", default=False, help="Stop the VM." |
| ) |
| parser.add_argument( |
| "--image-path", |
| type="str_path", |
| help="Path to VM image to launch with --start.", |
| ) |
| parser.add_argument( |
| "--image-format", |
| default=VM.IMAGE_FORMAT, |
| help="Format of the VM image (raw, qcow2, ...).", |
| ) |
| parser.add_argument( |
| "--copy-on-write", |
| action="store_true", |
| default=False, |
| help="Generates a temporary copy-on-write image backed " |
| "by the normal boot image. All filesystem changes " |
| "will instead be reflected in the temporary " |
| "image.", |
| ) |
| parser.add_argument( |
| "--disable-kvm", |
| dest="enable_kvm", |
| action="store_false", |
| default=None, |
| help="Disable KVM, use software emulation.", |
| ) |
| parser.add_bool_argument( |
| "--display", |
| True, |
| "Enable display (VNC on port 5900)", |
| "Disable display", |
| ) |
| parser.add_argument( |
| "--ssh-port", type=int, help="ssh port to communicate with VM." |
| ) |
| parser.add_argument( |
| "--chroot-path", |
| type="str_path", |
| default=os.path.join( |
| constants.SOURCE_ROOT, constants.DEFAULT_CHROOT_DIR |
| ), |
| ) |
| parser.add_argument( |
| "--cache-dir", |
| type="str_path", |
| default=path_util.GetCacheDir(), |
| help="Cache directory to use.", |
| ) |
| parser.add_argument( |
| "--vm-dir", type="str_path", help="Temp VM directory to use." |
| ) |
| parser.add_bool_argument( |
| "--wait-for-boot", |
| True, |
| "Wait for the VM to boot after starting.", |
| "Don't wait for the VM to boot after starting.", |
| ) |
| |
| group = parser.add_argument_group("QEMU Options") |
| group.add_argument( |
| "--qemu-path", |
| metavar="PATH", |
| type="str_path", |
| help="Path of qemu binary to launch with --start.", |
| ) |
| group.add_argument( |
| "--qemu-m", |
| metavar="MEM", |
| type=str, |
| default="8G", |
| help="Memory argument that will be passed to qemu.", |
| ) |
| group.add_argument( |
| "--qemu-smp", |
| metavar="NCPUS", |
| type=int, |
| default="0", |
| help="SMP argument that will be passed to qemu. (0 " |
| "means auto-detection.)", |
| ) |
| group.add_argument( |
| "--qemu-cpu", |
| metavar="CPU", |
| type=str, |
| help="CPU argument that will be passed to qemu.", |
| ) |
| group.add_argument( |
| "--qemu-bios-path", |
| metavar="PATH", |
| type="str_path", |
| help="Path of directory with qemu bios files.", |
| ) |
| group.add_argument( |
| "--qemu-hostfwd", |
| metavar="PORTS", |
| action="append", |
| help="Ports to forward from the VM to the host in the " |
| "QEMU hostfwd format, eg tcp:127.0.0.1:12345-:54321 to " |
| "forward port 54321 on the VM to 12345 on the host.", |
| ) |
| group.add_argument( |
| "--qemu-args", |
| metavar="ARGS", |
| action="append", |
| help="Additional args to pass to qemu. Note that if " |
| "you want to pass an argument that starts with a " |
| "dash, e.g. -display you will need to enclose it " |
| "in quotes and add a space at the beginning: " |
| '" -display ..."', |
| ) |
| group.add_argument( |
| "--qemu-img-path", |
| metavar="PATH", |
| type="str_path", |
| help="Path to qemu-img binary used to create temporary " |
| "copy-on-write images.", |
| ) |
| |
| return parser |