blob: a47a485201092f01e747c6819daa8d3f5f00cc65 [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.
"""Transforms and validates cros config from source YAML to target JSON"""
from __future__ import print_function
import argparse
import collections
import json
from jsonschema import validate
import os
import sys
import yaml
this_dir = os.path.dirname(__file__)
CHROMEOS = 'chromeos'
MODELS = 'models'
BUILD_ONLY_ELEMENTS = [
'/firmware',
'/audio/main/card',
'/audio/main/cras-config-dir',
'/audio/main/files'
]
CRAS_CONFIG_DIR = '/etc/cras'
def GetNamedTuple(mapping):
"""Converts a mapping into Named Tuple recursively.
Args:
mapping: A mapping object to be converted.
Returns:
A named tuple generated from mapping
"""
if not isinstance(mapping, collections.Mapping):
return mapping
new_mapping = {}
for k, v in mapping.iteritems():
if type(v) is list:
new_list = []
for val in v:
new_list.append(GetNamedTuple(val))
new_mapping[k.replace('-', '_').replace('@', '_')] = new_list
else:
new_mapping[k.replace('-', '_').replace('@', '_')] = GetNamedTuple(v)
return collections.namedtuple('Config', new_mapping.iterkeys())(**new_mapping)
def ParseArgs(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='Validates a YAML cros-config and transforms it to JSON')
parser.add_argument(
'-s',
'--schema',
type=str,
help='Path to the schema file used to validate the config')
parser.add_argument(
'-c',
'--config',
type=str,
help='Path to the config file (YAML) that will be validated/transformed')
parser.add_argument(
'-o',
'--output',
type=str,
help='Output file that will be generated by the transform (system file)')
parser.add_argument(
'-f',
'--filter',
type=bool,
default=False,
help='Filter build specific elements from the output JSON')
return parser.parse_args(argv)
def TransformConfig(config, drop_family=True):
"""Transforms the source config (YAML) to the target system format (JSON)
Applies consistent transforms to covert a source YAML configuration into
JSON output that will be used on the system by cros_config.
Args:
config: Config that will be transformed.
drop_family: True to drop the 'family' node from the output, leaving only
the 'models' node. This is the normal case.
Returns:
Resulting JSON output from the transform.
"""
config_yaml = yaml.load(config)
json_from_yaml = json.dumps(config_yaml, sort_keys=True, indent=2)
json_config = json.loads(json_from_yaml)
# Drop everything except for models since they were just used as shared
# config in the source yaml.
if drop_family:
json_config = {CHROMEOS: {MODELS: json_config[CHROMEOS][MODELS]}}
return json.dumps(json_config, sort_keys=True, indent=2)
def _GetFirmwareUris(model_dict):
"""Returns a list of (string) firmware URIs.
Generates and returns a list of firmware URIs for this model. These URIs
can be used to pull down remote firmware packages.
Returns:
A list of (string) full firmware URIs, or an empty list on failure.
"""
model = GetNamedTuple(model_dict)
fw = model.firmware
fw_dict = model.firmware._asdict()
if not getattr(fw, 'bcs_overlay'):
return []
bcs_overlay = fw.bcs_overlay.replace('overlay-', '')
base_model = fw.build_targets.coreboot
valid_images = [p for n, p in fw_dict.iteritems()
if n.endswith('image') and p]
uri_format = ('gs://chromeos-binaries/HOME/bcs-{bcs}/overlay-{bcs}/'
'chromeos-base/chromeos-firmware-{base_model}/{fname}')
return [uri_format.format(bcs=bcs_overlay, model=model.name, fname=fname,
base_model=base_model)
for fname in valid_images]
def FilterBuildElements(config):
"""Removes build only elements from the schema.
Removes build only elements from the schema in preparation for the platform.
Args:
config: Config (transformed) that will be filtered
"""
json_config = json.loads(config)
for model in json_config[CHROMEOS][MODELS]:
_FilterBuildElements(model, "")
return json.dumps(json_config, sort_keys=True, indent=2)
def _FilterBuildElements(config, path):
"""Recursively checks and removes build only elements.
Args:
config: Dict that will be checked.
path: Path of elements to filter.
"""
to_delete = []
for key in config:
full_path = "%s/%s" % (path, key)
if full_path in BUILD_ONLY_ELEMENTS:
to_delete.append(key)
elif isinstance(config[key], dict):
_FilterBuildElements(config[key], full_path)
for key in to_delete:
config.pop(key)
def ValidateConfigSchema(schema, config):
"""Validates a transformed cros config against the schema specified
Verifies that the config complies with the schema supplied.
Args:
schema: Source schema used to verify the config.
config: Config (transformed) that will be verified.
"""
json_config = json.loads(config)
schema_yaml = yaml.load(schema)
schema_json_from_yaml = json.dumps(schema_yaml, sort_keys=True, indent=2)
schema_json = json.loads(schema_json_from_yaml)
validate(json_config, schema_json)
class ValidationError(Exception):
"""Exception raised for a validation error"""
pass
def ValidateConfig(config):
"""Validates a transformed cros config for general business rules.
Performs name uniqueness checks and any other validation that can't be
easily performed using the schema.
Args:
config: Config (transformed) that will be verified.
"""
json_config = json.loads(config)
model_names = [model['name'] for model in json_config['chromeos']['models']]
if len(model_names) != len(set(model_names)):
raise ValidationError("Model names are not unique: %s" % model_names)
def Main(schema, config, output, filter_build_details=False):
"""Transforms and validates a cros config file for use on the system
Applies consistent transforms to covert a source YAML configuration into
a JSON file that will be used on the system by cros_config.
Verifies that the file complies with the schema verification rules and
performs additional verification checks for config consistency.
Args:
schema: Schema file used to verify the config.
config: Config file that will be verified.
output: Output file that will be generated by the transform.
filter_build_details: Whether build only details should be filtered or not.
"""
if not schema:
schema = os.path.join(this_dir, 'cros_config_schema.yaml')
with open(config, 'r') as config_stream:
json_transform = TransformConfig(config_stream.read())
with open(schema, 'r') as schema_stream:
ValidateConfigSchema(schema_stream.read(), json_transform)
ValidateConfig(json_transform)
if filter_build_details:
json_transform = FilterBuildElements(json_transform)
if output:
with open(output, 'w') as output_stream:
output_stream.write(json_transform)
else:
print (json_transform)
def main(_argv=None):
"""Main program which parses args and runs
Args:
_argv: Intended to be the list of arguments to the program, or None to use
sys.argv (but at present this is unused)
"""
args = ParseArgs(sys.argv[1:])
Main(args.schema, args.config, args.output, args.filter)
if __name__ == "__main__":
main()