blob: a9e78bbe5336bf23e2a1e90d341224208a5d78e3 [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
Ryan Cui3045c5d2012-07-13 18:00:33 -070010import os
11import time
Ryan Cui3045c5d2012-07-13 18:00:33 -070012
13from chromite.lib import cros_build_lib
Ryan Cuie535b172012-10-19 18:25:03 -070014from chromite.lib import commandline
Ryan Cui3045c5d2012-07-13 18:00:33 -070015from chromite.lib import osutils
16from chromite.lib import remote_access as remote
17from chromite.lib import sudo
18
19
20GS_HTTP = 'https://commondatastorage.googleapis.com'
21GSUTIL_URL = '%s/chromeos-public/gsutil.tar.gz' % GS_HTTP
22GS_RETRIES = 5
23KERNEL_A_PARTITION = 2
24KERNEL_B_PARTITION = 4
25
26KILL_PROC_MAX_WAIT = 10
27POST_KILL_WAIT = 2
28
Ryan Cuie535b172012-10-19 18:25:03 -070029MOUNT_RW_COMMAND = 'mount -o remount,rw /'
Ryan Cui3045c5d2012-07-13 18:00:33 -070030
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."""
Ryan Cuiafd6c5c2012-07-30 17:48:22 -070076 def __init__(self, options, tempdir):
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
86 self.chrome_dir = os.path.join(tempdir, 'chrome')
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
90 def _FetchChrome(self):
91 """Get the chrome prebuilt tarball from GS.
92
93 Returns: Path to the fetched chrome tarball.
94 """
95 logging.info('Fetching gsutil.')
96 gsutil_tar = os.path.join(self.tempdir, 'gsutil.tar.gz')
97 cros_build_lib.RunCurl([GSUTIL_URL, '-o', gsutil_tar],
98 debug_level=logging.DEBUG)
99 DebugRunCommand(['tar', '-xzf', gsutil_tar], cwd=self.tempdir)
100 gs_bin = os.path.join(self.tempdir, 'gsutil', 'gsutil')
101 _SetupBotoConfig(gs_bin)
102 cmd = [gs_bin, 'ls', self.options.gs_path]
103 files = DebugRunCommandCaptureOutput(cmd).output.splitlines()
104 files = [found for found in files if
105 _UrlBaseName(found).startswith('chromeos-chrome-')]
106 if not files:
107 raise Exception('No chrome package found at %s' % self.options.gs_path)
108 elif len(files) > 1:
109 # - Users should provide us with a direct link to either a stripped or
110 # unstripped chrome package.
111 # - In the case of being provided with an archive directory, where both
112 # stripped and unstripped chrome available, use the stripped chrome
113 # package (comes on top after sort).
114 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
115 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
116 files.sort()
117 cros_build_lib.logger.warning('Multiple chrome packages found. Using %s',
118 files[0])
119
120 filename = _UrlBaseName(files[0])
121 logging.info('Fetching %s.', filename)
122 cros_build_lib.RunCommand([gs_bin, 'cp', files[0], self.tempdir],
123 print_cmd=False)
124 chrome_path = os.path.join(self.tempdir, filename)
125 assert os.path.exists(chrome_path)
126 return chrome_path
127
128 def _ChromeFileInUse(self):
129 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
130 error_code_ok=True)
131 return result.returncode == 0
132
133 def _DisableRootfsVerification(self):
134 if not self.options.force:
135 logging.error('Detected that the device has rootfs verification enabled.')
136 logging.info('This script can automatically remove the rootfs '
137 'verification, which requires that it reboot the device.')
138 logging.info('Make sure the device is in developer mode!')
139 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -0700140 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700141 cros_build_lib.Die('Need rootfs verification to be disabled. '
142 'Aborting.')
143
144 logging.info('Removing rootfs verification from %s', self.options.to)
145 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
146 # Use --force to bypass the checks.
147 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
148 '--remove_rootfs_verification --force')
149 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
150 self.host.RemoteSh(cmd % partition, error_code_ok=True)
151
152 # A reboot in developer mode takes a while (and has delays), so the user
153 # will have time to read and act on the USB boot instructions below.
154 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
155 self.host.RemoteReboot()
156
157 def _CheckRootfsWriteable(self):
158 # /proc/mounts is in the format:
159 # <device> <dir> <type> <options>
160 result = self.host.RemoteSh('cat /proc/mounts')
161 for line in result.output.splitlines():
162 components = line.split()
163 if components[0] == '/dev/root' and components[1] == '/':
164 return 'rw' in components[3].split(',')
165 else:
166 raise Exception('Internal error - rootfs mount not found!')
167
168 def _CheckUiJobStarted(self):
169 # status output is in the format:
170 # <job_name> <status> ['process' <pid>].
171 # <status> is in the format <goal>/<state>.
172 result = self.host.RemoteSh('status ui')
173 return result.output.split()[1].split('/')[0] == 'start'
174
175 def _KillProcsIfNeeded(self):
176 if self._CheckUiJobStarted():
177 logging.info('Shutting down Chrome.')
178 self.start_ui_needed = True
179 self.host.RemoteSh('stop ui')
180
181 # Developers sometimes run session_manager manually, in which case we'll
182 # need to help shut the chrome processes down.
183 try:
184 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
185 while self._ChromeFileInUse():
186 logging.warning('The chrome binary on the device is in use.')
187 logging.warning('Killing chrome and session_manager processes...\n')
188
189 self.host.RemoteSh("pkill 'chrome|session_manager'",
190 error_code_ok=True)
191 # Wait for processes to actually terminate
192 time.sleep(POST_KILL_WAIT)
193 logging.info('Rechecking the chrome binary...')
194 except cros_build_lib.TimeoutError:
195 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
196 'exit any running chrome processes and try again.')
197
198 def _PrepareTarget(self):
199 # Mount root partition as read/write
200 if not self._CheckRootfsWriteable():
201 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700202 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700203 if result.returncode:
204 self._DisableRootfsVerification()
205 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700206 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700207
208 if not self._CheckRootfsWriteable():
209 cros_build_lib.Die('Root partition still read-only')
210
211 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
212 # makes sense to have even when going the sshfs route.
213 self._KillProcsIfNeeded()
214
215 def _Deploy(self):
216 logging.info('Copying Chrome to device.')
217 # Show the output (status) for this command.
218 self.host.Rsync('%s/' % os.path.abspath(self.chrome_dir), '/', inplace=True,
219 debug_level=logging.INFO)
220 if self.start_ui_needed:
221 self.host.RemoteSh('start ui')
222
223 def Perform(self):
224 try:
225 logging.info('Testing connection to the device.')
226 self.host.RemoteSh('true')
227 except cros_build_lib.RunCommandError:
228 logging.error('Error connecting to the test device.')
229 raise
230
231 pkg_path = self.options.local_path
232 if self.options.gs_path:
233 pkg_path = self._FetchChrome()
234
235 logging.info('Extracting %s.', pkg_path)
236 _ExtractChrome(pkg_path, self.chrome_dir)
237
238 self._PrepareTarget()
239 self._Deploy()
240
241
Ryan Cuie535b172012-10-19 18:25:03 -0700242def _CreateParser():
243 """Create our custom parser."""
244 usage = 'usage: %prog [--]'
245 parser = commandline.OptionParser(usage=usage,)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700246
247 parser.add_option('--force', action='store_true', default=False,
248 help=('Skip all prompts (i.e., for disabling of rootfs '
249 'verification). This may result in the target '
250 'machine being rebooted.'))
251 parser.add_option('-g', '--gs-path', type='gs_path',
252 help=('GS path that contains the chrome to deploy.'))
253 parser.add_option('-l', '--local-path', type='path',
254 help='path to local chrome prebuilt package to deploy.')
255 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
256 help=('Port of the target device to connect to.'))
257 parser.add_option('-t', '--to',
258 help=('The IP address of the CrOS device to deploy to.'))
259 parser.add_option('-v', '--verbose', action='store_true', default=False,
260 help=('Show more debug output.'))
Ryan Cuie535b172012-10-19 18:25:03 -0700261 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700262
Ryan Cuie535b172012-10-19 18:25:03 -0700263
264def _ParseCommandLine(argv):
265 """Parse args, and run environment-independent checks."""
266 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700267 (options, args) = parser.parse_args(argv)
268
269 if not options.gs_path and not options.local_path:
270 parser.error('Need to specify either --gs-path or --local-path')
271 if options.gs_path and options.local_path:
272 parser.error('Cannot specify both --gs-path and --local-path')
273 if not options.to:
274 parser.error('Need to specify --to')
275
276 return options, args
277
278
Ryan Cuie535b172012-10-19 18:25:03 -0700279def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700280 """Perform some usage validation (after we've parsed the arguments
281
282 Args:
283 options/args: The options/args object returned by optparse
284 """
285 if options.local_path and not os.path.isfile(options.local_path):
286 cros_build_lib.Die('%s is not a file.', options.local_path)
287
288
289def main(argv):
290 options, args = _ParseCommandLine(argv)
291 _PostParseCheck(options, args)
292
293 # Set cros_build_lib debug level to hide RunCommand spew.
294 if options.verbose:
295 cros_build_lib.logger.setLevel(logging.DEBUG)
296 else:
297 cros_build_lib.logger.setLevel(logging.INFO)
298
David James891dccf2012-08-20 14:19:54 -0700299 with sudo.SudoKeepAlive(ttyless_sudo=False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700300 with osutils.TempDirContextManager(sudo_rm=True) as tempdir:
301 deploy = DeployChrome(options, tempdir)
302 deploy.Perform()