feature 7919 - Subscription API for OSS/BSS management systems 11/8811/4
authorpreethika.p <preethika.p@tataelxsi.co.in>
Wed, 22 Apr 2020 06:55:39 +0000 (12:25 +0530)
committerpreethika.p <preethika.p@tataelxsi.co.in>
Fri, 19 Jun 2020 07:44:19 +0000 (13:14 +0530)
Delete_extra used to remove mapped_subscription
Moved code to use db.create_list function
Addressed review comments.

Change-Id: I84bd39e9a9c942d15762d4715843c7c539842767
Signed-off-by: preethika.p <preethika.p@tataelxsi.co.in>
Dockerfile.local
debian/python3-osm-nbi.postinst
osm_nbi/engine.py
osm_nbi/nbi.py
osm_nbi/subscription_topics.py [new file with mode: 0644]
osm_nbi/validation.py
requirements.txt
setup.py

index 2f34561..2d76bfd 100644 (file)
@@ -20,10 +20,10 @@ WORKDIR /app/NBI
 
 RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
 
 
 RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
 
-RUN apt-get update && apt-get install -y git python3 python3-jsonschema \
+RUN apt-get update && apt-get install -y git python3 \
     python3-pymongo python3-yaml python3-pip python3-keystoneclient \
     && python3 -m pip install pip --upgrade \
     python3-pymongo python3-yaml python3-pip python3-keystoneclient \
     && python3 -m pip install pip --upgrade \
-    && python3 -m pip install aiokafka aiohttp cherrypy==18.1.2 keystoneauth1 requests \
+    && python3 -m pip install aiokafka aiohttp cherrypy==18.1.2 keystoneauth1 requests jsonschema==3.2.0 \
     && mkdir -p /app/storage/kafka && mkdir -p /app/log 
 
 # OSM_COMMON
     && mkdir -p /app/storage/kafka && mkdir -p /app/log 
 
 # OSM_COMMON
index c2400c6..54dadb1 100755 (executable)
@@ -23,6 +23,7 @@ echo "Installing python dependencies via pip..."
 python3 -m pip install -U pip
 python3 -m pip install cherrypy==18.1.2
 python3 -m pip install keystoneauth1
 python3 -m pip install -U pip
 python3 -m pip install cherrypy==18.1.2
 python3 -m pip install keystoneauth1
+python3 -m pip install jsonschema==3.2.0
 
 #Creation of log folder
 mkdir -p /var/log/osm
 
 #Creation of log folder
 mkdir -p /var/log/osm
index 8f70bb6..33d7791 100644 (file)
@@ -30,6 +30,7 @@ from osm_nbi.admin_topics import UserTopicAuth, ProjectTopicAuth, RoleTopicAuth
 from osm_nbi.descriptor_topics import VnfdTopic, NsdTopic, PduTopic, NstTopic, VnfPkgOpTopic
 from osm_nbi.instance_topics import NsrTopic, VnfrTopic, NsLcmOpTopic, NsiTopic, NsiLcmOpTopic
 from osm_nbi.pmjobs_topics import PmJobsTopic
 from osm_nbi.descriptor_topics import VnfdTopic, NsdTopic, PduTopic, NstTopic, VnfPkgOpTopic
 from osm_nbi.instance_topics import NsrTopic, VnfrTopic, NsLcmOpTopic, NsiTopic, NsiLcmOpTopic
 from osm_nbi.pmjobs_topics import PmJobsTopic
+from osm_nbi.subscription_topics import NslcmSubscriptionsTopic
 from base64 import b64encode
 from os import urandom   # , path
 from threading import Lock
 from base64 import b64encode
 from os import urandom   # , path
 from threading import Lock
@@ -59,6 +60,7 @@ class Engine(object):
         "nsis": NsiTopic,
         "nsilcmops": NsiLcmOpTopic,
         "vnfpkgops": VnfPkgOpTopic,
         "nsis": NsiTopic,
         "nsilcmops": NsiLcmOpTopic,
         "vnfpkgops": VnfPkgOpTopic,
+        "nslcm_subscriptions": NslcmSubscriptionsTopic,
         # [NEW_TOPIC]: add an entry here
         # "pm_jobs": PmJobsTopic will be added manually because it needs other parameters
     }
         # [NEW_TOPIC]: add an entry here
         # "pm_jobs": PmJobsTopic will be added manually because it needs other parameters
     }
index 3172846..5f822f7 100644 (file)
@@ -408,6 +408,12 @@ valid_url_methods = {
                                        "ROLE_PERMISSION": "vnf_instances:id:"
                                        }
                               },
                                        "ROLE_PERMISSION": "vnf_instances:id:"
                                        }
                               },
+            "subscriptions": {"METHODS": ("GET", "POST"),
+                              "ROLE_PERMISSION": "ns_subscriptions:",
+                              "<ID>": {"METHODS": ("GET", "DELETE"),
+                                       "ROLE_PERMISSION": "ns_subscriptions:id:"
+                                       }
+                              },
         }
     },
     "nst": {
         }
     },
     "nst": {
@@ -1006,8 +1012,7 @@ class Server(object):
             engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
             indata = self._format_in(kwargs)
             engine_topic = topic
             engine_session = self._manage_admin_query(token_info, kwargs, method, _id)
             indata = self._format_in(kwargs)
             engine_topic = topic
-            if topic == "subscriptions":
-                engine_topic = main_topic + "_" + topic
+
             if item and topic != "pm_jobs":
                 engine_topic = item
 
             if item and topic != "pm_jobs":
                 engine_topic = item
 
@@ -1036,6 +1041,9 @@ class Server(object):
             if engine_topic == "vims":   # TODO this is for backward compatibility, it will be removed in the future
                 engine_topic = "vim_accounts"
 
             if engine_topic == "vims":   # TODO this is for backward compatibility, it will be removed in the future
                 engine_topic = "vim_accounts"
 
+            if topic == "subscriptions":
+                engine_topic = main_topic + "_" + topic
+
             if method == "GET":
                 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
                     if item in ("vnfd", "nsd", "nst"):
             if method == "GET":
                 if item in ("nsd_content", "package_content", "artifacts", "vnfd", "nsd", "nst", "nst_content"):
                     if item in ("vnfd", "nsd", "nst"):
@@ -1109,6 +1117,14 @@ class Server(object):
                     self._set_location_header(main_topic, version, "vnfpkg_op_occs", _id)
                     outdata = {"id": _id}
                     cherrypy.response.status = HTTPStatus.ACCEPTED.value
                     self._set_location_header(main_topic, version, "vnfpkg_op_occs", _id)
                     outdata = {"id": _id}
                     cherrypy.response.status = HTTPStatus.ACCEPTED.value
+                elif topic == "subscriptions":
+                    _id, _ = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs)
+                    self._set_location_header(main_topic, version, topic, _id)
+                    link = {}
+                    link["self"] = cherrypy.response.headers["Location"]
+                    outdata = {"id": _id, "filter": indata["filter"], "callbackUri": indata["CallbackUri"],
+                               "_links": link}
+                    cherrypy.response.status = HTTPStatus.CREATED.value
                 else:
                     _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs,
                                                       cherrypy.request.headers)
                 else:
                     _id, op_id = self.engine.new_item(rollback, engine_session, engine_topic, indata, kwargs,
                                                       cherrypy.request.headers)
@@ -1201,6 +1217,9 @@ class Server(object):
                     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)
                     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)
+                    elif rollback_item.get("operation") == "del_list":
+                        self.engine.db.del_list(rollback_item["topic"], rollback_item["filter"], 
+                                                fail_on_empty=False)
                     else:
                         self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
                                                fail_on_empty=False)
                     else:
                         self.engine.db.del_one(rollback_item["topic"], {"_id": rollback_item["_id"]},
                                                fail_on_empty=False)
diff --git a/osm_nbi/subscription_topics.py b/osm_nbi/subscription_topics.py
new file mode 100644 (file)
index 0000000..a5c9c44
--- /dev/null
@@ -0,0 +1,144 @@
+# Copyright 2020 Preethika P(Tata Elxsi)
+#
+# 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.
+
+__author__ = "Preethika P,preethika.p@tataelxsi.co.in"
+
+
+import requests
+from osm_nbi.base_topic import BaseTopic, EngineException
+from osm_nbi.validation import subscription
+
+
+class CommonSubscriptions(BaseTopic):
+    topic = "subscriptions"
+    topic_msg = None
+    
+    def format_on_new(self, content, project_id=None, make_public=False):
+        super().format_on_new(content, project_id=project_id, make_public=make_public)
+        
+        # TODO check how to release Engine.write_lock during the check
+        def _check_endpoint(url, auth):
+            """
+            Checks if the notification endpoint is valid
+            :param url: the notification end
+            :param auth: contains the aunthentication details with type basic
+            """
+            try:
+                if auth is None:
+                    response = requests.get(url, timeout=5)
+                    if response.status_code != 204:
+                        raise EngineException("Cannot access to the notification URL '{}',received {}: {}"
+                                              .format(url, response.status_code, response.content))
+                elif auth["authType"] == "basic":
+                    username = auth["paramsBasic"].get("userName")
+                    password = auth["paramsBasic"].get("password")
+                    response = requests.get(url, auth=(username, password), timeout=5)
+                    if response.status_code != 204:
+                        raise EngineException("Cannot access to the notification URL '{}',received {}: {}"
+                                              .format(url, response.status_code, response.content))
+            except requests.exceptions.RequestException as e:
+                error_text = type(e).__name__ + ": " + str(e)
+                raise EngineException("Cannot access to the notification URL '{}': {}".format(url, error_text))
+        url = content["CallbackUri"]
+        auth = content.get("authentication")
+        _check_endpoint(url, auth)
+        content["schema_version"] = schema_version = "1.1"
+        if auth is not None and auth["authType"] == "basic":
+            if content["authentication"]["paramsBasic"].get("password"):
+                content["authentication"]["paramsBasic"]["password"] = \
+                    self.db.encrypt(content["authentication"]["paramsBasic"]["password"], 
+                                    schema_version=schema_version, salt=content["_id"])
+        return None
+
+    def new(self, rollback, session, indata=None, kwargs=None, headers=None):
+        """
+        Uses BaseTopic.new to create entry into db
+        Once entry is made into subscriptions,mapper function is invoked
+        """
+        _id, op_id = BaseTopic.new(self, rollback, session, indata=indata, kwargs=kwargs, headers=headers)
+        rollback.append({"topic": "mapped_subscriptions", "operation": "del_list", "filter": {"reference": _id}})
+        self._subscription_mapper(_id, indata, table="mapped_subscriptions")
+        return _id, op_id
+
+    def delete_extra(self, session, _id, db_content, not_send_msg=None):
+        """
+        Deletes the mapped_subscription entry for this particular subscriber
+        :param _id: subscription_id deleted
+        """
+        super().delete_extra(session, _id, db_content, not_send_msg)
+        filter_q = {}
+        filter_q["reference"] = _id
+        self.db.del_list("mapped_subscriptions", filter_q)
+
+
+class NslcmSubscriptionsTopic(CommonSubscriptions):
+    schema_new = subscription
+
+    def _subscription_mapper(self, _id, data, table):
+        """
+        Performs data transformation on subscription request
+        :param data: data to be trasformed
+        :param table: table in which transformed data are inserted
+        """
+        formatted_data = []
+        formed_data = {"reference": data.get("_id"),
+                       "CallbackUri": data.get("CallbackUri")}
+        if data.get("authentication"):
+            formed_data.update({"authentication": data.get("authentication")})
+        if data.get("filter"):
+            if data["filter"].get("nsInstanceSubscriptionFilter"):
+                key = list(data["filter"]["nsInstanceSubscriptionFilter"].keys())[0]
+                identifier = data["filter"]["nsInstanceSubscriptionFilter"][key]
+                formed_data.update({"identifier": identifier})
+            if data["filter"].get("notificationTypes"):
+                for elem in data["filter"].get("notificationTypes"):
+                    update_dict = formed_data.copy()
+                    update_dict["notificationType"] = elem
+                    if elem == "NsIdentifierCreationNotification":
+                        update_dict["operationTypes"] = "INSTANTIATE"
+                        update_dict["operationStates"] = "ANY"
+                        formatted_data.append(update_dict)
+                    elif elem == "NsIdentifierDeletionNotification":
+                        update_dict["operationTypes"] = "TERMINATE"
+                        update_dict["operationStates"] = "ANY"
+                        formatted_data.append(update_dict)
+                    elif elem == "NsLcmOperationOccurrenceNotification":
+                        if "operationTypes" in data["filter"].keys():
+                            update_dict["operationTypes"] = data["filter"]["operationTypes"]
+                        else:
+                            update_dict["operationTypes"] = "ANY"
+                        if "operationStates" in data["filter"].keys():
+                            update_dict["operationStates"] = data["filter"]["operationStates"]
+                        else:
+                            update_dict["operationStates"] = "ANY"
+                        formatted_data.append(update_dict)
+                    elif elem == "NsChangeNotification":
+                        if "nsComponentTypes" in data["filter"].keys():
+                            update_dict["nsComponentTypes"] = data["filter"]["nsComponentTypes"]
+                        else:
+                            update_dict["nsComponentTypes"] = "ANY"
+                        if "lcmOpNameImpactingNsComponent" in data["filter"].keys():
+                            update_dict["lcmOpNameImpactingNsComponent"] = \
+                                data["filter"]["lcmOpNameImpactingNsComponent"]
+                        else:
+                            update_dict["lcmOpNameImpactingNsComponent"] = "ANY"
+                        if "lcmOpOccStatusImpactingNsComponent" in data["filter"].keys():
+                            update_dict["lcmOpOccStatusImpactingNsComponent"] = \
+                                data["filter"]["lcmOpOccStatusImpactingNsComponent"]
+                        else:
+                            update_dict["lcmOpOccStatusImpactingNsComponent"] = "ANY"
+                        formatted_data.append(update_dict)
+        self.db.create_list(table, formatted_data)
+        return None
index d93feb0..beea575 100644 (file)
@@ -832,7 +832,7 @@ user_edit_schema = {
 
 # PROJECTS
 topics_with_quota = ["vnfds", "nsds", "slice_templates", "pduds", "ns_instances", "slice_instances", "vim_accounts",
 
 # PROJECTS
 topics_with_quota = ["vnfds", "nsds", "slice_templates", "pduds", "ns_instances", "slice_instances", "vim_accounts",
-                     "wim_accounts", "sdn_controllers", "k8sclusters", "k8srepos", "osmrepos"]
+                     "wim_accounts", "sdn_controllers", "k8sclusters", "k8srepos", "osmrepos", "ns_subscriptions"]
 project_new_schema = {
     "$schema": "http://json-schema.org/draft-04/schema#",
     "title": "New project schema for administrators",
 project_new_schema = {
     "$schema": "http://json-schema.org/draft-04/schema#",
     "title": "New project schema for administrators",
@@ -984,6 +984,129 @@ nsi_terminate = {
 
 }
 
 
 }
 
+nsinstancesubscriptionfilter_schema = {
+    "title": "instance identifier schema",
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "type": "object",
+    "properties": {
+        "nsdIds": {"type": "array"},
+        "vnfdIds": {"type": "array"},
+        "pnfdIds": {"type": "array"},
+        "nsInstanceIds": {"type": "array"},
+        "nsInstanceNames": {"type": "array"},
+    },
+}
+
+nslcmsub_schema = {
+    "title": "nslcmsubscription input schema",
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "type": "object",
+    "properties": {
+        "nsInstanceSubscriptionFilter": nsinstancesubscriptionfilter_schema,
+        "notificationTypes": {
+            "type": "array",
+            "items": {
+                "enum": ['NsLcmOperationOccurrenceNotification', 'NsChangeNotification',
+                         'NsIdentifierCreationNotification', 'NsIdentifierDeletionNotification']
+            }
+        },
+        "operationTypes": {
+            "type": "array",
+            "items": {
+                "enum": ['INSTANTIATE', 'SCALE', 'TERMINATE', 'UPDATE', 'HEAL']
+            }
+        },
+        "operationStates": {
+            "type": "array",
+            "items": {
+                "enum": ['PROCESSING', 'COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED',
+                         'FAILED_TEMP', 'ROLLING_BACK', 'ROLLED_BACK']
+            }
+        },
+        "nsComponentTypes": {
+            "type": "array",
+            "items": {
+                "enum": ['VNF', 'NS', 'PNF']
+            }
+        },
+        "lcmOpNameImpactingNsComponent": {
+            "type": "array",
+            "items": {
+                "enum": ['VNF_INSTANTIATE', 'VNF_SCALE', 'VNF_SCALE_TO_LEVEL', 'VNF_CHANGE_FLAVOUR',
+                         'VNF_TERMINATE', 'VNF_HEAL', 'VNF_OPERATE', 'VNF_CHANGE_EXT_CONN', 'VNF_MODIFY_INFO',
+                         'NS_INSTANTIATE', 'NS_SCALE', 'NS_UPDATE', 'NS_TERMINATE', 'NS_HEAL']
+            }
+        },
+        "lcmOpOccStatusImpactingNsComponent": {
+            "type": "array",
+            "items": {
+                "enum": ['START', 'COMPLETED', 'PARTIALLY_COMPLETED', 'FAILED', 'ROLLED_BACK']
+            }
+        },
+    },
+    "allOf": [
+        {
+            "if": {
+                "properties": {
+                    "notificationTypes": {
+                        "contains": {"const": "NsLcmOperationOccurrenceNotification"}
+                    }
+                },
+            },
+            "then": {
+                "anyOf": [
+                    {"required": ["operationTypes"]},
+                    {"required": ["operationStates"]},
+                ]
+            } 
+        },
+        {
+            "if": {
+                "properties": {
+                    "notificationTypes": {
+                        "contains": {"const": "NsChangeNotification"}
+                    }
+                },
+            },
+            "then": {
+                "anyOf": [
+                    {"required": ["nsComponentTypes"]},
+                    {"required": ["lcmOpNameImpactingNsComponent"]},
+                    {"required": ["lcmOpOccStatusImpactingNsComponent"]},
+                ]
+            }
+        }
+    ]
+}
+
+authentication_schema = {
+    "title": "authentication schema for subscription",
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "type": "object",
+    "properties": {
+        "authType": {"enum": ["basic"]},
+        "paramsBasic": {
+            "type": "object",
+            "properties": {
+                "userName": shortname_schema,
+                "password": passwd_schema,
+            },
+        },
+    },
+}
+
+subscription = {
+    "title": "subscription input schema",
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "type": "object",
+    "properties": {
+        "filter": nslcmsub_schema,
+        "CallbackUri": description_schema,
+        "authentication": authentication_schema
+    },
+    "required": ["CallbackUri"],
+}
+
 
 class ValidationError(Exception):
     def __init__(self, message, http_code=HTTPStatus.UNPROCESSABLE_ENTITY):
 
 class ValidationError(Exception):
     def __init__(self, message, http_code=HTTPStatus.UNPROCESSABLE_ENTITY):
index 99a136b..1f2adf9 100644 (file)
@@ -11,7 +11,7 @@
 # under the License.
 
 CherryPy==18.1.2
 # under the License.
 
 CherryPy==18.1.2
-jsonschema
+jsonschema==3.2.0
 PyYAML
 python-keystoneclient
 requests
 PyYAML
 python-keystoneclient
 requests
index 67c1740..f134876 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,7 @@ setup(
     install_requires=[
         'CherryPy==18.1.2',
         'osm-common @ git+https://osm.etsi.org/gerrit/osm/common.git#egg=osm-common',
     install_requires=[
         'CherryPy==18.1.2',
         'osm-common @ git+https://osm.etsi.org/gerrit/osm/common.git#egg=osm-common',
-        'jsonschema',
+        'jsonschema==3.2.0',
         'PyYAML',
         'osm-im @ git+https://osm.etsi.org/gerrit/osm/IM.git#egg=osm-im',
         'python-keystoneclient',
         'PyYAML',
         'osm-im @ git+https://osm.etsi.org/gerrit/osm/IM.git#egg=osm-im',
         'python-keystoneclient',