blob: b7305377e81ea4ba1875d11a7606ed02394822d3 [file] [log] [blame]
Ryan Cui3045c5d2012-07-13 18:00:33 -07001#!/usr/bin/python
2# Copyright (c) 2012 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
Ryan Cuia56a71e2012-10-18 18:40:35 -07006
7"""
8Script that deploys a Chrome build to a device.
9
10The script supports deploying Chrome from these sources:
11
121. A local build output directory, such as chromium/src/out/[Debug|Release].
132. A Chrome tarball uploaded by a trybot/official-builder to GoogleStorage.
143. A Chrome tarball existing locally.
15
16The script copies the necessary contents of the source location (tarball or
17build directory) and rsyncs the contents of the staging directory onto your
18device's rootfs.
19"""
Ryan Cui3045c5d2012-07-13 18:00:33 -070020
21import functools
22import logging
Ryan Cui3045c5d2012-07-13 18:00:33 -070023import os
Ryan Cuia56a71e2012-10-18 18:40:35 -070024import optparse
Ryan Cui3045c5d2012-07-13 18:00:33 -070025import time
Ryan Cui3045c5d2012-07-13 18:00:33 -070026
Ryan Cuia56a71e2012-10-18 18:40:35 -070027
David James629febb2012-11-25 13:07:34 -080028from chromite.buildbot import constants
Ryan Cuia56a71e2012-10-18 18:40:35 -070029from chromite.lib import chrome_util
Ryan Cui3045c5d2012-07-13 18:00:33 -070030from chromite.lib import cros_build_lib
Ryan Cuie535b172012-10-19 18:25:03 -070031from chromite.lib import commandline
Ryan Cui3045c5d2012-07-13 18:00:33 -070032from chromite.lib import osutils
33from chromite.lib import remote_access as remote
34from chromite.lib import sudo
35
36
Ryan Cuia56a71e2012-10-18 18:40:35 -070037_USAGE = "deploy_chrome [--]\n\n %s" % __doc__
38
Ryan Cui3045c5d2012-07-13 18:00:33 -070039GS_HTTP = 'https://commondatastorage.googleapis.com'
40GSUTIL_URL = '%s/chromeos-public/gsutil.tar.gz' % GS_HTTP
41GS_RETRIES = 5
42KERNEL_A_PARTITION = 2
43KERNEL_B_PARTITION = 4
44
45KILL_PROC_MAX_WAIT = 10
46POST_KILL_WAIT = 2
47
Ryan Cuie535b172012-10-19 18:25:03 -070048MOUNT_RW_COMMAND = 'mount -o remount,rw /'
Ryan Cui3045c5d2012-07-13 18:00:33 -070049
50# Convenience RunCommand methods
51DebugRunCommand = functools.partial(
52 cros_build_lib.RunCommand, debug_level=logging.DEBUG)
53
54DebugRunCommandCaptureOutput = functools.partial(
55 cros_build_lib.RunCommandCaptureOutput, debug_level=logging.DEBUG)
56
57DebugSudoRunCommand = functools.partial(
58 cros_build_lib.SudoRunCommand, debug_level=logging.DEBUG)
59
60
61def _TestGSLs(gs_bin):
62 """Quick test of gsutil functionality."""
63 result = DebugRunCommandCaptureOutput([gs_bin, 'ls'], error_code_ok=True)
64 return not result.returncode
65
66
67def _SetupBotoConfig(gs_bin):
68 """Make sure we can access protected bits in GS."""
69 boto_path = os.path.expanduser('~/.boto')
70 if os.path.isfile(boto_path) or _TestGSLs(gs_bin):
71 return
72
73 logging.info('Configuring gsutil. Please use your @google.com account.')
74 try:
75 cros_build_lib.RunCommand([gs_bin, 'config'], print_cmd=False)
76 finally:
77 if os.path.exists(boto_path) and not os.path.getsize(boto_path):
78 os.remove(boto_path)
79
80
81def _UrlBaseName(url):
82 """Return the last component of the URL."""
83 return url.rstrip('/').rpartition('/')[-1]
84
85
86def _ExtractChrome(src, dest):
87 osutils.SafeMakedirs(dest)
88 # Preserve permissions (-p). This is default when running tar with 'sudo'.
89 DebugSudoRunCommand(['tar', '--checkpoint', '-xf', src],
90 cwd=dest)
91
92
93class DeployChrome(object):
94 """Wraps the core deployment functionality."""
Ryan Cuia56a71e2012-10-18 18:40:35 -070095 def __init__(self, options, tempdir, staging_dir):
Ryan Cuie535b172012-10-19 18:25:03 -070096 """Initialize the class.
97
98 Arguments:
99 options: Optparse result structure.
100 tempdir: Scratch space for the class. Caller has responsibility to clean
101 it up.
Ryan Cuie535b172012-10-19 18:25:03 -0700102 """
Ryan Cui3045c5d2012-07-13 18:00:33 -0700103 self.tempdir = tempdir
104 self.options = options
Ryan Cuia56a71e2012-10-18 18:40:35 -0700105 self.staging_dir = staging_dir
Ryan Cuiafd6c5c2012-07-30 17:48:22 -0700106 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700107 self.start_ui_needed = False
108
Ryan Cui3045c5d2012-07-13 18:00:33 -0700109 def _ChromeFileInUse(self):
110 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
111 error_code_ok=True)
112 return result.returncode == 0
113
114 def _DisableRootfsVerification(self):
115 if not self.options.force:
116 logging.error('Detected that the device has rootfs verification enabled.')
117 logging.info('This script can automatically remove the rootfs '
118 'verification, which requires that it reboot the device.')
119 logging.info('Make sure the device is in developer mode!')
120 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -0700121 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700122 cros_build_lib.Die('Need rootfs verification to be disabled. '
123 'Aborting.')
124
125 logging.info('Removing rootfs verification from %s', self.options.to)
126 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
127 # Use --force to bypass the checks.
128 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
129 '--remove_rootfs_verification --force')
130 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
131 self.host.RemoteSh(cmd % partition, error_code_ok=True)
132
133 # A reboot in developer mode takes a while (and has delays), so the user
134 # will have time to read and act on the USB boot instructions below.
135 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
136 self.host.RemoteReboot()
137
138 def _CheckRootfsWriteable(self):
139 # /proc/mounts is in the format:
140 # <device> <dir> <type> <options>
141 result = self.host.RemoteSh('cat /proc/mounts')
142 for line in result.output.splitlines():
143 components = line.split()
144 if components[0] == '/dev/root' and components[1] == '/':
145 return 'rw' in components[3].split(',')
146 else:
147 raise Exception('Internal error - rootfs mount not found!')
148
149 def _CheckUiJobStarted(self):
150 # status output is in the format:
151 # <job_name> <status> ['process' <pid>].
152 # <status> is in the format <goal>/<state>.
153 result = self.host.RemoteSh('status ui')
154 return result.output.split()[1].split('/')[0] == 'start'
155
156 def _KillProcsIfNeeded(self):
157 if self._CheckUiJobStarted():
158 logging.info('Shutting down Chrome.')
159 self.start_ui_needed = True
160 self.host.RemoteSh('stop ui')
161
162 # Developers sometimes run session_manager manually, in which case we'll
163 # need to help shut the chrome processes down.
164 try:
165 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
166 while self._ChromeFileInUse():
167 logging.warning('The chrome binary on the device is in use.')
168 logging.warning('Killing chrome and session_manager processes...\n')
169
170 self.host.RemoteSh("pkill 'chrome|session_manager'",
171 error_code_ok=True)
172 # Wait for processes to actually terminate
173 time.sleep(POST_KILL_WAIT)
174 logging.info('Rechecking the chrome binary...')
175 except cros_build_lib.TimeoutError:
176 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
177 'exit any running chrome processes and try again.')
178
179 def _PrepareTarget(self):
180 # Mount root partition as read/write
181 if not self._CheckRootfsWriteable():
182 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700183 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700184 if result.returncode:
185 self._DisableRootfsVerification()
186 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700187 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700188
189 if not self._CheckRootfsWriteable():
190 cros_build_lib.Die('Root partition still read-only')
191
192 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
193 # makes sense to have even when going the sshfs route.
194 self._KillProcsIfNeeded()
195
196 def _Deploy(self):
197 logging.info('Copying Chrome to device.')
198 # Show the output (status) for this command.
Ryan Cuia56a71e2012-10-18 18:40:35 -0700199 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir), '/',
200 inplace=True, debug_level=logging.INFO)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700201 if self.start_ui_needed:
202 self.host.RemoteSh('start ui')
203
204 def Perform(self):
205 try:
206 logging.info('Testing connection to the device.')
207 self.host.RemoteSh('true')
208 except cros_build_lib.RunCommandError:
209 logging.error('Error connecting to the test device.')
210 raise
211
Ryan Cui3045c5d2012-07-13 18:00:33 -0700212 self._PrepareTarget()
213 self._Deploy()
214
215
Ryan Cuia56a71e2012-10-18 18:40:35 -0700216def ValidateGypDefines(_option, _opt, value):
217 """Convert GYP_DEFINES-formatted string to dictionary."""
218 return chrome_util.ProcessGypDefines(value)
219
220
221class CustomOption(commandline.Option):
222 """Subclass Option class to implement path evaluation."""
223 TYPES = commandline.Option.TYPES + ('gyp_defines',)
224 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
225 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
226
227
Ryan Cuie535b172012-10-19 18:25:03 -0700228def _CreateParser():
229 """Create our custom parser."""
Ryan Cuia56a71e2012-10-18 18:40:35 -0700230 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700231
Ryan Cuia56a71e2012-10-18 18:40:35 -0700232 # TODO(rcui): Have this use the UI-V2 format of having source and target
233 # device be specified as positional arguments.
Ryan Cui3045c5d2012-07-13 18:00:33 -0700234 parser.add_option('--force', action='store_true', default=False,
235 help=('Skip all prompts (i.e., for disabling of rootfs '
236 'verification). This may result in the target '
237 'machine being rebooted.'))
Ryan Cuia56a71e2012-10-18 18:40:35 -0700238 parser.add_option('--build-dir', type='path',
239 help=('The directory with Chrome build artifacts to deploy '
240 'from. Typically of format <chrome_root>/out/Debug. '
241 'When this option is used, the GYP_DEFINES '
242 'environment variable must be set.'))
Ryan Cui3045c5d2012-07-13 18:00:33 -0700243 parser.add_option('-g', '--gs-path', type='gs_path',
244 help=('GS path that contains the chrome to deploy.'))
Ryan Cui3045c5d2012-07-13 18:00:33 -0700245 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
246 help=('Port of the target device to connect to.'))
247 parser.add_option('-t', '--to',
248 help=('The IP address of the CrOS device to deploy to.'))
249 parser.add_option('-v', '--verbose', action='store_true', default=False,
250 help=('Show more debug output.'))
Ryan Cuia56a71e2012-10-18 18:40:35 -0700251
252 group = optparse.OptionGroup(parser, 'Advanced Options')
253 group.add_option('-l', '--local-pkg-path', type='path',
254 help='path to local chrome prebuilt package to deploy.')
255 group.add_option('--staging-flags', default={}, type='gyp_defines',
256 help=('Extra flags to control staging. Valid flags '
257 'are - %s' % ', '.join(chrome_util.STAGING_FLAGS)))
258 parser.add_option_group(group)
259
260 # Path of an empty directory to stage chrome artifacts to. Defaults to a
261 # temporary directory that is removed when the script finishes. If the path
262 # is specified, then it will not be removed.
263 parser.add_option('--staging-dir', type='path', default=None,
264 help=optparse.SUPPRESS_HELP)
265 # Only prepare the staging directory, and skip deploying to the device.
266 parser.add_option('--staging-only', action='store_true', default=False,
267 help=optparse.SUPPRESS_HELP)
268 # GYP_DEFINES that Chrome was built with. Influences which files are staged
269 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
270 # enviroment variable.
271 parser.add_option('--gyp-defines', default={}, type='gyp_defines',
272 help=optparse.SUPPRESS_HELP)
Ryan Cuie535b172012-10-19 18:25:03 -0700273 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700274
Ryan Cuie535b172012-10-19 18:25:03 -0700275
276def _ParseCommandLine(argv):
277 """Parse args, and run environment-independent checks."""
278 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700279 (options, args) = parser.parse_args(argv)
280
Ryan Cuia56a71e2012-10-18 18:40:35 -0700281 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
282 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
283 '--build_dir')
284 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
285 parser.error('Cannot specify both --build_dir and '
286 '--gs-path/--local-pkg-patch')
287 if options.gs_path and options.local_pkg_path:
288 parser.error('Cannot specify both --gs-path and --local-pkg-path')
289 if not (options.staging_only or options.to):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700290 parser.error('Need to specify --to')
291
292 return options, args
293
294
Ryan Cuie535b172012-10-19 18:25:03 -0700295def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700296 """Perform some usage validation (after we've parsed the arguments
297
298 Args:
299 options/args: The options/args object returned by optparse
300 """
Ryan Cuia56a71e2012-10-18 18:40:35 -0700301 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
302 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
303
304 if options.build_dir and not options.gyp_defines:
305 gyp_env = os.getenv('GYP_DEFINES', None)
306 if gyp_env is not None:
307 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
308 logging.info('GYP_DEFINES taken from environment: %s',
309 options.gyp_defines)
310 else:
311 cros_build_lib.Die('When --build-dir is set, the GYP_DEFINES environment '
312 'variable must be set.')
313
314
315def _FetchChromePackage(tempdir, gs_path):
316 """Get the chrome prebuilt tarball from GS.
317
318 Returns: Path to the fetched chrome tarball.
319 """
320 logging.info('Fetching gsutil.')
321 gsutil_tar = os.path.join(tempdir, 'gsutil.tar.gz')
322 cros_build_lib.RunCurl([GSUTIL_URL, '-o', gsutil_tar],
323 debug_level=logging.DEBUG)
324 DebugRunCommand(['tar', '-xzf', gsutil_tar], cwd=tempdir)
325 gs_bin = os.path.join(tempdir, 'gsutil', 'gsutil')
326 _SetupBotoConfig(gs_bin)
327 cmd = [gs_bin, 'ls', gs_path]
328 files = DebugRunCommandCaptureOutput(cmd).output.splitlines()
329 files = [found for found in files if
David James629febb2012-11-25 13:07:34 -0800330 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
Ryan Cuia56a71e2012-10-18 18:40:35 -0700331 if not files:
332 raise Exception('No chrome package found at %s' % gs_path)
333 elif len(files) > 1:
334 # - Users should provide us with a direct link to either a stripped or
335 # unstripped chrome package.
336 # - In the case of being provided with an archive directory, where both
337 # stripped and unstripped chrome available, use the stripped chrome
338 # package.
339 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
340 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
341 files = [f for f in files if not 'unstripped' in f]
342 assert len(files) == 1
343 logging.warning('Multiple chrome packages found. Using %s', files[0])
344
345 filename = _UrlBaseName(files[0])
346 logging.info('Fetching %s.', filename)
347 cros_build_lib.RunCommand([gs_bin, 'cp', files[0], tempdir],
348 print_cmd=False)
349 chrome_path = os.path.join(tempdir, filename)
350 assert os.path.exists(chrome_path)
351 return chrome_path
352
353
354def _PrepareStagingDir(options, tempdir, staging_dir):
355 """Place the necessary files in the staging directory.
356
357 The staging directory is the directory used to rsync the build artifacts over
358 to the device. Only the necessary Chrome build artifacts are put into the
359 staging directory.
360 """
361 if options.build_dir:
362 chrome_util.StageChromeFromBuildDir(
363 staging_dir, options.build_dir, options.gyp_defines,
364 options.staging_flags)
365 else:
366 pkg_path = options.local_pkg_path
367 if options.gs_path:
368 pkg_path = _FetchChromePackage(tempdir, options.gs_path)
369
370 assert pkg_path
371 logging.info('Extracting %s.', pkg_path)
372 _ExtractChrome(pkg_path, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700373
374
375def main(argv):
376 options, args = _ParseCommandLine(argv)
377 _PostParseCheck(options, args)
378
379 # Set cros_build_lib debug level to hide RunCommand spew.
380 if options.verbose:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700381 logging.getLogger().setLevel(logging.DEBUG)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700382 else:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700383 logging.getLogger().setLevel(logging.WARNING)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700384
David James891dccf2012-08-20 14:19:54 -0700385 with sudo.SudoKeepAlive(ttyless_sudo=False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700386 with osutils.TempDirContextManager(sudo_rm=True) as tempdir:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700387 staging_dir = options.staging_dir
388 if not staging_dir:
389 staging_dir = os.path.join(tempdir, 'chrome')
390 _PrepareStagingDir(options, tempdir, staging_dir)
391
392 if options.staging_only:
393 return 0
394
395 deploy = DeployChrome(options, tempdir, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700396 deploy.Perform()