# Copyright 2016 ARM Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from copy import copy from collections import defaultdict from wlauto.core import pluginloader from wlauto.exceptions import ConfigError from wlauto.utils.types import obj_dict from devlib.utils.misc import memoized GENERIC_CONFIGS = ["device_config", "workload_parameters", "boot_parameters", "runtime_parameters"] class PluginCache(object): """ The plugin cache is used to store configuration that cannot be processed at this stage, whether thats because it is unknown if its needed (in the case of disabled plug-ins) or it is not know what it belongs to (in the case of "device-config" ect.). It also maintains where configuration came from, and the priority order of said sources. """ def __init__(self, loader=pluginloader): self.loader = loader self.sources = [] self.plugin_configs = defaultdict(lambda: defaultdict(dict)) self.global_alias_values = defaultdict(dict) # Generate a mapping of what global aliases belong to self._global_alias_map = defaultdict(dict) self._list_of_global_aliases = set() for plugin in self.loader.list_plugins(): for param in plugin.parameters: if param.aliases: self._global_alias_map[plugin.name][param.aliases] = param self._list_of_global_aliases.add(param.aliases) def add_source(self, source): if source in self.sources: raise Exception("Source has already been added.") self.sources.append(source) def add_global_alias(self, alias, value, source): if source not in self.sources: msg = "Source '{}' has not been added to the plugin cache." raise RuntimeError(msg.format(source)) if not self.is_global_alias(alias): msg = "'{} is not a valid global alias'" raise RuntimeError(msg.format(alias)) self.global_alias_values[alias][source] = value def add_configs(self, plugin_name, values, source): print plugin_name, values if self.is_global_alias(plugin_name): self.add_global_alias(plugin_name, values, source) return for name, value in values.iteritems(): self.add_config(plugin_name, name, value, source) def add_config(self, plugin_name, name, value, source): if source not in self.sources: msg = "Source '{}' has not been added to the plugin cache." raise RuntimeError(msg.format(source)) if not self.loader.has_plugin(plugin_name) and plugin_name not in GENERIC_CONFIGS: msg = 'configuration provided for unknown plugin "{}"' raise ConfigError(msg.format(plugin_name)) if (plugin_name not in GENERIC_CONFIGS and name not in self.get_plugin_parameters(plugin_name)): msg = "'{}' is not a valid parameter for '{}'" raise ConfigError(msg.format(name, plugin_name)) self.plugin_configs[plugin_name][source][name] = value def is_global_alias(self, name): return name in self._list_of_global_aliases def get_plugin_config(self, plugin_name, generic_name=None): config = obj_dict(not_in_dict=['name']) config.name = plugin_name # Load plugin defaults cfg_points = self.get_plugin_parameters(plugin_name) for cfg_point in cfg_points.itervalues(): cfg_point.set_value(config, check_mandatory=False) # Merge global aliases for alias, param in self._global_alias_map[plugin_name].iteritems(): if alias in self.global_alias_values: for source in self.sources: if source not in self.global_alias_values[alias]: continue param.set_value(config, value=self.global_alias_values[alias][source]) # Merge user config # Perform a simple merge with the order of sources representing priority if generic_name is None: plugin_config = self.plugin_configs[plugin_name] for source in self.sources: if source not in plugin_config: continue for name, value in plugin_config[source].iteritems(): cfg_points[name].set_value(config, value=value) # A more complicated merge that involves priority of sources and specificity else: self._merge_using_priority_specificity(plugin_name, generic_name, config) return config @memoized def get_plugin_parameters(self, name): params = self.loader.get_plugin_class(name).parameters return {param.name: param for param in params} # pylint: disable=too-many-nested-blocks, too-many-branches def _merge_using_priority_specificity(self, specific_name, generic_name, final_config): """ WA configuration can come from various sources of increasing priority, as well as being specified in a generic and specific manner (e.g. ``device_config`` and ``nexus10`` respectivly). WA has two rules for the priority of configuration: - Configuration from higher priority sources overrides configuration from lower priority sources. - More specific configuration overrides less specific configuration. There is a situation where these two rules come into conflict. When a generic configuration is given in config source of high priority and a specific configuration is given in a config source of lower priority. In this situation it is not possible to know the end users intention and WA will error. :param generic_name: The name of the generic configuration e.g ``device_config`` :param specific_name: The name of the specific configuration used, e.g ``nexus10`` :param cfg_point: A dict of ``ConfigurationPoint``s to be used when merging configuration. keys=config point name, values=config point :rtype: A fully merged and validated configuration in the form of a obj_dict. """ generic_config = copy(self.plugin_configs[generic_name]) specific_config = copy(self.plugin_configs[specific_name]) cfg_points = self.get_plugin_parameters(specific_name) sources = self.sources seen_specific_config = defaultdict(list) # set_value uses the 'name' attribute of the passed object in it error # messages, to ensure these messages make sense the name will have to be # changed several times during this function. final_config.name = specific_name # pylint: disable=too-many-nested-blocks for source in sources: try: if source in generic_config: final_config.name = generic_name for name, cfg_point in cfg_points.iteritems(): if name in generic_config[source]: if name in seen_specific_config: msg = ('"{generic_name}" configuration "{config_name}" has already been ' 'specified more specifically for {specific_name} in:\n\t\t{sources}') msg = msg.format(generic_name=generic_name, config_name=name, specific_name=specific_name, sources=", ".join(seen_specific_config[name])) raise ConfigError(msg) value = generic_config[source][name] cfg_point.set_value(final_config, value, check_mandatory=False) if source in specific_config: final_config.name = specific_name for name, cfg_point in cfg_points.iteritems(): if name in specific_config[source]: seen_specific_config[name].append(str(source)) value = specific_config[source][name] cfg_point.set_value(final_config, value, check_mandatory=False) except ConfigError as e: raise ConfigError('Error in "{}":\n\t{}'.format(source, str(e))) # Validate final configuration final_config.name = specific_name for cfg_point in cfg_points.itervalues(): cfg_point.validate(final_config)