Fix bug 744
[osm/NBI.git] / osm_nbi / nbi.py
index 3dd3348..269af33 100644 (file)
@@ -27,6 +27,7 @@ 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
@@ -37,11 +38,13 @@ 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"
-database_version = '1.0'
+version_date = "Jan 2019"
+database_version = '1.1'
 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)
@@ -91,7 +94,7 @@ URL: /osm                                                       GET     POST
                 /<subscriptionId>                               5                       X
 
         /pdu/v1
-            /pdu_descriptor                                     O       O
+            /pdu_descriptors                                    O       O
                 /<id>                                           O               O       O       O
 
         /admin/v1
@@ -155,6 +158,12 @@ query string:
         exclude_default and include=<list>     … all attributes except those complex attributes with a minimum cardinality
         of zero that are not conditionally mandatory and that are part of the "default exclude set" defined in the
         present specification for the particular resource, but that are not part of <list>
+    Additionally it admits some administrator values:
+        FORCE: To force operations skipping dependency checkings
+        ADMIN: To act as an administrator or a different project
+        PUBLIC: To get public descriptors or set a descriptor as public
+        SET_PROJECT: To make a descriptor available for other project
+        
 Header field name      Reference       Example Descriptions
     Accept     IETF RFC 7231 [19]      application/json        Content-Types that are acceptable for the response.
     This header field shall be present if the response is expected to have a non-empty message body.
@@ -208,8 +217,11 @@ class Server(object):
                               "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PATCH", "PUT")}
                               },
                     "projects": {"METHODS": ("GET", "POST"),
-                                 "<ID>": {"METHODS": ("GET", "DELETE")}
+                                 "<ID>": {"METHODS": ("GET", "DELETE", "PUT")}
                                  },
+                    "roles": {"METHODS": ("GET", "POST"),
+                              "<ID>": {"METHODS": ("GET", "POST", "DELETE", "PUT")}
+                              },
                     "vims": {"METHODS": ("GET", "POST"),
                              "<ID>": {"METHODS": ("GET", "DELETE", "PATCH", "PUT")}
                              },
@@ -330,6 +342,17 @@ class Server(object):
                                         },
                 }
             },
+            "nspm": {
+                "v1": {
+                    "pm_jobs": {
+                        "<ID>": {
+                            "reports": {
+                                "<ID>": {"METHODS": ("GET")}
+                            }
+                        },
+                    },
+                },
+            },
         }
 
     def _format_in(self, kwargs):
@@ -679,6 +702,78 @@ class Server(object):
         cherrypy.response.headers["Location"] = "/osm/{}/{}/{}/{}".format(main_topic, version, topic, id)
         return
 
+    @staticmethod
+    def _manage_admin_query(session, kwargs, method, _id):
+        """
+        Processes the administrator query inputs (if any) of FORCE, ADMIN, PUBLIC, SET_PROJECT
+        Check that users has rights to use them and returs the admin_query
+        :param session: session rights obtained by token
+        :param kwargs: query string input.
+        :param method: http method: GET, POSST, PUT, ...
+        :param _id:
+        :return: admin_query dictionary with keys:
+            public: True, False or None
+            force: True or False
+            project_id: tuple with projects used for accessing an element
+            set_project: tuple with projects that a created element will belong to
+            method: show, list, delete, write
+        """
+        admin_query = {"force": False, "project_id": (session["project_id"], ), "username": session["username"],
+                       "admin": session["admin"], "public": None}
+        if kwargs:
+            # FORCE
+            if "FORCE" in kwargs:
+                if kwargs["FORCE"].lower() != "false":  # if None or True set force to True
+                    admin_query["force"] = True
+                del kwargs["FORCE"]
+            # PUBLIC
+            if "PUBLIC" in kwargs:
+                if kwargs["PUBLIC"].lower() != "false":  # if None or True set public to True
+                    admin_query["public"] = True
+                else:
+                    admin_query["public"] = False
+                del kwargs["PUBLIC"]
+            # ADMIN
+            if "ADMIN" in kwargs:
+                behave_as = kwargs.pop("ADMIN")
+                if behave_as.lower() != "false":
+                    if not session["admin"]:
+                        raise NbiException("Only admin projects can use 'ADMIN' query string", HTTPStatus.UNAUTHORIZED)
+                    if not behave_as or behave_as.lower() == "true":  # convert True, None to empty list
+                        admin_query["project_id"] = ()
+                    elif isinstance(behave_as, (list, tuple)):
+                        admin_query["project_id"] = behave_as
+                    else:   # isinstance(behave_as, str)
+                        admin_query["project_id"] = (behave_as, )
+            if "SET_PROJECT" in kwargs:
+                set_project = kwargs.pop("SET_PROJECT")
+                if not set_project:
+                    admin_query["set_project"] = list(admin_query["project_id"])
+                else:
+                    if isinstance(set_project, str):
+                        set_project = (set_project, )
+                    if admin_query["project_id"]:
+                        for p in set_project:
+                            if p not in admin_query["project_id"]:
+                                raise NbiException("Unauthorized for 'SET_PROJECT={p}'. Try with 'ADMIN=True' or "
+                                                   "'ADMIN='{p}'".format(p=p), HTTPStatus.UNAUTHORIZED)
+                    admin_query["set_project"] = set_project
+
+            # PROJECT_READ
+            # if "PROJECT_READ" in kwargs:
+            #     admin_query["project"] = kwargs.pop("project")
+            #     if admin_query["project"] == session["project_id"]:
+        if method == "GET":
+            if _id:
+                admin_query["method"] = "show"
+            else:
+                admin_query["method"] = "list"
+        elif method == "DELETE":
+            admin_query["method"] = "delete"
+        else:
+            admin_query["method"] = "write"
+        return admin_query
+
     @cherrypy.expose
     def default(self, main_topic=None, version=None, topic=None, _id=None, item=None, *args, **kwargs):
         session = None
@@ -692,7 +787,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", "pdu", "nst", "nsilcm"):
+            if main_topic not in ("admin", "vnfpkgm", "nsd", "nslcm", "pdu", "nst", "nsilcm", "nspm"):
                 raise NbiException("URL main_topic '{}' not supported".format(main_topic),
                                    HTTPStatus.METHOD_NOT_ALLOWED)
             if version != 'v1':
@@ -702,21 +797,20 @@ class Server(object):
                 method = kwargs.pop("METHOD")
             else:
                 method = cherrypy.request.method
-            if kwargs and "FORCE" in kwargs:
-                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)
 
             # self.engine.load_dbase(cherrypy.request.app.config)
             session = self.authenticator.authorize()
+            session = self._manage_admin_query(session, kwargs, method, _id)
             indata = self._format_in(kwargs)
             engine_topic = topic
             if topic == "subscriptions":
                 engine_topic = main_topic + "_" + topic
-            if item:
+            if item and topic != "pm_jobs":
                 engine_topic = item
 
             if main_topic == "nsd":
@@ -734,10 +828,10 @@ class Server(object):
             elif main_topic == "nsilcm":
                 engine_topic = "nsis"
                 if topic == "nsi_lcm_op_occs":
-                    engine_topic = "nsilcmops" 
+                    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
+            if engine_topic == "vims":   # TODO this is for backward compatibility, it will be removed in the future
                 engine_topic = "vim_accounts"
 
             if method == "GET":
@@ -756,15 +850,17 @@ class Server(object):
                 elif not _id:
                     outdata = self.engine.get_item_list(session, engine_topic, kwargs)
                 else:
+                    if item == "reports":
+                        # TODO check that project_id (_id in this context) has permissions
+                        _id = args[0]
                     outdata = self.engine.get_item(session, engine_topic, _id)
             elif method == "POST":
                 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,
-                                                   force=force)
+                        _id = self.engine.new_item(rollback, session, engine_topic, {}, None, cherrypy.request.headers)
                     completed = self.engine.upload_content(session, engine_topic, _id, indata, kwargs,
-                                                           cherrypy.request.headers, force=force)
+                                                           cherrypy.request.headers)
                     if completed:
                         self._set_location_header(main_topic, version, topic, _id)
                     else:
@@ -772,13 +868,13 @@ class Server(object):
                     outdata = {"id": _id}
                 elif topic == "ns_instances_content":
                     # creates NSR
-                    _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs, force=force)
+                    _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs)
                     # creates nslcmop
                     indata["lcmOperationType"] = "instantiate"
                     indata["nsInstanceId"] = _id
-                    self.engine.new_item(rollback, session, "nslcmops", indata, None)
+                    nslcmop_id = self.engine.new_item(rollback, session, "nslcmops", indata, None)
                     self._set_location_header(main_topic, version, topic, _id)
-                    outdata = {"id": _id}
+                    outdata = {"id": _id, "nslcmop_id": nslcmop_id}
                 elif topic == "ns_instances" and item:
                     indata["lcmOperationType"] = item
                     indata["nsInstanceId"] = _id
@@ -788,23 +884,23 @@ class Server(object):
                     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)
+                    _id = self.engine.new_item(rollback, session, engine_topic, indata, kwargs)
                     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}
-                    
+                    indata["netsliceInstanceId"] = _id
+                    nsilcmop_id = self.engine.new_item(rollback, session, "nsilcmops", indata, kwargs)
+                    outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
+
                 elif topic == "netslice_instances" and item:
                     indata["lcmOperationType"] = item
-                    indata["nsiInstanceId"] = _id
+                    indata["netsliceInstanceId"] = _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)
+                                               cherrypy.request.headers)
                     self._set_location_header(main_topic, version, topic, _id)
                     outdata = {"id": _id}
                     # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
@@ -815,42 +911,47 @@ class Server(object):
                     outdata = self.engine.del_item_list(session, engine_topic, kwargs)
                     cherrypy.response.status = HTTPStatus.OK.value
                 else:  # len(args) > 1
-                    if topic == "ns_instances_content" and not force:
+                    delete_in_process = False
+                    if topic == "ns_instances_content" and not session["force"]:
                         nslcmop_desc = {
                             "lcmOperationType": "terminate",
                             "nsInstanceId": _id,
                             "autoremove": True
                         }
                         opp_id = self.engine.new_item(rollback, session, "nslcmops", nslcmop_desc, None)
-                        outdata = {"_id": opp_id}
-                        cherrypy.response.status = HTTPStatus.ACCEPTED.value
-                    elif topic == "netslice_instances_content" and not force:
+                        if opp_id:
+                            delete_in_process = True
+                            outdata = {"_id": opp_id}
+                            cherrypy.response.status = HTTPStatus.ACCEPTED.value
+                    elif topic == "netslice_instances_content" and not session["force"]:
                         nsilcmop_desc = {
                             "lcmOperationType": "terminate",
-                            "nsiInstanceId": _id,
+                            "netsliceInstanceId": _id,
                             "autoremove": True
                         }
                         opp_id = self.engine.new_item(rollback, session, "nsilcmops", nsilcmop_desc, None)
-                        outdata = {"_id": opp_id}
-                        cherrypy.response.status = HTTPStatus.ACCEPTED.value
-                    else:
-                        self.engine.del_item(session, engine_topic, _id, force)
+                        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)
                         cherrypy.response.status = HTTPStatus.NO_CONTENT.value
                 if engine_topic in ("vim_accounts", "wim_accounts", "sdns"):
                     cherrypy.response.status = HTTPStatus.ACCEPTED.value
 
             elif method in ("PUT", "PATCH"):
                 outdata = None
-                if not indata and not kwargs:
+                if not indata and not kwargs and not session.get("set_project"):
                     raise NbiException("Nothing to update. Provide payload and/or query string",
                                        HTTPStatus.BAD_REQUEST)
                 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)
+                                                           cherrypy.request.headers)
                     if not completed:
                         cherrypy.response.headers["Transaction-Id"] = id
                 else:
-                    self.engine.edit_item(session, engine_topic, _id, indata, kwargs, force=force)
+                    self.engine.edit_item(session, engine_topic, _id, indata, kwargs)
                 cherrypy.response.status = HTTPStatus.NO_CONTENT.value
             else:
                 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
@@ -875,7 +976,8 @@ class Server(object):
                         self.engine.db.set_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
                                                rollback_item["content"], fail_on_empty=False)
                     else:
-                        self.engine.del_item(**rollback_item, session=session, force=True)
+                        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)
@@ -892,13 +994,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
@@ -906,6 +1001,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 = {}
@@ -989,7 +1086,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():
@@ -997,11 +1106,16 @@ def _stop_service():
     Callback function called when cherrypy.engine stops
     TODO: Ending database connections.
     """
+    global subscription_thread
+    if 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(),
@@ -1019,9 +1133,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():