blob: ff8d146997a06723382da55c70f547498d2a5215 [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 Cui777ff422012-12-07 13:12:54 -080032from chromite.lib import gs
Ryan Cui3045c5d2012-07-13 18:00:33 -070033from chromite.lib import osutils
34from chromite.lib import remote_access as remote
35from chromite.lib import sudo
36
37
Ryan Cuia56a71e2012-10-18 18:40:35 -070038_USAGE = "deploy_chrome [--]\n\n %s" % __doc__
39
Ryan Cui3045c5d2012-07-13 18:00:33 -070040GS_HTTP = 'https://commondatastorage.googleapis.com'
41GSUTIL_URL = '%s/chromeos-public/gsutil.tar.gz' % GS_HTTP
42GS_RETRIES = 5
43KERNEL_A_PARTITION = 2
44KERNEL_B_PARTITION = 4
45
46KILL_PROC_MAX_WAIT = 10
47POST_KILL_WAIT = 2
48
Ryan Cuie535b172012-10-19 18:25:03 -070049MOUNT_RW_COMMAND = 'mount -o remount,rw /'
Ryan Cui3045c5d2012-07-13 18:00:33 -070050
51# Convenience RunCommand methods
52DebugRunCommand = functools.partial(
53 cros_build_lib.RunCommand, debug_level=logging.DEBUG)
54
55DebugRunCommandCaptureOutput = functools.partial(
56 cros_build_lib.RunCommandCaptureOutput, debug_level=logging.DEBUG)
57
58DebugSudoRunCommand = functools.partial(
59 cros_build_lib.SudoRunCommand, debug_level=logging.DEBUG)
60
61
Ryan Cui3045c5d2012-07-13 18:00:33 -070062def _UrlBaseName(url):
63 """Return the last component of the URL."""
64 return url.rstrip('/').rpartition('/')[-1]
65
66
67def _ExtractChrome(src, dest):
68 osutils.SafeMakedirs(dest)
69 # Preserve permissions (-p). This is default when running tar with 'sudo'.
70 DebugSudoRunCommand(['tar', '--checkpoint', '-xf', src],
71 cwd=dest)
72
73
74class DeployChrome(object):
75 """Wraps the core deployment functionality."""
Ryan Cuia56a71e2012-10-18 18:40:35 -070076 def __init__(self, options, tempdir, staging_dir):
Ryan Cuie535b172012-10-19 18:25:03 -070077 """Initialize the class.
78
79 Arguments:
80 options: Optparse result structure.
81 tempdir: Scratch space for the class. Caller has responsibility to clean
82 it up.
Ryan Cuie535b172012-10-19 18:25:03 -070083 """
Ryan Cui3045c5d2012-07-13 18:00:33 -070084 self.tempdir = tempdir
85 self.options = options
Ryan Cuia56a71e2012-10-18 18:40:35 -070086 self.staging_dir = staging_dir
Ryan Cuiafd6c5c2012-07-30 17:48:22 -070087 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
Ryan Cui3045c5d2012-07-13 18:00:33 -070088 self.start_ui_needed = False
89
Ryan Cui3045c5d2012-07-13 18:00:33 -070090 def _ChromeFileInUse(self):
91 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
92 error_code_ok=True)
93 return result.returncode == 0
94
95 def _DisableRootfsVerification(self):
96 if not self.options.force:
97 logging.error('Detected that the device has rootfs verification enabled.')
98 logging.info('This script can automatically remove the rootfs '
99 'verification, which requires that it reboot the device.')
100 logging.info('Make sure the device is in developer mode!')
101 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -0700102 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700103 cros_build_lib.Die('Need rootfs verification to be disabled. '
104 'Aborting.')
105
106 logging.info('Removing rootfs verification from %s', self.options.to)
107 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
108 # Use --force to bypass the checks.
109 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
110 '--remove_rootfs_verification --force')
111 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
112 self.host.RemoteSh(cmd % partition, error_code_ok=True)
113
114 # A reboot in developer mode takes a while (and has delays), so the user
115 # will have time to read and act on the USB boot instructions below.
116 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
117 self.host.RemoteReboot()
118
119 def _CheckRootfsWriteable(self):
120 # /proc/mounts is in the format:
121 # <device> <dir> <type> <options>
122 result = self.host.RemoteSh('cat /proc/mounts')
123 for line in result.output.splitlines():
124 components = line.split()
125 if components[0] == '/dev/root' and components[1] == '/':
126 return 'rw' in components[3].split(',')
127 else:
128 raise Exception('Internal error - rootfs mount not found!')
129
130 def _CheckUiJobStarted(self):
131 # status output is in the format:
132 # <job_name> <status> ['process' <pid>].
133 # <status> is in the format <goal>/<state>.
134 result = self.host.RemoteSh('status ui')
135 return result.output.split()[1].split('/')[0] == 'start'
136
137 def _KillProcsIfNeeded(self):
138 if self._CheckUiJobStarted():
139 logging.info('Shutting down Chrome.')
140 self.start_ui_needed = True
141 self.host.RemoteSh('stop ui')
142
143 # Developers sometimes run session_manager manually, in which case we'll
144 # need to help shut the chrome processes down.
145 try:
146 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
147 while self._ChromeFileInUse():
148 logging.warning('The chrome binary on the device is in use.')
149 logging.warning('Killing chrome and session_manager processes...\n')
150
151 self.host.RemoteSh("pkill 'chrome|session_manager'",
152 error_code_ok=True)
153 # Wait for processes to actually terminate
154 time.sleep(POST_KILL_WAIT)
155 logging.info('Rechecking the chrome binary...')
156 except cros_build_lib.TimeoutError:
157 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
158 'exit any running chrome processes and try again.')
159
160 def _PrepareTarget(self):
161 # Mount root partition as read/write
162 if not self._CheckRootfsWriteable():
163 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700164 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700165 if result.returncode:
166 self._DisableRootfsVerification()
167 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700168 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700169
170 if not self._CheckRootfsWriteable():
171 cros_build_lib.Die('Root partition still read-only')
172
173 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
174 # makes sense to have even when going the sshfs route.
175 self._KillProcsIfNeeded()
176
177 def _Deploy(self):
178 logging.info('Copying Chrome to device.')
179 # Show the output (status) for this command.
Ryan Cuia56a71e2012-10-18 18:40:35 -0700180 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir), '/',
181 inplace=True, debug_level=logging.INFO)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700182 if self.start_ui_needed:
183 self.host.RemoteSh('start ui')
184
185 def Perform(self):
186 try:
187 logging.info('Testing connection to the device.')
188 self.host.RemoteSh('true')
189 except cros_build_lib.RunCommandError:
190 logging.error('Error connecting to the test device.')
191 raise
192
Ryan Cui3045c5d2012-07-13 18:00:33 -0700193 self._PrepareTarget()
194 self._Deploy()
195
196
Ryan Cuia56a71e2012-10-18 18:40:35 -0700197def ValidateGypDefines(_option, _opt, value):
198 """Convert GYP_DEFINES-formatted string to dictionary."""
199 return chrome_util.ProcessGypDefines(value)
200
201
202class CustomOption(commandline.Option):
203 """Subclass Option class to implement path evaluation."""
204 TYPES = commandline.Option.TYPES + ('gyp_defines',)
205 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
206 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
207
208
Ryan Cuie535b172012-10-19 18:25:03 -0700209def _CreateParser():
210 """Create our custom parser."""
Ryan Cuia56a71e2012-10-18 18:40:35 -0700211 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700212
Ryan Cuia56a71e2012-10-18 18:40:35 -0700213 # TODO(rcui): Have this use the UI-V2 format of having source and target
214 # device be specified as positional arguments.
Ryan Cui3045c5d2012-07-13 18:00:33 -0700215 parser.add_option('--force', action='store_true', default=False,
216 help=('Skip all prompts (i.e., for disabling of rootfs '
217 'verification). This may result in the target '
218 'machine being rebooted.'))
Ryan Cuia56a71e2012-10-18 18:40:35 -0700219 parser.add_option('--build-dir', type='path',
220 help=('The directory with Chrome build artifacts to deploy '
221 'from. Typically of format <chrome_root>/out/Debug. '
222 'When this option is used, the GYP_DEFINES '
223 'environment variable must be set.'))
Ryan Cui3045c5d2012-07-13 18:00:33 -0700224 parser.add_option('-g', '--gs-path', type='gs_path',
225 help=('GS path that contains the chrome to deploy.'))
Ryan Cui3045c5d2012-07-13 18:00:33 -0700226 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
227 help=('Port of the target device to connect to.'))
228 parser.add_option('-t', '--to',
229 help=('The IP address of the CrOS device to deploy to.'))
230 parser.add_option('-v', '--verbose', action='store_true', default=False,
231 help=('Show more debug output.'))
Ryan Cuia56a71e2012-10-18 18:40:35 -0700232
233 group = optparse.OptionGroup(parser, 'Advanced Options')
234 group.add_option('-l', '--local-pkg-path', type='path',
235 help='path to local chrome prebuilt package to deploy.')
236 group.add_option('--staging-flags', default={}, type='gyp_defines',
237 help=('Extra flags to control staging. Valid flags '
238 'are - %s' % ', '.join(chrome_util.STAGING_FLAGS)))
239 parser.add_option_group(group)
240
241 # Path of an empty directory to stage chrome artifacts to. Defaults to a
242 # temporary directory that is removed when the script finishes. If the path
243 # is specified, then it will not be removed.
244 parser.add_option('--staging-dir', type='path', default=None,
245 help=optparse.SUPPRESS_HELP)
246 # Only prepare the staging directory, and skip deploying to the device.
247 parser.add_option('--staging-only', action='store_true', default=False,
248 help=optparse.SUPPRESS_HELP)
249 # GYP_DEFINES that Chrome was built with. Influences which files are staged
250 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
251 # enviroment variable.
252 parser.add_option('--gyp-defines', default={}, type='gyp_defines',
253 help=optparse.SUPPRESS_HELP)
Ryan Cuie535b172012-10-19 18:25:03 -0700254 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700255
Ryan Cuie535b172012-10-19 18:25:03 -0700256
257def _ParseCommandLine(argv):
258 """Parse args, and run environment-independent checks."""
259 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700260 (options, args) = parser.parse_args(argv)
261
Ryan Cuia56a71e2012-10-18 18:40:35 -0700262 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
263 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
264 '--build_dir')
265 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
266 parser.error('Cannot specify both --build_dir and '
267 '--gs-path/--local-pkg-patch')
268 if options.gs_path and options.local_pkg_path:
269 parser.error('Cannot specify both --gs-path and --local-pkg-path')
270 if not (options.staging_only or options.to):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700271 parser.error('Need to specify --to')
272
273 return options, args
274
275
Ryan Cuie535b172012-10-19 18:25:03 -0700276def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700277 """Perform some usage validation (after we've parsed the arguments
278
279 Args:
280 options/args: The options/args object returned by optparse
281 """
Ryan Cuia56a71e2012-10-18 18:40:35 -0700282 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
283 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
284
285 if options.build_dir and not options.gyp_defines:
286 gyp_env = os.getenv('GYP_DEFINES', None)
287 if gyp_env is not None:
288 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
289 logging.info('GYP_DEFINES taken from environment: %s',
290 options.gyp_defines)
291 else:
292 cros_build_lib.Die('When --build-dir is set, the GYP_DEFINES environment '
293 'variable must be set.')
294
295
296def _FetchChromePackage(tempdir, gs_path):
297 """Get the chrome prebuilt tarball from GS.
298
299 Returns: Path to the fetched chrome tarball.
300 """
Ryan Cui777ff422012-12-07 13:12:54 -0800301
302 gs_bin = gs.FetchGSUtil(tempdir)
303 os.path.join(tempdir, 'gsutil', 'gsutil')
304 ctx = gs.GSContext(gsutil_bin=gs_bin, init_boto=True)
305 files = ctx.LS(gs_path).output.splitlines()
Ryan Cuia56a71e2012-10-18 18:40:35 -0700306 files = [found for found in files if
David James629febb2012-11-25 13:07:34 -0800307 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
Ryan Cuia56a71e2012-10-18 18:40:35 -0700308 if not files:
309 raise Exception('No chrome package found at %s' % gs_path)
310 elif len(files) > 1:
311 # - Users should provide us with a direct link to either a stripped or
312 # unstripped chrome package.
313 # - In the case of being provided with an archive directory, where both
314 # stripped and unstripped chrome available, use the stripped chrome
315 # package.
316 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
317 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
318 files = [f for f in files if not 'unstripped' in f]
319 assert len(files) == 1
320 logging.warning('Multiple chrome packages found. Using %s', files[0])
321
322 filename = _UrlBaseName(files[0])
323 logging.info('Fetching %s.', filename)
Ryan Cui777ff422012-12-07 13:12:54 -0800324 ctx.Copy(files[0], tempdir, print_cmd=False)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700325 chrome_path = os.path.join(tempdir, filename)
326 assert os.path.exists(chrome_path)
327 return chrome_path
328
329
330def _PrepareStagingDir(options, tempdir, staging_dir):
331 """Place the necessary files in the staging directory.
332
333 The staging directory is the directory used to rsync the build artifacts over
334 to the device. Only the necessary Chrome build artifacts are put into the
335 staging directory.
336 """
337 if options.build_dir:
338 chrome_util.StageChromeFromBuildDir(
339 staging_dir, options.build_dir, options.gyp_defines,
340 options.staging_flags)
341 else:
342 pkg_path = options.local_pkg_path
343 if options.gs_path:
344 pkg_path = _FetchChromePackage(tempdir, options.gs_path)
345
346 assert pkg_path
347 logging.info('Extracting %s.', pkg_path)
348 _ExtractChrome(pkg_path, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700349
350
351def main(argv):
352 options, args = _ParseCommandLine(argv)
353 _PostParseCheck(options, args)
354
355 # Set cros_build_lib debug level to hide RunCommand spew.
356 if options.verbose:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700357 logging.getLogger().setLevel(logging.DEBUG)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700358 else:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700359 logging.getLogger().setLevel(logging.WARNING)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700360
David James891dccf2012-08-20 14:19:54 -0700361 with sudo.SudoKeepAlive(ttyless_sudo=False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700362 with osutils.TempDirContextManager(sudo_rm=True) as tempdir:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700363 staging_dir = options.staging_dir
364 if not staging_dir:
365 staging_dir = os.path.join(tempdir, 'chrome')
366 _PrepareStagingDir(options, tempdir, staging_dir)
367
368 if options.staging_only:
369 return 0
370
371 deploy = DeployChrome(options, tempdir, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700372 deploy.Perform()