1 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """VM-related helper functions/classes."""
7 from __future__ import print_function
14 from chromite.cbuildbot import constants
15 from chromite.lib import cros_build_lib
16 from chromite.lib import osutils
17 from chromite.lib import remote_access
20 class VMError(Exception):
21 """A base exception for VM errors."""
24 class VMCreationError(VMError):
25 """Raised when failed to create a VM image."""
28 def VMIsUpdatable(path):
29 """Check if the existing VM image is updatable.
32 path: Path to the VM image.
35 True if VM is updatable; False otherwise.
37 table = cros_build_lib.GetImageDiskPartitionInfo(path, unit='MB')
38 # Assume if size of the two root partitions match, the image
40 return table['ROOT-B'].size == table['ROOT-A'].size
43 def CreateVMImage(image=None, board=None, updatable=True, dest_dir=None):
44 """Returns the path of the image built to run in a VM.
46 By default, the returned VM is a test image that can run full update
47 testing on it. If there exists a VM image with the matching
48 |updatable| setting, this method returns the path to the existing
49 image. If |dest_dir| is set, it will copy/create the VM image to the
53 image: Path to the (non-VM) image. Defaults to None to use the latest
55 board: Board that the image was built with. If None, attempts to use the
56 configured default board.
57 updatable: Create a VM image that supports AU.
58 dest_dir: If set, create/copy the VM image to |dest|; otherwise,
59 use the folder where |image| resides.
61 if not image and not board:
62 raise VMCreationError(
63 'Cannot create VM when both image and board are None.')
65 image_dir = os.path.dirname(image)
66 src_path = dest_path = os.path.join(image_dir, constants.VM_IMAGE_BIN)
69 dest_path = os.path.join(dest_dir, constants.VM_IMAGE_BIN)
72 # Do not create a new VM image if a matching image already exists.
73 exists = os.path.exists(src_path) and (
74 not updatable or VMIsUpdatable(src_path))
76 if exists and dest_dir:
77 # Copy the existing VM image to dest_dir.
78 shutil.copyfile(src_path, dest_path)
81 # No existing VM image that we can reuse. Create a new VM image.
82 logging.info('Creating %s', dest_path)
83 cmd = ['./image_to_vm.sh', '--test_image']
86 cmd.append('--from=%s' % cros_build_lib.ToChrootPath(image_dir))
89 cmd.extend(['--disk_layout', '2gb-rootfs-updatable'])
92 cmd.extend(['--board', board])
94 # image_to_vm.sh only runs in chroot, but dest_dir may not be
95 # reachable from chroot. In that case, we copy it to a temporary
96 # directory in chroot, and then move it to dest_dir .
99 # Create a temporary directory in chroot to store the VM
100 # image. This is to avoid the case where dest_dir is not
101 # reachable within chroot.
102 tempdir = cros_build_lib.RunCommand(
105 enter_chroot=True).output.strip()
106 cmd.append('--to=%s' % tempdir)
108 msg = 'Failed to create the VM image'
110 cros_build_lib.RunCommand(cmd, enter_chroot=True,
111 cwd=constants.SOURCE_ROOT)
112 except cros_build_lib.RunCommandError as e:
113 logging.error('%s: %s', msg, e)
116 cros_build_lib.FromChrootPath(tempdir), ignore_missing=True)
117 raise VMCreationError(msg)
120 # Move VM from tempdir to dest_dir.
122 cros_build_lib.FromChrootPath(
123 os.path.join(tempdir, constants.VM_IMAGE_BIN)), dest_path)
124 osutils.RmDir(cros_build_lib.FromChrootPath(tempdir), ignore_missing=True)
126 if not os.path.exists(dest_path):
127 raise VMCreationError(msg)
132 class VMStartupError(VMError):
133 """Raised when failed to start a VM instance."""
136 class VMStopError(VMError):
137 """Raised when failed to stop a VM instance."""
140 class VMInstance(object):
141 """This is a wrapper of a VM instance."""
143 MAX_LAUNCH_ATTEMPTS = 5
144 TIME_BETWEEN_LAUNCH_ATTEMPTS = 30
146 # VM needs a longer timeout.
147 SSH_CONNECT_TIMEOUT = 120
149 def __init__(self, image_path, port=None, tempdir=None,
150 debug_level=logging.DEBUG):
151 """Initializes VMWrapper with a VM image path.
154 image_path: Path to the VM image.
155 port: SSH port of the VM.
156 tempdir: Temporary working directory.
157 debug_level: Debug level for logging.
159 self.image_path = image_path
160 self.tempdir = tempdir
161 self._tempdir_obj = None
163 self._tempdir_obj = osutils.TempDir(prefix='vm_wrapper', sudo_rm=True)
164 self.tempdir = self._tempdir_obj.tempdir
165 self.kvm_pid_path = os.path.join(self.tempdir, 'kvm.pid')
166 self.port = (remote_access.GetUnusedPort() if port is None
167 else remote_access.NormalizePort(port))
168 self.debug_level = debug_level
169 self.ssh_settings = remote_access.CompileSSHConnectSettings(
170 ConnectTimeout=self.SSH_CONNECT_TIMEOUT)
171 self.agent = remote_access.RemoteAccess(
172 remote_access.LOCALHOST, self.tempdir, self.port,
173 debug_level=self.debug_level, interactive=False)
176 """Run the command to start VM."""
177 cmd = [os.path.join(constants.CROSUTILS_DIR, 'bin', 'cros_start_vm'),
178 '--ssh_port', str(self.port),
179 '--image_path', self.image_path,
181 '--kvm_pid', self.kvm_pid_path]
183 self._RunCommand(cmd, capture_output=True)
184 except cros_build_lib.RunCommandError as e:
185 msg = 'VM failed to start'
186 logging.warning('%s: %s', msg, e)
187 raise VMStartupError(msg)
190 """Returns True if we can connect to VM via SSH."""
192 self.agent.RemoteSh(['true'], connect_settings=self.ssh_settings)
198 def Stop(self, ignore_error=False):
199 """Stops a running VM.
202 ignore_error: If set True, do not raise an exception on error.
204 cmd = [os.path.join(constants.CROSUTILS_DIR, 'bin', 'cros_stop_vm'),
205 '--kvm_pid', self.kvm_pid_path]
206 result = self._RunCommand(cmd, capture_output=True, error_code_ok=True)
207 if result.returncode:
208 msg = 'Failed to stop VM'
210 logging.warning('%s: %s', msg, result.error)
212 logging.error('%s: %s', msg, result.error)
213 raise VMStopError(msg)
216 """Start VM and wait until we can ssh into it.
218 This command is more robust than just naively starting the VM as it will
219 try to start the VM multiple times if the VM fails to start up. This is
220 inspired by retry_until_ssh in crosutils/lib/cros_vm_lib.sh.
222 for _ in range(self.MAX_LAUNCH_ATTEMPTS):
225 except VMStartupError:
226 logging.warning('VM failed to start.')
230 # VM is started up successfully if we can connect to it.
233 logging.warning('Cannot connect to VM...')
234 self.Stop(ignore_error=True)
235 time.sleep(self.TIME_BETWEEN_LAUNCH_ATTEMPTS)
237 raise VMStartupError('Max attempts (%d) to start VM exceeded.'
238 % self.MAX_LAUNCH_ATTEMPTS)
240 logging.info('VM started at port %d', self.port)
242 def _RunCommand(self, *args, **kwargs):
243 """Runs a commmand on the host machine."""
244 kwargs.setdefault('debug_level', self.debug_level)
245 return cros_build_lib.RunCommand(*args, **kwargs)