| # -*- coding: utf-8 -*- |
| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # 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.""" |
| |
| from __future__ import print_function |
| |
| import distutils.version # pylint: disable=import-error,no-name-in-module |
| import errno |
| import fcntl |
| import glob |
| import multiprocessing |
| import os |
| import re |
| import shutil |
| import socket |
| import sys |
| 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 cros_logging as logging |
| 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 remote_access |
| from chromite.lib import retry_util |
| from chromite.utils import memoize |
| |
| |
| assert sys.version_info >= (3, 6), 'This module requires Python 3.6+' |
| |
| |
| 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.VM_IMAGE_BIN) |
| |
| if dest_dir: |
| dest_path = os.path.join(dest_dir, constants.VM_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 = [os.path.join(constants.CROSUTILS_DIR, 'image_to_vm.sh'), |
| '--test_image'] |
| |
| if image: |
| cmd.append('--from=%s' % path_util.ToChrootPath(image_dir)) |
| |
| if updatable: |
| cmd.extend(['--disk_layout', '2gb-rootfs-updatable']) |
| |
| 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).output.strip() |
| cmd.append('--to=%s' % tempdir) |
| |
| msg = 'Failed to create the VM image' |
| try: |
| cros_build_lib.run(cmd, enter_chroot=True, cwd=constants.SOURCE_ROOT) |
| 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.VM_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): |
| """Initialize VM. |
| |
| Args: |
| opts: command line options. |
| """ |
| super(VM, self).__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 |
| 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.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(osutils.GetGlobalTempDir(), |
| 'cros_vm_%d' % 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): |
| """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' % 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): |
| """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): |
| """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): |
| """Find a suitable Qemu executable.""" |
| qemu_exe = 'qemu-system-x86_64' |
| qemu_exe_path = os.path.join('usr/bin', qemu_exe) |
| |
| # Check SDK cache. |
| if not self.qemu_path: |
| qemu_dir = cros_chrome_sdk.SDKFetcher.GetCachePath( |
| cros_chrome_sdk.SDKFetcher.QEMU_BIN_PATH, self.cache_dir, self.board) |
| if qemu_dir: |
| qemu_path = os.path.join(qemu_dir, qemu_exe_path) |
| if os.path.isfile(qemu_path): |
| self.qemu_path = qemu_path |
| |
| # Check chroot. |
| if not self.qemu_path: |
| qemu_path = os.path.join(self.chroot_path, qemu_exe_path) |
| if os.path.isfile(qemu_path): |
| self.qemu_path = qemu_path |
| |
| # Check system. |
| if not self.qemu_path: |
| logging.warning('Using system QEMU.') |
| self.qemu_path = osutils.Which(qemu_exe) |
| |
| if not self.qemu_path or not os.path.isfile(self.qemu_path): |
| raise VMError('QEMU not found.') |
| |
| 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.VM_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.VM_IMAGE_TAR, self.cache_dir, self.board) |
| if cache_path: |
| vm_image = os.path.join(cache_path, constants.VM_IMAGE_BIN) |
| if os.path.isfile(vm_image): |
| return vm_image |
| return None |
| |
| def _SetVMImagePath(self): |
| """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.VM_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): |
| """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 self.board: |
| return |
| sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV) |
| self.board = cros_build_lib.GetBoard(sdk_board_env, strict=True) |
| |
| def _WaitForSSHPort(self, sleep=5): |
| """Wait for SSH port to become available.""" |
| class _SSHPortInUseError(Exception): |
| """Exception for _CheckSSHPortBusy to throw.""" |
| |
| def _CheckSSHPortBusy(ssh_port): |
| """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): |
| """Performs 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.remote_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. |
| """ |
| # 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://www.kernel.org/doc/Documentation/virtual/kvm/nested-vmx.txt |
| 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' |
| break |
| |
| 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), '-vga', 'virtio', |
| '-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', 'usb-tablet', |
| '-device', 'virtio-net,netdev=eth0', |
| '-device', 'virtio-scsi-pci,id=scsi', |
| '-device', 'virtio-rng', |
| '-device', 'scsi-hd,drive=hd', |
| '-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'] |
| |
| return qemu_args |
| |
| def Start(self, retries=1): |
| """Start the VM. |
| |
| Args: |
| retries: Number of times to retry launching the VM if it fails to boot-up. |
| """ |
| if not self.enable_kvm: |
| logging.warning('KVM is not supported; Chrome VM will be slow') |
| self._SetBoard() |
| 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: |
| 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): |
| """Takes a VM snapshot via savevm and signals 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') 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) 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): |
| """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): |
| """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): |
| """Stop the VM.""" |
| logging.debug('Stop VM') |
| |
| self._KillVM() |
| self._WaitForSSHPort() |
| self._MaybeCopyVMImage() |
| self._RmVMDir() |
| |
| def _WaitForProcs(self, sleep=2): |
| """Wait for expected processes to launch.""" |
| class _TooFewPidsException(Exception): |
| """Exception for _GetRunningPids to throw.""" |
| |
| def _GetRunningPids(exe, numpids): |
| 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): |
| 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=3, sleep=5): |
| """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(VM, self).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(): |
| """Parse a list of args. |
| |
| Args: |
| argv: list of command line arguments. |
| |
| Returns: |
| List of parsed opts. |
| """ |
| parser = device.Device.GetParser() |
| 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='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('--qemu-path', type='path', |
| help='Path of qemu binary to launch with --start.') |
| parser.add_argument('--qemu-m', type=str, default='8G', |
| help='Memory argument that will be passed to qemu.') |
| parser.add_argument('--qemu-smp', type=int, default='0', |
| help='SMP argument that will be passed to qemu. (0 ' |
| 'means auto-detection.)') |
| # TODO(pwang): replace SandyBridge to Haswell-noTSX once lab machine |
| # running VMTest all migrate to GCE. |
| parser.add_argument('--qemu-cpu', type=str, |
| default='SandyBridge,-invpcid,-tsc-deadline', |
| help='CPU argument that will be passed to qemu.') |
| parser.add_argument('--qemu-bios-path', type='path', |
| help='Path of directory with qemu bios files.') |
| parser.add_argument('--qemu-hostfwd', 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.') |
| parser.add_argument('--qemu-args', action='append', |
| help='Additional args to pass to qemu.') |
| 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('--qemu-img-path', type='path', |
| help='Path to qemu-img binary used to create temporary ' |
| 'copy-on-write images.') |
| parser.add_argument('--disable-kvm', dest='enable_kvm', |
| action='store_false', default=None, |
| help='Disable KVM, use software emulation.') |
| parser.add_argument('--no-display', dest='display', |
| action='store_false', default=True, |
| help='Do not display video output.') |
| parser.add_argument('--ssh-port', type=int, |
| help='ssh port to communicate with VM.') |
| parser.add_argument('--chroot-path', type='path', |
| default=os.path.join(constants.SOURCE_ROOT, |
| constants.DEFAULT_CHROOT_DIR)) |
| parser.add_argument('--cache-dir', type='path', |
| default=path_util.GetCacheDir(), |
| help='Cache directory to use.') |
| parser.add_argument('--vm-dir', type='path', |
| help='Temp VM directory to use.') |
| return parser |