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