blob: 2c23f02001fd4e8b9c6b9e7c3bb10a245fd96a04 [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
6"""Script that resets your Chrome GIT checkout."""
7
8import functools
9import logging
10import optparse
11import os
12import time
13import urlparse
14
15from chromite.lib import cros_build_lib
16from chromite.lib import osutils
17from chromite.lib import remote_access as remote
18from chromite.lib import sudo
19
20
21GS_HTTP = 'https://commondatastorage.googleapis.com'
22GSUTIL_URL = '%s/chromeos-public/gsutil.tar.gz' % GS_HTTP
23GS_RETRIES = 5
24KERNEL_A_PARTITION = 2
25KERNEL_B_PARTITION = 4
26
27KILL_PROC_MAX_WAIT = 10
28POST_KILL_WAIT = 2
29
30
31# Convenience RunCommand methods
32DebugRunCommand = functools.partial(
33 cros_build_lib.RunCommand, debug_level=logging.DEBUG)
34
35DebugRunCommandCaptureOutput = functools.partial(
36 cros_build_lib.RunCommandCaptureOutput, debug_level=logging.DEBUG)
37
38DebugSudoRunCommand = functools.partial(
39 cros_build_lib.SudoRunCommand, debug_level=logging.DEBUG)
40
41
42def _TestGSLs(gs_bin):
43 """Quick test of gsutil functionality."""
44 result = DebugRunCommandCaptureOutput([gs_bin, 'ls'], error_code_ok=True)
45 return not result.returncode
46
47
48def _SetupBotoConfig(gs_bin):
49 """Make sure we can access protected bits in GS."""
50 boto_path = os.path.expanduser('~/.boto')
51 if os.path.isfile(boto_path) or _TestGSLs(gs_bin):
52 return
53
54 logging.info('Configuring gsutil. Please use your @google.com account.')
55 try:
56 cros_build_lib.RunCommand([gs_bin, 'config'], print_cmd=False)
57 finally:
58 if os.path.exists(boto_path) and not os.path.getsize(boto_path):
59 os.remove(boto_path)
60
61
62def _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."""
76 def __init__(self, options, tempdir):
77 self.tempdir = tempdir
78 self.options = options
79 self.chrome_dir = os.path.join(tempdir, 'chrome')
80 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
81 self.start_ui_needed = False
82
83 def _FetchChrome(self):
84 """Get the chrome prebuilt tarball from GS.
85
86 Returns: Path to the fetched chrome tarball.
87 """
88 logging.info('Fetching gsutil.')
89 gsutil_tar = os.path.join(self.tempdir, 'gsutil.tar.gz')
90 cros_build_lib.RunCurl([GSUTIL_URL, '-o', gsutil_tar],
91 debug_level=logging.DEBUG)
92 DebugRunCommand(['tar', '-xzf', gsutil_tar], cwd=self.tempdir)
93 gs_bin = os.path.join(self.tempdir, 'gsutil', 'gsutil')
94 _SetupBotoConfig(gs_bin)
95 cmd = [gs_bin, 'ls', self.options.gs_path]
96 files = DebugRunCommandCaptureOutput(cmd).output.splitlines()
97 files = [found for found in files if
98 _UrlBaseName(found).startswith('chromeos-chrome-')]
99 if not files:
100 raise Exception('No chrome package found at %s' % self.options.gs_path)
101 elif len(files) > 1:
102 # - Users should provide us with a direct link to either a stripped or
103 # unstripped chrome package.
104 # - In the case of being provided with an archive directory, where both
105 # stripped and unstripped chrome available, use the stripped chrome
106 # package (comes on top after sort).
107 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
108 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
109 files.sort()
110 cros_build_lib.logger.warning('Multiple chrome packages found. Using %s',
111 files[0])
112
113 filename = _UrlBaseName(files[0])
114 logging.info('Fetching %s.', filename)
115 cros_build_lib.RunCommand([gs_bin, 'cp', files[0], self.tempdir],
116 print_cmd=False)
117 chrome_path = os.path.join(self.tempdir, filename)
118 assert os.path.exists(chrome_path)
119 return chrome_path
120
121 def _ChromeFileInUse(self):
122 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
123 error_code_ok=True)
124 return result.returncode == 0
125
126 def _DisableRootfsVerification(self):
127 if not self.options.force:
128 logging.error('Detected that the device has rootfs verification enabled.')
129 logging.info('This script can automatically remove the rootfs '
130 'verification, which requires that it reboot the device.')
131 logging.info('Make sure the device is in developer mode!')
132 logging.info('Skip this prompt by specifying --force.')
133 result = cros_build_lib.YesNoPrompt(
134 'no', prompt='Remove roots verification?')
135 if result == 'no':
136 cros_build_lib.Die('Need rootfs verification to be disabled. '
137 'Aborting.')
138
139 logging.info('Removing rootfs verification from %s', self.options.to)
140 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
141 # Use --force to bypass the checks.
142 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
143 '--remove_rootfs_verification --force')
144 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
145 self.host.RemoteSh(cmd % partition, error_code_ok=True)
146
147 # A reboot in developer mode takes a while (and has delays), so the user
148 # will have time to read and act on the USB boot instructions below.
149 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
150 self.host.RemoteReboot()
151
152 def _CheckRootfsWriteable(self):
153 # /proc/mounts is in the format:
154 # <device> <dir> <type> <options>
155 result = self.host.RemoteSh('cat /proc/mounts')
156 for line in result.output.splitlines():
157 components = line.split()
158 if components[0] == '/dev/root' and components[1] == '/':
159 return 'rw' in components[3].split(',')
160 else:
161 raise Exception('Internal error - rootfs mount not found!')
162
163 def _CheckUiJobStarted(self):
164 # status output is in the format:
165 # <job_name> <status> ['process' <pid>].
166 # <status> is in the format <goal>/<state>.
167 result = self.host.RemoteSh('status ui')
168 return result.output.split()[1].split('/')[0] == 'start'
169
170 def _KillProcsIfNeeded(self):
171 if self._CheckUiJobStarted():
172 logging.info('Shutting down Chrome.')
173 self.start_ui_needed = True
174 self.host.RemoteSh('stop ui')
175
176 # Developers sometimes run session_manager manually, in which case we'll
177 # need to help shut the chrome processes down.
178 try:
179 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
180 while self._ChromeFileInUse():
181 logging.warning('The chrome binary on the device is in use.')
182 logging.warning('Killing chrome and session_manager processes...\n')
183
184 self.host.RemoteSh("pkill 'chrome|session_manager'",
185 error_code_ok=True)
186 # Wait for processes to actually terminate
187 time.sleep(POST_KILL_WAIT)
188 logging.info('Rechecking the chrome binary...')
189 except cros_build_lib.TimeoutError:
190 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
191 'exit any running chrome processes and try again.')
192
193 def _PrepareTarget(self):
194 # Mount root partition as read/write
195 if not self._CheckRootfsWriteable():
196 logging.info('Mounting rootfs as writeable...')
197 result = self.host.RemoteSh('mount -o remount,rw /', error_code_ok=True)
198 if result.returncode:
199 self._DisableRootfsVerification()
200 logging.info('Trying again to mount rootfs as writeable...')
201 self.host.RemoteSh('mount -o remount,rw /')
202
203 if not self._CheckRootfsWriteable():
204 cros_build_lib.Die('Root partition still read-only')
205
206 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
207 # makes sense to have even when going the sshfs route.
208 self._KillProcsIfNeeded()
209
210 def _Deploy(self):
211 logging.info('Copying Chrome to device.')
212 # Show the output (status) for this command.
213 self.host.Rsync('%s/' % os.path.abspath(self.chrome_dir), '/', inplace=True,
214 debug_level=logging.INFO)
215 if self.start_ui_needed:
216 self.host.RemoteSh('start ui')
217
218 def Perform(self):
219 try:
220 logging.info('Testing connection to the device.')
221 self.host.RemoteSh('true')
222 except cros_build_lib.RunCommandError:
223 logging.error('Error connecting to the test device.')
224 raise
225
226 pkg_path = self.options.local_path
227 if self.options.gs_path:
228 pkg_path = self._FetchChrome()
229
230 logging.info('Extracting %s.', pkg_path)
231 _ExtractChrome(pkg_path, self.chrome_dir)
232
233 self._PrepareTarget()
234 self._Deploy()
235
236
237def check_gs_path(option, opt, value):
238 """Convert passed-in path to gs:// path."""
239 parsed = urlparse.urlparse(value.rstrip('/ '))
240 # pylint: disable=E1101
241 path = parsed.path.lstrip('/')
242 if parsed.hostname.startswith('sandbox.google.com'):
243 # Sandbox paths are 'storage/<bucket>/<path_to_object>', so strip out the
244 # first component.
245 storage, _, path = path.partition('/')
246 assert storage == 'storage', 'GS URL %s not in expected format.' % value
247
248 return 'gs://%s' % path
249
250
251def check_path(option, opt, value):
252 """Expand the local path"""
253 return osutils.ExpandPath(value)
254
255
256class CustomOption(optparse.Option):
257 """Subclass Option class to implement path evaluation."""
258 TYPES = optparse.Option.TYPES + ('path', 'gs_path')
259 TYPE_CHECKER = optparse.Option.TYPE_CHECKER.copy()
260 TYPE_CHECKER['path'] = check_path
261 TYPE_CHECKER['gs_path'] = check_gs_path
262
263
264def _ParseCommandLine(argv):
265 """Create the parser, parse args, and run environment-independent checks."""
266 usage = 'usage: %prog [--] [command]'
267 parser = optparse.OptionParser(usage=usage, option_class=CustomOption)
268
269 parser.add_option('--force', action='store_true', default=False,
270 help=('Skip all prompts (i.e., for disabling of rootfs '
271 'verification). This may result in the target '
272 'machine being rebooted.'))
273 parser.add_option('-g', '--gs-path', type='gs_path',
274 help=('GS path that contains the chrome to deploy.'))
275 parser.add_option('-l', '--local-path', type='path',
276 help='path to local chrome prebuilt package to deploy.')
277 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
278 help=('Port of the target device to connect to.'))
279 parser.add_option('-t', '--to',
280 help=('The IP address of the CrOS device to deploy to.'))
281 parser.add_option('-v', '--verbose', action='store_true', default=False,
282 help=('Show more debug output.'))
283
284 (options, args) = parser.parse_args(argv)
285
286 if not options.gs_path and not options.local_path:
287 parser.error('Need to specify either --gs-path or --local-path')
288 if options.gs_path and options.local_path:
289 parser.error('Cannot specify both --gs-path and --local-path')
290 if not options.to:
291 parser.error('Need to specify --to')
292
293 return options, args
294
295
296def _PostParseCheck(options, args):
297 """Perform some usage validation (after we've parsed the arguments
298
299 Args:
300 options/args: The options/args object returned by optparse
301 """
302 if options.local_path and not os.path.isfile(options.local_path):
303 cros_build_lib.Die('%s is not a file.', options.local_path)
304
305
306def main(argv):
307 options, args = _ParseCommandLine(argv)
308 _PostParseCheck(options, args)
309
310 # Set cros_build_lib debug level to hide RunCommand spew.
311 if options.verbose:
312 cros_build_lib.logger.setLevel(logging.DEBUG)
313 else:
314 cros_build_lib.logger.setLevel(logging.INFO)
315
316 with sudo.SudoKeepAlive():
317 with osutils.TempDirContextManager(sudo_rm=True) as tempdir:
318 deploy = DeployChrome(options, tempdir)
319 deploy.Perform()