OpenStack Orchestration (Heat)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

template.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. #
  2. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  3. # not use this file except in compliance with the License. You may obtain
  4. # a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software
  9. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  10. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  11. # License for the specific language governing permissions and limitations
  12. # under the License.
  13. import abc
  14. import collections
  15. import copy
  16. import functools
  17. import hashlib
  18. import six
  19. from stevedore import extension
  20. from heat.common import exception
  21. from heat.common.i18n import _
  22. from heat.common import template_format
  23. from heat.engine import conditions
  24. from heat.engine import environment
  25. from heat.engine import function
  26. from heat.engine import template_files
  27. from heat.objects import raw_template as template_object
  28. __all__ = ['Template']
  29. _template_classes = None
  30. def get_version(template_data, available_versions):
  31. version_keys = set(key for key, version in available_versions)
  32. candidate_keys = set(k for k, v in six.iteritems(template_data) if
  33. isinstance(v, six.string_types))
  34. keys_present = version_keys & candidate_keys
  35. if len(keys_present) > 1:
  36. explanation = _('Ambiguous versions (%s)') % ', '.join(keys_present)
  37. raise exception.InvalidTemplateVersion(explanation=explanation)
  38. try:
  39. version_key = keys_present.pop()
  40. except KeyError:
  41. explanation = _('Template version was not provided')
  42. raise exception.InvalidTemplateVersion(explanation=explanation)
  43. return version_key, template_data[version_key]
  44. def _get_template_extension_manager():
  45. return extension.ExtensionManager(
  46. namespace='heat.templates',
  47. invoke_on_load=False,
  48. on_load_failure_callback=raise_extension_exception)
  49. def raise_extension_exception(extmanager, ep, err):
  50. raise TemplatePluginNotRegistered(name=ep.name, error=six.text_type(err))
  51. class TemplatePluginNotRegistered(exception.HeatException):
  52. msg_fmt = _("Could not load %(name)s: %(error)s")
  53. def get_template_class(template_data):
  54. available_versions = _template_classes.keys()
  55. version = get_version(template_data, available_versions)
  56. version_type = version[0]
  57. try:
  58. return _template_classes[version]
  59. except KeyError:
  60. av_list = sorted(
  61. [v for k, v in available_versions if k == version_type])
  62. msg_data = {'version': ': '.join(version),
  63. 'version_type': version_type,
  64. 'available': ', '.join(v for v in av_list)}
  65. if len(av_list) > 1:
  66. explanation = _('"%(version)s". "%(version_type)s" '
  67. 'should be one of: %(available)s') % msg_data
  68. else:
  69. explanation = _('"%(version)s". "%(version_type)s" '
  70. 'should be: %(available)s') % msg_data
  71. raise exception.InvalidTemplateVersion(explanation=explanation)
  72. class Template(collections.Mapping):
  73. """Abstract base class for template format plugins.
  74. All template formats (both internal and third-party) should derive from
  75. Template and implement the abstract functions to provide resource
  76. definitions and other data.
  77. This is a stable third-party API. Do not add implementations that are
  78. specific to internal template formats. Do not add new abstract methods.
  79. """
  80. condition_functions = {}
  81. functions = {}
  82. def __new__(cls, template, *args, **kwargs):
  83. """Create a new Template of the appropriate class."""
  84. global _template_classes
  85. if _template_classes is None:
  86. mgr = _get_template_extension_manager()
  87. _template_classes = dict((tuple(name.split('.')), mgr[name].plugin)
  88. for name in mgr.names())
  89. if cls != Template:
  90. TemplateClass = cls
  91. else:
  92. TemplateClass = get_template_class(template)
  93. return super(Template, cls).__new__(TemplateClass)
  94. def __init__(self, template, template_id=None, files=None, env=None):
  95. """Initialise the template with JSON object and set of Parameters."""
  96. self.id = template_id
  97. self.t = template
  98. self.files = files or {}
  99. self.maps = self[self.MAPPINGS]
  100. self.env = env or environment.Environment({})
  101. self.merge_sections = [self.PARAMETERS]
  102. self.version = get_version(self.t, _template_classes.keys())
  103. self.t_digest = None
  104. condition_functions = {n: function.Invalid for n in self.functions}
  105. condition_functions.update(self.condition_functions)
  106. self._parser_condition_functions = condition_functions
  107. def __deepcopy__(self, memo):
  108. return Template(copy.deepcopy(self.t, memo), files=self.files,
  109. env=self.env)
  110. def merge_snippets(self, other):
  111. for s in self.merge_sections:
  112. if s not in other.t:
  113. continue
  114. if s not in self.t:
  115. self.t[s] = {}
  116. self.t[s].update(other.t[s])
  117. @classmethod
  118. def load(cls, context, template_id, t=None):
  119. """Retrieve a Template with the given ID from the database."""
  120. if t is None:
  121. t = template_object.RawTemplate.get_by_id(context, template_id)
  122. env = environment.Environment(t.environment)
  123. # support loading the legacy t.files, but modern templates will
  124. # have a t.files_id
  125. t_files = t.files or t.files_id
  126. return cls(t.template, template_id=template_id, env=env,
  127. files=t_files)
  128. def store(self, context):
  129. """Store the Template in the database and return its ID."""
  130. rt = {
  131. 'template': self.t,
  132. 'files_id': self.files.store(context),
  133. 'environment': self.env.env_as_dict()
  134. }
  135. if self.id is None:
  136. new_rt = template_object.RawTemplate.create(context, rt)
  137. self.id = new_rt.id
  138. else:
  139. template_object.RawTemplate.update_by_id(context, self.id, rt)
  140. return self.id
  141. @property
  142. def files(self):
  143. return self._template_files
  144. @files.setter
  145. def files(self, files):
  146. self._template_files = template_files.TemplateFiles(files)
  147. def __iter__(self):
  148. """Return an iterator over the section names."""
  149. return (s for s in self.SECTIONS
  150. if s not in self.SECTIONS_NO_DIRECT_ACCESS)
  151. def __len__(self):
  152. """Return the number of sections."""
  153. return len(self.SECTIONS) - len(self.SECTIONS_NO_DIRECT_ACCESS)
  154. @abc.abstractmethod
  155. def param_schemata(self, param_defaults=None):
  156. """Return a dict of parameters.Schema objects for the parameters."""
  157. pass
  158. def all_param_schemata(self, files):
  159. schema = {}
  160. files = files if files is not None else {}
  161. for f in files.values():
  162. try:
  163. data = template_format.parse(f)
  164. except ValueError:
  165. continue
  166. else:
  167. sub_tmpl = Template(data)
  168. schema.update(sub_tmpl.param_schemata())
  169. # Parent template has precedence, so update the schema last.
  170. schema.update(self.param_schemata())
  171. return schema
  172. @abc.abstractmethod
  173. def get_section_name(self, section):
  174. """Get the name of a field within a resource or output definition.
  175. Return the name of the given field (specified by the constants given
  176. in heat.engine.rsrc_defn and heat.engine.output) in the template
  177. format. This is used in error reporting to help users find the
  178. location of errors in the template.
  179. Note that 'section' here does not refer to a top-level section of the
  180. template (like parameters, resources, &c.) as it does everywhere else.
  181. """
  182. pass
  183. @abc.abstractmethod
  184. def parameters(self, stack_identifier, user_params, param_defaults=None):
  185. """Return a parameters.Parameters object for the stack."""
  186. pass
  187. def validate_resource_definitions(self, stack):
  188. """Check validity of resource definitions.
  189. This method is deprecated. Subclasses should validate the resource
  190. definitions in the process of generating them when calling
  191. resource_definitions(). However, for now this method is still called
  192. in case any third-party plugins are relying on this for validation and
  193. need time to migrate.
  194. """
  195. pass
  196. def conditions(self, stack):
  197. """Return a dictionary of resolved conditions."""
  198. return conditions.Conditions({})
  199. @abc.abstractmethod
  200. def outputs(self, stack):
  201. """Return a dictionary of OutputDefinition objects."""
  202. pass
  203. @abc.abstractmethod
  204. def resource_definitions(self, stack):
  205. """Return a dictionary of ResourceDefinition objects."""
  206. pass
  207. @abc.abstractmethod
  208. def add_resource(self, definition, name=None):
  209. """Add a resource to the template.
  210. The resource is passed as a ResourceDefinition object. If no name is
  211. specified, the name from the ResourceDefinition should be used.
  212. """
  213. pass
  214. def add_output(self, definition):
  215. """Add an output to the template.
  216. The output is passed as a OutputDefinition object.
  217. """
  218. raise NotImplementedError
  219. def remove_resource(self, name):
  220. """Remove a resource from the template."""
  221. self.t.get(self.RESOURCES, {}).pop(name)
  222. def remove_all_resources(self):
  223. """Remove all the resources from the template."""
  224. if self.RESOURCES in self.t:
  225. self.t.update({self.RESOURCES: {}})
  226. def parse(self, stack, snippet, path=''):
  227. return parse(self.functions, stack, snippet, path, self)
  228. def parse_condition(self, stack, snippet, path=''):
  229. return parse(self._parser_condition_functions, stack, snippet,
  230. path, self)
  231. def validate(self):
  232. """Validate the template.
  233. Validates the top-level sections of the template as well as syntax
  234. inside select sections. Some sections are not checked here but in
  235. code parts that are responsible for working with the respective
  236. sections (e.g. parameters are check by parameters schema class).
  237. """
  238. t_digest = hashlib.sha256(
  239. six.text_type(self.t).encode('utf-8')).hexdigest()
  240. # TODO(kanagaraj-manickam) currently t_digest is stored in self. which
  241. # is used to check whether already template is validated or not.
  242. # But it needs to be loaded from dogpile cache backend once its
  243. # available in heat (https://specs.openstack.org/openstack/heat-specs/
  244. # specs/liberty/constraint-validation-cache.html). This is required
  245. # as multiple heat-engines may process the same template at least
  246. # in case of instance_group. And it fixes partially bug 1444316
  247. if t_digest == self.t_digest:
  248. return
  249. # check top-level sections
  250. for k in self.t.keys():
  251. if k not in self.SECTIONS:
  252. raise exception.InvalidTemplateSection(section=k)
  253. # check resources
  254. for res in six.itervalues(self[self.RESOURCES]):
  255. try:
  256. if not res or not res.get('Type'):
  257. message = _('Each Resource must contain '
  258. 'a Type key.')
  259. raise exception.StackValidationFailed(message=message)
  260. except AttributeError:
  261. message = _('Resources must contain Resource. '
  262. 'Found a [%s] instead') % type(res)
  263. raise exception.StackValidationFailed(message=message)
  264. self.t_digest = t_digest
  265. @classmethod
  266. def create_empty_template(cls,
  267. version=('heat_template_version', '2015-04-30'),
  268. from_template=None):
  269. """Create an empty template.
  270. Creates a new empty template with given version. If version is
  271. not provided, a new empty HOT template of version "2015-04-30"
  272. is returned.
  273. :param version: A tuple containing version header of the template
  274. version key and value,
  275. e.g. ``('heat_template_version', '2015-04-30')``
  276. :returns: A new empty template.
  277. """
  278. if from_template:
  279. # remove resources from the template and return; keep the
  280. # env and other things intact
  281. tmpl = copy.deepcopy(from_template)
  282. tmpl.remove_all_resources()
  283. return tmpl
  284. else:
  285. tmpl = {version[0]: version[1]}
  286. return cls(tmpl)
  287. def parse(functions, stack, snippet, path='', template=None):
  288. recurse = functools.partial(parse, functions, stack, template=template)
  289. if isinstance(snippet, collections.Mapping):
  290. def mkpath(key):
  291. return '.'.join([path, six.text_type(key)])
  292. if len(snippet) == 1:
  293. fn_name, args = next(six.iteritems(snippet))
  294. Func = functions.get(fn_name)
  295. if Func is not None:
  296. try:
  297. path = '.'.join([path, fn_name])
  298. if (isinstance(Func, type) and
  299. issubclass(Func, function.Macro)):
  300. return Func(stack, fn_name, args,
  301. functools.partial(recurse, path=path),
  302. template)
  303. else:
  304. return Func(stack, fn_name, recurse(args, path))
  305. except (ValueError, TypeError, KeyError) as e:
  306. raise exception.StackValidationFailed(
  307. path=path,
  308. message=six.text_type(e))
  309. return dict((k, recurse(v, mkpath(k)))
  310. for k, v in six.iteritems(snippet))
  311. elif (not isinstance(snippet, six.string_types) and
  312. isinstance(snippet, collections.Iterable)):
  313. def mkpath(idx):
  314. return ''.join([path, '[%d]' % idx])
  315. return [recurse(v, mkpath(i)) for i, v in enumerate(snippet)]
  316. else:
  317. return snippet