Autotest to download and flash Android image to run Android APCT and other tests
This test is a part of Android infra on ChromeOS to run APCT and other tests
during CQ and PFQ stages.
TEST=ran locally
BUG=b:26895109
Change-Id: I047d4c193e42233ae05c7ac29ab5072d999aa05b
Reviewed-on: https://chromium-review.googlesource.com/469286
Commit-Ready: Rohit Makasana <rohitbm@chromium.org>
Tested-by: Rohit Makasana <rohitbm@chromium.org>
Reviewed-by: Rohit Makasana <rohitbm@chromium.org>
diff --git a/server/site_tests/provision_CheetsUpdate/control b/server/site_tests/provision_CheetsUpdate/control
new file mode 100644
index 0000000..3948c68
--- /dev/null
+++ b/server/site_tests/provision_CheetsUpdate/control
@@ -0,0 +1,41 @@
+# Copyright 2017 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.
+
+AUTHOR = 'ARC Team'
+NAME = 'provision_CheetsUpdate'
+ATTRIBUTES = ''
+DEPENDENCIES = 'arc'
+TEST_TYPE = 'server'
+TIME = 'LENGTHY'
+
+DOC = """This test downloads and installs an Android test image on the DUT to
+prepare the DUT for Android PFQ tests.
+
+This test expects android build full name as a local env variable |value| or
+part of the test args.
+e.g.
+--args='value=git_mnc-dr-arc-dev/cheets_arm-user/P3836840'
+--args='value=git_mnc-dr-arc-dev/cheets_x86-user/P3836840'
+
+This test expects test servers are equipped with ssh keys to talk to lab DUTs
+without entering password while copying Android test image on DUTs.
+"""
+
+from autotest_lib.client.common_lib import error, utils
+
+# Autoserv may inject a local variable called value to supply the desired
+# version. If it does not exist, check if it was supplied as a test arg.
+
+if not locals().get('value'):
+ args = utils.args_to_dict(args)
+ if not args.get('value'):
+ raise error.TestError('No provision value!')
+ value = args['value']
+
+
+def run(machine):
+ host = hosts.create_host(machine)
+ job.run_test('provision_CheetsUpdate', host=host, value=value)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/provision_CheetsUpdate/lib/__init__.py b/server/site_tests/provision_CheetsUpdate/lib/__init__.py
new file mode 100644
index 0000000..b7f9f95
--- /dev/null
+++ b/server/site_tests/provision_CheetsUpdate/lib/__init__.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2016 The Android Open-Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""" This space intentionally left blank. """
+
+import sys
+if sys.version_info < (3, 0):
+ sys.stdout.write("Sorry, requires Python 3.x\n")
+ sys.exit(1)
diff --git a/server/site_tests/provision_CheetsUpdate/lib/util.py b/server/site_tests/provision_CheetsUpdate/lib/util.py
new file mode 100644
index 0000000..64c2be4
--- /dev/null
+++ b/server/site_tests/provision_CheetsUpdate/lib/util.py
@@ -0,0 +1,88 @@
+# Copyright (C) 2016 The Android Open-Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Various utility functions"""
+
+import logging
+import os
+import shlex
+import subprocess
+
+
+# The default location in which symbols and minidumps will be saved.
+_DEFAULT_ARTIFACT_CACHE_ROOT = os.environ.get('ARC_ARTIFACT_CACHE_ROOT',
+ '/tmp/arc-artifact-cache')
+
+
+def get_command_str(command):
+ """Returns a quoted version of the command, friendly to copy/paste."""
+ return ' '.join(shlex.quote(arg) for arg in command)
+
+
+def check_call(*subprocess_args, dryrun=False, **kwargs):
+ """Runs a subprocess and returns its exit code."""
+ if logging.getLogger().isEnabledFor(logging.DEBUG):
+ logging.debug('Calling: %s', get_command_str(subprocess_args))
+ if dryrun:
+ return
+ try:
+ return subprocess.check_call(subprocess_args, **kwargs)
+ except subprocess.CalledProcessError as e:
+ logging.error('Error while executing %s', get_command_str(subprocess_args))
+ logging.error(e.output)
+ raise
+
+
+def check_output(*subprocess_args, dryrun=False, **kwargs):
+ """Runs a subprocess and returns its output."""
+ if logging.getLogger().isEnabledFor(logging.DEBUG):
+ logging.debug('Calling: %s', get_command_str(subprocess_args))
+ if dryrun:
+ logging.info('Cannot return any output without running the command. '
+ 'Returning an empty string instead.')
+ return ''
+ try:
+ return subprocess.check_output(subprocess_args, universal_newlines=True,
+ **kwargs)
+ except subprocess.CalledProcessError as e:
+ logging.error('Error while executing %s', get_command_str(subprocess_args))
+ logging.error(e.output)
+ raise
+
+
+def makedirs(path):
+ """Makes directories if necessary, like 'mkdir -p'"""
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+
+def get_prebuilt(tool):
+ """Locates a prebuilt file to run."""
+ return os.path.abspath(os.path.join(
+ os.path.dirname(os.path.dirname(__file__)), 'prebuilt/x86-linux/', tool))
+
+
+def helper_temp_path(*path, artifact_cache_root=_DEFAULT_ARTIFACT_CACHE_ROOT):
+ """Returns the path to use for temporary/cached files."""
+ return os.path.join(artifact_cache_root, *path)
+
+
+def get_product_arch(product):
+ """Returns the architecture of a given target |product|."""
+ # The prefix can itself have other prefixes, like 'generic_' or 'aosp_'.
+ product_prefix = 'cheets_'
+
+ idx = product.index(product_prefix)
+ assert idx >= 0, 'Unrecognized product name: %s' % product
+ return product[idx + len(product_prefix):]
diff --git a/server/site_tests/provision_CheetsUpdate/provision_CheetsUpdate.py b/server/site_tests/provision_CheetsUpdate/provision_CheetsUpdate.py
new file mode 100644
index 0000000..6c38da6
--- /dev/null
+++ b/server/site_tests/provision_CheetsUpdate/provision_CheetsUpdate.py
@@ -0,0 +1,166 @@
+# Copyright 2017 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.
+
+import logging
+import pipes
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+
+from autotest_lib.client.common_lib.cros import dev_server
+from autotest_lib.client.common_lib import error
+from autotest_lib.server import test
+from autotest_lib.server import utils
+
+# 2 & 4 are default partitions, and the system boots from one of them.
+# Code from chromite/scripts/deploy_chrome.py
+KERNEL_A_PARTITION = 2
+KERNEL_B_PARTITION = 4
+
+SIMG2IMG_PATH = '/usr/bin/simg2img'
+
+
+class provision_CheetsUpdate(test.test):
+ """
+ Update Android build On the target DUT.
+
+ This test is designed for ARC++ Treehugger style CQ to update Android image
+ on the DUT.
+ """
+ version = 1
+
+
+ def initialize(self):
+ self.android_build_path = None
+ self.__build_temp_dir = None
+
+
+ def download_android_build(self, android_build):
+ """
+ Setup devserver and download the Android test build.
+
+ @param android_build: android build to test.
+ """
+ build_filename = self.generate_android_build_filename(android_build)
+ logging.info('Setting up devserver.')
+ ds = dev_server.AndroidBuildServer.resolve(android_build)
+ branch, target, build_id = (
+ utils.parse_launch_control_build(android_build))
+ ds.stage_artifacts(target, build_id, branch, artifacts=['zip_images'])
+ zip_image = ds.get_staged_file_url(
+ build_filename,
+ target,
+ build_id,
+ branch)
+ logging.info('Downloading the test build.')
+ self.__build_temp_dir = tempfile.mkdtemp()
+ test_filepath = os.path.join(self.__build_temp_dir, build_filename)
+ logging.info('Android test file download path: %s', test_filepath)
+ ds.download_file(zip_image, test_filepath)
+ if not os.path.exists(test_filepath):
+ raise error.TestFail(
+ 'Android test build %s download failed' % test_filepath)
+ self.android_build_path = test_filepath
+
+
+ def remove_rootfs(self, host):
+ """
+ Remove rootfs verification on DUT.
+
+ Removing rootfs is required to push a new Android image to DUT.
+
+ @param host: DUT on which rootfs needs to be disabled.
+ """
+ logging.info('Disabling rootfs on the DUT.')
+ cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
+ '--remove_rootfs_verification --force')
+ for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
+ cmd_with_partition = cmd % partition
+ logging.info(cmd_with_partition)
+ host.run(cmd_with_partition)
+ host.reboot()
+
+
+ def generate_android_build_filename(self, android_build):
+ """
+ Parse Android build version to generate the build file name.
+
+ @param android_build: android build info with branch and build type.
+ e.g. git_mnc-dr-arc-dev/cheets_arm-user/P3909418
+ e.g. git_mnc-dr-arc-dev/cheets_x86-user/P3909418
+
+ @return Android test file name to download and update on the DUT.
+ """
+ m = re.findall(r'cheets_\w+|P?\d+', android_build)
+ if m:
+ return m[0] + '-img-' + m[1] + '.zip'
+ else:
+ raise error.TestFail(
+ 'Android build arg %s is missing build version info.' %
+ android_build)
+
+
+ def run_push_to_device(self, host):
+ """
+ Run push_to_device command to push the test Android build to the DUT.
+
+ @param host: DUT on which the new Android image needs to be pushed.
+ """
+ cmd = ['python3',
+ os.path.join(self.bindir, 'push_to_device.py'),
+ '--use-prebuilt-file',
+ self.android_build_path,
+ '--simg2img',
+ SIMG2IMG_PATH,
+ host.hostname]
+ try:
+ logging.info('Running push to device:')
+ logging.info(
+ '%s',
+ ' '.join(pipes.quote(arg) for arg in cmd))
+ output = subprocess.check_output(
+ cmd,
+ stderr=subprocess.STDOUT)
+ logging.info(output)
+ except subprocess.CalledProcessError as e:
+ logging.error(
+ 'Error while executing %s',
+ ' '.join(pipes.quote(arg) for arg in cmd))
+ logging.error(e.output)
+ raise error.TestFail(
+ 'Pushing Android test build failed due to: %s' %
+ e.output)
+
+
+ def run_once(self, host, value=None):
+ """
+ Installs test Android version `value` on `host`.
+
+ This method is invoked by the test control file to start the
+ provisioning test.
+
+ @param host: DUT on which the test to be run.
+ @param value: contains Android build info to test.
+ """
+ logging.debug('Start provisioning %s to %s', host, value)
+
+ if not value:
+ raise error.TestFail('No build provided.')
+
+ self.download_android_build(value)
+ self.remove_rootfs(host)
+ self.run_push_to_device(host)
+
+
+ def cleanup(self):
+ if self.android_build_path and os.path.exists(self.android_build_path):
+ try:
+ logging.info(
+ 'Deleting Android build dir at %s',
+ self.__build_temp_dir)
+ shutil.rmtree(self.__build_temp_dir)
+ except OSError as e:
+ raise error.TestFail('%s' % e)
diff --git a/server/site_tests/provision_CheetsUpdate/push_to_device.py b/server/site_tests/provision_CheetsUpdate/push_to_device.py
new file mode 100755
index 0000000..4181c98
--- /dev/null
+++ b/server/site_tests/provision_CheetsUpdate/push_to_device.py
@@ -0,0 +1,1052 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2016 The Android Open-Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This script is located in internal ARC repo.
+# Search for android_libs/arc/push_to_device.py
+
+from __future__ import print_function
+
+import argparse
+import atexit
+import hashlib
+import itertools
+import logging
+import os
+import pipes
+import re
+import shutil
+import string
+import subprocess
+import sys
+import tempfile
+import time
+import xml.etree.cElementTree as ElementTree
+import zipfile
+
+import lib.util
+
+
+_SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
+
+_EXPECTED_TARGET_PRODUCTS = {
+ '^x86': ('cheets_x86', 'cheets_x86_64'),
+ '^arm': ('cheets_arm',),
+ '^aarch64$': ('cheets_arm',),
+}
+_ANDROID_ROOT = '/opt/google/containers/android'
+_ANDROID_ROOT_STATEFUL = os.path.join('/usr/local',
+ os.path.relpath(_ANDROID_ROOT, '/'))
+_CONTAINER_INSTANCE_ROOT_WILDCARD = '/run/containers/android_*'
+_CONTAINER_ROOT = os.path.join(_ANDROID_ROOT, 'rootfs', 'root')
+_RSYNC_COMMAND = ['rsync', '--inplace', '-v', '--progress']
+_SCP_COMMAND = ['scp']
+
+_BUILD_FILENAME = string.Template('${product}-img-${build_id}.zip')
+_BUILD_TARGET = string.Template('${product}-${build_variant}')
+
+_CHROMEOS_ARC_ANDROID_SDK_VERSION = 'CHROMEOS_ARC_ANDROID_SDK_VERSION='
+
+_GENERIC_DEVICE = 'generic_%(arch)s_cheets'
+_RO_BUILD_TYPE = 'ro.build.type='
+_RO_BUILD_VERSION_SDK = 'ro.build.version.sdk='
+_RO_PRODUCT_DEVICE = 'ro.product.device='
+
+_ANDROID_REL_KEY_SIGNATURE_SUBSTRING = (
+ '55b390dd7fdb9418631895d5f759f30112687ff621410c069308a')
+_APK_KEY_DEBUG = 'debug-key'
+_APK_KEY_RELEASE = 'release-key'
+_APK_KEY_UNKNOWN = 'unknown'
+_GMS_CORE_PACKAGE_NAME = 'com.google.android.gms'
+
+_ANDROID_SDK_MAPPING = {
+ 23: "M (API 23)",
+ 24: "N (API 24)",
+ 25: "N_MR1 (API 25)",
+ 26: "O (API 26)",
+}
+
+class RemoteProxy(object):
+ """Proxy class to run command line on the remote test device."""
+
+ def __init__(self, remote, dryrun):
+ self._remote = remote
+ self._dryrun = dryrun
+ self._sync_command = (
+ _RSYNC_COMMAND if self._has_rsync_on_remote_device() else _SCP_COMMAND)
+
+ def check_call(self, remote_command):
+ """Runs |remote_command| on the remote test device via ssh."""
+ command = self.get_ssh_commandline(remote_command)
+ lib.util.check_call(dryrun=self._dryrun, *command)
+
+ def check_output(self, remote_command):
+ """Runs |remote_command| on the remote test device via ssh, and returns
+ its output."""
+ command = self.get_ssh_commandline(remote_command)
+ return lib.util.check_output(dryrun=self._dryrun, *command)
+
+ def sync(self, file_list, dest_dir):
+ """Copies |file_list| to the |dest_dir| on the remote test device."""
+ target = 'root@%s:%s' % (self._remote, dest_dir)
+ command = self._sync_command + file_list + [target]
+ lib.util.check_call(dryrun=self._dryrun, *command)
+
+ def push(self, source_path, dest_path):
+ """Pushes |source_path| on the host, to |dest_path| on the remote test
+ device.
+
+ Args:
+ source_path: Host file path to be pushed.
+ dest_path: Path to the destination location on the remote test device.
+ """
+ target = 'root@%s:%s' % (self._remote, dest_path)
+ command = _SCP_COMMAND + [source_path, target]
+ lib.util.check_call(dryrun=self._dryrun, *command)
+
+ def pull(self, source_path, dest_path):
+ """Pulls |source_path| from the remote test device, to |dest_path| on the
+ host.
+
+ Args:
+ source_path: Remote test device file path to be pulled.
+ dest_path: Path to the destination location on the host.
+ """
+ target = 'root@%s:%s' % (self._remote, source_path)
+ command = _SCP_COMMAND + [target, dest_path]
+ return lib.util.check_call(dryrun=self._dryrun, *command)
+
+ def get_ssh_commandline(self, remote_command):
+ return ['ssh', 'root@' + self._remote, remote_command]
+
+ def _has_rsync_on_remote_device(self):
+ command = self.get_ssh_commandline('which rsync')
+ logging.debug('Calling: %s', lib.util.get_command_str(command))
+ # Always return true for --dryrun.
+ return self._dryrun or subprocess.call(command) == 0
+
+
+class TemporaryDirectory(object):
+ """A context object that has a temporary directory with the same lifetime."""
+
+ def __init__(self):
+ self.name = None
+
+ def __enter__(self):
+ self.name = tempfile.mkdtemp()
+ return self
+
+ def __exit__(self, exception_type, exception_value, traceback):
+ shutil.rmtree(self.name)
+
+
+class MountWrapper(object):
+ """A context object that mounts an image during the lifetime."""
+
+ def __init__(self, image_path, mountpoint):
+ self._image_path = image_path
+ self._mountpoint = mountpoint
+
+ def __enter__(self):
+ lib.util.check_call('/usr/bin/sudo', '/bin/mount', '-o', 'loop',
+ self._image_path, self._mountpoint)
+ return self
+
+ def __exit__(self, exception_type, exception_value, traceback):
+ try:
+ lib.util.check_call('/usr/bin/sudo', '/bin/umount', self._mountpoint)
+ except Exception:
+ if not exception_type:
+ raise
+ # Instead of propagate the exception, record the one from exit body.
+ logging.exception('Failed to umount ' + self._mountpoint)
+
+
+class Simg2img(object):
+ """Wrapper class of simg2img"""
+
+ def __init__(self, simg2img_path, dryrun):
+ self._path = simg2img_path
+ self._dryrun = dryrun
+
+ def convert(self, src, dest):
+ """Converts the image to the raw image by simg2img command line.
+
+ If |dryrun| is set, does not execute the commandline.
+ """
+ lib.util.check_call(self._path, src, dest, dryrun=self._dryrun)
+
+
+def _verify_machine_arch(remote_proxy, target_product, dryrun):
+ """Verifies if the data being pushed is build for the target architecture.
+
+ Args:
+ remote_proxy: RemoteProxy instance for the remote test device.
+ target_product: Target product name of the image being pushed. This is
+ usually set by "lunch" command. E.g. "cheets_x86" or "cheets_arm".
+ dryrun: If set, this function assumes the machine architectures match.
+
+ Raises:
+ AssertionError: If the pushing image does not match to the remote test
+ device.
+ """
+ if dryrun:
+ logging.debug('Pretending machine architectures match')
+ return
+ remote_arch = remote_proxy.check_output('uname -m')
+ for arch_pattern, expected_set in _EXPECTED_TARGET_PRODUCTS.items():
+ if re.search(arch_pattern, remote_arch):
+ expected = itertools.chain.from_iterable(
+ (expected, 'aosp_' + expected, expected + '_gmscore_next') for
+ expected in expected_set)
+ assert target_product in expected, (
+ ('Architecture mismatch: Deploying \'%s\' to \'%s\' seems incorrect.'
+ % (target_product, remote_arch)))
+ return
+ logging.warning('Unknown remote machine type \'%s\'. Skipping '
+ 'architecture sanity check.', remote_arch)
+
+
+def _convert_images(simg2img, out, push_vendor_image):
+ """Converts the images being pushed to the raw images.
+
+ Returns:
+ A tuple of (large_file_list, file_list). Each list consists of paths of
+ converted files.
+ """
+ result = []
+ result_large = []
+
+ system_raw_img = os.path.join(out, 'system.raw.img')
+ simg2img.convert(os.path.join(out, 'system.img'), system_raw_img)
+ result_large.append(system_raw_img)
+
+ if push_vendor_image:
+ vendor_raw_img = os.path.join(out, 'vendor.raw.img')
+ simg2img.convert(os.path.join(out, 'vendor.img'), vendor_raw_img)
+ result.append(vendor_raw_img)
+
+ return (result_large, result)
+
+
+def _update_build_fingerprint(remote_proxy, build_fingerprint):
+ """Updates CHROMEOS_ARC_VERSION in /etc/lsb-release.
+
+ Args:
+ remote_proxy: RemoteProxy instance connected to the test device.
+ build_fingerprint: The version code which should be embedded into
+ /etc/lsb-release.
+ """
+ if not build_fingerprint:
+ logging.warning(
+ 'Skipping version update. ARC version will be reported incorrectly')
+ return
+
+ # Replace the ARC version on disk with what we're pushing there.
+ logging.info('Updating CHROMEOS_ARC_VERSION...')
+ remote_proxy.check_call(' '.join([
+ '/bin/sed', '-i',
+ # Note: we assume build_fingerprint does not contain any char which
+ # needs to be escaped.
+ r'"s/^\(CHROMEOS_ARC_VERSION=\).*/\1%(_BUILD_FINGERPRINT)s/"',
+ '/etc/lsb-release'
+ ]) % {'_BUILD_FINGERPRINT': build_fingerprint})
+
+
+def _get_remote_device_android_sdk_version(remote_proxy, dryrun):
+ """ Returns the Android SDK version on the remote device.
+
+ Args:
+ remote_proxy: RemoteProxy instance for the remote test device.
+ dryrun: If set, this function assumes Android SDK version is 1.
+ """
+ if dryrun:
+ logging.debug('Pretending target device\'s Android SDK version is 1')
+ return 1
+ try:
+ line = remote_proxy.check_output(
+ 'grep ^%s /etc/lsb-release' % _CHROMEOS_ARC_ANDROID_SDK_VERSION).strip()
+ except subprocess.CalledProcessError:
+ logging.exception('Failed to inspect /etc/lsb-release remotely')
+ return None
+
+ if not line.startswith(_CHROMEOS_ARC_ANDROID_SDK_VERSION):
+ logging.warning('Failed to find the correct string format.\n'
+ 'Expected format: "%s"\nActual string: "%s"',
+ _CHROMEOS_ARC_ANDROID_SDK_VERSION, line)
+ return None
+
+ android_sdk_version = int(
+ line[len(_CHROMEOS_ARC_ANDROID_SDK_VERSION):].strip())
+ logging.debug('Target device\'s Android SDK version: %d', android_sdk_version)
+ return android_sdk_version
+
+
+def _verify_android_sdk_version(remote_proxy, provider, dryrun):
+ """Verifies if the Android SDK versions of the pushing image and the test
+ device are the same.
+
+ Args:
+ remote_proxy: RemoteProxy instance for the remote test device.
+ provider: Android image provider.
+ dryrun: If set, this function assumes Android SDK versions match.
+
+ Raises:
+ AssertionError: If the Android SDK version of pushing image does not match
+ the Android SDK version on the remote test device.
+ """
+ if dryrun:
+ logging.debug('Pretending Android SDK versions match')
+ return
+ logging.debug('New image\'s Android SDK version: %d',
+ provider.get_build_version_sdk())
+
+ device_android_sdk_version = _get_remote_device_android_sdk_version(
+ remote_proxy, dryrun)
+
+ if device_android_sdk_version is None:
+ if not boolean_prompt(('Unable to determine the target device\'s Android '
+ 'SDK version. Continue?'), False):
+ sys.exit(1)
+ else:
+ assert device_android_sdk_version == provider.get_build_version_sdk(), (
+ 'Android SDK versions do not match. The target device has {}, while '
+ 'the new image is {}'.format(
+ _android_sdk_version_to_string(device_android_sdk_version),
+ _android_sdk_version_to_string(provider.get_build_version_sdk())))
+
+
+def _android_sdk_version_to_string(android_sdk_version):
+ """Converts the |android_sdk_version| to a human readable string
+
+ Args:
+ android_sdk_version: The Android SDK version number as a string
+ """
+ return _ANDROID_SDK_MAPPING.get(
+ android_sdk_version,
+ 'Unknown SDK Version (API {})'.format(android_sdk_version))
+
+
+def _is_selinux_policy_updated(remote_proxy, out, dryrun):
+ """Returns True if SELinux policy is updated."""
+ if dryrun:
+ logging.debug('Pretending sepolicy is not updated in dryrun mode')
+ return False
+ remote_sepolicy_sha1, _ = remote_proxy.check_output(
+ 'sha1sum /etc/selinux/arc/policy/policy.30').split()
+ with open(os.path.join(out, 'root', 'sepolicy'), 'rb') as f:
+ host_sepolicy_sha1 = hashlib.sha1(f.read()).hexdigest()
+ return remote_sepolicy_sha1 != host_sepolicy_sha1
+
+
+def _update_selinux_policy(remote_proxy, out):
+ """Updates the selinux policy file."""
+ remote_proxy.push(os.path.join(out, 'root', 'sepolicy'),
+ '/etc/selinux/arc/policy/policy.30')
+
+
+def _remount_rootfs_as_writable(remote_proxy):
+ """Remounts root file system to make it writable."""
+ remote_proxy.check_call('mount -o remount,rw /')
+
+
+def boolean_prompt(prompt, default=True, true_value='yes', false_value='no',
+ prolog=None):
+ """Helper function for processing boolean choice prompts.
+
+ Args:
+ prompt: The question to present to the user.
+ default: Boolean to return if the user just presses enter.
+ true_value: The text to display that represents a True returned.
+ false_value: The text to display that represents a False returned.
+ prolog: The text to display before prompt.
+
+ Returns:
+ True or False.
+ """
+ true_value, false_value = true_value.lower(), false_value.lower()
+ true_text, false_text = true_value, false_value
+ if true_value == false_value:
+ raise ValueError('true_value and false_value must differ: got %r'
+ % true_value)
+
+ if default:
+ true_text = true_text[0].upper() + true_text[1:]
+ else:
+ false_text = false_text[0].upper() + false_text[1:]
+
+ prompt = ('\n%s (%s/%s)? ' % (prompt, true_text, false_text))
+
+ if prolog:
+ prompt = ('\n%s\n%s' % (prolog, prompt))
+
+ while True:
+ try:
+ response = input(prompt).lower()
+ except EOFError:
+ # If the user hits CTRL+D, or stdin is disabled, use the default.
+ print(file=sys.stderr)
+ response = None
+ except KeyboardInterrupt:
+ # If the user hits CTRL+C, just exit the process.
+ print(file=sys.stderr)
+ print('CTRL+C detected; exiting', file=sys.stderr)
+ raise
+
+ if not response:
+ return default
+ if true_value.startswith(response):
+ if not false_value.startswith(response):
+ return True
+ # common prefix between the two...
+ elif false_value.startswith(response):
+ return False
+
+
+def _disable_rootfs_verification(force, remote_proxy):
+ make_dev_ssd_path = \
+ '/usr/libexec/debugd/helpers/dev_features_rootfs_verification'
+ make_dev_ssd_command = remote_proxy.get_ssh_commandline(make_dev_ssd_path)
+ if not force:
+ logging.error('Detected that the device has rootfs verification enabled.')
+ logging.info('This script can automatically remove the rootfs '
+ 'verification using `%s`, which requires that the device is '
+ 'rebooted afterwards.',
+ lib.util.get_command_str(make_dev_ssd_command))
+ logging.info('Skip this prompt by specifying --force.')
+ if not boolean_prompt('Remove rootfs verification?', False):
+ return False
+ remote_proxy.check_call(make_dev_ssd_path)
+ reboot_time = time.time()
+ remote_proxy.check_call('reboot')
+ logging.debug('Waiting up to 10 seconds for the machine to reboot')
+ for _ in range(10):
+ time.sleep(1)
+ try:
+ device_boot_time = remote_proxy.check_output('grep btime /proc/stat | ' +
+ 'cut -d" " -f2')
+ if int(device_boot_time) >= reboot_time:
+ return True
+ except subprocess.CalledProcessError:
+ pass
+ logging.error('Failed to detect whether the device had successfully rebooted')
+ return False
+
+def _stop_ui(remote_proxy):
+ remote_proxy.check_call('\n'.join([
+ # Stop UI if necessary.
+ 'if ! (status ui | grep -q stop); then',
+ ' stop ui',
+ 'fi',
+
+ # Unmount the container root/vendor and root if necessary.
+ 'stop arc-system-mount',
+ # TODO(yusukes): Remove the manual umount below once everyone starts using
+ # arc-system-mount.conf with the post-stop script.
+ 'if mountpoint -q %(_CONTAINER_ROOT)s/vendor; then',
+ ' umount %(_CONTAINER_ROOT)s/vendor',
+ 'fi',
+ 'if mountpoint -q %(_CONTAINER_ROOT)s; then',
+ ' umount %(_CONTAINER_ROOT)s',
+ 'fi',
+ ]) % {'_CONTAINER_ROOT': _CONTAINER_ROOT})
+
+
+class ImageUpdateMode(object):
+ """Context object to manage remote host writable status."""
+ def __init__(self, remote_proxy, is_selinux_policy_updated, push_to_stateful,
+ clobber_data, force):
+ self._remote_proxy = remote_proxy
+ self._is_selinux_policy_updated = is_selinux_policy_updated
+ self._push_to_stateful = push_to_stateful
+ self._clobber_data = clobber_data
+ self._force = force
+
+ def __enter__(self):
+ logging.info('Setting up ChromeOS device to image-writable...')
+
+ if self._clobber_data:
+ self._remote_proxy.check_call(
+ 'if [ -e %(ANDROID_ROOT_WILDCARD)s/root/data ]; then'
+ ' kill -9 `cat %(ANDROID_ROOT_WILDCARD)s/container.pid`;'
+ ' find %(ANDROID_ROOT_WILDCARD)s/root/data'
+ ' %(ANDROID_ROOT_WILDCARD)s/root/cache -mindepth 1 -delete;'
+ 'fi' % {'ANDROID_ROOT_WILDCARD': _CONTAINER_INSTANCE_ROOT_WILDCARD})
+
+ _stop_ui(self._remote_proxy)
+ try:
+ _remount_rootfs_as_writable(self._remote_proxy)
+ except subprocess.CalledProcessError:
+ if not _disable_rootfs_verification(self._force, self._remote_proxy):
+ raise
+ _stop_ui(self._remote_proxy)
+ # Try to remount rootfs as writable. Bail out if it fails this time.
+ _remount_rootfs_as_writable(self._remote_proxy)
+ try:
+ self._remote_proxy.check_call('\n'.join([
+ # Delete the image file if it is a symlink.
+ 'test -L %(_ANDROID_ROOT)s/system.raw.img && '
+ ' rm %(_ANDROID_ROOT)s/system.raw.img',
+ ]) % {'_ANDROID_ROOT': _ANDROID_ROOT})
+ except Exception:
+ # Not a symlink.
+ pass
+ if self._push_to_stateful:
+ self._remote_proxy.check_call('\n'.join([
+ # Create the destination directory in the stateful partition.
+ 'mkdir -p %(_ANDROID_ROOT_STATEFUL)s',
+ ]) % {'_ANDROID_ROOT_STATEFUL': _ANDROID_ROOT_STATEFUL})
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self._push_to_stateful:
+ # Push the image to _ANDROID_ROOT_STATEFUL instead of _ANDROID_ROOT.
+ # Create a symlink so that arc-system-mount can handle it.
+ self._remote_proxy.check_call('\n'.join([
+ 'ln -sf %(_ANDROID_ROOT_STATEFUL)s/system.raw.img '
+ ' %(_ANDROID_ROOT)s/system.raw.img',
+ ]) % {'_ANDROID_ROOT': _ANDROID_ROOT,
+ '_ANDROID_ROOT_STATEFUL': _ANDROID_ROOT_STATEFUL})
+
+ if self._is_selinux_policy_updated:
+ logging.info('*** SELinux policy updated. ***')
+ else:
+ logging.info('*** SELinux policy is not updated. Restarting ui. ***')
+ try:
+ self._remote_proxy.check_call('\n'.join([
+ # Make the whole invocation fail if any individual command does.
+ 'set -e',
+
+ # Remount the root file system to readonly.
+ 'mount -o remount,ro /',
+
+ # Restart UI.
+ 'start ui',
+
+ # Mount the updated {system,vendor}.raw.img. This will also trigger
+ # android-ureadahead once it's done and should remove the packfile.
+ 'start arc-system-mount',
+ ]))
+ return
+ except Exception:
+ # The above commands are just an optimization to avoid having to reboot
+ # every single time an image is pushed, which saves 6-10s. If any of
+ # them fail, the only safe thing to do is reboot the device.
+ logging.exception('Failed to cleanly restart ui, fall back to reboot')
+
+ logging.info('*** Reboot required. ***')
+ try:
+ self._remote_proxy.check_call('reboot')
+ except Exception:
+ if exc_type is None:
+ raise
+ # If the body block of a with statement also raises an error, here we
+ # just log the exception, so that the main exception will be propagated to
+ # the caller properly.
+ logging.exception('Failed to reboot the device')
+
+
+class PreserveTimestamps(object):
+ """Context object to modify a file but preserve the original timestamp."""
+ def __init__(self, path):
+ self.path = path
+ self._original_timestamp = None
+
+ def __enter__(self):
+ # Save the original timestamp
+ self._original_timestamp = os.stat(self.path)
+ return self
+
+ def __exit__(self, exception_type, exception_value, traceback):
+ # Apply the original timestamp
+ os.utime(self.path, (self._original_timestamp.st_atime,
+ self._original_timestamp.st_mtime))
+
+
+def _extract_artifact(simg2img, out_dir, filename):
+ with zipfile.ZipFile(filename, 'r') as z:
+ z.extract('system.img', out_dir)
+ z.extract('vendor.img', out_dir)
+ # Note that the same simg2img conversion is performed again for system.img
+ # later, but the extra run is acceptable (<2s). If this is important, we
+ # could try to change the program flow.
+ simg2img.convert(os.path.join(out_dir, 'system.img'),
+ os.path.join(out_dir, 'system.raw.img'))
+ # Extract the SELinux policy.
+ with TemporaryDirectory() as mnt_dir:
+ with MountWrapper(os.path.join(out_dir, 'system.raw.img'),
+ mnt_dir.name):
+ os.makedirs(os.path.join(out_dir, 'root'))
+ shutil.copyfile(os.path.join(mnt_dir.name, 'sepolicy'),
+ os.path.join(out_dir, 'root', 'sepolicy'))
+ shutil.copyfile(os.path.join(mnt_dir.name, 'system', 'build.prop'),
+ os.path.join(out_dir, 'build.prop'))
+
+
+def _make_tempdir_deleted_on_exit():
+ d = tempfile.mkdtemp()
+ atexit.register(shutil.rmtree, d, ignore_errors=True)
+ return d
+
+
+def _detect_cert_inconsistency(remote_proxy, new_variant, dryrun):
+ """Prompt to ask for deleting data based on detected situation (best effort).
+
+ Detection is only accurate for active session, so it won't fix other profiles.
+
+ As GMS apps are signed with different key between user and non-user build,
+ the container won't run correctly if old key has been registered in /data.
+ """
+ if dryrun:
+ return False
+
+ # Get current build variant on device.
+ cmd = 'grep %s %s' % (_RO_BUILD_TYPE,
+ os.path.join(_CONTAINER_ROOT, 'system/build.prop'))
+ try:
+ line = remote_proxy.check_output(cmd).strip()
+ except subprocess.CalledProcessError:
+ # Catch any error to avoid blocking the push.
+ logging.exception('Failed to inspect build property remotely')
+ return False
+ device_variant = line[len(_RO_BUILD_TYPE):]
+
+ device_apk_key = _APK_KEY_UNKNOWN
+ try:
+ device_apk_key = _get_remote_device_apk_key(remote_proxy)
+ except Exception as e:
+ logging.warning('There was an error getting the remote device APK '
+ 'key signature %s. Assuming APK key signature is '
+ '\'unknown\'', e)
+
+ logging.debug('device apk key: %s; build variant: %s -> %s', device_apk_key,
+ device_variant, new_variant)
+
+ # GMS signature in /data is inconsistent with the new build.
+ is_inconsistent = (
+ (device_apk_key == _APK_KEY_RELEASE and new_variant != 'user') or
+ (device_apk_key == _APK_KEY_DEBUG and new_variant == 'user'))
+
+ if is_inconsistent:
+ new_apk_key = _APK_KEY_RELEASE if new_variant == 'user' else _APK_KEY_DEBUG
+ return boolean_prompt(
+ 'Detected apk signature change (%s -> %s[%s]) on current user. Delete '
+ '/data and /cache?' % (device_apk_key, new_apk_key, new_variant),
+ default=True)
+
+ # Switching from/to user build.
+ if (device_variant == 'user') != (new_variant == 'user'):
+ logging.warn('\n\n** You are switching build variant (%s -> %s). If you '
+ 'have ever run with the old image, make sure to wipe out '
+ '/data first before starting the container. **\n',
+ device_variant, new_variant)
+ return False
+
+
+def _get_remote_device_apk_key(remote_proxy):
+ """Retrieves the APK key signature of the remote test device.
+
+ Args:
+ remote_proxy: RemoteProxy instance for the remote test device.
+ """
+ remote_packages_xml = os.path.join(_CONTAINER_INSTANCE_ROOT_WILDCARD,
+ 'root/data/system/packages.xml')
+ with TemporaryDirectory() as tmp_dir:
+ host_packages_xml = os.path.join(tmp_dir.name, 'packages.xml')
+ remote_proxy.pull(remote_packages_xml, host_packages_xml)
+ return _get_apk_key_from_xml(host_packages_xml)
+
+
+def _get_apk_key_from_xml(xml_file):
+ """Parses |xml_file| to determine the APK key signature.
+
+ Args:
+ xml_file: The XML file to parse.
+ """
+ if not os.path.exists(xml_file):
+ logging.warning('XML file doesn\'t exist: %s' % xml_file)
+ return _APK_KEY_UNKNOWN
+
+ root = ElementTree.parse(xml_file).getroot()
+ gms_core_elements = root.findall('package[@name=\'%s\']'
+ % _GMS_CORE_PACKAGE_NAME)
+ assert len(gms_core_elements) == 1, ('Invalid number of GmsCore package '
+ 'elements. Expected: 1 Actual: %d'
+ % len(gms_core_elements))
+ gms_core_element = gms_core_elements[0]
+ sigs_element = gms_core_element.find('sigs')
+ assert sigs_element, ('Unable to find the |sigs| tag under the GmsCore '
+ 'package tag.')
+ sigs_count_attribute = int(sigs_element.get('count'))
+ assert sigs_count_attribute == 1, ('Invalid signature count. Expected: 1 '
+ 'Actual: %d' % sigs_count_attribute)
+ cert_element = sigs_element.find('cert')
+ gms_core_cert_index = int(cert_element.get('index', -1))
+ logging.debug("GmsCore cert index: %d" % gms_core_cert_index)
+ if gms_core_cert_index == -1:
+ logging.warning('Invalid cert index (%d)' % gms_core_cert_index)
+ return _APK_KEY_UNKNOWN
+
+ cert_key = cert_element.get('key')
+ if cert_key:
+ return _get_android_key_type_from_cert_key(cert_key)
+
+ # The GmsCore package element for |cert| contains the cert index, but not the
+ # cert key. Find its the matching cert key.
+ for cert_element in root.findall('package/sigs/cert'):
+ cert_index = int(cert_element.get('index'))
+ cert_key = cert_element.get('key')
+ if cert_key and cert_index == gms_core_cert_index:
+ return _get_android_key_type_from_cert_key(cert_key)
+ logging.warning ('Unable to find a cert key matching index %d' % cert_index)
+ return _APK_KEY_UNKNOWN
+
+
+def _get_android_key_type_from_cert_key(cert_key):
+ """Returns |_APK_KEY_RELEASE| if |cert_key| contains the Android release key
+ signature substring, otherwise it returns |_APK_KEY_DEBUG|."""
+ if _ANDROID_REL_KEY_SIGNATURE_SUBSTRING in cert_key:
+ return _APK_KEY_RELEASE
+ else:
+ return _APK_KEY_DEBUG
+
+
+def _find_build_property(line, build_property_name):
+ """Returns the value that matches |build_property_name| in |line|."""
+ if line.startswith(build_property_name):
+ return line[len(build_property_name):].strip()
+ return None
+
+
+class BaseProvider(object):
+ """Base class of image provider.
+
+ Subclass should provide a directory with images in it.
+ """
+
+ def __init__(self):
+ self._build_variant = None
+ self._build_version_sdk = None
+
+ def prepare(self):
+ """Subclass should prepare image in its implementation.
+
+ Subclass must return the (image directory, product, fingerprint) tuple.
+ Product is a string like "cheets_arm". Fingerprint is the string that
+ will be updated to CHROMEOS_ARC_VERSION in /etc/lsb-release.
+ """
+ raise NotImplementedError()
+
+ def get_build_variant(self):
+ """ Returns the extracted build variant."""
+ return self._build_variant
+
+ def get_build_version_sdk(self):
+ """ Returns the extracted Android SDK version."""
+ return self._build_version_sdk
+
+ def read_build_prop_file(self, build_prop_file, remove_file=True):
+ """ Reads the specified build property file, and extracts the
+ "ro.build.variant" and "ro.build.version.sdk" fields. This method optionally
+ deletes |build_prop_file| when done
+
+ Args:
+ build_prop_file: The fully qualified path to the build.prop file.
+ remove_file: Removes the |build_prop_file| when done. (default=True)
+ """
+ logging.debug('Reading build prop file: %s', build_prop_file)
+ with open(build_prop_file, 'r') as f:
+ for line in f:
+ if self._build_version_sdk is None:
+ value = _find_build_property(line, _RO_BUILD_VERSION_SDK)
+ if value is not None:
+ self._build_version_sdk = int(value)
+ if self._build_variant is None:
+ value = _find_build_property(line, _RO_BUILD_TYPE)
+ if value is not None:
+ self._build_variant = value
+ if self._build_variant and self._build_version_sdk:
+ break
+ if remove_file:
+ logging.info('Deleting prop file: %s...', build_prop_file)
+ os.remove(build_prop_file)
+
+
+class LocalPrebuiltProvider(BaseProvider):
+ """A provider that provides prebuilt image from a local file."""
+
+ def __init__(self, prebuilt_file, simg2img):
+ super(LocalPrebuiltProvider, self).__init__()
+ self._prebuilt_file = prebuilt_file
+ self._simg2img = simg2img
+
+ def prepare(self):
+ out_dir = _make_tempdir_deleted_on_exit()
+ _extract_artifact(self._simg2img, out_dir, self._prebuilt_file)
+
+ build_prop_file = os.path.join(out_dir, 'build.prop')
+ self.read_build_prop_file(build_prop_file)
+ if self._build_variant is None:
+ self._build_variant = 'user' # default to non-eng
+
+ m = re.match(r'(cheets_\w+)-img-P?\d+\.zip',
+ os.path.basename(self._prebuilt_file))
+ if not m:
+ sys.exit('Unrecognized file name of prebuilt image archive.')
+ product = m.group(1)
+
+ fingerprint = os.path.splitext(os.path.basename(self._prebuilt_file))[0]
+ return out_dir, product, fingerprint
+
+
+class LocalBuildProvider(BaseProvider):
+ """A provider that provides local built image."""
+
+ def __init__(self, build_fingerprint, skip_build_prop_update):
+ super(LocalBuildProvider, self).__init__()
+ self._build_fingerprint = build_fingerprint
+ self._skip_build_prop_update = skip_build_prop_update
+ expected_env = ('TARGET_BUILD_VARIANT', 'TARGET_PRODUCT', 'OUT')
+ if not all(var in os.environ for var in expected_env):
+ sys.exit('Did you run lunch?')
+ self._build_variant = os.environ.get('TARGET_BUILD_VARIANT')
+ self._target_product = os.environ.get('TARGET_PRODUCT')
+ self._out_dir = os.environ.get('OUT')
+
+ def prepare(self):
+ # Use build fingerprint if set. Otherwise, read it from the text file.
+ build_fingerprint = self._build_fingerprint
+ if not build_fingerprint:
+ fingerprint_filepath = os.path.join(self._out_dir,
+ 'build_fingerprint.txt')
+ if os.path.isfile(fingerprint_filepath):
+ with open(fingerprint_filepath) as f:
+ build_fingerprint = f.read().strip().replace('/', '_')
+
+ # Find the absolute path of build.prop.
+ build_prop_file = os.path.join(self._out_dir, 'system/build.prop')
+ if not self._skip_build_prop_update:
+ self._update_local_build_prop_file(build_prop_file)
+ self.read_build_prop_file(build_prop_file, False)
+ return self._out_dir, self._target_product, build_fingerprint
+
+ def _update_local_build_prop_file(self, build_prop_file):
+ """Updates build.prop of the local prebuilt image."""
+
+ if not build_prop_file:
+ logging.warning('Skipping. build_prop_file was not specified.')
+ return
+ # Create the generic device name by extracting the architecture type
+ # from the target product.
+ generic_device = _GENERIC_DEVICE % dict(
+ arch=lib.util.get_product_arch(self._target_product))
+ # Get the current value of ro.product.device in build.prop.
+ current_prop_value = lib.util.check_output('grep', _RO_PRODUCT_DEVICE,
+ build_prop_file).strip()
+ new_prop_value = '%s%s' % (_RO_PRODUCT_DEVICE, generic_device)
+ # If build.prop contains the new value, return.
+ if current_prop_value == new_prop_value:
+ logging.info('build.prop does not need to be updated.')
+ return
+
+ logging.info('Setting "%s" to "%s" in build.prop...',
+ _RO_PRODUCT_DEVICE, generic_device)
+ with PreserveTimestamps(build_prop_file) as f:
+ # Make the changes to build.prop
+ lib.util.check_call(
+ '/bin/sed', '-i',
+ r's/^\(%(_KEY)s\).*/\1%(_VALUE)s/'
+ % {'_KEY': _RO_PRODUCT_DEVICE, '_VALUE': generic_device},
+ f.path)
+
+ logging.info('Recreating the system image with the updated build.prop ' +
+ 'file...')
+ system_dir = os.path.join(self._out_dir, 'system')
+ system_image_info_file = os.path.join(
+ self._out_dir,
+ 'obj/PACKAGING/systemimage_intermediates/system_image_info.txt')
+ system_image_file = os.path.join(self._out_dir, 'system.img')
+ with PreserveTimestamps(system_image_file) as f:
+ # Recreate system.img
+ lib.util.check_call(
+ './build/tools/releasetools/build_image.py',
+ system_dir,
+ system_image_info_file,
+ f.path,
+ system_dir)
+
+
+class NullProvider(BaseProvider):
+ """ Provider used for dry runs """
+
+ def __init__(self):
+ super(NullProvider, self).__init__()
+ self._build_variant = 'user'
+ self._build_version_sdk = 1
+
+ def prepare(self):
+ return ('<dir>', '<product>', '<fingerprint>')
+
+
+def _parse_prebuilt(param):
+ m = re.search(r'^(cheets_(?:arm|x86))/(user|userdebug|eng)/(P?\d+)$', param)
+ if not m:
+ sys.exit('Invalid format of --use-prebuilt')
+ return m.group(1), m.group(2), m.group(3)
+
+
+def _default_simg2img_path():
+ # Automatically resolve simg2img path if possible.
+ if 'ANDROID_HOST_OUT' in os.environ:
+ return os.path.join(os.environ.get('ANDROID_HOST_OUT'), 'bin', 'simg2img')
+ path = os.path.join(_SCRIPT_DIR, 'simg2img')
+ if os.path.isfile(path):
+ return path
+ return None
+
+
+def _resolve_args(args):
+ if not args.simg2img_path:
+ sys.exit('Cannot determine the path of simg2img')
+
+
+def _parse_args():
+ """Parses the arguments."""
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description='Push image to Chromebook',
+ epilog="""Examples:
+
+To push from local build
+$ %(prog)s <remote>
+
+To push from Android build prebuilt
+$ %(prog)s --use-prebuilt cheets_arm/eng/123456 <remote>
+
+To push from local prebuilt
+$ %(prog)s --use-prebuilt-file path/to/cheets_arm-img-123456.zip <remote>
+""")
+ parser.add_argument(
+ '--push-vendor-image', action='store_true', help='Push vendor image')
+ parser.add_argument(
+ '--use-prebuilt', metavar='PRODUCT/BUILD_VARIANT/BUILD_ID',
+ type=_parse_prebuilt,
+ help='Push prebuilt image instead. Example value: cheets_arm/eng/123456')
+ parser.add_argument(
+ '--use-prebuilt-file', dest='prebuilt_file', metavar='<path>',
+ help='The downloaded image path')
+ parser.add_argument(
+ '--build-fingerprint', default=os.environ.get('BUILD_FINGERPRINT'),
+ help='If set, embed this fingerprint data to the /etc/lsb-release '
+ 'as CHROMEOS_ARC_VERSION value.')
+ parser.add_argument(
+ '--dryrun', action='store_true',
+ help='Do not execute subprocesses.')
+ parser.add_argument(
+ '--loglevel', default='INFO',
+ choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'),
+ help='Logging level.')
+ parser.add_argument(
+ '--simg2img-path', default=_default_simg2img_path(),
+ help='Executable path of simg2img')
+ parser.add_argument(
+ '--force', action='store_true',
+ help=('Skip all prompts (i.e., for disabling of rootfs verification). '
+ 'This may result in the target machine being rebooted'))
+ parser.add_argument(
+ '--try-clobber-data', action='store_true',
+ help='If currently logged in, also clobber /data and /cache')
+ parser.add_argument(
+ '--skip_build_prop_update', action='store_true',
+ help=('Do not change ro.product.device to "generic_cheets" for local '
+ 'builds'))
+ parser.add_argument(
+ '--push-to-stateful-partition', action='store_true',
+ help=('Place the system.raw.img on the stateful partition instead of /. '
+ 'This is always used for -eng builds since they do not fit on /.'))
+ parser.add_argument(
+ 'remote',
+ help=('The target test device. This is passed to ssh command etc., '
+ 'so IP or the name registered in your .ssh/config file can be '
+ 'accepted.'))
+ args = parser.parse_args()
+
+ _resolve_args(args)
+ return args
+
+
+def main():
+ # Set up arguments.
+ args = _parse_args()
+ logging.basicConfig(level=getattr(logging, args.loglevel))
+
+ simg2img = Simg2img(args.simg2img_path, args.dryrun)
+
+ # Prepare local source. A preparer is responsible to return an directory that
+ # contains necessary files to push. It also needs to return metadata like
+ # product (e.g. cheets_arm) and a build fingerprint.
+ if args.dryrun:
+ provider = NullProvider()
+ elif args.prebuilt_file:
+ provider = LocalPrebuiltProvider(args.prebuilt_file, simg2img)
+ else:
+ provider = LocalBuildProvider(args.build_fingerprint,
+ args.skip_build_prop_update)
+
+ # Actually prepare the files to push.
+ out, product, fingerprint = provider.prepare()
+
+ # Update the image.
+ remote_proxy = RemoteProxy(args.remote, args.dryrun)
+ _verify_android_sdk_version(remote_proxy, provider, args.dryrun)
+ _verify_machine_arch(remote_proxy, product, args.dryrun)
+
+ if args.try_clobber_data:
+ clobber_data = True
+ else:
+ clobber_data = _detect_cert_inconsistency(
+ remote_proxy, provider.get_build_variant(), args.dryrun)
+
+ logging.info('Converting images to raw images...')
+ (large_image_list, image_list) = _convert_images(
+ simg2img, out, args.push_vendor_image)
+
+ is_selinux_policy_updated = _is_selinux_policy_updated(remote_proxy, out,
+ args.dryrun)
+ push_to_stateful = (args.push_to_stateful_partition or
+ 'eng' == provider.get_build_variant())
+
+ with ImageUpdateMode(remote_proxy, is_selinux_policy_updated,
+ push_to_stateful, clobber_data, args.force):
+ is_debuggable = 'user' != provider.get_build_variant()
+ remote_proxy.check_call(' '.join([
+ '/bin/sed', '-i',
+ r'"s/^\(env ANDROID_DEBUGGABLE=\).*/\1%(_IS_DEBUGGABLE)d/"',
+ '/etc/init/arc-setup.conf'
+ ]) % {'_IS_DEBUGGABLE': is_debuggable})
+
+ logging.info('Syncing image files to ChromeOS...')
+ if large_image_list:
+ remote_proxy.sync(large_image_list,
+ _ANDROID_ROOT_STATEFUL if push_to_stateful else
+ _ANDROID_ROOT)
+ if image_list:
+ remote_proxy.sync(image_list, _ANDROID_ROOT)
+ _update_build_fingerprint(remote_proxy, fingerprint)
+ if is_selinux_policy_updated:
+ _update_selinux_policy(remote_proxy, out)
+
+
+if __name__ == '__main__':
+ main()