blob: 5e7aa8fb5eb5cd39b37803b308e4394a3c3d1f94 [file] [log] [blame]
# Copyright (c) 2014 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.
from __future__ import print_function
import ctypes
import logging
import os
import pprint
import re
import StringIO
import time
import uuid
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib import global_config
from autotest_lib.client.common_lib.cros import retry
from autotest_lib.client.common_lib.cros import tpm_utils
from autotest_lib.server import test
from autotest_lib.server.cros import vboot_constants as vboot
from autotest_lib.server.cros.faft.utils.config import Config as FAFTConfig
from autotest_lib.server.cros.faft.rpc_proxy import RPCProxy
from autotest_lib.server.cros.faft.utils import mode_switcher
from autotest_lib.server.cros.faft.utils.faft_checkers import FAFTCheckers
from autotest_lib.server.cros.power import utils as PowerUtils
from autotest_lib.server.cros.servo import chrome_base_ec
from autotest_lib.server.cros.servo import chrome_cr50
from autotest_lib.server.cros.servo import chrome_ec
from autotest_lib.server.cros.servo import servo
from autotest_lib.server.cros.faft import telemetry
# Experimentally tuned time in minutes to wait for partition device nodes on a
# USB stick to be ready after plugging in the stick.
PARTITION_TABLE_READINESS_TIMEOUT = 0.1 # minutes
# Experimentally tuned time in seconds to wait for the first retry of reading
# the sysfs node of a USB stick's partition device node.
PARTITION_TABLE_READINESS_FIRST_RETRY_DELAY = 1 # seconds
ConnectionError = mode_switcher.ConnectionError
class FirmwareTest(test.test):
"""
Base class that sets up helper objects/functions for firmware tests.
It launches the FAFTClient on DUT, such that the test can access its
firmware functions and interfaces. It also provides some methods to
handle the reboot mechanism, in order to ensure FAFTClient is still
connected after reboot.
@type servo: servo.Servo
@type _client: autotest_lib.server.hosts.ssh_host.SSHHost |
autotest_lib.server.hosts.cros_host.CrosHost
TODO: add documentaion as the FAFT rework progresses.
"""
version = 1
# Set this to True in test classes that need to boot from the USB stick.
# When True, initialize() will raise TestWarn if USB stick is marked bad.
NEEDS_SERVO_USB = False
# Mapping of partition number of kernel and rootfs.
KERNEL_MAP = {'a':'2', 'b':'4', '2':'2', '4':'4', '3':'2', '5':'4'}
ROOTFS_MAP = {'a':'3', 'b':'5', '2':'3', '4':'5', '3':'3', '5':'5'}
OTHER_KERNEL_MAP = {'a':'4', 'b':'2', '2':'4', '4':'2', '3':'4', '5':'2'}
OTHER_ROOTFS_MAP = {'a':'5', 'b':'3', '2':'5', '4':'3', '3':'5', '5':'3'}
CHROMEOS_MAGIC = "CHROMEOS"
CORRUPTED_MAGIC = "CORRUPTD"
# System power states
POWER_STATE_S0 = 'S0'
POWER_STATE_S0IX = 'S0ix'
POWER_STATE_S3 = 'S3'
POWER_STATE_S5 = 'S5'
POWER_STATE_G3 = 'G3'
POWER_STATE_SUSPEND = '|'.join([POWER_STATE_S0IX, POWER_STATE_S3])
# Delay for waiting client to return before EC suspend
EC_SUSPEND_DELAY = 5
# Delay between EC suspend and wake
WAKE_DELAY = 10
# Delay between closing and opening lid
LID_DELAY = 1
# Delay for establishing state after changing PD settings
PD_RESYNC_DELAY = 2
# The default number of power state check retries (each try takes 3 secs)
DEFAULT_PWR_RETRIES = 5
# FWMP space constants
FWMP_CLEARED_EXIT_STATUS = 1
FWMP_CLEARED_ERROR_MSG = ('CRYPTOHOME_ERROR_FIRMWARE_MANAGEMENT_PARAMETERS'
'_INVALID')
_ROOTFS_PARTITION_NUMBER = 3
# Class level variable, keep track the states of one time setup.
# This variable is preserved across tests which inherit this class.
_global_setup_done = {
'gbb_flags': False,
'reimage': False,
'usb_check': False,
}
# CCD password used by tests.
CCD_PASSWORD = 'Password'
RESPONSE_TIMEOUT = 180
@classmethod
def check_setup_done(cls, label):
"""Check if the given setup is done.
@param label: The label of the setup.
"""
return cls._global_setup_done[label]
@classmethod
def mark_setup_done(cls, label):
"""Mark the given setup done.
@param label: The label of the setup.
"""
cls._global_setup_done[label] = True
@classmethod
def unmark_setup_done(cls, label):
"""Mark the given setup not done.
@param label: The label of the setup.
"""
cls._global_setup_done[label] = False
def initialize(self, host, cmdline_args, ec_wp=None):
"""Initialize the FirmwareTest.
This method interacts with the Servo, FAFT RPC client, FAFT Config,
Mode Switcher, EC consoles, write-protection, GBB flags, and a lockfile.
@type host: autotest_lib.server.hosts.CrosHost
"""
self.run_id = str(uuid.uuid4())
self._client = host
self.servo = host.servo
self.lockfile = '/usr/local/tmp/faft/lock'
self._backup_gbb_flags = None
self._backup_firmware_identity = dict()
self._backup_kernel_sha = dict()
self._backup_cgpt_attr = dict()
self._backup_dev_mode = None
self._restore_power_mode = None
self._uart_file_dict = {}
logging.info('FirmwareTest initialize begin (id=%s)', self.run_id)
# Parse arguments from command line
args = {}
self.power_control = host.POWER_CONTROL_RPM
for arg in cmdline_args:
match = re.search("^(\w+)=(.+)", arg)
if match:
args[match.group(1)] = match.group(2)
self._no_fw_rollback_check = False
if 'no_fw_rollback_check' in args:
if 'true' in args['no_fw_rollback_check'].lower():
self._no_fw_rollback_check = True
self._no_ec_sync = False
if 'no_ec_sync' in args:
if 'true' in args['no_ec_sync'].lower():
self._no_ec_sync = True
self._use_sync_script = global_config.global_config.get_config_value(
'CROS', 'enable_fs_sync_script', type=bool, default=False)
self.servo.initialize_dut()
self.faft_client = RPCProxy(host)
self.faft_config = FAFTConfig(
self.faft_client.system.get_platform_name(),
self.faft_client.system.get_model_name())
self.checkers = FAFTCheckers(self)
if self.faft_config.chrome_ec:
self.ec = chrome_ec.ChromeEC(self.servo)
self.switcher = mode_switcher.create_mode_switcher(self)
# Check for presence of a USBPD console
if self.faft_config.chrome_usbpd:
self.usbpd = chrome_ec.ChromeUSBPD(self.servo)
elif self.faft_config.chrome_ec:
# If no separate USBPD console, then PD exists on EC console
self.usbpd = self.ec
# Get pdtester console
self.pdtester = host.pdtester
self.pdtester_host = host._pdtester_host
# Check for presence of a working Cr50 console
if self.servo.has_control('cr50_version'):
try:
# Check that the console works before declaring the cr50 console
# connection exists and enabling uart capture.
cr50 = chrome_cr50.ChromeCr50(self.servo, self.faft_config)
cr50.get_version()
self.cr50 = cr50
except servo.ControlUnavailableError:
logging.warn('cr50 console not supported.')
except Exception as e:
logging.warn('Ignored unknown cr50 version error: %s', str(e))
if 'power_control' in args:
self.power_control = args['power_control']
if self.power_control not in host.POWER_CONTROL_VALID_ARGS:
raise error.TestError('Valid values for --args=power_control '
'are %s. But you entered wrong argument '
'as "%s".'
% (host.POWER_CONTROL_VALID_ARGS,
self.power_control))
if self.NEEDS_SERVO_USB and not host.is_servo_usb_usable():
usb_state = host.get_servo_usb_state()
raise error.TestWarn(
"Servo USB disk unusable (%s); canceling test." %
usb_state)
if not self.faft_client.system.dev_tpm_present():
raise error.TestError('/dev/tpm0 does not exist on the client')
# Initialize servo role to src
self.servo.set_servo_v4_role('src')
# Create the BaseEC object. None if not available.
self.base_ec = chrome_base_ec.create_base_ec(self.servo)
self._record_uart_capture()
self._record_system_info()
self.faft_client.system.set_dev_default_boot()
self.fw_vboot2 = self.faft_client.system.get_fw_vboot2()
logging.info('vboot version: %d', 2 if self.fw_vboot2 else 1)
if self.fw_vboot2:
self.faft_client.system.set_fw_try_next('A')
if self.faft_client.system.get_crossystem_value(
'mainfw_act') == 'B':
logging.info('mainfw_act is B. rebooting to set it A')
# TODO(crbug.com/1018322): remove try/catch once that bug is
# marked as fixed and verified. In that case the overlay for
# the board itself will map warm_reset to cold_reset.
try:
self.switcher.mode_aware_reboot()
except ConnectionError as e:
if 'DUT is still up unexpectedly' in str(e):
# In this case, try doing a cold_reset instead
self.switcher.mode_aware_reboot(reboot_type='cold')
else:
raise
# Check flashrom before first use, to avoid xmlrpclib.Fault.
if not self.faft_client.bios.is_available():
raise error.TestError(
"flashrom is broken; check 'flashrom -p host'"
"and rpc server log.")
self._setup_gbb_flags()
self.faft_client.updater.stop_daemon()
self._create_faft_lockfile()
self._create_old_faft_lockfile()
self._setup_ec_write_protect(ec_wp)
# See chromium:239034 regarding needing this sync.
self.blocking_sync()
logging.info('FirmwareTest initialize done (id=%s)', self.run_id)
def stage_build_to_usbkey(self):
"""Downloads host's build to the USB key attached to servo.
@return: True if build is verified to be on USB key, False otherwise.
"""
info = self._client.host_info_store.get()
if info.build:
current_build = self._client._servo_host.validate_image_usbkey()
if current_build != info.build:
logging.debug('Current build on USB: %s differs from test'
' build: %s, proceed with download.',
current_build, info.build)
try:
self._client.stage_build_to_usb(info.build)
return True
except error.AutotestError as e:
logging.warn('Stage build to USB failed, tests that require'
' test image on Servo USB may fail: {}'.format(e))
return False
else:
logging.debug('Current build on USB: %s is same as test'
' build, skip download.', current_build)
return True
else:
logging.warn('Failed to get build label from the DUT, will use'
' existing image in Servo USB.')
return False
def run_once(self, *args, **dargs):
"""Delegates testing to a test method.
test_name is either the 1st positional argument or a named argument.
test_name will be mapped to a test method as follows:
test_name method
-------------- -----------
<TestClass> test
<TestClass>.<Case> test_<Case>
<TestClass>.<Case>.<SubCase> test_<Case>_<SubCase>
Any arguments not consumed by FirmwareTest are passed to the test method.
@param test_name: Should be set to NAME in the control file.
@raise TestError: If test_name wasn't found in args, does not start
with test class, or if the method is not found.
"""
self_name = type(self).__name__
# Parse and remove test name from args.
if 'test_name' in dargs:
test_name = dargs.pop('test_name')
elif len(args) >= 1:
test_name = args[0]
args = args[1:]
else:
raise error.TestError('"%s" class must define run_once, or the'
' control file must specify "test_name".' %
self_name)
# Check that test_name starts with the test class name.
name_parts = test_name.split('.')
test_class = name_parts.pop(0)
if test_class != self_name:
raise error.TestError('Class "%s" does not match that found in test'
' name "%s"' % (self_name, test_class))
# Construct and call the test method.
method_name = '_'.join(['test'] + name_parts)
if not hasattr(self, method_name):
raise error.TestError('Method "%s" for testing "%s" not found in'
' "%s"' % (method_name, test_name, self_name))
logging.info('Starting test: "%s"', test_name)
utils.cherry_pick_call(getattr(self, method_name), *args, **dargs)
def cleanup(self):
"""Autotest cleanup function."""
# Unset state checker in case it's set by subclass
logging.info('FirmwareTest cleaning up (id=%s)', self.run_id)
# Capture UART before doing anything else, so we can guarantee we get
# some uart results.
try:
self._record_uart_capture()
except:
logging.warn('Failed initial uart capture during cleanup')
try:
self.faft_client.system.is_available()
except:
# Remote is not responding. Revive DUT so that subsequent tests
# don't fail.
self._restore_routine_from_timeout()
if hasattr(self, 'switcher'):
self.switcher.restore_mode()
self._restore_ec_write_protect()
self._restore_servo_v4_role()
if hasattr(self, 'faft_client'):
self._restore_gbb_flags()
self.faft_client.updater.start_daemon()
self.faft_client.updater.cleanup()
self._remove_faft_lockfile()
self._remove_old_faft_lockfile()
self._record_faft_client_log()
self.faft_client.quit()
# Capture any new uart output, then discard log messages again.
self._cleanup_uart_capture()
super(FirmwareTest, self).cleanup()
logging.info('FirmwareTest cleanup done (id=%s)', self.run_id)
def _record_system_info(self):
"""Record some critical system info to the attr keyval.
This info is used by generate_test_report later.
"""
system_info = {
'hwid': self.faft_client.system.get_crossystem_value('hwid'),
'ec_version': self.faft_client.ec.get_version(),
'ro_fwid': self.faft_client.system.get_crossystem_value('ro_fwid'),
'rw_fwid': self.faft_client.system.get_crossystem_value('fwid'),
'servo_host_os_version' : self.servo.get_os_version(),
'servod_version': self.servo.get_servod_version(),
'os_version': self._client.get_release_builder_path(),
'servo_type': self.servo.get_servo_version()
}
# Record the servo v4 and servo micro versions when possible
system_info.update(self.servo.get_servo_fw_versions())
if hasattr(self, 'cr50'):
system_info['cr50_version'] = self.cr50.get_full_version()
logging.info('System info:\n%s', pprint.pformat(system_info))
self.write_attr_keyval(system_info)
def invalidate_firmware_setup(self):
"""Invalidate all firmware related setup state.
This method is called when the firmware is re-flashed. It resets all
firmware related setup states so that the next test setup properly
again.
"""
self.unmark_setup_done('gbb_flags')
def _retrieve_recovery_reason_from_trap(self):
"""Try to retrieve the recovery reason from a trapped recovery screen.
@return: The recovery_reason, 0 if any error.
"""
recovery_reason = 0
logging.info('Try to retrieve recovery reason...')
if self.servo.get_usbkey_state() == 'dut':
self.switcher.bypass_rec_mode()
else:
self.servo.switch_usbkey('dut')
try:
self.switcher.wait_for_client()
lines = self.faft_client.system.run_shell_command_get_output(
'crossystem recovery_reason')
recovery_reason = int(lines[0])
logging.info('Got the recovery reason %d.', recovery_reason)
except ConnectionError:
logging.error('Failed to get the recovery reason due to connection '
'error.')
return recovery_reason
def _reset_client(self):
"""Reset client to a workable state.
This method is called when the client is not responsive. It may be
caused by the following cases:
- halt on a firmware screen without timeout, e.g. REC_INSERT screen;
- corrupted firmware;
- corrutped OS image.
"""
# DUT may halt on a firmware screen. Try cold reboot.
logging.info('Try cold reboot...')
self.switcher.mode_aware_reboot(reboot_type='cold',
sync_before_boot=False,
wait_for_dut_up=False)
self.switcher.wait_for_client_offline()
self.switcher.bypass_dev_mode()
try:
self.switcher.wait_for_client()
return
except ConnectionError:
logging.warn('Cold reboot doesn\'t help, still connection error.')
# DUT may be broken by a corrupted firmware. Restore firmware.
# We assume the recovery boot still works fine. Since the recovery
# code is in RO region and all FAFT tests don't change the RO region
# except GBB.
if self.is_firmware_saved():
self._ensure_client_in_recovery()
logging.info('Try restore the original firmware...')
if self.is_firmware_changed():
try:
self.restore_firmware()
return
except ConnectionError:
logging.warn('Restoring firmware doesn\'t help, still '
'connection error.')
# Perhaps it's kernel that's broken. Let's try restoring it.
if self.is_kernel_saved():
self._ensure_client_in_recovery()
logging.info('Try restore the original kernel...')
if self.is_kernel_changed():
try:
self.restore_kernel()
return
except ConnectionError:
logging.warn('Restoring kernel doesn\'t help, still '
'connection error.')
# DUT may be broken by a corrupted OS image. Restore OS image.
self._ensure_client_in_recovery()
logging.info('Try restore the OS image...')
self.faft_client.system.run_shell_command('chromeos-install --yes')
self.switcher.mode_aware_reboot(wait_for_dut_up=False)
self.switcher.wait_for_client_offline()
self.switcher.bypass_dev_mode()
try:
self.switcher.wait_for_client()
logging.info('Successfully restore OS image.')
return
except ConnectionError:
logging.warn('Restoring OS image doesn\'t help, still connection '
'error.')
def _ensure_client_in_recovery(self):
"""Ensure client in recovery boot; reboot into it if necessary.
@raise TestError: if failed to boot the USB image.
"""
logging.info('Try boot into USB image...')
self.switcher.reboot_to_mode(to_mode='rec', sync_before_boot=False,
wait_for_dut_up=False)
self.servo.switch_usbkey('host')
self.switcher.bypass_rec_mode()
try:
self.switcher.wait_for_client()
except ConnectionError:
raise error.TestError('Failed to boot the USB image.')
def _restore_routine_from_timeout(self):
"""A routine to try to restore the system from a timeout error.
This method is called when FAFT failed to connect DUT after reboot.
@raise TestFail: This exception is already raised, with a decription
why it failed.
"""
# DUT is disconnected. Capture the UART output for debug.
self._record_uart_capture()
# TODO(waihong@chromium.org): Implement replugging the Ethernet to
# identify if it is a network flaky.
recovery_reason = self._retrieve_recovery_reason_from_trap()
# Reset client to a workable state.
self._reset_client()
# Raise the proper TestFail exception.
if recovery_reason:
raise error.TestFail('Trapped in the recovery screen (reason: %d) '
'and timed out' % recovery_reason)
else:
raise error.TestFail('Timed out waiting for DUT reboot')
def assert_test_image_in_usb_disk(self, usb_dev=None):
"""Assert an USB disk plugged-in on servo and a test image inside.
@param usb_dev: A string of USB stick path on the host, like '/dev/sdc'.
If None, it is detected automatically.
@raise TestError: if USB disk not detected or not a test image.
"""
if self.check_setup_done('usb_check'):
return
if usb_dev:
assert self.servo.get_usbkey_state() == 'host'
else:
self.servo.switch_usbkey('host')
usb_dev = self.servo.probe_host_usb_dev()
if not usb_dev:
raise error.TestError(
'An USB disk should be plugged in the servo board. %s' %
telemetry.collect_usb_state(self.servo))
rootfs = '%s%s' % (usb_dev, self._ROOTFS_PARTITION_NUMBER)
logging.info('usb dev is %s', usb_dev)
tmpd = self.servo.system_output('mktemp -d -t usbcheck.XXXX')
# After the USB key is muxed from the DUT to the servo host, there
# appears to be a delay between when servod can confirm that a sysfs
# entry exists for the disk (as done by probe_host_usb_dev) and when
# sysfs entries get populated for the disk's partitions.
@retry.retry(error.AutoservRunError,
timeout_min=PARTITION_TABLE_READINESS_TIMEOUT,
delay_sec=PARTITION_TABLE_READINESS_FIRST_RETRY_DELAY)
def confirm_rootfs_partition_device_node_readable():
"""Repeatedly poll for the RootFS partition sysfs node."""
self.servo.system('ls {}'.format(rootfs))
# Incremental rollout of a large scale test change.
# TODO(kmshelton): Rollout to all platforms.
if self.faft_config.platform.lower() in ['coral', 'nami']:
try:
confirm_rootfs_partition_device_node_readable()
except error.AutoservRunError as e:
usb_info = telemetry.collect_usb_state(self.servo)
raise error.TestError((
'Could not ls the device node for the RootFS on the USB '
'device. %s: %s\nMore telemetry: %s') %
(type(e).__name__, e, usb_info))
try:
self.servo.system('mount -o ro %s %s' % (rootfs, tmpd))
except error.AutoservRunError as e:
usb_info = telemetry.collect_usb_state(self.servo)
raise error.TestError(
('Could not mount the partition on USB device. %s: %s\n'
'More telemetry: %s') % (type(e).__name__, e, usb_info))
try:
usb_lsb = self.servo.system_output('cat %s' %
os.path.join(tmpd, 'etc/lsb-release'))
logging.debug('Dumping lsb-release on USB stick:\n%s', usb_lsb)
dut_lsb = '\n'.join(self.faft_client.system.
run_shell_command_get_output('cat /etc/lsb-release'))
logging.debug('Dumping lsb-release on DUT:\n%s', dut_lsb)
if not re.search(r'RELEASE_TRACK=.*test', usb_lsb):
raise error.TestError('USB stick in servo is no test image')
usb_board = re.search(r'BOARD=(.*)', usb_lsb).group(1)
dut_board = re.search(r'BOARD=(.*)', dut_lsb).group(1)
if usb_board != dut_board:
raise error.TestError('USB stick in servo contains a %s '
'image, but DUT is a %s' % (usb_board, dut_board))
finally:
for cmd in ('umount -l %s' % tmpd, 'sync', 'rm -rf %s' % tmpd):
self.servo.system(cmd)
self.mark_setup_done('usb_check')
def setup_pdtester(self, flip_cc=False, dts_mode=False, pd_faft=True,
min_batt_level=None):
"""Setup the PDTester to a given state.
@param flip_cc: True to flip CC polarity; False to not flip it.
@param dts_mode: True to config PDTester to DTS mode; False to not.
@param pd_faft: True to config PD FAFT setup.
@param min_batt_level: An int for minimum battery level, or None for
skip.
@raise TestError: If Servo v4 not setup properly.
"""
# PD FAFT is only tested with a least a servo V4 with servo micro
# or C2D2.
if pd_faft and (
'servo_v4_with_servo_micro' not in self.pdtester.servo_type
) and ('servo_v4_with_c2d2' not in self.pdtester.servo_type):
raise error.TestError('servo_v4_with_servo_micro or '
'servo_v4_with_c2d2 is a mandatory setup '
'for PD FAFT. Got %s.' %
self.pdtester.servo_type)
# Ensure the battery is enough for testing, this should be done before
# all the following setup.
if (min_batt_level is not None) and self._client.has_battery():
logging.info('Start charging if batt level < %d', min_batt_level)
PowerUtils.put_host_battery_in_range(self._client, min_batt_level,
100, 600)
# Servo v4 by default has dts_mode enabled. Enabling dts_mode affects
# the behaviors of what PD FAFT tests. So we want it disabled.
if 'servo_v4' in self.pdtester.servo_type:
self.servo.set_dts_mode('on' if dts_mode else 'off')
else:
logging.warn('Configuring DTS mode only supported on Servo v4')
self.pdtester.set('usbc_polarity', 'cc2' if flip_cc else 'cc1')
# Make it sourcing max voltage.
self.pdtester.charge(self.pdtester.USBC_MAX_VOLTAGE)
time.sleep(self.PD_RESYNC_DELAY)
# Servo v4 requires an external charger to source power. Make sure
# this setup is correct.
if 'servo_v4' in self.pdtester.servo_type:
role = self.pdtester.get('servo_v4_role')
if role != 'src':
raise error.TestError(
'Servo v4 is not sourcing power! Make sure the servo '
'"DUT POWER" port is connected to a working charger. '
'servo_v4_role:%s' % role)
def setup_usbkey(self, usbkey, host=None, used_for_recovery=None):
"""Setup the USB disk for the test.
It checks the setup of USB disk and a valid ChromeOS test image inside.
It also muxes the USB disk to either the host or DUT by request.
@param usbkey: True if the USB disk is required for the test, False if
not required.
@param host: Optional, True to mux the USB disk to host, False to mux it
to DUT, default to do nothing.
@param used_for_recovery: Optional, True if the USB disk is used for
recovery boot; False if the USB disk is not
used for recovery boot, like Ctrl-U USB boot.
"""
if usbkey:
self.stage_build_to_usbkey()
self.assert_test_image_in_usb_disk()
elif host is None:
# USB disk is not required for the test. Better to mux it to host.
host = True
if host is True:
self.servo.switch_usbkey('host')
elif host is False:
self.servo.switch_usbkey('dut')
if used_for_recovery is None:
# Default value is True if usbkey == True.
# As the common usecase of USB disk is for recovery boot. Tests
# can define it explicitly if not.
used_for_recovery = usbkey
if used_for_recovery:
# In recovery boot, the locked EC RO doesn't support PD for most
# of the CrOS devices. The default servo v4 power role is a SRC.
# The DUT becomes a SNK. Lack of PD makes CrOS unable to do the
# data role swap from UFP to DFP; as a result, DUT can't see the
# USB disk and the Ethernet dongle on servo v4.
#
# This is a workaround to set servo v4 as a SNK, for every FAFT
# test which boots into the USB disk in the recovery mode.
#
# TODO(waihong): Add a check to see if the battery level is too
# low and sleep for a while for charging.
self.set_servo_v4_role_to_snk()
def set_servo_v4_role_to_snk(self, pd_comm=False):
"""Set the servo v4 role to SNK.
@param pd_comm: a bool. Enable PD communication if True, else otherwise
"""
self._needed_restore_servo_v4_role = True
self.servo.set_servo_v4_role('snk')
if pd_comm:
self.servo.set_servo_v4_pd_comm('on')
def _restore_servo_v4_role(self):
"""Restore the servo v4 role to default SRC."""
if not hasattr(self, '_needed_restore_servo_v4_role'):
return
if self._needed_restore_servo_v4_role:
self.servo.set_servo_v4_role('src')
def set_dut_low_power_idle_delay(self, delay):
"""Set EC low power idle delay
@param delay: Delay in seconds
"""
if not self.ec.has_command('dsleep'):
logging.info("Can't set low power idle delay.")
return
self._previous_ec_low_power_delay = int(
self.ec.send_command_get_output("dsleep",
["timeout:\s+(\d+)\ssec"])[0][1])
self.ec.send_command("dsleep " + str(delay))
def restore_dut_low_power_idle_delay(self):
"""Restore EC low power idle delay"""
if getattr(self, '_previous_ec_low_power_delay', None):
self.ec.send_command("dsleep " + str(
self._previous_ec_low_power_delay))
def get_usbdisk_path_on_dut(self):
"""Get the path of the USB disk device plugged-in the servo on DUT.
Returns:
A string representing USB disk path, like '/dev/sdb', or None if
no USB disk is found.
"""
cmd = 'ls -d /dev/s*[a-z]'
original_value = self.servo.get_usbkey_state()
# Make the dut unable to see the USB disk.
self.servo.switch_usbkey('off')
time.sleep(self.faft_config.usb_unplug)
no_usb_set = set(
self.faft_client.system.run_shell_command_get_output(cmd))
# Make the dut able to see the USB disk.
self.servo.switch_usbkey('dut')
time.sleep(self.faft_config.usb_plug)
has_usb_set = set(
self.faft_client.system.run_shell_command_get_output(cmd))
# Back to its original value.
if original_value != self.servo.get_usbkey_state():
self.servo.switch_usbkey(original_value)
diff_set = has_usb_set - no_usb_set
if len(diff_set) == 1:
return diff_set.pop()
else:
return None
def _create_faft_lockfile(self):
"""Creates the FAFT lockfile."""
logging.info('Creating FAFT lockfile...')
command = 'touch %s' % (self.lockfile)
self.faft_client.system.run_shell_command(command)
def _create_old_faft_lockfile(self):
"""
Creates the FAFT lockfile in its legacy location.
TODO (once M83 is stable, approx. June 9 2020):
Delete this function, as platform/installer/chromeos-setgoodkernel
will look for the lockfile in the new location
(/usr/local/tmp/faft/lock)
"""
logging.info('Creating legacy FAFT lockfile...')
self.faft_client.system.run_shell_command('mkdir -p /var/tmp/faft')
self.faft_client.system.run_shell_command('touch /var/tmp/faft/lock')
def _remove_faft_lockfile(self):
"""Removes the FAFT lockfile."""
logging.info('Removing FAFT lockfile...')
command = 'rm -f %s' % (self.lockfile)
self.faft_client.system.run_shell_command(command)
def _remove_old_faft_lockfile(self):
"""
Removes the FAFT lockfile from its legacy location.
TODO (once M83 is stable, approx. June 9 2020):
Delete this function, as platform/installer/chromeos-setgoodkernel
will look for the lockfile in the new location
(/usr/local/tmp/faft/lock)
"""
logging.info('Removing legacy FAFT lockfile...')
self.faft_client.system.run_shell_command('rm -rf /var/tmp/faft')
def clear_set_gbb_flags(self, clear_mask, set_mask):
"""Clear and set the GBB flags in the current flashrom.
@param clear_mask: A mask of flags to be cleared.
@param set_mask: A mask of flags to be set.
"""
gbb_flags = self.faft_client.bios.get_gbb_flags()
new_flags = gbb_flags & ctypes.c_uint32(~clear_mask).value | set_mask
self.gbb_flags = new_flags
if new_flags != gbb_flags:
self._backup_gbb_flags = gbb_flags
logging.info('Changing GBB flags from 0x%x to 0x%x.',
gbb_flags, new_flags)
self.faft_client.bios.set_gbb_flags(new_flags)
# If changing FORCE_DEV_SWITCH_ON or DISABLE_EC_SOFTWARE_SYNC flag,
# reboot to get a clear state
if ((gbb_flags ^ new_flags) &
(vboot.GBB_FLAG_FORCE_DEV_SWITCH_ON |
vboot.GBB_FLAG_DISABLE_EC_SOFTWARE_SYNC)):
self.switcher.mode_aware_reboot()
else:
logging.info('Current GBB flags look good for test: 0x%x.',
gbb_flags)
def _check_capability(self, target, required_cap, suppress_warning):
"""Check if current platform has required capabilities for the target.
@param required_cap: A list containing required capabilities.
@param suppress_warning: True to suppress any warning messages.
@return: True if requirements are met. Otherwise, False.
"""
if not required_cap:
return True
if target not in ['ec', 'cr50']:
raise error.TestError('Invalid capability target %r' % target)
for cap in required_cap:
if cap not in getattr(self.faft_config, target + '_capability'):
if not suppress_warning:
logging.warn('Requires %s capability "%s" to run this '
'test.', target, cap)
return False
return True
def check_ec_capability(self, required_cap=None, suppress_warning=False):
"""Check if current platform has required EC capabilities.
@param required_cap: A list containing required EC capabilities. Pass in
None to only check for presence of Chrome EC.
@param suppress_warning: True to suppress any warning messages.
@return: True if requirements are met. Otherwise, False.
"""
if not self.faft_config.chrome_ec:
if not suppress_warning:
logging.warn('Requires Chrome EC to run this test.')
return False
return self._check_capability('ec', required_cap, suppress_warning)
def check_cr50_capability(self, required_cap=None, suppress_warning=False):
"""Check if current platform has required Cr50 capabilities.
@param required_cap: A list containing required Cr50 capabilities. Pass
in None to only check for presence of cr50 uart.
@param suppress_warning: True to suppress any warning messages.
@return: True if requirements are met. Otherwise, False.
"""
if not hasattr(self, 'cr50'):
if not suppress_warning:
logging.warn('Requires Chrome Cr50 to run this test.')
return False
return self._check_capability('cr50', required_cap, suppress_warning)
def check_root_part_on_non_recovery(self, part):
"""Check the partition number of root device and on normal/dev boot.
@param part: A string of partition number, e.g.'3'.
@return: True if the root device matched and on normal/dev boot;
otherwise, False.
"""
return self.checkers.root_part_checker(part) and \
self.checkers.crossystem_checker({
'mainfw_type': ('normal', 'developer'),
})
def _join_part(self, dev, part):
"""Return a concatenated string of device and partition number.
@param dev: A string of device, e.g.'/dev/sda'.
@param part: A string of partition number, e.g.'3'.
@return: A concatenated string of device and partition number,
e.g.'/dev/sda3'.
>>> seq = FirmwareTest()
>>> seq._join_part('/dev/sda', '3')
'/dev/sda3'
>>> seq._join_part('/dev/mmcblk0', '2')
'/dev/mmcblk0p2'
"""
if 'mmcblk' in dev:
return dev + 'p' + part
elif 'nvme' in dev:
return dev + 'p' + part
else:
return dev + part
def copy_kernel_and_rootfs(self, from_part, to_part):
"""Copy kernel and rootfs from from_part to to_part.
@param from_part: A string of partition number to be copied from.
@param to_part: A string of partition number to be copied to.
"""
root_dev = self.faft_client.system.get_root_dev()
logging.info('Copying kernel from %s to %s. Please wait...',
from_part, to_part)
self.faft_client.system.run_shell_command('dd if=%s of=%s bs=4M' %
(self._join_part(root_dev, self.KERNEL_MAP[from_part]),
self._join_part(root_dev, self.KERNEL_MAP[to_part])))
logging.info('Copying rootfs from %s to %s. Please wait...',
from_part, to_part)
self.faft_client.system.run_shell_command('dd if=%s of=%s bs=4M' %
(self._join_part(root_dev, self.ROOTFS_MAP[from_part]),
self._join_part(root_dev, self.ROOTFS_MAP[to_part])))
def ensure_kernel_boot(self, part):
"""Ensure the request kernel boot.
If not, it duplicates the current kernel to the requested kernel
and sets the requested higher priority to ensure it boot.
@param part: A string of kernel partition number or 'a'/'b'.
"""
if not self.checkers.root_part_checker(part):
if self.faft_client.kernel.diff_a_b():
self.copy_kernel_and_rootfs(
from_part=self.OTHER_KERNEL_MAP[part],
to_part=part)
self.reset_and_prioritize_kernel(part)
self.switcher.mode_aware_reboot()
def ensure_dev_internal_boot(self, original_dev_boot_usb):
"""Ensure internal device boot in developer mode.
If not internal device boot, it will try to reboot the device and
bypass dev mode to boot into internal device.
@param original_dev_boot_usb: Original dev_boot_usb value.
"""
logging.info('Checking internal device boot.')
self.faft_client.system.set_dev_default_boot()
if self.faft_client.system.is_removable_device_boot():
logging.info('Reboot into internal disk...')
self.faft_client.system.set_dev_boot_usb(original_dev_boot_usb)
self.switcher.mode_aware_reboot()
self.check_state((self.checkers.dev_boot_usb_checker, False,
'Device not booted from internal disk properly.'))
def set_hardware_write_protect(self, enable):
"""Set hardware write protect pin.
@param enable: True if asserting write protect pin. Otherwise, False.
"""
self.servo.set('fw_wp_state', 'force_on' if enable else 'force_off')
def set_ap_write_protect_and_reboot(self, enable):
"""Set AP write protect status and reboot to take effect.
@param enable: True if asserting write protect. Otherwise, False.
"""
self.set_hardware_write_protect(enable)
if hasattr(self, 'ec'):
self.sync_and_ec_reboot()
self.switcher.wait_for_client()
def run_chromeos_firmwareupdate(self, mode, append=None, options=(),
ignore_status=False):
"""Use RPC to get the command to run, but do the actual run via ssh.
Running the command via SSH improves the reliability in cases where the
USB network connection gets interrupted. SSH will still return the
output, and won't hang like RPC would.
"""
update_cmd = self.faft_client.updater.get_firmwareupdate_command(
mode, append, options)
try:
result = self._client.run(
update_cmd, timeout=300, ignore_status=ignore_status)
if result.exit_status == 255:
self.faft_client.disconnect()
return result
except error.AutoservRunError as e:
if e.result_obj.exit_status == 255:
self.faft_client.disconnect()
if ignore_status:
return e.result_obj
raise
def set_ec_write_protect_and_reboot(self, enable):
"""Set EC write protect status and reboot to take effect.
The write protect state is only activated if both hardware write
protect pin is asserted and software write protect flag is set.
This method asserts/deasserts hardware write protect pin first, and
set corresponding EC software write protect flag.
If the device uses non-Chrome EC, set the software write protect via
flashrom.
If the device uses Chrome EC, a reboot is required for write protect
to take effect. Since the software write protect flag cannot be unset
if hardware write protect pin is asserted, we need to deasserted the
pin first if we are deactivating write protect. Similarly, a reboot
is required before we can modify the software flag.
@param enable: True if activating EC write protect. Otherwise, False.
"""
self.set_hardware_write_protect(enable)
if self.faft_config.chrome_ec:
self.set_chrome_ec_write_protect_and_reboot(enable)
else:
self.faft_client.ec.set_write_protect(enable)
self.switcher.mode_aware_reboot()
def set_chrome_ec_write_protect_and_reboot(self, enable):
"""Set Chrome EC write protect status and reboot to take effect.
@param enable: True if activating EC write protect. Otherwise, False.
"""
if enable:
# Set write protect flag and reboot to take effect.
self.ec.set_flash_write_protect(enable)
self.sync_and_ec_reboot(
flags='hard',
extra_sleep=self.faft_config.ec_boot_to_wp_en)
else:
# Reboot after deasserting hardware write protect pin to deactivate
# write protect. And then remove software write protect flag.
# Some ITE ECs can only clear their WP status on a power-on reset,
# no software-initiated reset will do.
self.sync_and_ec_reboot(flags='cold')
self.ec.set_flash_write_protect(enable)
def _setup_ec_write_protect(self, ec_wp):
"""Setup for EC write-protection.
It makes sure the EC in the requested write-protection state. If not, it
flips the state. Flipping the write-protection requires DUT reboot.
@param ec_wp: True to request EC write-protected; False to request EC
not write-protected; None to do nothing.
"""
if ec_wp is None:
return
self._old_wpsw_cur = self.checkers.crossystem_checker(
{'wpsw_cur': '1'}, suppress_logging=True)
if ec_wp != self._old_wpsw_cur:
if not self.faft_config.ap_access_ec_flash:
raise error.TestNAError(
"Cannot change EC write-protect for this device")
logging.info('The test required EC is %swrite-protected. Reboot '
'and flip the state.', '' if ec_wp else 'not ')
self.switcher.mode_aware_reboot(
'custom',
lambda:self.set_ec_write_protect_and_reboot(ec_wp))
wpsw_cur = '1' if ec_wp else '0'
self.check_state((self.checkers.crossystem_checker, {
'wpsw_cur': wpsw_cur}))
def _restore_ec_write_protect(self):
"""Restore the original EC write-protection."""
if (not hasattr(self, '_old_wpsw_cur')) or (self._old_wpsw_cur is
None):
return
if not self.checkers.crossystem_checker({'wpsw_cur': '1' if
self._old_wpsw_cur else '0'}, suppress_logging=True):
logging.info('Restore original EC write protection and reboot.')
self.switcher.mode_aware_reboot(
'custom',
lambda:self.set_ec_write_protect_and_reboot(
self._old_wpsw_cur))
self.check_state((self.checkers.crossystem_checker, {
'wpsw_cur': '1' if self._old_wpsw_cur else '0'}))
def _record_uart_capture(self):
"""Record the CPU/EC/PD UART output stream to files."""
self.servo.record_uart_capture(self.resultsdir)
def _cleanup_uart_capture(self):
"""Cleanup the CPU/EC/PD UART capture."""
self.servo.close(self.resultsdir)
def set_ap_off_power_mode(self, power_mode):
"""
Set the DUT power mode to suspend (S0ix/S3) or shutdown (G3/S5).
The DUT must be in S0 when calling this method.
@param power_mode: a string for the expected power mode, either
'suspend' or 'shutdown'.
"""
if power_mode == 'suspend':
target_power_state = self.POWER_STATE_SUSPEND
elif power_mode == 'shutdown':
target_power_state = self.POWER_STATE_G3
else:
raise error.TestError('%s is not a valid ap-off power mode.' %
power_mode)
if self.get_power_state() != self.POWER_STATE_S0:
raise error.TestError('The DUT is not in S0.')
self._restore_power_mode = True
if target_power_state == self.POWER_STATE_G3:
self.run_shutdown_cmd()
time.sleep(self.faft_config.shutdown)
elif target_power_state == self.POWER_STATE_SUSPEND:
self.suspend()
if self.wait_power_state(target_power_state, self.DEFAULT_PWR_RETRIES):
logging.info('System entered %s state.', target_power_state)
else:
self._restore_power_mode = False
raise error.TestFail('System fail to enter %s state. '
'Current state: %s', target_power_state,
self.get_power_state())
def restore_ap_on_power_mode(self):
"""
Wake up the DUT to S0. If the DUT was not set to suspend or
shutdown mode by set_ap_off_power_mode(), raise an error.
"""
if self.get_power_state() != self.POWER_STATE_S0:
logging.info('Wake up the DUT to S0.')
self.servo.power_normal_press()
# If the DUT is ping-able, it must be in S0.
self.switcher.wait_for_client()
if self._restore_power_mode != True:
raise error.TestFail('The DUT was not set to suspend/shutdown '
'mode by set_ap_off_power_mode().')
self._restore_power_mode = False
def get_power_state(self):
"""
Return the current power state of the AP (via EC 'powerinfo' command)
@return the name of the power state, or None if a problem occurred
"""
if not hasattr(self, 'ec'):
# Don't fail when EC not present or not fully initialized
return None
pattern = r'power state (\w+) = (\w+),'
try:
match = self.ec.send_command_get_output("powerinfo", [pattern])
except error.TestFail as err:
logging.warn("powerinfo command encountered an error: %s", err)
return None
if not match:
logging.warn("powerinfo output did not match pattern: %r", pattern)
return None
(line, state_num, state_name) = match[0]
logging.debug("power state info %r", match)
return state_name
def _check_power_state(self, power_state):
"""
Check for correct power state of the AP (via EC 'powerinfo' command)
@return: the line and the match, if the output matched.
@raise error.TestFail: if output didn't match after the delay.
"""
if not isinstance(power_state, str):
raise error.TestError('%s is not a string while it should be.' %
power_state)
return self.ec.send_command_get_output("powerinfo",
['\\b' + power_state + '\\b'])
def wait_power_state(self, power_state, retries, retry_delay=0):
"""
Wait for certain power state.
@param power_state: power state you are expecting
@param retries: retries. This is necessary if AP is powering down
and transitioning through different states.
@param retry_delay: delay between retries in seconds
"""
logging.info('Checking power state "%s" maximum %d times.',
power_state, retries)
# Reset the cache, in case previous calls silently changed it on servod
self.ec.set_uart_regexp('None')
while retries > 0:
logging.info("try count: %d", retries)
start_time = time.time()
try:
retries = retries - 1
if self._check_power_state(power_state):
return True
except error.TestFail:
pass
delay_time = retry_delay - time.time() + start_time
if delay_time > 0:
time.sleep(delay_time)
return False
def run_shutdown_cmd(self):
"""Shut down the DUT by running '/sbin/shutdown -P now'."""
self.faft_client.disconnect()
# Shut down in the background after sleeping so the call gets a reply.
try:
self._client.run_background('sleep 0.5; /sbin/shutdown -P now')
except error.AutoservRunError as e:
# From the ssh man page, error code 255 indicates ssh errors.
if e.result_obj.exit_status == 255:
logging.warn("Ignoring error from ssh: %s", e)
else:
raise
self.switcher.wait_for_client_offline()
def suspend(self):
"""Suspends the DUT."""
cmd = 'sleep %d; powerd_dbus_suspend' % self.EC_SUSPEND_DELAY
block = False
self.faft_client.system.run_shell_command(cmd, block)
time.sleep(self.EC_SUSPEND_DELAY)
def _record_faft_client_log(self):
"""Record the faft client log to the results directory."""
client_log = self.faft_client.system.dump_log(True)
client_log_file = os.path.join(self.resultsdir, 'faft_client.log')
with open(client_log_file, 'w') as f:
f.write(client_log)
def _setup_gbb_flags(self):
"""Setup the GBB flags for FAFT test."""
if self.check_setup_done('gbb_flags'):
return
logging.info('Set proper GBB flags for test.')
# Ensure that GBB flags are set to 0x140.
flags_to_set = (vboot.GBB_FLAG_FAFT_KEY_OVERIDE |
vboot.GBB_FLAG_ENTER_TRIGGERS_TONORM)
# And if the "no_ec_sync" argument is set, then disable EC software
# sync.
if self._no_ec_sync:
logging.info(
'User selected to disable EC software sync')
flags_to_set |= vboot.GBB_FLAG_DISABLE_EC_SOFTWARE_SYNC
# And if the "no_fw_rollback_check" argument is set, then disable fw
# rollback check.
if self._no_fw_rollback_check:
logging.info(
'User selected to disable FW rollback check')
flags_to_set |= vboot.GBB_FLAG_DISABLE_FW_ROLLBACK_CHECK
self.clear_set_gbb_flags(0xffffffff, flags_to_set)
self.mark_setup_done('gbb_flags')
def _restore_gbb_flags(self):
"""Restore GBB flags to their original state."""
if self._backup_gbb_flags is None:
return
# Setting up and restoring the GBB flags take a lot of time. For
# speed-up purpose, don't restore it.
logging.info('***')
logging.info('*** Please manually restore the original GBB flags to: '
'0x%x ***', self._backup_gbb_flags)
logging.info('***')
self.unmark_setup_done('gbb_flags')
def setup_tried_fwb(self, tried_fwb):
"""Setup for fw B tried state.
It makes sure the system in the requested fw B tried state. If not, it
tries to do so.
@param tried_fwb: True if requested in tried_fwb=1;
False if tried_fwb=0.
"""
if tried_fwb:
if not self.checkers.crossystem_checker({'tried_fwb': '1'}):
logging.info(
'Firmware is not booted with tried_fwb. Reboot into it.')
self.faft_client.system.set_try_fw_b()
else:
if not self.checkers.crossystem_checker({'tried_fwb': '0'}):
logging.info(
'Firmware is booted with tried_fwb. Reboot to clear.')
def power_on(self):
"""Switch DUT AC power on."""
self._client.power_on(self.power_control)
def power_off(self):
"""Switch DUT AC power off."""
self._client.power_off(self.power_control)
def power_cycle(self):
"""Power cycle DUT AC power."""
self._client.power_cycle(self.power_control)
def setup_rw_boot(self, section='a'):
"""Make sure firmware is in RW-boot mode.
If the given firmware section is in RO-boot mode, turn off the RO-boot
flag and reboot DUT into RW-boot mode.
@param section: A firmware section, either 'a' or 'b'.
"""
flags = self.faft_client.bios.get_preamble_flags(section)
if flags & vboot.PREAMBLE_USE_RO_NORMAL:
flags = flags ^ vboot.PREAMBLE_USE_RO_NORMAL
self.faft_client.bios.set_preamble_flags(section, flags)
self.switcher.mode_aware_reboot()
def setup_kernel(self, part):
"""Setup for kernel test.
It makes sure both kernel A and B bootable and the current boot is
the requested kernel part.
@param part: A string of kernel partition number or 'a'/'b'.
"""
self.ensure_kernel_boot(part)
logging.info('Checking the integrity of kernel B and rootfs B...')
if (self.faft_client.kernel.diff_a_b() or
not self.faft_client.rootfs.verify_rootfs('B')):
logging.info('Copying kernel and rootfs from A to B...')
self.copy_kernel_and_rootfs(from_part=part,
to_part=self.OTHER_KERNEL_MAP[part])
self.reset_and_prioritize_kernel(part)
def reset_and_prioritize_kernel(self, part):
"""Make the requested partition highest priority.
This function also reset kerenl A and B to bootable.
@param part: A string of partition number to be prioritized.
"""
root_dev = self.faft_client.system.get_root_dev()
# Reset kernel A and B to bootable.
self.faft_client.system.run_shell_command(
'cgpt add -i%s -P1 -S1 -T0 %s' % (self.KERNEL_MAP['a'], root_dev))
self.faft_client.system.run_shell_command(
'cgpt add -i%s -P1 -S1 -T0 %s' % (self.KERNEL_MAP['b'], root_dev))
# Set kernel part highest priority.
self.faft_client.system.run_shell_command('cgpt prioritize -i%s %s' %
(self.KERNEL_MAP[part], root_dev))
def do_blocking_sync(self, device):
"""Run a blocking sync command."""
logging.info("Blocking sync for %s", device)
if 'mmcblk' in device:
# For mmc devices, use `mmc status get` command to send an
# empty command to wait for the disk to be available again.
self.faft_client.system.run_shell_command('mmc status get %s' %
device)
elif 'nvme' in device:
# For NVMe devices, use `nvme flush` command to commit data
# and metadata to non-volatile media.
# Get a list of NVMe namespaces, and flush them individually.
# The output is assumed to be in the following format:
# [ 0]:0x1
# [ 1]:0x2
list_ns_cmd = "nvme list-ns %s" % device
available_ns = self.faft_client.system.run_shell_command_get_output(
list_ns_cmd)
if not available_ns:
raise error.TestError(
"Listing namespaces failed (empty output): %s"
% list_ns_cmd)
for ns in available_ns:
ns = ns.split(':')[-1]
flush_cmd = 'nvme flush %s -n %s' % (device, ns)
flush_rc = self.faft_client.system.run_shell_command_get_status(
flush_cmd)
if flush_rc != 0:
raise error.TestError(
"Flushing namespace %s failed (rc=%s): %s"
% (ns, flush_rc, flush_cmd))
else:
# For other devices, hdparm sends TUR to check if
# a device is ready for transfer operation.
self.faft_client.system.run_shell_command('hdparm -f %s' % device)
def blocking_sync(self, freeze_for_reset=False):
"""Sync root device and internal device, via script if possible.
The actual calls end up logged by the run() call, since they're printed
to stdout/stderr in the script.
@param freeze_for_reset: if True, prepare for reset by blocking writes
(only if enable_fs_sync_fsfreeze=True)
"""
if self._use_sync_script:
if freeze_for_reset:
self.faft_client.quit()
try:
return self._client.blocking_sync(freeze_for_reset)
except (AttributeError, ImportError, error.AutoservRunError) as e:
logging.warn(
'Falling back to old sync method due to error: %s', e)
# The double calls to sync fakes a blocking call
# since the first call returns before the flush
# is complete, but the second will wait for the
# first to finish.
self.faft_client.system.run_shell_command('sync')
self.faft_client.system.run_shell_command('sync')
# sync only sends SYNCHRONIZE_CACHE but doesn't check the status.
# This function will perform a device-specific sync command.
root_dev = self.faft_client.system.get_root_dev()
self.do_blocking_sync(root_dev)
# Also sync the internal device if booted from removable media.
if self.faft_client.system.is_removable_device_boot():
internal_dev = self.faft_client.system.get_internal_device()
self.do_blocking_sync(internal_dev)
def sync_and_ec_reboot(self, flags='', extra_sleep=0):
"""Request the client sync and do a EC triggered reboot.
@param flags: Optional, a space-separated string of flags passed to EC
reboot command, including:
default: EC soft reboot;
'hard': EC hard reboot.
'cold': Cold reboot via servo.
@param extra_sleep: Optional, int or float for extra wait time for EC
reboot in seconds.
"""
self.blocking_sync(freeze_for_reset=True)
if flags == 'cold':
self.servo.get_power_state_controller().reset()
else:
self.ec.reboot(flags)
time.sleep(self.faft_config.ec_boot_to_console + extra_sleep)
self.check_lid_and_power_on()
def reboot_and_reset_tpm(self):
"""Reboot into recovery mode, reset TPM, then reboot back to disk."""
self.switcher.reboot_to_mode(to_mode='rec')
self.faft_client.system.run_shell_command('chromeos-tpm-recovery')
self.switcher.mode_aware_reboot()
def full_power_off_and_on(self):
"""Shutdown the device by pressing power button and power on again."""
boot_id = self.get_bootid()
self.faft_client.disconnect()
# Press power button to trigger Chrome OS normal shutdown process.
# We use a customized delay since the normal-press 1.2s is not enough.
self.servo.power_key(self.faft_config.hold_pwr_button_poweroff)
# device can take 44-51 seconds to restart,
# add buffer from the default timeout of 60 seconds.
self.switcher.wait_for_client_offline(timeout=100, orig_boot_id=boot_id)
time.sleep(self.faft_config.shutdown)
if self.faft_config.chrome_ec:
self.check_shutdown_power_state(self.POWER_STATE_G3,
orig_boot_id=boot_id)
# Short press power button to boot DUT again.
self.servo.power_key(self.faft_config.hold_pwr_button_poweron)
def check_shutdown_power_state(self, power_state,
pwr_retries=DEFAULT_PWR_RETRIES,
orig_boot_id=None):
"""Check whether the device shut down and entered the given power state.
If orig_boot_id is specified, it will check whether the DUT responds to
ssh requests, then use orig_boot_id to check if it rebooted.
@param power_state: EC power state has to be checked. Either S5 or G3.
@param pwr_retries: Times to check if the DUT in expected power state.
@param orig_boot_id: Old boot_id, to check for unexpected reboots.
@raise TestFail: If device failed to enter into requested power state.
"""
if not self.wait_power_state(power_state, pwr_retries):
current_state = self.get_power_state()
if current_state == self.POWER_STATE_S0 and self._client.wait_up():
# DUT is unexpectedly up, so check whether it rebooted instead.
new_boot_id = self.get_bootid()
logging.debug('orig_boot_id=%s, new_boot_id=%s',
orig_boot_id, new_boot_id)
if orig_boot_id is None or new_boot_id is None:
# Can't say anything more specific without values to compare
raise error.TestFail(
"Expected state %s, but the system is unexpectedly"
" still up. Current state: %s"
% (power_state, current_state))
if new_boot_id == orig_boot_id:
raise error.TestFail(
"Expected state %s, but the system didn't shut"
" down. Current state: %s"
% (power_state, current_state))
else:
raise error.TestFail(
"Expected state %s, but the system rebooted instead"
" of shutting down. Current state: %s"
% (power_state, current_state))
if current_state is None:
current_state = '(unknown)'
if current_state == power_state:
raise error.TestFail(
"Expected state %s, but the system didn't reach it"
" until after the limit of %s tries."
% (power_state, pwr_retries))
raise error.TestFail('System not shutdown properly and EC fails'
' to enter into %s state. Current state: %s'
% (power_state, current_state))
logging.info('System entered into %s state..', power_state)
def check_lid_and_power_on(self):
"""
On devices with EC software sync, system powers on after EC reboots if
lid is open. Otherwise, the EC shuts down CPU after about 3 seconds.
This method checks lid switch state and presses power button if
necessary.
"""
if self.servo.get("lid_open") == "no":
time.sleep(self.faft_config.software_sync)
self.servo.power_short_press()
def stop_powerd(self):
"""Stop the powerd daemon on the AP.
This will cause the AP to ignore power button presses sent by the EC.
"""
powerd_running = self.faft_client.system.run_shell_command_check_output(
'status powerd', 'start/running')
if powerd_running:
logging.debug('Stopping powerd')
self.faft_client.system.run_shell_command("stop powerd")
def _modify_usb_kernel(self, usb_dev, from_magic, to_magic):
"""Modify the kernel header magic in USB stick.
The kernel header magic is the first 8-byte of kernel partition.
We modify it to make it fail on kernel verification check.
@param usb_dev: A string of USB stick path on the host, like '/dev/sdc'.
@param from_magic: A string of magic which we change it from.
@param to_magic: A string of magic which we change it to.
@raise TestError: if failed to change magic.
"""
assert len(from_magic) == 8
assert len(to_magic) == 8
# USB image only contains one kernel.
kernel_part = self._join_part(usb_dev, self.KERNEL_MAP['a'])
read_cmd = "sudo dd if=%s bs=8 count=1 2>/dev/null" % kernel_part
current_magic = self.servo.system_output(read_cmd)
if current_magic == to_magic:
logging.info("The kernel magic is already %s.", current_magic)
return
if current_magic != from_magic:
raise error.TestError("Invalid kernel image on USB: wrong magic.")
logging.info('Modify the kernel magic in USB, from %s to %s.',
from_magic, to_magic)
write_cmd = ("echo -n '%s' | sudo dd of=%s oflag=sync conv=notrunc "
" 2>/dev/null" % (to_magic, kernel_part))
self.servo.system(write_cmd)
if self.servo.system_output(read_cmd) != to_magic:
raise error.TestError("Failed to write new magic.")
def corrupt_usb_kernel(self, usb_dev):
"""Corrupt USB kernel by modifying its magic from CHROMEOS to CORRUPTD.
@param usb_dev: A string of USB stick path on the host, like '/dev/sdc'.
"""
self._modify_usb_kernel(usb_dev, self.CHROMEOS_MAGIC,
self.CORRUPTED_MAGIC)
def restore_usb_kernel(self, usb_dev):
"""Restore USB kernel by modifying its magic from CORRUPTD to CHROMEOS.
@param usb_dev: A string of USB stick path on the host, like '/dev/sdc'.
"""
self._modify_usb_kernel(usb_dev, self.CORRUPTED_MAGIC,
self.CHROMEOS_MAGIC)
def _call_action(self, action_tuple, check_status=False):
"""Call the action function with/without arguments.
@param action_tuple: A function, or a tuple (function, args, error_msg),
in which, args and error_msg are optional. args is
either a value or a tuple if multiple arguments.
This can also be a list containing multiple
function or tuple. In this case, these actions are
called in sequence.
@param check_status: Check the return value of action function. If not
succeed, raises a TestFail exception.
@return: The result value of the action function.
@raise TestError: An error when the action function is not callable.
@raise TestFail: When check_status=True, action function not succeed.
"""
if isinstance(action_tuple, list):
return all([self._call_action(action, check_status=check_status)
for action in action_tuple])
action = action_tuple
args = ()
error_msg = 'Not succeed'
if isinstance(action_tuple, tuple):
action = action_tuple[0]
if len(action_tuple) >= 2:
args = action_tuple[1]
if not isinstance(args, tuple):
args = (args,)
if len(action_tuple) >= 3:
error_msg = action_tuple[2]
if action is None:
return
if not callable(action):
raise error.TestError('action is not callable!')
info_msg = 'calling %s' % action.__name__
if args:
info_msg += ' with args %s' % str(args)
logging.info(info_msg)
ret = action(*args)
if check_status and not ret:
raise error.TestFail('%s: %s returning %s' %
(error_msg, info_msg, str(ret)))
return ret
def run_shutdown_process(self, shutdown_action, pre_power_action=None,
run_power_action=True, post_power_action=None,
shutdown_timeout=None):
"""Run shutdown_action(), which makes DUT shutdown, and power it on.
@param shutdown_action: function which makes DUT shutdown, like
pressing power key.
@param pre_power_action: function which is called before next power on.
@param run_power_action: power_key press by default, set to None to skip.
@param post_power_action: function which is called after next power on.
@param shutdown_timeout: a timeout to confirm DUT shutdown.
@raise TestFail: if the shutdown_action() failed to turn DUT off.
"""
self._call_action(shutdown_action)
logging.info('Wait to ensure DUT shut down...')
try:
if shutdown_timeout is None:
shutdown_timeout = self.faft_config.shutdown_timeout
self.switcher.wait_for_client(timeout=shutdown_timeout)
raise error.TestFail(
'Should shut the device down after calling %s.' %
shutdown_action.__name__)
except ConnectionError:
if self.faft_config.chrome_ec:
self.check_shutdown_power_state(self.POWER_STATE_G3)
logging.info(
'DUT is surely shutdown. We are going to power it on again...')
if pre_power_action:
self._call_action(pre_power_action)
if run_power_action:
self.servo.power_key(self.faft_config.hold_pwr_button_poweron)
if post_power_action:
self._call_action(post_power_action)
def get_bootid(self, retry=3):
"""
Return the bootid.
"""
boot_id = None
while retry:
try:
boot_id = self._client.get_boot_id()
break
except error.AutoservRunError:
retry -= 1
if retry:
logging.info('Retry to get boot_id...')
else:
logging.warning('Failed to get boot_id.')
logging.info('boot_id: %s', boot_id)
return boot_id
def check_state(self, func):
"""
Wrapper around _call_action with check_status set to True. This is a
helper function to be used by tests and is currently implemented by
calling _call_action with check_status=True.
TODO: This function's arguments need to be made more stringent. And
its functionality should be moved over to check functions directly in
the future.
@param func: A function, or a tuple (function, args, error_msg),
in which, args and error_msg are optional. args is
either a value or a tuple if multiple arguments.
This can also be a list containing multiple
function or tuple. In this case, these actions are
called in sequence.
@return: The result value of the action function.
@raise TestFail: If the function does notsucceed.
"""
logging.info("-[FAFT]-[ start stepstate_checker ]----------")
self._call_action(func, check_status=True)
logging.info("-[FAFT]-[ end state_checker ]----------------")
def get_current_firmware_identity(self):
"""Get current firmware sha and fwids of body and vblock.
@return: Current firmware checksums and fwids, as a dict
"""
current_checksums = {
'VBOOTA': self.faft_client.bios.get_sig_sha('a'),
'FVMAINA': self.faft_client.bios.get_body_sha('a'),
'VBOOTB': self.faft_client.bios.get_sig_sha('b'),
'FVMAINB': self.faft_client.bios.get_body_sha('b'),
}
if not all(current_checksums.values()):
raise error.TestError(
'Failed to get firmware sha: %s', current_checksums)
current_fwids = {
'RO_FRID': self.faft_client.bios.get_section_fwid('ro'),
'RW_FWID_A': self.faft_client.bios.get_section_fwid('a'),
'RW_FWID_B': self.faft_client.bios.get_section_fwid('b'),
}
if not all(current_fwids.values()):
raise error.TestError(
'Failed to get firmware fwid(s): %s', current_fwids)
identifying_info = dict(current_fwids)
identifying_info.update(current_checksums)
return identifying_info
def is_firmware_changed(self):
"""Check if the current firmware changed, by comparing its SHA and fwid.
@return: True if it is changed, otherwise False.
"""
# Device may not be rebooted after test.
self.faft_client.bios.reload()
current_info = self.get_current_firmware_identity()
prev_info = self._backup_firmware_identity
if current_info == prev_info:
return False
else:
changed = set()
for section in set(current_info.keys()) | set(prev_info.keys()):
if current_info.get(section) != prev_info.get(section):
changed.add(section)
logging.info('Firmware changed: %s', ', '.join(sorted(changed)))
return True
def backup_firmware(self, suffix='.original'):
"""Backup firmware to file, and then send it to host.
@param suffix: a string appended to backup file name
"""
remote_temp_dir = self.faft_client.system.create_temp_dir()
remote_bios_path = os.path.join(remote_temp_dir, 'bios')
self.faft_client.bios.dump_whole(remote_bios_path)
self._client.get_file(remote_bios_path,
os.path.join(self.resultsdir, 'bios' + suffix))
if self.faft_config.chrome_ec:
remote_ec_path = os.path.join(remote_temp_dir, 'ec')
self.faft_client.ec.dump_whole(remote_ec_path)
self._client.get_file(remote_ec_path,
os.path.join(self.resultsdir, 'ec' + suffix))
self._client.run('rm -rf %s' % remote_temp_dir)
logging.info('Backup firmware stored in %s with suffix %s',
self.resultsdir, suffix)
self._backup_firmware_identity = self.get_current_firmware_identity()
def is_firmware_saved(self):
"""Check if a firmware saved (called backup_firmware before).
@return: True if the firmware is backed up; otherwise False.
"""
return bool(self._backup_firmware_identity)
def restore_firmware(self, suffix='.original', restore_ec=True,
reboot_ec=False):
"""Restore firmware from host in resultsdir.
@param suffix: a string appended to backup file name
@param restore_ec: True to restore the ec firmware; False not to do.
@param reboot_ec: True to reboot EC after restore (if it was restored)
@return: True if firmware needed to be restored
"""
if not self.is_firmware_changed():
return False
# Backup current corrupted firmware.
self.backup_firmware(suffix='.corrupt')
# Restore firmware.
remote_temp_dir = self.faft_client.system.create_temp_dir()
bios_local = os.path.join(self.resultsdir, 'bios%s' % suffix)
bios_remote = os.path.join(remote_temp_dir, 'bios%s' % suffix)
self._client.send_file(bios_local, bios_remote)
self.faft_client.bios.write_whole(bios_remote)
if self.faft_config.chrome_ec and restore_ec:
ec_local = os.path.join(self.resultsdir, 'ec%s' % suffix)
ec_remote = os.path.join(remote_temp_dir, 'ec%s' % suffix)
self._client.send_file(ec_local, ec_remote)
ec_cmd = self.faft_client.ec.get_write_cmd(ec_remote)
try:
self._client.run(ec_cmd, timeout=300)
except error.AutoservSSHTimeout:
logging.warn("DUT connection died during EC restore")
self.faft_client.disconnect()
except error.GenericHostRunError:
logging.warn("DUT command failed during EC restore")
logging.debug("Full exception:", exc_info=True)
if reboot_ec:
self.switcher.mode_aware_reboot(
'custom', lambda: self.sync_and_ec_reboot('hard'))
else:
self.switcher.mode_aware_reboot()
else:
self.switcher.mode_aware_reboot()
logging.info('Successfully restored firmware.')
return True
def setup_firmwareupdate_shellball(self, shellball=None):
"""Setup a shellball to use in firmware update test.
Check if there is a given shellball, and it is a shell script. Then,
send it to the remote host. Otherwise, use the
/usr/sbin/chromeos-firmwareupdate in the image and replace its inside
BIOS and EC images with the active firmware images.
@param shellball: path of a shellball or default to None.
"""
if shellball:
# Determine the firmware file is a shellball or a raw binary.
is_shellball = (utils.system_output("file %s" % shellball).find(
"shell script") != -1)
if is_shellball:
logging.info('Device will update firmware with shellball %s',
shellball)
temp_path = self.faft_client.updater.get_temp_path()
working_shellball = os.path.join(temp_path,
'chromeos-firmwareupdate')
self._client.send_file(shellball, working_shellball)
self.faft_client.updater.extract_shellball()
else:
raise error.TestFail(
'The given shellball is not a shell script.')
else:
logging.info('No shellball given, use the original shellball and '
'replace its BIOS and EC images.')
work_path = self.faft_client.updater.get_work_path()
bios_in_work_path = os.path.join(
work_path, self.faft_client.updater.get_bios_relative_path())
ec_in_work_path = os.path.join(
work_path, self.faft_client.updater.get_ec_relative_path())
logging.info('Writing current BIOS to: %s', bios_in_work_path)
self.faft_client.bios.dump_whole(bios_in_work_path)
if self.faft_config.chrome_ec:
logging.info('Writing current EC to: %s', ec_in_work_path)
self.faft_client.ec.dump_firmware(ec_in_work_path)
self.faft_client.updater.repack_shellball()
def is_kernel_changed(self):
"""Check if the current kernel is changed, by comparing its SHA1 hash.
@return: True if it is changed; otherwise, False.
"""
changed = False
for p in ('A', 'B'):
backup_sha = self._backup_kernel_sha.get(p, None)
current_sha = self.faft_client.kernel.get_sha(p)
if backup_sha != current_sha:
changed = True
logging.info('Kernel %s is changed', p)
return changed
def backup_kernel(self, suffix='.original'):
"""Backup kernel to files, and the send them to host.
@param suffix: a string appended to backup file name.
"""
remote_temp_dir = self.faft_client.system.create_temp_dir()
for p in ('A', 'B'):
remote_path = os.path.join(remote_temp_dir, 'kernel_%s' % p)
self.faft_client.kernel.dump(p, remote_path)
self._client.get_file(
remote_path,
os.path.join(self.resultsdir, 'kernel_%s%s' % (p, suffix)))
self._backup_kernel_sha[p] = self.faft_client.kernel.get_sha(p)
logging.info('Backup kernel stored in %s with suffix %s',
self.resultsdir, suffix)
def is_kernel_saved(self):
"""Check if kernel images are saved (backup_kernel called before).
@return: True if the kernel is saved; otherwise, False.
"""
return len(self._backup_kernel_sha) != 0
def restore_kernel(self, suffix='.original'):
"""Restore kernel from host in resultsdir.
@param suffix: a string appended to backup file name.
"""
if not self.is_kernel_changed():
return
# Backup current corrupted kernel.
self.backup_kernel(suffix='.corrupt')
# Restore kernel.
remote_temp_dir = self.faft_client.system.create_temp_dir()
for p in ('A', 'B'):
remote_path = os.path.join(remote_temp_dir, 'kernel_%s' % p)
self._client.send_file(
os.path.join(self.resultsdir, 'kernel_%s%s' % (p, suffix)),
remote_path)
self.faft_client.kernel.write(p, remote_path)
self.switcher.mode_aware_reboot()
logging.info('Successfully restored kernel.')
def backup_cgpt_attributes(self):
"""Backup CGPT partition table attributes."""
self._backup_cgpt_attr = self.faft_client.cgpt.get_attributes()
def restore_cgpt_attributes(self):
"""Restore CGPT partition table attributes."""
current_table = self.faft_client.cgpt.get_attributes()
if current_table == self._backup_cgpt_attr:
return
logging.info('CGPT table is changed. Original: %r. Current: %r.',
self._backup_cgpt_attr,
current_table)
self.faft_client.cgpt.set_attributes(
self._backup_cgpt_attr['A'], self._backup_cgpt_attr['B'])
self.switcher.mode_aware_reboot()
logging.info('Successfully restored CGPT table.')
def try_fwb(self, count=0):
"""set to try booting FWB count # times
Wrapper to set fwb_tries for vboot1 and fw_try_count,fw_try_next for
vboot2
@param count: an integer specifying value to program into
fwb_tries(vb1)/fw_try_next(vb2)
"""
if self.fw_vboot2:
self.faft_client.system.set_fw_try_next('B', count)
else:
# vboot1: we need to boot into fwb at least once
if not count:
count = count + 1
self.faft_client.system.set_try_fw_b(count)
def identify_shellball(self, include_ec=None):
"""Get the FWIDs of all targets and sections in the shellball
@param include_ec: if True, get EC fwids.
If None (default), assume True if board has an EC
@return: the dict of versions in the shellball
"""
fwids = dict()
fwids['bios'] = self.faft_client.updater.get_image_fwids('bios')
if include_ec is None:
if self.faft_config.platform.lower() == 'samus':
include_ec = False # no ec.bin in shellball
else:
include_ec = self.faft_config.chrome_ec
if include_ec:
fwids['ec'] = self.faft_client.updater.get_image_fwids('ec')
return fwids
def modify_shellball(self, append, modify_ro=True, modify_ec=False):
"""Modify the FWIDs of targets and sections in the shellball
@return: the full path of the shellball
"""
if modify_ro:
self.faft_client.updater.modify_image_fwids(
'bios', ['ro', 'a', 'b'])
else:
self.faft_client.updater.modify_image_fwids(
'bios', ['a', 'b'])
if modify_ec:
if modify_ro:
self.faft_client.updater.modify_image_fwids(
'ec', ['ro', 'rw'])
else:
self.faft_client.updater.modify_image_fwids(
'ec', ['rw'])
modded_shellball = self.faft_client.updater.repack_shellball(append)
return modded_shellball
@staticmethod
def check_fwids_written(before_fwids, image_fwids, after_fwids,
expected_written):
"""Check the dicts of fwids for correctness after an update is applied.
The targets checked come from the keys of expected_written.
The sections checked come from the inner dicts of the fwids parameters.
The fwids should be keyed by target (flash type), then by section:
{'bios': {'ro': '<fwid>', 'a': '<fwid>', 'b': '<fwid>'},
'ec': {'ro': '<fwid>', 'rw': '<fwid>'}
For expected_written, the dict should be keyed by flash type only:
{'bios': ['ro'], 'ec': ['ro', 'rw']}
To expect the contents completely unchanged, give only the keys:
{'bios': [], 'ec': []} or {'bios': None, 'ec': None}
@param before_fwids: dict of versions from before the update
@param image_fwids: dict of versions in the update
@param after_fwids: dict of actual versions after the update
@param expected_written: dict indicating which ones should have changed
@return: list of error lines for mismatches
@type before_fwids: dict
@type image_fwids: dict | None
@type after_fwids: dict
@type expected_written: dict
@rtype: list
"""
errors = []
if image_fwids is None:
image_fwids = {}
for target in sorted(expected_written.keys()):
# target is BIOS or EC
before_missing = (target not in before_fwids)
after_missing = (target not in after_fwids)
if before_missing or after_missing:
if before_missing:
errors.append("...no before_fwids[%s]" % target)
if after_missing:
errors.append("...no after_fwids[%s]" % target)
continue
written_sections = expected_written.get(target) or list()
written_sections = set(written_sections)
before_sections = set(before_fwids.get(target) or dict())
image_sections = set(image_fwids.get(target) or dict())
after_sections = set(after_fwids.get(target) or dict())
for section in before_sections | image_sections | after_sections:
# section is RO, RW, A, or B
before_fwid = before_fwids[target][section]
image_fwid = image_fwids.get(target, {}).get(section, None)
actual_fwid = after_fwids[target][section]
if section in written_sections:
expected_fwid = image_fwid
expected_desc = 'rewritten fwid (%s)' % expected_fwid
if image_fwid == before_fwid:
expected_desc = ('rewritten (no changes) fwid (%s)' %
expected_fwid)
else:
expected_fwid = before_fwid
expected_desc = 'original fwid (%s)' % expected_fwid
if actual_fwid == expected_fwid:
actual_desc = 'correct value'
elif actual_fwid == image_fwid:
actual_desc = 'rewritten fwid (%s)' % actual_fwid
if image_fwid == before_fwid:
# The flash could have been rewritten with the same fwid
actual_desc = 'possibly written fwid (%s)' % actual_fwid
elif actual_fwid == before_fwid:
actual_desc = 'original fwid (%s)' % actual_fwid
else:
actual_desc = 'unknown fwid (%s)' % actual_fwid
msg = ("...FWID (%s %s): expected %s, got %s" %
(target.upper(), section.upper(),
expected_desc, actual_desc))
if actual_fwid != expected_fwid:
errors.append(msg)
return errors
def fwmp_is_cleared(self):
"""Return True if the FWMP has been created"""
res = self.host.run('cryptohome '
'--action=get_firmware_management_parameters',
ignore_status=True)
if res.exit_status and res.exit_status != self.FWMP_CLEARED_EXIT_STATUS:
raise error.TestError('Could not run cryptohome command %r' % res)
return self.FWMP_CLEARED_ERROR_MSG in res.stdout
def _tpm_is_owned(self):
"""Returns True if the tpm is owned"""
result = self.host.run('tpm_manager_client status --nonsensitive',
ignore_status=True)
logging.debug(result)
return result.exit_status == 0 and 'is_owned: true' in result.stdout
def clear_fwmp(self):
"""Clear the FWMP"""
if self.fwmp_is_cleared():
return
tpm_utils.ClearTPMOwnerRequest(self.host, wait_for_ready=True)
self.host.run('tpm_manager_client take_ownership')
if not utils.wait_for_value(self._tpm_is_owned, expected_value=True):
raise error.TestError('Unable to own tpm while clearing fwmp.')
self.host.run('cryptohome '
'--action=remove_firmware_management_parameters')
def wait_for(self, cfg_field, action_msg=None, extra_time=0):
"""Waits for time specified in a config.
@ivar cfg_field: The name of the config field that specifies the
time to wait.
@ivar action_msg: Optional log message describing the action that
will occur after the wait.
@ivar extra_time: Additional time to be added to time from config.
"""
wait_time = self.faft_config.__getattr__(cfg_field) + extra_time
if extra_time:
wait_src = "%s + %s" % (cfg_field, extra_time)
else:
wait_src = cfg_field
units = 'second' if wait_time==1 else 'seconds'
start_msg = "Waiting %s(%s) %s" % (wait_time, wait_src, units)
if action_msg:
start_msg += ", before '%s'" % action_msg
start_msg += "."
logging.info(start_msg)
time.sleep(wait_time)
logging.info("Done waiting.")
def _try_to_bring_dut_up(self):
"""Try to quickly get the dut in a pingable state"""
if not hasattr(self, 'cr50'):
raise error.TestNAError('Test can only be run on devices with '
'access to the Cr50 console')
logging.info('checking dut state')
self.servo.set_nocheck('cold_reset', 'off')
try:
self.servo.set_nocheck('warm_reset', 'off')
except error.TestFail as e:
# TODO(b/159338538): remove once the kukui remap issue is resolved.
if 'Timed out waiting for interfaces to become available' in str(e):
logging.warn('Ignoring warm_reset interface issue b/159338538')
else:
raise
time.sleep(self.cr50.SHORT_WAIT)
if not self.cr50.ap_is_on():
logging.info('Pressing power button to turn on AP')
self.servo.power_short_press()
end_time = time.time() + self.RESPONSE_TIMEOUT
while not self.host.ping_wait_up(
self.faft_config.delay_reboot_to_ping * 2):
if time.time() > end_time:
logging.warn('DUT is unresponsive after trying to bring it up')
return
self.servo.get_power_state_controller().reset()
logging.info('DUT did not respond. Resetting it.')
def _check_open_and_press_power_button(self):
"""Check stdout and press the power button if prompted.
Returns:
True if the process is still running.
"""
if not hasattr(self, 'cr50'):
raise error.TestNAError('Test can only be run on devices with '
'access to the Cr50 console')
logging.info(self._get_ccd_open_output())
self.servo.power_short_press()
logging.info('long int power button press')
# power button press cr50 erases nvmem and resets the dut before setting
# the state to open. Wait a bit so we don't check the ccd state in the
# middle of this reset process. Power button requests happen once a
# minute, so waiting 10 seconds isn't a big deal.
time.sleep(10)
return (self.cr50.OPEN == self.cr50.get_ccd_level() or
self._ccd_open_job.sp.poll() is not None)
def _get_ccd_open_output(self):
"""Read the new output."""
if not hasattr(self, 'cr50'):
raise error.TestNAError('Test can only be run on devices with '
'access to the Cr50 console')
self._ccd_open_job.process_output()
self._ccd_open_stdout.seek(self._ccd_open_last_len)
output = self._ccd_open_stdout.read()
self._ccd_open_last_len = self._ccd_open_stdout.len
return output
def _close_ccd_open_job(self):
"""Terminate the process and check the results."""
if not hasattr(self, 'cr50'):
raise error.TestNAError('Test can only be run on devices with '
'access to the Cr50 console')
exit_status = utils.nuke_subprocess(self._ccd_open_job.sp)
stdout = self._ccd_open_stdout.getvalue().strip()
delattr(self, '_ccd_open_job')
if stdout:
logging.info('stdout of ccd open:\n%s', stdout)
if exit_status:
logging.info('exit status: %d', exit_status)
if 'Error' in stdout:
raise error.TestFail('ccd open Error %s' %
stdout.split('Error')[-1])
if self.cr50.OPEN != self.cr50.get_ccd_level():
raise error.TestFail('unable to open cr50: %s' % stdout)
else:
logging.info('Opened Cr50')
def ccd_open_from_ap(self):
"""Start the open process and press the power button."""
if not hasattr(self, 'cr50'):
raise error.TestNAError('Test can only be run on devices with '
'access to the Cr50 console')
# Opening CCD requires power button presses. If those presses would
# power off the AP and prevent CCD open from completing, ignore them.
if self.faft_config.ec_forwards_short_pp_press:
self.stop_powerd()
# Make sure the test waits long enough to avoid ccd rate limiting.
time.sleep(self.cr50.CCD_PASSWORD_RATE_LIMIT)
self._ccd_open_last_len = 0
self._ccd_open_stdout = StringIO.StringIO()
ccd_open_cmd = utils.sh_escape('gsctool -a -o')
full_ssh_cmd = '%s "%s"' % (self.host.ssh_command(options='-tt'),
ccd_open_cmd)
# Start running the Cr50 Open process in the background.
self._ccd_open_job = utils.BgJob(full_ssh_cmd,
nickname='ccd_open',
stdout_tee=self._ccd_open_stdout,
stderr_tee=utils.TEE_TO_LOGS)
if self._ccd_open_job == None:
raise error.TestFail('could not start ccd open')
try:
# Wait for the first gsctool power button prompt before starting the
# open process.
logging.info(self._get_ccd_open_output())
# Cr50 starts out by requesting 5 quick presses then 4 longer
# power button presses. Run the quick presses without looking at the
# command output, because getting the output can take some time. For
# the presses that require a 1 minute wait check the output between
# presses, so we can catch errors
#
# run quick presses for 30 seconds. It may take a couple of seconds
# for open to start. 10 seconds should be enough. 30 is just used
# because it will definitely be enough, and this process takes 300
# seconds, so doing quick presses for 30 seconds won't matter.
end_time = time.time() + 30
while time.time() < end_time:
self.servo.power_short_press()
logging.info('short int power button press')
time.sleep(self.PP_SHORT_INTERVAL)
# Poll the output and press the power button for the longer presses.
utils.wait_for_value(self._check_open_and_press_power_button,
expected_value=True,
timeout_sec=self.cr50.PP_LONG)
except Exception as e:
logging.info(e)
raise
finally:
self._close_ccd_open_job()
self._try_to_bring_dut_up()
logging.info(self.cr50.get_ccd_info())
def enter_mode_after_checking_cr50_state(self, mode):
"""Reboot to mode if cr50 doesn't already match the state"""
if not hasattr(self, 'cr50'):
raise error.TestNAError('Test can only be run on devices with '
'access to the Cr50 console')
# If the device is already in the correct mode, don't do anything
if (mode == 'dev') == self.cr50.in_dev_mode():
logging.info('already in %r mode', mode)
return
self.switcher.reboot_to_mode(to_mode=mode)
if (mode == 'dev') != self.cr50.in_dev_mode():
raise error.TestError('Unable to enter %r mode' % mode)
def fast_ccd_open(self, enable_testlab=False, reset_ccd=True,
dev_mode=False):
"""Try to use ccd testlab open. If that fails, do regular ap open.
Args:
enable_testlab: If True, enable testlab mode after cr50 is open.
reset_ccd: If True, reset ccd after open.
dev_mode: True if the device should be in dev mode after ccd is
is opened.
"""
if not hasattr(self, 'cr50'):
raise error.TestNAError('Test can only be run on devices with '
'access to the Cr50 console')
if self.servo.main_device_is_ccd():
error_txt = 'because the main servo device is CCD.'
if enable_testlab:
raise error.TestNAError('Cannot enable testlab: %s' % error_txt)
elif reset_ccd:
raise error.TestNAError('CCD reset not allowed: %s' % error_txt)
if not self.faft_config.has_powerbutton:
logging.warning('No power button', exc_info=True)
enable_testlab = False
# Try to use testlab open first, so we don't have to wait for the
# physical presence check.
self.cr50.send_command('ccd testlab open')
if self.cr50.OPEN != self.cr50.get_ccd_level():
if self.servo.has_control('chassis_open'):
self.servo.set('chassis_open', 'yes')
pw = '' if self.cr50.password_is_reset() else self.CCD_PASSWORD
# Use the console to open cr50 without entering dev mode if
# possible. Ittakes longer and relies on more systems to enter dev
# mode and ssh into the AP. Skip the steps that aren't required.
if not (pw or self.cr50.get_cap(
'OpenNoDevMode')[self.cr50.CAP_IS_ACCESSIBLE]):
self.enter_mode_after_checking_cr50_state('dev')
if pw or self.cr50.get_cap(
'OpenFromUSB')[self.cr50.CAP_IS_ACCESSIBLE]:
self.cr50.set_ccd_level(self.cr50.OPEN, pw)
else:
self.ccd_open_from_ap()
if self.servo.has_control('chassis_open'):
self.servo.set('chassis_open', 'no')
if enable_testlab:
self.cr50.set_ccd_testlab('on')
if reset_ccd:
self.cr50.send_command('ccd reset')
# In default, the device should be in normal mode. After opening cr50,
# the TPM should be cleared and the device should automatically reset to
# normal mode. However, some tests might want the device in 'dev' mode.
self.enter_mode_after_checking_cr50_state('dev' if dev_mode else
'normal')