| #!/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() |