blob: 16d282c2665279357ae322691f35c73fd4c27d48 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Achuith Bhandarkard8d19292016-05-03 14:32:58 -07002# Copyright 2016 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger666566c2016-09-21 00:00:21 -04006"""Script for VM Management."""
Achuith Bhandarkard8d19292016-05-03 14:32:58 -07007
8from __future__ import print_function
9
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010010import argparse
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +010011import distutils.version
Achuith Bhandarkarb00bafc2018-09-11 16:30:13 -070012import errno
Po-Hsien Wang1cec8d12018-01-25 16:36:44 -080013import multiprocessing
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070014import os
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +010015import re
Achuith Bhandarkarb00bafc2018-09-11 16:30:13 -070016import socket
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070017
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -080018from chromite.cli.cros import cros_chrome_sdk
19from chromite.lib import cache
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070020from chromite.lib import commandline
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -080021from chromite.lib import constants
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070022from chromite.lib import cros_build_lib
23from chromite.lib import cros_logging as logging
Mike Frysinger10666292018-07-12 01:03:38 -040024from chromite.lib import memoize
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070025from chromite.lib import osutils
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -080026from chromite.lib import path_util
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070027from chromite.lib import remote_access
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -070028from chromite.lib import retry_util
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070029
30
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -070031class DeviceError(Exception):
32 """Exception for Device failures."""
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070033
34 def __init__(self, message):
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -070035 super(DeviceError, self).__init__()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070036 logging.error(message)
37
38
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -070039class Device(object):
40 """Class for managing a test device."""
41
42 def __init__(self, opts):
43 """Initialize Device.
44
45 Args:
46 opts: command line options.
47 """
48 self.device = opts.device
49 self.ssh_port = None
50 self.board = opts.board
51
52 self.cmd = opts.args[1:] if opts.cmd else None
53 self.private_key = opts.private_key
54 self.dry_run = opts.dry_run
55 # log_level is only set if --log-level or --debug is specified.
56 self.log_level = getattr(opts, 'log_level', None)
57 self.InitRemote()
58
59 def InitRemote(self):
60 """Initialize remote access."""
61 self.remote = remote_access.RemoteDevice(self.device,
62 port=self.ssh_port,
63 private_key=self.private_key)
64
65 self.device_addr = 'ssh://%s' % self.device
66 if self.ssh_port:
67 self.device_addr += ':%d' % self.ssh_port
68
69 def WaitForBoot(self):
70 """Wait for the device to boot up.
71
72 Wait for the ssh connection to become active.
73 """
74 try:
75 result = retry_util.RetryException(
76 exception=remote_access.SSHConnectionError,
77 max_retry=10,
78 functor=lambda: self.RemoteCommand(cmd=['echo']),
79 sleep=5)
80 except remote_access.SSHConnectionError:
81 raise DeviceError(
82 'WaitForBoot timed out trying to connect to the device.')
83
84 if result.returncode != 0:
85 raise DeviceError('WaitForBoot failed: %s.' % result.error)
86
87 def RemoteCommand(self, cmd, stream_output=False, **kwargs):
88 """Run a remote command.
89
90 Args:
91 cmd: command to run.
92 stream_output: Stream output of long-running commands.
93 kwargs: additional args (see documentation for RemoteDevice.RunCommand).
94
95 Returns:
96 cros_build_lib.CommandResult object.
97 """
Achuith Bhandarkara58355c2018-11-16 15:56:40 -080098 if self.dry_run:
99 return self._DryRunCommand(cmd)
100 else:
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -0700101 kwargs.setdefault('error_code_ok', True)
102 if stream_output:
103 kwargs.setdefault('capture_output', False)
104 else:
105 kwargs.setdefault('combine_stdout_stderr', True)
106 kwargs.setdefault('log_output', True)
107 return self.remote.RunCommand(cmd, debug_level=logging.INFO, **kwargs)
108
Achuith Bhandarkara58355c2018-11-16 15:56:40 -0800109 def _DryRunCommand(self, cmd):
110 """Print a command for dry_run.
111
112 Args:
113 cmd: command to print.
114
115 Returns:
116 cros_build_lib.CommandResult object.
117 """
118 assert self.dry_run, 'Use with --dry-run only'
119 logging.info('[DRY RUN] %s', cros_build_lib.CmdToStr(cmd))
120 return cros_build_lib.CommandResult(cmd, output='', returncode=0)
121
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -0700122 @property
123 def is_vm(self):
124 """Returns true if we're a VM."""
125 return self._IsVM(self.device)
126
127 @staticmethod
128 def _IsVM(device):
129 """VM if |device| is specified and it's not localhost."""
130 return not device or device == remote_access.LOCALHOST
131
132 @staticmethod
133 def Create(opts):
134 """Create either a Device or VM based on |opts.device|."""
135 if Device._IsVM(opts.device):
136 return VM(opts)
137 return Device(opts)
138
139 @staticmethod
140 def GetParser():
141 """Parse a list of args.
142
143 Args:
144 argv: list of command line arguments.
145
146 Returns:
147 List of parsed opts.
148 """
149 parser = commandline.ArgumentParser(description=__doc__)
150 parser.add_argument('--device', help='Hostname or Device IP.')
151 sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
152 parser.add_argument('--board', default=sdk_board_env, help='Board to use.')
153 parser.add_argument('--private-key', help='Path to ssh private key.')
154 parser.add_argument('--dry-run', action='store_true', default=False,
155 help='dry run for debugging.')
156 parser.add_argument('--cmd', action='store_true', default=False,
157 help='Run a command.')
158 parser.add_argument('args', nargs=argparse.REMAINDER,
159 help='Command to run.')
160 return parser
161
162
163class VMError(DeviceError):
164 """Exception for VM failures."""
165
166
167class VM(Device):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700168 """Class for managing a VM."""
169
170 SSH_PORT = 9222
Nicolas Norvezf527cdf2018-01-26 11:55:44 -0800171 IMAGE_FORMAT = 'raw'
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700172
Achuith Bhandarkar2beae292018-03-26 16:44:53 -0700173 def __init__(self, opts):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700174 """Initialize VM.
175
176 Args:
Achuith Bhandarkar2beae292018-03-26 16:44:53 -0700177 opts: command line options.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700178 """
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -0700179 super(VM, self).__init__(opts)
180
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100181 self.qemu_path = opts.qemu_path
Ben Pastene243302c2018-09-21 16:06:08 -0700182 self.qemu_img_path = opts.qemu_img_path
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800183 self.qemu_bios_path = opts.qemu_bios_path
Po-Hsien Wang1cec8d12018-01-25 16:36:44 -0800184 self.qemu_m = opts.qemu_m
185 self.qemu_cpu = opts.qemu_cpu
186 self.qemu_smp = opts.qemu_smp
187 if self.qemu_smp == 0:
188 self.qemu_smp = min(8, multiprocessing.cpu_count)
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100189 self.enable_kvm = opts.enable_kvm
Ben Pastene243302c2018-09-21 16:06:08 -0700190 self.copy_on_write = opts.copy_on_write
Achuith Bhandarkarf877da22017-09-12 12:27:39 -0700191 # We don't need sudo access for software emulation or if /dev/kvm is
192 # writeable.
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100193 self.use_sudo = self.enable_kvm and not os.access('/dev/kvm', os.W_OK)
194 self.display = opts.display
195 self.image_path = opts.image_path
Nicolas Norvezf527cdf2018-01-26 11:55:44 -0800196 self.image_format = opts.image_format
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -0700197
198 self.device = remote_access.LOCALHOST
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100199 self.ssh_port = opts.ssh_port
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100200
201 self.start = opts.start
202 self.stop = opts.stop
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700203
Achuith Bhandarkar02ef76b2018-04-05 10:47:33 +0200204 self.cache_dir = os.path.abspath(opts.cache_dir)
205 assert os.path.isdir(self.cache_dir), "Cache directory doesn't exist"
206
Achuith Bhandarkar2beae292018-03-26 16:44:53 -0700207 self.vm_dir = opts.vm_dir
208 if not self.vm_dir:
209 self.vm_dir = os.path.join(osutils.GetGlobalTempDir(),
210 'cros_vm_%d' % self.ssh_port)
Achuith Bhandarkarfd5d1852018-03-26 14:50:54 -0700211 self._CreateVMDir()
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700212
213 self.pidfile = os.path.join(self.vm_dir, 'kvm.pid')
214 self.kvm_monitor = os.path.join(self.vm_dir, 'kvm.monitor')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700215 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
216 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
217 self.kvm_serial = '%s.serial' % self.kvm_monitor
218
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -0700219 self.InitRemote()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700220
Achuith Bhandarkara58355c2018-11-16 15:56:40 -0800221 def RunCommand(self, *args, **kwargs):
Achuith Bhandarkar665a9b32018-05-17 11:39:19 -0700222 """Use SudoRunCommand or RunCommand as necessary.
223
224 Args:
225 args and kwargs: positional and optional args to RunCommand.
226
227 Returns:
228 cros_build_lib.CommandResult object.
229 """
Achuith Bhandarkara58355c2018-11-16 15:56:40 -0800230 if self.dry_run:
231 return self._DryRunCommand(*args)
232 elif self.use_sudo:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700233 return cros_build_lib.SudoRunCommand(*args, **kwargs)
234 else:
235 return cros_build_lib.RunCommand(*args, **kwargs)
236
Achuith Bhandarkarfd5d1852018-03-26 14:50:54 -0700237 def _CreateVMDir(self):
238 """Safely create vm_dir."""
Steven Bennettsc57ca0e2018-09-18 09:33:59 -0700239 if not osutils.SafeMakedirs(self.vm_dir):
240 # For security, ensure that vm_dir is not a symlink, and is owned by us.
Achuith Bhandarkarfd5d1852018-03-26 14:50:54 -0700241 error_str = ('VM state dir is misconfigured; please recreate: %s'
242 % self.vm_dir)
Achuith Bhandarkar46d83872018-04-04 01:49:55 -0700243 assert os.path.isdir(self.vm_dir), error_str
Achuith Bhandarkarfd5d1852018-03-26 14:50:54 -0700244 assert not os.path.islink(self.vm_dir), error_str
Steven Bennettsc57ca0e2018-09-18 09:33:59 -0700245 assert os.stat(self.vm_dir).st_uid == os.getuid(), error_str
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700246
Ben Pastene243302c2018-09-21 16:06:08 -0700247 def _CreateQcow2Image(self):
248 """Creates a qcow2-formatted image in the temporary VM dir.
249
250 This image will get removed on VM shutdown.
251 """
252 cow_image_path = os.path.join(self.vm_dir, 'qcow2.img')
253 qemu_img_args = [
254 self.qemu_img_path,
255 'create', '-f', 'qcow2',
256 '-o', 'backing_file=%s' % self.image_path,
257 cow_image_path,
258 ]
Achuith Bhandarkara58355c2018-11-16 15:56:40 -0800259 self.RunCommand(qemu_img_args)
260 logging.info('qcow2 image created at %s.', cow_image_path)
Ben Pastene243302c2018-09-21 16:06:08 -0700261 self.image_path = cow_image_path
262 self.image_format = 'qcow2'
263
Achuith Bhandarkarfd5d1852018-03-26 14:50:54 -0700264 def _RmVMDir(self):
265 """Cleanup vm_dir."""
Mike Frysinger97080242017-09-13 01:58:45 -0400266 osutils.RmDir(self.vm_dir, ignore_missing=True, sudo=self.use_sudo)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700267
Achuith Bhandarkar02ef76b2018-04-05 10:47:33 +0200268 def _GetCachePath(self, cache_name):
Achuith Bhandarkarf6bccdb2018-03-23 13:12:29 -0700269 """Return path to cache.
270
271 Args:
272 cache_name: Name of cache.
273
274 Returns:
275 File path of cache.
276 """
Achuith Bhandarkar02ef76b2018-04-05 10:47:33 +0200277 return os.path.join(self.cache_dir,
Achuith Bhandarkarf6bccdb2018-03-23 13:12:29 -0700278 cros_chrome_sdk.COMMAND_NAME,
279 cache_name)
280
Mike Frysinger10666292018-07-12 01:03:38 -0400281 @memoize.MemoizedSingleCall
Achuith Bhandarkarf6bccdb2018-03-23 13:12:29 -0700282 def _SDKVersion(self):
283 """Determine SDK version.
284
285 Check the environment if we're in the SDK shell, and failing that, look at
286 the misc cache.
287
288 Returns:
289 SDK version.
290 """
291 sdk_version = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_VERSION_ENV)
Achuith Bhandarkar4e367c22018-03-27 15:32:48 -0700292 if not sdk_version and self.board:
Achuith Bhandarkarf6bccdb2018-03-23 13:12:29 -0700293 misc_cache = cache.DiskCache(self._GetCachePath(
294 cros_chrome_sdk.SDKFetcher.MISC_CACHE))
295 with misc_cache.Lookup((self.board, 'latest')) as ref:
296 if ref.Exists(lock=True):
297 sdk_version = osutils.ReadFile(ref.path).strip()
298 return sdk_version
299
300 def _CachePathForKey(self, key):
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800301 """Get cache path for key.
302
303 Args:
304 key: cache key.
305 """
Achuith Bhandarkarf6bccdb2018-03-23 13:12:29 -0700306 tarball_cache = cache.TarballCache(self._GetCachePath(
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800307 cros_chrome_sdk.SDKFetcher.TARBALL_CACHE))
Achuith Bhandarkar4e367c22018-03-27 15:32:48 -0700308 if self.board and self._SDKVersion():
Achuith Bhandarkarf6bccdb2018-03-23 13:12:29 -0700309 cache_key = (self.board, self._SDKVersion(), key)
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800310 with tarball_cache.Lookup(cache_key) as ref:
311 if ref.Exists():
312 return ref.path
313 return None
314
Mike Frysinger10666292018-07-12 01:03:38 -0400315 @memoize.MemoizedSingleCall
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +0100316 def QemuVersion(self):
Achuith Bhandarkarf6bccdb2018-03-23 13:12:29 -0700317 """Determine QEMU version.
318
319 Returns:
320 QEMU version.
321 """
Achuith Bhandarkara58355c2018-11-16 15:56:40 -0800322 version_str = self.RunCommand([self.qemu_path, '--version'],
323 capture_output=True).output
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +0100324 # version string looks like one of these:
325 # QEMU emulator version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.36), Copyright (c)
326 # 2003-2008 Fabrice Bellard
327 #
328 # QEMU emulator version 2.6.0, Copyright (c) 2003-2008 Fabrice Bellard
329 #
330 # qemu-x86_64 version 2.10.1
331 # Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project developers
332 m = re.search(r"version ([0-9.]+)", version_str)
333 if not m:
334 raise VMError('Unable to determine QEMU version from:\n%s.' % version_str)
335 return m.group(1)
336
337 def _CheckQemuMinVersion(self):
338 """Ensure minimum QEMU version."""
Achuith Bhandarkara58355c2018-11-16 15:56:40 -0800339 if self.dry_run:
340 return
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +0100341 min_qemu_version = '2.6.0'
342 logging.info('QEMU version %s', self.QemuVersion())
343 LooseVersion = distutils.version.LooseVersion
344 if LooseVersion(self.QemuVersion()) < LooseVersion(min_qemu_version):
345 raise VMError('QEMU %s is the minimum supported version. You have %s.'
346 % (min_qemu_version, self.QemuVersion()))
347
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800348 def _SetQemuPath(self):
349 """Find a suitable Qemu executable."""
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800350 qemu_exe = 'qemu-system-x86_64'
351 qemu_exe_path = os.path.join('usr/bin', qemu_exe)
352
353 # Check SDK cache.
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800354 if not self.qemu_path:
Achuith Bhandarkar761322d2018-10-24 12:26:06 -0700355 qemu_dir = self._CachePathForKey(cros_chrome_sdk.SDKFetcher.QEMU_BIN_PATH)
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800356 if qemu_dir:
357 qemu_path = os.path.join(qemu_dir, qemu_exe_path)
358 if os.path.isfile(qemu_path):
359 self.qemu_path = qemu_path
360
361 # Check chroot.
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800362 if not self.qemu_path:
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800363 qemu_path = os.path.join(
364 constants.SOURCE_ROOT, constants.DEFAULT_CHROOT_DIR, qemu_exe_path)
365 if os.path.isfile(qemu_path):
366 self.qemu_path = qemu_path
367
368 # Check system.
369 if not self.qemu_path:
370 self.qemu_path = osutils.Which(qemu_exe)
371
372 if not self.qemu_path or not os.path.isfile(self.qemu_path):
373 raise VMError('QEMU not found.')
Ben Pastene243302c2018-09-21 16:06:08 -0700374
375 if self.copy_on_write:
376 if not self.qemu_img_path:
377 # Look for qemu-img right next to qemu-system-x86_64.
378 self.qemu_img_path = os.path.join(
379 os.path.dirname(self.qemu_path), 'qemu-img')
380 if not os.path.isfile(self.qemu_img_path):
381 raise VMError('qemu-img not found. (Needed to create qcow2 image).')
382
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800383 logging.debug('QEMU path: %s', self.qemu_path)
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800384 self._CheckQemuMinVersion()
385
386 def _GetBuiltVMImagePath(self):
387 """Get path of a locally built VM image."""
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800388 vm_image_path = os.path.join(constants.SOURCE_ROOT, 'src/build/images',
389 cros_build_lib.GetBoard(self.board),
390 'latest', constants.VM_IMAGE_BIN)
391 return vm_image_path if os.path.isfile(vm_image_path) else None
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800392
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800393 def _GetCacheVMImagePath(self):
394 """Get path of a cached VM image."""
Achuith Bhandarkarf6bccdb2018-03-23 13:12:29 -0700395 cache_path = self._CachePathForKey(constants.VM_IMAGE_TAR)
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800396 if cache_path:
397 vm_image = os.path.join(cache_path, constants.VM_IMAGE_BIN)
398 if os.path.isfile(vm_image):
399 return vm_image
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800400 return None
401
402 def _SetVMImagePath(self):
403 """Detect VM image path in SDK and chroot."""
404 if not self.image_path:
Achuith Bhandarkarfc75f692018-01-10 11:33:09 -0800405 self.image_path = (self._GetCacheVMImagePath() or
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800406 self._GetBuiltVMImagePath())
407 if not self.image_path:
408 raise VMError('No VM image found. Use cros chrome-sdk --download-vm.')
409 if not os.path.isfile(self.image_path):
410 raise VMError('VM image does not exist: %s' % self.image_path)
411 logging.debug('VM image path: %s', self.image_path)
412
Achuith Bhandarkarb00bafc2018-09-11 16:30:13 -0700413 def _WaitForSSHPort(self):
414 """Wait for SSH port to become available."""
415 class _SSHPortInUseError(Exception):
416 """Exception for _CheckSSHPortBusy to throw."""
417
418 def _CheckSSHPortBusy(ssh_port):
419 """Check if the SSH port is in use."""
420 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
421 try:
422 sock.bind((remote_access.LOCALHOST_IP, ssh_port))
423 except socket.error as e:
424 if e.errno == errno.EADDRINUSE:
425 logging.info('SSH port %d in use...', self.ssh_port)
426 raise _SSHPortInUseError()
427 sock.close()
428
429 try:
430 retry_util.RetryException(
431 exception=_SSHPortInUseError,
432 max_retry=7,
433 functor=lambda: _CheckSSHPortBusy(self.ssh_port),
434 sleep=1)
435 except _SSHPortInUseError:
436 raise VMError('SSH port %d in use' % self.ssh_port)
437
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100438 def Run(self):
Achuith Bhandarkar665a9b32018-05-17 11:39:19 -0700439 """Performs an action, one of start, stop, or run a command in the VM."""
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100440 if not self.start and not self.stop and not self.cmd:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700441 raise VMError('Must specify one of start, stop, or cmd.')
Achuith Bhandarkar665a9b32018-05-17 11:39:19 -0700442
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100443 if self.start:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700444 self.Start()
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100445 if self.cmd:
Achuith Bhandarkar665a9b32018-05-17 11:39:19 -0700446 if not self.IsRunning():
447 raise VMError('VM not running.')
448 self.RemoteCommand(self.cmd, stream_output=True)
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100449 if self.stop:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700450 self.Stop()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700451
452 def Start(self):
453 """Start the VM."""
454
455 self.Stop()
Achuith Bhandarkarb00bafc2018-09-11 16:30:13 -0700456 self._WaitForSSHPort()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700457
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700458 logging.debug('Start VM')
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800459 self._SetQemuPath()
460 self._SetVMImagePath()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700461
Achuith Bhandarkarfd5d1852018-03-26 14:50:54 -0700462 self._RmVMDir()
463 self._CreateVMDir()
Ben Pastene243302c2018-09-21 16:06:08 -0700464 if self.copy_on_write:
465 self._CreateQcow2Image()
Mike Frysinger97080242017-09-13 01:58:45 -0400466 # Make sure we can read these files later on by creating them as ourselves.
467 osutils.Touch(self.kvm_serial)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700468 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
Mike Frysinger0444f4c2018-08-03 15:12:46 -0400469 os.mkfifo(pipe, 0o600)
Mike Frysinger97080242017-09-13 01:58:45 -0400470 osutils.Touch(self.pidfile)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700471
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800472 qemu_args = [self.qemu_path]
473 if self.qemu_bios_path:
474 if not os.path.isdir(self.qemu_bios_path):
475 raise VMError('Invalid QEMU bios path: %s' % self.qemu_bios_path)
476 qemu_args += ['-L', self.qemu_bios_path]
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +0100477
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800478 qemu_args += [
Po-Hsien Wang1cec8d12018-01-25 16:36:44 -0800479 '-m', self.qemu_m, '-smp', str(self.qemu_smp), '-vga', 'virtio',
480 '-daemonize', '-usbdevice', 'tablet',
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800481 '-pidfile', self.pidfile,
482 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
483 '-serial', 'file:%s' % self.kvm_serial,
484 '-mon', 'chardev=control_pipe',
Po-Hsien Wang1cec8d12018-01-25 16:36:44 -0800485 # Append 'check' to warn if the requested CPU is not fully supported.
486 '-cpu', self.qemu_cpu + ',check',
Lepton Wuaa6cca42018-04-19 18:56:29 -0700487 '-device', 'virtio-net,netdev=eth0',
Achuith Bhandarkarb00bafc2018-09-11 16:30:13 -0700488 '-netdev', 'user,id=eth0,net=10.0.2.0/27,hostfwd=tcp:%s:%d-:22'
489 % (remote_access.LOCALHOST_IP, self.ssh_port),
Nicolas Norvezf527cdf2018-01-26 11:55:44 -0800490 '-drive', 'file=%s,index=0,media=disk,cache=unsafe,format=%s'
491 % (self.image_path, self.image_format),
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800492 ]
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700493 if self.enable_kvm:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800494 qemu_args.append('-enable-kvm')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700495 if not self.display:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800496 qemu_args.extend(['-display', 'none'])
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700497 logging.info('Pid file: %s', self.pidfile)
Achuith Bhandarkara58355c2018-11-16 15:56:40 -0800498 self.RunCommand(qemu_args)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700499
500 def _GetVMPid(self):
501 """Get the pid of the VM.
502
503 Returns:
504 pid of the VM.
505 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700506 if not os.path.exists(self.vm_dir):
507 logging.debug('%s not present.', self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700508 return 0
509
510 if not os.path.exists(self.pidfile):
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700511 logging.info('%s does not exist.', self.pidfile)
512 return 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700513
Mike Frysinger97080242017-09-13 01:58:45 -0400514 pid = osutils.ReadFile(self.pidfile).rstrip()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700515 if not pid.isdigit():
Mike Frysinger97080242017-09-13 01:58:45 -0400516 # Ignore blank/empty files.
517 if pid:
518 logging.error('%s in %s is not a pid.', pid, self.pidfile)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700519 return 0
520
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700521 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700522
523 def IsRunning(self):
524 """Returns True if there's a running VM.
525
526 Returns:
527 True if there's a running VM.
528 """
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700529 pid = self._GetVMPid()
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700530 if not pid:
531 return False
532
533 # Make sure the process actually exists.
Mike Frysinger97080242017-09-13 01:58:45 -0400534 return os.path.isdir('/proc/%i' % pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700535
536 def Stop(self):
537 """Stop the VM."""
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700538 logging.debug('Stop VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700539
540 pid = self._GetVMPid()
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700541 if pid:
Achuith Bhandarkara58355c2018-11-16 15:56:40 -0800542 self.RunCommand(['kill', '-9', str(pid)], error_code_ok=True)
Achuith Bhandarkarfd5d1852018-03-26 14:50:54 -0700543 self._RmVMDir()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700544
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700545 def _WaitForProcs(self):
546 """Wait for expected processes to launch."""
547 class _TooFewPidsException(Exception):
548 """Exception for _GetRunningPids to throw."""
549
550 def _GetRunningPids(exe, numpids):
551 pids = self.remote.GetRunningPids(exe, full_path=False)
552 logging.info('%s pids: %s', exe, repr(pids))
553 if len(pids) < numpids:
554 raise _TooFewPidsException()
555
556 def _WaitForProc(exe, numpids):
557 try:
558 retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700559 exception=_TooFewPidsException,
Achuith Bhandarkarb00bafc2018-09-11 16:30:13 -0700560 max_retry=5,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700561 functor=lambda: _GetRunningPids(exe, numpids),
562 sleep=2)
563 except _TooFewPidsException:
564 raise VMError('_WaitForProcs failed: timed out while waiting for '
565 '%d %s processes to start.' % (numpids, exe))
566
567 # We could also wait for session_manager, nacl_helper, etc, but chrome is
568 # the long pole. We expect the parent, 2 zygotes, gpu-process, renderer.
569 # This could potentially break with Mustash.
570 _WaitForProc('chrome', 5)
571
572 def WaitForBoot(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700573 """Wait for the VM to boot up.
574
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700575 Wait for ssh connection to become active, and wait for all expected chrome
576 processes to be launched.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700577 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700578 if not os.path.exists(self.vm_dir):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700579 self.Start()
580
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -0700581 super(VM, self).WaitForBoot()
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700582
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100583 # Chrome can take a while to start with software emulation.
584 if not self.enable_kvm:
585 self._WaitForProcs()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700586
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100587 @staticmethod
Achuith Bhandarkar2beae292018-03-26 16:44:53 -0700588 def GetParser():
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100589 """Parse a list of args.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700590
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100591 Args:
592 argv: list of command line arguments.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700593
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100594 Returns:
595 List of parsed opts.
596 """
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -0700597 device_parser = Device.GetParser()
598 parser = commandline.ArgumentParser(description=__doc__,
599 parents=[device_parser],
600 add_help=False, logging=False)
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100601 parser.add_argument('--start', action='store_true', default=False,
602 help='Start the VM.')
603 parser.add_argument('--stop', action='store_true', default=False,
604 help='Stop the VM.')
Ben Pastene243302c2018-09-21 16:06:08 -0700605 parser.add_argument('--image-path', type='path',
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100606 help='Path to VM image to launch with --start.')
Nicolas Norvezf527cdf2018-01-26 11:55:44 -0800607 parser.add_argument('--image-format', default=VM.IMAGE_FORMAT,
608 help='Format of the VM image (raw, qcow2, ...).')
Ben Pastene243302c2018-09-21 16:06:08 -0700609 parser.add_argument('--qemu-path', type='path',
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100610 help='Path of qemu binary to launch with --start.')
Po-Hsien Wang1cec8d12018-01-25 16:36:44 -0800611 parser.add_argument('--qemu-m', type=str, default='8G',
612 help='Memory argument that will be passed to qemu.')
613 parser.add_argument('--qemu-smp', type=int, default='0',
614 help='SMP argument that will be passed to qemu. (0 '
615 'means auto-detection.)')
Po-Hsien Wangc5b335f2018-02-06 18:36:16 -0800616 # TODO(pwang): replace SandyBridge to Haswell-noTSX once lab machine
617 # running VMTest all migrate to GCE.
Po-Hsien Wang1cec8d12018-01-25 16:36:44 -0800618 parser.add_argument('--qemu-cpu', type=str,
Po-Hsien Wangc5b335f2018-02-06 18:36:16 -0800619 default='SandyBridge,-invpcid,-tsc-deadline',
Po-Hsien Wang1cec8d12018-01-25 16:36:44 -0800620 help='CPU argument that will be passed to qemu.')
Ben Pastene243302c2018-09-21 16:06:08 -0700621 parser.add_argument('--qemu-bios-path', type='path',
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800622 help='Path of directory with qemu bios files.')
Ben Pastene243302c2018-09-21 16:06:08 -0700623 parser.add_argument('--copy-on-write', action='store_true', default=False,
624 help='Generates a temporary copy-on-write image backed '
625 'by the normal boot image. All filesystem changes '
626 'will instead be reflected in the temporary '
627 'image.')
628 parser.add_argument('--qemu-img-path', type='path',
629 help='Path to qemu-img binary used to create temporary '
630 'copy-on-write images.')
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100631 parser.add_argument('--disable-kvm', dest='enable_kvm',
632 action='store_false', default=True,
633 help='Disable KVM, use software emulation.')
634 parser.add_argument('--no-display', dest='display',
635 action='store_false', default=True,
636 help='Do not display video output.')
637 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
638 help='ssh port to communicate with VM.')
Ben Pastene243302c2018-09-21 16:06:08 -0700639 parser.add_argument('--cache-dir', type='path',
Achuith Bhandarkar02ef76b2018-04-05 10:47:33 +0200640 default=path_util.GetCacheDir(),
641 help='Cache directory to use.')
Ben Pastene243302c2018-09-21 16:06:08 -0700642 parser.add_argument('--vm-dir', type='path',
Achuith Bhandarkar2beae292018-03-26 16:44:53 -0700643 help='Temp VM directory to use.')
Achuith Bhandarkar2beae292018-03-26 16:44:53 -0700644 return parser
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700645
Achuith Bhandarkar9f49aca2018-10-30 17:46:07 -0700646
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700647def main(argv):
Achuith Bhandarkar2beae292018-03-26 16:44:53 -0700648 opts = VM.GetParser().parse_args(argv)
649 opts.Freeze()
650
651 vm = VM(opts)
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100652 vm.Run()