diff --git a/meta/io.murano/Classes/Exception.yaml b/meta/io.murano/Classes/Exception.yaml new file mode 100644 index 000000000..732026368 --- /dev/null +++ b/meta/io.murano/Classes/Exception.yaml @@ -0,0 +1,29 @@ +Namespaces: + =: io.murano + +Name: Exception + +Properties: + name: + Contract: $.string() + Usage: Runtime + + message: + Contract: $.string() + Usage: Runtime + + stackTrace: + Contract: $ + Usage: Runtime + + extra: + Contract: {} + Usage: Runtime + + nativeException: + Contract: $ + Usage: Runtime + + cause: + Contract: $.class(Exception) + Usage: Runtime diff --git a/meta/io.murano/Classes/StackTrace.yaml b/meta/io.murano/Classes/StackTrace.yaml new file mode 100644 index 000000000..636275dcf --- /dev/null +++ b/meta/io.murano/Classes/StackTrace.yaml @@ -0,0 +1,12 @@ +Namespaces: + =: io.murano + +Name: StackTrace + +Properties: + frames: + Contract: + - instruction: $.string() + location: $ + method: $ + class: $ diff --git a/meta/io.murano/manifest.yaml b/meta/io.murano/manifest.yaml index efa6ccd86..9afa14685 100644 --- a/meta/io.murano/manifest.yaml +++ b/meta/io.murano/manifest.yaml @@ -17,6 +17,8 @@ Classes: io.murano.Object: Object.yaml io.murano.Environment: Environment.yaml io.murano.Application: Application.yaml + io.murano.Exception: Exception.yaml + io.murano.StackTrace: StackTrace.yaml io.murano.SharedIp: SharedIp.yaml io.murano.system.SecurityGroupManager: SecurityGroupManager.yaml diff --git a/murano/common/engine.py b/murano/common/engine.py index bba134e46..018018311 100644 --- a/murano/common/engine.py +++ b/murano/common/engine.py @@ -16,6 +16,7 @@ import uuid import anyjson +import eventlet.debug from oslo import messaging from oslo.messaging import target @@ -24,6 +25,7 @@ from murano.common.helpers import token_sanitizer from murano.common import rpc from murano.dsl import executor from murano.dsl import results_serializer +from murano.dsl import virtual_exceptions from murano.engine import environment from murano.engine import package_class_loader from murano.engine import package_loader @@ -36,6 +38,8 @@ RPC_SERVICE = None LOG = logging.getLogger(__name__) +eventlet.debug.hub_exceptions(False) + class TaskProcessingEndpoint(object): @staticmethod @@ -118,7 +122,10 @@ class TaskExecutor(object): if self.action: self._invoke(exc) except Exception as e: - LOG.warn(e, exc_info=1) + if isinstance(e, virtual_exceptions.MuranoPlException): + LOG.error(e.format()) + else: + LOG.exception(e) reporter = status_reporter.StatusReporter() reporter.initialize(obj) reporter.report_error(obj, str(e)) diff --git a/murano/dsl/dsl_exception.py b/murano/dsl/dsl_exception.py new file mode 100644 index 000000000..0dea60092 --- /dev/null +++ b/murano/dsl/dsl_exception.py @@ -0,0 +1,76 @@ +# Copyright (c) 2014 Mirantis, Inc. +# +# 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 murano.dsl.yaql_functions as yaql_functions + + +class MuranoPlException(Exception): + def __init__(self, names, message, stacktrace, extra=None, cause=None): + super(MuranoPlException, self).__init__( + '{0}: {1}'.format(names, message)) + if not isinstance(names, list): + names = [names] + self._names = names + self._message = message + self._stacktrace = stacktrace + self._extra = extra or {} + self._cause = cause + + @property + def names(self): + return self._names + + @property + def message(self): + return self._message + + @property + def stacktrace(self): + return self._stacktrace + + @property + def extra(self): + return self._extra + + @property + def cause(self): + return self._cause + + @staticmethod + def from_python_exception(exception, context): + stacktrace = yaql_functions.new('io.murano.StackTrace', context) + exception_type = type(exception) + names = ['{0}.{1}'.format(exception_type.__module__, + exception_type.__name__)] + + return MuranoPlException( + names, exception.message, stacktrace) + + def _format_name(self): + if not self._names: + return '' + elif len(self._names) == 1: + return self._names[0] + else: + return self._names + + def format(self, prefix=' '): + text = '\n{3}{0}: {1}\n' \ + '{3}Traceback (most recent call last):\n' \ + '{2}'.format(self._format_name(), self.message, + self.stacktrace.toString(prefix + ' '), prefix) + if self._cause is not None: + text += '\n\n{0} Caused by {1}'.format( + prefix, self._cause.format(prefix + ' ').lstrip()) + return text diff --git a/murano/dsl/executor.py b/murano/dsl/executor.py index d40871745..f138dd56a 100644 --- a/murano/dsl/executor.py +++ b/murano/dsl/executor.py @@ -21,7 +21,9 @@ import eventlet import eventlet.event import yaql.context + import murano.dsl.attribute_store as attribute_store +import murano.dsl.dsl_exception as dsl_exception import murano.dsl.expressions as expressions import murano.dsl.helpers as helpers import murano.dsl.murano_method as murano_method @@ -150,12 +152,18 @@ class MuranoDslExecutor(object): if '_context' in inspect.getargspec(body).args: params['_context'] = self._create_context( this, murano_class, context, **params) - if inspect.ismethod(body) and not body.__self__: - return body(this, **params) - else: - return body(**params) + try: + if inspect.ismethod(body) and not body.__self__: + return body(this, **params) + else: + return body(**params) + except Exception as e: + raise dsl_exception.MuranoPlException.from_python_exception( + e, context) elif isinstance(body, expressions.DslExpression): - return self.execute(body, murano_class, this, context, **params) + return self.execute( + body, murano_class, this, context, **params) + else: raise ValueError() diff --git a/murano/dsl/expressions.py b/murano/dsl/expressions.py index 72fd0e622..19cd7c494 100644 --- a/murano/dsl/expressions.py +++ b/murano/dsl/expressions.py @@ -14,6 +14,7 @@ import types +import murano.dsl.dsl_exception as dsl_exception import murano.dsl.helpers as helpers import murano.dsl.lhs_expression as lhs_expression import murano.dsl.yaql_expression as yaql_expression @@ -21,6 +22,15 @@ import murano.dsl.yaql_expression as yaql_expression _macros = [] +class InstructionStub(object): + def __init__(self, title, position): + self._title = title + self.source_file_position = position + + def __str__(self): + return self._title + + def register_macro(cls): _macros.append(cls) @@ -55,11 +65,16 @@ class Statement(DslExpression): return self._expression def execute(self, context, murano_class): - result = helpers.evaluate(self.expression, context) - if self.destination: - self.destination(result, context, murano_class) - - return result + try: + result = helpers.evaluate(self.expression, context) + if self.destination: + self.destination(result, context, murano_class) + return result + except dsl_exception.MuranoPlException: + raise + except Exception as e: + raise dsl_exception.MuranoPlException.from_python_exception( + e, context) def parse_expression(expr): @@ -79,7 +94,16 @@ def parse_expression(expr): if result is None: for cls in _macros: try: - return cls(**kwds) + macro = cls(**kwds) + position = None + title = 'block construct' + if hasattr(expr, 'source_file_position'): + position = expr.source_file_position + if '__str__' in cls.__dict__: + title = str(macro) + macro.virtual_instruction = InstructionStub( + title, position) + return macro except TypeError: continue diff --git a/murano/dsl/helpers.py b/murano/dsl/helpers.py index a8182eccb..47fd022b5 100644 --- a/murano/dsl/helpers.py +++ b/murano/dsl/helpers.py @@ -46,6 +46,14 @@ def serialize(value, memo=None): return value +def execute_instruction(instruction, action, context): + old_instruction = context.get_data('$?currentInstruction') + context.set_data(instruction, '?currentInstruction') + result = action() + context.set_data(old_instruction, '?currentInstruction') + return result + + def evaluate(value, context, max_depth=sys.maxint): if isinstance(value, yaql.expressions.Expression): value = yaql_expression.YaqlExpression(value) @@ -55,11 +63,7 @@ def evaluate(value, context, max_depth=sys.maxint): if max_depth <= 0: return func else: - try: - context.set_data(value, '?currentInstruction') - return func() - finally: - context.set_data(None, '?currentInstruction') + return execute_instruction(value, func, context) elif isinstance(value, types.DictionaryType): result = {} @@ -192,3 +196,7 @@ def get_current_instruction(context): def get_current_method(context): return context.get_data('$?currentMethod') + + +def get_current_exception(context): + return context.get_data('$?currentException') diff --git a/murano/dsl/macros.py b/murano/dsl/macros.py index a7e4e67cf..2369c8a59 100644 --- a/murano/dsl/macros.py +++ b/murano/dsl/macros.py @@ -17,6 +17,7 @@ import types import eventlet.greenpool as greenpool import yaql.context +import murano.dsl.dsl_exception as dsl_exception import murano.dsl.exceptions as exceptions import murano.dsl.expressions as expressions import murano.dsl.helpers as helpers @@ -33,7 +34,20 @@ class CodeBlock(expressions.DslExpression): def execute(self, context, murano_class): try: for expr in self.code_block: - expr.execute(context, murano_class) + def action(): + try: + expr.execute(context, murano_class) + except dsl_exception.MuranoPlException: + raise + except Exception as ex: + raise dsl_exception.MuranoPlException.\ + from_python_exception(ex, context) + + if hasattr(expr, 'virtual_instruction'): + instruction = expr.virtual_instruction + helpers.execute_instruction(instruction, action, context) + else: + action() except exceptions.BreakException as e: if self._breakable: raise e @@ -47,16 +61,14 @@ class MethodBlock(CodeBlock): self._name = name def execute(self, context, murano_class): + new_context = yaql.context.Context(context) + new_context.set_data(self._name, '?currentMethod') try: - context.set_data(self._name, '?currentMethod') - super(MethodBlock, self).execute( - context, murano_class) + super(MethodBlock, self).execute(new_context, murano_class) except exceptions.ReturnException as e: return e.value else: return None - finally: - context.set_data(None, '?currentMethod') class ReturnMacro(expressions.DslExpression): @@ -220,13 +232,14 @@ class DoMacro(expressions.DslExpression): self._code.execute(child_context, murano_class) -expressions.register_macro(DoMacro) -expressions.register_macro(ReturnMacro) -expressions.register_macro(BreakMacro) -expressions.register_macro(ParallelMacro) -expressions.register_macro(IfMacro) -expressions.register_macro(WhileDoMacro) -expressions.register_macro(ForMacro) -expressions.register_macro(RepeatMacro) -expressions.register_macro(MatchMacro) -expressions.register_macro(SwitchMacro) +def register(): + expressions.register_macro(DoMacro) + expressions.register_macro(ReturnMacro) + expressions.register_macro(BreakMacro) + expressions.register_macro(ParallelMacro) + expressions.register_macro(IfMacro) + expressions.register_macro(WhileDoMacro) + expressions.register_macro(ForMacro) + expressions.register_macro(RepeatMacro) + expressions.register_macro(MatchMacro) + expressions.register_macro(SwitchMacro) diff --git a/murano/dsl/murano_method.py b/murano/dsl/murano_method.py index 3dd830332..f9b818660 100644 --- a/murano/dsl/murano_method.py +++ b/murano/dsl/murano_method.py @@ -17,6 +17,7 @@ import types import murano.dsl.macros as macros import murano.dsl.typespec as typespec +import murano.dsl.virtual_exceptions as virtual_exceptions import murano.dsl.yaql_expression as yaql_expression try: @@ -25,6 +26,10 @@ except ImportError: # python2.6 from ordereddict import OrderedDict # noqa +macros.register() +virtual_exceptions.register() + + class MethodUsages(object): Action = 'Action' Runtime = 'Runtime' diff --git a/murano/dsl/principal_objects/__init__.py b/murano/dsl/principal_objects/__init__.py index ce6d8776d..bfb6c6e52 100644 --- a/murano/dsl/principal_objects/__init__.py +++ b/murano/dsl/principal_objects/__init__.py @@ -1,6 +1,28 @@ +# Copyright (c) 2014 Mirantis, Inc. +# +# 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 murano.dsl.principal_objects.exception +import murano.dsl.principal_objects.stack_trace import murano.dsl.principal_objects.sys_object def register(class_loader): sys_object = murano.dsl.principal_objects.sys_object class_loader.import_class(sys_object.SysObject) + + stack_trace = murano.dsl.principal_objects.stack_trace + class_loader.import_class(stack_trace.StackTrace) + + exception = murano.dsl.principal_objects.exception + class_loader.import_class(exception.DslException) diff --git a/murano/dsl/principal_objects/exception.py b/murano/dsl/principal_objects/exception.py new file mode 100644 index 000000000..9cdb0514d --- /dev/null +++ b/murano/dsl/principal_objects/exception.py @@ -0,0 +1,22 @@ +# Copyright (c) 2014 Mirantis, Inc. +# +# 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 murano.dsl import murano_class +from murano.dsl import murano_object + + +@murano_class.classname('io.murano.Exception') +class DslException(murano_object.MuranoObject): + def toString(self): + return self.get_property('nativeException').format() diff --git a/murano/dsl/principal_objects/stack_trace.py b/murano/dsl/principal_objects/stack_trace.py new file mode 100644 index 000000000..9f01821b6 --- /dev/null +++ b/murano/dsl/principal_objects/stack_trace.py @@ -0,0 +1,99 @@ +# Copyright (c) 2014 Mirantis, Inc. +# +# 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 inspect +import os.path + +from murano.dsl import helpers +from murano.dsl import murano_class +from murano.dsl import murano_object +from murano.dsl import yaql_expression + + +@murano_class.classname('io.murano.StackTrace') +class StackTrace(murano_object.MuranoObject): + def initialize(self, _context, includeNativeFrames=True): + frames = [] + context = _context + while True: + if not context: + break + instruction = helpers.get_current_instruction(context) + frames.append({ + 'instruction': None if instruction is None + else str(instruction), + + 'location': None if instruction is None + else instruction.source_file_position, + + 'method': helpers.get_current_method(context), + 'class': helpers.get_type(context) + }) + context = helpers.get_caller_context(context) + frames.pop() + frames.reverse() + + if includeNativeFrames: + class InstructionStub(object): + def __init__(self, title, position): + self._title = title + self.source_file_position = position + + def __str__(self): + return self._title + + native_frames = [] + for frame in inspect.trace()[1:]: + info = inspect.getframeinfo(frame[0]) + position = yaql_expression.YaqlExpressionFilePosition( + os.path.abspath(info.filename), info.lineno, + -1, -1, -1, -1, -1) + instruction = InstructionStub( + info.code_context[0].strip(), position) + method = info.function + native_frames.append({ + 'instruction': instruction, + 'method': method, + 'class': None + }) + frames.extend(native_frames) + + self.set_property('frames', frames) + + def toString(self, prefix=''): + def format_frame(frame): + instruction = frame['instruction'] + method = frame['method'] + murano_class = frame['class'] + location = frame['location'] + + if location: + args = ( + os.path.abspath(location.file_path), + location.start_line, + ':' + str(location.start_column) + if location.start_column >= 0 else '', + method, + instruction, + prefix, + '' if not murano_class else murano_class.name + '::' + ) + return '{5}File "{0}", line {1}{2} in method {6}{3}\n' \ + '{5} {4}'.format(*args) + else: + return '{2}File in method {3}{0}\n{2} {1}'.format( + method, instruction, prefix, + '' if not murano_class else murano_class.name + '::') + + return '\n'.join([format_frame(t)for t in self.get_property('frames')]) diff --git a/murano/dsl/virtual_exceptions.py b/murano/dsl/virtual_exceptions.py new file mode 100644 index 000000000..f9f9dcd76 --- /dev/null +++ b/murano/dsl/virtual_exceptions.py @@ -0,0 +1,151 @@ +# Copyright (c) 2014 Mirantis, Inc. +# +# 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 murano.dsl.dsl_exception as dsl_exception +import murano.dsl.expressions as expressions +import murano.dsl.helpers as helpers +import murano.dsl.macros as macros +import murano.dsl.yaql_functions as yaql_functions + + +class ThrowMacro(expressions.DslExpression): + def __init__(self, Throw, Message=None, Extra=None, Cause=None): + if not Throw: + raise ValueError() + if not isinstance(Throw, list): + Throw = [Throw] + + self._names = Throw + self._message = Message + self._extra = Extra or {} + self._cause = Cause + + def _resolve_names(self, names, context): + murano_class = helpers.get_type(context) + for name in names: + yield murano_class.namespace_resolver.resolve_name(name) + + def execute(self, context, murano_class): + stacktrace = yaql_functions.new('io.murano.StackTrace', context, + includeNativeFrames=False) + cause = None + if self._cause: + cause = helpers.evaluate(self._cause, context).get_property( + 'nativeException') + raise dsl_exception.MuranoPlException( + list(self._resolve_names(helpers.evaluate(self._names, context), + context)), + helpers.evaluate(self._message, context), + stacktrace, self._extra, cause) + + def __str__(self): + if self._message: + return 'Throw {0}: {1}'.format(self._names, self._message) + return 'Throw ' + self._names + + +class CatchBlock(expressions.DslExpression): + def __init__(self, With=None, As=None, Do=None): + if With is not None and not isinstance(With, list): + With = [With] + self._with = With + self._as = As + self._code_block = None if Do is None else macros.CodeBlock(Do) + + def _resolve_names(self, names, context): + murano_class = helpers.get_type(context) + for name in names: + yield murano_class.namespace_resolver.resolve_name(name) + + def execute(self, context, murano_class): + exception = helpers.get_current_exception(context) + names = None if self._with is None else \ + list(self._resolve_names(self._with, context)) + + for name in exception.names: + if self._with is None or name in names: + if self._code_block: + if self._as: + wrapped = self._wrap_internal_exception( + exception, context, name) + context.set_data(wrapped, self._as) + self._code_block.execute(context, murano_class) + return True + return False + + def _wrap_internal_exception(self, exception, context, name): + obj = yaql_functions.new('io.murano.Exception', context) + obj.set_property('name', name) + obj.set_property('message', exception.message) + obj.set_property('stackTrace', exception.stacktrace) + obj.set_property('extra', exception.extra) + obj.set_property('nativeException', exception) + return obj + + +class TryBlockMacro(expressions.DslExpression): + def __init__(self, Try, Catch=None, Finally=None, Else=None): + self._try_block = macros.CodeBlock(Try) + self._catch_block = None + if Catch is not None: + if not isinstance(Catch, list): + Catch = [Catch] + self._catch_block = [CatchBlock(**c) for c in Catch] + self._finally_block = None if Finally is None \ + else macros.CodeBlock(Finally) + self._else_block = None if Else is None \ + else macros.CodeBlock(Else) + + def execute(self, context, murano_class): + try: + self._try_block.execute(context, murano_class) + except dsl_exception.MuranoPlException as e: + caught = False + if self._catch_block: + try: + context.set_data(e, '?currentException') + for cb in self._catch_block: + if cb.execute(context, murano_class): + caught = True + break + if not caught: + raise + finally: + context.set_data(None, '?currentException') + else: + if self._else_block: + self._else_block.execute(context, murano_class) + finally: + if self._finally_block: + self._finally_block.execute(context, murano_class) + + +class RethrowMacro(expressions.DslExpression): + def __init__(self, Rethrow): + pass + + def execute(self, context, murano_class): + exception = context.get_data('$?currentException') + if not exception: + raise TypeError('Rethrow must be inside Catch') + raise exception + + def __str__(self): + return 'Rethrow' + + +def register(): + expressions.register_macro(ThrowMacro) + expressions.register_macro(TryBlockMacro) + expressions.register_macro(RethrowMacro) diff --git a/murano/dsl/virtual_stack.py b/murano/dsl/virtual_stack.py deleted file mode 100644 index 6c1ba1908..000000000 --- a/murano/dsl/virtual_stack.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright (c) 2014 Mirantis, Inc. -# -# 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 os.path - -import yaql.context - -import murano.dsl.helpers as helpers - - -@yaql.context.ContextAware() -def stack_trace(context): - frames = [] - while True: - context = helpers.get_caller_context(context) - if not context: - break - instruction = helpers.get_current_instruction(context) - frames.append({ - 'instruction': None if instruction is None else str(instruction), - 'location': None if instruction is None - else instruction.file_position, - 'method': helpers.get_current_method(context), - 'class': helpers.get_type(context) - }) - frames.pop() - return frames - - -def format_stack_trace_yaql(trace): - return format_stack_trace(trace, '') - - -def format_stack_trace(trace, prefix=''): - def format_frame(frame): - instruction = frame['instruction'] - method = frame['method'] - murano_class = frame['class'] - location = frame['location'] - - if location: - return '{5}File "{0}" at {1}:{2} in method {3} of class {6}\n' \ - '{5} {4}'.format( - os.path.abspath(location.file_path), - location.start_line, - location.start_column, - method, - instruction, - prefix, - murano_class.name) - else: - return '{2}File in method {0}\n{2} {1}'.format( - method, instruction, prefix) - - return '\n'.join([format_frame(t)for t in trace()]) diff --git a/murano/dsl/yaql_expression.py b/murano/dsl/yaql_expression.py index 7f1ef43d6..f84f9c14e 100644 --- a/murano/dsl/yaql_expression.py +++ b/murano/dsl/yaql_expression.py @@ -42,11 +42,11 @@ class YaqlExpression(object): return self._expression @property - def file_position(self): + def source_file_position(self): return self._file_position - @file_position.setter - def file_position(self, value): + @source_file_position.setter + def source_file_position(self, value): self._file_position = value def __repr__(self): diff --git a/murano/dsl/yaql_functions.py b/murano/dsl/yaql_functions.py index 8ceda3ae5..10f143e2d 100644 --- a/murano/dsl/yaql_functions.py +++ b/murano/dsl/yaql_functions.py @@ -53,7 +53,7 @@ def _id(value): @yaql.context.EvalArg('type', str) @yaql.context.ContextAware() def _cast(context, value, type): - if not '.' in type: + if '.' not in type: murano_class = helpers.get_type(context) type = murano_class.namespace_resolver.resolve_name(type) class_loader = helpers.get_class_loader(context) @@ -86,6 +86,10 @@ def _new(context, name, *args): None, object_store, new_context, parameters=parameters) +def new(name, context, **kwargs): + return _new(context, name, lambda: kwargs) + + @yaql.context.EvalArg('value', murano_object.MuranoObject) @yaql.context.ContextAware() def _super(context, value): diff --git a/murano/engine/yaql_yaml_loader.py b/murano/engine/yaql_yaml_loader.py index cc134d968..87bee560f 100644 --- a/murano/engine/yaql_yaml_loader.py +++ b/murano/engine/yaql_yaml_loader.py @@ -14,13 +14,33 @@ # limitations under the License. import yaml +import yaml.composer +import yaml.constructor from murano.dsl import yaql_expression -class YaqlYamlLoader(yaml.Loader): +class MuranoPlDict(dict): pass + +class MuranoPlYamlConstructor(yaml.constructor.Constructor): + def construct_yaml_map(self, node): + data = MuranoPlDict() + data.source_file_position = build_position(node) + yield data + value = self.construct_mapping(node) + data.update(value) + + +class YaqlYamlLoader(yaml.Loader, MuranoPlYamlConstructor): + pass + + +YaqlYamlLoader.add_constructor(u'tag:yaml.org,2002:map', + MuranoPlYamlConstructor.construct_yaml_map) + + # workaround for PyYAML bug: http://pyyaml.org/ticket/221 resolvers = {} for k, v in yaml.Loader.yaml_implicit_resolvers.items(): @@ -28,10 +48,8 @@ for k, v in yaml.Loader.yaml_implicit_resolvers.items(): YaqlYamlLoader.yaml_implicit_resolvers = resolvers -def yaql_constructor(loader, node): - value = loader.construct_scalar(node) - result = yaql_expression.YaqlExpression(value) - position = yaql_expression.YaqlExpressionFilePosition( +def build_position(node): + return yaql_expression.YaqlExpressionFilePosition( node.start_mark.name, node.start_mark.line + 1, node.start_mark.column + 1, @@ -39,7 +57,12 @@ def yaql_constructor(loader, node): node.end_mark.line + 1, node.end_mark.column + 1, node.end_mark.index - node.start_mark.index) - result.file_position = position + + +def yaql_constructor(loader, node): + value = loader.construct_scalar(node) + result = yaql_expression.YaqlExpression(value) + result.source_file_position = build_position(node) return result yaml.add_constructor(u'!yaql', yaql_constructor, YaqlYamlLoader) diff --git a/murano/tests/test_engine.py b/murano/tests/test_engine.py index d4e795eb7..e290c80a7 100644 --- a/murano/tests/test_engine.py +++ b/murano/tests/test_engine.py @@ -18,12 +18,8 @@ import re import mock import yaql -import murano.dsl.exceptions as exceptions import murano.dsl.helpers as helpers -import murano.dsl.murano_class as murano_class -import murano.dsl.murano_object as murano_object import murano.dsl.namespace_resolver as ns_resolver -import murano.dsl.typespec as typespec import murano.dsl.yaql_expression as yaql_expression from murano.tests import base @@ -114,325 +110,6 @@ class Bunch(object): setattr(self, key, value) -class TestClassesManipulation(base.MuranoTestCase): - resolver = mock.Mock(resolve_name=lambda name: name) - - def setUp(self): - super(TestClassesManipulation, self).setUp() - - def test_class_name(self): - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - - self.assertEqual(ROOT_CLASS, cls.name) - - def test_class_namespace_resolver(self): - resolver = ns_resolver.NamespaceResolver({}) - cls = murano_class.MuranoClass(None, resolver, ROOT_CLASS, None) - - self.assertEqual(resolver, cls.namespace_resolver) - - def test_root_class_has_no_parents(self): - root_class = murano_class.MuranoClass( - None, self.resolver, ROOT_CLASS, ['You should not see me!']) - - self.assertEqual([], root_class.parents) - - def test_non_root_class_resolves_parents(self): - root_cls = murano_class.MuranoClass(None, self.resolver, - ROOT_CLASS, None) - class_loader = mock.Mock(get_class=lambda name: root_cls) - desc_cl1 = murano_class.MuranoClass(class_loader, self.resolver, - 'Obj', None) - desc_cl2 = murano_class.MuranoClass( - class_loader, self.resolver, 'Obj', None, [root_cls]) - - self.assertEqual([root_cls], desc_cl1.parents) - self.assertEqual([root_cls], desc_cl2.parents) - - def test_class_initial_properties(self): - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - self.assertEqual([], cls.properties) - - def test_fails_add_incompatible_property_to_class(self): - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - kwargs = {'name': 'sampleProperty', 'property_typespec': {}} - - self.assertRaises(TypeError, cls.add_property, **kwargs) - - def test_add_property_to_class(self): - self.skipTest("FIXME!") - - prop = typespec.PropertySpec({'Default': 1}, self.resolver) - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - cls.add_property('firstPrime', prop) - - class_properties = cls.properties - class_property = cls.get_property('firstPrime') - - self.assertEqual(['firstPrime'], class_properties) - self.assertEqual(prop, class_property) - - def test_class_property_search(self): - self.skipTest("FIXME!") - - void_prop = typespec.PropertySpec({'Default': 'Void'}, self.resolver) - mother_prop = typespec.PropertySpec({'Default': 'Mother'}, - self.resolver) - father_prop = typespec.PropertySpec({'Default': 'Father'}, - self.resolver) - child_prop = typespec.PropertySpec({'Default': 'Child'}, - self.resolver) - root = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - mother = murano_class.MuranoClass(None, self.resolver, - 'Mother', [root]) - father = murano_class.MuranoClass(None, self.resolver, - 'Father', [root]) - child = murano_class.MuranoClass( - None, self.resolver, 'Child', [mother, father]) - - root.add_property('Void', void_prop) - mother.add_property('Mother', mother_prop) - father.add_property('Father', father_prop) - child.add_property('Child', child_prop) - - self.assertEqual(child_prop, child.find_property('Child')) - self.assertEqual(father_prop, child.find_property('Father')) - self.assertEqual(mother_prop, child.find_property('Mother')) - self.assertEqual(void_prop, child.find_property('Void')) - - def test_class_is_compatible(self): - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - descendant_cls = murano_class.MuranoClass( - None, self.resolver, 'DescendantCls', None, [cls]) - obj = mock.Mock(spec=murano_object.MuranoObject) - descendant_obj = mock.Mock(spec=murano_object.MuranoObject) - obj.type = cls - descendant_obj.type = descendant_cls - descendant_obj.parents = [obj] - - self.assertTrue(cls.is_compatible(obj)) - self.assertTrue(cls.is_compatible(descendant_obj)) - self.assertFalse(descendant_cls.is_compatible(obj)) - - def test_new_method_calls_initialize(self): - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - cls.object_class = mock.Mock() - - with mock.patch('inspect.getargspec') as spec_mock: - spec_mock.return_value = Bunch(args=()) - obj = cls.new(None, None, None, {}) - - self.assertTrue(obj.initialize.called) - - def test_new_method_not_calls_initialize(self): - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - cls.object_class = mock.Mock() - - obj = cls.new(None, None, None) - - self.assertFalse(obj.initialize.called) - - -class TestObjectsManipulation(base.MuranoTestCase): - - def setUp(self): - super(TestObjectsManipulation, self).setUp() - - self.resolver = mock.Mock(resolve_name=lambda name: name) - self.cls = mock.Mock() - self.cls.name = ROOT_CLASS - self.cls.parents = [] - - def test_object_valid_type_instantiation(self): - obj = murano_object.MuranoObject(self.cls, None, None, None) - - self.assertEqual(self.cls, obj.type) - - def test_object_own_properties_initialization(self): - # TODO: there should be test for initializing first non-dependent - # object properties, then the dependent ones (given as - # YAQL-expressions) - pass - - def test_object_parent_properties_initialization(self): - root = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - cls = murano_class.MuranoClass(None, self.resolver, - 'SomeClass', None, [root]) - root.new = mock.Mock() - init_kwargs = {'theArg': 0} - obj = murano_object.MuranoObject(cls, None, None, None) - expected_calls = [mock.call().initialize(**init_kwargs)] - - obj.initialize(**init_kwargs) - - # each object should also initialize his parent objects - self.assertEqual(expected_calls, root.new.mock_calls[1:]) - - def test_object_id(self): - _id = 'some_id' - patch_at = 'murano.dsl.helpers.generate_id' - - obj = murano_object.MuranoObject(self.cls, None, None, None, - object_id=_id) - with mock.patch(patch_at) as gen_id_mock: - gen_id_mock.return_value = _id - obj1 = murano_object.MuranoObject(self.cls, None, None, None) - - self.assertEqual(_id, obj.object_id) - self.assertEqual(_id, obj1.object_id) - - def test_parent_obj(self): - parent = mock.Mock() - obj = murano_object.MuranoObject(self.cls, parent, None, None) - - self.assertEqual(parent, obj.parent) - - def test_fails_internal_property_access(self): - self.skipTest("FIXME!") - - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - - cls.add_property('__hidden', - typespec.PropertySpec({'Default': 10}, self.resolver)) - obj = murano_object.MuranoObject(cls, None, None, None) - - self.assertRaises(AttributeError, lambda: obj.__hidden) - - def test_proper_property_access(self): - self.skipTest("FIXME!") - - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - - cls.add_property('someProperty', - typespec.PropertySpec({'Default': 0}, self.resolver)) - obj = cls.new(None, None, None, {}) - - self.assertEqual(0, obj.someProperty) - - def test_parent_class_property_access(self): - self.skipTest("FIXME!") - - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - child_cls = murano_class.MuranoClass(None, self.resolver, - 'Child', [cls]) - - cls.add_property('anotherProperty', - typespec.PropertySpec({'Default': 0}, self.resolver)) - obj = child_cls.new(None, None, None, {}) - - self.assertEqual(0, obj.anotherProperty) - - def test_fails_on_parents_property_collision(self): - self.skipTest("FIXME!") - - root = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - mother = murano_class.MuranoClass(None, self.resolver, - 'Mother', [root]) - father = murano_class.MuranoClass(None, self.resolver, - 'Father', [root]) - child = murano_class.MuranoClass( - None, self.resolver, 'Child', [mother, father]) - - mother.add_property( - 'conflictProp', - typespec.PropertySpec({'Default': 0}, self.resolver)) - father.add_property( - 'conflictProp', - typespec.PropertySpec({'Default': 0}, self.resolver)) - obj = child.new(None, None, None, {}) - - self.assertRaises(LookupError, lambda: obj.conflictProp) - - def test_fails_setting_undeclared_property(self): - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - obj = cls.new(None, None, None, {}) - - self.assertRaises(AttributeError, obj.set_property, 'newOne', 10) - - def test_set_undeclared_property_as_internal(self): - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - obj = cls.new(None, None, None, {}) - obj.cast = mock.Mock(return_value=obj) - prop_value = 10 - - obj.set_property('internalProp', prop_value, caller_class=cls) - resolved_value = obj.get_property('internalProp', caller_class=cls) - - self.assertEqual(prop_value, resolved_value) - - def test_fails_forbidden_set_property(self): - self.skipTest("FIXME!") - - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - cls.add_property('someProperty', - typespec.PropertySpec({'Default': 0}, self.resolver)) - cls.is_compatible = mock.Mock(return_value=False) - obj = cls.new(None, None, None, {}) - - self.assertRaises(exceptions.NoWriteAccess, obj.set_property, - 'someProperty', 10, caller_class=cls) - - def test_set_property(self): - self.skipTest("FIXME!") - - cls = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - cls.add_property('someProperty', - typespec.PropertySpec({'Default': 0}, self.resolver)) - obj = cls.new(None, None, None, {}) - - with mock.patch('yaql.context.Context'): - with mock.patch('murano.engine.helpers') as helpers_mock: - helpers_mock.evaluate = lambda val, ctx, _: val - obj.set_property('someProperty', 10) - - self.assertEqual(10, obj.someProperty) - - def test_set_parent_property(self): - self.skipTest("FIXME!") - - root = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - cls = murano_class.MuranoClass(None, self.resolver, - 'SomeClass', [root]) - root.add_property('rootProperty', - typespec.PropertySpec({'Default': 0}, self.resolver)) - obj = cls.new(None, None, None, {}) - - with mock.patch('murano.engine.helpers') as helpers_mock: - with mock.patch('yaql.context.Context'): - helpers_mock.evaluate = lambda val, ctx, _: val - obj.set_property('rootProperty', 20) - - self.assertEqual(20, obj.rootProperty) - - def test_object_up_cast(self): - self.skipTest("FIXME!") - - root = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS) - root_alt = murano_class.MuranoClass(None, self.resolver, 'RootAlt', []) - cls = murano_class.MuranoClass( - None, self.resolver, 'SomeClass', [root, root_alt]) - root_obj = root.new(None, None, None) - cls_obj = cls.new(None, None, None) - - root_obj_casted2root = root_obj.cast(root) - cls_obj_casted2root = cls_obj.cast(root) - cls_obj_casted2root_alt = cls_obj.cast(root_alt) - - self.assertEqual(root_obj, root_obj_casted2root) - # each object creates an _internal_ parent objects hierarchy, - # so direct comparison of objects is not possible - self.assertEqual(root, cls_obj_casted2root.type) - self.assertEqual(root_alt, cls_obj_casted2root_alt.type) - - def test_fails_object_down_cast(self): - root = murano_class.MuranoClass(None, self.resolver, ROOT_CLASS, None) - cls = murano_class.MuranoClass( - None, self.resolver, 'SomeClass', None, [root]) - root_obj = root.new(None, None, None) - - self.assertRaises(TypeError, root_obj.cast, cls) - - class TestHelperFunctions(base.MuranoTestCase): def setUp(self):