| # Copyright 2018 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 os |
| import time |
| |
| from autotest_lib.server import test |
| from autotest_lib.server.cros import filesystem_util |
| from autotest_lib.client.common_lib import error, utils |
| |
| |
| class FingerprintTest(test.test): |
| """Base class that sets up helpers for fingerprint tests.""" |
| version = 1 |
| |
| # Location of firmware from the build on the DUT |
| _FINGERPRINT_BUILD_FW_DIR = '/opt/google/biod/fw' |
| |
| _DISABLE_FP_UPDATER_FILE = '.disable_fp_updater' |
| |
| _UPSTART_DIR = '/etc/init' |
| _BIOD_UPSTART_JOB_FILE = 'biod.conf' |
| _STATEFUL_PARTITION_DIR = '/mnt/stateful_partition' |
| |
| _GENIMAGES_SCRIPT_NAME = 'gen_test_images.sh' |
| _GENIMAGES_OUTPUT_DIR_NAME = 'images' |
| |
| _TEST_IMAGE_FORMAT_MAP = { |
| 'TEST_IMAGE_ORIGINAL': '%s.bin', |
| 'TEST_IMAGE_DEV': '%s.dev', |
| 'TEST_IMAGE_CORRUPT_FIRST_BYTE': '%s_corrupt_first_byte.bin', |
| 'TEST_IMAGE_CORRUPT_LAST_BYTE': '%s_corrupt_last_byte.bin', |
| 'TEST_IMAGE_DEV_RB_ZERO': '%s.dev.rb0', |
| 'TEST_IMAGE_DEV_RB_ONE': '%s.dev.rb1', |
| 'TEST_IMAGE_DEV_RB_NINE': '%s.dev.rb9' |
| } |
| |
| _ROLLBACK_ZERO_BLOCK_ID = '0' |
| _ROLLBACK_INITIAL_BLOCK_ID = '1' |
| _ROLLBACK_INITIAL_MIN_VERSION = '0' |
| _ROLLBACK_INITIAL_RW_VERSION = '0' |
| |
| _SERVER_GENERATED_FW_DIR_NAME = 'generated_fw' |
| |
| _DUT_TMP_PATH_BASE = '/tmp/fp_test' |
| |
| # Name of key in "futility show" output corresponds to the signing key ID |
| _FUTILITY_KEY_ID_KEY_NAME = 'ID' |
| |
| # Types of firmware |
| _FIRMWARE_TYPE_RO = 'RO' |
| _FIRMWARE_TYPE_RW = 'RW' |
| |
| # Types of signing keys |
| _KEY_TYPE_DEV = 'dev' |
| _KEY_TYPE_PRE_MP = 'premp' |
| _KEY_TYPE_MP = 'mp' |
| |
| # EC board names for FPMCUs |
| _FP_BOARD_NAME_BLOONCHIPPER = 'bloonchipper' |
| _FP_BOARD_NAME_DARTMONKEY = 'dartmonkey' |
| _FP_BOARD_NAME_NOCTURNE = 'nocturne_fp' |
| _FP_BOARD_NAME_NAMI = 'nami_fp' |
| |
| # Map from signing key ID to type of signing key |
| _KEY_ID_MAP_ = { |
| # bloonchipper |
| '61382804da86b4156d666cc9a976088f8b647d44': _KEY_TYPE_DEV, |
| '07b1af57220c196e363e68d73a5966047c77011e': _KEY_TYPE_PRE_MP, |
| '1c590ef36399f6a2b2ef87079c135b69ef89eb60': _KEY_TYPE_MP, |
| |
| # dartmonkey |
| '257a0aa3ac9e81aa4bc3aabdb6d3d079117c5799': _KEY_TYPE_MP, |
| |
| # nocturne |
| '8a8fc039a9463271995392f079b83ce33832d07d': _KEY_TYPE_DEV, |
| '6f38c866182bd9bf7a4462c06ac04fa6a0074351': _KEY_TYPE_MP, |
| 'f6f7d96c48bd154dbae7e3fe3a3b4c6268a10934': _KEY_TYPE_PRE_MP, |
| |
| # nami |
| '754aea623d69975a22998f7b97315dd53115d723': _KEY_TYPE_PRE_MP, |
| '35486c0090ca390408f1fbbf2a182966084fe2f8': _KEY_TYPE_MP |
| |
| } |
| |
| # RO versions that are flashed in the factory |
| # (for eternity for a given board) |
| _GOLDEN_RO_FIRMWARE_VERSION_MAP = { |
| _FP_BOARD_NAME_BLOONCHIPPER: { |
| 'hatch': 'bloonchipper_v2.0.4277-9f652bb3', |
| 'zork': 'bloonchipper_v2.0.5938-197506c1', |
| }, |
| _FP_BOARD_NAME_DARTMONKEY: 'dartmonkey_v2.0.2887-311310808', |
| _FP_BOARD_NAME_NOCTURNE: 'nocturne_fp_v2.2.64-58cf5974e', |
| _FP_BOARD_NAME_NAMI: 'nami_fp_v2.2.144-7a08e07eb', |
| } |
| |
| _FIRMWARE_VERSION_SHA256SUM = 'sha256sum' |
| _FIRMWARE_VERSION_RO_VERSION = 'ro_version' |
| _FIRMWARE_VERSION_RW_VERSION = 'rw_version' |
| _FIRMWARE_VERSION_KEY_ID = 'key_id' |
| |
| # Map of attributes for a given board's various firmware file releases |
| # |
| # Two purposes: |
| # 1) Documents the exact versions and keys used for a given firmware file. |
| # 2) Used to verify that files that end up in the build (and therefore |
| # what we release) is exactly what we expect. |
| _FIRMWARE_VERSION_MAP = { |
| _FP_BOARD_NAME_BLOONCHIPPER: { |
| 'bloonchipper_v2.0.4277-9f652bb3.bin': { |
| _FIRMWARE_VERSION_SHA256SUM: '7d9b788a908bee5c83e27450258b2bbf110d7253d49faa4804562ae27e42cb3b', |
| _FIRMWARE_VERSION_RO_VERSION: 'bloonchipper_v2.0.4277-9f652bb3', |
| _FIRMWARE_VERSION_RW_VERSION: 'bloonchipper_v2.0.4277-9f652bb3', |
| _FIRMWARE_VERSION_KEY_ID: '1c590ef36399f6a2b2ef87079c135b69ef89eb60', |
| }, |
| 'bloonchipper_v2.0.5938-197506c1.bin': { |
| _FIRMWARE_VERSION_SHA256SUM: 'dc62e4b05eaf4fa8ab5546dcf18abdb30c8e64e9bf0fbf377ebc85155c7c3a47', |
| _FIRMWARE_VERSION_RO_VERSION: 'bloonchipper_v2.0.5938-197506c1', |
| _FIRMWARE_VERSION_RW_VERSION: 'bloonchipper_v2.0.5938-197506c1', |
| _FIRMWARE_VERSION_KEY_ID: '1c590ef36399f6a2b2ef87079c135b69ef89eb60', |
| }, |
| }, |
| _FP_BOARD_NAME_NOCTURNE: { |
| 'nocturne_fp_v2.2.64-58cf5974e-RO_v2.0.4017-9c45fb4b3-RW.bin': { |
| _FIRMWARE_VERSION_SHA256SUM: '16c405eeaff75dcbc76dbc9f368f66e3fabc47e2ebcf13bd2b64b8b133bbff97', |
| _FIRMWARE_VERSION_RO_VERSION: 'nocturne_fp_v2.2.64-58cf5974e', |
| _FIRMWARE_VERSION_RW_VERSION: 'nocturne_fp_v2.0.4017-9c45fb4b3', |
| _FIRMWARE_VERSION_KEY_ID: '6f38c866182bd9bf7a4462c06ac04fa6a0074351', |
| }, |
| }, |
| _FP_BOARD_NAME_NAMI: { |
| 'nami_fp_v2.2.144-7a08e07eb-RO_v2.0.4017-9c45fb4b3-RW.bin': { |
| _FIRMWARE_VERSION_SHA256SUM: '7965ea4c4371ee6d21dc462b9ed7c99078d17f4b772bec51441ca9af7d8f3a80', |
| _FIRMWARE_VERSION_RO_VERSION: 'nami_fp_v2.2.144-7a08e07eb', |
| _FIRMWARE_VERSION_RW_VERSION: 'nami_fp_v2.0.4017-9c45fb4b3', |
| _FIRMWARE_VERSION_KEY_ID: '35486c0090ca390408f1fbbf2a182966084fe2f8', |
| }, |
| }, |
| _FP_BOARD_NAME_DARTMONKEY: { |
| 'dartmonkey_v2.0.2887-311310808-RO_v2.0.4017-9c45fb4b3-RW.bin': { |
| _FIRMWARE_VERSION_SHA256SUM: 'b84914c70e93c28e2221f48be338dbf0ad0cfb12b7877baaf6b47f7bfd2aa958', |
| _FIRMWARE_VERSION_RO_VERSION: 'dartmonkey_v2.0.2887-311310808', |
| _FIRMWARE_VERSION_RW_VERSION: 'dartmonkey_v2.0.4017-9c45fb4b3', |
| _FIRMWARE_VERSION_KEY_ID: '257a0aa3ac9e81aa4bc3aabdb6d3d079117c5799', |
| } |
| } |
| } |
| |
| _BIOD_UPSTART_JOB_NAME = 'biod' |
| # TODO(crbug.com/925545) |
| _TIMBERSLIDE_UPSTART_JOB_NAME = \ |
| 'timberslide LOG_PATH=/sys/kernel/debug/cros_fp/console_log' |
| |
| _INIT_ENTROPY_CMD = 'bio_wash --factory_init' |
| |
| _CROS_FP_ARG = '--name=cros_fp' |
| _CROS_CONFIG_FINGERPRINT_PATH = '/fingerprint' |
| _ECTOOL_RO_VERSION = 'RO version' |
| _ECTOOL_RW_VERSION = 'RW version' |
| _ECTOOL_FIRMWARE_COPY = 'Firmware copy' |
| _ECTOOL_ROLLBACK_BLOCK_ID = 'Rollback block id' |
| _ECTOOL_ROLLBACK_MIN_VERSION = 'Rollback min version' |
| _ECTOOL_ROLLBACK_RW_VERSION = 'RW rollback version' |
| |
| @staticmethod |
| def _parse_colon_delimited_output(ectool_output): |
| """ |
| Converts ectool's (or any other tool with similar output) colon |
| delimited output into python dict. Ignores any lines that do not have |
| colons. |
| |
| Example: |
| RO version: nocturne_fp_v2.2.64-58cf5974e |
| RW version: nocturne_fp_v2.2.110-b936c0a3c |
| |
| becomes: |
| { |
| 'RO version': 'nocturne_fp_v2.2.64-58cf5974e', |
| 'RW version': 'nocturne_fp_v2.2.110-b936c0a3c' |
| } |
| """ |
| ret = {} |
| try: |
| for line in ectool_output.strip().split('\n'): |
| splits = line.split(':', 1) |
| if len(splits) != 2: |
| continue |
| key = splits[0].strip() |
| val = splits[1].strip() |
| ret[key] = val |
| except: |
| raise error.TestFail('Unable to parse ectool output: %s' |
| % ectool_output) |
| return ret |
| |
| def initialize(self, host): |
| """Perform minimal initialization, to avoid AttributeError in cleanup""" |
| self.host = host |
| self.servo = host.servo |
| |
| self._validate_compatible_servo_version() |
| |
| self.servo.initialize_dut() |
| |
| self.fp_board = self.get_fp_board() |
| self._build_fw_file = self.get_build_fw_file() |
| |
| def setup_test(self, test_dir, use_dev_signed_fw=False, |
| enable_hardware_write_protect=True, |
| enable_software_write_protect=True, |
| force_firmware_flashing=False, init_entropy=True): |
| """Perform more complete initialization, including copying test files""" |
| logging.info('HW write protect enabled: %s', |
| self.is_hardware_write_protect_enabled()) |
| |
| # TODO(crbug.com/925545): stop timberslide so /var/log/cros_fp.log |
| # continues to update after flashing. |
| self._timberslide_running = self.host.upstart_status( |
| self._TIMBERSLIDE_UPSTART_JOB_NAME) |
| if self._timberslide_running: |
| logging.info('Stopping %s', self._TIMBERSLIDE_UPSTART_JOB_NAME) |
| self.host.upstart_stop(self._TIMBERSLIDE_UPSTART_JOB_NAME) |
| |
| self._biod_running = self.host.upstart_status( |
| self._BIOD_UPSTART_JOB_NAME) |
| if self._biod_running: |
| logging.info('Stopping %s', self._BIOD_UPSTART_JOB_NAME) |
| self.host.upstart_stop(self._BIOD_UPSTART_JOB_NAME) |
| |
| # On some platforms an AP reboot is needed after flashing firmware to |
| # rebind the driver. |
| self._dut_needs_reboot = self.get_host_board() == 'zork' |
| |
| if filesystem_util.is_rootfs_writable(self.host): |
| if self._dut_needs_reboot: |
| logging.warning('rootfs is writable') |
| else: |
| raise error.TestFail('rootfs is writable') |
| |
| if not self.biod_upstart_job_enabled(): |
| raise error.TestFail( |
| 'Biod upstart job is disabled at the beginning of test') |
| if not self.fp_updater_is_enabled(): |
| raise error.TestFail( |
| 'Fingerprint firmware updater is disabled at the beginning of test' |
| ) |
| |
| # Disable biod and updater so that they won't interfere after reboot. |
| if self._dut_needs_reboot: |
| self.disable_biod_upstart_job() |
| self.disable_fp_updater() |
| |
| # create tmp working directory on device (automatically cleaned up) |
| self._dut_working_dir = self.host.get_tmp_dir( |
| parent=self._DUT_TMP_PATH_BASE) |
| logging.info('Created dut_working_dir: %s', self._dut_working_dir) |
| self.copy_files_to_dut(test_dir, self._dut_working_dir) |
| |
| self.validate_build_fw_file() |
| |
| gen_script = os.path.abspath(os.path.join(self.autodir, |
| 'server', 'cros', 'faft', |
| self._GENIMAGES_SCRIPT_NAME)) |
| self._dut_firmware_test_images_dir = \ |
| self._generate_test_firmware_images(gen_script, |
| self._build_fw_file, |
| self._dut_working_dir) |
| logging.info('dut_firmware_test_images_dir: %s', |
| self._dut_firmware_test_images_dir) |
| |
| self._initialize_test_firmware_image_attrs( |
| self._dut_firmware_test_images_dir) |
| |
| self._initialize_running_fw_version(use_dev_signed_fw, |
| force_firmware_flashing) |
| |
| if init_entropy: |
| self._initialize_fw_entropy() |
| |
| self._initialize_hw_and_sw_write_protect(enable_hardware_write_protect, |
| enable_software_write_protect) |
| |
| def cleanup(self): |
| """Restores original state.""" |
| # Once the tests complete we need to make sure we're running the |
| # original firmware (not dev version) and potentially reset rollback. |
| self._initialize_running_fw_version(use_dev_signed_fw=False, |
| force_firmware_flashing=False) |
| self._initialize_fw_entropy() |
| # Re-enable biod and updater after flashing and initializing entropy so |
| # that they don't interfere if there was a reboot. |
| if hasattr(self, '_dut_needs_reboot') and self._dut_needs_reboot: |
| if not self.biod_upstart_job_enabled(): |
| self.enable_biod_upstart_job() |
| if not self.fp_updater_is_enabled(): |
| self.enable_fp_updater() |
| self._initialize_hw_and_sw_write_protect( |
| enable_hardware_write_protect=True, |
| enable_software_write_protect=True) |
| if hasattr(self, '_biod_running') and self._biod_running: |
| logging.info('Restarting biod') |
| self.host.upstart_restart(self._BIOD_UPSTART_JOB_NAME) |
| # TODO(crbug.com/925545) |
| if hasattr(self, '_timberslide_running') and self._timberslide_running: |
| logging.info('Restarting timberslide') |
| self.host.upstart_restart(self._TIMBERSLIDE_UPSTART_JOB_NAME) |
| |
| super(FingerprintTest, self).cleanup() |
| |
| def after_run_once(self): |
| """Logs which iteration just ran.""" |
| logging.info('successfully ran iteration %d', self.iteration) |
| |
| def _validate_compatible_servo_version(self): |
| """Asserts if a compatible servo version is not attached.""" |
| servo_version = self.servo.get_servo_version() |
| logging.info('servo version: %s', servo_version) |
| |
| def _generate_test_firmware_images(self, gen_script, build_fw_file, |
| dut_working_dir): |
| """ |
| Copies the fingerprint firmware from the DUT to the server running |
| the tests, which runs a script to generate various test versions of |
| the firmware. |
| |
| @return full path to location of test images on DUT |
| """ |
| # create subdirectory under existing tmp dir |
| server_tmp_dir = os.path.join(self.tmpdir, |
| self._SERVER_GENERATED_FW_DIR_NAME) |
| os.mkdir(server_tmp_dir) |
| logging.info('server_tmp_dir: %s', server_tmp_dir) |
| |
| # Copy firmware from device to server |
| self.get_files_from_dut(build_fw_file, server_tmp_dir) |
| |
| # Run the test image generation script on server |
| pushd = os.getcwd() |
| os.chdir(server_tmp_dir) |
| cmd = ' '.join([gen_script, |
| self.get_fp_board(), |
| os.path.basename(build_fw_file)]) |
| result = self.run_server_cmd(cmd) |
| if result.exit_status != 0: |
| raise error.TestFail('Failed to run test image generation script') |
| |
| os.chdir(pushd) |
| |
| # Copy resulting files to DUT tmp dir |
| server_generated_images_dir = \ |
| os.path.join(server_tmp_dir, self._GENIMAGES_OUTPUT_DIR_NAME) |
| self.copy_files_to_dut(server_generated_images_dir, dut_working_dir) |
| |
| return os.path.join(dut_working_dir, self._GENIMAGES_OUTPUT_DIR_NAME) |
| |
| def _initialize_test_firmware_image_attrs(self, dut_fw_test_images_dir): |
| """Sets attributes with full path to test images on DUT. |
| |
| Example: self.TEST_IMAGE_DEV = /some/path/images/nocturne_fp.dev |
| """ |
| for key, val in self._TEST_IMAGE_FORMAT_MAP.iteritems(): |
| full_path = os.path.join(dut_fw_test_images_dir, |
| val % self.get_fp_board()) |
| setattr(self, key, full_path) |
| |
| def _initialize_running_fw_version(self, use_dev_signed_fw, |
| force_firmware_flashing): |
| """ |
| Ensures that the running firmware version matches build version |
| and factory rollback settings; flashes to correct version if either |
| fails to match is requested to force flashing. |
| |
| RO firmware: original version released at factory |
| RW firmware: firmware from current build |
| """ |
| build_rw_firmware_version = \ |
| self.get_build_rw_firmware_version(use_dev_signed_fw) |
| golden_ro_firmware_version = \ |
| self.get_golden_ro_firmware_version(use_dev_signed_fw) |
| logging.info('Build RW firmware version: %s', build_rw_firmware_version) |
| logging.info('Golden RO firmware version: %s', |
| golden_ro_firmware_version) |
| |
| running_rw_firmware = self.ensure_running_rw_firmware() |
| |
| fw_versions_match = self.running_fw_version_matches_given_version( |
| build_rw_firmware_version, golden_ro_firmware_version) |
| |
| if not running_rw_firmware or not fw_versions_match \ |
| or not self.is_rollback_set_to_initial_val() \ |
| or force_firmware_flashing: |
| fw_file = self._build_fw_file |
| if use_dev_signed_fw: |
| fw_file = self.TEST_IMAGE_DEV |
| self.flash_rw_ro_firmware(fw_file) |
| if not self.running_fw_version_matches_given_version( |
| build_rw_firmware_version, golden_ro_firmware_version): |
| raise error.TestFail( |
| 'Running firmware version does not match expected version') |
| |
| def _initialize_fw_entropy(self): |
| """Sets the entropy (key) in FPMCU flash (if not set).""" |
| result = self.run_cmd(self._INIT_ENTROPY_CMD) |
| if result.exit_status != 0: |
| raise error.TestFail('Unable to initialize entropy') |
| |
| def _initialize_hw_and_sw_write_protect(self, enable_hardware_write_protect, |
| enable_software_write_protect): |
| """Enables/disables hardware/software write protect.""" |
| # sw: 0, hw: 0 => initial_hw(0) -> sw(0) -> hw(0) |
| # sw: 0, hw: 1 => initial_hw(0) -> sw(0) -> hw(1) |
| # sw: 1, hw: 0 => initial_hw(1) -> sw(1) -> hw(0) |
| # sw: 1, hw: 1 => initial_hw(1) -> sw(1) -> hw(1) |
| hardware_write_protect_initial_enabled = True |
| if not enable_software_write_protect: |
| hardware_write_protect_initial_enabled = False |
| |
| self.set_hardware_write_protect(hardware_write_protect_initial_enabled) |
| |
| self.set_software_write_protect(enable_software_write_protect) |
| self.set_hardware_write_protect(enable_hardware_write_protect) |
| |
| def get_fp_board(self): |
| """Returns name of fingerprint EC. |
| |
| nocturne and nami are special cases and have "_fp" appended. Newer |
| FPMCUs have unique names. |
| See go/cros-fingerprint-firmware-branching-and-signing. |
| """ |
| # Use cros_config to get fingerprint board. |
| result = self._run_cros_config_cmd('board') |
| if result.exit_status != 0: |
| raise error.TestFail( |
| 'Unable to get fingerprint board with cros_config') |
| return result.stdout.rstrip() |
| |
| def get_host_board(self): |
| """Returns name of the host board.""" |
| return self.host.get_board().split(':')[-1] |
| |
| def get_build_fw_file(self): |
| """Returns full path to build FW file on DUT.""" |
| ls_cmd = 'ls %s/%s*.bin' % ( |
| self._FINGERPRINT_BUILD_FW_DIR, self.fp_board) |
| result = self.run_cmd(ls_cmd) |
| if result.exit_status != 0: |
| raise error.TestFail( |
| 'Unable to find firmware file on device:' |
| ' command failed (rc=%s): %s' |
| % (result.exit_status, result.stderr.strip() or ls_cmd)) |
| ret = result.stdout.rstrip() |
| logging.info('Build firmware file: %s', ret) |
| return ret |
| |
| def check_equal(self, a, b): |
| """Raises exception if "a" does not equal "b".""" |
| if a != b: |
| raise error.TestFail('"%s" does not match expected "%s" for board ' |
| '%s' % (a, b, self.get_fp_board())) |
| |
| def validate_build_fw_file(self, |
| allowed_types=(_KEY_TYPE_PRE_MP, _KEY_TYPE_MP)): |
| """ |
| Checks that all attributes in the given firmware file match their |
| expected values. |
| |
| @param allowed_types: If key type is something else, raise TestFail. |
| Default: pre-MP or MP. |
| @type allowed_types: tuple | list |
| """ |
| build_fw_file = self._build_fw_file |
| # check hash |
| actual_hash = self._calculate_sha256sum(build_fw_file) |
| expected_hash = self._get_expected_firmware_hash(build_fw_file) |
| self.check_equal(actual_hash, expected_hash) |
| |
| # check signing key_id |
| actual_key_id = self._read_firmware_key_id(build_fw_file) |
| expected_key_id = self._get_expected_firmware_key_id(build_fw_file) |
| self.check_equal(actual_key_id, expected_key_id) |
| |
| # check that the signing key for firmware in the build |
| # is "pre mass production" (pre-mp) or "mass production" (MP) |
| key_type = self._get_key_type(actual_key_id) |
| if key_type not in allowed_types: |
| raise error.TestFail( |
| 'Firmware key type must be %s for board %s; got %s (%s)' % |
| (' or '.join(allowed_types), self.fp_board, key_type, |
| actual_key_id)) |
| |
| # check ro_version |
| actual_ro_version = self._read_firmware_ro_version(build_fw_file) |
| expected_ro_version = \ |
| self._get_expected_firmware_ro_version(build_fw_file) |
| self.check_equal(actual_ro_version, expected_ro_version) |
| |
| # check rw_version |
| actual_rw_version = self._read_firmware_rw_version(build_fw_file) |
| expected_rw_version = \ |
| self._get_expected_firmware_rw_version(build_fw_file) |
| self.check_equal(actual_rw_version, expected_rw_version) |
| |
| logging.info("Validated build firmware metadata.") |
| |
| def _get_key_type(self, key_id): |
| """Returns the key "type" for a given "key id".""" |
| key_type = self._KEY_ID_MAP_.get(key_id) |
| if key_type is None: |
| raise error.TestFail('Unable to get key type for key id: %s' |
| % key_id) |
| return key_type |
| |
| def _get_expected_firmware_info(self, build_fw_file, info_type): |
| """ |
| Returns expected firmware info for a given firmware file name. |
| """ |
| build_fw_file_name = os.path.basename(build_fw_file) |
| |
| board = self.get_fp_board() |
| board_expected_fw_info = self._FIRMWARE_VERSION_MAP.get(board) |
| if board_expected_fw_info is None: |
| raise error.TestFail('Unable to get firmware info for board: %s' |
| % board) |
| |
| expected_fw_info = board_expected_fw_info.get(build_fw_file_name) |
| if expected_fw_info is None: |
| raise error.TestFail('Unable to get firmware info for file: %s' |
| % build_fw_file_name) |
| |
| ret = expected_fw_info.get(info_type) |
| if ret is None: |
| raise error.TestFail('Unable to get firmware info type: %s' |
| % info_type) |
| |
| return ret |
| |
| def _get_expected_firmware_hash(self, build_fw_file): |
| """Returns expected hash of firmware file.""" |
| return self._get_expected_firmware_info( |
| build_fw_file, self._FIRMWARE_VERSION_SHA256SUM) |
| |
| def _get_expected_firmware_key_id(self, build_fw_file): |
| """Returns expected "key id" for firmware file.""" |
| return self._get_expected_firmware_info( |
| build_fw_file, self._FIRMWARE_VERSION_KEY_ID) |
| |
| def _get_expected_firmware_ro_version(self, build_fw_file): |
| """Returns expected RO version for firmware file.""" |
| return self._get_expected_firmware_info( |
| build_fw_file, self._FIRMWARE_VERSION_RO_VERSION) |
| |
| def _get_expected_firmware_rw_version(self, build_fw_file): |
| """Returns expected RW version for firmware file.""" |
| return self._get_expected_firmware_info( |
| build_fw_file, self._FIRMWARE_VERSION_RW_VERSION) |
| |
| def _read_firmware_key_id(self, file_name): |
| """Returns "key id" as read from the given file.""" |
| result = self._run_futility_show_cmd(file_name) |
| parsed = self._parse_colon_delimited_output(result) |
| key_id = parsed.get(self._FUTILITY_KEY_ID_KEY_NAME) |
| if key_id is None: |
| raise error.TestFail('Failed to get key ID for file: %s' |
| % file_name) |
| return key_id |
| |
| def _read_firmware_ro_version(self, file_name): |
| """Returns RO firmware version as read from the given file.""" |
| return self._run_dump_fmap_cmd(file_name, 'RO_FRID') |
| |
| def _read_firmware_rw_version(self, file_name): |
| """Returns RW firmware version as read from the given file.""" |
| return self._run_dump_fmap_cmd(file_name, 'RW_FWID') |
| |
| def _calculate_sha256sum(self, file_name): |
| """Returns SHA256 hash of the given file contents.""" |
| result = self._run_sha256sum_cmd(file_name) |
| return result.stdout.split()[0] |
| |
| def _get_running_firmware_info(self, key): |
| """ |
| Returns requested firmware info (RW version, RO version, or firmware |
| type). |
| """ |
| result = self._run_ectool_cmd('version') |
| parsed = self._parse_colon_delimited_output(result.stdout) |
| if result.exit_status != 0: |
| raise error.TestFail('Failed to get running firmware info') |
| info = parsed.get(key) |
| if info is None: |
| raise error.TestFail( |
| 'Failed to get running firmware info: %s' % key) |
| return info |
| |
| def get_running_rw_firmware_version(self): |
| """Returns running RW firmware version.""" |
| return self._get_running_firmware_info(self._ECTOOL_RW_VERSION) |
| |
| def get_running_ro_firmware_version(self): |
| """Returns running RO firmware version.""" |
| return self._get_running_firmware_info(self._ECTOOL_RO_VERSION) |
| |
| def get_running_firmware_type(self): |
| """Returns type of firmware we are running (RW or RO).""" |
| return self._get_running_firmware_info(self._ECTOOL_FIRMWARE_COPY) |
| |
| def _get_rollback_info(self, info_type): |
| """Returns requested type of rollback info.""" |
| result = self._run_ectool_cmd('rollbackinfo') |
| parsed = self._parse_colon_delimited_output(result.stdout) |
| if result.exit_status != 0: |
| raise error.TestFail('Failed to get rollback info') |
| info = parsed.get(info_type) |
| if info is None: |
| raise error.TestFail('Failed to get rollback info: %s' % info_type) |
| return info |
| |
| def get_rollback_id(self): |
| """Returns rollback ID.""" |
| return self._get_rollback_info(self._ECTOOL_ROLLBACK_BLOCK_ID) |
| |
| def get_rollback_min_version(self): |
| """Returns rollback min version.""" |
| return self._get_rollback_info(self._ECTOOL_ROLLBACK_MIN_VERSION) |
| |
| def get_rollback_rw_version(self): |
| """Returns RW rollback version.""" |
| return self._get_rollback_info(self._ECTOOL_ROLLBACK_RW_VERSION) |
| |
| def _construct_dev_version(self, orig_version): |
| """ |
| Given a "regular" version string from a signed build, returns the |
| special "dev" version that we use when creating the test images. |
| """ |
| fw_version = orig_version |
| if len(fw_version) + len('.dev') > 31: |
| fw_version = fw_version[:27] |
| fw_version = fw_version + '.dev' |
| return fw_version |
| |
| def get_golden_ro_firmware_version(self, use_dev_signed_fw): |
| """Returns RO firmware version used in factory.""" |
| board = self.get_fp_board() |
| golden_version = self._GOLDEN_RO_FIRMWARE_VERSION_MAP.get(board) |
| if isinstance(golden_version, dict): |
| golden_version = golden_version.get(self.get_host_board()) |
| if golden_version is None: |
| raise error.TestFail('Unable to get golden RO version for board: %s' |
| % board) |
| if use_dev_signed_fw: |
| golden_version = self._construct_dev_version(golden_version) |
| return golden_version |
| |
| def get_build_rw_firmware_version(self, use_dev_signed_fw): |
| """Returns RW firmware version from build.""" |
| fw_version = self._read_firmware_rw_version(self._build_fw_file) |
| if use_dev_signed_fw: |
| fw_version = self._construct_dev_version(fw_version) |
| return fw_version |
| |
| def ensure_running_rw_firmware(self): |
| """ |
| Check whether the device is running RW firmware. If not, try rebooting |
| to RW. |
| |
| @return true if successfully verified running RW firmware, false |
| otherwise. |
| """ |
| try: |
| if self.get_running_firmware_type() != self._FIRMWARE_TYPE_RW: |
| self._reboot_ec() |
| if self.get_running_firmware_type() != self._FIRMWARE_TYPE_RW: |
| # RW may be corrupted. |
| return False |
| except: |
| # We may not always be able to read the firmware version. |
| # For example, if the firmware is erased due to RDP1, running any |
| # commands (such as getting the version) won't work. |
| return False |
| return True |
| |
| def running_fw_version_matches_given_version(self, rw_version, ro_version): |
| """ |
| Returns True if the running RO and RW firmware versions match the |
| provided versions. |
| """ |
| try: |
| running_rw_firmware_version = self.get_running_rw_firmware_version() |
| running_ro_firmware_version = self.get_running_ro_firmware_version() |
| |
| logging.info('RW firmware, running: %s, expected: %s', |
| running_rw_firmware_version, rw_version) |
| logging.info('RO firmware, running: %s, expected: %s', |
| running_ro_firmware_version, ro_version) |
| |
| return (running_rw_firmware_version == rw_version and |
| running_ro_firmware_version == ro_version) |
| except: |
| # We may not always be able to read the firmware version. |
| # For example, if the firmware is erased due to RDP1, running any |
| # commands (such as getting the version) won't work. |
| return False |
| |
| def is_rollback_set_to_initial_val(self): |
| """ |
| Returns True if rollbackinfo matches the initial value that it |
| should have coming from the factory. |
| """ |
| return (self.get_rollback_id() == |
| self._ROLLBACK_INITIAL_BLOCK_ID |
| and |
| self.get_rollback_min_version() == |
| self._ROLLBACK_INITIAL_MIN_VERSION |
| and |
| self.get_rollback_rw_version() == |
| self._ROLLBACK_INITIAL_RW_VERSION) |
| |
| def is_rollback_unset(self): |
| """ |
| Returns True if rollbackinfo matches the uninitialized value that it |
| should have after flashing the entire flash. |
| """ |
| return (self.get_rollback_id() == self._ROLLBACK_ZERO_BLOCK_ID |
| and self.get_rollback_min_version() == |
| self._ROLLBACK_INITIAL_MIN_VERSION |
| and self.get_rollback_rw_version() == |
| self._ROLLBACK_INITIAL_RW_VERSION) |
| |
| def biod_upstart_job_enabled(self): |
| """Returns whether biod's upstart job file is at original location.""" |
| return self.host.is_file_exists( |
| os.path.join(self._UPSTART_DIR, self._BIOD_UPSTART_JOB_FILE)) |
| |
| def disable_biod_upstart_job(self): |
| """ |
| Disable biod's upstart job so that biod will not run after a reboot. |
| """ |
| logging.info('Disabling biod\'s upstart job') |
| filesystem_util.make_rootfs_writable(self.host) |
| cmd = 'mv %s %s' % (os.path.join( |
| self._UPSTART_DIR, |
| self._BIOD_UPSTART_JOB_FILE), self._STATEFUL_PARTITION_DIR) |
| result = self.run_cmd(cmd) |
| if result.exit_status != 0: |
| raise error.TestFail('Unable to disable biod upstart job: %s' % |
| result.stderr.strip()) |
| |
| def enable_biod_upstart_job(self): |
| """ |
| Enable biod's upstart job so that biod will run after a reboot. |
| """ |
| logging.info('Enabling biod\'s upstart job') |
| filesystem_util.make_rootfs_writable(self.host) |
| cmd = 'mv %s %s' % (os.path.join( |
| self._STATEFUL_PARTITION_DIR, |
| self._BIOD_UPSTART_JOB_FILE), self._UPSTART_DIR) |
| result = self.run_cmd(cmd) |
| if result.exit_status != 0: |
| raise error.TestFail('Unable to enable biod upstart job: %s' % |
| result.stderr.strip()) |
| |
| def fp_updater_is_enabled(self): |
| """Returns whether the fingerprint firmware updater is disabled.""" |
| return not self.host.is_file_exists( |
| os.path.join(self._FINGERPRINT_BUILD_FW_DIR, |
| self._DISABLE_FP_UPDATER_FILE)) |
| |
| def disable_fp_updater(self): |
| """Disable the fingerprint firmware updater.""" |
| filesystem_util.make_rootfs_writable(self.host) |
| touch_cmd = 'touch %s' % os.path.join(self._FINGERPRINT_BUILD_FW_DIR, |
| self._DISABLE_FP_UPDATER_FILE) |
| logging.info('Disabling fp firmware updater') |
| result = self.run_cmd(touch_cmd) |
| if result.exit_status != 0: |
| raise error.TestFail( |
| 'Unable to write file to disable fp updater:' |
| ' command failed (rc=%s): %s' % |
| (result.exit_status, result.stderr.strip() or touch_cmd)) |
| self.run_cmd('sync') |
| |
| def enable_fp_updater(self): |
| """ |
| Enable the fingerprint firmware updater. Must be called only after |
| disable_fp_updater(). |
| """ |
| filesystem_util.make_rootfs_writable(self.host) |
| rm_cmd = 'rm %s' % os.path.join(self._FINGERPRINT_BUILD_FW_DIR, |
| self._DISABLE_FP_UPDATER_FILE) |
| logging.info('Enabling fp firmware updater') |
| result = self.run_cmd(rm_cmd) |
| if result.exit_status != 0: |
| raise error.TestFail( |
| 'Unable to rm .disable_fp_updater:' |
| ' command failed (rc=%s): %s' % |
| (result.exit_status, result.stderr.strip() or rm_cmd)) |
| self.run_cmd('sync') |
| |
| def flash_rw_ro_firmware(self, fw_path): |
| """Flashes *all* firmware (both RO and RW).""" |
| self.set_hardware_write_protect(False) |
| flash_cmd = 'flash_fp_mcu' + ' ' + fw_path |
| logging.info('Running flash cmd: %s', flash_cmd) |
| flash_result = self.run_cmd(flash_cmd) |
| self.set_hardware_write_protect(True) |
| |
| # Zork cannot rebind cros-ec-uart after flashing, so an AP reboot is |
| # needed to talk to FPMCU. See b/170213489. |
| # We have to do this even if flashing failed. |
| if hasattr(self, '_dut_needs_reboot') and self._dut_needs_reboot: |
| self.host.reboot() |
| if self.fp_updater_is_enabled(): |
| raise error.TestFail( |
| 'Fp updater was not disabled when firmware is flashed') |
| # If we just re-enable fp updater, it can still update (race |
| # condition), so do it later in cleanup. |
| |
| if flash_result.exit_status != 0: |
| raise error.TestFail('Flashing RW/RO firmware failed') |
| |
| def is_hardware_write_protect_enabled(self): |
| """Returns state of hardware write protect.""" |
| fw_wp_state = self.servo.get('fw_wp_state') |
| return fw_wp_state == 'on' or fw_wp_state == 'force_on' |
| |
| def set_hardware_write_protect(self, enable): |
| """Enables or disables hardware write protect.""" |
| self.servo.set('fw_wp_state', 'force_on' if enable else 'force_off') |
| |
| def set_software_write_protect(self, enable): |
| """Enables or disables software write protect.""" |
| arg = 'enable' if enable else 'disable' |
| self._run_ectool_cmd('flashprotect ' + arg) |
| # TODO(b/116396469): The flashprotect command returns an error even on |
| # success. |
| # if result.exit_status != 0: |
| # raise error.TestFail('Failed to modify software write protect') |
| |
| # TODO(b/116396469): "flashprotect enable" command is slow, so wait for |
| # it to complete before attempting to reboot. |
| time.sleep(2) |
| self._reboot_ec() |
| |
| def _reboot_ec(self): |
| """Reboots the fingerprint MCU (FPMCU).""" |
| self._run_ectool_cmd('reboot_ec') |
| # TODO(b/116396469): The reboot_ec command returns an error even on |
| # success. |
| # if result.exit_status != 0: |
| # raise error.TestFail('Failed to reboot ec') |
| time.sleep(2) |
| |
| def get_files_from_dut(self, src, dst): |
| """Copes files from DUT to server.""" |
| logging.info('Copying files from (%s) to (%s).', src, dst) |
| self.host.get_file(src, dst, delete_dest=True) |
| |
| def copy_files_to_dut(self, src_dir, dst_dir): |
| """Copies files from server to DUT.""" |
| logging.info('Copying files from (%s) to (%s).', src_dir, dst_dir) |
| self.host.send_file(src_dir, dst_dir, delete_dest=True) |
| # Sync the filesystem in case we need to reboot the AP soon. |
| self.run_cmd('sync') |
| |
| def run_server_cmd(self, command, timeout=60): |
| """Runs command on server; return result with output and exit code.""" |
| logging.info('Server execute: %s', command) |
| result = utils.run(command, timeout=timeout, ignore_status=True) |
| logging.info('exit_code: %d', result.exit_status) |
| logging.info('stdout:\n%s', result.stdout) |
| logging.info('stderr:\n%s', result.stderr) |
| return result |
| |
| def run_cmd(self, command, timeout=300): |
| """Runs command on the DUT; return result with output and exit code.""" |
| logging.debug('DUT Execute: %s', command) |
| result = self.host.run(command, timeout=timeout, ignore_status=True) |
| logging.info('exit_code: %d', result.exit_status) |
| logging.info('stdout:\n%s', result.stdout) |
| logging.info('stderr:\n%s', result.stderr) |
| return result |
| |
| def _run_ectool_cmd(self, command): |
| """Runs ectool on DUT; return result with output and exit code.""" |
| cmd = 'ectool ' + self._CROS_FP_ARG + ' ' + command |
| result = self.run_cmd(cmd) |
| return result |
| |
| def _run_cros_config_cmd(self, command): |
| """Runs cros_config on DUT; return result with output and exit code.""" |
| cmd = 'cros_config ' + self._CROS_CONFIG_FINGERPRINT_PATH + ' ' \ |
| + command |
| result = self.run_cmd(cmd) |
| return result |
| |
| def _run_dump_fmap_cmd(self, fw_file, section): |
| """ |
| Runs "dump_fmap" on DUT for given file. |
| Returns value of given section. |
| """ |
| # Write result to stderr while redirecting stderr to stdout |
| # and dropping stdout. This is done because dump_map only writes the |
| # value read from a section to a file (will not just print it to |
| # stdout). |
| cmd = 'dump_fmap -x ' + fw_file + ' ' + section +\ |
| ':/dev/stderr /dev/stderr >& /dev/stdout > /dev/null' |
| result = self.run_cmd(cmd) |
| if result.exit_status != 0: |
| raise error.TestFail('Failed to read section: %s' % section) |
| return result.stdout.rstrip('\0') |
| |
| def _run_futility_show_cmd(self, fw_file): |
| """ |
| Runs "futility show" on DUT for given file. |
| Returns stdout on success. |
| """ |
| futility_cmd = 'futility show ' + fw_file |
| result = self.run_cmd(futility_cmd) |
| if result.exit_status != 0: |
| raise error.TestFail('Unable to run futility on device') |
| return result.stdout |
| |
| def _run_sha256sum_cmd(self, file_name): |
| """ |
| Runs "sha256sum" on DUT for given file. |
| Returns stdout on success. |
| """ |
| sha_cmd = 'sha256sum ' + file_name |
| result = self.run_cmd(sha_cmd) |
| if result.exit_status != 0: |
| raise error.TestFail('Unable to calculate sha256sum on device') |
| return result |
| |
| def run_test(self, test_name, *args): |
| """Runs test on DUT.""" |
| logging.info('Running %s', test_name) |
| # Redirecting stderr to stdout since some commands intentionally fail |
| # and it's easier to read when everything ordered in the same output |
| test_cmd = ' '.join([os.path.join(self._dut_working_dir, test_name)] + |
| list(args) + ['2>&1']) |
| # Change the working dir so we can write files from within the test |
| # (otherwise defaults to $HOME (/root), which is not usually writable) |
| # Note that dut_working_dir is automatically cleaned up so tests don't |
| # need to worry about files from previous invocations or other tests. |
| test_cmd = '(cd ' + self._dut_working_dir + ' && ' + test_cmd + ')' |
| logging.info('Test command: %s', test_cmd) |
| result = self.run_cmd(test_cmd) |
| if result.exit_status != 0: |
| raise error.TestFail(test_name + ' failed') |