| #!/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 sys |
| import yaml |
| |
| COMPONENT_CONFIG = 'componentConfig' |
| MODELS = 'models' |
| |
| 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] = new_list |
| else: |
| new_mapping[k] = 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)') |
| return parser.parse_args(argv) |
| |
| def TransformConfig(config): |
| """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. |
| |
| Currently, this copies any shared family level config into the model |
| levels (if they haven't explicitly overridden the values) and then |
| deletes any family level config (since this will never be used on the |
| platform). |
| |
| Args: |
| config: Config that will be transformed. |
| |
| 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) |
| # If the model didn't set specific config settings, then have it |
| # automatically inherit the common settings from the family. |
| if COMPONENT_CONFIG in json_config: |
| family_config = json_config[COMPONENT_CONFIG] |
| for model in json_config[MODELS]: |
| if not COMPONENT_CONFIG in model or model[COMPONENT_CONFIG] is None: |
| model[COMPONENT_CONFIG] = family_config |
| else: |
| model_config = model[COMPONENT_CONFIG] |
| for attr_name in family_config.keys(): |
| if attr_name not in model_config or not model_config[attr_name]: |
| model_config[attr_name] = family_config[attr_name] |
| json_config.pop(COMPONENT_CONFIG) |
| return json.dumps(json_config, sort_keys=True, indent=2) |
| |
| |
| 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_json = json.loads(schema) |
| 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['models']] |
| if len(model_names) != len(set(model_names)): |
| raise ValidationError("Model names are not unique: %s" % model_names) |
| |
| |
| def Main(schema, config, output): |
| """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. |
| """ |
| 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) |
| with open(output, 'w') as output_stream: |
| output_stream.write(json_transform) |
| |
| if __name__ == "__main__": |
| args = ParseArgs(sys.argv[1:]) |
| Main(args.schema, args.config, args.output) |