def _parse_provider_yaml(path):
    """Loads schema, parses a provider.yaml file and validates the content.

    :param path: File system path to the file to parse.
    :return: dict representing the contents of the file.
    :raise ProviderConfigException: If the specified file does
        not validate against the schema, the schema version is not supported,
        or if unable to read configuration or schema files.
    """
    yaml_file = _load_yaml_file(path)

    try:
        schema_version = microversion_parse.parse_version_string(
            yaml_file['meta']['schema_version'])
    except (KeyError, TypeError):
        message = _("Unable to detect schema version: %s") % yaml_file
        raise nova_exc.ProviderConfigException(error=message)

    if schema_version.major not in SUPPORTED_SCHEMA_VERSIONS:
        message = _(
            "Unsupported schema major version: %d") % schema_version.major
        raise nova_exc.ProviderConfigException(error=message)

    if schema_version.minor not in \
            SUPPORTED_SCHEMA_VERSIONS[schema_version.major]:
        # TODO(sean-k-mooney): We should try to provide a better
        # message that identifies which fields may be ignored
        # and the max minor version supported by this version of nova.
        message = ("Provider config file [%(path)s] is at schema version "
                   "%(schema_version)s. Nova supports the major version, "
                   "but not the minor. Some fields may be ignored." % {
                       "path": path,
                       "schema_version": schema_version
                   })
        LOG.warning(message)

    try:
        jsonschema.validate(yaml_file, SCHEMA_V1)
    except jsonschema.exceptions.ValidationError as e:
        message = _(
            "The provider config file %(path)s did not pass validation "
            "for schema version %(schema_version)s: %(reason)s") % {
                "path": path,
                "schema_version": schema_version,
                "reason": e
            }
        raise nova_exc.ProviderConfigException(error=message)
    return yaml_file
 def _validate_traits(provider):
     # Check that traits are custom
     additional_traits = set(
         provider.get("traits", {}).get("additional", []))
     trait_conflicts = [
         trait for trait in additional_traits
         if not os_traits.is_custom(trait)
     ]
     if trait_conflicts:
         # sort for more predictable message for testing
         message = _("Invalid traits, only custom traits are allowed: %s"
                     ) % sorted(trait_conflicts)
         raise nova_exc.ProviderConfigException(error=message)
     return additional_traits
def _load_yaml_file(path):
    """Loads and parses a provider.yaml config file into a dict.

    :param path: Path to the yaml file to load.
    :return: Dict representing the yaml file requested.
    :raise: ProviderConfigException if the path provided cannot be read
            or the file is not valid yaml.
    """
    try:
        with open(path) as open_file:
            try:
                return yaml.safe_load(open_file)
            except yaml.YAMLError as ex:
                message = _("Unable to load yaml file: %s ") % ex
                if hasattr(ex, 'problem_mark'):
                    pos = ex.problem_mark
                    message += _("File: %s ") % open_file.name
                    message += _("Error position: (%s:%s)") % (pos.line + 1,
                                                               pos.column + 1)
                raise nova_exc.ProviderConfigException(error=message)
    except OSError:
        message = _("Unable to read yaml config file: %s") % path
        raise nova_exc.ProviderConfigException(error=message)
    def _validate_rc(provider):
        # Check that resource classes are custom
        additional_inventories = provider.get("inventories",
                                              {}).get("additional", [])
        all_inventory_conflicts = []
        for inventory in additional_inventories:
            inventory_conflicts = [
                rc for rc in inventory if not os_resource_classes.is_custom(rc)
            ]
            all_inventory_conflicts += inventory_conflicts

        if all_inventory_conflicts:
            # sort for more predictable message for testing
            message = _("Invalid resource class, only custom resource classes "
                        "are allowed: %s") % ', '.join(
                            sorted(all_inventory_conflicts))
            raise nova_exc.ProviderConfigException(error=message)
        return additional_inventories
def get_provider_configs(provider_config_dir):
    """Gathers files in the provided path and calls the parser for each file
    and merges them into a list while checking for a number of possible
    conflicts.

    :param provider_config_dir: Path to a directory containing provider config
        files to be loaded.
    :raise nova.exception.ProviderConfigException: If unable to read provider
        config directory or if one of a number of validation checks fail:
        - Unknown, unsupported, or missing schema major version.
        - Unknown, unsupported, or missing resource provider identification.
        - A specific resource provider is identified twice with the same
          method. If the same provider identified by *different* methods,
          such conflict will be detected in a later stage.
        - A resource class or trait name is invalid or not custom.
        - A general schema validation error occurs (required fields,
          types, etc).
    :return: A dict of dicts keyed by uuid_or_name with the parsed and
    validated contents of all files in the provided dir. Each value in the dict
    will include the source file name the value of the __source_file key.
    """
    provider_configs = {}

    provider_config_paths = glob.glob(
        os.path.join(provider_config_dir, "*.yaml"))
    provider_config_paths.sort()

    if not provider_config_paths:
        message = ("No provider configs found in %s. If files are present, "
                   "ensure the Nova process has access.")
        LOG.info(message, provider_config_dir)

        # return an empty dict as no provider configs found
        return provider_configs

    for provider_config_path in provider_config_paths:
        provider_config = _parse_provider_yaml(provider_config_path)

        for provider in _validate_provider_config(
                provider_config,
                provider_config_path,
        ):
            provider['__source_file'] = os.path.basename(provider_config_path)
            pid = provider["identification"]
            uuid_or_name = pid.get("uuid") or pid.get("name")
            # raise exception if this provider was already processed
            if uuid_or_name in provider_configs:
                raise nova_exc.ProviderConfigException(
                    error=_(
                        "Provider %(provider_id)s has multiple definitions "
                        "in source file(s): %(source_files)s.") % {
                            "provider_id":
                            uuid_or_name,
                            # sorted set for deduplication and consistent order
                            "source_files":
                            sorted({
                                provider_configs[uuid_or_name]
                                ["__source_file"], provider_config_path
                            })
                        })
            provider_configs[uuid_or_name] = provider
    return provider_configs