blob: 001776edf8231dad22b646cf1f45462a7cf321b5 [file] [log] [blame]
# Copyright 2017 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A module to manipulate device manufacturing information.
The "Device Data" is a set of mapping data storing manufacturing information for
DUT (device under test), including peripheral information (for example if
touchscreen should be available or not), per-device provisioned data (also known
as Vital Product Data - VPD; for example serial number or shipping region), test
status (has passed SMT, FATP, or other stations).
The Device Data is shared by tests in Chrome OS Factory Software, the test
harness "Goofy" and the "Shopfloor Service API". The values usually come from
pre-defined values, shopfloor backend, or set by tests using manual selection
or barcode scanner (especially serial numbers).
Device Data can be considered as a mapping or dictionary, with following keys:
- ``serials``: A dictionary for serial numbers of device, components, and
mainboard. All serial numbers here will be logged by `testlog`, including:
- ``serial_number``: The serial number of "device" itself (printed on device
panel).
- ``mlb_serial_number``: The serial number of main logic board (mainboard).
- ``component``: A dictionary to indicate what peripherals should exist, for
example:
- ``has_touchscreen=True``: A touch screen should be available.
- ``has_dram=2``: Two DRAM components should be available.
- ``vpd``: A dict for what VPD values need to be set, including:
- ``ro``: VPD values in RO section (RO_VPD), usually including:
- ``region``: Region code as defined in http://go/cros-regions.
- ``rw``: VPD values in RW section (RW_VPD), usually including:
- ``ubind_attribute``: User registration code.
- ``gbind_attribute``: Group registration code.
- ``hwid``: A value of probed Hardware ID.
- ``factory``: A dict for manufacturing flow control, used by shopfloor
backends. See Shopfloor Service API for more details.
- ``feature management``: A dictionary for what feature management will be
reading, including:
- ``feature management``: boolean value specifying if panel A contains
logo.
- ``hw_compliance_version``: Int value representing the version in factory?
For example, a typical device usually has both device serial number and
main board serial number, region VPD, registration codes, thus the device data
will be set to::
{
'serials': {
'serial_number': 'SN1234567890',
'mlb_serial_number': 'MLB1234567890'
},
'vpd': {
'ro': {
'region': 'us'
},
'rw': {
'ubind_attribute': '12345',
'gbind_attribute': '54321',
}
}
}
Using Device Data
-----------------
Device Data is internally stored as Python dict inside shelves, and provided by
`cros.factory.test.state` module via RPC calls.
To get all data as single dict,
use ``GetAllDeviceData``. To get partial data, use ``GetDeviceData`` with key
names joined using dot. For example, to fetch only the ``ro`` values inside
``vpd``::
GetDeviceData('vpd.ro')
The key names are also defined in this module. All constants starting with
``KEY_`` are complete key names for ``GetDeviceData`` to use. Constants starting
with ``NAME_`` are key names (no dot) of the single dict. For example, following
calls are equivalent if ``vpd.ro.region`` exists::
GetDeviceData('vpd.ro.region')
GetDeviceData('vpd').get('ro').get('region')
GetDeviceData(KEY_VPD).get(NAME_RO).get(NAME_REGION)
GetDeviceData(KEY_VPD_RO).get(NAME_REGION)
GetDeviceData(KEY_VPD_REGION)
If ``vpd.ro`` does not exist, ``get('ro')`` will return None so you can't invoke
another ``get('region')`` on it. So using the complete key path
(``vpd.ro.region``) provides an easier way to retrieve single value without
worrying if the intermediate dictionaries exist or not.
Using Serial Number
-------------------
There are some special helpers to access serial number. ``GetSerialNumber`` and
``SetSerialNumber`` expect names of serial numbers (``NAME_*``). But as a syntax
sugar, they will also accept keys with ``KEY_SERIALS`` prefixed. For example,
following calls are equivalent::
GetSerialNumber('serial_number')
GetSerialNumber(NAME_SERIAL_NUMBER)
GetSerialNumber(KEY_SERIAL_NUMBER)
GetDeviceData(KEY_SERIAL_NUMBER)
Note when setting serial numbers (``SetSerialNumber``), a value evaluates to
false (None, false, empty string...) will **delete** the stored serial number.
API Spec
--------
"""
import collections.abc
import logging
import os
from typing import Dict, Union
# pylint: disable=wildcard-import,unused-wildcard-import
from cros.factory.test.device_data_constants import *
from cros.factory.test import event
from cros.factory.test.rules import privacy
from cros.factory.test import state
from cros.factory.utils import config_utils
from cros.factory.utils import shelve_utils
# Helper utility for manipulating keys.
JoinKeys = shelve_utils.DictKey.Join
def _GetInstance():
"""An internal helper utility to get DEVICE_DATA from state module."""
return state.GetInstance().data_shelf[state.KEY_DEVICE_DATA]
def CheckValidDeviceDataKey(key, key_prefix=None):
"""Checks if given key is a valid device data key.
Args:
key: A string of key for device data.
key_prefix: Key must start with this token.
Raises:
KeyError if the key is not valid.
"""
prefix, dot, postfix = key.partition('.')
if key_prefix and prefix != key_prefix:
raise KeyError(f'Key {key} must start with {key_prefix}.')
top_level_keys = [
KEY_SERIALS, KEY_HWID, KEY_VPD, KEY_COMPONENT, KEY_FACTORY, KEY_FM
]
if prefix not in top_level_keys:
raise KeyError(f'Key {key} must start with one of {top_level_keys!r}')
if prefix == KEY_SERIALS:
if '.' in postfix:
raise KeyError(f'Serial number name must not contain dots: {postfix}')
elif prefix == KEY_HWID:
if dot != '':
raise KeyError(f'HWID must not have sub keys: {postfix}')
elif prefix == KEY_VPD:
vpd_sections = [NAME_RO, NAME_RW]
section, unused_dot, name = postfix.partition('.')
if section not in vpd_sections:
raise KeyError(f'VPD key [{key}] must be in the sections: {vpd_sections}')
if '.' in name:
raise KeyError(f'VPD entry name must not contain dots: {name}')
return True
def GetDeviceData(key: str, default=None, data_type: Union[int, str,
None] = None,
throw_if_none: bool = False):
"""Returns the device data associated by key.
Args:
key: A string of key to access device data.
default: The default value if key does not exist.
data_type: The returned type of the function. We may save integer as an hex
string in the device data. Use data_type to convert it into int. Do no
conversion if data_type is None.
throw_if_none: If set, throw a KeyError if the key is not found.
Returns:
Associated value if key exists in device data, otherwise the value specified
by default. Defaults to None.
"""
if data_type not in (int, str, None):
raise TypeError('data_type must be int, str, or None.')
if not isinstance(key, str):
raise KeyError('key must be a string')
value = _GetInstance()[key].Get(default)
if throw_if_none and value is None:
raise KeyError(f'No device data ({key})')
if data_type is None:
return value
if data_type == str:
return str(value)
if isinstance(value, int):
return value
if isinstance(value, str):
# This can convert both 10-based and 16-based starting with '0x'.
return int(value, 0)
raise ValueError('The value in device-data is not an integer nor a '
'string representing an integer literal in radix base.')
def GetAllDeviceData():
"""Returns all device data in a single dict."""
return _GetInstance().Get({})
def GetDeviceDataSelector():
"""Returns the data shelf selector rooted at device data.
This is primarily used by invocation module to resolve TestListArgs.
"""
return _GetInstance()
def _PostUpdateSystemInfo():
if not os.getenv(event.CROS_FACTORY_EVENT):
logging.debug('No CROS_FACTORY_EVENT found, ignore posting event.')
return
try:
event.PostNewEvent(event.Event.Type.UPDATE_SYSTEM_INFO)
except Exception:
logging.exception('Failed to post update event')
def DeleteDeviceData(delete_keys, optional=False):
"""Deletes given keys from device data.
Args:
delete_keys: A list of keys (or a single string) to be deleted.
optional: False to raise a KeyError if not found.
Returns:
The updated dictionary.
"""
if isinstance(delete_keys, str):
delete_keys = [delete_keys]
logging.info('Deleting device data: %s', delete_keys)
delete_device_keys = [shelve_utils.DictKey.Join(state.KEY_DEVICE_DATA, key)
for key in delete_keys]
instance = state.GetInstance()
instance.DataShelfDeleteKeys(delete_device_keys, optional)
data = instance.DataShelfGetValue(state.KEY_DEVICE_DATA, True) or {}
logging.info('Updated device data; complete device data is now %s',
privacy.FilterDict(data))
_PostUpdateSystemInfo()
return data
def VerifyDeviceData(device_data):
"""Verifies whether all fields in the device data dictionary are valid.
Args:
device_data: A dict with key/value pairs to verify.
Raises:
`ValueError` if the device data is invalid.
"""
for key, value in device_data.items():
if key.startswith(JoinKeys(KEY_COMPONENT, 'has_')):
if value is not None and not isinstance(value, (bool, int)):
raise ValueError('Values in the "component" domain should be None or'
' in type of either `bool` or `int`.')
def UpdateDeviceData(new_device_data):
"""Updates existing device data with given new dict data.
Args:
new_device_data: A dict with key/value pairs to update. Old values
are overwritten.
Returns:
The updated dictionary.
"""
new_device_data = FlattenData(new_device_data)
logging.info('Updating device data: setting %s',
privacy.FilterDict(new_device_data))
VerifyDeviceData(new_device_data)
instance = state.GetInstance()
instance.DataShelfUpdateValue(state.KEY_DEVICE_DATA, new_device_data)
data = instance.DataShelfGetValue(state.KEY_DEVICE_DATA, True) or {}
logging.info('Updated device data; complete device data is now %s',
privacy.FilterDict(data))
_PostUpdateSystemInfo()
return data
def _GetSerialNumberKey(name):
"""Returns a full path or serial number key for Device Data API to access."""
if '.' not in name:
return JoinKeys(KEY_SERIALS, name)
CheckValidDeviceDataKey(name, KEY_SERIALS)
return name
def _GetSerialNumberName(key):
"""Returns the name part of serial number key."""
return _GetSerialNumberKey(key).partition('.')[2]
def GetAllSerialNumbers():
"""Returns all serial numbers available in device data as dict."""
return GetDeviceData(KEY_SERIALS, {})
def ClearAllSerialNumbers():
"""Clears all serial numbers stored in device data."""
DeleteDeviceData([KEY_SERIALS], optional=True)
def GetSerialNumber(name=NAME_SERIAL_NUMBER):
"""Returns a serial number (default to device serial number)."""
return GetDeviceData(_GetSerialNumberKey(name))
def SetSerialNumber(name, value):
"""Sets a serial number to give nvalue.
Args:
name: A string to indicate serial number name.
value: A string representing the serial number, or anything evaluated
as False to delete the serial number.
"""
UpdateSerialNumbers({name: value})
def UpdateSerialNumbers(dict_):
"""Updates stored serial numbers by given dict.
Args:
dict_: A mapping of serial number names and values to change.
A value evaluated as False will delete the serial number from device
data.
"""
assert isinstance(dict_, dict)
new_dict = {}
keys_to_delete = []
for key, value in dict_.items():
if value:
new_dict[_GetSerialNumberName(key)] = value
else:
keys_to_delete.append(_GetSerialNumberKey(key))
if dict_:
UpdateDeviceData({KEY_SERIALS: new_dict})
if keys_to_delete:
DeleteDeviceData(keys_to_delete, optional=True)
def GetOEMName():
"""Returns OEM name in device data."""
return GetDeviceData(KEY_OEM_NAME)
def FlattenData(data, parent=''):
"""An helper utility to flatten multiple layers of dict into one dict.
For example, {'a': {'b': 'c'}} => {'a.b': 'c'}
Args:
data: The dict type data to be flattened.
parent: A string to encode as key prefix for recursion.
Returns:
A flattened dict.
"""
items = []
for k, v in data.items():
new_key = JoinKeys(parent, k) if parent else k
if isinstance(v, collections.abc.Mapping):
items.extend(FlattenData(v, new_key).items())
else:
items.append((new_key, v))
return dict(items)
def LoadConfig(config_name=None):
"""Helper utility to load a JSON config that represents device data.
Args:
config_name: A string for name to be passed to config_utils.LoadConfig.
Returns:
A dictionary as device data (already flattened).
"""
return FlattenData(
config_utils.LoadConfig(config_name, schema_name='device_data'))
def UpdateDeviceDataFromVPD(key_map, vpd_data):
"""Update device data from VPD data.
Please see pytest `read_device_data_from_vpd` for more details.
For both `key_map` and `vpd_data`, they should be a dictionary, with at most
two keys: 'ro' and 'rw' (NAME_RO and NAME_RW). key_map['ro'] and
key_map['rw'] should follow the format of ro_key_map and rw_key_map in
`read_device_data_from_vpd`. If key_map is None, a default key_map will be
used.
"""
if key_map is None:
key_map = {
NAME_RO: DEFAULT_RO_VPD_KEY_MAP,
NAME_RW: DEFAULT_RW_VPD_KEY_MAP,
}
assert isinstance(key_map, dict)
assert isinstance(vpd_data, dict)
def _MatchKey(rule, vpd_key):
expected_key = rule[0]
if expected_key.endswith('*'):
return vpd_key.startswith(expected_key[:-1])
return vpd_key == expected_key
data = {}
for section in [NAME_RO, NAME_RW]:
if section not in key_map:
continue
vpd_section = vpd_data.get(section, {})
for rule in key_map[section].items():
for vpd_key in vpd_section:
if _MatchKey(rule, vpd_key):
data_key = _DeriveDeviceDataKey(rule, vpd_key)
if vpd_section[vpd_key].upper() in ['TRUE', 'FALSE']:
data[data_key] = (vpd_section[vpd_key].upper() == 'TRUE')
else:
data[data_key] = vpd_section[vpd_key]
UpdateDeviceData(data)
def _DeriveDeviceDataKey(rule, vpd_key):
"""Derive device data key from `vpd_key` according to `rule`.
This is a helper function for UpdateDeviceDataFromVPD.
Args:
rule: a tuple (<VPD key>, <device data key>), for example:
('serial_number', 'serials.serial_number'). If VPD key ends with '*',
maps all VPD starts with the prefix to device data. For example,
('foo.*', 'bar') will maps all 'foo.*' in VPD to 'bar.*' in device data.
That is, 'foo.region' will become 'bar.region'.
vpd_key: use this VPD key to derive device key.
"""
expected_key = rule[0]
if not expected_key.endswith('*'):
return rule[1]
# Remove the prefix.
vpd_key = vpd_key[len(expected_key[:-1]):]
# Pre-pend new prefix.
return JoinKeys(rule[1], vpd_key)
class InvalidFeatureData(Exception):
"""Exception to raise when data is incorrect"""
class InconsistentFeatureData(Exception):
"""Raised when data is not matching with existing device_data"""
def GetFeatureDeviceData() -> Dict[str, Union[int, bool]]:
"""Returns Feature Management"""
return GetDeviceData(KEY_FM, {}) # type: ignore
def VerifyFeatureData(data: dict) -> bool:
if len(data.keys()) != 2:
return False
if NAME_CHASSIS_BRANDED not in data or NAME_HW_COMPLIANCE_VERSION not in data:
return False
if not isinstance(data[NAME_CHASSIS_BRANDED], bool):
return False
if not isinstance(data[NAME_HW_COMPLIANCE_VERSION], int):
return False
return True
def SetFeatureDeviceData(new_data: Dict) -> None:
"""Sets the data of feature device."""
if not VerifyFeatureData(new_data):
raise InvalidFeatureData
UpdateDeviceData({KEY_FM: new_data})
def SetBrandedChassisData(branded: bool) -> None:
if not isinstance(branded, bool):
raise InvalidFeatureData
UpdateDeviceData({KEY_FM_CHASSIS_BRANDED: branded})
def SetHWComplianceVersionData(version: int) -> None:
if not isinstance(version, int):
raise InvalidFeatureData
UpdateDeviceData({KEY_FM_HW_COMPLIANCE_VERSION: version})