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.

1665 lines
54KB

  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 collections
  14. import hashlib
  15. import itertools
  16. from oslo_config import cfg
  17. from oslo_log import log as logging
  18. from oslo_serialization import jsonutils
  19. import six
  20. from six.moves.urllib import parse as urlparse
  21. import yaql
  22. from yaql.language import exceptions
  23. from heat.common import exception
  24. from heat.common.i18n import _
  25. from heat.engine import attributes
  26. from heat.engine import function
  27. LOG = logging.getLogger(__name__)
  28. opts = [
  29. cfg.IntOpt('limit_iterators',
  30. default=200,
  31. help=_('The maximum number of elements in collection '
  32. 'expression can take for its evaluation.')),
  33. cfg.IntOpt('memory_quota',
  34. default=10000,
  35. help=_('The maximum size of memory in bytes that '
  36. 'expression can take for its evaluation.'))
  37. ]
  38. cfg.CONF.register_opts(opts, group='yaql')
  39. class GetParam(function.Function):
  40. """A function for resolving parameter references.
  41. Takes the form::
  42. get_param: <param_name>
  43. or::
  44. get_param:
  45. - <param_name>
  46. - <path1>
  47. - ...
  48. """
  49. def __init__(self, stack, fn_name, args):
  50. super(GetParam, self).__init__(stack, fn_name, args)
  51. if self.stack is not None:
  52. self.parameters = self.stack.parameters
  53. else:
  54. self.parameters = None
  55. def result(self):
  56. assert self.parameters is not None, "No stack definition in Function"
  57. args = function.resolve(self.args)
  58. if not args:
  59. raise ValueError(_('Function "%s" must have arguments') %
  60. self.fn_name)
  61. if isinstance(args, six.string_types):
  62. param_name = args
  63. path_components = []
  64. elif isinstance(args, collections.Sequence):
  65. param_name = args[0]
  66. path_components = args[1:]
  67. else:
  68. raise TypeError(_('Argument to "%s" must be string or list') %
  69. self.fn_name)
  70. if not isinstance(param_name, six.string_types):
  71. raise TypeError(_('Parameter name in "%s" must be string') %
  72. self.fn_name)
  73. try:
  74. parameter = self.parameters[param_name]
  75. except KeyError:
  76. raise exception.UserParameterMissing(key=param_name)
  77. def get_path_component(collection, key):
  78. if not isinstance(collection, (collections.Mapping,
  79. collections.Sequence)):
  80. raise TypeError(_('"%s" can\'t traverse path') % self.fn_name)
  81. if not isinstance(key, (six.string_types, int)):
  82. raise TypeError(_('Path components in "%s" '
  83. 'must be strings') % self.fn_name)
  84. if isinstance(collection, collections.Sequence
  85. ) and isinstance(key, six.string_types):
  86. try:
  87. key = int(key)
  88. except ValueError:
  89. raise TypeError(_("Path components in '%s' "
  90. "must be a string that can be "
  91. "parsed into an "
  92. "integer.") % self.fn_name)
  93. return collection[key]
  94. try:
  95. return six.moves.reduce(get_path_component, path_components,
  96. parameter)
  97. except (KeyError, IndexError, TypeError):
  98. return ''
  99. class GetResource(function.Function):
  100. """A function for resolving resource references.
  101. Takes the form::
  102. get_resource: <resource_name>
  103. """
  104. def _resource(self, path='unknown'):
  105. resource_name = function.resolve(self.args)
  106. try:
  107. return self.stack[resource_name]
  108. except KeyError:
  109. raise exception.InvalidTemplateReference(resource=resource_name,
  110. key=path)
  111. def dependencies(self, path):
  112. return itertools.chain(super(GetResource, self).dependencies(path),
  113. [self._resource(path)])
  114. def result(self):
  115. return self._resource().FnGetRefId()
  116. class GetAttThenSelect(function.Function):
  117. """A function for resolving resource attributes.
  118. Takes the form::
  119. get_attr:
  120. - <resource_name>
  121. - <attribute_name>
  122. - <path1>
  123. - ...
  124. """
  125. def __init__(self, stack, fn_name, args):
  126. super(GetAttThenSelect, self).__init__(stack, fn_name, args)
  127. (self._resource_name,
  128. self._attribute,
  129. self._path_components) = self._parse_args()
  130. def _parse_args(self):
  131. if (not isinstance(self.args, collections.Sequence) or
  132. isinstance(self.args, six.string_types)):
  133. raise TypeError(_('Argument to "%s" must be a list') %
  134. self.fn_name)
  135. if len(self.args) < 2:
  136. raise ValueError(_('Arguments to "%s" must be of the form '
  137. '[resource_name, attribute, (path), ...]') %
  138. self.fn_name)
  139. return self.args[0], self.args[1], self.args[2:]
  140. def _res_name(self):
  141. return function.resolve(self._resource_name)
  142. def _resource(self, path='unknown'):
  143. resource_name = self._res_name()
  144. try:
  145. return self.stack[resource_name]
  146. except KeyError:
  147. raise exception.InvalidTemplateReference(resource=resource_name,
  148. key=path)
  149. def _attr_path(self):
  150. return function.resolve(self._attribute)
  151. def dep_attrs(self, resource_name):
  152. if self._res_name() == resource_name:
  153. try:
  154. attrs = [self._attr_path()]
  155. except Exception as exc:
  156. LOG.debug("Ignoring exception calculating required attributes"
  157. ": %s %s", type(exc).__name__, six.text_type(exc))
  158. attrs = []
  159. else:
  160. attrs = []
  161. return itertools.chain(super(GetAttThenSelect,
  162. self).dep_attrs(resource_name),
  163. attrs)
  164. def all_dep_attrs(self):
  165. try:
  166. attrs = [(self._res_name(), self._attr_path())]
  167. except Exception:
  168. attrs = []
  169. return itertools.chain(function.all_dep_attrs(self.args), attrs)
  170. def dependencies(self, path):
  171. return itertools.chain(super(GetAttThenSelect,
  172. self).dependencies(path),
  173. [self._resource(path)])
  174. def _allow_without_attribute_name(self):
  175. return False
  176. def validate(self):
  177. super(GetAttThenSelect, self).validate()
  178. res = self._resource()
  179. if self._allow_without_attribute_name():
  180. # if allow without attribute_name, then don't check
  181. # when attribute_name is None
  182. if self._attribute is None:
  183. return
  184. attr = function.resolve(self._attribute)
  185. if attr not in res.attributes_schema:
  186. raise exception.InvalidTemplateAttribute(
  187. resource=self._resource_name, key=attr)
  188. def _result_ready(self, r):
  189. if r.action in (r.CREATE, r.ADOPT, r.SUSPEND, r.RESUME,
  190. r.UPDATE, r.ROLLBACK, r.SNAPSHOT, r.CHECK):
  191. return True
  192. return False
  193. def result(self):
  194. attr_name = function.resolve(self._attribute)
  195. resource = self._resource()
  196. if self._result_ready(resource):
  197. attribute = resource.FnGetAtt(attr_name)
  198. else:
  199. attribute = None
  200. if attribute is None:
  201. return None
  202. path_components = function.resolve(self._path_components)
  203. return attributes.select_from_attribute(attribute, path_components)
  204. class GetAtt(GetAttThenSelect):
  205. """A function for resolving resource attributes.
  206. Takes the form::
  207. get_attr:
  208. - <resource_name>
  209. - <attribute_name>
  210. - <path1>
  211. - ...
  212. """
  213. def result(self):
  214. path_components = function.resolve(self._path_components)
  215. attribute = function.resolve(self._attribute)
  216. resource = self._resource()
  217. if self._result_ready(resource):
  218. return resource.FnGetAtt(attribute, *path_components)
  219. else:
  220. return None
  221. def _attr_path(self):
  222. path = function.resolve(self._path_components)
  223. attr = function.resolve(self._attribute)
  224. if path:
  225. return tuple([attr] + path)
  226. else:
  227. return attr
  228. class GetAttAllAttributes(GetAtt):
  229. """A function for resolving resource attributes.
  230. Takes the form::
  231. get_attr:
  232. - <resource_name>
  233. - <attributes_name>
  234. - <path1>
  235. - ...
  236. where <attributes_name> and <path1>, ... are optional arguments. If there
  237. is no <attributes_name>, result will be dict of all resource's attributes.
  238. Else function returns resolved resource's attribute.
  239. """
  240. def _parse_args(self):
  241. if not self.args:
  242. raise ValueError(_('Arguments to "%s" can be of the next '
  243. 'forms: [resource_name] or '
  244. '[resource_name, attribute, (path), ...]'
  245. ) % self.fn_name)
  246. elif isinstance(self.args, collections.Sequence):
  247. if len(self.args) > 1:
  248. return super(GetAttAllAttributes, self)._parse_args()
  249. else:
  250. return self.args[0], None, []
  251. else:
  252. raise TypeError(_('Argument to "%s" must be a list') %
  253. self.fn_name)
  254. def _attr_path(self):
  255. if self._attribute is None:
  256. return attributes.ALL_ATTRIBUTES
  257. return super(GetAttAllAttributes, self)._attr_path()
  258. def result(self):
  259. if self._attribute is None:
  260. r = self._resource()
  261. if (r.status in (r.IN_PROGRESS, r.COMPLETE) and
  262. r.action in (r.CREATE, r.ADOPT, r.SUSPEND, r.RESUME,
  263. r.UPDATE, r.CHECK, r.SNAPSHOT)):
  264. return r.FnGetAtts()
  265. else:
  266. return None
  267. else:
  268. return super(GetAttAllAttributes, self).result()
  269. def _allow_without_attribute_name(self):
  270. return True
  271. class Replace(function.Function):
  272. """A function for performing string substitutions.
  273. Takes the form::
  274. str_replace:
  275. template: <key_1> <key_2>
  276. params:
  277. <key_1>: <value_1>
  278. <key_2>: <value_2>
  279. ...
  280. And resolves to::
  281. "<value_1> <value_2>"
  282. When keys overlap in the template, longer matches are preferred. For keys
  283. of equal length, lexicographically smaller keys are preferred.
  284. """
  285. _strict = False
  286. _allow_empty_value = True
  287. def __init__(self, stack, fn_name, args):
  288. super(Replace, self).__init__(stack, fn_name, args)
  289. self._mapping, self._string = self._parse_args()
  290. if not isinstance(self._mapping,
  291. (collections.Mapping, function.Function)):
  292. raise TypeError(_('"%s" parameters must be a mapping') %
  293. self.fn_name)
  294. def _parse_args(self):
  295. if not isinstance(self.args, collections.Mapping):
  296. raise TypeError(_('Arguments to "%s" must be a map') %
  297. self.fn_name)
  298. try:
  299. mapping = self.args['params']
  300. string = self.args['template']
  301. except (KeyError, TypeError):
  302. example = _('''%s:
  303. template: This is var1 template var2
  304. params:
  305. var1: a
  306. var2: string''') % self.fn_name
  307. raise KeyError(_('"%(fn_name)s" syntax should be %(example)s') %
  308. {'fn_name': self.fn_name, 'example': example})
  309. else:
  310. return mapping, string
  311. def _validate_replacement(self, value, param):
  312. if value is None:
  313. return ''
  314. if not isinstance(value,
  315. (six.string_types, six.integer_types,
  316. float, bool)):
  317. raise TypeError(_('"%(name)s" params must be strings or numbers, '
  318. 'param %(param)s is not valid') %
  319. {'name': self.fn_name, 'param': param})
  320. return six.text_type(value)
  321. def result(self):
  322. template = function.resolve(self._string)
  323. mapping = function.resolve(self._mapping)
  324. if self._strict:
  325. unreplaced_keys = set(mapping)
  326. if not isinstance(template, six.string_types):
  327. raise TypeError(_('"%s" template must be a string') % self.fn_name)
  328. if not isinstance(mapping, collections.Mapping):
  329. raise TypeError(_('"%s" params must be a map') % self.fn_name)
  330. def replace(strings, keys):
  331. if not keys:
  332. return strings
  333. placeholder = keys[0]
  334. if not isinstance(placeholder, six.string_types):
  335. raise TypeError(_('"%s" param placeholders must be strings') %
  336. self.fn_name)
  337. remaining_keys = keys[1:]
  338. value = self._validate_replacement(mapping[placeholder],
  339. placeholder)
  340. def string_split(s):
  341. ss = s.split(placeholder)
  342. if self._strict and len(ss) > 1:
  343. unreplaced_keys.discard(placeholder)
  344. return ss
  345. return [value.join(replace(string_split(s),
  346. remaining_keys)) for s in strings]
  347. ret_val = replace([template], sorted(sorted(mapping),
  348. key=len, reverse=True))[0]
  349. if self._strict and len(unreplaced_keys) > 0:
  350. raise ValueError(
  351. _("The following params were not found in the template: %s") %
  352. ','.join(sorted(sorted(unreplaced_keys),
  353. key=len, reverse=True)))
  354. return ret_val
  355. class ReplaceJson(Replace):
  356. """A function for performing string substitutions.
  357. Takes the form::
  358. str_replace:
  359. template: <key_1> <key_2>
  360. params:
  361. <key_1>: <value_1>
  362. <key_2>: <value_2>
  363. ...
  364. And resolves to::
  365. "<value_1> <value_2>"
  366. When keys overlap in the template, longer matches are preferred. For keys
  367. of equal length, lexicographically smaller keys are preferred.
  368. Non-string param values (e.g maps or lists) are serialized as JSON before
  369. being substituted in.
  370. """
  371. def _validate_replacement(self, value, param):
  372. def _raise_empty_param_value_error():
  373. raise ValueError(
  374. _('%(name)s has an undefined or empty value for param '
  375. '%(param)s, must be a defined non-empty value') %
  376. {'name': self.fn_name, 'param': param})
  377. if value is None:
  378. if self._allow_empty_value:
  379. return ''
  380. else:
  381. _raise_empty_param_value_error()
  382. if not isinstance(value, (six.string_types, six.integer_types,
  383. float, bool)):
  384. if isinstance(value, (collections.Mapping, collections.Sequence)):
  385. if not self._allow_empty_value and len(value) == 0:
  386. _raise_empty_param_value_error()
  387. try:
  388. return jsonutils.dumps(value, default=None, sort_keys=True)
  389. except TypeError:
  390. raise TypeError(_('"%(name)s" params must be strings, '
  391. 'numbers, list or map. '
  392. 'Failed to json serialize %(value)s'
  393. ) % {'name': self.fn_name,
  394. 'value': value})
  395. else:
  396. raise TypeError(_('"%s" params must be strings, numbers, '
  397. 'list or map.') % self.fn_name)
  398. ret_value = six.text_type(value)
  399. if not self._allow_empty_value and not ret_value:
  400. _raise_empty_param_value_error()
  401. return ret_value
  402. class ReplaceJsonStrict(ReplaceJson):
  403. """A function for performing string substitutions.
  404. str_replace_strict is identical to the str_replace function, only
  405. a ValueError is raised if any of the params are not present in
  406. the template.
  407. """
  408. _strict = True
  409. class ReplaceJsonVeryStrict(ReplaceJsonStrict):
  410. """A function for performing string substitutions.
  411. str_replace_vstrict is identical to the str_replace_strict
  412. function, only a ValueError is raised if any of the params are
  413. None or empty.
  414. """
  415. _allow_empty_value = False
  416. class GetFile(function.Function):
  417. """A function for including a file inline.
  418. Takes the form::
  419. get_file: <file_key>
  420. And resolves to the content stored in the files dictionary under the given
  421. key.
  422. """
  423. def __init__(self, stack, fn_name, args):
  424. super(GetFile, self).__init__(stack, fn_name, args)
  425. self.files = self.stack.t.files if self.stack is not None else None
  426. def result(self):
  427. assert self.files is not None, "No stack definition in Function"
  428. args = function.resolve(self.args)
  429. if not (isinstance(args, six.string_types)):
  430. raise TypeError(_('Argument to "%s" must be a string') %
  431. self.fn_name)
  432. f = self.files.get(args)
  433. if f is None:
  434. fmt_data = {'fn_name': self.fn_name,
  435. 'file_key': args}
  436. raise ValueError(_('No content found in the "files" section for '
  437. '%(fn_name)s path: %(file_key)s') % fmt_data)
  438. return f
  439. class Join(function.Function):
  440. """A function for joining strings.
  441. Takes the form::
  442. list_join:
  443. - <delim>
  444. - - <string_1>
  445. - <string_2>
  446. - ...
  447. And resolves to::
  448. "<string_1><delim><string_2><delim>..."
  449. """
  450. def __init__(self, stack, fn_name, args):
  451. super(Join, self).__init__(stack, fn_name, args)
  452. example = '"%s" : [ " ", [ "str1", "str2"]]' % self.fn_name
  453. fmt_data = {'fn_name': self.fn_name,
  454. 'example': example}
  455. if not isinstance(self.args, list):
  456. raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
  457. 'should be: %(example)s') % fmt_data)
  458. try:
  459. self._delim, self._strings = self.args
  460. except ValueError:
  461. raise ValueError(_('Incorrect arguments to "%(fn_name)s" '
  462. 'should be: %(example)s') % fmt_data)
  463. def result(self):
  464. strings = function.resolve(self._strings)
  465. if strings is None:
  466. strings = []
  467. if (isinstance(strings, six.string_types) or
  468. not isinstance(strings, collections.Sequence)):
  469. raise TypeError(_('"%s" must operate on a list') % self.fn_name)
  470. delim = function.resolve(self._delim)
  471. if not isinstance(delim, six.string_types):
  472. raise TypeError(_('"%s" delimiter must be a string') %
  473. self.fn_name)
  474. def ensure_string(s):
  475. if s is None:
  476. return ''
  477. if not isinstance(s, six.string_types):
  478. raise TypeError(
  479. _('Items to join must be strings not %s'
  480. ) % (repr(s)[:200]))
  481. return s
  482. return delim.join(ensure_string(s) for s in strings)
  483. class JoinMultiple(function.Function):
  484. """A function for joining one or more lists of strings.
  485. Takes the form::
  486. list_join:
  487. - <delim>
  488. - - <string_1>
  489. - <string_2>
  490. - ...
  491. - - ...
  492. And resolves to::
  493. "<string_1><delim><string_2><delim>..."
  494. Optionally multiple lists may be specified, which will also be joined.
  495. """
  496. def __init__(self, stack, fn_name, args):
  497. super(JoinMultiple, self).__init__(stack, fn_name, args)
  498. example = '"%s" : [ " ", [ "str1", "str2"] ...]' % fn_name
  499. fmt_data = {'fn_name': fn_name,
  500. 'example': example}
  501. if not isinstance(args, list):
  502. raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
  503. 'should be: %(example)s') % fmt_data)
  504. try:
  505. self._delim = args[0]
  506. self._joinlists = args[1:]
  507. if len(self._joinlists) < 1:
  508. raise ValueError
  509. except (IndexError, ValueError):
  510. raise ValueError(_('Incorrect arguments to "%(fn_name)s" '
  511. 'should be: %(example)s') % fmt_data)
  512. def result(self):
  513. r_joinlists = function.resolve(self._joinlists)
  514. strings = []
  515. for jl in r_joinlists:
  516. if jl:
  517. if (isinstance(jl, six.string_types) or
  518. not isinstance(jl, collections.Sequence)):
  519. raise TypeError(_('"%s" must operate on '
  520. 'a list') % self.fn_name)
  521. strings += jl
  522. delim = function.resolve(self._delim)
  523. if not isinstance(delim, six.string_types):
  524. raise TypeError(_('"%s" delimiter must be a string') %
  525. self.fn_name)
  526. def ensure_string(s):
  527. msg = _('Items to join must be string, map or list not %s'
  528. ) % (repr(s)[:200])
  529. if s is None:
  530. return ''
  531. elif isinstance(s, six.string_types):
  532. return s
  533. elif isinstance(s, (collections.Mapping, collections.Sequence)):
  534. try:
  535. return jsonutils.dumps(s, default=None, sort_keys=True)
  536. except TypeError:
  537. msg = _('Items to join must be string, map or list. '
  538. '%s failed json serialization'
  539. ) % (repr(s)[:200])
  540. raise TypeError(msg)
  541. return delim.join(ensure_string(s) for s in strings)
  542. class MapMerge(function.Function):
  543. """A function for merging maps.
  544. Takes the form::
  545. map_merge:
  546. - <k1>: <v1>
  547. <k2>: <v2>
  548. - <k1>: <v3>
  549. And resolves to::
  550. {"<k1>": "<v3>", "<k2>": "<v2>"}
  551. """
  552. def __init__(self, stack, fn_name, args):
  553. super(MapMerge, self).__init__(stack, fn_name, args)
  554. example = (_('"%s" : [ { "key1": "val1" }, { "key2": "val2" } ]')
  555. % fn_name)
  556. self.fmt_data = {'fn_name': fn_name, 'example': example}
  557. def result(self):
  558. args = function.resolve(self.args)
  559. if not isinstance(args, collections.Sequence):
  560. raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
  561. 'should be: %(example)s') % self.fmt_data)
  562. def ensure_map(m):
  563. if m is None:
  564. return {}
  565. elif isinstance(m, collections.Mapping):
  566. return m
  567. else:
  568. msg = _('Incorrect arguments: Items to merge must be maps.')
  569. raise TypeError(msg)
  570. ret_map = {}
  571. for m in args:
  572. ret_map.update(ensure_map(m))
  573. return ret_map
  574. class MapReplace(function.Function):
  575. """A function for performing substitutions on maps.
  576. Takes the form::
  577. map_replace:
  578. - <k1>: <v1>
  579. <k2>: <v2>
  580. - keys:
  581. <k1>: <K1>
  582. values:
  583. <v2>: <V2>
  584. And resolves to::
  585. {"<K1>": "<v1>", "<k2>": "<V2>"}
  586. """
  587. def __init__(self, stack, fn_name, args):
  588. super(MapReplace, self).__init__(stack, fn_name, args)
  589. example = (_('"%s" : [ { "key1": "val1" }, '
  590. '{"keys": {"key1": "key2"}, "values": {"val1": "val2"}}]')
  591. % fn_name)
  592. self.fmt_data = {'fn_name': fn_name, 'example': example}
  593. def result(self):
  594. args = function.resolve(self.args)
  595. def ensure_map(m):
  596. if m is None:
  597. return {}
  598. elif isinstance(m, collections.Mapping):
  599. return m
  600. else:
  601. msg = (_('Incorrect arguments: to "%(fn_name)s", arguments '
  602. 'must be a list of maps. Example: %(example)s')
  603. % self.fmt_data)
  604. raise TypeError(msg)
  605. try:
  606. in_map = ensure_map(args.pop(0))
  607. repl_map = ensure_map(args.pop(0))
  608. if args != []:
  609. raise IndexError
  610. except (IndexError, AttributeError):
  611. raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
  612. 'should be: %(example)s') % self.fmt_data)
  613. for k in repl_map:
  614. if k not in ('keys', 'values'):
  615. raise ValueError(_('Incorrect arguments to "%(fn_name)s" '
  616. 'should be: %(example)s') % self.fmt_data)
  617. repl_keys = ensure_map(repl_map.get('keys', {}))
  618. repl_values = ensure_map(repl_map.get('values', {}))
  619. ret_map = {}
  620. for k, v in six.iteritems(in_map):
  621. key = repl_keys.get(k)
  622. if key is None:
  623. key = k
  624. elif key in in_map and key != k:
  625. # Keys collide
  626. msg = _('key replacement %s collides with '
  627. 'a key in the input map')
  628. raise ValueError(msg % key)
  629. elif key in ret_map:
  630. # Keys collide
  631. msg = _('key replacement %s collides with '
  632. 'a key in the output map')
  633. raise ValueError(msg % key)
  634. try:
  635. value = repl_values.get(v, v)
  636. except TypeError:
  637. # If the value is unhashable, we get here
  638. value = v
  639. ret_map[key] = value
  640. return ret_map
  641. class ResourceFacade(function.Function):
  642. """A function for retrieving data in a parent provider template.
  643. A function for obtaining data from the facade resource from within the
  644. corresponding provider template.
  645. Takes the form::
  646. resource_facade: <attribute_type>
  647. where the valid attribute types are "metadata", "deletion_policy" and
  648. "update_policy".
  649. """
  650. _RESOURCE_ATTRIBUTES = (
  651. METADATA, DELETION_POLICY, UPDATE_POLICY,
  652. ) = (
  653. 'metadata', 'deletion_policy', 'update_policy'
  654. )
  655. def __init__(self, stack, fn_name, args):
  656. super(ResourceFacade, self).__init__(stack, fn_name, args)
  657. if self.args not in self._RESOURCE_ATTRIBUTES:
  658. fmt_data = {'fn_name': self.fn_name,
  659. 'allowed': ', '.join(self._RESOURCE_ATTRIBUTES)}
  660. raise ValueError(_('Incorrect arguments to "%(fn_name)s" '
  661. 'should be one of: %(allowed)s') % fmt_data)
  662. def result(self):
  663. attr = function.resolve(self.args)
  664. if attr == self.METADATA:
  665. return self.stack.parent_resource.metadata_get()
  666. elif attr == self.UPDATE_POLICY:
  667. up = self.stack.parent_resource.t._update_policy or {}
  668. return function.resolve(up)
  669. elif attr == self.DELETION_POLICY:
  670. return self.stack.parent_resource.t.deletion_policy()
  671. class Removed(function.Function):
  672. """This function existed in previous versions of HOT, but has been removed.
  673. Check the HOT guide for an equivalent native function.
  674. """
  675. def validate(self):
  676. exp = (_("The function %s is not supported in this version of HOT.") %
  677. self.fn_name)
  678. raise exception.InvalidTemplateVersion(explanation=exp)
  679. def result(self):
  680. return super(Removed, self).result()
  681. class Repeat(function.Function):
  682. """A function for iterating over a list of items.
  683. Takes the form::
  684. repeat:
  685. template:
  686. <body>
  687. for_each:
  688. <var>: <list>
  689. The result is a new list of the same size as <list>, where each element
  690. is a copy of <body> with any occurrences of <var> replaced with the
  691. corresponding item of <list>.
  692. """
  693. def __init__(self, stack, fn_name, args):
  694. super(Repeat, self).__init__(stack, fn_name, args)
  695. self._parse_args()
  696. def _parse_args(self):
  697. if not isinstance(self.args, collections.Mapping):
  698. raise TypeError(_('Arguments to "%s" must be a map') %
  699. self.fn_name)
  700. # We don't check for invalid keys appearing here, which is wrong but
  701. # it's probably too late to change
  702. try:
  703. self._for_each = self.args['for_each']
  704. self._template = self.args['template']
  705. except KeyError:
  706. example = ('''repeat:
  707. template: This is %var%
  708. for_each:
  709. %var%: ['a', 'b', 'c']''')
  710. raise KeyError(_('"repeat" syntax should be %s') % example)
  711. self._nested_loop = True
  712. def validate(self):
  713. super(Repeat, self).validate()
  714. if not isinstance(self._for_each, function.Function):
  715. if not isinstance(self._for_each, collections.Mapping):
  716. raise TypeError(_('The "for_each" argument to "%s" must '
  717. 'contain a map') % self.fn_name)
  718. def _valid_arg(self, arg):
  719. if not (isinstance(arg, (collections.Sequence,
  720. function.Function)) and
  721. not isinstance(arg, six.string_types)):
  722. raise TypeError(_('The values of the "for_each" argument to '
  723. '"%s" must be lists') % self.fn_name)
  724. def _do_replacement(self, keys, values, template):
  725. if isinstance(template, six.string_types):
  726. for (key, value) in zip(keys, values):
  727. template = template.replace(key, value)
  728. return template
  729. elif isinstance(template, collections.Sequence):
  730. return [self._do_replacement(keys, values, elem)
  731. for elem in template]
  732. elif isinstance(template, collections.Mapping):
  733. return dict((self._do_replacement(keys, values, k),
  734. self._do_replacement(keys, values, v))
  735. for (k, v) in template.items())
  736. else:
  737. return template
  738. def result(self):
  739. for_each = function.resolve(self._for_each)
  740. keys, lists = six.moves.zip(*for_each.items())
  741. # use empty list for references(None) else validation will fail
  742. value_lens = []
  743. values = []
  744. for value in lists:
  745. if value is None:
  746. values.append([])
  747. else:
  748. self._valid_arg(value)
  749. values.append(value)
  750. value_lens.append(len(value))
  751. if not self._nested_loop and value_lens:
  752. if len(set(value_lens)) != 1:
  753. raise ValueError(_('For %s, the length of for_each values '
  754. 'should be equal if no nested '
  755. 'loop.') % self.fn_name)
  756. template = function.resolve(self._template)
  757. iter_func = itertools.product if self._nested_loop else six.moves.zip
  758. return [self._do_replacement(keys, replacements, template)
  759. for replacements in iter_func(*values)]
  760. class RepeatWithMap(Repeat):
  761. """A function for iterating over a list of items or a dict of keys.
  762. Takes the form::
  763. repeat:
  764. template:
  765. <body>
  766. for_each:
  767. <var>: <list> or <dict>
  768. The result is a new list of the same size as <list> or <dict>, where each
  769. element is a copy of <body> with any occurrences of <var> replaced with the
  770. corresponding item of <list> or key of <dict>.
  771. """
  772. def _valid_arg(self, arg):
  773. if not (isinstance(arg, (collections.Sequence,
  774. collections.Mapping,
  775. function.Function)) and
  776. not isinstance(arg, six.string_types)):
  777. raise TypeError(_('The values of the "for_each" argument to '
  778. '"%s" must be lists or maps') % self.fn_name)
  779. class RepeatWithNestedLoop(RepeatWithMap):
  780. """A function for iterating over a list of items or a dict of keys.
  781. Takes the form::
  782. repeat:
  783. template:
  784. <body>
  785. for_each:
  786. <var>: <list> or <dict>
  787. The result is a new list of the same size as <list> or <dict>, where each
  788. element is a copy of <body> with any occurrences of <var> replaced with the
  789. corresponding item of <list> or key of <dict>.
  790. This function also allows to specify 'permutations' to decide
  791. whether to iterate nested the over all the permutations of the
  792. elements in the given lists.
  793. Takes the form::
  794. repeat:
  795. template:
  796. var: %var%
  797. bar: %bar%
  798. for_each:
  799. %var%: <list1>
  800. %bar%: <list2>
  801. permutations: false
  802. If 'permutations' is not specified, we set the default value to true to
  803. compatible with before behavior. The args have to be lists instead of
  804. dicts if 'permutations' is False because keys in a dict are unordered,
  805. and the list args all have to be of the same length.
  806. """
  807. def _parse_args(self):
  808. super(RepeatWithNestedLoop, self)._parse_args()
  809. self._nested_loop = self.args.get('permutations', True)
  810. if not isinstance(self._nested_loop, bool):
  811. raise TypeError(_('"permutations" should be boolean type '
  812. 'for %s function.') % self.fn_name)
  813. def _valid_arg(self, arg):
  814. if self._nested_loop:
  815. super(RepeatWithNestedLoop, self)._valid_arg(arg)
  816. else:
  817. Repeat._valid_arg(self, arg)
  818. class Digest(function.Function):
  819. """A function for performing digest operations.
  820. Takes the form::
  821. digest:
  822. - <algorithm>
  823. - <value>
  824. Valid algorithms are the ones provided by natively by hashlib (md5, sha1,
  825. sha224, sha256, sha384, and sha512) or any one provided by OpenSSL.
  826. """
  827. def validate_usage(self, args):
  828. if not (isinstance(args, list) and
  829. all([isinstance(a, six.string_types) for a in args])):
  830. msg = _('Argument to function "%s" must be a list of strings')
  831. raise TypeError(msg % self.fn_name)
  832. if len(args) != 2:
  833. msg = _('Function "%s" usage: ["<algorithm>", "<value>"]')
  834. raise ValueError(msg % self.fn_name)
  835. if six.PY3:
  836. algorithms = hashlib.algorithms_available
  837. else:
  838. algorithms = hashlib.algorithms
  839. if args[0].lower() not in algorithms:
  840. msg = _('Algorithm must be one of %s')
  841. raise ValueError(msg % six.text_type(algorithms))
  842. def digest(self, algorithm, value):
  843. _hash = hashlib.new(algorithm)
  844. _hash.update(six.b(value))
  845. return _hash.hexdigest()
  846. def result(self):
  847. args = function.resolve(self.args)
  848. self.validate_usage(args)
  849. return self.digest(*args)
  850. class StrSplit(function.Function):
  851. """A function for splitting delimited strings into a list.
  852. Optionally extracting a specific list member by index.
  853. Takes the form::
  854. str_split:
  855. - <delimiter>
  856. - <string>
  857. - <index>
  858. If <index> is specified, the specified list item will be returned
  859. otherwise, the whole list is returned, similar to get_attr with
  860. path based attributes accessing lists.
  861. """
  862. def __init__(self, stack, fn_name, args):
  863. super(StrSplit, self).__init__(stack, fn_name, args)
  864. example = '"%s" : [ ",", "apples,pears", <index>]' % fn_name
  865. self.fmt_data = {'fn_name': fn_name,
  866. 'example': example}
  867. self.fn_name = fn_name
  868. if isinstance(args, (six.string_types, collections.Mapping)):
  869. raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
  870. 'should be: %(example)s') % self.fmt_data)
  871. def result(self):
  872. args = function.resolve(self.args)
  873. try:
  874. delim = args.pop(0)
  875. str_to_split = args.pop(0)
  876. except (AttributeError, IndexError):
  877. raise ValueError(_('Incorrect arguments to "%(fn_name)s" '
  878. 'should be: %(example)s') % self.fmt_data)
  879. if str_to_split is None:
  880. return None
  881. split_list = str_to_split.split(delim)
  882. # Optionally allow an index to be specified
  883. if args:
  884. try:
  885. index = int(args.pop(0))
  886. except ValueError:
  887. raise ValueError(_('Incorrect index to "%(fn_name)s" '
  888. 'should be: %(example)s') % self.fmt_data)
  889. else:
  890. try:
  891. res = split_list[index]
  892. except IndexError:
  893. raise ValueError(_('Incorrect index to "%(fn_name)s" '
  894. 'should be between 0 and '
  895. '%(max_index)s')
  896. % {'fn_name': self.fn_name,
  897. 'max_index': len(split_list) - 1})
  898. else:
  899. res = split_list
  900. return res
  901. class Yaql(function.Function):
  902. """A function for executing a yaql expression.
  903. Takes the form::
  904. yaql:
  905. expression:
  906. <body>
  907. data:
  908. <var>: <list>
  909. Evaluates expression <body> on the given data.
  910. """
  911. _parser = None
  912. @classmethod
  913. def get_yaql_parser(cls):
  914. if cls._parser is None:
  915. global_options = {
  916. 'yaql.limitIterators': cfg.CONF.yaql.limit_iterators,
  917. 'yaql.memoryQuota': cfg.CONF.yaql.memory_quota
  918. }
  919. cls._parser = yaql.YaqlFactory().create(global_options)
  920. cls._context = yaql.create_context()
  921. return cls._parser
  922. def __init__(self, stack, fn_name, args):
  923. super(Yaql, self).__init__(stack, fn_name, args)
  924. if not isinstance(self.args, collections.Mapping):
  925. raise TypeError(_('Arguments to "%s" must be a map.') %
  926. self.fn_name)
  927. try:
  928. self._expression = self.args['expression']
  929. self._data = self.args.get('data', {})
  930. if set(self.args) - set(['expression', 'data']):
  931. raise KeyError
  932. except (KeyError, TypeError):
  933. example = ('''%s:
  934. expression: $.data.var1.sum()
  935. data:
  936. var1: [3, 2, 1]''') % self.fn_name
  937. raise KeyError(_('"%(name)s" syntax should be %(example)s') % {
  938. 'name': self.fn_name, 'example': example})
  939. def validate(self):
  940. super(Yaql, self).validate()
  941. if not isinstance(self._expression, function.Function):
  942. self._parse(self._expression)
  943. def _parse(self, expression):
  944. if not isinstance(expression, six.string_types):
  945. raise TypeError(_('The "expression" argument to %s must '
  946. 'contain a string.') % self.fn_name)
  947. parse = self.get_yaql_parser()
  948. try:
  949. return parse(expression)
  950. except exceptions.YaqlException as yex:
  951. raise ValueError(_('Bad expression %s.') % yex)
  952. def result(self):
  953. statement = self._parse(function.resolve(self._expression))
  954. data = function.resolve(self._data)
  955. context = self._context.create_child_context()
  956. return statement.evaluate({'data': data}, context)
  957. class Equals(function.Function):
  958. """A function for comparing whether two values are equal.
  959. Takes the form::
  960. equals:
  961. - <value_1>
  962. - <value_2>
  963. The value can be any type that you want to compare. Returns true
  964. if the two values are equal or false if they aren't.
  965. """
  966. def __init__(self, stack, fn_name, args):
  967. super(Equals, self).__init__(stack, fn_name, args)
  968. try:
  969. if (not self.args or
  970. not isinstance(self.args, list)):
  971. raise ValueError()
  972. self.value1, self.value2 = self.args
  973. except ValueError:
  974. msg = _('Arguments to "%s" must be of the form: '
  975. '[value_1, value_2]')
  976. raise ValueError(msg % self.fn_name)
  977. def result(self):
  978. resolved_v1 = function.resolve(self.value1)
  979. resolved_v2 = function.resolve(self.value2)
  980. return resolved_v1 == resolved_v2
  981. class If(function.Macro):
  982. """A function to return corresponding value based on condition evaluation.
  983. Takes the form::
  984. if:
  985. - <condition_name>
  986. - <value_if_true>
  987. - <value_if_false>
  988. The value_if_true to be returned if the specified condition evaluates
  989. to true, the value_if_false to be returned if the specified condition
  990. evaluates to false.
  991. """
  992. def parse_args(self, parse_func):
  993. try:
  994. if (not self.args or
  995. not isinstance(self.args, collections.Sequence) or
  996. isinstance(self.args, six.string_types)):
  997. raise ValueError()
  998. condition, value_if_true, value_if_false = self.args
  999. except ValueError:
  1000. msg = _('Arguments to "%s" must be of the form: '
  1001. '[condition_name, value_if_true, value_if_false]')
  1002. raise ValueError(msg % self.fn_name)
  1003. cond = self.template.parse_condition(self.stack, condition,
  1004. self.fn_name)
  1005. cd = self._get_condition(function.resolve(cond))
  1006. return parse_func(value_if_true if cd else value_if_false)
  1007. def _get_condition(self, cond):
  1008. if isinstance(cond, bool):
  1009. return cond
  1010. return self.template.conditions(self.stack).is_enabled(cond)
  1011. class ConditionBoolean(function.Function):
  1012. """Abstract parent class of boolean condition functions."""
  1013. def __init__(self, stack, fn_name, args):
  1014. super(ConditionBoolean, self).__init__(stack, fn_name, args)
  1015. self._check_args()
  1016. def _check_args(self):
  1017. if not (isinstance(self.args, collections.Sequence) and
  1018. not isinstance(self.args, six.string_types)):
  1019. msg = _('Arguments to "%s" must be a list of conditions')
  1020. raise ValueError(msg % self.fn_name)
  1021. if not self.args or len(self.args) < 2:
  1022. msg = _('The minimum number of condition arguments to "%s" is 2.')
  1023. raise ValueError(msg % self.fn_name)
  1024. def _get_condition(self, arg):
  1025. if isinstance(arg, bool):
  1026. return arg
  1027. conditions = self.stack.t.conditions(self.stack)
  1028. return conditions.is_enabled(arg)
  1029. class Not(ConditionBoolean):
  1030. """A function that acts as a NOT operator on a condition.
  1031. Takes the form::
  1032. not: <condition>
  1033. Returns true for a condition that evaluates to false or
  1034. returns false for a condition that evaluates to true.
  1035. """
  1036. def _check_args(self):
  1037. self.condition = self.args
  1038. if self.args is None:
  1039. msg = _('Argument to "%s" must be a condition')
  1040. raise ValueError(msg % self.fn_name)
  1041. def result(self):
  1042. cd = function.resolve(self.condition)
  1043. return not self._get_condition(cd)
  1044. class And(ConditionBoolean):
  1045. """A function that acts as an AND operator on conditions.
  1046. Takes the form::
  1047. and:
  1048. - <condition_1>
  1049. - <condition_2>
  1050. - ...
  1051. Returns true if all the specified conditions evaluate to true, or returns
  1052. false if any one of the conditions evaluates to false. The minimum number
  1053. of conditions that you can include is 2.
  1054. """
  1055. def result(self):
  1056. return all(self._get_condition(cd)
  1057. for cd in function.resolve(self.args))
  1058. class Or(ConditionBoolean):
  1059. """A function that acts as an OR operator on conditions.
  1060. Takes the form::
  1061. or:
  1062. - <condition_1>
  1063. - <condition_2>
  1064. - ...
  1065. Returns true if any one of the specified conditions evaluate to true,
  1066. or returns false if all of the conditions evaluates to false. The minimum
  1067. number of conditions that you can include is 2.
  1068. """
  1069. def result(self):
  1070. return any(self._get_condition(cd)
  1071. for cd in function.resolve(self.args))
  1072. class Filter(function.Function):
  1073. """A function for filtering out values from lists.
  1074. Takes the form::
  1075. filter:
  1076. - <values>
  1077. - <list>
  1078. Returns a new list without the values.
  1079. """
  1080. def __init__(self, stack, fn_name, args):
  1081. super(Filter, self).__init__(stack, fn_name, args)
  1082. self._values, self._sequence = self._parse_args()
  1083. def _parse_args(self):
  1084. if (not isinstance(self.args, collections.Sequence) or
  1085. isinstance(self.args, six.string_types)):
  1086. raise TypeError(_('Argument to "%s" must be a list') %
  1087. self.fn_name)
  1088. if len(self.args) != 2:
  1089. raise ValueError(_('"%(fn)s" expected 2 arguments of the form '
  1090. '[values, sequence] but got %(len)d arguments '
  1091. 'instead') %
  1092. {'fn': self.fn_name, 'len': len(self.args)})
  1093. return self.args[0], self.args[1]
  1094. def result(self):
  1095. sequence = function.resolve(self._sequence)
  1096. if not sequence:
  1097. return sequence
  1098. if not isinstance(sequence, list):
  1099. raise TypeError(_('"%s" only works with lists') % self.fn_name)
  1100. values = function.resolve(self._values)
  1101. if not values:
  1102. return sequence
  1103. if not isinstance(values, list):
  1104. raise TypeError(
  1105. _('"%(fn)s" filters a list of values') % self.fn_name)
  1106. return [i for i in sequence if i not in values]
  1107. class MakeURL(function.Function):
  1108. """A function for performing substitutions on maps.
  1109. Takes the form::
  1110. make_url:
  1111. scheme: <protocol>
  1112. username: <username>
  1113. password: <password>
  1114. host: <hostname or IP>
  1115. port: <port>
  1116. path: <path>
  1117. query:
  1118. <key1>: <value1>
  1119. fragment: <fragment>
  1120. And resolves to a correctly-escaped URL constructed from the various
  1121. components.
  1122. """
  1123. _ARG_KEYS = (
  1124. SCHEME, USERNAME, PASSWORD, HOST, PORT,
  1125. PATH, QUERY, FRAGMENT,
  1126. ) = (
  1127. 'scheme', 'username', 'password', 'host', 'port',
  1128. 'path', 'query', 'fragment',
  1129. )
  1130. def _check_args(self, args):
  1131. for arg in self._ARG_KEYS:
  1132. if arg in args:
  1133. if arg == self.QUERY:
  1134. if not isinstance(args[arg], (function.Function,
  1135. collections.Mapping)):
  1136. raise TypeError(_('The "%(arg)s" argument to '
  1137. '"%(fn_name)s" must be a map') %
  1138. {'arg': arg,
  1139. 'fn_name': self.fn_name})
  1140. return
  1141. elif arg == self.PORT:
  1142. port = args[arg]
  1143. if not isinstance(port, function.Function):
  1144. if not isinstance(port, six.integer_types):
  1145. try:
  1146. port = int(port)
  1147. except ValueError:
  1148. raise ValueError(
  1149. _('Invalid URL port "%(port)s" '
  1150. 'for %(fn_name)s called with '
  1151. '%(args)s')
  1152. % {'fn_name': self.fn_name,
  1153. 'port': port, 'args': args})
  1154. if not (0 < port <= 65535):
  1155. raise ValueError(
  1156. _('Invalid URL port %d, '
  1157. 'must be in range 1-65535') % port)
  1158. else:
  1159. if not isinstance(args[arg], (function.Function,
  1160. six.string_types)):
  1161. raise TypeError(_('The "%(arg)s" argument to '
  1162. '"%(fn_name)s" must be a string') %
  1163. {'arg': arg,
  1164. 'fn_name': self.fn_name})
  1165. def validate(self):
  1166. super(MakeURL, self).validate()
  1167. if not isinstance(self.args, collections.Mapping):
  1168. raise TypeError(_('The arguments to "%s" must '
  1169. 'be a map') % self.fn_name)
  1170. invalid_keys = set(self.args) - set(self._ARG_KEYS)
  1171. if invalid_keys:
  1172. raise ValueError(_('Invalid arguments to "%(fn)s": %(args)s') %
  1173. {'fn': self.fn_name,
  1174. 'args': ', '.join(invalid_keys)})
  1175. self._check_args(self.args)
  1176. def result(self):
  1177. args = function.resolve(self.args)
  1178. self._check_args(args)
  1179. scheme = args.get(self.SCHEME, '')
  1180. if ':' in scheme:
  1181. raise ValueError(_('URL "%s" should not contain \':\'') %
  1182. self.SCHEME)
  1183. def netloc():
  1184. username = urlparse.quote(args.get(self.USERNAME, ''), safe='')
  1185. password = urlparse.quote(args.get(self.PASSWORD, ''), safe='')
  1186. if username or password:
  1187. yield username
  1188. if password:
  1189. yield ':'
  1190. yield password
  1191. yield '@'
  1192. host = args.get(self.HOST, '')
  1193. if host.startswith('[') and host.endswith(']'):
  1194. host = host[1:-1]
  1195. host = urlparse.quote(host, safe=':')
  1196. if ':' in host:
  1197. host = '[%s]' % host
  1198. yield host
  1199. port = args.get(self.PORT, '')
  1200. if port:
  1201. yield ':'
  1202. yield six.text_type(port)
  1203. path = urlparse.quote(args.get(self.PATH, ''))
  1204. query_dict = args.get(self.QUERY, {})
  1205. query = urlparse.urlencode(query_dict).replace('%2F', '/')
  1206. fragment = urlparse.quote(args.get(self.FRAGMENT, ''))
  1207. return urlparse.urlunsplit((scheme, ''.join(netloc()),
  1208. path, query, fragment))
  1209. class ListConcat(function.Function):
  1210. """A function for extending lists.
  1211. Takes the form::
  1212. list_concat:
  1213. - [<value 1>, <value 2>]
  1214. - [<value 3>, <value 4>]
  1215. And resolves to::
  1216. [<value 1>, <value 2>, <value 3>, <value 4>]
  1217. """
  1218. _unique = False
  1219. def __init__(self, stack, fn_name, args):
  1220. super(ListConcat, self).__init__(stack, fn_name, args)
  1221. example = (_('"%s" : [ [ <value 1>, <value 2> ], '
  1222. '[ <value 3>, <value 4> ] ]')
  1223. % fn_name)
  1224. self.fmt_data = {'fn_name': fn_name, 'example': example}
  1225. def result(self):
  1226. args = function.resolve(self.args)
  1227. if (isinstance(args, six.string_types) or
  1228. not isinstance(args, collections.Sequence)):
  1229. raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
  1230. 'should be: %(example)s') % self.fmt_data)
  1231. def ensure_list(m):
  1232. if m is None:
  1233. return []
  1234. elif (isinstance(m, collections.Sequence) and
  1235. not isinstance(m, six.string_types)):
  1236. return m
  1237. else:
  1238. msg = _('Incorrect arguments: Items to concat must be lists. '
  1239. '%(args)s contains an item that is not a list: '
  1240. '%(item)s')
  1241. raise TypeError(msg % dict(item=jsonutils.dumps(m),
  1242. args=jsonutils.dumps(args)))
  1243. ret_list = []
  1244. for m in args:
  1245. ret_list.extend(ensure_list(m))
  1246. if self._unique:
  1247. for i in ret_list:
  1248. while ret_list.count(i) > 1:
  1249. del ret_list[ret_list.index(i)]
  1250. return ret_list
  1251. class ListConcatUnique(ListConcat):
  1252. """A function for extending lists with unique items.
  1253. list_concat_unique is identical to the list_concat function, only
  1254. contains unique items in retuning list.
  1255. """
  1256. _unique = True
  1257. class Contains(function.Function):
  1258. """A function for checking whether specific value is in sequence.
  1259. Takes the form::
  1260. contains:
  1261. - <value>
  1262. - <sequence>
  1263. The value can be any type that you want to check. Returns true
  1264. if the specific value is in the sequence, otherwise returns false.
  1265. """
  1266. def __init__(self, stack, fn_name, args):
  1267. super(Contains, self).__init__(stack, fn_name, args)
  1268. example = '"%s" : [ "value1", [ "value1", "value2"]]' % self.fn_name
  1269. fmt_data = {'fn_name': self.fn_name,
  1270. 'example': example}
  1271. if not self.args or not isinstance(self.args, list):
  1272. raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
  1273. 'should be: %(example)s') % fmt_data)
  1274. try:
  1275. self.value, self.sequence = self.args
  1276. except ValueError:
  1277. msg = _('Arguments to "%s" must be of the form: '
  1278. '[value1, [value1, value2]]')
  1279. raise ValueError(msg % self.fn_name)
  1280. def result(self):
  1281. resolved_value = function.resolve(self.value)
  1282. resolved_sequence = function.resolve(self.sequence)
  1283. if not isinstance(resolved_sequence, collections.Sequence):
  1284. raise TypeError(_('Second argument to "%s" should be '
  1285. 'a sequence.') % self.fn_name)
  1286. return resolved_value in resolved_sequence