| # -*- coding: utf-8 -*- |
| # Copyright 2017 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. |
| |
| """Module to manage a setup of moblab and its DUT VMs.""" |
| |
| from __future__ import print_function |
| |
| import collections |
| import contextlib |
| import json |
| import os |
| import random |
| import shutil |
| import sys |
| |
| from chromite.lib import chroot_util |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import osutils |
| from chromite.lib import path_util |
| from chromite.lib import pformat |
| from chromite.lib import retry_util |
| |
| |
| assert sys.version_info >= (3, 6), 'This module requires Python 3.6+' |
| |
| |
| _CONFIG_FILE_NAME = 'moblabvm.json' |
| |
| # Keys into the config dict persisted to disk |
| _CONFIG_INITIALIZED = 'initialized' |
| _CONFIG_STARTED = 'started' |
| _CONFIG_MOBLAB_IMAGE = 'image_path_moblab' |
| _CONFIG_DUT_IMAGE = 'image_path_dut' |
| _CONFIG_MOBLAB_DISK = 'disk_path_moblab' |
| # Some paths are stored as relative paths in the config so that we can freely |
| # move around the workspace on disk. They're always used as absolute paths once |
| # loaded, because absolute paths are easier to work with. |
| _CONFIG_RELATIVE_PATH_KEYS = ( |
| _CONFIG_MOBLAB_IMAGE, |
| _CONFIG_DUT_IMAGE, |
| _CONFIG_MOBLAB_DISK, |
| ) |
| |
| _CONFIG_MOBLAB_SSH_PORT = 'ssh_port_moblab' |
| _CONFIG_DUT_SSH_PORT = 'ssh_port_dut' |
| _CONFIG_NETWORK_BRIDGE = 'network_bridge' |
| _CONFIG_MOBLAB_TAP_DEV = 'tap_moblab' |
| _CONFIG_DUT_TAP_DEV = 'tap_dut' |
| _CONFIG_MOBLAB_TAP_MAC = 'mac_tap_moblab' |
| _CONFIG_DUT_TAP_MAC = 'mac_tap_dut' |
| |
| # Paths within the workspace |
| _WORKSPACE_MOBLAB_DIR = 'moblab_image' |
| _WORKSPACE_DUT_DIR = 'dut_image' |
| |
| |
| class MoblabVmError(Exception): |
| """Base exception for all ya folks who want to catch-all.""" |
| |
| |
| class RetriableError(MoblabVmError): |
| """Some error happened where the best option is perhaps to retry.""" |
| |
| |
| class SetupError(MoblabVmError): |
| """Some error happened while setting up the VMs.""" |
| |
| |
| class StartError(MoblabVmError): |
| """Some error happened while setting up the VMs.""" |
| |
| |
| class MoblabVm(object): |
| """Manage a setup of VMs required for a VM-only moblab setup. |
| |
| See cli/cros/cros_moblabvm.py for details about the setup. |
| """ |
| |
| def __init__(self, workspace_dir, chroot_dir=None): |
| """Initialize us. |
| |
| Args: |
| workspace_dir: An existing directory to be used for to drop files |
| necessary for the setup. |
| chroot_dir: Specific chroot to pass to all cros_vm commands. |
| """ |
| self.workspace = workspace_dir |
| self.chroot = chroot_dir |
| self._config_path = os.path.join(self.workspace, _CONFIG_FILE_NAME) |
| self._config = self._LoadConfig() |
| |
| @property |
| def initialized(self): |
| """Is this MoblabVm initialized? Returns bool.""" |
| return bool(self._config[_CONFIG_INITIALIZED]) |
| |
| @property |
| def running(self): |
| """Is this MoblabVm running? Returns bool.""" |
| return (bool(self._config[_CONFIG_STARTED]) and |
| bool(self._config[_CONFIG_MOBLAB_SSH_PORT])) |
| |
| @property |
| def moblab_ssh_port(self): |
| """The (str) SSH port to connect to the moblab VM.""" |
| return str(self._config[_CONFIG_MOBLAB_SSH_PORT]) |
| |
| @property |
| def moblab_internal_mac(self): |
| """The MAC address used by moblab VM for the internal network. |
| |
| This is the network that moblab uses to connect to its DUTs. |
| """ |
| return self._config[_CONFIG_MOBLAB_TAP_MAC] |
| |
| @property |
| def dut_running(self): |
| """Is the sub-DUT VM running? Returns bool. |
| |
| A moblabvm may be launched without any sub-DUTs. |
| """ |
| return (bool(self._config[_CONFIG_STARTED]) and |
| bool(self._config[_CONFIG_DUT_SSH_PORT])) |
| |
| @property |
| def dut_ssh_port(self): |
| """The (str) SSH port to connect to the sub-DUT VM.""" |
| return str(self._config[_CONFIG_DUT_SSH_PORT]) |
| |
| @property |
| def dut_internal_mac(self): |
| """The MAC address used by sub-DUT VM for the internal network. |
| |
| This is the network that moblab uses to connect to its DUTs. |
| """ |
| return self._config[_CONFIG_DUT_TAP_MAC] |
| |
| def Create(self, moblab_image_dir, dut_image_dir='', create_vm_images=True): |
| """Create a new moblabvm setup. |
| |
| Args: |
| moblab_image_dir: A directory containing the image for moblab. This |
| directory should ideally contain all the files output by build_image |
| because they are needed to manipulate the image properly. |
| dut_image_dir: A directory containing the image for the sub-DUT. Only |
| required if a DUT should be attached to the moblab VM. This directory |
| should ideally contain all the files output by build_image because |
| they are needed to manipulate the image properly. |
| create_vm_images: If True, the source directories contain test images that |
| need to be converted to vm images. If False, the source directories |
| should already contain VM images to be used directly. |
| """ |
| if self._config[_CONFIG_INITIALIZED]: |
| raise SetupError('Cannot overwrite existing setup at %s' % |
| self.workspace) |
| |
| logging.notice('Initializing workspace in %s', self.workspace) |
| logging.notice('This involves creating some VM images. ' |
| 'May take a few minutes.') |
| |
| logging.notice('Preparing moblab image...') |
| moblab_dir = os.path.join(self.workspace, _WORKSPACE_MOBLAB_DIR) |
| osutils.SafeMakedirsNonRoot(moblab_dir) |
| if create_vm_images: |
| self._config[_CONFIG_MOBLAB_IMAGE] = _CreateVMImage( |
| moblab_image_dir, moblab_dir) |
| else: |
| self._config[_CONFIG_MOBLAB_IMAGE] = self._CopyVMImage( |
| moblab_image_dir, moblab_dir) |
| |
| logging.notice('Generating moblab external disk...') |
| moblab_disk = _CreateMoblabDisk(moblab_dir) |
| self._config[_CONFIG_MOBLAB_DISK] = moblab_disk |
| |
| if dut_image_dir: |
| logging.notice('Preparing dut image...') |
| dut_dir = os.path.join(self.workspace, _WORKSPACE_DUT_DIR) |
| osutils.SafeMakedirsNonRoot(dut_dir) |
| if create_vm_images: |
| self._config[_CONFIG_DUT_IMAGE] = _CreateVMImage( |
| dut_image_dir, dut_dir) |
| else: |
| self._config[_CONFIG_DUT_IMAGE] = self._CopyVMImage( |
| dut_image_dir, dut_dir) |
| |
| self._config[_CONFIG_INITIALIZED] = 'true' |
| self._Persist() |
| logging.notice('All Done!') |
| |
| def Start(self): |
| """Launch the VM(s) in this setup. |
| |
| This involves grabbing some global resources on the host for networking |
| between the launched VMs. |
| """ |
| if not self._config[_CONFIG_INITIALIZED]: |
| raise StartError('No moblabvm initialized at %s' % self.workspace) |
| |
| if self._config[_CONFIG_STARTED]: |
| raise StartError('Can not start (partially) started VMs. ' |
| 'Pelase call Stop first.') |
| |
| # Start grabs system global resources. Try hard to persist even partial |
| # attempts so that Stop can cleanup everything we do setup correctly. |
| try: |
| self._Start() |
| self._config[_CONFIG_STARTED] = 'true' |
| finally: |
| self._Persist() |
| |
| def Stop(self): |
| """Stop the VM(s) in this setup. |
| |
| Also releases global network resources. |
| """ |
| # These should strictly be in LIFO order of setup done in Start. |
| # Moreover, it should be able to cleanup after Start fails after partial |
| # setup. |
| if self._config[_CONFIG_DUT_SSH_PORT]: |
| _StopVM(self._config[_CONFIG_DUT_SSH_PORT], chroot_path=self.chroot) |
| del self._config[_CONFIG_DUT_SSH_PORT] |
| if self._config[_CONFIG_MOBLAB_SSH_PORT]: |
| _StopVM(self._config[_CONFIG_MOBLAB_SSH_PORT], chroot_path=self.chroot) |
| del self._config[_CONFIG_MOBLAB_SSH_PORT] |
| if self._config[_CONFIG_DUT_TAP_DEV]: |
| _RemoveTapDeviceIgnoringErrors(self._config[_CONFIG_DUT_TAP_DEV]) |
| del self._config[_CONFIG_DUT_TAP_DEV] |
| if self._config[_CONFIG_MOBLAB_TAP_DEV]: |
| _RemoveTapDeviceIgnoringErrors(self._config[_CONFIG_MOBLAB_TAP_DEV]) |
| del self._config[_CONFIG_MOBLAB_TAP_DEV] |
| if self._config[_CONFIG_NETWORK_BRIDGE]: |
| _RemoveNetworkBridgeIgnoringErrors(self._config[_CONFIG_NETWORK_BRIDGE]) |
| del self._config[_CONFIG_NETWORK_BRIDGE] |
| |
| if self._config[_CONFIG_STARTED]: |
| del self._config[_CONFIG_STARTED] |
| self._Persist() |
| |
| def Destroy(self): |
| """Completely destroy this moblabvm setup. Cleans out workspace_dir.""" |
| self.Stop() |
| osutils.EmptyDir(self.workspace, ignore_missing=True, sudo=True) |
| self._config = collections.defaultdict(str) |
| # Don't self._Persist since the workspace is now clean. |
| |
| @contextlib.contextmanager |
| def RunVmsContext(self): |
| """A context manager to start the VM and guarantee it is stopped.""" |
| try: |
| self.Start() |
| yield |
| finally: |
| self.Destroy() |
| |
| @contextlib.contextmanager |
| def MountedMoblabDiskContext(self): |
| """A contextmanager to mount the already prepared moblab disk. |
| |
| This can be used to modify the external disk mounted by the moblab VM, while |
| the VMs are not running. It is often much more performance to modify the |
| disk on the host than SCP'ing large files into the VM. |
| |
| vms = moblab_vm.MoblabVm(workspace_dir) |
| vms.Create(...) |
| with vms.MountedMoblabDiskContext() as moblab_disk_dir: |
| # Copy stuff into the directory at moblab_disk_dir |
| vms.Start() |
| ... |
| """ |
| if not self.initialized: |
| raise MoblabVmError('Uninitialized workspace %s. Can not mount disk.' |
| % self.workspace) |
| if self.running: |
| raise MoblabVmError( |
| 'VM at %s is already running. Stop() before mounting disk.' |
| % self.workspace) |
| |
| with osutils.TempDir() as tempdir: |
| osutils.MountDir(self._config[_CONFIG_MOBLAB_DISK], tempdir, 'ext4', |
| skip_mtab=True) |
| try: |
| yield tempdir |
| finally: |
| osutils.UmountDir(tempdir) |
| |
| def _LoadConfig(self): |
| """Attempts to load the config from workspace, or sets up defaults.""" |
| config = collections.defaultdict(str) |
| if os.path.isfile(self._config_path): |
| with open(self._config_path, 'r') as config_file: |
| config_data = json.load(config_file) |
| self._ConfigRelativePathsToAbsolute(config_data, |
| os.path.dirname(self._config_path)) |
| config.update(config_data) |
| return config |
| |
| def _Persist(self): |
| """Dump our config to disk for later MoblabVm objects.""" |
| # We do not want to modify config dict for the object. |
| config = dict(self._config) |
| self._ConfigAbsolutePathsToRelative(config, |
| os.path.dirname(self._config_path)) |
| pformat.json(config, fp=self._config_path) |
| |
| def _ConfigRelativePathsToAbsolute(self, config_data, workspace_dir): |
| """Converts all the relative paths loaded from config to absolute paths.""" |
| for key, relpath in config_data.items(): |
| if key not in _CONFIG_RELATIVE_PATH_KEYS: |
| continue |
| config_data[key] = os.path.realpath(os.path.join(workspace_dir, relpath)) |
| |
| def _ConfigAbsolutePathsToRelative(self, config_data, workspace_dir): |
| """Converts all absolute paths to relative paths for persisting on disk.""" |
| workspace_dir = os.path.realpath(workspace_dir) |
| for key, abspath in config_data.items(): |
| if key not in _CONFIG_RELATIVE_PATH_KEYS: |
| continue |
| abspath = os.path.realpath(abspath) |
| config_data[key] = os.path.relpath(abspath, workspace_dir) |
| |
| def _Start(self): |
| """Actually start the VM(s), without persisting the updated config.""" |
| # Mulitple moblabvms should be able to run in parallel. At the same time, we |
| # want to minimize flaky failures due to unavailable system global |
| # resources. We attempt to create a bridge device a few times. Once we get a |
| # hold of that, we base all other system global constants off of that to not |
| # step our other moblabvms' toes. |
| for _ in range(5): |
| try: |
| port, bridge_name = _TryCreateBridgeDevice() |
| break |
| except RetriableError: |
| pass |
| else: |
| raise SetupError('Failed to create bridge device. ' |
| 'You seem to have many moblabvms running in parallel.') |
| |
| self._config[_CONFIG_NETWORK_BRIDGE] = bridge_name |
| self._config[_CONFIG_MOBLAB_TAP_DEV] = _CreateTapDevice(port) |
| self._config[_CONFIG_DUT_TAP_DEV] = _CreateTapDevice(port + 1) |
| _ConnectDeviceToBridge(self._config[_CONFIG_MOBLAB_TAP_DEV], |
| self._config[_CONFIG_NETWORK_BRIDGE]) |
| _ConnectDeviceToBridge(self._config[_CONFIG_DUT_TAP_DEV], |
| self._config[_CONFIG_NETWORK_BRIDGE]) |
| _DeviceUp(self._config[_CONFIG_NETWORK_BRIDGE]) |
| |
| moblab_ssh_port = port + 2 |
| # moblab grabs 3 extra consecutive ports for forwarding AFE and devserver to |
| # the host. |
| dut_ssh_port = moblab_ssh_port + 4 |
| self._StartMoblabVM(moblab_ssh_port, dut_ssh_port) |
| if self._config[_CONFIG_DUT_IMAGE]: |
| self._StartDutVM(dut_ssh_port) |
| |
| def _CopyVMImage(self, source_dir, target_dir): |
| """Converts or copies VM images from source_dir to target_dir.""" |
| source_path = os.path.join(source_dir, constants.VM_IMAGE_BIN) |
| if not os.path.isfile(source_path): |
| raise SetupError('Could not find VM image at %s' % source_path) |
| |
| target_path = os.path.join(target_dir, constants.VM_IMAGE_BIN) |
| shutil.copyfile(source_path, target_path) |
| return target_path |
| |
| def _StartMoblabVM(self, ssh_port, max_port): |
| """Starts a VM running moblab. |
| |
| Args: |
| ssh_port: (int) VM SSH port to use. |
| max_port: (int) ports upto this are available. |
| """ |
| # We want a unicast, locally configured address with an obviously bogus |
| # organisation id. |
| tap_mac_addr = '02:00:00:99:99:01' |
| self._WriteToMoblabDiskImage('private-network-macaddr.conf', [tap_mac_addr]) |
| _StartVM( |
| self._config[_CONFIG_MOBLAB_IMAGE], |
| ssh_port, |
| max_port, |
| self._config[_CONFIG_MOBLAB_TAP_DEV], |
| tap_mac_addr, |
| is_moblab=True, |
| disk_path=self._config[_CONFIG_MOBLAB_DISK], |
| chroot_path=self.chroot, |
| ) |
| # Update config _after_ we've successfully launched the VM so that we don't |
| # _Persist incorrect information. |
| self._config[_CONFIG_MOBLAB_SSH_PORT] = ssh_port |
| self._config[_CONFIG_MOBLAB_TAP_MAC] = tap_mac_addr |
| |
| def _StartDutVM(self, ssh_port): |
| """Starts a VM running the sub-DUT image. |
| |
| Args: |
| ssh_port: VM SSH port to use. |
| """ |
| # We want a unicast, locally configured address with an obviously bogus |
| # organisation id. |
| tap_mac_addr = '02:00:00:99:99:51' |
| _StartVM( |
| self._config[_CONFIG_DUT_IMAGE], |
| ssh_port, |
| ssh_port + 1, |
| self._config[_CONFIG_DUT_TAP_DEV], |
| tap_mac_addr, |
| is_moblab=False, |
| chroot_path=self.chroot, |
| ) |
| # Update config _after_ we've successfully launched the VM so that we don't |
| # _Persist incorrect information. |
| self._config[_CONFIG_DUT_SSH_PORT] = ssh_port |
| self._config[_CONFIG_DUT_TAP_MAC] = tap_mac_addr |
| |
| def _WriteToMoblabDiskImage(self, target_path, lines): |
| """Write a file in the moblab external disk. |
| |
| Args: |
| target_path: Path within the mounted disk to write. This path will be |
| overwritten. |
| lines: An iterator of lines to write. |
| """ |
| with self.MountedMoblabDiskContext() as disk_dir: |
| osutils.WriteFile(os.path.join(disk_dir, target_path), lines, sudo=True) |
| |
| |
| def _CreateVMImage(src_dir, dest_dir): |
| """Creates a VM image from a given chromiumos image. |
| |
| Args: |
| src_dir: Path to the directory containing (non-VM) image. Defaults to None |
| to use the latest image for the board. |
| dest_dir: Path to the directory where the VM image should be written. |
| |
| Returns: |
| The path of the created VM image. |
| """ |
| # image_to_vm.sh only runs in chroot, but src_dir / dest_dir may not be |
| # reachable from chroot. Also, image_to_vm.sh needs all the contents of |
| # src_dir to work correctly (it silently does the wrong thing if some files |
| # are missing). |
| # So, create a tempdir reachable from chroot, copy everything to that path, |
| # create vm image there and finally move it all to dest_dir. |
| with chroot_util.TempDirInChroot() as tempdir: |
| logging.debug('Copying images from %s to %s.', src_dir, tempdir) |
| osutils.CopyDirContents(src_dir, tempdir) |
| # image_to_vm.sh doesn't let us provide arbitrary names for the input image. |
| # Instead, it picks the name based on whether we pass in --test_image or not |
| # (and doesn't use that flag for anything else). |
| cmd = [ |
| path_util.ToChrootPath( |
| os.path.join(constants.CROSUTILS_DIR, 'image_to_vm.sh')), |
| '--from=%s' % path_util.ToChrootPath(tempdir), |
| '--disk_layout=16gb-rootfs', |
| '--test_image', |
| ] |
| try: |
| cros_build_lib.run(cmd, enter_chroot=True, cwd=constants.SOURCE_ROOT) |
| except cros_build_lib.RunCommandError as e: |
| raise SetupError('Failed to create VM image for %s: %s' % (src_dir, e)) |
| |
| # Preserve most content, although we should need only the generated VM |
| # image. Other files like boot.desc might be needed elsewhere, but the |
| # source images should no longer be needed. |
| osutils.SafeUnlink(os.path.join(tempdir, constants.BASE_IMAGE_BIN), |
| sudo=True) |
| osutils.SafeUnlink(os.path.join(tempdir, constants.TEST_IMAGE_BIN), |
| sudo=True) |
| osutils.CopyDirContents(tempdir, dest_dir) |
| # The exact name of the output image is hard-coded in image_to_vm.sh |
| return os.path.join(dest_dir, constants.VM_IMAGE_BIN) |
| |
| |
| def _CreateMoblabDisk(dest_dir): |
| """Creates an empty moblab virtual disk at the given path. |
| |
| Args: |
| dest_dir: Directory where the disk image should be created. |
| |
| Returns: |
| Path to the created disk image. |
| """ |
| dest_path = os.path.join(dest_dir, 'moblab_disk') |
| cros_build_lib.run(['qemu-img', 'create', '-f', 'raw', dest_path, '64g']) |
| cros_build_lib.run(['mkfs.ext4', '-F', dest_path]) |
| cros_build_lib.run(['e2label', dest_path, 'MOBLAB-STORAGE']) |
| return dest_path |
| |
| |
| def _TryCreateBridgeDevice(): |
| """Creates a bridge device named moblabvmbrXX, generating XX randomly. |
| |
| Returns: |
| num, name: Counter to be used to generate unique names, name of the bridge. |
| """ |
| # Keep this number between 1024-49151 (with padding) because: |
| # - We use it to reserve networking ports, so stay within user range. |
| # - We use it to name kernel devices, which has length restrictions. |
| num = random.randint(10000, 30000) |
| # device names can only be 16 char long. So suffix can be between 0 - 10^9. |
| name = 'brmob%s' % num |
| cros_build_lib.sudo_run(['ip', 'link', 'add', name, 'type', 'bridge'], |
| quiet=True) |
| return num + 1, name |
| |
| |
| def _CreateTapDevice(suffix): |
| """Create a tap device using the suffix provided.""" |
| # device names can only be 16 char long. So suffix can be between 0 - 10^10. |
| name = 'tapmob%s' % suffix |
| cros_build_lib.sudo_run(['ip', 'tuntap', 'add', 'mode', 'tap', name]) |
| |
| # Creating the device can take some time, and trying to create another device |
| # in the meantime returns 'Device or resource busy'. So, wait for device |
| # creation to complete. |
| @retry_util.WithRetry(max_retry=3, sleep=0.2, exception=RetriableError) |
| def _VerifyDeviceCreated(name): |
| result = cros_build_lib.run(['ip', 'tuntap', 'show'], stdout=True) |
| if name not in result.output: |
| raise RetriableError('Device %s not found in output "%s".' % |
| (name, result.output)) |
| |
| _VerifyDeviceCreated(name) |
| return name |
| |
| |
| def _ConnectDeviceToBridge(device, bridge): |
| """Connects link device to network bridge.""" |
| cros_build_lib.sudo_run(['ip', 'link', 'set', device, 'master', bridge]) |
| |
| |
| def _DeviceUp(device): |
| """Bring up the given link device.""" |
| cros_build_lib.sudo_run(['ip', 'link', 'set', 'dev', device, 'up']) |
| |
| |
| def _StartVM(image_path, ssh_port, max_port, tap_dev, tap_mac_addr, is_moblab, |
| disk_path=None, chroot_path=None): |
| """Starts a VM instance. |
| |
| Args: |
| image_path: Path to the OS image to use. |
| ssh_port: (int) port to use for SSH. |
| max_port: (int) ports upto this are available. |
| tap_dev: Name of the tap device to use for secondary network. |
| tap_mac_addr: The MAC address of the secondary network device. |
| is_moblab: Whether we're starting a moblab VM. |
| disk_path: Moblab disk. |
| chroot_path: Location of chroot on disk to pass to cros_vm. |
| """ |
| cmd = [ |
| './cros_vm', '--start', '--image-path=%s' % image_path, |
| '--ssh-port=%d' % ssh_port, |
| '--qemu-m', '32G', |
| '--qemu-args', '-net nic,macaddr=%s' % tap_mac_addr, |
| '--qemu-args', '-net tap,ifname=%s' % tap_dev |
| ] |
| if chroot_path: |
| cmd.extend(['--chroot-path', chroot_path]) |
| if is_moblab: |
| moblab_monitoring_port = ssh_port + 1 |
| afe_port = ssh_port + 2 |
| dev_server_port = ssh_port + 3 |
| assert dev_server_port < max_port, 'Exceeded maximum available ports.' |
| cmd += [ |
| # Moblab monitoring port. |
| '--qemu-hostfwd', 'tcp:127.0.0.1:%d-:9991' % moblab_monitoring_port, |
| # AFE port. |
| '--qemu-hostfwd', 'tcp:127.0.0.1:%d-:80' % afe_port, |
| # DevServer port. |
| '--qemu-hostfwd', 'tcp:127.0.0.1:%d-:8080' % dev_server_port, |
| # Create a dedicated scsi controller for the external disk, and attach |
| # the disk to that bus. This separates us from any scsi controllers that |
| # may be created for the boot disk. |
| '--qemu-args', |
| '-drive id=moblabdisk,if=none,file=%s,format=raw' % disk_path, |
| '--qemu-args', '-device virtio-scsi-pci,id=scsiext', |
| '--qemu-args', '-device scsi-hd,bus=scsiext.0,drive=moblabdisk', |
| ] |
| cros_build_lib.sudo_run(cmd, cwd=constants.CHROMITE_BIN_DIR) |
| |
| |
| def _RemoveNetworkBridgeIgnoringErrors(name): |
| """Removes a previously created network bridge. Ignores errors.""" |
| _RunIgnoringErrors(['ip', 'link', 'del', name, 'type', 'bridge']) |
| |
| |
| def _RemoveTapDeviceIgnoringErrors(name): |
| """Removes a previously created tap device. Ignores errors.""" |
| _RunIgnoringErrors(['ip', 'tuntap', 'del', 'mode', 'tap', name]) |
| |
| |
| def _StopVM(ssh_port, chroot_path=None): |
| """Stops a running VM instance. |
| |
| Args: |
| ssh_port: (int) port to use for ssh. |
| chroot_path: Location of chroot on disk to pass to cros_vm. |
| """ |
| logging.notice('Stopping the VM. This may take a minute.') |
| cmd = [ |
| '%s/cros_vm' % constants.CHROMITE_BIN_DIR, |
| '--stop', '--ssh-port=%d' % ssh_port, |
| ] |
| if chroot_path: |
| cmd.extend(['--chroot-path', chroot_path]) |
| _RunIgnoringErrors(cmd) |
| |
| |
| def _RunIgnoringErrors(cmd): |
| """Runs the given command, ignores errors but warns user.""" |
| try: |
| cros_build_lib.sudo_run(cmd, quiet=True) |
| except cros_build_lib.RunCommandError as e: |
| logging.error('Cleanup operation failed. Please run "%s" manually.', |
| ' '.join(cmd)) |
| logging.debug('Encountered error: %s', e) |