change version to be aligned with OSM version
[osm/NBI.git] / osm_nbi / nbi.py
index 9188834..1085ebc 100644 (file)
@@ -18,28 +18,30 @@ import cherrypy
 import time
 import json
 import yaml
-import html_out as html
+import osm_nbi.html_out as html
 import logging
 import logging.handlers
 import getopt
 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_nbi.authconn import AuthException, AuthconnException
+from osm_nbi.auth import Authenticator
+from osm_nbi.engine import Engine, EngineException
+from osm_nbi.subscriptions import SubscriptionThread
+from osm_nbi.validation import ValidationError
 from osm_common.dbbase import DbException
 from osm_common.fsbase import FsException
 from osm_common.msgbase import MsgException
 from http import HTTPStatus
 from codecs import getreader
 from os import environ, path
+from osm_nbi import version as nbi_version, version_date as nbi_version_date
 
 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
 
-__version__ = "0.1.3"
-version_date = "Jan 2019"
+__version__ = "0.1.3"    # file version, not NBI version
+version_date = "Aug 2019"
+
 database_version = '1.2'
 auth_database_version = '1.0'
 nbi_server = None           # instance of Server class
@@ -598,7 +600,7 @@ class Server(object):
         try:
             if cherrypy.request.method == "GET":
                 token_info = self.authenticator.authorize()
-                outdata = "Index page"
+                outdata = token_info   # Home page
             else:
                 raise cherrypy.HTTPError(HTTPStatus.METHOD_NOT_ALLOWED.value,
                                          "Method {} not allowed for tokens".format(cherrypy.request.method))
@@ -613,13 +615,13 @@ class Server(object):
     @cherrypy.expose
     def version(self, *args, **kwargs):
         # TODO consider to remove and provide version using the static version file
-        global __version__, version_date
         try:
             if cherrypy.request.method != "GET":
                 raise NbiException("Only method GET is allowed", HTTPStatus.METHOD_NOT_ALLOWED)
             elif args or kwargs:
                 raise NbiException("Invalid URL or query string for version", HTTPStatus.METHOD_NOT_ALLOWED)
-            return __version__ + " " + version_date
+            # TODO include version of other modules, pick up from some kafka admin message
+            return "<pre>NBI:\n    version: {}\n    date: {}\n".format(nbi_version, nbi_version_date)
         except NbiException as e:
             cherrypy.response.status = e.http_code.value
             problem_details = {
@@ -629,6 +631,20 @@ class Server(object):
             }
             return self._format_out(problem_details, None)
 
+    @staticmethod
+    def _format_login(token_info):
+        """
+        Changes cherrypy.request.login to include username/project_name;session so that cherrypy access log will
+        log this information
+        :param token_info: Dictionary with token content
+        :return: None
+        """
+        cherrypy.request.login = token_info.get("username", "-")
+        if token_info.get("project_name"):
+            cherrypy.request.login += "/" + token_info["project_name"]
+        if token_info.get("id"):
+            cherrypy.request.login += ";session=" + token_info["id"][0:12]
+
     @cherrypy.expose
     def token(self, method, token_id=None, kwargs=None):
         token_info = None
@@ -636,49 +652,48 @@ class Server(object):
         indata = self._format_in(kwargs)
         if not isinstance(indata, dict):
             raise NbiException("Expected application/yaml or application/json Content-Type", HTTPStatus.BAD_REQUEST)
-        try:
-            if method == "GET":
+
+        if method == "GET":
+            token_info = self.authenticator.authorize()
+            # for logging
+            self._format_login(token_info)
+            if token_id:
+                outdata = self.authenticator.get_token(token_info, token_id)
+            else:
+                outdata = self.authenticator.get_token_list(token_info)
+        elif method == "POST":
+            try:
                 token_info = self.authenticator.authorize()
-                if token_id:
-                    outdata = self.authenticator.get_token(token_info, token_id)
-                else:
-                    outdata = self.authenticator.get_token_list(token_info)
-            elif method == "POST":
-                try:
-                    token_info = self.authenticator.authorize()
-                except Exception:
-                    token_info = None
-                if kwargs:
-                    indata.update(kwargs)
-                outdata = self.authenticator.new_token(token_info, indata, cherrypy.request.remote)
-                token_info = outdata
-                cherrypy.session['Authorization'] = outdata["_id"]
-                self._set_location_header("admin", "v1", "tokens", outdata["_id"])
-                # cherrypy.response.cookie["Authorization"] = outdata["id"]
-                # cherrypy.response.cookie["Authorization"]['expires'] = 3600
-            elif method == "DELETE":
-                if not token_id and "id" in kwargs:
-                    token_id = kwargs["id"]
-                elif not token_id:
-                    token_info = self.authenticator.authorize()
-                    token_id = token_info["_id"]
-                outdata = self.authenticator.del_token(token_id)
+            except Exception:
                 token_info = None
-                cherrypy.session['Authorization'] = "logout"
-                # cherrypy.response.cookie["Authorization"] = token_id
-                # cherrypy.response.cookie["Authorization"]['expires'] = 0
-            else:
-                raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
-            return self._format_out(outdata, token_info)
-        except (NbiException, EngineException, DbException, AuthException) as e:
-            cherrypy.log("tokens Exception {}".format(e))
-            cherrypy.response.status = e.http_code.value
-            problem_details = {
-                "code": e.http_code.name,
-                "status": e.http_code.value,
-                "detail": str(e),
-            }
-            return self._format_out(problem_details, token_info)
+            if kwargs:
+                indata.update(kwargs)
+            # This is needed to log the user when authentication fails
+            cherrypy.request.login = "{}".format(indata.get("username", "-"))
+            outdata = token_info = self.authenticator.new_token(token_info, indata, cherrypy.request.remote)
+            cherrypy.session['Authorization'] = outdata["_id"]
+            self._set_location_header("admin", "v1", "tokens", outdata["_id"])
+            # for logging
+            self._format_login(token_info)
+
+            # cherrypy.response.cookie["Authorization"] = outdata["id"]
+            # cherrypy.response.cookie["Authorization"]['expires'] = 3600
+        elif method == "DELETE":
+            if not token_id and "id" in kwargs:
+                token_id = kwargs["id"]
+            elif not token_id:
+                token_info = self.authenticator.authorize()
+                # for logging
+                self._format_login(token_info)
+                token_id = token_info["_id"]
+            outdata = self.authenticator.del_token(token_id)
+            token_info = None
+            cherrypy.session['Authorization'] = "logout"
+            # cherrypy.response.cookie["Authorization"] = token_id
+            # cherrypy.response.cookie["Authorization"]['expires'] = 0
+        else:
+            raise NbiException("Method {} not allowed for token".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
+        return self._format_out(outdata, token_info)
 
     @cherrypy.expose
     def test(self, *args, **kwargs):
@@ -929,8 +944,6 @@ class Server(object):
             if main_topic == "admin" and topic == "tokens":
                 return self.token(method, _id, kwargs)
 
-            # self.engine.load_dbase(cherrypy.request.app.config)
-
             token_info = self.authenticator.authorize(role_permission, query_string_operations)
             engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
             indata = self._format_in(kwargs)
@@ -982,11 +995,12 @@ class Server(object):
                         _id = args[0]
                     outdata = self.engine.get_item(engine_session, engine_topic, _id)
             elif method == "POST":
+                cherrypy.response.status = HTTPStatus.CREATED.value
                 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, engine_session, engine_topic, {}, None,
-                                                   cherrypy.request.headers)
+                        _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, {}, None,
+                                                      cherrypy.request.headers)
                     completed = self.engine.upload_content(engine_session, engine_topic, _id, indata, kwargs,
                                                            cherrypy.request.headers)
                     if completed:
@@ -996,43 +1010,45 @@ class Server(object):
                     outdata = {"id": _id}
                 elif topic == "ns_instances_content":
                     # creates NSR
-                    _id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
+                    _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
                     # creates nslcmop
                     indata["lcmOperationType"] = "instantiate"
                     indata["nsInstanceId"] = _id
-                    nslcmop_id = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None)
+                    nslcmop_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, None)
                     self._set_location_header(main_topic, version, topic, _id)
                     outdata = {"id": _id, "nslcmop_id": nslcmop_id}
                 elif topic == "ns_instances" and item:
                     indata["lcmOperationType"] = item
                     indata["nsInstanceId"] = _id
-                    _id = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs)
+                    _id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", indata, kwargs)
                     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, engine_session, engine_topic, indata, kwargs)
+                    _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
                     self._set_location_header(main_topic, version, topic, _id)
                     indata["lcmOperationType"] = "instantiate"
                     indata["netsliceInstanceId"] = _id
-                    nsilcmop_id = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
+                    nsilcmop_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
                     outdata = {"id": _id, "nsilcmop_id": nsilcmop_id}
 
                 elif topic == "netslice_instances" and item:
                     indata["lcmOperationType"] = item
                     indata["netsliceInstanceId"] = _id
-                    _id = self.engine.new_item(rollback, engine_session, "nsilcmops", indata, kwargs)
+                    _id, _ = self.engine.new_item(rollback, engine_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, engine_session, engine_topic, indata, kwargs,
-                                               cherrypy.request.headers)
+                    _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs,
+                                                      cherrypy.request.headers)
                     self._set_location_header(main_topic, version, topic, _id)
                     outdata = {"id": _id}
+                    if op_id:
+                        outdata["op_id"] = op_id
+                        cherrypy.response.status = HTTPStatus.ACCEPTED.value
                     # TODO form NsdInfo when topic in ("ns_descriptors", "vnf_packages")
-                cherrypy.response.status = HTTPStatus.CREATED.value
 
             elif method == "DELETE":
                 if not _id:
@@ -1046,7 +1062,7 @@ class Server(object):
                             "nsInstanceId": _id,
                             "autoremove": True
                         }
-                        opp_id = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, None)
+                        opp_id, _ = self.engine.new_item(rollback, engine_session, "nslcmops", nslcmop_desc, None)
                         if opp_id:
                             delete_in_process = True
                             outdata = {"_id": opp_id}
@@ -1057,7 +1073,7 @@ class Server(object):
                             "netsliceInstanceId": _id,
                             "autoremove": True
                         }
-                        opp_id = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None)
+                        opp_id, _ = self.engine.new_item(rollback, engine_session, "nsilcmops", nsilcmop_desc, None)
                         if opp_id:
                             delete_in_process = True
                             outdata = {"_id": opp_id}
@@ -1069,7 +1085,7 @@ class Server(object):
                     cherrypy.response.status = HTTPStatus.ACCEPTED.value
 
             elif method in ("PUT", "PATCH"):
-                outdata = None
+                op_id = None
                 if not indata and not kwargs and not engine_session.get("set_project"):
                     raise NbiException("Nothing to update. Provide payload and/or query string",
                                        HTTPStatus.BAD_REQUEST)
@@ -1079,8 +1095,14 @@ class Server(object):
                     if not completed:
                         cherrypy.response.headers["Transaction-Id"] = id
                 else:
-                    self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs)
-                cherrypy.response.status = HTTPStatus.NO_CONTENT.value
+                    op_id = self.engine.edit_item(engine_session, engine_topic, _id, indata, kwargs)
+
+                if op_id:
+                    cherrypy.response.status = HTTPStatus.ACCEPTED.value
+                    outdata = {"op_id": op_id}
+                else:
+                    cherrypy.response.status = HTTPStatus.NO_CONTENT.value
+                    outdata = None
             else:
                 raise NbiException("Method {} not allowed".format(method), HTTPStatus.METHOD_NOT_ALLOWED)
 
@@ -1090,7 +1112,7 @@ class Server(object):
             return self._format_out(outdata, token_info, _format)
         except Exception as e:
             if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
-                              ValidationError)):
+                              ValidationError, AuthconnException)):
                 http_code_value = cherrypy.response.status = e.http_code.value
                 http_code_name = e.http_code.name
                 cherrypy.log("Exception {}".format(e))
@@ -1124,6 +1146,13 @@ class Server(object):
             }
             return self._format_out(problem_details, token_info)
             # raise cherrypy.HTTPError(e.http_code.value, str(e))
+        finally:
+            if token_info:
+                self._format_login(token_info)
+                if method in ("PUT", "PATCH", "POST") and isinstance(outdata, dict):
+                    for logging_id in ("id", "op_id", "nsilcmop_id", "nslcmop_id"):
+                        if outdata.get(logging_id):
+                            cherrypy.request.login += ";{}={}".format(logging_id, outdata[logging_id][:36])
 
 
 def _start_service():
@@ -1226,9 +1255,10 @@ def _start_service():
 
     # 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", " ")))
+        backend = engine_config["authentication"]["backend"]
+        nbi_version
+        cherrypy.log.error("Starting OSM NBI Version '{}' with '{}' authentication backend"
+                           .format(nbi_version + " " + nbi_version_date, backend))
     except Exception:
         pass