From: preethika.p Date: Wed, 22 Apr 2020 06:55:39 +0000 (+0530) Subject: feature 7919 - Subscription API for OSS/BSS management systems X-Git-Tag: release-v8.0-start~7 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2FNBI.git;a=commitdiff_plain;h=329b81892dc8d2549209fd73505d1f88cf862a72 feature 7919 - Subscription API for OSS/BSS management systems 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 --- diff --git a/Dockerfile.local b/Dockerfile.local index 2f34561..2d76bfd 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -20,10 +20,10 @@ WORKDIR /app/NBI 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 -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 diff --git a/debian/python3-osm-nbi.postinst b/debian/python3-osm-nbi.postinst index c2400c6..54dadb1 100755 --- a/debian/python3-osm-nbi.postinst +++ b/debian/python3-osm-nbi.postinst @@ -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 jsonschema==3.2.0 #Creation of log folder mkdir -p /var/log/osm diff --git a/osm_nbi/engine.py b/osm_nbi/engine.py index 8f70bb6..33d7791 100644 --- a/osm_nbi/engine.py +++ b/osm_nbi/engine.py @@ -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.subscription_topics import NslcmSubscriptionsTopic 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, + "nslcm_subscriptions": NslcmSubscriptionsTopic, # [NEW_TOPIC]: add an entry here # "pm_jobs": PmJobsTopic will be added manually because it needs other parameters } diff --git a/osm_nbi/nbi.py b/osm_nbi/nbi.py index 3172846..5f822f7 100644 --- a/osm_nbi/nbi.py +++ b/osm_nbi/nbi.py @@ -408,6 +408,12 @@ valid_url_methods = { "ROLE_PERMISSION": "vnf_instances:id:" } }, + "subscriptions": {"METHODS": ("GET", "POST"), + "ROLE_PERMISSION": "ns_subscriptions:", + "": {"METHODS": ("GET", "DELETE"), + "ROLE_PERMISSION": "ns_subscriptions:id:" + } + }, } }, "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 - if topic == "subscriptions": - engine_topic = main_topic + "_" + topic + 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 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"): @@ -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 + 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) @@ -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) + 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) diff --git a/osm_nbi/subscription_topics.py b/osm_nbi/subscription_topics.py new file mode 100644 index 0000000..a5c9c44 --- /dev/null +++ b/osm_nbi/subscription_topics.py @@ -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 diff --git a/osm_nbi/validation.py b/osm_nbi/validation.py index d93feb0..beea575 100644 --- a/osm_nbi/validation.py +++ b/osm_nbi/validation.py @@ -832,7 +832,7 @@ user_edit_schema = { # 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", @@ -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): diff --git a/requirements.txt b/requirements.txt index 99a136b..1f2adf9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ # under the License. CherryPy==18.1.2 -jsonschema +jsonschema==3.2.0 PyYAML python-keystoneclient requests diff --git a/setup.py b/setup.py index 67c1740..f134876 100644 --- 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', - 'jsonschema', + 'jsonschema==3.2.0', 'PyYAML', 'osm-im @ git+https://osm.etsi.org/gerrit/osm/IM.git#egg=osm-im', 'python-keystoneclient',