blob: 2c23f02001fd4e8b9c6b9e7c3bb10a245fd96a04 [file] [log] [blame]
#!/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()