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