blob: 52185c8ebe512c1ab784ef0861a2811eb5e3f17a [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 Bhandarkard8d19292016-05-03 14:32:58 -070012import os
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +010013import re
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070014
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -080015from chromite.cli.cros import cros_chrome_sdk
16from chromite.lib import cache
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070017from chromite.lib import commandline
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -080018from chromite.lib import constants
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070019from chromite.lib import cros_build_lib
20from chromite.lib import cros_logging as logging
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -070021from chromite.lib import osutils
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -080022from chromite.lib import path_util
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070023from chromite.lib import remote_access
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -070024from chromite.lib import retry_util
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070025
26
27class VMError(Exception):
28 """Exception for VM failures."""
29
30 def __init__(self, message):
31 super(VMError, self).__init__()
32 logging.error(message)
33
34
35class VM(object):
36 """Class for managing a VM."""
37
38 SSH_PORT = 9222
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070039
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010040 def __init__(self, argv):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070041 """Initialize VM.
42
43 Args:
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010044 argv: command line args.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070045 """
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010046 opts = self._ParseArgs(argv)
47 opts.Freeze()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070048
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010049 self.qemu_path = opts.qemu_path
Achuith Bhandarkar41259652017-11-14 10:31:02 -080050 self.qemu_bios_path = opts.qemu_bios_path
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010051 self.enable_kvm = opts.enable_kvm
Achuith Bhandarkarf877da22017-09-12 12:27:39 -070052 # We don't need sudo access for software emulation or if /dev/kvm is
53 # writeable.
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010054 self.use_sudo = self.enable_kvm and not os.access('/dev/kvm', os.W_OK)
55 self.display = opts.display
56 self.image_path = opts.image_path
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -080057 self.board = opts.board
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010058 self.ssh_port = opts.ssh_port
59 self.dry_run = opts.dry_run
60
61 self.start = opts.start
62 self.stop = opts.stop
63 self.cmd = opts.args[1:] if opts.cmd else None
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070064
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070065 self.vm_dir = os.path.join(osutils.GetGlobalTempDir(), 'cros_vm')
66 if os.path.exists(self.vm_dir):
67 # For security, ensure that vm_dir is not a symlink, and is owned by us or
68 # by root.
69 assert not os.path.islink(self.vm_dir), \
70 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
71 st_uid = os.stat(self.vm_dir).st_uid
72 assert st_uid == 0 or st_uid == os.getuid(), \
73 'VM state dir is misconfigured; please recreate: %s' % self.vm_dir
74
75 self.pidfile = os.path.join(self.vm_dir, 'kvm.pid')
76 self.kvm_monitor = os.path.join(self.vm_dir, 'kvm.monitor')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070077 self.kvm_pipe_in = '%s.in' % self.kvm_monitor # to KVM
78 self.kvm_pipe_out = '%s.out' % self.kvm_monitor # from KVM
79 self.kvm_serial = '%s.serial' % self.kvm_monitor
80
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -070081 self.remote = remote_access.RemoteDevice(remote_access.LOCALHOST,
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +010082 port=self.ssh_port)
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -070083
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070084 # TODO(achuith): support nographics, snapshot, mem_path, usb_passthrough,
85 # moblab, etc.
86
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070087 def _RunCommand(self, *args, **kwargs):
88 """Use SudoRunCommand or RunCommand as necessary."""
89 if self.use_sudo:
90 return cros_build_lib.SudoRunCommand(*args, **kwargs)
91 else:
92 return cros_build_lib.RunCommand(*args, **kwargs)
93
94 def _CleanupFiles(self, recreate):
95 """Cleanup vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070096
97 Args:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -070098 recreate: recreate vm_dir.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -070099 """
Mike Frysinger97080242017-09-13 01:58:45 -0400100 osutils.RmDir(self.vm_dir, ignore_missing=True, sudo=self.use_sudo)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700101 if recreate:
Mike Frysinger97080242017-09-13 01:58:45 -0400102 osutils.SafeMakedirs(self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700103
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +0100104 @cros_build_lib.MemoizedSingleCall
105 def QemuVersion(self):
106 """Determine QEMU version."""
107 version_str = self._RunCommand([self.qemu_path, '--version'],
108 capture_output=True).output
109 # version string looks like one of these:
110 # QEMU emulator version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.36), Copyright (c)
111 # 2003-2008 Fabrice Bellard
112 #
113 # QEMU emulator version 2.6.0, Copyright (c) 2003-2008 Fabrice Bellard
114 #
115 # qemu-x86_64 version 2.10.1
116 # Copyright (c) 2003-2017 Fabrice Bellard and the QEMU Project developers
117 m = re.search(r"version ([0-9.]+)", version_str)
118 if not m:
119 raise VMError('Unable to determine QEMU version from:\n%s.' % version_str)
120 return m.group(1)
121
122 def _CheckQemuMinVersion(self):
123 """Ensure minimum QEMU version."""
124 min_qemu_version = '2.6.0'
125 logging.info('QEMU version %s', self.QemuVersion())
126 LooseVersion = distutils.version.LooseVersion
127 if LooseVersion(self.QemuVersion()) < LooseVersion(min_qemu_version):
128 raise VMError('QEMU %s is the minimum supported version. You have %s.'
129 % (min_qemu_version, self.QemuVersion()))
130
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800131 def _SetQemuPath(self):
132 """Find a suitable Qemu executable."""
133 if not self.qemu_path:
134 self.qemu_path = osutils.Which('qemu-system-x86_64')
135 if not self.qemu_path:
136 raise VMError('Qemu not found.')
137 logging.debug('Qemu path: %s', self.qemu_path)
138 self._CheckQemuMinVersion()
139
140 def _GetBuiltVMImagePath(self):
141 """Get path of a locally built VM image."""
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800142 return os.path.join(constants.SOURCE_ROOT, 'src/build/images',
143 cros_build_lib.GetBoard(self.board),
144 'latest', constants.VM_IMAGE_BIN)
145
146 def _GetDownloadedVMImagePath(self):
147 """Get path of a downloaded VM image."""
148 tarball_cache = cache.TarballCache(os.path.join(
149 path_util.GetCacheDir(),
150 cros_chrome_sdk.COMMAND_NAME,
151 cros_chrome_sdk.SDKFetcher.TARBALL_CACHE))
152 lkgm = cros_chrome_sdk.SDKFetcher.GetChromeLKGM()
153 if not lkgm:
154 return None
155 cache_key = (self.board, lkgm, constants.VM_IMAGE_TAR)
156 with tarball_cache.Lookup(cache_key) as ref:
157 if ref.Exists():
158 return os.path.join(ref.path, constants.VM_IMAGE_BIN)
159 return None
160
161 def _SetVMImagePath(self):
162 """Detect VM image path in SDK and chroot."""
163 if not self.image_path:
164 self.image_path = (self._GetDownloadedVMImagePath() or
165 self._GetBuiltVMImagePath())
166 if not self.image_path:
167 raise VMError('No VM image found. Use cros chrome-sdk --download-vm.')
168 if not os.path.isfile(self.image_path):
169 raise VMError('VM image does not exist: %s' % self.image_path)
170 logging.debug('VM image path: %s', self.image_path)
171
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100172 def Run(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700173 """Performs an action, one of start, stop, or run a command in the VM.
174
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700175 Returns:
176 cmd output.
177 """
178
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100179 if not self.start and not self.stop and not self.cmd:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700180 raise VMError('Must specify one of start, stop, or cmd.')
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100181 if self.start:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700182 self.Start()
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100183 if self.cmd:
184 return self.RemoteCommand(self.cmd)
185 if self.stop:
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700186 self.Stop()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700187
188 def Start(self):
189 """Start the VM."""
190
191 self.Stop()
192
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700193 logging.debug('Start VM')
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800194 self._SetQemuPath()
195 self._SetVMImagePath()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700196
197 self._CleanupFiles(recreate=True)
Mike Frysinger97080242017-09-13 01:58:45 -0400198 # Make sure we can read these files later on by creating them as ourselves.
199 osutils.Touch(self.kvm_serial)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700200 for pipe in [self.kvm_pipe_in, self.kvm_pipe_out]:
201 os.mkfifo(pipe, 0600)
Mike Frysinger97080242017-09-13 01:58:45 -0400202 osutils.Touch(self.pidfile)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700203
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800204 qemu_args = [self.qemu_path]
205 if self.qemu_bios_path:
206 if not os.path.isdir(self.qemu_bios_path):
207 raise VMError('Invalid QEMU bios path: %s' % self.qemu_bios_path)
208 qemu_args += ['-L', self.qemu_bios_path]
Achuith Bhandarkar22bfedf2017-11-08 11:59:38 +0100209
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800210 qemu_args += [
211 '-m', '2G', '-smp', '4', '-vga', 'virtio', '-daemonize',
Nicolas Norvez02525112017-11-30 16:29:23 -0800212 '-usbdevice', 'tablet',
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800213 '-pidfile', self.pidfile,
214 '-chardev', 'pipe,id=control_pipe,path=%s' % self.kvm_monitor,
215 '-serial', 'file:%s' % self.kvm_serial,
216 '-mon', 'chardev=control_pipe',
217 # Qemu-vlans are used by qemu to separate out network traffic on the
218 # slirp network bridge. qemu forwards traffic on a slirp vlan to all
219 # ports conected on that vlan. By default, slirp ports are on vlan
220 # 0. We explicitly set a vlan here so that another qemu VM using
221 # slirp doesn't conflict with our network traffic.
222 '-net', 'nic,model=virtio,vlan=%d' % self.ssh_port,
223 '-net', 'user,hostfwd=tcp:127.0.0.1:%d-:22,vlan=%d'
224 % (self.ssh_port, self.ssh_port),
225 '-drive', 'file=%s,index=0,media=disk,cache=unsafe,format=raw'
226 % self.image_path,
227 ]
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700228 if self.enable_kvm:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800229 qemu_args.append('-enable-kvm')
Achuith Bhandarkarb891adb2016-10-24 18:43:22 -0700230 if not self.display:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800231 qemu_args.extend(['-display', 'none'])
232 logging.info(cros_build_lib.CmdToStr(qemu_args))
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700233 logging.info('Pid file: %s', self.pidfile)
234 if not self.dry_run:
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800235 self._RunCommand(qemu_args)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700236
237 def _GetVMPid(self):
238 """Get the pid of the VM.
239
240 Returns:
241 pid of the VM.
242 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700243 if not os.path.exists(self.vm_dir):
244 logging.debug('%s not present.', self.vm_dir)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700245 return 0
246
247 if not os.path.exists(self.pidfile):
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700248 logging.info('%s does not exist.', self.pidfile)
249 return 0
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700250
Mike Frysinger97080242017-09-13 01:58:45 -0400251 pid = osutils.ReadFile(self.pidfile).rstrip()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700252 if not pid.isdigit():
Mike Frysinger97080242017-09-13 01:58:45 -0400253 # Ignore blank/empty files.
254 if pid:
255 logging.error('%s in %s is not a pid.', pid, self.pidfile)
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700256 return 0
257
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700258 return int(pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700259
260 def IsRunning(self):
261 """Returns True if there's a running VM.
262
263 Returns:
264 True if there's a running VM.
265 """
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700266 pid = self._GetVMPid()
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700267 if not pid:
268 return False
269
270 # Make sure the process actually exists.
Mike Frysinger97080242017-09-13 01:58:45 -0400271 return os.path.isdir('/proc/%i' % pid)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700272
273 def Stop(self):
274 """Stop the VM."""
Achuith Bhandarkar022d69c2016-10-05 14:28:14 -0700275 logging.debug('Stop VM')
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700276
277 pid = self._GetVMPid()
Achuith Bhandarkarf4950ba2016-10-11 15:40:07 -0700278 if pid:
279 logging.info('Killing %d.', pid)
280 if not self.dry_run:
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700281 self._RunCommand(['kill', '-9', str(pid)], error_code_ok=True)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700282
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700283 self._CleanupFiles(recreate=False)
284
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700285 def _WaitForProcs(self):
286 """Wait for expected processes to launch."""
287 class _TooFewPidsException(Exception):
288 """Exception for _GetRunningPids to throw."""
289
290 def _GetRunningPids(exe, numpids):
291 pids = self.remote.GetRunningPids(exe, full_path=False)
292 logging.info('%s pids: %s', exe, repr(pids))
293 if len(pids) < numpids:
294 raise _TooFewPidsException()
295
296 def _WaitForProc(exe, numpids):
297 try:
298 retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700299 exception=_TooFewPidsException,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700300 max_retry=20,
301 functor=lambda: _GetRunningPids(exe, numpids),
302 sleep=2)
303 except _TooFewPidsException:
304 raise VMError('_WaitForProcs failed: timed out while waiting for '
305 '%d %s processes to start.' % (numpids, exe))
306
307 # We could also wait for session_manager, nacl_helper, etc, but chrome is
308 # the long pole. We expect the parent, 2 zygotes, gpu-process, renderer.
309 # This could potentially break with Mustash.
310 _WaitForProc('chrome', 5)
311
312 def WaitForBoot(self):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700313 """Wait for the VM to boot up.
314
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700315 Wait for ssh connection to become active, and wait for all expected chrome
316 processes to be launched.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700317 """
Achuith Bhandarkaree3163d2016-10-19 12:58:35 -0700318 if not os.path.exists(self.vm_dir):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700319 self.Start()
320
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700321 try:
322 result = retry_util.RetryException(
Achuith Bhandarkar0e7b8502017-06-12 15:32:41 -0700323 exception=remote_access.SSHConnectionError,
Achuith Bhandarkar2f8352f2017-06-02 12:47:18 -0700324 max_retry=10,
325 functor=lambda: self.RemoteCommand(cmd=['echo']),
326 sleep=5)
327 except remote_access.SSHConnectionError:
328 raise VMError('WaitForBoot timed out trying to connect to VM.')
329
330 if result.returncode != 0:
331 raise VMError('WaitForBoot failed: %s.' % result.error)
332
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100333 # Chrome can take a while to start with software emulation.
334 if not self.enable_kvm:
335 self._WaitForProcs()
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700336
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100337 def RemoteCommand(self, cmd, **kwargs):
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700338 """Run a remote command in the VM.
339
340 Args:
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100341 cmd: command to run.
342 kwargs: additional args (see documentation for RemoteDevice.RunCommand).
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700343 """
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700344 if not self.dry_run:
Achuith Bhandarkar65d1a892017-05-08 14:13:12 -0700345 return self.remote.RunCommand(cmd, debug_level=logging.INFO,
346 combine_stdout_stderr=True,
347 log_output=True,
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100348 error_code_ok=True,
349 **kwargs)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700350
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100351 @staticmethod
352 def _ParseArgs(argv):
353 """Parse a list of args.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700354
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100355 Args:
356 argv: list of command line arguments.
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700357
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100358 Returns:
359 List of parsed opts.
360 """
361 parser = commandline.ArgumentParser(description=__doc__)
362 parser.add_argument('--start', action='store_true', default=False,
363 help='Start the VM.')
364 parser.add_argument('--stop', action='store_true', default=False,
365 help='Stop the VM.')
366 parser.add_argument('--image-path', type='path',
367 help='Path to VM image to launch with --start.')
368 parser.add_argument('--qemu-path', type='path',
369 help='Path of qemu binary to launch with --start.')
Achuith Bhandarkar41259652017-11-14 10:31:02 -0800370 parser.add_argument('--qemu-bios-path', type='path',
371 help='Path of directory with qemu bios files.')
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100372 parser.add_argument('--disable-kvm', dest='enable_kvm',
373 action='store_false', default=True,
374 help='Disable KVM, use software emulation.')
375 parser.add_argument('--no-display', dest='display',
376 action='store_false', default=True,
377 help='Do not display video output.')
378 parser.add_argument('--ssh-port', type=int, default=VM.SSH_PORT,
379 help='ssh port to communicate with VM.')
Achuith Bhandarkar1297dcf2017-11-21 12:03:48 -0800380 sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
381 parser.add_argument('--board', default=sdk_board_env, help='Board to use.')
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100382 parser.add_argument('--dry-run', action='store_true', default=False,
383 help='dry run for debugging.')
384 parser.add_argument('--cmd', action='store_true', default=False,
385 help='Run a command in the VM.')
386 parser.add_argument('args', nargs=argparse.REMAINDER,
387 help='Command to run in the VM.')
388 return parser.parse_args(argv)
Achuith Bhandarkard8d19292016-05-03 14:32:58 -0700389
390def main(argv):
Achuith Bhandarkar2a39adf2017-10-30 10:24:45 +0100391 vm = VM(argv)
392 vm.Run()