blob: 97b2fb3bcb622e8de1c0e8c3a95837e15965cde5 [file] [log] [blame] [edit]
# Copyright 2012 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""System configuration module."""
import collections
import copy
import functools
import glob
import logging
import os
import re
import xml.etree.ElementTree
# valid tags in system config xml. Any others will be ignored
MAP_TAG = "map"
CONTROL_TAG = "control"
CLOBBER_ATTR = "clobber_ok"
CLOBBER_NEVER = "never"
CLOBBER_PATCH = "patch"
CLOBBER_FULL = "full"
CONTENT_TAG = "content"
CONTENT_ITEM_TAG = "item"
CONTENT_ITEM_KEY_ATTR = "key"
CONTENT_ITEM_TYPE_ATTR = "type"
CONTENT_PARAM = "CONTENT"
SYSCFG_TAG_LIST = [MAP_TAG, CONTROL_TAG]
ALLOWABLE_INPUT_TYPES = {"float": float, "int": int, "str": str}
# A control to use when set/get is explicitly not defined for a control.
UNDEF_CONTROL_DICT = {"drv": "undefined", "interface": "servo", "input_type": "str"}
# Valid pattern for control names and aliases
IDENTIFIER_RE = re.compile(r"[a-z][a-z0-9_]+")
# TODO(coconutruben): figure out if it's worth it to rename this so that it
# removes the 'stutter'
class SystemConfigError(Exception):
"""Error class for SystemConfig."""
class SystemConfig:
"""SystemConfig Class.
System config files describe how to talk to various pieces on the device under
test. The system config may be broken up into multiple file to make it easier
to share configs among similar DUTs. This class has the support to take in
multiple SystemConfig files and treat them as one unified structure
SystemConfig files are written in xml and consist of four main elements
0. Include : Ability to include other config files
<include>
<name>servo_loc.xml</name>
</include>
NOTE, All includes in a file WILL be sourced prior to any other elements in
the XML.
1. Map : Allow user-friendly naming for things to abstract
certain things like on=0 for things that are assertive low on
actual h/w
<map>
<name>onoff_i</name>
<doc>assertive low map for on/off</doc>
<params on="0" off="1" />
</map>
2. Control : Bulk of the system file. These elements are
typically gpios, adcs, dacs which allow either control or sampling
of events on the h/w. Controls should have a 1to1 correspondence
with hardware elements between control system and DUT.
<control>
<name>warm_reset</name>
<doc>Reset the device warmly</doc>
<params interface="1" drv="gpio" offset="5" map="onoff_i" />
</control>
Some controls also use the a <content> element inside the <params> element as
input. This text, when present, is interpreted into an arbitrarily nested
structure of Python dict and list objects, ultimately containing str and None
values. For example:
<control>
<name>my_control</name>
<doc>Does cool stuff!</doc>
<params drv="mydriver">
<content>
<item key="somefield">5</item>
<item key="list_field">
<item>one two three</item>
<item></item>
<item>foobar</item>
</item>
</content>
</params>
</control>
That content would get parsed into:
{'somefield': '5',
'list_field': ['one two three', None, 'foobar']}
Or:
{'somefield': '5',
'list_field': ['one two three', '', 'foobar']}
It is not guaranteed whether empty <item> text results in None or '' (empty
string). Drivers should handle either case and not discriminate between them.
The <content> element is allowed to directly contain text instead of nested
elements. For example:
<control>
<name>my_control</name>
<doc>Does cool stuff!</doc>
<params drv="mydriver">
<content>-29.5</content>
</params>
</control>
That content would get parsed into:
'-29.5'
Rules for <content> sections:
* <item> is the only element permitted within <content> or <item>.
* If one <item> in a section uses key= attribute, then all must.
* Use of key= attribute in <item> indicates a map entry, which gets placed
into a Python dict.
* The behavior with duplicate keys in a map is undefined (and could be or
become an error).
* Use of <item> without key= attribute indicates a list.
* The behavior if map and list <item> are mixed together in one parent
element is undefined (and could be or become an error).
* The behavior if any attributes not described above are used is undefined
(and could be or become an error).
* When <content> or <item> contains nested elements then any text content
directly in the parent element should be whitespace-only, and is ignored.
* The behavior with non-whitespace text alongside nested <item> elements is
undefined (and could be or become an error).
* There is no policy limit to how deep <item> can be nested, however there
may be practical implementation limits, don't go nuts.
The structure enforced by <content> / <item> is designed to be easily ported
to other possible config file formats besides XML, and it avoids exposing
drivers to XML.
Public Attributes:
control_tags: a dictionary of each base control and their tags if any
aliases: a dictionary of an alias mapped to its base control name
syscfg_dict: 3-deep dictionary created when parsing system files. Its
organized as [tag][name][type] where:
tag: map | control
name: string name of tag element
type: data type of payload either, doc | get | set presently
doc: string describing the map or control
get: a dictionary for getting values from named control
set: a dictionary for setting values to named control
hwinit: list of control tuples (name, value) to be initialized in order
Private Attributes:
_loaded_xml_files: set of filenames already loaded to avoid sourcing XML
multiple times.
"""
def __init__(self):
"""SystemConfig constructor."""
self._logger = logging.getLogger("SystemConfig")
self.control_tags = collections.defaultdict(set)
self.aliases = {}
self.syscfg_dict = collections.defaultdict(dict)
self.hwinit = []
self._loaded_xml_files = set()
self._board_cfg = None
def find_cfg_file(self, filename):
"""Find the filename for a system XML config file.
If the provided `filename` names a valid file, use that.
Otherwise, `filename` must name a file in the 'data'
subdirectory stored with this module.
Returns the path selected as described above; if neither of the
paths names a valid file, return `None`.
Args:
filename: string of path to system file ( xml )
Returns:
string full path of |filename| if it exists, otherwise None
"""
if os.path.isfile(filename):
return filename
default_path = os.path.join(os.path.dirname(__file__), "data")
fullname = os.path.join(default_path, filename)
if os.path.isfile(fullname):
return fullname
return None
@staticmethod
def tag_string_to_tags(tag_str):
"""Helper to split tag string into individual tags."""
return tag_str.split(",")
def get_all_cfg_names(self):
"""Return all XML config file names.
Returns:
A list of file names.
"""
exclude_re = re.compile(r"servo_.*_overlay\.xml")
pattern = os.path.join(os.path.dirname(__file__), "data", "*.xml")
cfg_names = []
for name in glob.glob(pattern):
name = os.path.basename(name)
if not exclude_re.match(name):
cfg_names.append(name)
return cfg_names
def set_board_cfg(self, filename):
"""Save the filename for the board config."""
self._board_cfg = filename
def get_board_cfg(self):
"""Return the board filename."""
return self._board_cfg
@staticmethod
def _parse_content(content):
"""Parse a <content> structure from a control's params element.
Args:
content: xml.etree.ElementTree.Element - the <content> XML element
stack: [(element, callback)] - list of 2-item tuples, each containing:
element: xml.etree.ElementTree.Element - <content> or <item> XML element
callback: callable(object) - This will be called exactly once, in order
Returns:
None or str or list or dict
"""
if content is None:
return None
retval_list = []
# [(element, callback)] - list of 2-item tuples of:
# element: xml.etree.ElementTree.Element - <content> or <item> XML element
# callback: callable(object) - This will be called exactly once, with the
# value to use for this element.
stack = [(content, retval_list.append)]
while stack:
element, callback = stack.pop()
nested = element.findall(CONTENT_ITEM_TAG)
if not nested:
this = element.text
elif CONTENT_ITEM_KEY_ATTR in nested[0].attrib:
this = {}
for item in nested:
key = item.attrib[CONTENT_ITEM_KEY_ATTR]
stack.append((item, functools.partial(this.setdefault, key)))
else:
this = []
for item in reversed(nested):
stack.append((item, this.append))
callback(this)
assert len(retval_list) == 1
return retval_list[0]
def _check_controls_for_drv(self):
"""Check that every control has a driver configured.
Raises:
SystemConfigError: A control is missing get or set driver configuration.
"""
for name, control_dict in sorted(self.syscfg_dict[CONTROL_TAG].items()):
for cmd, key in ("get", "get_params"), ("set", "set_params"):
if "drv" not in control_dict[key]:
raise SystemConfigError(
'%s %r cmd="%s" has no driver configured (drv= attribute)'
% (CONTROL_TAG, name, cmd)
)
def add_cfg_file(self, name_prefix, filename):
"""Add system config file to the system config object.
Each design may rely on multiple system files so need to have the facility
to parse them all.
For example, we may have a:
1. default for all controls that are the same for each of the
control systems
2. default for a particular DUT system's usage across the
connector
3. specific one for particular version of DUT (evt,dvt,mp)
4. specific one for a one-off rework done to a system
Special key parameters in config files:
clobber_ok: signifies this control may _clobber_ an existing definition
of the same name. If its value is "full" then parameters from the
clobbered control are completely thrown away, otherwise only those
which are also specified in this control will be replaced.
clobber_ok: Gives special instructions for how to reconcile an existing
control definition with the same name or alias. By default, if this
is not specified, attempting to redefine a control is an error.
Supported values:
"full": This control will always be defined, and will completely
replace any existing control with the same name or alias
"patch": This control will update the params of an existing control,
but this will never define a new control.
"never": This control will be ignored if there is already a control
under the same name or alias. Otherwise, this will define a new
control.
"" (or any string not listed above): DEPRECATED, DO NOT USE in new
control definitions! https://issuetracker.google.com/287541200
tracks removal of this option. With clobber_ok="" this control
will update the params of an existing control if present, or
will define a new control if there isn't one to update.
NOTE, method is recursive when parsing 'include' elements from XML.
Args:
name_prefix: string to prepend to all control names
filename: string of path to system file ( xml )
Raises:
SystemConfigError: for schema violations, or file not found.
"""
cfgname = self.find_cfg_file(filename)
if not cfgname:
msg = "Unable to find system file %s" % filename
self._logger.error(msg)
raise SystemConfigError(msg)
filename = cfgname
if (name_prefix, filename) in self._loaded_xml_files:
self._logger.warning(
"Already sourced system file (%r, %r).", filename, name_prefix
)
return
self._loaded_xml_files.add((name_prefix, filename))
self._logger.info("Loading XML config (%r, %r)", filename, name_prefix)
# set of tuples representing config entities seen already in this file
seen_entities = set() # {(str, str)} - set of (tag, name) tuples
root = xml.etree.ElementTree.parse(filename).getroot()
for element in root.findall("include"):
self.add_cfg_file(name_prefix, element.find("name").text)
for tag in SYSCFG_TAG_LIST:
for element in root.findall(tag):
element_str = xml.etree.ElementTree.tostring(element)
name = element.find("name")
if name is None:
# TODO(tbroch) would rather have lineno but dumping element seems
# better than nothing. Ultimately a DTD/XSD for the XML schema will
# catch these anyways.
raise SystemConfigError(
"%s: no name ... see XML\n%s" % (tag, element_str)
)
name = name.text
doc = element.findtext("doc", default="undocumented")
doc = " ".join(doc.split())
alias = element.findtext("alias")
this_entity = tag, name
if this_entity in seen_entities:
raise SystemConfigError(
"config file %r contains redundant or conflicting definitions "
"for %s %r" % (filename, tag, name)
)
seen_entities.add(this_entity)
get_dict = None
set_dict = None
get_is_defined = True
set_is_defined = True
params_list = element.findall("params")
if tag == CONTROL_TAG:
for p in params_list:
if CONTENT_PARAM in p:
raise SystemConfigError(
"file %r %s element %r specifies "
"reserved params attribute name %r"
% (filename, tag, name, CONTENT_PARAM)
)
p.attrib[CONTENT_PARAM] = self._parse_content(
p.find(CONTENT_TAG)
)
# Make sure that if |cmd| is defined, it is correctly defined as
# either set or get.
if "cmd" in p.attrib and p.attrib["cmd"] not in ("set", "get"):
raise SystemConfigError(
"%s %s cmd has to be set|get, not %r"
% (tag, name, p.attrib["cmd"])
)
# Modify the interface attributes.
if "interface" in p.attrib:
if p.attrib["interface"] != "servo":
p.attrib["interface"] = int(p.attrib["interface"])
if len(params_list) == 2:
assert tag != MAP_TAG, "maps have only one params entry"
for params in params_list:
if "cmd" not in params.attrib:
raise SystemConfigError(
"%s %s multiple params but no cmd\n%s"
% (tag, name, element_str)
)
cmd = params.attrib["cmd"]
if cmd == "get":
if get_dict:
raise SystemConfigError(
"%s %s multiple get params defined\n%s"
% (tag, name, element_str)
)
get_dict = params.attrib
else: # |cmd| is 'set'
# We know from above that cmd is guaranteed to be 'set'
# or 'get'
if set_dict:
raise SystemConfigError(
"%s %s multiple set params defined\n%s"
% (tag, name, element_str)
)
set_dict = params.attrib
elif len(params_list) == 1:
# Some controls work for both set and get. Some controls only work
# for one of the two, and the other is undefined. If there is only
# one |params| defined and it does *not* define cmd, then the policy
# is to treat it like the same dictionary for both. If it does
# define it, then the policy is that the control is *only* valid for
# one direction: set or get.
# |pd| here stands for params dict
pd = params_list[0].attrib
if "cmd" in pd:
cmd = pd["cmd"]
if cmd == "get":
get_dict = copy.copy(pd)
set_dict = copy.copy(UNDEF_CONTROL_DICT)
set_is_defined = False
else: # |cmd| is 'set'
set_dict = copy.copy(pd)
get_dict = copy.copy(UNDEF_CONTROL_DICT)
get_is_defined = False
else:
# |cmd| is not set. assume it's the same for both.
get_dict = copy.copy(pd)
set_dict = copy.copy(pd)
if tag == CONTROL_TAG:
# Lastly, to allow the |drv| full visibility in whether it's a
# set or a get instance, make sure to store set and get in the
# dict regardless of whether it was already there or has been
# inferred here.
get_dict["cmd"] = "get"
set_dict["cmd"] = "set"
else:
raise SystemConfigError(
"%s %s has illegal number of params %d\n%s"
% (tag, name, len(params_list), element_str)
)
if tag == CONTROL_TAG:
set_dict["interface_prefix"] = name_prefix
get_dict["interface_prefix"] = name_prefix
# Save the control name to the params dicts, such that the driver can
# refer to it.
if tag == CONTROL_TAG:
get_dict["control_name"] = name
set_dict["control_name"] = name
if tag == MAP_TAG:
self.syscfg_dict[tag][name] = {"doc": doc, "map_params": get_dict}
if alias:
raise SystemConfigError("No aliases for maps allowed")
continue
assert tag == CONTROL_TAG
clobber_vals = set()
if get_is_defined:
clobber_vals.add(get_dict.get(CLOBBER_ATTR))
if set_is_defined:
clobber_vals.add(set_dict.get(CLOBBER_ATTR))
if not clobber_vals:
clobber_ok = None
elif len(clobber_vals) == 1:
clobber_ok = clobber_vals.pop()
else:
raise SystemConfigError(
"config file %r %s %r has conflicting %s= values between "
'cmd="get" and cmd="set"' % (filename, tag, name, CLOBBER_ATTR)
)
if clobber_ok == CLOBBER_NEVER:
if name in self.syscfg_dict[tag]:
self._logger.debug(
"Quietly refusing to clobber existing %s %r", tag, name
)
continue
if clobber_ok == CLOBBER_PATCH:
if name not in self.syscfg_dict[tag]:
self._logger.debug(
"Ignoring clobber patch for nonexistent %s %r", tag, name
)
continue
self._logger.debug("Applying clobber patch to %s %r", tag, name)
elif clobber_ok is None and name in self.syscfg_dict[tag]:
raise SystemConfigError(
"Duplicate %s %r without %r key\n%s"
% (tag, name, CLOBBER_ATTR, element_str)
)
if "init" in set_dict:
hwinit_found = False
# only allow one hwinit per control
if clobber_ok is not None:
# if we clobbered an alias, look for its hwinit under its
# real name
realname = self.aliases.get(name, name)
for i, (hwinit_name, _unused) in enumerate(self.hwinit):
if hwinit_name == realname:
self.hwinit[i] = (realname, set_dict["init"])
hwinit_found = True
break
if not hwinit_found:
self.hwinit.append((name, set_dict["init"]))
if name in self.syscfg_dict[tag]:
assert clobber_ok is not None
# Always update existing dicts when present, to avoid splitting
# aliases into separate controls.
if clobber_ok == CLOBBER_FULL:
self.syscfg_dict[tag][name]["get_params"].clear()
self.syscfg_dict[tag][name]["set_params"].clear()
self.syscfg_dict[tag][name]["get_params"].update(get_dict)
self.syscfg_dict[tag][name]["set_params"].update(set_dict)
if doc != "undocumented" or clobber_ok == CLOBBER_FULL:
self.syscfg_dict[tag][name]["doc"] = doc
else:
self.syscfg_dict[tag][name] = {
"doc": doc,
"get_params": get_dict,
"set_params": set_dict,
}
if "drv" not in self.syscfg_dict[tag][name]["get_params"]:
raise SystemConfigError(
'control %r cmd="get" has no driver configured (drv= attribute)'
% (name,)
)
if "drv" not in self.syscfg_dict[tag][name]["set_params"]:
raise SystemConfigError(
'control %r cmd="set" has no driver configured (drv= attribute)'
% (name,)
)
if alias:
# if we clobbered an alias, point our aliases to its real name
realname = self.aliases.get(name, name)
for aliasname in alias.split(","):
if not IDENTIFIER_RE.fullmatch(aliasname):
raise SystemConfigError(
"file %r %s element %r invalid "
'alias "%s"' % (filename, tag, name, aliasname)
)
self.syscfg_dict[tag][aliasname] = self.syscfg_dict[tag][name]
# Also store what the alias relationship
self.aliases[aliasname] = realname
def finalize(self):
"""Finalize setup, Call this after no more config files will be added.
Note: this can be called repeatedly, and will overwrite the previous
results.
- Sets up tags for each control, if provided
"""
self._check_controls_for_drv()
self.control_tags.clear()
for control in self.syscfg_dict[CONTROL_TAG]:
# Tags are only stored for the primary control name, not their aliases.
if control not in self.aliases:
# Tags can be in either params.
for params_dict in self.syscfg_dict[CONTROL_TAG][control].values():
if "tags" in params_dict:
tags = SystemConfig.tag_string_to_tags(params_dict["tags"])
for tag in tags:
self.control_tags[tag].add(control)
def get_controls_for_tag(self, tag):
"""Get list of controls for a given tag.
Args:
tag: str, tag to query
Returns:
list of controls with that tag, or an empty list if no such tag, or
controls under that tag
"""
# Checking here ensures that we do not generate an empty list (as it's a
# default dict)
if tag not in self.control_tags:
self._logger.info("Tag %s unknown.", tag)
return []
return list(self.control_tags[tag])
def lookup_map_params(self, name):
"""Lookup & return map parameter dictionary.
Args:
name: string of map name to lookup
Returns:
params: dictionary of map params
Raises:
NameError: if map name not found
"""
if name not in self.syscfg_dict[MAP_TAG]:
raise NameError(
"No map named %s. All maps:\n%s"
% (name, ",".join(sorted(self.syscfg_dict[MAP_TAG])))
)
return self.syscfg_dict[MAP_TAG][name]["map_params"]
def lookup_control_params(self, name):
"""Lookup & return control parameter dictionary.
Each control has a set and get implementation. See |add_cfg_file()| for
the policy on how those are generated and the guarantee that both always
exist.
Args:
name: string of control name to lookup
Returns:
tuple(get params, set params) the params for each set and get
Raises:
NameError: if control name not found
"""
if name not in self.syscfg_dict[CONTROL_TAG]:
raise NameError(
"No control named %s. All controls:\n%s"
% (name, ",".join(sorted(self.syscfg_dict[CONTROL_TAG])))
)
return (
self.syscfg_dict[CONTROL_TAG][name]["get_params"],
self.syscfg_dict[CONTROL_TAG][name]["set_params"],
)
def get_all_controls(self):
"""Return an iterable of all controls specified.
Returns:
ctrls: set of all control names known to SystemConfig
"""
return set(self.syscfg_dict[CONTROL_TAG].keys())
def is_control(self, name):
"""Determine if name is a control or not.
Args:
name: string of control name to lookup
Returns:
boolean, True if name is control, False otherwise
"""
return name in self.syscfg_dict[CONTROL_TAG]
def get_control_str(self, name):
"""Generate a string that describes all information of the control.
Args:
name: string of control name to lookup
Returns:
A string representing the control
"""
ctrl_dict = self.syscfg_dict[CONTROL_TAG]
max_len = max(len(name) for name in ctrl_dict)
dashes = "-" * max_len
padded_name = "%-*s" % (max_len, "%s" % name)
doc_str = "%s DOC: %s" % (padded_name, ctrl_dict[name]["doc"])
get_str = "%s GET: %s" % (dashes, str(ctrl_dict[name]["get_params"]))
set_str = "%s SET: %s" % (dashes, str(ctrl_dict[name]["set_params"]))
return "%s\n%s\n%s" % (doc_str, get_str, set_str)
def is_map(self, name):
"""Determine if name is a map or not.
Args:
name: string of map name to lookup
Returns:
boolean, True if name is map, False otherwise
"""
return name in self.syscfg_dict[MAP_TAG]
def get_control_docstring(self, name):
"""Get controls doc string.
Args:
name: string of control name to lookup
Returns:
doc string of the control
"""
return self.syscfg_dict[CONTROL_TAG][name]["doc"]
def _lookup(self, tag, name_str):
"""Lookup the tag name_str and return dictionary or None if not found.
Args:
tag: string of tag (from SYSCFG_TAG_LIST) to look for name_str under.
name_str: string of name to lookup
Returns:
dictionary from syscfg_dict[tag][name_str] or None
"""
self._logger.debug("lookup of %s %s", tag, name_str)
return self.syscfg_dict[tag].get(name_str)
def resolve_val(self, params, map_vstr):
"""Resolve string value.
Values to set the control to can be mapped to symbolic strings for better
readability. For example, its difficult to remember assertion levels of
various gpios. Maps allow things like 'reset:on'. Also provides
abstraction so that assertion level doesn't have to be exposed.
Args:
params: parameters dictionary for control
map_vstr: string thats acceptable values are:
an int (can be "DECIMAL", "0xHEX", 0OCT", or "0bBINARY".
a floating point value.
an alphanumeric which is key in the corresponding map dictionary.
Returns:
Resolved value as float or int or str depending on mapping & input type
Raises:
SystemConfigError: mapping issues found
"""
# its a map
err = "Error formatting input value."
if "map" in params:
map_dict = self._lookup(MAP_TAG, params["map"])
if map_dict is None:
raise SystemConfigError("Map %s isn't defined" % params["map"])
try:
map_vstr = map_dict["map_params"][map_vstr]
except KeyError:
# Do not raise error yet. This might just be that the input is not
# using the map i.e. it's directly writing a raw mapped value.
err = "Map '%s' doesn't contain key '%s'\n" % (params["map"], map_vstr)
err += "Try one of -> '%s'" % "', '".join(map_dict["map_params"])
if "input_type" in params:
if params["input_type"] in ALLOWABLE_INPUT_TYPES:
try:
input_type = ALLOWABLE_INPUT_TYPES[params["input_type"]]
return input_type(map_vstr)
except ValueError as e:
err += "\n%s Input should be 'int' or 'float'." % (
"Or" if "Map" in err else ""
)
raise SystemConfigError(err) from e
else:
self._logger.error("Unrecognized input type.")
# TODO(tbroch): deprecate below once all controls have input_type params
try:
return int(str(map_vstr), 0)
except ValueError:
pass
try:
return float(str(map_vstr))
except ValueError as e:
# No we know that nothing worked, and there was an error.
err += (
" %r can't be cast to default input type %r or fallback input "
"type %r" % (map_vstr, "int", "float")
)
raise SystemConfigError(err) from e
# pylint: disable=invalid-name
# Naming convention to dynamically find methods based on config parameter
def _Fmt_hex(self, int_val):
"""Format integer into hex.
Args:
int_val: integer to be formatted into hex string
Returns:
string of integer in hex format
"""
return hex(int_val)
def _Fmt_lowercase(self, val):
"""Lowercase the output
Args:
val: input string
Returns:
lowercased input
"""
return val.lower()
def reformat_val(self, params, value):
"""Reformat value.
Formatting determined via:
1. if it has fmt param, reformat based on that
2. if value (or value after fmt) matches a map in the param, use
the symbolic name from the map, otherwise the (fmt) value
Args:
params: parameter dictionary for control
value: value to reformat
Returns:
formatted string value if reformatting needed
value otherwise
Raises:
SystemConfigError: errors using formatting param
"""
# TODO(crbug.com/841097): revisit logic for value here once
# resolution found on bug.
if value is not None and "map" not in params and "fmt" not in params:
return value
reformat_value = str(value)
if "fmt" in params:
fmt = params["fmt"]
try:
func = getattr(self, "_Fmt_%s" % fmt)
except AttributeError as e:
raise SystemConfigError("Unrecognized format %s" % fmt) from e
try:
reformat_value = func(value)
except Exception as e:
raise SystemConfigError("Problem executing format %s" % fmt) from e
if "map" in params:
map_dict = self._lookup(MAP_TAG, params["map"])
if map_dict:
map_params = map_dict["map_params"]
for keyname, val in map_params.items():
# try treating val as a regex expression
if params["map"].endswith("_re"):
if re.search(val, reformat_value):
reformat_value = keyname
break
# try matching it as a simple string
elif val == reformat_value:
reformat_value = keyname
break
# check for the possibility that there's need to reformat
elif keyname == reformat_value:
break
else:
if reformat_value and reformat_value != "not_applicable":
control = params["control_name"]
logging.warning(
"%s: %r not found in the param values",
control,
reformat_value,
)
logging.warning(
"%s: update drv to get and set values from the "
"param map %r",
control,
map_params,
)
return reformat_value
def display_config(self, tag_param=None, prefix=None):
"""Display human-readable values of a map or control
Args:
tag: 'map' or 'control' or None for all
prefix: prefix string to print in front of control tags
Returns:
string to be displayed.
"""
rsp = []
if tag_param is None:
tag_list = SYSCFG_TAG_LIST
else:
tag_list = [tag_param]
for tag in sorted(tag_list):
prefix_str = ""
if tag == CONTROL_TAG and prefix:
prefix_str = "%s." % prefix
rsp.append("*************")
rsp.append("* " + tag.upper())
rsp.append("*************")
max_len = max(len(name) for name in self.syscfg_dict[tag])
max_len += len(prefix_str)
dashes = "-" * max_len
for name in sorted(self.syscfg_dict[tag]):
item_dict = self.syscfg_dict[tag][name]
padded_name = "%-*s" % (max_len, "%s%s" % (prefix_str, name))
rsp.append("%s DOC: %s" % (padded_name, item_dict["doc"]))
if tag == MAP_TAG:
rsp.append("%s MAP: %s" % (dashes, str(item_dict["map_params"])))
else:
rsp.append("%s GET: %s" % (dashes, str(item_dict["get_params"])))
rsp.append("%s SET: %s" % (dashes, str(item_dict["set_params"])))
return "\n".join(rsp)
def get_board_model_config(self, board=None, model=None):
"""Get the configuration file and board name for |board| & |model| pair.
This essentially tries to find a configuration file for board/model first,
before attempting to find a configuration file just for board, before
giving up.
Configuration filename format is servo_[board][_model]?_overlay.xml
Args:
board: board name
model: model name under |board|
Returns:
tuple (board_config, board_id)
board_config: the file name of the board's overlay file
board_id: |board| or |board_model| if model was used to find an overlay
"""
board_config = board_id = None
if board:
board_id = board
# Handle differentiated model case.
if model:
board_id = "%s_%s" % (board_id, model)
board_config = "servo_%s_overlay.xml" % board_id
if self.find_cfg_file(board_config):
self._logger.info("Found XML overlay for model %s:%s", board, model)
else:
self._logger.info(
"No XML overlay for model %s, falling back to "
"board %s default",
model,
board,
)
board_config = board_id = None
# Handle generic board config.
if not board_config:
board_id = board
board_config = "servo_%s_overlay.xml" % board_id
if self.find_cfg_file(board_config):
self._logger.info("Found XML overlay for board %s", board)
else:
self._logger.error("No XML overlay for board %s", board)
board_config = board_id = None
return board_config, board_id