X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=blobdiff_plain;f=osm_nbi%2Fnbi.py;h=d78379f77103b8b70629183333300361acf35944;hp=bec0cfa04a3e97c6eb308494c2c7eb55c4fec6f5;hb=HEAD;hpb=4568a372eb5a204e04d917213de03ec51f9110c1 diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index bec0cfa..8f87135 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -28,6 +28,7 @@ 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.utils import cef_event, cef_event_builder from osm_nbi.validation import ValidationError from osm_common.dbbase import DbException from osm_common.fsbase import FsException @@ -46,6 +47,7 @@ database_version = "1.2" auth_database_version = "1.0" nbi_server = None # instance of Server class subscription_thread = None # instance of SubscriptionThread class +cef_logger = None """ North Bound Interface (O: OSM specific; 5,X: SOL005 not implemented yet; O5: SOL005 implemented) @@ -85,10 +87,12 @@ URL: /osm GET POST terminate O5 action O scale O5 - heal 5 + migrate O + update 05 + heal O5 /ns_lcm_op_occs 5 5 / 5 5 5 - TO BE COMPLETED 5 5 + cancel 05 /vnf_instances (also vnfrs for compatibility) O / O /subscriptions 5 5 @@ -170,7 +174,7 @@ query string: 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. @@ -431,6 +435,10 @@ valid_url_methods = { "": { "METHODS": ("GET", "DELETE"), "ROLE_PERMISSION": "ns_instances:id:", + "heal": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:id:heal:", + }, "scale": { "METHODS": ("POST",), "ROLE_PERMISSION": "ns_instances:id:scale:", @@ -443,10 +451,22 @@ valid_url_methods = { "METHODS": ("POST",), "ROLE_PERMISSION": "ns_instances:id:instantiate:", }, + "migrate": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:id:migrate:", + }, "action": { "METHODS": ("POST",), "ROLE_PERMISSION": "ns_instances:id:action:", }, + "update": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:id:update:", + }, + "verticalscale": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:id:verticalscale:", + }, }, }, "ns_lcm_op_occs": { @@ -455,6 +475,10 @@ valid_url_methods = { "": { "METHODS": ("GET",), "ROLE_PERMISSION": "ns_instances:opps:id:", + "cancel": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "ns_instances:opps:cancel:", + }, }, }, "vnfrs": { @@ -477,6 +501,46 @@ valid_url_methods = { }, } }, + "vnflcm": { + "v1": { + "vnf_instances": { + "METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "vnflcm_instances:", + "": { + "METHODS": ("GET", "DELETE"), + "ROLE_PERMISSION": "vnflcm_instances:id:", + "scale": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "vnflcm_instances:id:scale:", + }, + "terminate": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "vnflcm_instances:id:terminate:", + }, + "instantiate": { + "METHODS": ("POST",), + "ROLE_PERMISSION": "vnflcm_instances:id:instantiate:", + }, + }, + }, + "vnf_lcm_op_occs": { + "METHODS": ("GET",), + "ROLE_PERMISSION": "vnf_instances:opps:", + "": { + "METHODS": ("GET",), + "ROLE_PERMISSION": "vnf_instances:opps:id:", + }, + }, + "subscriptions": { + "METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "vnflcm_subscriptions:", + "": { + "METHODS": ("GET", "DELETE"), + "ROLE_PERMISSION": "vnflcm_subscriptions:id:", + }, + }, + } + }, "nst": { "v1": { "netslice_templates_content": { @@ -569,6 +633,18 @@ valid_url_methods = { }, }, }, + "nsfm": { + "v1": { + "alarms": { + "METHODS": ("GET", "PATCH"), + "ROLE_PERMISSION": "alarms:", + "": { + "METHODS": ("GET", "PATCH"), + "ROLE_PERMISSION": "alarms:id:", + }, + } + }, + }, } @@ -589,6 +665,7 @@ class Server(object): self.engine = Engine(self.authenticator) def _format_in(self, kwargs): + error_text = "" # error_text must be initialized outside try try: indata = None if cherrypy.request.body.length: @@ -601,9 +678,7 @@ class Server(object): cherrypy.request.headers.pop("Content-File-MD5", None) elif "application/yaml" in cherrypy.request.headers["Content-Type"]: error_text = "Invalid yaml format " - indata = yaml.load( - cherrypy.request.body, Loader=yaml.SafeLoader - ) + indata = yaml.safe_load(cherrypy.request.body) cherrypy.request.headers.pop("Content-File-MD5", None) elif ( "application/binary" in cherrypy.request.headers["Content-Type"] @@ -633,13 +708,11 @@ class Server(object): # "Only 'Content-Type' of type 'application/json' or # 'application/yaml' for input format are available") error_text = "Invalid yaml format " - indata = yaml.load( - cherrypy.request.body, Loader=yaml.SafeLoader - ) + indata = yaml.safe_load(cherrypy.request.body) cherrypy.request.headers.pop("Content-File-MD5", None) else: error_text = "Invalid yaml format " - indata = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader) + indata = yaml.safe_load(cherrypy.request.body) cherrypy.request.headers.pop("Content-File-MD5", None) if not indata: indata = {} @@ -654,7 +727,7 @@ class Server(object): kwargs[k] = None elif format_yaml: try: - kwargs[k] = yaml.load(v, Loader=yaml.SafeLoader) + kwargs[k] = yaml.safe_load(v) except Exception: pass elif ( @@ -678,7 +751,7 @@ class Server(object): v[index] = None elif format_yaml: try: - v[index] = yaml.load(v[index], Loader=yaml.SafeLoader) + v[index] = yaml.safe_load(v[index]) except Exception: pass @@ -823,6 +896,142 @@ class Server(object): if token_info.get("id"): cherrypy.request.login += ";session=" + token_info["id"][0:12] + # NS Fault Management + @cherrypy.expose + def nsfm( + self, + version=None, + topic=None, + uuid=None, + project_name=None, + ns_id=None, + *args, + **kwargs + ): + if topic == "alarms": + try: + method = cherrypy.request.method + role_permission = self._check_valid_url_method( + method, "nsfm", version, topic, None, None, *args + ) + query_string_operations = self._extract_query_string_operations( + kwargs, method + ) + + self.authenticator.authorize( + role_permission, query_string_operations, None + ) + + # to handle get request + if cherrypy.request.method == "GET": + # if request is on basis of uuid + if uuid and uuid != "None": + try: + alarm = self.engine.db.get_one("alarms", {"uuid": uuid}) + alarm_action = self.engine.db.get_one( + "alarms_action", {"uuid": uuid} + ) + alarm.update(alarm_action) + vnf = self.engine.db.get_one( + "vnfrs", {"nsr-id-ref": alarm["tags"]["ns_id"]} + ) + alarm["vnf-id"] = vnf["_id"] + return self._format_out(str(alarm)) + except Exception: + return self._format_out("Please provide valid alarm uuid") + elif ns_id and ns_id != "None": + # if request is on basis of ns_id + try: + alarms = self.engine.db.get_list( + "alarms", {"tags.ns_id": ns_id} + ) + for alarm in alarms: + alarm_action = self.engine.db.get_one( + "alarms_action", {"uuid": alarm["uuid"]} + ) + alarm.update(alarm_action) + return self._format_out(str(alarms)) + except Exception: + return self._format_out("Please provide valid ns id") + else: + # to return only alarm which are related to given project + project = self.engine.db.get_one( + "projects", {"name": project_name} + ) + project_id = project.get("_id") + ns_list = self.engine.db.get_list( + "nsrs", {"_admin.projects_read": project_id} + ) + ns_ids = [] + for ns in ns_list: + ns_ids.append(ns.get("_id")) + alarms = self.engine.db.get_list("alarms") + alarm_list = [ + alarm + for alarm in alarms + if alarm["tags"]["ns_id"] in ns_ids + ] + for alrm in alarm_list: + action = self.engine.db.get_one( + "alarms_action", {"uuid": alrm.get("uuid")} + ) + alrm.update(action) + return self._format_out(str(alarm_list)) + # to handle patch request for alarm update + elif cherrypy.request.method == "PATCH": + data = yaml.safe_load(cherrypy.request.body) + try: + # check if uuid is valid + self.engine.db.get_one("alarms", {"uuid": data.get("uuid")}) + except Exception: + return self._format_out("Please provide valid alarm uuid.") + if data.get("is_enable") is not None: + if data.get("is_enable"): + alarm_status = "ok" + else: + alarm_status = "disabled" + self.engine.db.set_one( + "alarms", + {"uuid": data.get("uuid")}, + {"alarm_status": alarm_status}, + ) + else: + self.engine.db.set_one( + "alarms", + {"uuid": data.get("uuid")}, + {"threshold": data.get("threshold")}, + ) + return self._format_out("Alarm updated") + except Exception as e: + if isinstance( + e, + ( + NbiException, + EngineException, + DbException, + FsException, + MsgException, + AuthException, + ValidationError, + AuthconnException, + ), + ): + 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), traceback=True) + http_code_name = HTTPStatus.BAD_REQUEST.name + problem_details = { + "code": http_code_name, + "status": http_code_value, + "detail": str(e), + } + return self._format_out(problem_details) + @cherrypy.expose def token(self, method, token_id=None, kwargs=None): token_info = None @@ -854,13 +1063,30 @@ class Server(object): outdata = token_info = self.authenticator.new_token( token_info, indata, cherrypy.request.remote ) - cherrypy.session["Authorization"] = outdata["_id"] + cherrypy.session["Authorization"] = outdata["_id"] # pylint: disable=E1101 self._set_location_header("admin", "v1", "tokens", outdata["_id"]) # for logging self._format_login(token_info) - + # password expiry check + if self.authenticator.check_password_expiry(outdata): + outdata = { + "id": outdata["id"], + "message": "change_password", + "user_id": outdata["user_id"], + } # cherrypy.response.cookie["Authorization"] = outdata["id"] # cherrypy.response.cookie["Authorization"]['expires'] = 3600 + cef_event( + cef_logger, + { + "name": "User Login", + "sourceUserName": token_info.get("username"), + "message": "User Logged In, Project={} Outcome=Success".format( + token_info.get("project_name") + ), + }, + ) + cherrypy.log("{}".format(cef_logger)) elif method == "DELETE": if not token_id and "id" in kwargs: token_id = kwargs["id"] @@ -869,9 +1095,27 @@ class Server(object): # for logging self._format_login(token_info) token_id = token_info["_id"] + if current_backend != "keystone": + token_details = self.engine.db.get_one("tokens", {"_id": token_id}) + current_user = token_details.get("username") + current_project = token_details.get("project_name") + else: + current_user = "keystone backend" + current_project = "keystone backend" outdata = self.authenticator.del_token(token_id) token_info = None - cherrypy.session["Authorization"] = "logout" + cherrypy.session["Authorization"] = "logout" # pylint: disable=E1101 + cef_event( + cef_logger, + { + "name": "User Logout", + "sourceUserName": current_user, + "message": "User Logged Out, Project={} Outcome=Success".format( + current_project + ), + }, + ) + cherrypy.log("{}".format(cef_logger)) # cherrypy.response.cookie["Authorization"] = token_id # cherrypy.response.cookie["Authorization"]['expires'] = 0 else: @@ -899,7 +1143,8 @@ class Server(object): elif args and args[0] == "init": try: # self.engine.load_dbase(cherrypy.request.app.config) - self.engine.create_admin() + pid = self.authenticator.create_admin_project() + self.authenticator.create_admin_user(pid) return "Done. User 'admin', password 'admin' created" except Exception: cherrypy.response.status = HTTPStatus.FORBIDDEN.value @@ -957,13 +1202,13 @@ class Server(object): return_text = "
{} ->\n".format(main_topic)
             try:
                 if cherrypy.request.method == "POST":
-                    to_send = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
+                    to_send = yaml.safe_load(cherrypy.request.body)
                     for k, v in to_send.items():
                         self.engine.msg.write(main_topic, k, v)
                         return_text += "  {}: {}\n".format(k, v)
                 elif cherrypy.request.method == "GET":
                     for k, v in kwargs.items():
-                        v_dict = yaml.load(v, Loader=yaml.SafeLoader)
+                        v_dict = yaml.safe_load(v)
                         self.engine.msg.write(main_topic, k, v_dict)
                         return_text += "  {}: {}\n".format(k, v_dict)
             except Exception as e:
@@ -977,10 +1222,12 @@ class Server(object):
             + "  headers: {}\n".format(cherrypy.request.headers)
             + "  path_info: {}\n".format(cherrypy.request.path_info)
             + "  query_string: {}\n".format(cherrypy.request.query_string)
-            + "  session: {}\n".format(cherrypy.session)
+            + "  session: {}\n".format(cherrypy.session)  # pylint: disable=E1101
             + "  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")  # pylint: disable=E1101
+            )
             + "  body:\n"
         )
         return_text += "    length: {}\n".format(cherrypy.request.body.length)
@@ -1172,12 +1419,20 @@ class Server(object):
         **kwargs
     ):
         token_info = None
-        outdata = None
+        outdata = {}
         _format = None
         method = "DONE"
         engine_topic = None
         rollback = []
         engine_session = None
+        url_id = ""
+        log_mapping = {
+            "POST": "Creating",
+            "GET": "Fetching",
+            "DELETE": "Deleting",
+            "PUT": "Updating",
+            "PATCH": "Updating",
+        }
         try:
             if not main_topic or not version or not topic:
                 raise NbiException(
@@ -1193,6 +1448,7 @@ class Server(object):
                 "nst",
                 "nsilcm",
                 "nspm",
+                "vnflcm",
             ):
                 raise NbiException(
                     "URL main_topic '{}' not supported".format(main_topic),
@@ -1203,6 +1459,8 @@ class Server(object):
                     "URL version '{}' not supported".format(version),
                     HTTPStatus.METHOD_NOT_ALLOWED,
                 )
+            if _id is not None:
+                url_id = _id
 
             if (
                 kwargs
@@ -1247,6 +1505,9 @@ class Server(object):
                     engine_topic = "nslcmops"
                 if topic == "vnfrs" or topic == "vnf_instances":
                     engine_topic = "vnfrs"
+            elif main_topic == "vnflcm":
+                if topic == "vnf_lcm_op_occs":
+                    engine_topic = "vnflcmops"
             elif main_topic == "nst":
                 engine_topic = "nsts"
             elif main_topic == "nsilcm":
@@ -1297,8 +1558,11 @@ class Server(object):
                     if item == "reports":
                         # TODO check that project_id (_id in this context) has permissions
                         _id = args[0]
+                    filter_q = None
+                    if "vcaStatusRefresh" in kwargs:
+                        filter_q = {"vcaStatusRefresh": kwargs["vcaStatusRefresh"]}
                     outdata = self.engine.get_item(
-                        engine_session, engine_topic, _id, True
+                        engine_session, engine_topic, _id, filter_q, True
                     )
 
             elif method == "POST":
@@ -1403,6 +1667,24 @@ class Server(object):
                         "_links": link,
                     }
                     cherrypy.response.status = HTTPStatus.CREATED.value
+                elif topic == "vnf_instances" and item:
+                    indata["lcmOperationType"] = item
+                    indata["vnfInstanceId"] = _id
+                    _id, _ = self.engine.new_item(
+                        rollback, engine_session, "vnflcmops", indata, kwargs
+                    )
+                    self._set_location_header(
+                        main_topic, version, "vnf_lcm_op_occs", _id
+                    )
+                    outdata = {"id": _id}
+                    cherrypy.response.status = HTTPStatus.ACCEPTED.value
+                elif topic == "ns_lcm_op_occs" and item == "cancel":
+                    indata["nsLcmOpOccId"] = _id
+                    self.engine.cancel_item(
+                        rollback, engine_session, "nslcmops", indata, None
+                    )
+                    self._set_location_header(main_topic, version, topic, _id)
+                    cherrypy.response.status = HTTPStatus.ACCEPTED.value
                 else:
                     _id, op_id = self.engine.new_item(
                         rollback,
@@ -1514,6 +1796,36 @@ class Server(object):
             ):
                 self.authenticator.remove_token_from_cache()
 
+            if item is not None:
+                cef_event(
+                    cef_logger,
+                    {
+                        "name": "User Operation",
+                        "sourceUserName": token_info.get("username"),
+                        "message": "Performing {} operation on {} {}, Project={} Outcome=Success".format(
+                            item,
+                            topic,
+                            url_id,
+                            token_info.get("project_name"),
+                        ),
+                    },
+                )
+                cherrypy.log("{}".format(cef_logger))
+            else:
+                cef_event(
+                    cef_logger,
+                    {
+                        "name": "User Operation",
+                        "sourceUserName": token_info.get("username"),
+                        "message": "{} {} {}, Project={} Outcome=Success".format(
+                            log_mapping[method],
+                            topic,
+                            url_id,
+                            token_info.get("project_name"),
+                        ),
+                    },
+                )
+                cherrypy.log("{}".format(cef_logger))
             return self._format_out(outdata, token_info, _format)
         except Exception as e:
             if isinstance(
@@ -1555,7 +1867,6 @@ class Server(object):
                         self.engine.db.del_list(
                             rollback_item["topic"],
                             rollback_item["filter"],
-                            fail_on_empty=False,
                         )
                     else:
                         self.engine.db.del_one(
@@ -1577,6 +1888,38 @@ class Server(object):
                 "status": http_code_value,
                 "detail": error_text,
             }
+            if item is not None and token_info is not None:
+                cef_event(
+                    cef_logger,
+                    {
+                        "name": "User Operation",
+                        "sourceUserName": token_info.get("username", None),
+                        "message": "Performing {} operation on {} {}, Project={} Outcome=Failure".format(
+                            item,
+                            topic,
+                            url_id,
+                            token_info.get("project_name", None),
+                        ),
+                        "severity": "2",
+                    },
+                )
+                cherrypy.log("{}".format(cef_logger))
+            elif token_info is not None:
+                cef_event(
+                    cef_logger,
+                    {
+                        "name": "User Operation",
+                        "sourceUserName": token_info.get("username", None),
+                        "message": "{} {} {}, Project={} Outcome=Failure".format(
+                            item,
+                            topic,
+                            url_id,
+                            token_info.get("project_name", None),
+                        ),
+                        "severity": "2",
+                    },
+                )
+                cherrypy.log("{}".format(cef_logger))
             return self._format_out(problem_details, token_info)
             # raise cherrypy.HTTPError(e.http_code.value, str(e))
         finally:
@@ -1599,12 +1942,17 @@ def _start_service():
     """
     global nbi_server
     global subscription_thread
+    global cef_logger
+    global current_backend
     cherrypy.log.error("Starting osm_nbi")
     # update general cherrypy configuration
     update_dict = {}
 
     engine_config = cherrypy.tree.apps["/osm"].config
     for k, v in environ.items():
+        if k == "OSMNBI_USER_MANAGEMENT":
+            feature_state = eval(v.title())
+            engine_config["authentication"]["user_management"] = feature_state
         if not k.startswith("OSMNBI_"):
             continue
         k1, _, k2 = k[7:].lower().partition("_")
@@ -1631,7 +1979,9 @@ def _start_service():
         except ValueError as e:
             cherrypy.log.error("Ignoring environ '{}': " + str(e))
         except Exception as e:
-            cherrypy.log.warn("skipping environ '{}' on exception '{}'".format(k, e))
+            cherrypy.log(
+                "WARNING: skipping environ '{}' on exception '{}'".format(k, e)
+            )
 
     if update_dict:
         cherrypy.config.update(update_dict)
@@ -1698,6 +2048,8 @@ def _start_service():
         target_version=auth_database_version
     )
 
+    cef_logger = cef_event_builder(engine_config["authentication"])
+
     # start subscriptions thread:
     subscription_thread = SubscriptionThread(
         config=engine_config, engine=nbi_server.engine
@@ -1706,6 +2058,7 @@ def _start_service():
     # Do not capture except SubscriptionException
 
     backend = engine_config["authentication"]["backend"]
+    current_backend = backend
     cherrypy.log.error(
         "Starting OSM NBI Version '{} {}' with '{}' authentication backend".format(
             nbi_version, nbi_version_date, backend