| #!/usr/bin/python |
| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Script that resets your Chrome GIT checkout.""" |
| |
| import functools |
| import logging |
| import optparse |
| import os |
| import time |
| import urlparse |
| |
| from chromite.lib import cros_build_lib |
| from chromite.lib import osutils |
| from chromite.lib import remote_access as remote |
| from chromite.lib import sudo |
| |
| |
| GS_HTTP = 'https://commondatastorage.googleapis.com' |
| GSUTIL_URL = '%s/chromeos-public/gsutil.tar.gz' % GS_HTTP |
| GS_RETRIES = 5 |
| KERNEL_A_PARTITION = 2 |
| KERNEL_B_PARTITION = 4 |
| |
| KILL_PROC_MAX_WAIT = 10 |
| POST_KILL_WAIT = 2 |
| |
| |
| # Convenience RunCommand methods |
| DebugRunCommand = functools.partial( |
| cros_build_lib.RunCommand, debug_level=logging.DEBUG) |
| |
| DebugRunCommandCaptureOutput = functools.partial( |
| cros_build_lib.RunCommandCaptureOutput, debug_level=logging.DEBUG) |
| |
| DebugSudoRunCommand = functools.partial( |
| cros_build_lib.SudoRunCommand, debug_level=logging.DEBUG) |
| |
| |
| def _TestGSLs(gs_bin): |
| """Quick test of gsutil functionality.""" |
| result = DebugRunCommandCaptureOutput([gs_bin, 'ls'], error_code_ok=True) |
| return not result.returncode |
| |
| |
| def _SetupBotoConfig(gs_bin): |
| """Make sure we can access protected bits in GS.""" |
| boto_path = os.path.expanduser('~/.boto') |
| if os.path.isfile(boto_path) or _TestGSLs(gs_bin): |
| return |
| |
| logging.info('Configuring gsutil. Please use your @google.com account.') |
| try: |
| cros_build_lib.RunCommand([gs_bin, 'config'], print_cmd=False) |
| finally: |
| if os.path.exists(boto_path) and not os.path.getsize(boto_path): |
| os.remove(boto_path) |
| |
| |
| def _UrlBaseName(url): |
| """Return the last component of the URL.""" |
| return url.rstrip('/').rpartition('/')[-1] |
| |
| |
| def _ExtractChrome(src, dest): |
| osutils.SafeMakedirs(dest) |
| # Preserve permissions (-p). This is default when running tar with 'sudo'. |
| DebugSudoRunCommand(['tar', '--checkpoint', '-xf', src], |
| cwd=dest) |
| |
| |
| class DeployChrome(object): |
| """Wraps the core deployment functionality.""" |
| def __init__(self, options, tempdir): |
| self.tempdir = tempdir |
| self.options = options |
| self.chrome_dir = os.path.join(tempdir, 'chrome') |
| self.host = remote.RemoteAccess(options.to, tempdir, port=options.port) |
| self.start_ui_needed = False |
| |
| def _FetchChrome(self): |
| """Get the chrome prebuilt tarball from GS. |
| |
| Returns: Path to the fetched chrome tarball. |
| """ |
| logging.info('Fetching gsutil.') |
| gsutil_tar = os.path.join(self.tempdir, 'gsutil.tar.gz') |
| cros_build_lib.RunCurl([GSUTIL_URL, '-o', gsutil_tar], |
| debug_level=logging.DEBUG) |
| DebugRunCommand(['tar', '-xzf', gsutil_tar], cwd=self.tempdir) |
| gs_bin = os.path.join(self.tempdir, 'gsutil', 'gsutil') |
| _SetupBotoConfig(gs_bin) |
| cmd = [gs_bin, 'ls', self.options.gs_path] |
| files = DebugRunCommandCaptureOutput(cmd).output.splitlines() |
| files = [found for found in files if |
| _UrlBaseName(found).startswith('chromeos-chrome-')] |
| if not files: |
| raise Exception('No chrome package found at %s' % self.options.gs_path) |
| elif len(files) > 1: |
| # - Users should provide us with a direct link to either a stripped or |
| # unstripped chrome package. |
| # - In the case of being provided with an archive directory, where both |
| # stripped and unstripped chrome available, use the stripped chrome |
| # package (comes on top after sort). |
| # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz |
| # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz. |
| files.sort() |
| cros_build_lib.logger.warning('Multiple chrome packages found. Using %s', |
| files[0]) |
| |
| filename = _UrlBaseName(files[0]) |
| logging.info('Fetching %s.', filename) |
| cros_build_lib.RunCommand([gs_bin, 'cp', files[0], self.tempdir], |
| print_cmd=False) |
| chrome_path = os.path.join(self.tempdir, filename) |
| assert os.path.exists(chrome_path) |
| return chrome_path |
| |
| def _ChromeFileInUse(self): |
| result = self.host.RemoteSh('lsof /opt/google/chrome/chrome', |
| error_code_ok=True) |
| return result.returncode == 0 |
| |
| def _DisableRootfsVerification(self): |
| if not self.options.force: |
| logging.error('Detected that the device has rootfs verification enabled.') |
| logging.info('This script can automatically remove the rootfs ' |
| 'verification, which requires that it reboot the device.') |
| logging.info('Make sure the device is in developer mode!') |
| logging.info('Skip this prompt by specifying --force.') |
| result = cros_build_lib.YesNoPrompt( |
| 'no', prompt='Remove roots verification?') |
| if result == 'no': |
| cros_build_lib.Die('Need rootfs verification to be disabled. ' |
| 'Aborting.') |
| |
| logging.info('Removing rootfs verification from %s', self.options.to) |
| # Running in VM's cause make_dev_ssd's firmware sanity checks to fail. |
| # Use --force to bypass the checks. |
| cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d ' |
| '--remove_rootfs_verification --force') |
| for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION): |
| self.host.RemoteSh(cmd % partition, error_code_ok=True) |
| |
| # A reboot in developer mode takes a while (and has delays), so the user |
| # will have time to read and act on the USB boot instructions below. |
| logging.info('Please remember to press Ctrl-U if you are booting from USB.') |
| self.host.RemoteReboot() |
| |
| def _CheckRootfsWriteable(self): |
| # /proc/mounts is in the format: |
| # <device> <dir> <type> <options> |
| result = self.host.RemoteSh('cat /proc/mounts') |
| for line in result.output.splitlines(): |
| components = line.split() |
| if components[0] == '/dev/root' and components[1] == '/': |
| return 'rw' in components[3].split(',') |
| else: |
| raise Exception('Internal error - rootfs mount not found!') |
| |
| def _CheckUiJobStarted(self): |
| # status output is in the format: |
| # <job_name> <status> ['process' <pid>]. |
| # <status> is in the format <goal>/<state>. |
| result = self.host.RemoteSh('status ui') |
| return result.output.split()[1].split('/')[0] == 'start' |
| |
| def _KillProcsIfNeeded(self): |
| if self._CheckUiJobStarted(): |
| logging.info('Shutting down Chrome.') |
| self.start_ui_needed = True |
| self.host.RemoteSh('stop ui') |
| |
| # Developers sometimes run session_manager manually, in which case we'll |
| # need to help shut the chrome processes down. |
| try: |
| with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT): |
| while self._ChromeFileInUse(): |
| logging.warning('The chrome binary on the device is in use.') |
| logging.warning('Killing chrome and session_manager processes...\n') |
| |
| self.host.RemoteSh("pkill 'chrome|session_manager'", |
| error_code_ok=True) |
| # Wait for processes to actually terminate |
| time.sleep(POST_KILL_WAIT) |
| logging.info('Rechecking the chrome binary...') |
| except cros_build_lib.TimeoutError: |
| cros_build_lib.Die('Could not kill processes after %s seconds. Please ' |
| 'exit any running chrome processes and try again.') |
| |
| def _PrepareTarget(self): |
| # Mount root partition as read/write |
| if not self._CheckRootfsWriteable(): |
| logging.info('Mounting rootfs as writeable...') |
| result = self.host.RemoteSh('mount -o remount,rw /', error_code_ok=True) |
| if result.returncode: |
| self._DisableRootfsVerification() |
| logging.info('Trying again to mount rootfs as writeable...') |
| self.host.RemoteSh('mount -o remount,rw /') |
| |
| if not self._CheckRootfsWriteable(): |
| cros_build_lib.Die('Root partition still read-only') |
| |
| # This is needed because we're doing an 'rsync --inplace' of Chrome, but |
| # makes sense to have even when going the sshfs route. |
| self._KillProcsIfNeeded() |
| |
| def _Deploy(self): |
| logging.info('Copying Chrome to device.') |
| # Show the output (status) for this command. |
| self.host.Rsync('%s/' % os.path.abspath(self.chrome_dir), '/', inplace=True, |
| debug_level=logging.INFO) |
| if self.start_ui_needed: |
| self.host.RemoteSh('start ui') |
| |
| def Perform(self): |
| try: |
| logging.info('Testing connection to the device.') |
| self.host.RemoteSh('true') |
| except cros_build_lib.RunCommandError: |
| logging.error('Error connecting to the test device.') |
| raise |
| |
| pkg_path = self.options.local_path |
| if self.options.gs_path: |
| pkg_path = self._FetchChrome() |
| |
| logging.info('Extracting %s.', pkg_path) |
| _ExtractChrome(pkg_path, self.chrome_dir) |
| |
| self._PrepareTarget() |
| self._Deploy() |
| |
| |
| def check_gs_path(option, opt, value): |
| """Convert passed-in path to gs:// path.""" |
| parsed = urlparse.urlparse(value.rstrip('/ ')) |
| # pylint: disable=E1101 |
| path = parsed.path.lstrip('/') |
| if parsed.hostname.startswith('sandbox.google.com'): |
| # Sandbox paths are 'storage/<bucket>/<path_to_object>', so strip out the |
| # first component. |
| storage, _, path = path.partition('/') |
| assert storage == 'storage', 'GS URL %s not in expected format.' % value |
| |
| return 'gs://%s' % path |
| |
| |
| def check_path(option, opt, value): |
| """Expand the local path""" |
| return osutils.ExpandPath(value) |
| |
| |
| class CustomOption(optparse.Option): |
| """Subclass Option class to implement path evaluation.""" |
| TYPES = optparse.Option.TYPES + ('path', 'gs_path') |
| TYPE_CHECKER = optparse.Option.TYPE_CHECKER.copy() |
| TYPE_CHECKER['path'] = check_path |
| TYPE_CHECKER['gs_path'] = check_gs_path |
| |
| |
| def _ParseCommandLine(argv): |
| """Create the parser, parse args, and run environment-independent checks.""" |
| usage = 'usage: %prog [--] [command]' |
| parser = optparse.OptionParser(usage=usage, option_class=CustomOption) |
| |
| parser.add_option('--force', action='store_true', default=False, |
| help=('Skip all prompts (i.e., for disabling of rootfs ' |
| 'verification). This may result in the target ' |
| 'machine being rebooted.')) |
| parser.add_option('-g', '--gs-path', type='gs_path', |
| help=('GS path that contains the chrome to deploy.')) |
| parser.add_option('-l', '--local-path', type='path', |
| help='path to local chrome prebuilt package to deploy.') |
| parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT, |
| help=('Port of the target device to connect to.')) |
| parser.add_option('-t', '--to', |
| help=('The IP address of the CrOS device to deploy to.')) |
| parser.add_option('-v', '--verbose', action='store_true', default=False, |
| help=('Show more debug output.')) |
| |
| (options, args) = parser.parse_args(argv) |
| |
| if not options.gs_path and not options.local_path: |
| parser.error('Need to specify either --gs-path or --local-path') |
| if options.gs_path and options.local_path: |
| parser.error('Cannot specify both --gs-path and --local-path') |
| if not options.to: |
| parser.error('Need to specify --to') |
| |
| return options, args |
| |
| |
| def _PostParseCheck(options, args): |
| """Perform some usage validation (after we've parsed the arguments |
| |
| Args: |
| options/args: The options/args object returned by optparse |
| """ |
| if options.local_path and not os.path.isfile(options.local_path): |
| cros_build_lib.Die('%s is not a file.', options.local_path) |
| |
| |
| def main(argv): |
| options, args = _ParseCommandLine(argv) |
| _PostParseCheck(options, args) |
| |
| # Set cros_build_lib debug level to hide RunCommand spew. |
| if options.verbose: |
| cros_build_lib.logger.setLevel(logging.DEBUG) |
| else: |
| cros_build_lib.logger.setLevel(logging.INFO) |
| |
| with sudo.SudoKeepAlive(): |
| with osutils.TempDirContextManager(sudo_rm=True) as tempdir: |
| deploy = DeployChrome(options, tempdir) |
| deploy.Perform() |