You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ariatosca.apache.org by mx...@apache.org on 2017/07/26 15:51:23 UTC

incubator-ariatosca git commit: fixed all of the tests, still remain fix the dump_types, figure out the relationship between the context issues and the handler issues, is the handler as a singleton is realy necessary, figure out the local imports [Forced Update!]

Repository: incubator-ariatosca
Updated Branches:
  refs/heads/ARIA-174-Refactor-instantiation-phase a0e776fa8 -> 3a4ed87ba (forced update)


fixed all of the tests, still remain fix the dump_types, figure out the relationship between the context issues and the handler issues, is the handler as a singleton is realy necessary, figure out the local imports


Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/3a4ed87b
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/3a4ed87b
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/3a4ed87b

Branch: refs/heads/ARIA-174-Refactor-instantiation-phase
Commit: 3a4ed87ba1b525c1477582a6348ccc7089d6fded
Parents: 50d4c1d
Author: max-orlov <ma...@gigaspaces.com>
Authored: Wed Jul 26 18:03:26 2017 +0300
Committer: max-orlov <ma...@gigaspaces.com>
Committed: Wed Jul 26 18:51:16 2017 +0300

----------------------------------------------------------------------
 aria/orchestrator/topology/__init__.py    |  12 ++-
 aria/orchestrator/topology/instance.py    | 136 ++++++++++---------------
 aria/orchestrator/topology/template.py    |  51 ++++------
 aria/parser/consumption/consumer.py       |   4 +-
 aria/parser/consumption/modeling.py       |   6 +-
 aria/parser/modeling/context.py           |   6 +-
 aria/parser/validation/context.py         |  59 +----------
 aria/parser/validation/issue.py           |  68 ++++++++++++-
 tests/instantiation/test_configuration.py |  13 +--
 9 files changed, 165 insertions(+), 190 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/aria/orchestrator/topology/__init__.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/topology/__init__.py b/aria/orchestrator/topology/__init__.py
index 5c7747b..afa5334 100644
--- a/aria/orchestrator/topology/__init__.py
+++ b/aria/orchestrator/topology/__init__.py
@@ -14,6 +14,8 @@
 # limitations under the License.
 from StringIO import StringIO
 
+from ...parser.validation import issue
+from ...parser.consumption.style import Style
 from ...modeling import models
 from ...utils import console
 from . import (
@@ -23,7 +25,7 @@ from . import (
 )
 
 
-class Handler(object):
+class Handler(issue.Reporter):
 
     _init_map = {
         models.ServiceTemplate: models.Service,
@@ -52,8 +54,7 @@ class Handler(object):
 
     class TopologyStylizer(object):
         def __init__(self):
-            from aria.parser.consumption import style
-            self._style = style.Style()
+            self._style = Style()
             self._str = StringIO()
 
         def write(self, str_):
@@ -71,9 +72,10 @@ class Handler(object):
             except AttributeError:
                 return super(Handler.TopologyStylizer, self).__getattribute__(item)
 
-    def __init__(self, model_storage=None):
+    def __init__(self, model_storage=None, *args, **kwargs):
         # TODO: model storage is required only for the list of plugins, can we get it
         # somewhere else?
+        super(Handler, self).__init__(*args, **kwargs)
         self._model_storage = model_storage
         self._handlers = dict(self._init_handlers(instance), **self._init_handlers(template))
 
@@ -217,7 +219,7 @@ class Handler(object):
 
     def find_hosts(self, service):
         for node in service.nodes.values():
-            service.host = self._find_host(node)
+            node.host = self._find_host(node)
 
     def configure_operations(self, model, **kwargs):
         if isinstance(model, dict):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/aria/orchestrator/topology/instance.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/topology/instance.py b/aria/orchestrator/topology/instance.py
index 5990792..e160d2b 100644
--- a/aria/orchestrator/topology/instance.py
+++ b/aria/orchestrator/topology/instance.py
@@ -13,14 +13,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from ... parser.modeling import context
 from ... modeling import models
+from ... utils import formatting
 from .. import execution_plugin
 from .. import decorators
 from . import common
 
-# TODO: this should this be here?
-from aria.utils import formatting
-
 
 class Artifact(common._InstanceHandler):
 
@@ -129,16 +128,12 @@ class Node(common._OperatorHolderHandler):
                      **kwargs)
 
     def validate(self, **kwargs):
-        # TODO: fix the context
-        # context = ConsumptionContext.get_thread_local()
-        # if len(self._template.name) > context.modeling.id_max_length:
-        #     pass
-            # context.validation.report('"{0}" has an ID longer than the limit of {1:d} characters: '
-            #                           '{2:d}'.format(
-            #                               self.name,
-            #                               context.modeling.id_max_length,
-            #                               len(self.name)),
-            #                           level=validation.Issue.BETWEEN_INSTANCES)
+        if len(self._model.name) > context.ID_MAX_LENGTH:
+            pass
+            self._topology.report('"{0}" has an ID longer than the limit of {1:d} characters: '
+                                  '{2:d}'.format(
+                self._model.name, context.ID_MAX_LENGTH, len(self._model.name)),
+                level=self._topology.Issue.BETWEEN_INSTANCES)
 
         self._validate(self._model.properties,
                        self._model.attributes,
@@ -166,18 +161,13 @@ class Node(common._OperatorHolderHandler):
             self._topology.configure_operations(relationship)
 
     def validate_capabilities(self):
-        # TODO: fix
-        # context = ConsumptionContext.get_thread_local()
         satisfied = False
         for capability in self._model.capabilities.itervalues():
             if not capability.has_enough_relationships:
-                # context.validation.report('capability "{0}" of node "{1}" requires at least {2:d} '
-                #                           'relationships but has {3:d}'.format(
-                #                               capability.name,
-                #                               self.name,
-                #                               capability.min_occurrences,
-                #                               capability.occurrences),
-                #                           level=validation.Issue.BETWEEN_INSTANCES)
+                self._topology.report('capability "{0}" of node "{1}" requires at least {2:d} '
+                                      'relationships but has {3:d}'.format(
+                    capability.name, self._model.name, capability.min_occurrences, capability.occurrences),
+                    level=self._topology.Issue.BETWEEN_INSTANCES)
                 satisfied = False
         return satisfied
 
@@ -188,22 +178,16 @@ class Node(common._OperatorHolderHandler):
             target_node_template, target_node_capability = self._find_target(requirement_template)
             if target_node_template is not None:
                 satisfied = self._satisfy_capability(
-                    target_node_capability, target_node_template,
-                                                     requirement_template)
+                    target_node_capability, target_node_template, requirement_template)
             else:
-                # TODO: fix
-
-                # context = ConsumptionContext.get_thread_local()
-                # context.validation.report('requirement "{0}" of node "{1}" has no target node '
-                #                           'template'.format(requirement_template.name, self.name),
-                #                           level=validation.Issue.BETWEEN_INSTANCES)
+                self._topology.report('requirement "{0}" of node "{1}" has no target node template'.
+                                      format(requirement_template.name, self._model.name),
+                                      level=self._topology.Issue.BETWEEN_INSTANCES)
                 satisfied = False
         return satisfied
 
     def _satisfy_capability(self, target_node_capability, target_node_template,
                             requirement_template):
-        # TODO: fix reporting
-        # context = ConsumptionContext.get_thread_local()
         # Find target nodes
         target_nodes = target_node_template.nodes
         if target_nodes:
@@ -236,38 +220,32 @@ class Node(common._OperatorHolderHandler):
                 self._model.outbound_relationships.append(relationship_model)
                 return True
             else:
-                # context.validation.report('requirement "{0}" of node "{1}" targets node '
-                #                           'template "{2}" but its instantiated nodes do not '
-                #                           'have enough capacity'.format(
-                #                               requirement_template.name,
-                #                               self.name,
-                #                               target_node_template.name),
-                #                           level=validation.Issue.BETWEEN_INSTANCES)
+                self._topology.report('requirement "{0}" of node "{1}" targets node '
+                                      'template "{2}" but its instantiated nodes do not '
+                                      'have enough capacity'.format(
+                    requirement_template.name, self._model.name, target_node_template.name),
+                    level=self._topology.Issue.BETWEEN_INSTANCES)
                 return False
         else:
-            # context.validation.report('requirement "{0}" of node "{1}" targets node template '
-            #                           '"{2}" but it has no instantiated nodes'.format(
-            #                               requirement_template.name,
-            #                               self.name,
-            #                               target_node_template.name),
-            #                           level=validation.Issue.BETWEEN_INSTANCES)
+            self._topology.report('requirement "{0}" of node "{1}" targets node template '
+                                  '"{2}" but it has no instantiated nodes'.format(
+                requirement_template.name, self._model.name, target_node_template.name),
+                level=self._topology.Issue.BETWEEN_INSTANCES)
             return False
 
     def _find_target(self, requirement_template):
         # We might already have a specific node template, so we'll just verify it
         if requirement_template.target_node_template is not None:
             if not self._model.node_template.is_target_node_template_valid(requirement_template.target_node_template):
-                # TODO: fix
-                pass
-                # context.validation.report('requirement "{0}" of node template "{1}" is for node '
-                                          # 'template "{2}" but it does not match constraints'.format(
-                                          #     self.name,
-                                          #     self.target_node_template.name,
-                                          #     source_node_template.name),
-                                          # level=validation.Issue.BETWEEN_TYPES)
+                self._topology.report('requirement "{0}" of node template "{1}" is for node '
+                                      'template "{2}" but it does not match constraints'.format(
+                    requirement_template.name,
+                    requirement_template.target_node_template.name,
+                    self._model.node_template.name),
+                    level=self._topology.Issue.BETWEEN_TYPES)
             if (requirement_template.target_capability_type is not None or
                 requirement_template.target_capability_name is not None):
-                target_node_capability = self._get_capability_from_requirement(requirement_template)
+                target_node_capability = self._get_capability(requirement_template)
                 if target_node_capability is None:
                     return None, None
             else:
@@ -285,22 +263,27 @@ class Node(common._OperatorHolderHandler):
                 if not self._model.node_template.is_target_node_template_valid(target_node_template):
                     continue
 
-                if requirement_template.target_node_template:
-                    target_node_capability = self._get_capability_from_requirement(requirement_template)
-                    if target_node_capability is None:
-                        continue
-                    else:
-                        return target_node_template, target_node_capability
+                target_node_capability = self._get_capability(requirement_template,
+                                                              target_node_template)
+
+                if target_node_capability is None:
+                    continue
+
+                return target_node_template, target_node_capability
 
         return None, None
 
-    def _get_capability_from_requirement(self, requirement_template):
-        for capability_template in requirement_template.target_node_template.capability_templates.values():
-            if self._satisfies_requirement(capability_template, requirement_template):
+    def _get_capability(self, requirement_template, target_node_template=None):
+        target_node_template = target_node_template or requirement_template.target_node_template
+
+        for capability_template in target_node_template.capability_templates.values():
+            if self._satisfies_requirement(
+                    capability_template, requirement_template, target_node_template):
                 return capability_template
+
         return None
 
-    def _satisfies_requirement(self, capability_template, requirement_template):
+    def _satisfies_requirement(self, capability_template, requirement_template, target_node_template):
         # Do we match the required capability type?
         if (requirement_template.target_capability_type and
             requirement_template.target_capability_type.get_descendant(
@@ -318,7 +301,7 @@ class Node(common._OperatorHolderHandler):
         if requirement_template.target_node_template_constraints:
             for node_template_constraint in requirement_template.target_node_template_constraints:
                 if not node_template_constraint.matches(
-                        self._model.node_template, requirement_template.target_node_template):
+                        self._model.node_template, target_node_template):
                     return False
 
         return True
@@ -396,13 +379,10 @@ class Operation(common._OperatorHolderHandler):
         # Check for reserved arguments
         used_reserved_names = decorators.OPERATION_DECORATOR_RESERVED_ARGUMENTS.intersection(
             self._model.arguments.keys())
-        # if used_reserved_names:
-        #     # context = ConsumptionContext.get_thread_local()
-        #     context.validation.report('using reserved arguments in operation "{0}": {1}'
-        #                               .format(
-        #                                   self.name,
-        #                                   formatting.string_list_as_string(used_reserved_names)),
-        #                               level=validation.Issue.EXTERNAL)
+        if used_reserved_names:
+            self._topology.report('using reserved arguments in operation "{0}": {1}'.format(
+                self._model.name, formatting.string_list_as_string(used_reserved_names)),
+                level=self._topology.Issue.EXTERNAL)
 
 
 class Policy(common._InstanceHandler):
@@ -535,15 +515,11 @@ class Substitution(common._InstanceHandler):
 class SubstitutionMapping(common._InstanceHandler):
 
     def validate(self, **kwargs):
-        # context = ConsumptionContext.get_thread_local()
         if (self._model.capability is None) and (self._model.requirement_template is None):
-            pass
-            # TODO: handler reports
-            # context.validation.report('mapping "{0}" refers to neither capability nor a requirement'
-            #                           ' in node: {1}'.format(
-            #                               self.name,
-            #                               formatting.safe_repr(self.node.name)),
-            #                           level=validation.Issue.BETWEEN_TYPES)
+            self._topology.report('mapping "{0}" refers to neither capability nor a requirement'
+                                  ' in node: {1}'.format(
+                                  self._model.name, formatting.safe_repr(self._model.node.name)),
+                level=self._topology.Issue.BETWEEN_TYPES)
 
     def dump(self, console):
         if self._model.capability is not None:

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/aria/orchestrator/topology/template.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/topology/template.py b/aria/orchestrator/topology/template.py
index 2d1319a..917f7a8 100644
--- a/aria/orchestrator/topology/template.py
+++ b/aria/orchestrator/topology/template.py
@@ -15,6 +15,7 @@
 
 from datetime import datetime
 
+from ...utils import formatting
 from ...modeling import utils as modeling_utils
 from . import utils, common
 
@@ -60,10 +61,8 @@ class ServiceTemplate(common._TemplateHandler):
                     plugin = plugin_specification.plugin
                     service.plugins[plugin.name] = plugin
                 else:
-                    # TODO: fix the context report usage
-                    pass
-                    # self._context.validation.report('specified plugin not found: {0}'.format(
-                    #     plugin_specification.name), level=validation.Issue.EXTERNAL)
+                    self._topology.report('specified plugin not found: {0}'.format(
+                        plugin_specification.name), level=self._topology.Issue.EXTERNAL)
         service.meta_data = self._topology.instantiate(self._model.meta_data)
 
         for node_template in self._model.node_templates.itervalues():
@@ -81,8 +80,7 @@ class ServiceTemplate(common._TemplateHandler):
 
         return service
 
-    @staticmethod
-    def _scaling(node_template):
+    def _scaling(self, node_template):
         scaling = {}
 
         def extract_property(properties, name):
@@ -125,16 +123,10 @@ class ServiceTemplate(common._TemplateHandler):
             scaling['max_instances'] < scaling['min_instances'] or
             scaling['default_instances'] < scaling['min_instances'] or
             scaling['default_instances'] > scaling['max_instances']):
-            pass
-            # TODO: fix this
-            # context = ConsumptionContext.get_thread_local()
-            # context.validation.report('invalid scaling parameters for node template "{0}": '
-                                      # 'min={1}, max={2}, default={3}'.format(
-                                      #     self.name,
-                                      #     scaling['min_instances'],
-                                      #     scaling['max_instances'],
-                                      #     scaling['default_instances']),
-                                      # level=validation.Issue.BETWEEN_TYPES)
+            self._topology.report(
+                'invalid scaling parameters for node template "{0}": min={min_instances}, max='
+                '{max_instances}, default={default_instances}'.format(self._model.name, **scaling),
+                level=self._topology.Issue.BETWEEN_TYPES)
 
         return scaling
 
@@ -475,12 +467,11 @@ class SubstitutionTemplateMapping(common._TemplateHandler):
             node_template = self._model.requirement_template.node_template
         nodes = node_template.nodes
         if len(nodes) == 0:
-            # TODO: manage the context report
-            # self._context.validation.report(
-            #     'mapping "{0}" refers to node template "{1}" but there are no node instances'.
-            #         format(self._template.mapped_name,
-            #                self._template.node_template.name),
-            #     level=validation.Issue.BETWEEN_INSTANCES)
+            self._topology.report(
+                'mapping "{0}" refers to node template "{1}" but there are no node instances'.
+                    format(self._model.mapped_name,
+                           self._model.node_template.name),
+                level=self._topology.Issue.BETWEEN_INSTANCES)
             return None
         # The TOSCA spec does not provide a way to choose the node,
         # so we will just pick the first one
@@ -494,18 +485,14 @@ class SubstitutionTemplateMapping(common._TemplateHandler):
         return substitution_mapping
 
     def validate(self):
-        # context = ConsumptionContext.get_thread_local()
         if all([
                     self._model.capability_template is None,
                     self._model.requirement_template is None
         ]):
-            pass
-            # TODO: handle reporting
-            # context.validation.report('mapping "{0}" refers to neither capability nor a requirement'
-            #                           ' in node template: {1}'.format(
-            #                               self.name,
-            #                               formatting.safe_repr(self.node_template.name)),
-            #                           level=validation.Issue.BETWEEN_TYPES)
+            self._topology.report('mapping "{0}" refers to neither capability nor a requirement '
+                                  'in node template: {1}'.format(
+                self._model.name, formatting.safe_repr(self._model.node_template.name)),
+                level=self._topology.Issue.BETWEEN_TYPES)
 
 
 class RelationshipTemplate(common._TemplateHandler):
@@ -535,9 +522,7 @@ class RelationshipTemplate(common._TemplateHandler):
         return relationship
 
     def validate(self):
-        # TODO: either type or name must be set
-        self._validate(self._model.properties,
-                       self._model.interface_templates)
+        self._validate(self._model.properties, self._model.interface_templates)
 
 
 class OperationTemplate(common._TemplateHandler):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/aria/parser/consumption/consumer.py
----------------------------------------------------------------------
diff --git a/aria/parser/consumption/consumer.py b/aria/parser/consumption/consumer.py
index 4c79aab..8acbf31 100644
--- a/aria/parser/consumption/consumer.py
+++ b/aria/parser/consumption/consumer.py
@@ -13,8 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from aria.orchestrator import topology
-
 
 from ...exceptions import AriaException
 from ...utils.exceptions import print_exception
@@ -29,6 +27,8 @@ class Consumer(object):
     """
 
     def __init__(self, context):
+        from aria.orchestrator import topology
+
         self.handler = topology.handler
         self.context = context
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/aria/parser/consumption/modeling.py
----------------------------------------------------------------------
diff --git a/aria/parser/consumption/modeling.py b/aria/parser/consumption/modeling.py
index 8216816..828cc30 100644
--- a/aria/parser/consumption/modeling.py
+++ b/aria/parser/consumption/modeling.py
@@ -13,8 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from aria.orchestrator import topology
-
 from .consumer import Consumer, ConsumerChain
 from ...utils.formatting import json_dumps, yaml_dumps
 from ... import exceptions
@@ -108,7 +106,7 @@ class InstantiateServiceInstance(Consumer):
             self.context.validation.report('InstantiateServiceInstance consumer: missing service '
                                            'template')
             return
-        self.context.modeling.instance = topology.handler.instantiate(
+        self.context.modeling.instance = self.handler.instantiate(
             self.context.modeling.template,
             inputs=dict(self.context.modeling.inputs)
         )
@@ -125,7 +123,7 @@ class InstantiateServiceInstance(Consumer):
                 CoerceServiceInstanceValues
             )).consume()
 
-        if self.context.validation.dump_issues():
+        if self.handler.dump_issues():
             raise exceptions.InstantiationError('Failed to instantiate service template `{0}`'
                                                 .format(self.context.modeling.template.name))
 

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/aria/parser/modeling/context.py
----------------------------------------------------------------------
diff --git a/aria/parser/modeling/context.py b/aria/parser/modeling/context.py
index 3d75617..286ab60 100644
--- a/aria/parser/modeling/context.py
+++ b/aria/parser/modeling/context.py
@@ -19,6 +19,10 @@ from ...utils.collections import StrictDict, prune
 from ...utils.uuid import generate_uuid
 
 
+# See: http://www.faqs.org/rfcs/rfc1035.html
+ID_MAX_LENGTH = 63
+
+1
 class IdType(object):
     LOCAL_SERIAL = 0
     """
@@ -61,7 +65,7 @@ class ModelingContext(object):
         #self.id_type = IdType.LOCAL_SERIAL
         #self.id_type = IdType.LOCAL_RANDOM
         self.id_type = IdType.UNIVERSAL_RANDOM
-        self.id_max_length = 63 # See: http://www.faqs.org/rfcs/rfc1035.html
+        self.id_max_length = ID_MAX_LENGTH
         self.inputs = StrictDict(key_class=basestring)
 
         self._serial_id_counter = itertools.count(1)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/aria/parser/validation/context.py
----------------------------------------------------------------------
diff --git a/aria/parser/validation/context.py b/aria/parser/validation/context.py
index ef641bd..a245518 100644
--- a/aria/parser/validation/context.py
+++ b/aria/parser/validation/context.py
@@ -13,15 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from .issue import Issue
-from ...utils.threading import LockedList
-from ...utils.collections import FrozenList
-from ...utils.exceptions import print_exception
-from ...utils.console import puts, Colored, indent
-from ...utils.formatting import as_raw
+from . import issue
 
 
-class ValidationContext(object):
+class ValidationContext(issue.Reporter):
     """
     Validation context.
 
@@ -35,53 +30,7 @@ class ValidationContext(object):
     :vartype max_level: int
     """
 
-    def __init__(self):
+    def __init__(self, *args, **kwargs):
+        super(ValidationContext, self).__init__(*args, **kwargs)
         self.allow_unknown_fields = False
         self.allow_primitive_coersion = False
-        self.max_level = Issue.ALL
-
-        self._issues = LockedList()
-
-    def report(self, message=None, exception=None, location=None, line=None,
-               column=None, locator=None, snippet=None, level=Issue.PLATFORM, issue=None):
-        if issue is None:
-            issue = Issue(message, exception, location, line, column, locator, snippet, level)
-
-        # Avoid duplicate issues
-        with self._issues:
-            for i in self._issues:
-                if str(i) == str(issue):
-                    return
-
-            self._issues.append(issue)
-
-    @property
-    def has_issues(self):
-        return len(self._issues) > 0
-
-    @property
-    def issues(self):
-        issues = [i for i in self._issues if i.level <= self.max_level]
-        issues.sort(key=lambda i: (i.level, i.location, i.line, i.column, i.message))
-        return FrozenList(issues)
-
-    @property
-    def issues_as_raw(self):
-        return [as_raw(i) for i in self.issues]
-
-    def dump_issues(self):
-        issues = self.issues
-        if issues:
-            puts(Colored.blue('Validation issues:', bold=True))
-            with indent(2):
-                for issue in issues:
-                    puts(Colored.blue(issue.heading_as_str))
-                    details = issue.details_as_str
-                    if details:
-                        with indent(3):
-                            puts(details)
-                    if issue.exception is not None:
-                        with indent(3):
-                            print_exception(issue.exception)
-            return True
-        return False

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/aria/parser/validation/issue.py
----------------------------------------------------------------------
diff --git a/aria/parser/validation/issue.py b/aria/parser/validation/issue.py
index db8065d..f4304b7 100644
--- a/aria/parser/validation/issue.py
+++ b/aria/parser/validation/issue.py
@@ -15,8 +15,14 @@
 
 from __future__ import absolute_import  # so we can import standard 'collections'
 
-from ...utils.collections import OrderedDict
-from ...utils.type import full_type_name
+from ...utils import (
+    collections,
+    type,
+    threading,
+    exceptions,
+    console,
+    formatting
+)
 
 
 class Issue(object):
@@ -82,14 +88,14 @@ class Issue(object):
 
     @property
     def as_raw(self):
-        return OrderedDict((
+        return collections.OrderedDict((
             ('level', self.level),
             ('message', self.message),
             ('location', self.location),
             ('line', self.line),
             ('column', self.column),
             ('snippet', self.snippet),
-            ('exception', full_type_name(self.exception) if self.exception else None)))
+            ('exception', type.full_type_name(self.exception) if self.exception else None)))
 
     @property
     def locator_as_str(self):
@@ -124,3 +130,57 @@ class Issue(object):
         if details:
             heading_str += ', ' + details
         return heading_str
+
+
+class Reporter(object):
+
+    Issue = Issue
+
+    def __init__(self, *args, **kwargs):
+        super(Reporter, self).__init__(*args, **kwargs)
+        self._issues = threading.LockedList()
+        self.max_level = self.Issue.ALL
+
+    def report(self, message=None, exception=None, location=None, line=None,
+               column=None, locator=None, snippet=None, level=Issue.PLATFORM, issue=None):
+        if issue is None:
+            issue = self.Issue(message, exception, location, line, column, locator, snippet, level)
+
+        # Avoid duplicate issues
+        with self._issues:
+            for i in self._issues:
+                if str(i) == str(issue):
+                    return
+
+            self._issues.append(issue)
+
+    @property
+    def has_issues(self):
+        return len(self._issues) > 0
+
+    @property
+    def issues(self):
+        issues = [i for i in self._issues if i.level <= self.max_level]
+        issues.sort(key=lambda i: (i.level, i.location, i.line, i.column, i.message))
+        return collections.FrozenList(issues)
+
+    @property
+    def issues_as_raw(self):
+        return [formatting.as_raw(i) for i in self.issues]
+
+    def dump_issues(self):
+        issues = self.issues
+        if issues:
+            console.puts(console.Colored.blue('Validation issues:', bold=True))
+            with console.indent(2):
+                for issue in issues:
+                    console.puts(console.Colored.blue(issue.heading_as_str))
+                    details = issue.details_as_str
+                    if details:
+                        with console.indent(3):
+                            console.puts(details)
+                    if issue.exception is not None:
+                        with console.indent(3):
+                            exceptions.print_exception(issue.exception)
+            return True
+        return False
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/3a4ed87b/tests/instantiation/test_configuration.py
----------------------------------------------------------------------
diff --git a/tests/instantiation/test_configuration.py b/tests/instantiation/test_configuration.py
index 659e334..d4d91da 100644
--- a/tests/instantiation/test_configuration.py
+++ b/tests/instantiation/test_configuration.py
@@ -17,6 +17,7 @@ import pytest
 
 from tests.parser.service_templates import consume_literal
 from aria.modeling.utils import parameters_as_values
+from aria.orchestrator.topology import handler
 
 
 TEMPLATE = """
@@ -165,9 +166,9 @@ def test_remote(service):
 
 
 def test_reserved_arguments(broken_service_issues):
-    assert len(broken_service_issues) == 2
-    assert any(
-        all([issue.message.startswith('using reserved arguments in operation "operation":'),
-             'ctx' in issue.message,
-             'toolbelt' in issue.message])
-        for issue in broken_service_issues)
+    assert len(broken_service_issues) == 1
+    assert len(handler.issues) == 1
+    issue = handler.issues[0].message
+    assert all([issue.startswith('using reserved arguments in operation "operation": '),
+                'ctx' in issue,
+                'toolbelt' in issue])