[CORD-2068] [CORD-2069] [CORD-2070] [CORD-2071] [CORD-2072] [CORD-2073]
 Exposing parsing and saving errors

Change-Id: I98ded64a9d5b09a2e69e8f47242be4f775075b00
diff --git a/Dockerfile b/Dockerfile
index 3c56d8a..3a29989 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,3 +1,5 @@
+# docker build -t xosproject/xos-tosca:candidate .
+
 # xosproject/xos-tosca
 FROM xosproject/xos-client:candidate
 
diff --git a/src/grpc_client/main.py b/src/grpc_client/main.py
index aa13362..edda57c 100644
--- a/src/grpc_client/main.py
+++ b/src/grpc_client/main.py
@@ -31,8 +31,6 @@
         self.grpc_secure_endpoint = secure + ":50051"
         self.grpc_insecure_endpoint = insecure + ":50055"
 
-        print self.grpc_secure_endpoint, self.grpc_insecure_endpoint
-
     def setup_resources(self, client, key, deferred, recipe):
         print "[XOS-TOSCA] Loading resources"
         if key not in RESOURCES:
diff --git a/src/grpc_client/models_accessor.py b/src/grpc_client/models_accessor.py
index 079915a..b51ee90 100644
--- a/src/grpc_client/models_accessor.py
+++ b/src/grpc_client/models_accessor.py
@@ -30,8 +30,11 @@
         if data.get('name'):
             used_key = 'name'
         else:
-            # FIXME apparently we're not matching model without a name field
-            used_key = data.keys()[0]
+            if len(data.keys()) > 0:
+                # FIXME apparently we're not matching model without a name field
+                used_key = data.keys()[0]
+            else:
+                used_key = None
 
         key = "%s~%s" % (username, password)
         if not key in RESOURCES:
@@ -40,7 +43,11 @@
             raise Exception('[XOS-TOSCA] The model you are trying to create (%s: %s, class: %s) is not know by xos-core' % (used_key, data[used_key], class_name))
 
         cls = RESOURCES[key][class_name]
-        models = cls.objects.filter(**{used_key: data[used_key]})
+        if used_key:
+            models = cls.objects.filter(**{used_key: data[used_key]})
+        else:
+            # NOTE if we don't have a way to track the model, create a new one
+            models = []
 
         if len(models) == 1:
             print "[XOS-Tosca] Model %s already exist, retrieving instance..." % data[used_key]
@@ -51,7 +58,8 @@
                 raise Exception("[XOS-TOSCA] Model %s:%s has property 'must-exist' but cannot be found" % (class_name, data[used_key]))
 
             model = cls.objects.new()
-            print "[XOS-Tosca] Model %s is new, creating new instance..." % data[used_key]
+            print "[XOS-Tosca] Model %s is new, creating new instance..." % data[used_key] if used_key else class_name
         else:
             raise Exception("[XOS-Tosca] Model %s has multiple instances, I can't handle it" % data[used_key])
+
         return model
\ No newline at end of file
diff --git a/src/tosca/parser.py b/src/tosca/parser.py
index 7c14243..acbd5f1 100644
--- a/src/tosca/parser.py
+++ b/src/tosca/parser.py
@@ -98,20 +98,24 @@
     @staticmethod
     def populate_model(model, data):
         for k,v in data.iteritems():
-            setattr(model, k, v)
+            # NOTE must-exists is a TOSCA implementation choice, remove it before saving the model
+            if k != "must-exist":
+                setattr(model, k, v)
         return model
 
     @staticmethod
     def _translate_exception(msg):
         readable = []
         for line in msg.splitlines():
-            if line.strip().startswith('MissingRequiredFieldError'):
-                readable.append(line)
-            if line.strip().startswith('UnknownFieldError'):
+            if line.strip().startswith('MissingRequiredFieldError') or \
+                    line.strip().startswith('UnknownFieldError') or \
+                    line.strip().startswith('ImportError') or \
+                    line.strip().startswith('InvalidTypeError') or \
+                    line.strip().startswith('TypeMismatchError'):
                 readable.append(line)
 
         if len(readable) > 0:
-            return '/n'.join(readable)
+            return '\n'.join(readable) + '\n'
         else:
             return msg
 
@@ -184,23 +188,33 @@
             self.ordered_models_template = self.get_ordered_models_template(self.ordered_models_name, self.templates_by_model_name)
 
             for recipe in self.ordered_models_template:
-                # get properties from tosca
-                data = recipe.templates[recipe.name]['properties']
-                # [] get model by class name
-                class_name = recipe.type.replace("tosca.nodes.", "")
-                model = GRPCModelsAccessor.get_model_from_classname(class_name, data, self.username, self.password)
-                # [] populate model with data
-                model = self.populate_model(model, data)
-                # [] check if the model has requirements
-                # [] if it has populate them
-                model = self.populate_dependencies(model, recipe.requirements, self.saved_model_by_name)
-                # [] save, update or delete
-                if self.delete and not model.is_new:
-                    model.delete()
-                elif not self.delete:
-                    model.save()
+                try:
+                    # get properties from tosca
+                    if not 'properties' in recipe.templates[recipe.name]:
+                        data = {}
+                    else:
+                        data = recipe.templates[recipe.name]['properties']
+                        if data == None:
+                            data = {}
+                    # [] get model by class name
+                    class_name = recipe.type.replace("tosca.nodes.", "")
+                    model = GRPCModelsAccessor.get_model_from_classname(class_name, data, self.username, self.password)
+                    # [] populate model with data
+                    model = self.populate_model(model, data)
+                    # [] check if the model has requirements
+                    # [] if it has populate them
+                    model = self.populate_dependencies(model, recipe.requirements, self.saved_model_by_name)
+                    # [] save, update or delete
 
-                self.saved_model_by_name[recipe.name] = model
+                    if self.delete and not model.is_new:
+                        model.delete()
+                    elif not self.delete:
+                        model.save()
+
+                    self.saved_model_by_name[recipe.name] = model
+                except Exception, e:
+                    print "[XOS-TOSCA] Failed to save model: %s [%s]" % (class_name, recipe.name)
+                    raise e
 
         except ValidationError as e:
             if e.message:
@@ -216,6 +230,6 @@
                 exception_msg = e._state.details
             raise Exception(exception_msg)
         except Exception, e:
-            raise e
+            raise Exception(e)
 
 
diff --git a/src/tosca/xtarget/tosca.xtarget b/src/tosca/xtarget/tosca.xtarget
index 5faafee..f0eb78b 100644
--- a/src/tosca/xtarget/tosca.xtarget
+++ b/src/tosca/xtarget/tosca.xtarget
@@ -5,6 +5,20 @@
 
 node_types:
 
+    # Example usage:
+    #
+    # <node-name>:
+    #     type: tosca.nodes.{{ m.name }}
+    #     properties:
+    #       must-exist: true # optional to reference models created in other recipes
+{%- for f in (m.fields + xproto_base_fields(m, proto.message_table)) | sort(attribute='name') %}
+{%- if not f.link and xproto_tosca_required(f.options.null, f.options.blank, f.options.default) %}
+    #       {{ f.name }}: <value>
+{%- endif -%}
+{%- endfor %}
+
+
+
     tosca.nodes.{{ m.name }}:
         derived_from: tosca.nodes.Root
         description: {% if m.options.description -%}{{ m.options.description}}{% else%}"An XOS {{ m.name }}"{%- endif %}
@@ -27,6 +41,16 @@
 
     {% for l in m.links %}
     {%- if l.link_type == "manytoone" -%}
+
+
+    # Identify a {{ l.peer.name }} that belongs to a {{ m.name }}
+    #
+    # example usage:
+    # requirements:
+    #   - {{ l.src_port }}:
+    #       node: <node-name>:
+    #       relationship: tosca.relationships.BelongsToOne
+
     tosca.relationships.BelongsToOne:
         derived_from: tosca.relationships.Root
         valid_target_types: [ tosca.capabilities.xos.{{ l.peer.name }} ]
diff --git a/src/web_server/main.py b/src/web_server/main.py
index 319654b..632b943 100644
--- a/src/web_server/main.py
+++ b/src/web_server/main.py
@@ -37,12 +37,17 @@
     app = Klein()
 
     def execute_tosca(self, recipe):
+        self.parser.execute()
+        response_text = "Created models: %s" % str(self.parser.ordered_models_name)
+        return response_text
+
+    def errorCallback(self, failure, request):
+        request.setResponseCode(500)
         try:
-            self.parser.execute()
-            response_text = "Created models: %s" % str(self.parser.ordered_models_name)
-            return response_text
-        except Exception, e:
-            return e.message
+            return failure.getErrorMessage()
+        except:
+            print failure
+            return "Internal server error, please report this along with the failed recipe."
 
     @app.route('/', methods=['GET'])
     def index(self, request):
@@ -70,7 +75,8 @@
 
         d = GRPC_Client().create_secure_client(username, password, recipe)
         self.parser = TOSCA_Parser(recipe, username, password)
-        d.addCallback(self.execute_tosca)
+        tosca_execution = d.addCallback(self.execute_tosca)
+        tosca_execution.addErrback(self.errorCallback, request)
         return d
 
     @app.route('/delete', methods=['POST'])
@@ -82,7 +88,8 @@
 
         d = GRPC_Client().create_secure_client(username, password, recipe)
         self.parser = TOSCA_Parser(recipe, username, password, delete=True)
-        d.addCallback(self.execute_tosca)
+        tosca_execution = d.addCallback(self.execute_tosca)
+        tosca_execution.addErrback(self.errorCallback, request)
         return d
 
     def __init__(self):
diff --git a/test/test_grpc_models_accessor.py b/test/test_grpc_models_accessor.py
index ffafaa3..3851857 100644
--- a/test/test_grpc_models_accessor.py
+++ b/test/test_grpc_models_accessor.py
@@ -120,5 +120,16 @@
         mock_filter.assert_called_with(foo="bar")
         mock_new.assert_called()
 
+    @patch.dict(RESOURCES, mock_resources, clear=True)
+    @patch.object(FakeResource.objects, "new")
+    def test_model_without_properties(self, mock_new):
+        """
+        [GRPCModelsAccessor] get_model_from_classname: should create a new model if not properties are specified
+        """
+        data = {
+        }
+        GRPCModelsAccessor.get_model_from_classname('test-model', data, USERNAME, PASSWORD)
+        mock_new.assert_called()
+
 if __name__ == '__main__':
     unittest.main()
\ No newline at end of file
diff --git a/test/test_tosca_generator.py b/test/test_tosca_generator.py
index 4e3ecda..0011d31 100644
--- a/test/test_tosca_generator.py
+++ b/test/test_tosca_generator.py
@@ -78,7 +78,7 @@
         args.write_to_file = 'target'
         args.quiet = False
         output = XOSGenerator.generate(args)
-        self.assertEqual(output.count("name:"), 2)
+        self.assertEqual(output.count("name:"), 4)
         self.assertIn("prop:", output)
 
 if __name__ == '__main__':
diff --git a/test/test_tosca_parser.py b/test/test_tosca_parser.py
index c8d8947..829fed0 100644
--- a/test/test_tosca_parser.py
+++ b/test/test_tosca_parser.py
@@ -175,8 +175,21 @@
         python error
         starting at line
             38209834 of some file
+UnknownFieldError: with some message
+    followed by useless things
+ImportError: with some message
+    followed by useless things
+InvalidTypeError: with some message
+    followed by useless things
+TypeMismatchError: with some message
+    followed by useless things
         """)
-        self.assertEqual(e, "MissingRequiredFieldError: some message")
+        self.assertEqual(e, """MissingRequiredFieldError: some message
+UnknownFieldError: with some message
+ImportError: with some message
+InvalidTypeError: with some message
+TypeMismatchError: with some message
+""")
 
     def test_save_recipe_to_tmp_file(self):
         """
diff --git a/test/test_tosca_parser_e2e.py b/test/test_tosca_parser_e2e.py
index 5e1f916..a4c8e29 100644
--- a/test/test_tosca_parser_e2e.py
+++ b/test/test_tosca_parser_e2e.py
@@ -220,4 +220,4 @@
         with self.assertRaises(Exception) as e:
             parser.execute()
 
-        self.assertEqual(e.exception.message, "[XOS-TOSCA] Model Site:Open Networking Lab has property 'must-exist' but cannot be found")
+        self.assertEqual(e.exception.message.message, "[XOS-TOSCA] Model Site:Open Networking Lab has property 'must-exist' but cannot be found")