blob: 20cd2f8e107cd43742ed28fa6aee58f5fe3d36a0 [file] [log] [blame]
#!/usr/bin/env python2
# Copyright 2017 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.
"""Validates a given master configuration
This enforces various rules defined by the master configuration. Some of these
are fairly simple (the valid properties and subnodes for each node, the
allowable values for properties) and some are more complex (where phandles
are allowed to point).
The schema is defined by Python objects containing variable SchemaElement
subclasses. Each subclass defines how the device tree property is validated.
For strings this is via a regex. Phandles properties are validated by the
target they are expected to point to.
Schema elements can be optional or required. Optional elements will not cause
a failure if the node does not include them.
The presence or absense of a particular schema element can also be controlled
by a 'conditional_props' option. This lists elements that must (or must not)
be present in the node for this element to be present. This provides some
flexibility where the schema for a node has two options, for example, where
the presence of one element conflicts with the presence of others.
Usage:
The validator can be run like this (set PYTHONPATH to your chromium dir):
PYTHONPATH=~/cosarm ./validate_config \
~/cosarm/chroot/build/coral/usr/share/chromeos-config/config.dtb \
~/cosarm/chroot/build/reef-uni/usr/share/chromeos-config/config.dtb \
README.md
The output format for each input file is the name of the file followed by a
list of validation problems. If there are no problems, the filename is not
shown.
Unit tests can be run like this:
PYTHONPATH=~/cosarm python validate_config_unittest.py
"""
from __future__ import print_function
import argparse
import copy
import itertools
import os
import re
import sys
from chromite.lib import cros_build_lib
from . import fdt, fdt_util
from .validate_schema import NodeAny, NodeDesc, NodeModel, NodeSubmodel
from .validate_schema import PropCustom, PropDesc, PropString, PropStringList
from .validate_schema import PropPhandleTarget, PropPhandle, CheckPhandleTarget
from .validate_schema import PropAny, PropFile
def ParseArgv(argv):
"""Parse the available arguments.
Invalid arguments or -h cause this function to print a message and exit.
Args:
argv: List of string arguments (excluding program name / argv[0])
Returns:
argparse.Namespace object containing the attributes.
"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-d', '--debug', action='store_true',
help='Run in debug mode (full exception traceback)')
parser.add_argument('-p', '--partial', action='store_true',
help='Validate a list partial files (.dtsi) individually')
parser.add_argument('-r', '--raise-on-error', action='store_true',
help='Causes the validator to raise on the first ' +
'error it finds. This is useful for debugging.')
parser.add_argument('config', type=str, nargs='+',
help='Paths to the config files (.dtb) to validated')
return parser.parse_args(argv)
class CrosConfigValidator(object):
"""Validator for the master configuration"""
def __init__(self, schema, raise_on_error):
"""Master configuration validator.
Properties:
_errors: List of validation errors detected (each a string)
_fdt: fdt.Fdt object containing device tree to validate
_raise_on_error: True if the validator should raise on the first error
(useful for debugging)
model_list: List of model names found in the config
submodel_list: Dict of submodel names found in the config:
key: Model name
value: List of submodel names
"""
self._errors = []
self._fdt = None
self._raise_on_error = raise_on_error
self._schema = schema
# This iniital value matches the standard schema object. This is
# overwritten by the real model list by Start().
self.model_list = ['MODEL']
self.submodel_list = {}
def Fail(self, location, msg):
"""Record a validation failure
Args:
location: fdt.Node object where the error occurred
msg: Message to record for this failure
"""
self._errors.append('%s: %s' % (location, msg))
if self._raise_on_error:
raise ValueError(self._errors[-1])
def _IsBuiltInProperty(self, node, prop_name):
"""Checks if a property is a built-in device-tree construct
This checks for 'reg', '#address-cells' and '#size-cells' properties which
are valid when correctly used in a device-tree context.
Args:
node: fdt.Node where the property appears
prop_name: Name of the property
Returns:
True if this property is a built-in property and does not have to be
covered by the schema
"""
if prop_name == 'reg' and '@' in node.name:
return True
if prop_name in ['#address-cells', '#size-cells']:
for subnode in node.subnodes.values():
if '@' in subnode.name:
return True
return False
def ElementPresent(self, schema, parent_node):
"""Check whether a schema element should be present
This handles the conditional_props feature. The list of names of sibling
nodes/properties that are actually present is checked to see if any of them
conflict with the conditional properties for this node. If there is a
conflict, then this element is considered to be absent.
Args:
schema: Schema element to check
parent_node: Parent fdt.Node containing this schema element (or None if
this is not known)
Returns:
True if this element is present, False if absent
"""
if schema.conditional_props and parent_node:
for rel_name, value in schema.conditional_props.iteritems():
name = rel_name
schema_target = schema.parent
node_target = parent_node
while name.startswith('../'):
schema_target = schema_target.parent
node_target = node_target.parent
name = name[3:]
parent_props = [e.name for e in schema_target.elements]
sibling_names = node_target.props.keys()
sibling_names += [n.name for n in node_target.subnodes.values()]
if name in parent_props and value != (name in sibling_names):
return False
return True
def GetElement(self, schema, name, node, expected=None):
"""Get an element from the schema by name
Args:
schema: Schema element to check
name: Name of element to find (string)
node: Node containing the property (or for nodes, the parent node
containing the subnode) we are looking up. None if none available
expected: The SchemaElement object that is expected. This can be NodeDesc
if a node is expected, PropDesc if a property is expected, or None
if either is fine.
Returns:
Tuple:
Schema for the node, or None if none found
True if the node should have schema, False if it can be ignored
(because it is internal to the device-tree format)
"""
for element in schema.elements:
if not self.ElementPresent(element, node):
continue
if element.name == name:
return element, True
elif (self.model_list and isinstance(element, NodeModel) and
name in self.model_list):
return element, True
elif self.submodel_list and isinstance(element, NodeSubmodel) and node:
m = re.match('/chromeos/models/([a-z0-9]+)/submodels', node.path)
if m and name in self.submodel_list[m.group(1)]:
return element, True
elif ((expected is None or expected == NodeDesc) and
isinstance(element, NodeAny)):
return element, True
elif ((expected is None or expected == PropDesc) and
isinstance(element, PropAny)):
return element, True
if expected == PropDesc:
if name == 'linux,phandle' or self._IsBuiltInProperty(node, name):
return None, False
return None, True
def GetElementByPath(self, path):
"""Find a schema element given its full path
Args:
path: Full path to look up (e.g. '/chromeos/models/MODEL/thermal/dptf-dv')
Returns:
SchemaElement object for that path
Raises:
AttributeError if not found
"""
parts = path.split('/')[1:]
schema = self._schema
for part in parts:
element, _ = self.GetElement(schema, part, None)
schema = element
return schema
def _ValidateSchema(self, node, schema):
"""Simple validation of properties.
This only handles simple mistakes like getting the name wrong. It
cannot handle relationships between different properties.
Args:
node: fdt.Node where the property appears
schema: NodeDesc containing schema for this node
"""
schema.Validate(self, node)
schema_props = [e.name for e in schema.elements
if isinstance(e, PropDesc) and
self.ElementPresent(e, node)]
# Validate each property and check that there are no extra properties not
# mentioned in the schema.
for prop_name in node.props.keys():
if prop_name == 'linux,phandle': # Ignore this (use 'phandle' instead)
continue
element, _ = self.GetElement(schema, prop_name, node, PropDesc)
if not element or not isinstance(element, PropDesc):
if prop_name == 'phandle':
self.Fail(node.path, 'phandle target not valid for this node')
elif not self._IsBuiltInProperty(node, prop_name):
self.Fail(node.path, "Unexpected property '%s', valid list is (%s)" %
(prop_name, ', '.join(schema_props)))
continue
element.Validate(self, node.props[prop_name])
# Check that there are no required properties which we don't have
for element in schema.elements:
if (not isinstance(element, PropDesc) or
not self.ElementPresent(element, node)):
continue
if element.required and element.name not in node.props.keys():
self.Fail(node.path, "Required property '%s' missing" % element.name)
# Check that any required subnodes are present
subnode_names = [n.name for n in node.subnodes.values()]
for element in schema.elements:
if (not isinstance(element, NodeDesc) or not element.required
or not self.ElementPresent(element, node)):
continue
if element.name not in subnode_names:
msg = "Missing subnode '%s'" % element.name
if subnode_names:
msg += ' in %s' % ', '.join(subnode_names)
self.Fail(node.path, msg)
def GetSchema(self, node, parent_schema):
"""Obtain the schema for a subnode
This finds the schema for a subnode, by scanning for a matching element.
Args:
node: fdt.Node whose schema we are searching for
parent_schema: Schema for the parent node, which contains that schema
Returns:
Schema for the node, or None if none found
"""
schema, needed = self.GetElement(parent_schema, node.name, node.parent,
NodeDesc)
if not schema and needed:
elements = [e.name for e in parent_schema.GetNodes()
if self.ElementPresent(e, node.parent)]
self.Fail(os.path.dirname(node.path),
"Unexpected subnode '%s', valid list is (%s)" %
(node.name, ', '.join(elements)))
return schema
def _ValidateTree(self, node, parent_schema):
"""Validate a node and all its subnodes recursively
Args:
node: name of fdt.Node to search for
parent_schema: Schema for the parent node
"""
if node.name == '/':
schema = parent_schema
else:
schema = self.GetSchema(node, parent_schema)
if schema is None:
return
self._ValidateSchema(node, schema)
for subnode in node.subnodes.values():
self._ValidateTree(subnode, schema)
@staticmethod
def ValidateSkuMap(val, prop):
it = iter(prop.value)
sku_set = set()
for sku, phandle in itertools.izip(it, it):
sku_id = fdt_util.fdt32_to_cpu(sku)
if sku_id > 255:
val.Fail(prop.node.path, 'sku_id %d out of range' % sku_id)
if sku_id in sku_set:
val.Fail(prop.node.path, 'Duplicate sku_id %d' % sku_id)
sku_set.add(sku_id)
phandle_val = fdt_util.fdt32_to_cpu(phandle)
target = prop.fdt.LookupPhandle(phandle_val)
if (not CheckPhandleTarget(val, target, '/chromeos/models/MODEL') and
not CheckPhandleTarget(val, target,
'/chromeos/models/MODEL/submodels/SUBMODEL')):
val.Fail(prop.node.path,
"Phandle '%s' sku-id %d must target a model or submodel'" %
(prop.name, sku_id))
def GetModelTargetDir(self, path, prop_name):
"""Get the target directory for a given path and property
This looks up the model schema for a given path and property, and locates
the target directory for that property.
Args:
path: Path within model schema to examine (e.g. /thermal)
prop_name: Property name to examine (e.g. 'dptf-dv')
Returns:
target directory for that property (e.g. '/etc/dptf')
"""
element = self.GetElementByPath(
'/chromeos/models/MODEL%s/%s' % (path, prop_name))
return element.target_dir
def Prepare(self, _fdt):
"""Locate all the models and submodels before we start"""
self._fdt = _fdt
models = self._fdt.GetNode('/chromeos/models')
for model in models.subnodes.values():
self.model_list.append(model.name)
sub_models = model.FindNode('submodels')
if sub_models:
self.submodel_list[model.name] = (
[sm.name for sm in sub_models.subnodes.values()])
else:
self.submodel_list[model.name] = []
def Start(self, fnames, partial=False):
"""Start validating a master configuration file
Args:
fnames: List of filenames containing the configuration to validate.
Supports compiled .dtb files, source .dts files and README.md (which
has configuration source between ``` markers). If partial is False
then there can be only one filename in the list.
partial: True to process a list of partial config files (.dtsi)
"""
tmpfile = None
self.model_list = []
self.submodel_list = {}
self._errors = []
try:
if partial:
dtb, tmpfile = fdt_util.CompileAll(fnames)
else:
dtb, tmpfile = fdt_util.EnsureCompiled(fnames[0])
self.Prepare(fdt.FdtScan(dtb))
# Validate the entire master configuration
self._ValidateTree(self._fdt.GetRoot(), self._schema)
finally:
if tmpfile:
os.unlink(tmpfile.name)
return self._errors
@classmethod
def AddElementTargetDirectories(cls, target_dirs, parent):
if isinstance(parent, PropFile):
if parent.name in target_dirs:
if target_dirs[parent.name] != parent.target_dir:
raise ValueError(
"Path for element '%s' is inconsistent with previous path '%s'" %
(parent.target_dir, target_dirs[parent.name]))
else:
target_dirs[parent.name] = parent.target_dir
if isinstance(parent, NodeDesc):
for element in parent.elements:
cls.AddElementTargetDirectories(target_dirs, element)
def GetTargetDirectories(self):
"""Gets a dict of directory targets for each PropFile property
Returns:
Dict:
key: Property name
value: Ansolute path for this property
"""
target_dirs = {}
self.AddElementTargetDirectories(target_dirs, self._schema)
return target_dirs
# Known directories for installation
CRAS_CONFIG_DIR = '/etc/cras'
UCM_CONFIG_DIR = '/usr/share/alsa/ucm'
LIB_FIRMWARE = '/lib/firmware'
# Basic firmware schema, which is augmented depending on the situation.
FW_COND = {'shares': False, '../whitelabel': False}
BASE_FIRMWARE_SCHEMA = [
PropString('bcs-overlay', True, 'overlay-.*', FW_COND),
PropString('ec-image', False, r'bcs://.*\.tbz2', FW_COND),
PropString('main-image', False, r'bcs://.*\.tbz2', FW_COND),
PropString('main-rw-image', False, r'bcs://.*\.tbz2', FW_COND),
PropString('pd-image', False, r'bcs://.*\.tbz2', FW_COND),
PropStringList('extra', False,
r'(\${(FILESDIR|SYSROOT)}/[a-z/]+)|' +
r'(bcs://[A-Za-z0-9\.]+\.tbz2)', FW_COND),
]
# Firmware build targets schema, defined here since it is used in a few places.
BUILD_TARGETS_SCHEMA = NodeDesc('build-targets', True, elements=[
PropString('coreboot', True),
PropString('ec', True),
PropString('depthcharge', True),
PropString('libpayload', True),
PropString('cr50'),
], conditional_props={'shares': False, '../whitelabel': False})
BASE_AUDIO_SCHEMA = [
PropString('card', True, '', {'audio-type': False}),
PropFile('volume', True, '', {'audio-type': False}, CRAS_CONFIG_DIR),
PropFile('dsp-ini', True, '', {'audio-type': False}, CRAS_CONFIG_DIR),
PropFile('hifi-conf', True, '', {'audio-type': False}, UCM_CONFIG_DIR),
PropFile('alsa-conf', True, '', {'audio-type': False}, UCM_CONFIG_DIR),
PropString('topology-name', False, r'\w+'),
PropFile('topology-bin', False, '', {'audio-type': False}, LIB_FIRMWARE),
# TODO(sjg@chromium.org): There is no validation that we have these two.
# They must both exist either in the model's audio node or here.
PropFile('cras-config-dir', False, r'[\w${}]+', target_dir=CRAS_CONFIG_DIR),
PropString('ucm-suffix', False, r'[\w${}]+'),
]
BASE_AUDIO_NODE = [
NodeAny(r'main', [
PropPhandle('audio-type', '/chromeos/family/audio/ANY',
False),
] + BASE_AUDIO_SCHEMA)
]
NOT_WL = {'whitelabel': False}
"""This is the schema. It is a hierarchical set of nodes and properties, just
like the device tree. If an object subclasses NodeDesc then it is a node,
possibly with properties and subnodes.
In this way it is possible to describe the schema in a fairly natural,
hierarchical way.
"""
SCHEMA = NodeDesc('/', True, [
NodeDesc('chromeos', True, [
NodeDesc('family', True, [
NodeDesc('audio', elements=[
NodeAny('', [PropPhandleTarget()] +
copy.deepcopy(BASE_AUDIO_SCHEMA)),
]),
NodeDesc('firmware', elements=[
PropString('script', True, r'updater4\.sh'),
NodeAny('', [
PropPhandleTarget(),
copy.deepcopy(BUILD_TARGETS_SCHEMA),
] + copy.deepcopy(BASE_FIRMWARE_SCHEMA))
]),
NodeDesc('touch', False, [
NodeAny('', [
PropPhandleTarget(),
PropString('firmware-bin', True, ''),
PropString('firmware-symlink', True, ''),
PropString('vendor', True, ''),
]),
]),
NodeDesc('mapping', False, [
NodeAny(r'sku-map(@[0-9])?', [
PropString('platform-name', False, ''),
PropString('smbios-name-match', False, ''),
PropPhandle('single-sku', '/chromeos/models/MODEL', False),
PropCustom('simple-sku-map',
CrosConfigValidator.ValidateSkuMap, False),
]),
]),
]),
NodeDesc('models', True, [
NodeModel([
PropPhandleTarget(),
PropPhandle('default', '/chromeos/models/MODEL', False),
PropPhandle('whitelabel', '/chromeos/models/MODEL', False),
NodeDesc('firmware', False, [
PropPhandle('shares', '/chromeos/family/firmware/ANY',
False, {'../whitelabel': False}),
PropString(('sig-id-in-customization-id'),
conditional_props={'../whitelabel': False}),
PropString('key-id', False, '[A-Z][A-Z0-9]+'),
copy.deepcopy(BUILD_TARGETS_SCHEMA)
] + copy.deepcopy(BASE_FIRMWARE_SCHEMA)),
PropString('brand-code', False, '[A-Z]{4}'),
PropString('powerd-prefs', conditional_props=NOT_WL),
PropString('wallpaper', False, '[a-z_]+'),
NodeDesc('audio', False, copy.deepcopy(BASE_AUDIO_NODE),
conditional_props=NOT_WL),
NodeDesc('submodels', False, [
NodeSubmodel([
PropPhandleTarget(),
NodeDesc('audio', False, copy.deepcopy(BASE_AUDIO_NODE),
conditional_props={'../../audio': False}),
NodeDesc('touch', False, [
PropString('present', False, r'yes|no|probe'),
PropString('probe-regex', False, ''),
]),
])
], conditional_props=NOT_WL),
NodeDesc('thermal', False, [
PropFile('dptf-dv', False, r'\w+/dptf.dv',
target_dir='/etc/dptf'),
], conditional_props=NOT_WL),
NodeDesc('touch', False, [
PropString('present', False, r'yes|no|probe'),
# We want to validate that probe-regex is only present when
# 'present' = 'probe', but have no way of doing this
# currently.
PropString('probe-regex', False, ''),
NodeAny(r'(stylus|touchpad|touchscreen)(@[0-9])?', [
PropString('pid', False),
PropString('version', True),
PropPhandle('touch-type', '/chromeos/family/touch/ANY',
False),
PropString('firmware-bin', True, '',
{'touch-type': False}),
PropString('firmware-symlink', True, '',
{'touch-type': False}),
PropString('date-code', False),
]),
], conditional_props=NOT_WL),
NodeDesc('whitelabels', False, [
NodeAny('', [
PropString('brand-code', False, '[A-Z]{4}'),
PropString('wallpaper', False, '[a-z_]+'),
PropString('key-id', False, '[A-Z][A-Z0-9]+'),
]),
], conditional_props=NOT_WL),
])
]),
NodeDesc('schema', False, [
NodeDesc('target-dirs', False, [
PropAny(),
])
])
])
])
def GetValidator():
"""Get a schema validator for use by another module
Returns:
CrosConfigValidator object
"""
return CrosConfigValidator(SCHEMA, raise_on_error=True)
def ShowErrors(fname, errors):
"""Show validation errors
Args:
fname: Filename containng the errors
errors: List of errors, each a string
"""
print('%s:' % fname, file=sys.stderr)
for error in errors:
print(error, file=sys.stderr)
print(file=sys.stderr)
def Main(argv=None):
"""Main program for validator
This validates each of the provided files and prints the errors for each, if
any.
Args:
argv: Arguments to the problem (excluding argv[0]); if None, uses sys.argv
"""
if argv is None:
argv = sys.argv[1:]
args = ParseArgv(argv)
validator = CrosConfigValidator(SCHEMA, args.raise_on_error)
found_errors = False
try:
# If we are given partial files (.dtsi) then we compile them all into one
# .dtb and validate that.
if args.partial:
errors = validator.Start(args.config, partial=True)
fname = args.config[0]
if errors:
ShowErrors(fname, errors)
found_errors = True
# Otherwise process each file individually
else:
for fname in args.config:
errors = validator.Start([fname])
if errors:
found_errors = True
if errors:
ShowErrors(fname, errors)
found_errors = True
except cros_build_lib.RunCommandError as e:
if args.debug:
raise
print('Failed: %s' % e, file=sys.stderr)
found_errors = True
if found_errors:
sys.exit(1)
if __name__ == "__main__":
Main()