Workflow Service for OpenStack.
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.

194 lines
6.5 KiB

  1. # Copyright 2013 - Mirantis, Inc.
  2. # Copyright 2015 - StackStorm, Inc.
  3. # Copyright 2016 - Brocade Communications Systems, Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import inspect
  17. import re
  18. from oslo_db import exception as db_exc
  19. from oslo_log import log as logging
  20. import six
  21. from yaql.language import exceptions as yaql_exc
  22. from yaql.language import factory
  23. from yaql.language import utils as yaql_utils
  24. from mistral.config import cfg
  25. from mistral import exceptions as exc
  26. from mistral.expressions.base_expression import Evaluator
  27. from mistral.utils import expression_utils
  28. from mistral_lib import utils
  29. LOG = logging.getLogger(__name__)
  30. _YAQL_CONF = cfg.CONF.yaql
  31. def get_yaql_engine_options():
  32. return {
  33. "yaql.limitIterators": _YAQL_CONF.limit_iterators,
  34. "yaql.memoryQuota": _YAQL_CONF.memory_quota,
  35. "yaql.convertTuplesToLists": _YAQL_CONF.convert_tuples_to_lists,
  36. "yaql.convertSetsToLists": _YAQL_CONF.convert_sets_to_lists,
  37. "yaql.iterableDicts": _YAQL_CONF.iterable_dicts,
  38. "yaql.convertOutputData": _YAQL_CONF.convert_output_data
  39. }
  40. def create_yaql_engine_class(keyword_operator, allow_delegates,
  41. engine_options):
  42. return factory.YaqlFactory(
  43. keyword_operator=keyword_operator,
  44. allow_delegates=allow_delegates
  45. ).create(options=engine_options)
  46. YAQL_ENGINE = create_yaql_engine_class(
  47. _YAQL_CONF.keyword_operator,
  48. _YAQL_CONF.allow_delegates,
  49. get_yaql_engine_options()
  50. )
  51. LOG.info(
  52. "YAQL engine has been initialized with the options: \n%s",
  53. utils.merge_dicts(
  54. get_yaql_engine_options(),
  55. {
  56. "keyword_operator": _YAQL_CONF.keyword_operator,
  57. "allow_delegates": _YAQL_CONF.allow_delegates
  58. }
  59. )
  60. )
  61. INLINE_YAQL_REGEXP = '<%.*?%>'
  62. def _sanitize_yaql_result(result):
  63. # Expression output conversion can be disabled but we can still
  64. # do some basic unboxing if we got an internal YAQL type.
  65. # TODO(rakhmerov): FrozenDict doesn't provide any public method
  66. # or property to access a regular dict that it wraps so ideally
  67. # we need to add it to YAQL. Once it's there we need to make a
  68. # fix here.
  69. if isinstance(result, yaql_utils.FrozenDict):
  70. return result._d
  71. return result if not inspect.isgenerator(result) else list(result)
  72. class YAQLEvaluator(Evaluator):
  73. @classmethod
  74. def validate(cls, expression):
  75. try:
  76. YAQL_ENGINE(expression)
  77. except (yaql_exc.YaqlException, KeyError, ValueError, TypeError) as e:
  78. raise exc.YaqlGrammarException(getattr(e, 'message', e))
  79. @classmethod
  80. def evaluate(cls, expression, data_context):
  81. expression = expression.strip() if expression else expression
  82. try:
  83. result = YAQL_ENGINE(expression).evaluate(
  84. context=expression_utils.get_yaql_context(data_context)
  85. )
  86. except Exception as e:
  87. # NOTE(rakhmerov): if we hit a database error then we need to
  88. # re-raise the initial exception so that upper layers had a
  89. # chance to handle it properly (e.g. in case of DB deadlock
  90. # the operations needs to retry. Essentially, such situation
  91. # indicates a problem with DB rather than with the expression
  92. # syntax or values.
  93. if isinstance(e, db_exc.DBError):
  94. LOG.error(
  95. "Failed to evaluate YAQL expression due to a database"
  96. " error, re-raising initial exception [expression=%s,"
  97. " error=%s, data=%s]",
  98. expression,
  99. str(e),
  100. data_context
  101. )
  102. raise e
  103. raise exc.YaqlEvaluationException(
  104. "Can not evaluate YAQL expression [expression=%s, error=%s"
  105. ", data=%s]" % (expression, str(e), data_context)
  106. )
  107. return _sanitize_yaql_result(result)
  108. @classmethod
  109. def is_expression(cls, s):
  110. # The class should not be used outside of InlineYAQLEvaluator since by
  111. # convention, YAQL expression should always be wrapped in '<% %>'.
  112. return False
  113. class InlineYAQLEvaluator(YAQLEvaluator):
  114. # This regular expression will look for multiple occurrences of YAQL
  115. # expressions in '<% %>' (i.e. <% any_symbols %>) within a string.
  116. find_expression_pattern = re.compile(INLINE_YAQL_REGEXP)
  117. @classmethod
  118. def validate(cls, expression):
  119. if not isinstance(expression, six.string_types):
  120. raise exc.YaqlEvaluationException(
  121. "Unsupported type '%s'." % type(expression)
  122. )
  123. found_expressions = cls.find_inline_expressions(expression)
  124. if found_expressions:
  125. [super(InlineYAQLEvaluator, cls).validate(expr.strip("<%>"))
  126. for expr in found_expressions]
  127. @classmethod
  128. def evaluate(cls, expression, data_context):
  129. LOG.debug(
  130. "Start to evaluate YAQL expression. "
  131. "[expression='%s', context=%s]",
  132. expression,
  133. data_context
  134. )
  135. result = expression
  136. found_expressions = cls.find_inline_expressions(expression)
  137. if found_expressions:
  138. for expr in found_expressions:
  139. trim_expr = expr.strip("<%>")
  140. evaluated = super(InlineYAQLEvaluator,
  141. cls).evaluate(trim_expr, data_context)
  142. if len(expression) == len(expr):
  143. result = evaluated
  144. else:
  145. result = result.replace(expr, str(evaluated))
  146. LOG.debug(
  147. "Finished evaluation. [expression='%s', result: %s]",
  148. expression,
  149. utils.cut(result, length=200)
  150. )
  151. return result
  152. @classmethod
  153. def is_expression(cls, s):
  154. return cls.find_expression_pattern.search(s)
  155. @classmethod
  156. def find_inline_expressions(cls, s):
  157. return cls.find_expression_pattern.findall(s)