blob: 5403c807703a5f2b4545820e51a40a46adf178d1 [file] [log] [blame]
# Copyright 2020 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Detachable base update test
Description
-----------
For detachable projects, the detachable base (as a USB device) may need to be
updated with its standalone EC and touchpad firmware.
This test leverages ``hammerd``, which is the dedicated daemon for updating
detachable base firmware, to help factory automize firmware update and basic
verification.
Test Procedure
--------------
1. Test will try to locate detachable base with the given USB device info.
If the info is not provided from test lists, the test will try to get it
from ``cros_config``.
2. Hammerd is invoked to update base's EC and touch firmware. If the path to EC
or touch firmware is not specified, the test will extract the firmware from
the release or test rootfs depending on ``from_release``.
3. Verify base is properly updated by probing base info and doing some
preliminary checkings.
Note that one might want to disable hammerd being inited on boot by configuring
upstart. See `here
<https://chromium.googlesource.com/chromiumos/platform/factory/+/HEAD/init/preinit.d/inhibit_jobs/README.md>`_
for detail.
In addition, it's recommended to configure detachable base info in
chromeos-config (instead of test lists) with following properties provided:
- ec-image-name
- touch-image-name
- vendor-id
- product-id
- usb-path
So that the it can be maintained in the standalone config and be easily shared
across the system.
Dependency
----------
- chromeos_config (cros_config)
- hammerd
- usb_updater2
- hammer_info.py
Examples
--------
If detachable base information is ready in cros_config and user would like to
update using the images extracted from release rootfs::
{
"pytest_name": "update_detachable_base"
}
If explicitly supplying detachable base info (Krane for example)::
{
"pytest_name": "update_detachable_base",
"args": {
"from_release": false,
"usb_path": "1-1.1",
"product_id": 20540,
"vendor_id": 6353,
"ec_image_path": "/lib/firmware/masterball.fw",
"touchpad_image_path": "/lib/firmware/masterball-touch.fw"
}
}
"""
import logging
import os.path
import re
from cros.factory.device import device_utils
from cros.factory.test.i18n import _
from cros.factory.test import session
from cros.factory.test import test_case
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import process_utils
from cros.factory.utils import sync_utils
from cros.factory.utils import sys_utils
BASE_FW_DIR = '/lib/firmware'
ELAN_VENDOR_ID = 0x04f3
ST_VENDOR_ID = 0x0483
VENDOR_IDS = (ELAN_VENDOR_ID, ST_VENDOR_ID)
class UpdateDetachableBaseTest(test_case.TestCase):
related_components = tuple()
ARGS = [
Arg('from_release', bool, 'Find the firmwares from release rootfs.',
default=True),
Arg('usb_path', str, 'USB path for searching the detachable base.',
default=None),
Arg('i2c_path', str, 'I2C path for searching the detachable base.',
default=None),
Arg('product_id', int, 'Product ID of the USB device.', default=None),
Arg('vendor_id', int, 'Vendor ID of the USB device.', default=None),
Arg('ec_image_path', str,
f'Path to the EC firmware image file under {BASE_FW_DIR}.',
default=None),
Arg('touchpad_image_path', str,
f'Path to the touchpad image file under {BASE_FW_DIR}.',
default=None),
Arg('update', bool, 'Update detachable base FW (hammerd is needed)',
default=True),
Arg('verify', bool,
'Verify base info after FW update. (usb_updater2 is needed)',
default=True),
]
def setUp(self):
self.dut = device_utils.CreateDUTInterface()
# yapf: disable
self.ui.ToggleTemplateClass('font-large', True) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# Read preconfigured values from cros_config if args are not provided.
# yapf: disable
if self.args.usb_path is None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.usb_path = self.CrosConfig('usb-path', check_output=False) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.args.i2c_path is None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.i2c_path = self.CrosConfig('i2c-path', check_output=False) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.args.product_id is None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.product_id = int(self.CrosConfig('product-id')) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.args.vendor_id is None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.vendor_id = int(self.CrosConfig('vendor-id')) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.args.ec_image_path is None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.ec_image_path = self.dut.path.join( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
BASE_FW_DIR, self.CrosConfig('ec-image-name'))
# yapf: disable
if self.args.touchpad_image_path is None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.touchpad_image_path = self.dut.path.join( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
BASE_FW_DIR, self.CrosConfig('touch-image-name'))
assert self.args.usb_path or self.args.i2c_path # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: disable
self.device_id = f'{self.args.vendor_id:04x}:{self.args.product_id:04x}' # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def runDetachableTest(self):
# yapf: disable
if self.args.update: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.ui.SetState(_('Updating base firmware. Do not remove the base.')) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.UpdateDetachableBase()
session.console.info('Base firmware update done.')
# yapf: disable
if self.args.verify: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.ui.SetState(_('Verifying detachable base information...')) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# Sleep for a while, because usb_updater2 may not be able to read
# touchpad info right after FW is flashed.
self.Sleep(3)
# Getting info of touchpad on base / EC on base / target EC image
# respectively. b/146536191: Must query touchpad info before base EC
# info.
ec_info = self.GetBaseInfo()
# yapf: disable
fw_info = self.GetFirmwareInfo(self.args.ec_image_path) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.VerifyBaseInfo(ec=ec_info, fw=fw_info)
session.console.info('Detachable base verification done.')
def runTest(self):
# yapf: disable
self.ui.SetState(_('Please connect the detachable base.')) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
sync_utils.PollForCondition(poll_method=self.BaseIsReady, timeout_secs=60,
poll_interval_secs=1)
# yapf: disable
if self.args.from_release: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
with sys_utils.MountPartition(self.dut.partitions.RELEASE_ROOTFS.path,
dut=self.dut) as root:
logging.info('Get EC and touch FW images from the release rootfs.')
# yapf: disable
self.args.ec_image_path = self.dut.path.join( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
root, self.args.ec_image_path[1:]) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.touchpad_image_path = self.dut.path.join( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
root, self.args.touchpad_image_path[1:]) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.runDetachableTest()
else:
self.runDetachableTest()
@classmethod
def CrosConfig(cls, key, check_output=True):
"""Helper method for cros_config key retrieval.
Args:
key: The key under detachable-base path.
Returns:
The value of the provided key.
"""
return process_utils.SpawnOutput(['cros_config', '/detachable-base', key],
check_output=check_output, log=True)
def UpdateDetachableBase(self):
"""Main update method which calls hammerd to do base FW update.
Raises:
CalledProcessError if hammerd returns non-zero code.
"""
minijail0_cmd = ['/sbin/minijail0', '-e', '-N', '-p', '-l',
'-u', 'hammerd', '-g', 'hammerd', '-c', '0002']
if self.args.usb_path: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
device_path = f'--usb_path={self.args.usb_path}' # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
else:
device_path = f'--i2c_path={self.args.i2c_path}' # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
hammerd_cmd = [
'/usr/bin/hammerd',
'--at_boot=true',
'--force_inject_entropy=true',
# yapf: disable
'--update_if=always',
f'--product_id={int(self.args.product_id)}', # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
f'--vendor_id={int(self.args.vendor_id)}', # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
device_path,
# yapf: disable
f'--ec_image_path={self.args.ec_image_path}', # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
f'--touchpad_image_path={self.args.touchpad_image_path}' # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
]
try:
process_utils.Spawn(minijail0_cmd + hammerd_cmd,
log=True, check_call=True)
except process_utils.CalledProcessError as e:
# Note that hammerd prints log to stderr by default so reading stdout
# does not help.
# In addition, hammerd only prints log when stdin is a tty, so we can not
# read log from stderr when hammerd is invoked by Spawn() either.
# As a result, log can only be manually retrieved in hammerd.log.
exit_reason = {
1: 'kUnknownError',
10: 'kNeedUsbInfo',
11: 'kEcImageNotFound',
12: 'kTouchpadImageNotFound',
13: 'kUnknownUpdateCondition',
14: 'kConnectionError',
15: 'kInvalidFirmware',
16: 'kTouchpadMismatched',
}
if e.returncode in exit_reason:
logging.error('Hammerd exit reason: %s', exit_reason[e.returncode])
else:
logging.error('Hammerd exited due to unknown error.')
self.FailTask(
f'Hammerd update failed (exit status {int(e.returncode)}). Please '
f'check /var/log/hammerd.log for detail.')
def VerifyBaseInfo(self, ec, fw):
"""Verify base is updated properly by comparing its attributes with the
target FW.
Args:
ec: A dictionary of the keyboard info from hammer_info.py.
fw: A dictionary of the target EC FW info.
"""
self.assertEqual(
ec['ro_version'], fw['ro']['version'],
f"Base EC may not be properly updated: Base RO version "
f"{ec['ro_version']} ({fw['ro']['version']} expected).")
self.assertEqual(
ec['rw_version'], fw['rw']['version'],
f"Base EC may not be properly updated: Base RW version "
f"{ec['rw_version']} ({fw['rw']['version']} expected).")
self.assertIn(
int(ec['touchpad_pid'], 16), VENDOR_IDS,
f"Touchpad may not be properly updated: Vendor {ec['touchpad_pid']} "
f"(any of {[hex(x) for x in VENDOR_IDS]} expected).")
self.assertNotEqual(
ec['touchpad_fw_checksum'], '0x0000',
f"Touchpad may not be properly updated: checksum "
f"{ec['touchpad_fw_checksum']} (unexpected).")
self.assertGreaterEqual(
int(ec['key_version']), 3,
f'key version not greater or higher than MP (>= 3), used key version: '
f"{ec['key_version']}")
self.assertEqual(ec['wp_screw'], 'True', 'Hardware WP not enabled')
self.assertEqual(ec['wp_all'], 'True', 'Software WP not enabled')
def BaseIsReady(self):
if self.args.usb_path: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
try:
process_utils.CheckCall(['lsusb', '-d', self.device_id])
except process_utils.CalledProcessError:
return 0
return 1
return os.path.exists(os.path.join("/sys/bus/i2c/devices/",
self.args.i2c_path)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
def GetBaseInfo(self):
"""Retrieve and parse on-board EC info.
Returns:
A dictionary containing on-board EC info.
"""
output = process_utils.LogAndCheckOutput(['hammer_info.py'])
return dict(re.findall(r'(\S+)="(\S+?)"', output))
def GetFirmwareInfo(self, fw_path):
"""Retrieve and parse given EC FW info.
Args:
fw_path: The absolute path to target EC FW image.
Returns:
A dictionary containing target EC FW info.
"""
key_trans = {
'v': 'version',
'rb': 'rollback',
'off': 'offset',
'kv': 'key_version',
}
# yapf: disable
res = {'ro': {}, 'rw': {}} # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
for line in process_utils.LogAndCheckOutput(['usb_updater2', '-b',
fw_path]).splitlines():
mode, *rest = line.split()
if mode in ('RO', 'RW'):
for kv in rest:
k, v = kv.split('=', 1)
if k in key_trans:
res[mode.lower()][key_trans[k]] = v.strip()
return res