bug 636 validation vim_account-create config. Deny null values
[osm/NBI.git] / osm_nbi / nbi.py
index 0414dba..0417c9e 100644 (file)
@@ -1,6 +1,19 @@
 #!/usr/bin/python3
 # -*- coding: utf-8 -*-
 
+# 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 cherrypy
 import time
 import json
@@ -14,6 +27,8 @@ import sys
 from authconn import AuthException
 from auth import Authenticator
 from engine import Engine, EngineException
+from subscriptions import SubscriptionThread
+from validation import ValidationError
 from osm_common.dbbase import DbException
 from osm_common.fsbase import FsException
 from osm_common.msgbase import MsgException
@@ -23,16 +38,18 @@ from os import environ, path
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 
-# TODO consider to remove and provide version using the static version file
 __version__ = "0.1.3"
-version_date = "Apr 2018"
+version_date = "Jan 2019"
 database_version = '1.0'
 auth_database_version = '1.0'
+nbi_server = None           # instance of Server class
+subscription_thread = None  # instance of SubscriptionThread class
+
 
 """
 North Bound Interface  (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented)
 URL: /osm                                                       GET     POST    PUT     DELETE  PATCH
-        /nsd/v1                                                 O       O
+        /nsd/v1
             /ns_descriptors_content                             O       O
                 /<nsdInfoId>                                    O       O       O       O
             /ns_descriptors                                     O5      O5
@@ -75,9 +92,11 @@ URL: /osm                                                       GET     POST
                 /<vnfInstanceId>                                O
             /subscriptions                                      5       5
                 /<subscriptionId>                               5                       X
+
         /pdu/v1
             /pdu_descriptor                                     O       O
                 /<id>                                           O               O       O       O
+
         /admin/v1
             /tokens                                             O       O
                 /<id>                                           O                       O
@@ -85,13 +104,43 @@ URL: /osm                                                       GET     POST
                 /<id>                                           O               O       O       O
             /projects                                           O       O
                 /<id>                                           O                       O
-            /vims_accounts  (also vims for compatibility)       O       O
+            /vim_accounts  (also vims for compatibility)        O       O
+                /<id>                                           O                       O       O
+            /wim_accounts                                       O       O
                 /<id>                                           O                       O       O
             /sdns                                               O       O
                 /<id>                                           O                       O       O
 
+        /nst/v1                                                 O       O
+            /netslice_templates_content                         O       O
+                /<nstInfoId>                                    O       O       O       O
+            /netslice_templates                                 O       O
+                /<nstInfoId>                                    O                       O       O
+                    /nst_content                                O               O
+                    /nst                                        O
+                    /artifacts[/<artifactPath>]                 O
+            /subscriptions                                      X       X
+                /<subscriptionId>                               X                       X
+
+        /nsilcm/v1
+            /netslice_instances_content                         O       O
+                /<SliceInstanceId>                              O                       O
+            /netslice_instances                                 O       O
+                /<SliceInstanceId>                              O                       O
+                    instantiate                                         O
+                    terminate                                           O
+                    action                                              O
+            /nsi_lcm_op_occs                                    O       O
+                /<nsiLcmOpOccId>                                O                       O       O
+            /subscriptions                                      X       X
+                /<subscriptionId>                               X                       X
+
 query string:
     Follows SOL005 section 4.3.2 It contains extra METHOD to override http method, FORCE to force.
+        simpleFilterExpr := <attrName>["."<attrName>]*["."<op>]"="<value>[","<value>]*
+        filterExpr := <simpleFilterExpr>["&"<simpleFilterExpr>]*
+        op := "eq" | "neq" (or "ne") | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont"
+        attrName := string
     For filtering inside array, it must select the element of the array, or add ANYINDEX to apply the filtering over any
     item of the array, that is, pass if any item of the array pass the filter.
     It allows both ne and neq for not equal
@@ -170,6 +219,9 @@ class Server(object):
                     "vim_accounts": {"METHODS": ("GET", "POST"),
                                      "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
                                      },
+                    "wim_accounts": {"METHODS": ("GET", "POST"),
+                                     "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
+                                     },
                     "sdns": {"METHODS": ("GET", "POST"),
                              "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
                              },
@@ -188,7 +240,7 @@ class Server(object):
                                                "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
                                                },
                     "ns_descriptors": {"METHODS": ("GET", "POST"),
-                                       "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
+                                       "<ID>": {"METHODS": ("GET", "DELETE", "PATCH"),
                                                 "nsd_content": {"METHODS": ("GET", "PUT")},
                                                 "nsd": {"METHODS": "GET"},  # descriptor inside package
                                                 "artifacts": {"*": {"METHODS": "GET"}}
@@ -247,6 +299,40 @@ class Server(object):
                                       },
                 }
             },
+            "nst": {
+                "v1": {
+                    "netslice_templates_content": {"METHODS": ("GET", "POST"),
+                                                   "<ID>": {"METHODS": ("GET", "PUT", "DELETE")}
+                                                   },
+                    "netslice_templates": {"METHODS": ("GET", "POST"),
+                                           "<ID>": {"METHODS": ("GET", "DELETE"), "TODO": "PATCH",
+                                                    "nst_content": {"METHODS": ("GET", "PUT")},
+                                                    "nst": {"METHODS": "GET"},  # descriptor inside package
+                                                    "artifacts": {"*": {"METHODS": "GET"}}
+                                                    }
+                                           },
+                    "subscriptions": {"TODO": ("GET", "POST"),
+                                      "<ID>": {"TODO": ("GET", "DELETE")}
+                                      },
+                }
+            },
+            "nsilcm": {
+                "v1": {
+                    "netslice_instances_content": {"METHODS": ("GET", "POST"),
+                                                   "<ID>": {"METHODS": ("GET", "DELETE")}
+                                                   },
+                    "netslice_instances": {"METHODS": ("GET", "POST"),
+                                           "<ID>": {"METHODS": ("GET", "DELETE"),
+                                                    "terminate": {"METHODS": "POST"},
+                                                    "instantiate": {"METHODS": "POST"},
+                                                    "action": {"METHODS": "POST"},
+                                                    }
+                                           },
+                    "nsi_lcm_op_occs": {"METHODS": "GET",
+                                        "<ID>": {"METHODS": "GET"},
+                                        },
+                }
+            },
         }
 
     def _format_in(self, kwargs):
@@ -366,7 +452,8 @@ class Server(object):
 
             elif "application/yaml" in accept or "*/*" in accept or "text/plain" in accept:
                 pass
-            else:
+            # if there is not any valid accept, raise an error. But if response is already an error, format in yaml
+            elif cherrypy.response.status >= 400:
                 raise cherrypy.HTTPError(HTTPStatus.NOT_ACCEPTABLE.value,
                                          "Only 'Accept' of type 'application/json' or 'application/yaml' "
                                          "for output format are available")
@@ -466,7 +553,7 @@ class Server(object):
     def test(self, *args, **kwargs):
         thread_info = None
         if args and args[0] == "help":
-            return "<html><pre>\ninit\nfile/<name>  download file\ndb-clear/table\nprune\nlogin\nlogin2\n"\
+            return "<html><pre>\ninit\nfile/<name>  download file\ndb-clear/table\nfs-clear[/folder]\nlogin\nlogin2\n"\
                    "sleep/<time>\nmessage/topic\n</pre></html>"
 
         elif args and args[0] == "init":
@@ -487,9 +574,16 @@ class Server(object):
             return f
 
         elif len(args) == 2 and args[0] == "db-clear":
-            return self.engine.db.del_list(args[1], kwargs)
-        elif args and args[0] == "prune":
-            return self.engine.prune()
+            deleted_info = self.engine.db.del_list(args[1], kwargs)
+            return "{} {} deleted\n".format(deleted_info["deleted"], args[1])
+        elif len(args) and args[0] == "fs-clear":
+            if len(args) >= 2:
+                folders = (args[1],)
+            else:
+                folders = self.engine.fs.dir_ls(".")
+            for folder in folders:
+                self.engine.fs.file_delete(folder)
+            return ",".join(folders) + " folders deleted\n"
         elif args and args[0] == "login":
             if not cherrypy.request.headers.get("Authorization"):
                 cherrypy.response.headers["WWW-Authenticate"] = 'Basic realm="Access to OSM site", charset="UTF-8"'
@@ -536,7 +630,7 @@ class Server(object):
             "  session: {}\n".format(cherrypy.session) +
             "  cookie: {}\n".format(cherrypy.request.cookie) +
             "  method: {}\n".format(cherrypy.request.method) +
-            " session: {}\n".format(cherrypy.session.get('fieldname')) +
+            "  session: {}\n".format(cherrypy.session.get('fieldname')) +
             "  body:\n")
         return_text += "    length: {}\n".format(cherrypy.request.body.length)
         if cherrypy.request.body.length:
@@ -601,7 +695,7 @@ class Server(object):
             if not main_topic or not version or not topic:
                 raise NbiException("URL must contain at least 'main_topic/version/topic'",
                                    HTTPStatus.METHOD_NOT_ALLOWED)
-            if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm"):
+            if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm"):
                 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
                                    HTTPStatus.METHOD_NOT_ALLOWED)
             if version != 'v1':
@@ -615,9 +709,7 @@ class Server(object):
                 force = kwargs.pop("FORCE")
             else:
                 force = False
-
             self._check_valid_url_method(method, main_topic, version, topic, _id, item, *args)
-
             if main_topic == "admin" and topic == "tokens":
                 return self.token(method, _id, kwargs)
 
@@ -640,14 +732,20 @@ class Server(object):
                     engine_topic = "nslcmops"
                 if topic == "vnfrs" or topic == "vnf_instances":
                     engine_topic = "vnfrs"
+            elif main_topic == "nst":
+                engine_topic = "nsts"
+            elif main_topic == "nsilcm":
+                engine_topic = "nsis"
+                if topic == "nsi_lcm_op_occs":
+                    engine_topic = "nsilcmops" 
             elif main_topic == "pdu":
                 engine_topic = "pdus"
             if engine_topic == "vims":   # TODO this is for backward compatibility, it will remove in the future
                 engine_topic = "vim_accounts"
 
             if method == "GET":
-                if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd"):
-                    if item in ("vnfd", "nsd"):
+                if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
+                    if item in ("vnfd", "nsd", "nst"):
                         path = "$DESCRIPTOR"
                     elif args:
                         path = args
@@ -663,7 +761,7 @@ class Server(object):
                 else:
                     outdata = self.engine.get_item(session, engine_topic, _id)
             elif method == "POST":
-                if topic in ("ns_descriptors_content", "vnf_packages_content"):
+                if topic in ("ns_descriptors_content", "vnf_packages_content", "netslice_templates_content"):
                     _id = cherrypy.request.headers.get("Transaction-Id")
                     if not _id:
                         _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers,
@@ -691,6 +789,22 @@ class Server(object):
                     self._set_location_header(main_topic, version, "ns_lcm_op_occs", _id)
                     outdata = {"id": _id}
                     cherrypy.response.status = HTTPStatus.ACCEPTED.value
+                elif topic == "netslice_instances_content":
+                    # creates NetSlice_Instance_record (NSIR)
+                    _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
+                    self._set_location_header(main_topic, version, topic, _id)
+                    indata["lcmOperationType"] = "instantiate"
+                    indata["nsiInstanceId"] = _id
+                    self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
+                    outdata = {"id": _id}
+                    
+                elif topic == "netslice_instances" and item:
+                    indata["lcmOperationType"] = item
+                    indata["nsiInstanceId"] = _id
+                    _id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
+                    self._set_location_header(main_topic, version, "nsi_lcm_op_occs", _id)
+                    outdata = {"id": _id}
+                    cherrypy.response.status = HTTPStatus.ACCEPTED.value
                 else:
                     _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs,
                                                cherrypy.request.headers, force=force)
@@ -704,6 +818,7 @@ class Server(object):
                     outdata = self.engine.del_item_list(session, engine_topic, kwargs)
                     cherrypy.response.status = HTTPStatus.OK.value
                 else:  # len(args) > 1
+                    delete_in_process = False
                     if topic == "ns_instances_content" and not force:
                         nslcmop_desc = {
                             "lcmOperationType": "terminate",
@@ -711,12 +826,25 @@ class Server(object):
                             "autoremove": True
                         }
                         opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
-                        outdata = {"_id": opp_id}
-                        cherrypy.response.status = HTTPStatus.ACCEPTED.value
-                    else:
+                        if opp_id:
+                            delete_in_process = True
+                            outdata = {"_id": opp_id}
+                            cherrypy.response.status = HTTPStatus.ACCEPTED.value
+                    elif topic == "netslice_instances_content" and not force:
+                        nsilcmop_desc = {
+                            "lcmOperationType": "terminate",
+                            "nsiInstanceId": _id,
+                            "autoremove": True
+                        }
+                        opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
+                        if opp_id:
+                            delete_in_process = True
+                            outdata = {"_id": opp_id}
+                            cherrypy.response.status = HTTPStatus.ACCEPTED.value
+                    if not delete_in_process:
                         self.engine.del_item(session, engine_topic, _id, force)
                         cherrypy.response.status = HTTPStatus.NO_CONTENT.value
-                if engine_topic in ("vim_accounts", "sdns"):
+                if engine_topic in ("vim_accounts", "wim_accounts", "sdns"):
                     cherrypy.response.status = HTTPStatus.ACCEPTED.value
 
             elif method in ("PUT", "PATCH"):
@@ -724,7 +852,7 @@ class Server(object):
                 if not indata and not kwargs:
                     raise NbiException("Nothing to update. Provide payload and/or query string",
                                        HTTPStatus.BAD_REQUEST)
-                if item in ("nsd_content", "package_content") and method == "PUT":
+                if item in ("nsd_content", "package_content", "nst_content") and method == "PUT":
                     completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
                                                            cherrypy.request.headers, force=force)
                     if not completed:
@@ -736,13 +864,14 @@ class Server(object):
                 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
             return self._format_out(outdata, session, _format)
         except Exception as e:
-            if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException)):
+            if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
+                              ValidationError)):
                 http_code_value = cherrypy.response.status = e.http_code.value
                 http_code_name = e.http_code.name
                 cherrypy.log("Exception {}".format(e))
             else:
                 http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value  # INTERNAL_SERVER_ERROR
-                cherrypy.log("CRITICAL: Exception {}".format(e))
+                cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
                 http_code_name = HTTPStatus.BAD_REQUEST.name
             if hasattr(outdata, "close"):  # is an open file
                 outdata.close()
@@ -750,7 +879,12 @@ class Server(object):
             rollback.reverse()
             for rollback_item in rollback:
                 try:
-                    self.engine.del_item(**rollback_item, session=session, force=True)
+                    if rollback_item.get("operation") == "set":
+                        self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
+                                               rollback_item["content"], fail_on_empty=False)
+                    else:
+                        self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
+                                               fail_on_empty=False)
                 except Exception as e2:
                     rollback_error_text = "Rollback Exception {}: {}".format(rollback_item, e2)
                     cherrypy.log(rollback_error_text)
@@ -767,13 +901,6 @@ class Server(object):
             # raise cherrypy.HTTPError(e.http_code.value, str(e))
 
 
-# def validate_password(realm, username, password):
-#     cherrypy.log("realm "+ str(realm))
-#     if username == "admin" and password == "admin":
-#         return True
-#     return False
-
-
 def _start_service():
     """
     Callback function called when cherrypy.engine starts
@@ -781,6 +908,8 @@ def _start_service():
     Set database, storage, message configuration
     Init database with admin/admin user password
     """
+    global nbi_server
+    global subscription_thread
     cherrypy.log.error("Starting osm_nbi")
     # update general cherrypy configuration
     update_dict = {}
@@ -864,7 +993,19 @@ def _start_service():
     cherrypy.tree.apps['/osm'].root.authenticator.start(engine_config)
     cherrypy.tree.apps['/osm'].root.engine.init_db(target_version=database_version)
     cherrypy.tree.apps['/osm'].root.authenticator.init_db(target_version=auth_database_version)
-    # getenv('OSMOPENMANO_TENANT', None)
+
+    # start subscriptions thread:
+    subscription_thread = SubscriptionThread(config=engine_config, engine=nbi_server.engine)
+    subscription_thread.start()
+    # Do not capture except SubscriptionException
+
+    # load and print version. Ignore possible errors, e.g. file not found
+    try:
+        with open("{}/version".format(engine_config["/static"]['tools.staticdir.dir'])) as version_file:
+            version_data = version_file.read()
+            cherrypy.log.error("Starting OSM NBI Version: {}".format(version_data.replace("\n", " ")))
+    except Exception:
+        pass
 
 
 def _stop_service():
@@ -872,11 +1013,15 @@ def _stop_service():
     Callback function called when cherrypy.engine stops
     TODO: Ending database connections.
     """
+    global subscription_thread
+    subscription_thread.terminate()
+    subscription_thread = None
     cherrypy.tree.apps['/osm'].root.engine.stop()
     cherrypy.log.error("Stopping osm_nbi")
 
 
 def nbi(config_file):
+    global nbi_server
     # conf = {
     #     '/': {
     #         #'request.dispatch': cherrypy.dispatch.MethodDispatcher(),
@@ -894,9 +1039,10 @@ def nbi(config_file):
     # cherrypy.config.update({'tools.auth_basic.on': True,
     #    'tools.auth_basic.realm': 'localhost',
     #    'tools.auth_basic.checkpassword': validate_password})
+    nbi_server = Server()
     cherrypy.engine.subscribe('start', _start_service)
     cherrypy.engine.subscribe('stop', _stop_service)
-    cherrypy.quickstart(Server(), '/osm', config_file)
+    cherrypy.quickstart(nbi_server, '/osm', config_file)
 
 
 def usage():