blob: cedec2a0085117045c2ec17e3975ddc9d8473709 [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 -070040KERNEL_A_PARTITION = 2
41KERNEL_B_PARTITION = 4
42
43KILL_PROC_MAX_WAIT = 10
44POST_KILL_WAIT = 2
45
Ryan Cuie535b172012-10-19 18:25:03 -070046MOUNT_RW_COMMAND = 'mount -o remount,rw /'
Ryan Cui3045c5d2012-07-13 18:00:33 -070047
48# Convenience RunCommand methods
49DebugRunCommand = functools.partial(
50 cros_build_lib.RunCommand, debug_level=logging.DEBUG)
51
52DebugRunCommandCaptureOutput = functools.partial(
53 cros_build_lib.RunCommandCaptureOutput, debug_level=logging.DEBUG)
54
55DebugSudoRunCommand = functools.partial(
56 cros_build_lib.SudoRunCommand, debug_level=logging.DEBUG)
57
58
Ryan Cui3045c5d2012-07-13 18:00:33 -070059def _UrlBaseName(url):
60 """Return the last component of the URL."""
61 return url.rstrip('/').rpartition('/')[-1]
62
63
64def _ExtractChrome(src, dest):
65 osutils.SafeMakedirs(dest)
66 # Preserve permissions (-p). This is default when running tar with 'sudo'.
67 DebugSudoRunCommand(['tar', '--checkpoint', '-xf', src],
68 cwd=dest)
69
70
71class DeployChrome(object):
72 """Wraps the core deployment functionality."""
Ryan Cuia56a71e2012-10-18 18:40:35 -070073 def __init__(self, options, tempdir, staging_dir):
Ryan Cuie535b172012-10-19 18:25:03 -070074 """Initialize the class.
75
76 Arguments:
77 options: Optparse result structure.
78 tempdir: Scratch space for the class. Caller has responsibility to clean
79 it up.
Ryan Cuie535b172012-10-19 18:25:03 -070080 """
Ryan Cui3045c5d2012-07-13 18:00:33 -070081 self.tempdir = tempdir
82 self.options = options
Ryan Cuia56a71e2012-10-18 18:40:35 -070083 self.staging_dir = staging_dir
Ryan Cuiafd6c5c2012-07-30 17:48:22 -070084 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
Ryan Cui3045c5d2012-07-13 18:00:33 -070085 self.start_ui_needed = False
86
Ryan Cui3045c5d2012-07-13 18:00:33 -070087 def _ChromeFileInUse(self):
88 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
89 error_code_ok=True)
90 return result.returncode == 0
91
92 def _DisableRootfsVerification(self):
93 if not self.options.force:
94 logging.error('Detected that the device has rootfs verification enabled.')
95 logging.info('This script can automatically remove the rootfs '
96 'verification, which requires that it reboot the device.')
97 logging.info('Make sure the device is in developer mode!')
98 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -070099 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700100 cros_build_lib.Die('Need rootfs verification to be disabled. '
101 'Aborting.')
102
103 logging.info('Removing rootfs verification from %s', self.options.to)
104 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
105 # Use --force to bypass the checks.
106 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
107 '--remove_rootfs_verification --force')
108 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
109 self.host.RemoteSh(cmd % partition, error_code_ok=True)
110
111 # A reboot in developer mode takes a while (and has delays), so the user
112 # will have time to read and act on the USB boot instructions below.
113 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
114 self.host.RemoteReboot()
115
116 def _CheckRootfsWriteable(self):
117 # /proc/mounts is in the format:
118 # <device> <dir> <type> <options>
119 result = self.host.RemoteSh('cat /proc/mounts')
120 for line in result.output.splitlines():
121 components = line.split()
122 if components[0] == '/dev/root' and components[1] == '/':
123 return 'rw' in components[3].split(',')
124 else:
125 raise Exception('Internal error - rootfs mount not found!')
126
127 def _CheckUiJobStarted(self):
128 # status output is in the format:
129 # <job_name> <status> ['process' <pid>].
130 # <status> is in the format <goal>/<state>.
131 result = self.host.RemoteSh('status ui')
132 return result.output.split()[1].split('/')[0] == 'start'
133
134 def _KillProcsIfNeeded(self):
135 if self._CheckUiJobStarted():
136 logging.info('Shutting down Chrome.')
137 self.start_ui_needed = True
138 self.host.RemoteSh('stop ui')
139
140 # Developers sometimes run session_manager manually, in which case we'll
141 # need to help shut the chrome processes down.
142 try:
143 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
144 while self._ChromeFileInUse():
145 logging.warning('The chrome binary on the device is in use.')
146 logging.warning('Killing chrome and session_manager processes...\n')
147
148 self.host.RemoteSh("pkill 'chrome|session_manager'",
149 error_code_ok=True)
150 # Wait for processes to actually terminate
151 time.sleep(POST_KILL_WAIT)
152 logging.info('Rechecking the chrome binary...')
153 except cros_build_lib.TimeoutError:
154 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
155 'exit any running chrome processes and try again.')
156
157 def _PrepareTarget(self):
158 # Mount root partition as read/write
159 if not self._CheckRootfsWriteable():
160 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700161 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700162 if result.returncode:
163 self._DisableRootfsVerification()
164 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700165 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700166
167 if not self._CheckRootfsWriteable():
168 cros_build_lib.Die('Root partition still read-only')
169
170 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
171 # makes sense to have even when going the sshfs route.
172 self._KillProcsIfNeeded()
173
174 def _Deploy(self):
175 logging.info('Copying Chrome to device.')
176 # Show the output (status) for this command.
Ryan Cuia56a71e2012-10-18 18:40:35 -0700177 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir), '/',
Ryan Cui61ca1602013-01-04 15:55:56 -0800178 inplace=True, debug_level=logging.INFO, sudo=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700179 if self.start_ui_needed:
180 self.host.RemoteSh('start ui')
181
182 def Perform(self):
183 try:
184 logging.info('Testing connection to the device.')
185 self.host.RemoteSh('true')
186 except cros_build_lib.RunCommandError:
187 logging.error('Error connecting to the test device.')
188 raise
189
Ryan Cui3045c5d2012-07-13 18:00:33 -0700190 self._PrepareTarget()
191 self._Deploy()
192
193
Ryan Cuia56a71e2012-10-18 18:40:35 -0700194def ValidateGypDefines(_option, _opt, value):
195 """Convert GYP_DEFINES-formatted string to dictionary."""
196 return chrome_util.ProcessGypDefines(value)
197
198
199class CustomOption(commandline.Option):
200 """Subclass Option class to implement path evaluation."""
201 TYPES = commandline.Option.TYPES + ('gyp_defines',)
202 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
203 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
204
205
Ryan Cuie535b172012-10-19 18:25:03 -0700206def _CreateParser():
207 """Create our custom parser."""
Ryan Cui504db722013-01-22 11:48:01 -0800208 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
209 caching=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700210
Ryan Cuia56a71e2012-10-18 18:40:35 -0700211 # TODO(rcui): Have this use the UI-V2 format of having source and target
212 # device be specified as positional arguments.
Ryan Cui3045c5d2012-07-13 18:00:33 -0700213 parser.add_option('--force', action='store_true', default=False,
214 help=('Skip all prompts (i.e., for disabling of rootfs '
215 'verification). This may result in the target '
216 'machine being rebooted.'))
Ryan Cuia56a71e2012-10-18 18:40:35 -0700217 parser.add_option('--build-dir', type='path',
218 help=('The directory with Chrome build artifacts to deploy '
219 'from. Typically of format <chrome_root>/out/Debug. '
220 'When this option is used, the GYP_DEFINES '
221 'environment variable must be set.'))
Ryan Cui3045c5d2012-07-13 18:00:33 -0700222 parser.add_option('-g', '--gs-path', type='gs_path',
223 help=('GS path that contains the chrome to deploy.'))
Ryan Cui3045c5d2012-07-13 18:00:33 -0700224 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
225 help=('Port of the target device to connect to.'))
226 parser.add_option('-t', '--to',
227 help=('The IP address of the CrOS device to deploy to.'))
228 parser.add_option('-v', '--verbose', action='store_true', default=False,
229 help=('Show more debug output.'))
Ryan Cuia56a71e2012-10-18 18:40:35 -0700230
231 group = optparse.OptionGroup(parser, 'Advanced Options')
232 group.add_option('-l', '--local-pkg-path', type='path',
Ryan Cuief91e702013-02-04 12:06:36 -0800233 help='Path to local chrome prebuilt package to deploy.')
234 group.add_option('--strict', action='store_true', default=False,
235 help='Stage artifacts based on the GYP_DEFINES environment '
236 'variable and --staging-flags, if set.')
237 group.add_option('--staging-flags', default=None, type='gyp_defines',
238 help=('Requires --strict to be set. Extra flags to '
239 'control staging. Valid flags are - %s'
240 % ', '.join(chrome_util.STAGING_FLAGS)))
241
Ryan Cuia56a71e2012-10-18 18:40:35 -0700242 parser.add_option_group(group)
243
244 # Path of an empty directory to stage chrome artifacts to. Defaults to a
245 # temporary directory that is removed when the script finishes. If the path
246 # is specified, then it will not be removed.
247 parser.add_option('--staging-dir', type='path', default=None,
248 help=optparse.SUPPRESS_HELP)
249 # Only prepare the staging directory, and skip deploying to the device.
250 parser.add_option('--staging-only', action='store_true', default=False,
251 help=optparse.SUPPRESS_HELP)
252 # GYP_DEFINES that Chrome was built with. Influences which files are staged
253 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
254 # enviroment variable.
Ryan Cuief91e702013-02-04 12:06:36 -0800255 parser.add_option('--gyp-defines', default=None, type='gyp_defines',
Ryan Cuia56a71e2012-10-18 18:40:35 -0700256 help=optparse.SUPPRESS_HELP)
Ryan Cuie535b172012-10-19 18:25:03 -0700257 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700258
Ryan Cuie535b172012-10-19 18:25:03 -0700259
260def _ParseCommandLine(argv):
261 """Parse args, and run environment-independent checks."""
262 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700263 (options, args) = parser.parse_args(argv)
264
Ryan Cuia56a71e2012-10-18 18:40:35 -0700265 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
266 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
Ryan Cuief91e702013-02-04 12:06:36 -0800267 '--build-dir')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700268 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
269 parser.error('Cannot specify both --build_dir and '
270 '--gs-path/--local-pkg-patch')
271 if options.gs_path and options.local_pkg_path:
272 parser.error('Cannot specify both --gs-path and --local-pkg-path')
273 if not (options.staging_only or options.to):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700274 parser.error('Need to specify --to')
Ryan Cuief91e702013-02-04 12:06:36 -0800275 if (options.strict or options.staging_flags) and not options.build_dir:
276 parser.error('--strict and --staging-flags require --build-dir to be '
277 'set.')
278 if options.staging_flags and not options.strict:
279 parser.error('--strict requires --staging-flags to be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700280
281 return options, args
282
283
Ryan Cuie535b172012-10-19 18:25:03 -0700284def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700285 """Perform some usage validation (after we've parsed the arguments
286
287 Args:
288 options/args: The options/args object returned by optparse
289 """
Ryan Cuia56a71e2012-10-18 18:40:35 -0700290 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
291 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
292
Ryan Cuief91e702013-02-04 12:06:36 -0800293 if options.build_dir and options.strict and not options.gyp_defines:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700294 gyp_env = os.getenv('GYP_DEFINES', None)
295 if gyp_env is not None:
296 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
297 logging.info('GYP_DEFINES taken from environment: %s',
298 options.gyp_defines)
299 else:
Ryan Cuief91e702013-02-04 12:06:36 -0800300 cros_build_lib.Die('When --build-dir and --strict is set, the '
301 'GYP_DEFINES environment variable must be set.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700302
303
Ryan Cui504db722013-01-22 11:48:01 -0800304def _FetchChromePackage(cache_dir, tempdir, gs_path):
Ryan Cuia56a71e2012-10-18 18:40:35 -0700305 """Get the chrome prebuilt tarball from GS.
306
307 Returns: Path to the fetched chrome tarball.
308 """
Ryan Cui504db722013-01-22 11:48:01 -0800309 common_path = os.path.join(cache_dir, constants.COMMON_CACHE)
310 with gs.FetchGSUtil(common_path) as gs_bin:
311 ctx = gs.GSContext(gsutil_bin=gs_bin, init_boto=True)
312 files = ctx.LS(gs_path).output.splitlines()
313 files = [found for found in files if
314 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
315 if not files:
316 raise Exception('No chrome package found at %s' % gs_path)
317 elif len(files) > 1:
318 # - Users should provide us with a direct link to either a stripped or
319 # unstripped chrome package.
320 # - In the case of being provided with an archive directory, where both
321 # stripped and unstripped chrome available, use the stripped chrome
322 # package.
323 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
324 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
325 files = [f for f in files if not 'unstripped' in f]
326 assert len(files) == 1
327 logging.warning('Multiple chrome packages found. Using %s', files[0])
Ryan Cui777ff422012-12-07 13:12:54 -0800328
Ryan Cui504db722013-01-22 11:48:01 -0800329 filename = _UrlBaseName(files[0])
330 logging.info('Fetching %s.', filename)
331 ctx.Copy(files[0], tempdir, print_cmd=False)
332 chrome_path = os.path.join(tempdir, filename)
333 assert os.path.exists(chrome_path)
334 return chrome_path
Ryan Cuia56a71e2012-10-18 18:40:35 -0700335
336
337def _PrepareStagingDir(options, tempdir, staging_dir):
338 """Place the necessary files in the staging directory.
339
340 The staging directory is the directory used to rsync the build artifacts over
341 to the device. Only the necessary Chrome build artifacts are put into the
342 staging directory.
343 """
344 if options.build_dir:
345 chrome_util.StageChromeFromBuildDir(
346 staging_dir, options.build_dir, options.gyp_defines,
347 options.staging_flags)
348 else:
349 pkg_path = options.local_pkg_path
350 if options.gs_path:
Ryan Cui504db722013-01-22 11:48:01 -0800351 pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
352 options.gs_path)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700353
354 assert pkg_path
355 logging.info('Extracting %s.', pkg_path)
356 _ExtractChrome(pkg_path, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700357
358
359def main(argv):
360 options, args = _ParseCommandLine(argv)
361 _PostParseCheck(options, args)
362
363 # Set cros_build_lib debug level to hide RunCommand spew.
364 if options.verbose:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700365 logging.getLogger().setLevel(logging.DEBUG)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700366 else:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700367 logging.getLogger().setLevel(logging.WARNING)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700368
David James891dccf2012-08-20 14:19:54 -0700369 with sudo.SudoKeepAlive(ttyless_sudo=False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700370 with osutils.TempDirContextManager(sudo_rm=True) as tempdir:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700371 staging_dir = options.staging_dir
372 if not staging_dir:
373 staging_dir = os.path.join(tempdir, 'chrome')
374 _PrepareStagingDir(options, tempdir, staging_dir)
375
376 if options.staging_only:
377 return 0
378
379 deploy = DeployChrome(options, tempdir, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700380 deploy.Perform()