# # 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. import copy import glob import itertools import os.path import warnings from oslo.config import cfg import six from heat.common import environment_format as env_fmt from heat.common import exception from heat.common.i18n import _ from heat.engine import support from heat.openstack.common import log LOG = log.getLogger(__name__) class ResourceInfo(object): """Base mapping of resource type to implementation.""" def __new__(cls, registry, path, value, **kwargs): '''Create a new ResourceInfo of the appropriate class.''' if cls != ResourceInfo: # Call is already for a subclass, so pass it through return super(ResourceInfo, cls).__new__(cls) name = path[-1] if name.endswith(('.yaml', '.template')): # a template url for the resource "Type" return TemplateResourceInfo(registry, path, value) elif not isinstance(value, basestring): return ClassResourceInfo(registry, path, value) elif value.endswith(('.yaml', '.template')): # a registered template return TemplateResourceInfo(registry, path, value) elif name.endswith('*'): return GlobResourceInfo(registry, path, value) else: return MapResourceInfo(registry, path, value) def __init__(self, registry, path, value): self.registry = registry self.path = path self.name = path[-1] self.value = value self.user_resource = True def __eq__(self, other): if other is None: return False return (self.path == other.path and self.value == other.value and self.user_resource == other.user_resource) def __ne__(self, other): return not self.__eq__(other) def __lt__(self, other): if self.user_resource != other.user_resource: # user resource must be sorted above system ones. return self.user_resource > other.user_resource if len(self.path) != len(other.path): # more specific (longer) path must be sorted above system ones. return len(self.path) > len(other.path) return self.path < other.path def __gt__(self, other): return other.__lt__(self) def get_resource_info(self, resource_type=None, resource_name=None): return self def matches(self, resource_type): return False def __str__(self): return '[%s](User:%s) %s -> %s' % (self.description, self.user_resource, self.name, str(self.value)) class ClassResourceInfo(ResourceInfo): """Store the mapping of resource name to python class implementation.""" description = 'Plugin' def get_class(self): return self.value class TemplateResourceInfo(ResourceInfo): """Store the info needed to start a TemplateResource. """ description = 'Template' def __init__(self, registry, path, value): super(TemplateResourceInfo, self).__init__(registry, path, value) if self.name.endswith(('.yaml', '.template')): self.template_name = self.name else: self.template_name = value self.value = self.template_name def get_class(self): from heat.engine.resources import template_resource return template_resource.generate_class(str(self.name), self.template_name) class MapResourceInfo(ResourceInfo): """Store the mapping of one resource type to another. like: OS::Networking::FloatingIp -> OS::Neutron::FloatingIp """ description = 'Mapping' def get_class(self): return None def get_resource_info(self, resource_type=None, resource_name=None): return self.registry.get_resource_info(self.value, resource_name) class GlobResourceInfo(MapResourceInfo): """Store the mapping (with wild cards) of one resource type to another. like: OS::Networking::* -> OS::Neutron::* """ description = 'Wildcard Mapping' def get_resource_info(self, resource_type=None, resource_name=None): orig_prefix = self.name[:-1] new_type = self.value[:-1] + resource_type[len(orig_prefix):] return self.registry.get_resource_info(new_type, resource_name) def matches(self, resource_type): return resource_type.startswith(self.name[:-1]) class ResourceRegistry(object): """By looking at the environment, find the resource implementation.""" def __init__(self, global_registry): self._registry = {'resources': {}} self.global_registry = global_registry def load(self, json_snippet): self._load_registry([], json_snippet) def register_class(self, resource_type, resource_class): ri = ResourceInfo(self, [resource_type], resource_class) self._register_info([resource_type], ri) def _load_registry(self, path, registry): for k, v in iter(registry.items()): if v is None: self._register_info(path + [k], None) elif isinstance(v, dict): self._load_registry(path + [k], v) else: self._register_info(path + [k], ResourceInfo(self, path + [k], v)) def _register_info(self, path, info): """place the new info in the correct location in the registry. path: a list of keys ['resources', 'my_server', 'OS::Nova::Server'] """ descriptive_path = '/'.join(path) name = path[-1] # create the structure if needed registry = self._registry for key in path[:-1]: if key not in registry: registry[key] = {} registry = registry[key] if info is None: if name.endswith('*'): # delete all matching entries. for res_name in registry.keys(): if isinstance(registry[res_name], ResourceInfo) and \ res_name.startswith(name[:-1]): LOG.warn(_('Removing %(item)s from %(path)s') % { 'item': res_name, 'path': descriptive_path}) del registry[res_name] else: # delete this entry. LOG.warn(_('Removing %(item)s from %(path)s') % { 'item': name, 'path': descriptive_path}) registry.pop(name, None) return if name in registry and isinstance(registry[name], ResourceInfo): if registry[name] == info: return details = { 'path': descriptive_path, 'was': str(registry[name].value), 'now': str(info.value)} LOG.warn(_('Changing %(path)s from %(was)s to %(now)s') % details) else: LOG.info(_('Registering %(path)s -> %(value)s') % { 'path': descriptive_path, 'value': str(info.value)}) if isinstance(info, ClassResourceInfo): if info.value.support_status.status != support.SUPPORTED: warnings.warn(six.text_type(info.value.support_status.message)) info.user_resource = (self.global_registry is not None) registry[name] = info def iterable_by(self, resource_type, resource_name=None): is_templ_type = resource_type.endswith(('.yaml', '.template')) if self.global_registry is not None and is_templ_type: # we only support dynamic resource types in user environments # not the global environment. # resource with a Type == a template # we dynamically create an entry as it has not been registered. if resource_type not in self._registry: res = ResourceInfo(self, [resource_type], None) self._register_info([resource_type], res) yield self._registry[resource_type] # handle a specific resource mapping. if resource_name: impl = self._registry['resources'].get(resource_name) if impl and resource_type in impl: yield impl[resource_type] # handle: "OS::Nova::Server" -> "Rackspace::Cloud::Server" impl = self._registry.get(resource_type) if impl: yield impl # handle: "OS::*" -> "Dreamhost::*" def is_a_glob(resource_type): return resource_type.endswith('*') globs = itertools.ifilter(is_a_glob, self._registry.keys()) for pattern in globs: if self._registry[pattern].matches(resource_type): yield self._registry[pattern] def get_resource_info(self, resource_type, resource_name=None, registry_type=None, accept_fn=None): """Find possible matches to the resource type and name. chain the results from the global and user registry to find a match. """ # use cases # 1) get the impl. # - filter_by(res_type=X), sort_by(res_name=W, is_user=True) # 2) in TemplateResource we need to get both the # TemplateClass and the ResourceClass # - filter_by(res_type=X, impl_type=TemplateResourceInfo), # sort_by(res_name=W, is_user=True) # - filter_by(res_type=X, impl_type=ClassResourceInfo), # sort_by(res_name=W, is_user=True) # 3) get_types() from the api # - filter_by(is_user=False) # 4) as_dict() to write to the db # - filter_by(is_user=True) if self.global_registry is not None: giter = self.global_registry.iterable_by(resource_type, resource_name) else: giter = [] matches = itertools.chain(self.iterable_by(resource_type, resource_name), giter) for info in sorted(matches): match = info.get_resource_info(resource_type, resource_name) if ((registry_type is None or isinstance(match, registry_type)) and (accept_fn is None or accept_fn(info))): return match def get_class(self, resource_type, resource_name=None, accept_fn=None): if resource_type == "": msg = _('Resource "%s" has no type') % resource_name raise exception.StackValidationFailed(message=msg) elif resource_type is None: msg = _('Non-empty resource type is required ' 'for resource "%s"') % resource_name raise exception.StackValidationFailed(message=msg) elif not isinstance(resource_type, basestring): msg = _('Resource "%s" type is not a string') % resource_name raise exception.StackValidationFailed(message=msg) info = self.get_resource_info(resource_type, resource_name=resource_name, accept_fn=accept_fn) if info is None: msg = _("Unknown resource Type : %s") % resource_type raise exception.StackValidationFailed(message=msg) return info.get_class() def as_dict(self): """Return user resources in a dict format.""" def _as_dict(level): tmp = {} for k, v in iter(level.items()): if isinstance(v, dict): tmp[k] = _as_dict(v) elif v.user_resource: tmp[k] = v.value return tmp return _as_dict(self._registry) def get_types(self, support_status): '''Return a list of valid resource types.''' def is_resource(key): return isinstance(self._registry[key], (ClassResourceInfo, TemplateResourceInfo)) def status_matches(cls): return (support_status is None or cls.get_class().support_status.status == support_status.encode()) return [name for name, cls in six.iteritems(self._registry) if is_resource(name) and status_matches(cls)] class Environment(object): def __init__(self, env=None, user_env=True): """Create an Environment from a dict of varying format. 1) old-school flat parameters 2) or newer {resource_registry: bla, parameters: foo} :param env: the json environment :param user_env: boolean, if false then we manage python resources too. """ if env is None: env = {} if user_env: from heat.engine import resources global_registry = resources.global_env().registry else: global_registry = None self.registry = ResourceRegistry(global_registry) self.registry.load(env.get(env_fmt.RESOURCE_REGISTRY, {})) if env_fmt.PARAMETERS in env: self.params = env[env_fmt.PARAMETERS] else: self.params = dict((k, v) for (k, v) in six.iteritems(env) if k != env_fmt.RESOURCE_REGISTRY) self.constraints = {} self.stack_lifecycle_plugins = [] def load(self, env_snippet): self.registry.load(env_snippet.get(env_fmt.RESOURCE_REGISTRY, {})) self.params.update(env_snippet.get(env_fmt.PARAMETERS, {})) def patch_previous_parameters(self, previous_env, clear_parameters=[]): """This instance of Environment is the new environment where we are reusing as default the previous parameter values. """ previous_parameters = copy.deepcopy(previous_env.params) # clear the parameters from the previous set as requested for p in clear_parameters: previous_parameters.pop(p, None) # patch the new set of parameters previous_parameters.update(self.params) self.params = previous_parameters def user_env_as_dict(self): """Get the environment as a dict, ready for storing in the db.""" return {env_fmt.RESOURCE_REGISTRY: self.registry.as_dict(), env_fmt.PARAMETERS: self.params} def register_class(self, resource_type, resource_class): self.registry.register_class(resource_type, resource_class) def register_constraint(self, constraint_name, constraint): self.constraints[constraint_name] = constraint def register_stack_lifecycle_plugin(self, stack_lifecycle_name, stack_lifecycle_class): self.stack_lifecycle_plugins.append((stack_lifecycle_name, stack_lifecycle_class)) def get_class(self, resource_type, resource_name=None): return self.registry.get_class(resource_type, resource_name) def get_types(self, support_status=None): return self.registry.get_types(support_status) def get_resource_info(self, resource_type, resource_name=None, registry_type=None): return self.registry.get_resource_info(resource_type, resource_name, registry_type) def get_constraint(self, name): return self.constraints.get(name) def get_stack_lifecycle_plugins(self): return self.stack_lifecycle_plugins def read_global_environment(env, env_dir=None): if env_dir is None: cfg.CONF.import_opt('environment_dir', 'heat.common.config') env_dir = cfg.CONF.environment_dir try: env_files = glob.glob(os.path.join(env_dir, '*')) except OSError as osex: LOG.error(_('Failed to read %s') % env_dir) LOG.exception(osex) return for file_path in env_files: try: with open(file_path) as env_fd: LOG.info(_('Loading %s') % file_path) env_body = env_fmt.parse(env_fd.read()) env_fmt.default_for_missing(env_body) env.load(env_body) except ValueError as vex: LOG.error(_('Failed to parse %(file_path)s') % { 'file_path': file_path}) LOG.exception(vex) except IOError as ioex: LOG.error(_('Failed to read %(file_path)s') % { 'file_path': file_path}) LOG.exception(ioex)