423 lines
17 KiB
Python
423 lines
17 KiB
Python
# Copyright 2020 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, re
|
|
import os
|
|
import xmlrpclib
|
|
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.common_lib import utils
|
|
from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
|
|
|
|
|
|
class firmware_CsmeFwUpdate(FirmwareTest):
|
|
"""
|
|
This tests csme rw firmware update feature by changing the me_rw
|
|
image in firmware main regions with a different version
|
|
|
|
Accepted --args names:
|
|
old_bios = specify this argument to use a different bios
|
|
than shellball default for downgrade
|
|
|
|
"""
|
|
version = 1
|
|
ORIGINAL_BIOS = "/usr/local/tmp/bios_original.bin"
|
|
DOWNGRADE_BIOS = "/usr/local/tmp/bios_downgrade.bin"
|
|
# Region to use for flashrom wp-region commands
|
|
WP_REGION = 'WP_RO'
|
|
MODE = 'recovery'
|
|
|
|
def initialize(self, host, cmdline_args, dev_mode = False):
|
|
# Parse arguments from command line
|
|
dict_args = utils.args_to_dict(cmdline_args)
|
|
super(firmware_CsmeFwUpdate, self).initialize(host, cmdline_args)
|
|
|
|
self.bios_input = None
|
|
self.restore_required = False
|
|
self.downgrade_bios = None
|
|
self.spi_bios = None
|
|
self._orig_sw_wp = None
|
|
self._original_hw_wp = None
|
|
arg_name = "old_bios"
|
|
arg_value = dict_args.get(arg_name)
|
|
if arg_value:
|
|
logging.info('%s=%s', arg_name, arg_value)
|
|
image_path = os.path.expanduser(arg_value)
|
|
if not os.path.isfile(image_path):
|
|
raise error.TestError(
|
|
"Specified file does not exist: %s=%s"
|
|
% (arg_name, image_path))
|
|
self.bios_input = image_path
|
|
else:
|
|
logging.info("No bios specified. Using default " \
|
|
"shellball bios for downgrade")
|
|
|
|
self.backup_firmware()
|
|
self.switcher.setup_mode('dev' if dev_mode else 'normal')
|
|
|
|
# Save write protect configuration and enable it
|
|
logging.info("Enabling Write protection")
|
|
self._orig_sw_wp = self.faft_client.bios.get_write_protect_status()
|
|
self._original_hw_wp = 'on' in self.servo.get('fw_wp_state')
|
|
self.set_ap_write_protect_and_reboot(False)
|
|
self.faft_client.bios.set_write_protect_region(self.WP_REGION, True)
|
|
self.set_ap_write_protect_and_reboot(True)
|
|
|
|
# Make sure that the shellball is retained over subsequent power cycles
|
|
self.blocking_sync()
|
|
|
|
def cleanup(self):
|
|
"""
|
|
Flash the backed up firmware at the end of test
|
|
|
|
"""
|
|
self.faft_client.system.remove_file(self.ORIGINAL_BIOS)
|
|
self.faft_client.system.remove_file(self.DOWNGRADE_BIOS)
|
|
self.set_ap_write_protect_and_reboot(False)
|
|
|
|
try:
|
|
if self.is_firmware_saved() and self.restore_required:
|
|
logging.info("Restoring Original Image")
|
|
self.restore_firmware()
|
|
except (EnvironmentError, xmlrpclib.Fault,
|
|
error.AutoservError, error.TestBaseException):
|
|
logging.error("Problem restoring firmware:", exc_info=True)
|
|
|
|
try:
|
|
# Restore the old write-protection value at the end of the test.
|
|
logging.info("Restoring write protection configuration")
|
|
if self._orig_sw_wp:
|
|
self.faft_client.bios.set_write_protect_range(
|
|
self._orig_sw_wp['start'],
|
|
self._orig_sw_wp['length'],
|
|
self._orig_sw_wp['enabled'])
|
|
except (EnvironmentError, xmlrpclib.Fault,
|
|
error.AutoservError, error.TestBaseException):
|
|
logging.error("Problem restoring software write-protect:",
|
|
exc_info = True)
|
|
|
|
if self._original_hw_wp is not None:
|
|
self.set_ap_write_protect_and_reboot(self._original_hw_wp)
|
|
|
|
self.switcher.mode_aware_reboot(reboot_type = 'cold')
|
|
super(firmware_CsmeFwUpdate, self).cleanup()
|
|
|
|
def read_current_bios_and_save(self):
|
|
"""
|
|
Dumps current bios from spi to two file.(working copy and backup)
|
|
|
|
@returns the working copy file path
|
|
|
|
"""
|
|
# Dump the current spi bios to file
|
|
self.spi_bios = self.ORIGINAL_BIOS
|
|
logging.info("Copying current bios image to %s for upgrade " \
|
|
"test" % self.spi_bios)
|
|
self.faft_client.bios.dump_whole(self.spi_bios)
|
|
|
|
# Get the downgrade bios image from user or from shellball
|
|
self.downgrade_bios = self.DOWNGRADE_BIOS
|
|
if self.bios_input:
|
|
logging.info("Copying user given bios image to %s for downgrade " \
|
|
"test" % self.downgrade_bios)
|
|
self._client.send_file(self.bios_input, self.downgrade_bios)
|
|
else:
|
|
logging.info("Copying bios image from update shellball to %s " \
|
|
"for downgrade test" % self.downgrade_bios)
|
|
self.faft_client.updater.extract_shellball()
|
|
cbfs_work_dir = self.faft_client.updater.cbfs_setup_work_dir()
|
|
shellball_bios = os.path.join(cbfs_work_dir,
|
|
self.faft_client.updater.get_bios_relative_path())
|
|
command = "cp %s %s" % (shellball_bios, self.downgrade_bios)
|
|
self.faft_client.system.run_shell_command(command)
|
|
|
|
def check_fmap_format(self, image_path):
|
|
"""
|
|
Checks FMAP format used by the Image for CSME update
|
|
|
|
@param image_path: path of the image
|
|
@returns the fmap format string
|
|
|
|
"""
|
|
# Check if ME_RW_A is present in the image
|
|
logging.info("Checking if seperate CBFS is used for CSE RW in " \
|
|
"image : %s" % image_path)
|
|
command = "futility dump_fmap -F %s | grep ME_RW_A" % image_path
|
|
output = self.faft_client.system.run_shell_command_get_output(
|
|
command, True)
|
|
if output:
|
|
logging.info("Image uses seperate CBFS for CSE RW")
|
|
return "CSE_RW_SEPARATE_CBFS"
|
|
else:
|
|
return "DEFAULT"
|
|
|
|
def check_if_me_blob_exist_in_image(self, image_path):
|
|
"""
|
|
Checks if me_blob exists in FW MAIN section of an image
|
|
|
|
@param image_path: path of the image
|
|
@returns True if present else False
|
|
|
|
"""
|
|
# Check if me_rw.metadata present FW_MAIN region
|
|
logging.info("Checking if me_rw.metadata file " \
|
|
"present in image : %s" % image_path )
|
|
command = "cbfstool %s print -r FW_MAIN_A " \
|
|
"| grep me_rw.metadata" % image_path
|
|
output = self.faft_client.system.run_shell_command_get_output(
|
|
command, True)
|
|
if output:
|
|
available = True
|
|
logging.info("me_rw.metadata present in image")
|
|
else:
|
|
available = False
|
|
logging.info("me_rw.metadata not present in image")
|
|
|
|
return available
|
|
|
|
def extract_me_rw_version_from_bin(self, me_blob, version_offset = 0):
|
|
"""
|
|
Extract me_rw version from given me_rw blob. Version is first 8
|
|
bytes in the blob
|
|
|
|
@param me_blob: me_rw blob (old fmap) or me_rw_metadata blob
|
|
@param version_offset: version filed offset in the blob
|
|
@returns the CSME RW version string
|
|
|
|
"""
|
|
ver_res = ""
|
|
logging.info("Extracting version field from ME blob")
|
|
command = ("hexdump -n 8 -s %s %s | cut -c 9- |sed 's/ //g' |" \
|
|
"sed 's/.\{4\}/&./g;s/ $//' | head -c19" % ( \
|
|
str(int(version_offset)), me_blob))
|
|
output = self.faft_client.system.run_shell_command_get_output(
|
|
command, True)
|
|
for each_word in output[0].split("."):
|
|
version = (int(each_word, 16))
|
|
ver_res = "".join((ver_res, "".join((str(version),"."))))
|
|
ver_res = ver_res[:-1]
|
|
logging.info("Version : %s" % ver_res)
|
|
return ver_res
|
|
|
|
def get_image_fwmain_me_rw_version(self,
|
|
bios,
|
|
region = "FW_MAIN_A"):
|
|
"""
|
|
Extract CSME RW version of the me_rw blob of the given
|
|
region in the given bios
|
|
|
|
@param bios: Bios path
|
|
@param region: region which contains me_rw blob
|
|
@returns the CSME RW version string
|
|
|
|
"""
|
|
# Extract me_rw.metadata and check version.
|
|
cbfs_name = "me_rw.metadata"
|
|
temp_dir = self.faft_client.system.create_temp_dir()
|
|
me_blob = os.path.join(temp_dir, cbfs_name)
|
|
|
|
cmd_status = self.faft_client.updater.cbfs_extract(cbfs_name,
|
|
'',(region, ),
|
|
me_blob,'x86',bios)
|
|
|
|
if cmd_status is None:
|
|
self.faft_client.system.remove_dir(temp_dir)
|
|
raise error.TestError("Failed to extract ME blob from " \
|
|
"the given bios : %s" % bios)
|
|
|
|
version = self.extract_me_rw_version_from_bin(me_blob)
|
|
self.faft_client.system.remove_dir(temp_dir)
|
|
return version
|
|
|
|
def get_current_me_rw_version(self):
|
|
"""
|
|
Reads the current active CSME RW Version from coreboot logs
|
|
|
|
@returns the CSME RW version string
|
|
|
|
"""
|
|
logging.info("Extracting cselite version info from coreboot logs")
|
|
command = "cbmem -1 | grep 'cse_lite:'"
|
|
output = self.faft_client.system.run_shell_command_get_output(
|
|
command, True)
|
|
logging.info(output)
|
|
# Offset of rw portion in ME region
|
|
me_cse_rw_info = re.search(r"(cse_lite: RW version = )" \
|
|
"([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*)","".join(output))
|
|
|
|
if me_cse_rw_info:
|
|
me_version = me_cse_rw_info.group(2)
|
|
else:
|
|
raise error.TestError("cse_lite RW info not"
|
|
" found in coreboot logs!")
|
|
return me_version
|
|
|
|
def verify_me_version(self, expected_version, expected_slot):
|
|
"""
|
|
Reads the current active CSME RW Version from coreboot logs
|
|
and compares with expected version
|
|
|
|
@param expected_version: Expected CSME RW Version string
|
|
@returns True is matching else False
|
|
|
|
"""
|
|
me_version = self.get_current_me_rw_version()
|
|
command = "crossystem mainfw_act"
|
|
output = self.faft_client.system.run_shell_command_get_output(
|
|
command, True)
|
|
main_fw_act = output[0]
|
|
|
|
logging.info("Expected mainfw_act : %s\n" \
|
|
"Current mainfw_act : %s\n" \
|
|
"Expected ME RW Version : %s\n" \
|
|
"Current ME RW Version : %s\n" % (
|
|
expected_slot, main_fw_act,
|
|
expected_version, me_version))
|
|
|
|
if (expected_version not in me_version) or \
|
|
(expected_slot not in main_fw_act):
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def prepare_shellball(self, bios_image, append = None):
|
|
"""Prepare a shellball with the given bios image.
|
|
|
|
@param bios_image: bios image with shellball to be created
|
|
@param append: string to be updated with shellball name
|
|
"""
|
|
logging.info("Preparing shellball with %s" % bios_image)
|
|
self.faft_client.updater.reset_shellball()
|
|
# Copy the given bois to shellball
|
|
extract_dir = self.faft_client.updater.get_work_path()
|
|
bios_rel = self.faft_client.updater.get_bios_relative_path()
|
|
bios_shell = os.path.join(extract_dir, bios_rel)
|
|
command = "cp %s %s" % (bios_image, bios_shell)
|
|
output = self.faft_client.system.run_shell_command_get_output(
|
|
command, True)
|
|
if output:
|
|
raise error.TestError("File not found!: %s" % bios_image)
|
|
# Reload and repack the shellball
|
|
self.faft_client.updater.reload_images()
|
|
self.faft_client.updater.repack_shellball(append)
|
|
|
|
def run_shellball(self, append):
|
|
"""Run chromeos-firmwareupdate
|
|
|
|
@param append: additional piece to add to shellball name
|
|
"""
|
|
|
|
# make sure we restore firmware after the test, if it tried to flash.
|
|
self.restore_required = True
|
|
|
|
# Update only host firmware
|
|
options = ['--host_only', '--wp=1']
|
|
logging.info("Updating RW firmware using " \
|
|
"chromeos_firmwareupdate")
|
|
logging.info("Update command : chromeos_firmwareupdate-%s --mode=%s "
|
|
" %s" % (append,self.MODE,' '.join(options)))
|
|
result = self.run_chromeos_firmwareupdate(
|
|
self.MODE, append, options, ignore_status = True)
|
|
|
|
if result.exit_status == 255:
|
|
raise error.TestError("DUT network dropped during update.")
|
|
elif result.exit_status != 0:
|
|
if ('Good. It seems nothing was changed.' in result.stdout):
|
|
logging.info("DUT already matched the image; updater aborted.")
|
|
else:
|
|
raise error.TestError("Firmware updater unexpectedly" \
|
|
"failed (rc=%s)" % result.exit_status)
|
|
|
|
def run_once(self):
|
|
if not self.faft_config.intel_cse_lite:
|
|
raise error.TestNAError("CSELite feature not supported " \
|
|
"on this device. Test Skipped")
|
|
|
|
# Read current bios from SPI and create a backup copy
|
|
self.read_current_bios_and_save()
|
|
|
|
# Check fmap scheme of the bios read from SPI
|
|
spi_bios_fmap_ver = self.check_fmap_format(self.spi_bios)
|
|
|
|
if not self.check_if_me_blob_exist_in_image(self.spi_bios):
|
|
raise error.TestError("Test setup issue : me_rw blob is not " \
|
|
"present in the current bios.!")
|
|
|
|
# Check fmap scheme of the default bios in shellball
|
|
downgrade_bios_fmap = self.check_fmap_format(self.downgrade_bios)
|
|
|
|
# Check if me_rw blob is present in FW_MAIN
|
|
if not self.check_if_me_blob_exist_in_image(self.downgrade_bios):
|
|
raise error.TestError("Test setup issue : me_rw blob is not " \
|
|
"present in downgrade bios.")
|
|
|
|
# Check if both of the bios versions use same fmap structure for me_rw
|
|
if downgrade_bios_fmap not in spi_bios_fmap_ver:
|
|
raise error.TestError("Test setup issue : FMAP format is " \
|
|
"different in current and downgrade bios.")
|
|
|
|
# Get the version of me_rw in the downgrade bios
|
|
downgrade_me_version = self.get_image_fwmain_me_rw_version( \
|
|
self.downgrade_bios)
|
|
|
|
# Get the version of me_rw in the spi bios
|
|
spi_me_version = self.get_image_fwmain_me_rw_version(self.spi_bios)
|
|
|
|
# Get active CSME RW version from cbmem -1
|
|
active_csme_rw_version = self.get_current_me_rw_version()
|
|
|
|
logging.info("Active CSME RW Version : %s\n" \
|
|
"FW main CSME RW Version SPI Image : %s\n" \
|
|
"FW main CSME RW Version downgrade Image: %s\n" % (
|
|
active_csme_rw_version, spi_me_version,
|
|
downgrade_me_version ))
|
|
|
|
# Abort if downgrade me_rw version is same as spi me_rw version
|
|
if (spi_me_version in downgrade_me_version):
|
|
raise error.TestError("Test setup issue : CSME RW version is " \
|
|
"same in both of the images.")
|
|
|
|
for slot in ["A", "B"]:
|
|
operation = "downgrade"
|
|
# Create a shellball with downgrade bios
|
|
self.prepare_shellball(self.downgrade_bios, operation)
|
|
|
|
logging.info("Downgrading RW section. Downgrade ME " \
|
|
"Version: %s" % downgrade_me_version)
|
|
# Run firmware updater downgrade the bios RW
|
|
self.run_shellball(operation)
|
|
|
|
# Set fw_try_next to slot and reboot to trigger csme update
|
|
logging.info("Setting fw_try_next to %s: " % slot)
|
|
self.faft_client.system.set_fw_try_next(slot)
|
|
self.switcher.mode_aware_reboot(reboot_type = 'cold')
|
|
|
|
# Check if the Active CSME RW version changed to downgrade version
|
|
if not self.verify_me_version(downgrade_me_version, slot):
|
|
raise error.TestError("CSME RW Downgrade using "
|
|
"FW_MAIN_%s is Failed!" % slot)
|
|
logging.info("CSME RW Downgrade using FW_MAIN_%s is "
|
|
"successful" % slot)
|
|
|
|
operation = "upgrade"
|
|
# Create a shellball with the original spi bios
|
|
self.prepare_shellball(self.spi_bios, operation)
|
|
|
|
logging.info("Upgrading RW Section. Upgrade ME " \
|
|
"Version: %s" % spi_me_version)
|
|
# Run firmware updater and update RW section with shellball
|
|
self.run_shellball(operation)
|
|
|
|
# Set fw_try_next to slot and reboot to trigger csme update
|
|
logging.info("Setting fw_try_next to %s: " % slot)
|
|
self.faft_client.system.set_fw_try_next(slot)
|
|
self.switcher.mode_aware_reboot(reboot_type = 'cold')
|
|
|
|
# Check if the Active CSME RW version changed to original version
|
|
if not self.verify_me_version(spi_me_version, slot):
|
|
raise error.TestError("CSME RW Upgrade using "
|
|
"FW_MAIN_%s is Failed!" % slot)
|
|
logging.info("CSME RW Upgrade using FW_MAIN_%s is "
|
|
"successful" % slot)
|