From: SagarS Date: Wed, 4 Aug 2021 11:59:55 +0000 (+0100) Subject: [PMSH] Create subscription API changes with IPv4, IPv6 updates X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F99%2F123099%2F4;p=dcaegen2%2Fservices.git [PMSH] Create subscription API changes with IPv4, IPv6 updates Issue-ID: DCAEGEN2-2819 Signed-off-by: SagarS Change-Id: Id0db2ae3f57786e0eeea70589644bf1f7fa92de8 --- diff --git a/components/pm-subscription-handler/Changelog.md b/components/pm-subscription-handler/Changelog.md index 58d40cc9..421aed30 100755 --- a/components/pm-subscription-handler/Changelog.md +++ b/components/pm-subscription-handler/Changelog.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.4.0] +### Changed +* Enhanced API for PMSH subscription management (DCAEGEN2-2802) ## [1.3.2] ### Changed diff --git a/components/pm-subscription-handler/pmsh_service/mod/__init__.py b/components/pm-subscription-handler/pmsh_service/mod/__init__.py index 5f78ca19..f7455fc7 100644 --- a/components/pm-subscription-handler/pmsh_service/mod/__init__.py +++ b/components/pm-subscription-handler/pmsh_service/mod/__init__.py @@ -1,5 +1,5 @@ # ============LICENSE_START=================================================== -# Copyright (C) 2019-2021 Nordix Foundation. +# Copyright (C) 2020-2021 Nordix Foundation. # ============================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ def _get_app(): def launch_api_server(app_config): connex_app = _get_app() + connex_app.app.config['app_config'] = app_config connex_app.add_api('api/pmsh_swagger.yml') if app_config.enable_tls: logger.info('Launching secure http API server') diff --git a/components/pm-subscription-handler/pmsh_service/mod/api/controllers/subscription_controller.py b/components/pm-subscription-handler/pmsh_service/mod/api/controllers/subscription_controller.py new file mode 100644 index 00000000..c824f622 --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/controllers/subscription_controller.py @@ -0,0 +1,46 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2020-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== + +from http import HTTPStatus +from mod.api.services import subscription_service +from connexion import NoContent +from mod.api.custom_exception import InvalidDataException, DuplicateDataException + + +def post_subscription(body): + """ + Creates a subscription + + Args: + body (dict): subscription request body to save. + + Returns: + Success : NoContent, 201 + Invalid Data : List of Invalid messages, 400 + + Raises: + Error: If anything fails in the server. + """ + response = NoContent, HTTPStatus.CREATED.value + try: + subscription_service.create_subscription(body['subscription']) + except DuplicateDataException as exception: + response = exception.duplicate_fields_info, HTTPStatus.CONFLICT.value + except InvalidDataException as exception: + response = exception.invalid_messages, HTTPStatus.BAD_REQUEST.value + return response diff --git a/components/pm-subscription-handler/pmsh_service/mod/api/custom_exception.py b/components/pm-subscription-handler/pmsh_service/mod/api/custom_exception.py new file mode 100644 index 00000000..f893c945 --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/custom_exception.py @@ -0,0 +1,38 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2020-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== + +class InvalidDataException(Exception): + """Exception raised for invalid inputs. + + Attributes: + message -- detail on invalid fields + """ + + def __init__(self, invalid_messages): + self.invalid_messages = invalid_messages + + +class DuplicateDataException(Exception): + """Exception raised for invalid inputs. + + Attributes: + message -- detail on duplicate fields + """ + + def __init__(self, duplicate_fields_info): + self.duplicate_fields_info = duplicate_fields_info diff --git a/components/pm-subscription-handler/pmsh_service/mod/api/db_models.py b/components/pm-subscription-handler/pmsh_service/mod/api/db_models.py index a9dd6efe..ce4081b0 100755 --- a/components/pm-subscription-handler/pmsh_service/mod/api/db_models.py +++ b/components/pm-subscription-handler/pmsh_service/mod/api/db_models.py @@ -1,5 +1,5 @@ # ============LICENSE_START=================================================== -# Copyright (C) 2019-2020 Nordix Foundation. +# Copyright (C) 2020-2021 Nordix Foundation. # ============================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ # SPDX-License-Identifier: Apache-2.0 # ============LICENSE_END===================================================== -from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy import Column, Integer, String, ForeignKey, JSON from sqlalchemy.orm import relationship from mod import db @@ -33,6 +33,16 @@ class SubscriptionModel(db.Model): cascade='all, delete-orphan', backref='subscription') + network_filter = relationship( + 'NetworkFunctionFilterModel', + cascade='all, delete-orphan', + backref='subscription') + + measurement_groups = relationship( + 'MeasurementGroupModel', + cascade='all, delete-orphan', + backref='subscription') + def __init__(self, subscription_name, status): self.subscription_name = subscription_name self.status = status @@ -57,7 +67,8 @@ class NetworkFunctionModel(db.Model): __tablename__ = 'network_functions' id = Column(Integer, primary_key=True, autoincrement=True) nf_name = Column(String(100), unique=True) - ip_address = Column(String(50)) + ipv4_address = Column(String(50)) + ipv6_address = Column(String(50)) model_invariant_id = Column(String(100)) model_version_id = Column(String(100)) model_name = Column(String(100)) @@ -70,11 +81,12 @@ class NetworkFunctionModel(db.Model): cascade='all, delete-orphan', backref='nf') - def __init__(self, nf_name, ip_address, model_invariant_id, + def __init__(self, nf_name, ipv4_address, ipv6_address, model_invariant_id, model_version_id, model_name, sdnc_model_name, sdnc_model_version, retry_count=0): self.nf_name = nf_name - self.ip_address = ip_address + self.ipv4_address = ipv4_address + self.ipv6_address = ipv6_address self.model_invariant_id = model_invariant_id self.model_version_id = model_version_id self.model_name = model_name @@ -90,7 +102,8 @@ class NetworkFunctionModel(db.Model): return NetworkFunction(sdnc_model_name=self.sdnc_model_name, sdnc_model_version=self.sdnc_model_version, **{'nf_name': self.nf_name, - 'ip_address': self.ip_address, + 'ipv4_address': self.ipv4_address, + 'ipv6_address': self.ipv6_address, 'model_invariant_id': self.model_invariant_id, 'model_version_id': self.model_version_id}) @@ -129,10 +142,117 @@ class NfSubRelationalModel(db.Model): NetworkFunctionModel.nf_name == self.nf_name).one_or_none() db.session.remove() return {'nf_name': self.nf_name, - 'ip_address': nf.ip_address, + 'ipv4_address': nf.ipv4_address, + 'ipv6_address': nf.ipv6_address, 'nf_sub_status': self.nf_sub_status, 'model_invariant_id': nf.model_invariant_id, 'model_version_id': nf.model_version_id, 'model_name': nf.model_name, 'sdnc_model_name': nf.sdnc_model_name, 'sdnc_model_version': nf.sdnc_model_version} + + +class NetworkFunctionFilterModel(db.Model): + __tablename__ = 'nf_filter' + id = Column(Integer, primary_key=True, autoincrement=True) + subscription_name = Column( + String, + ForeignKey(SubscriptionModel.subscription_name, ondelete='cascade', onupdate='cascade'), + unique=True + ) + nf_names = Column(String(100)) + model_invariant_ids = Column(String(100)) + model_version_ids = Column(String(100)) + model_names = Column(String(100)) + + def __init__(self, subscription_name, nf_names, model_invariant_ids, model_version_ids, + model_names): + self.subscription_name = subscription_name + self.nf_names = nf_names + self.model_invariant_ids = model_invariant_ids + self.model_version_ids = model_version_ids + self.model_names = model_names + + def __repr__(self): + return f'subscription_name: {self.subscription_name}, ' \ + f'nf_names: {self.nf_names}, model_invariant_ids: {self.model_invariant_ids}' \ + f'model_version_ids: {self.model_version_ids}, model_names: {self.model_names}' + + def serialize(self): + return {'subscription_name': self.subscription_name, 'nf_names': self.nf_names, + 'model_invariant_ids': self.model_invariant_ids, + 'model_version_ids': self.model_version_ids, 'model_names': self.model_names} + + +class MeasurementGroupModel(db.Model): + __tablename__ = 'measurement_group' + id = Column(Integer, primary_key=True, autoincrement=True) + subscription_name = Column( + String, + ForeignKey(SubscriptionModel.subscription_name, ondelete='cascade', onupdate='cascade') + ) + measurement_group_name = Column(String(100), unique=True) + administrative_state = Column(String(20)) + file_based_gp = Column(Integer) + file_location = Column(String(100)) + measurement_type = Column(JSON) + managed_object_dns_basic = Column(JSON) + + def __init__(self, subscription_name, measurement_group_name, + administrative_state, file_based_gp, file_location, + measurement_type, managed_object_dns_basic): + self.subscription_name = subscription_name + self.measurement_group_name = measurement_group_name + self.administrative_state = administrative_state + self.file_based_gp = file_based_gp + self.file_location = file_location + self.measurement_type = measurement_type + self.managed_object_dns_basic = managed_object_dns_basic + + def __repr__(self): + return f'subscription_name: {self.subscription_name}, ' \ + f'measurement_group_name: {self.measurement_group_name},' \ + f'administrative_state: {self.administrative_state},' \ + f'file_based_gp: {self.file_based_gp},' \ + f'file_location: {self.file_location},' \ + f'measurement_type: {self.measurement_type}' \ + f'managed_object_dns_basic: {self.managed_object_dns_basic}' + + def serialize(self): + return {'subscription_name': self.subscription_name, + 'measurement_group_name': self.measurement_group_name, + 'administrative_state': self.administrative_state, + 'file_based_gp': self.file_based_gp, + 'file_location': self.file_location, + 'measurement_type': self.measurement_type, + 'managed_object_dns_basic': self.managed_object_dns_basic} + + +class NfMeasureGroupRelationalModel(db.Model): + __tablename__ = 'nf_to_measure_grp_rel' + __mapper_args__ = { + 'confirm_deleted_rows': False + } + id = Column(Integer, primary_key=True, autoincrement=True) + measurement_grp_name = Column( + String, + ForeignKey(MeasurementGroupModel.measurement_group_name, ondelete='cascade', + onupdate='cascade') + ) + nf_name = Column( + String, + ForeignKey(NetworkFunctionModel.nf_name, ondelete='cascade', onupdate='cascade') + ) + nf_measure_grp_status = Column(String(20)) + retry_count = Column(Integer) + + def __init__(self, measurement_grp_name, nf_name, nf_measure_grp_status=None, + retry_count=0): + self.measurement_grp_name = measurement_grp_name + self.nf_name = nf_name + self.nf_measure_grp_status = nf_measure_grp_status + self.retry_count = retry_count + + def __repr__(self): + return f'measurement_grp_name: {self.measurement_grp_name}, ' \ + f'nf_name: {self.nf_name}, nf_measure_grp_status: {self.nf_measure_grp_status}' diff --git a/components/pm-subscription-handler/pmsh_service/mod/api/pmsh_swagger.yml b/components/pm-subscription-handler/pmsh_service/mod/api/pmsh_swagger.yml index 58e6a788..3936497c 100644 --- a/components/pm-subscription-handler/pmsh_service/mod/api/pmsh_swagger.yml +++ b/components/pm-subscription-handler/pmsh_service/mod/api/pmsh_swagger.yml @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (C) 2020 Nordix Foundation. +# Copyright (C) 2021 Nordix Foundation. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -87,3 +87,108 @@ paths: enum: [healthy, unhealthy] 503: description: the pmsh service is unavailable + + /subscription: + post: + tags: + - "Subscriptions" + description: >- + Create a PM Subscription + operationId: mod.api.controllers.subscription_controller.post_subscription + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Subscription object to be created" + required: true + schema: + type: object + properties: + subscription: + type: object + properties: + subscriptionName: + type: string + nfFilter: + type: object + properties: + nfNames: + type: array + items: + type: string + modelInvariantIDs: + type: array + items: + type: string + modelVersionIDs: + type: array + items: + type: string + modelNames: + type: array + items: + type: string + additionalProperties: false + measurementGroups: + type: array + minItems: 1 + items: + type: object + properties: + measurementGroup: + type: object + properties: + administrativeState: + allOf: + - type: string + - enum: + - UNLOCKED + - LOCKED + - FILTERING + fileBasedGP: + type: integer + fileLocation: + type: string + measurementTypes: + type: array + minItems: 1 + items: + type: object + properties: + measurementType: + type: string + required: + - measurementType + managedObjectDNsBasic: + type: array + minItems: 1 + items: + type: object + properties: + DN: + type: string + required: + - DN + required: + - administrativeState + - fileBasedGP + - fileLocation + - measurementTypes + - managedObjectDNsBasic + required: + - measurementGroup + required: + - subscriptionName + - nfFilter + - measurementGroups + responses: + 201: + description: successfully created PM Subscription + 409: + description: Duplicate data + 400: + description: Invalid input + diff --git a/components/pm-subscription-handler/pmsh_service/mod/api/services/measurement_group_service.py b/components/pm-subscription-handler/pmsh_service/mod/api/services/measurement_group_service.py new file mode 100644 index 00000000..a4ba4c4b --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/services/measurement_group_service.py @@ -0,0 +1,100 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2020-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== + +from mod.api.db_models import MeasurementGroupModel, NfMeasureGroupRelationalModel +from mod import db +from mod.subscription import SubNfState +from mod.api.services import nf_service +from flask import current_app + + +def check_duplicate_fields(measurement_group, subscription_name): + """ + validates the measurement group content if already present + and if present raises an exception to indicate duplicate request + + Args: + measurement_group (dict): measurement group to validate + subscription_name (string): subscription name to check + Returns: + invalid_messages: list of duplicate data details. + """ + duplicate_field_details = [] + existing_measurement_grp = (MeasurementGroupModel.query.filter( + MeasurementGroupModel.measurement_group_name == measurement_group['measurementGroupName'], + MeasurementGroupModel.subscription_name == subscription_name) + .one_or_none()) + if existing_measurement_grp is not None: + duplicate_field_details.append(f'Measurement Group: ' + f'{measurement_group["measurementGroupName"]} ' + f' for Subscription: {subscription_name} ' + f'already exists.') + return duplicate_field_details + + +def save_measurement_group(measurement_group, subscription_name): + """ + Saves the measurement_group data request + + Args: + measurement_group (dict) : measurement group to save + subscription_name (string) : subscription name to associate with measurement group. + """ + new_measurement_group = MeasurementGroupModel( + subscription_name=subscription_name, + measurement_group_name=measurement_group['measurementGroupName'], + administrative_state=measurement_group['administrativeState'], + file_based_gp=measurement_group['fileBasedGP'], + file_location=measurement_group['fileLocation'], + measurement_type=measurement_group['measurementTypes'], + managed_object_dns_basic=measurement_group['managedObjectDNsBasic']) + db.session.add(new_measurement_group) + + +def apply_nf(nf, measurement_group): + """ + Associate and saves the measurement group with Network function + + Args: + nf (dict): list of filtered network functions to save. + measurement_group (string): measurement group to associate with nf + """ + new_nf_measure_grp_rel = NfMeasureGroupRelationalModel( + measurement_grp_name=measurement_group['measurementGroupName'], + nf_name=nf.nf_name, + nf_measure_grp_status=SubNfState.PENDING_CREATE.value + ) + db.session.add(new_nf_measure_grp_rel) + + +def publish_measurement_group(subscription_name, measurement_group, nfs): + """ + Publishes an event for measurement groups against nfs to MR + + Args: + subscription_name (string): subscription name to publish against nfs + measurement_group (dict): measurement group to publish + nfs (dict): list of filtered network functions to publish. + """ + app_conf = current_app.config['app_config'] + event_body = {"subscriptionName": subscription_name, + "measurementGroup": measurement_group, + "networkFunctions": [nf_service.create_nf_event_body(nf, 'CREATE') + for nf in nfs]} + policy_mr_pub = app_conf.get_mr_pub('policy_pm_publisher') + policy_mr_pub.publish_to_topic(event_body) diff --git a/components/pm-subscription-handler/pmsh_service/mod/api/services/nf_service.py b/components/pm-subscription-handler/pmsh_service/mod/api/services/nf_service.py new file mode 100644 index 00000000..6993630e --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/services/nf_service.py @@ -0,0 +1,111 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2020-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== +from mod import db, aai_client +from mod.api.db_models import NetworkFunctionFilterModel, NetworkFunctionModel +from flask import current_app +from mod.network_function_filter import NetworkFunctionFilter + + +def save_nf_filter(nf_filter, subscription_name): + """ + Saves the nf_filter data request + + Args: + nf_filter (dict) : network unction filter to save + subscription_name (string) : subscription name to associate with nf filter. + """ + new_filter = NetworkFunctionFilterModel(subscription_name=subscription_name, + nf_names=nf_filter['nfNames'], + model_invariant_ids=nf_filter['modelInvariantIDs'], + model_version_ids=nf_filter['modelVersionIDs'], + model_names=nf_filter['modelNames']) + db.session.add(new_filter) + + +def capture_filtered_nfs(nf_filter): + """ + Retrieves network functions from AAI client and + returns a list of filtered NetworkFunctions using the Filter + + Args: + nf_filter (dict): the nf json data from AAI. + Returns: + NetworkFunction (list): a list of filtered NetworkFunction Objects. + """ + filtered_nfs = None + app_conf = current_app.config['app_config'] + nfs_in_aai = aai_client._get_all_aai_nf_data(app_conf) + if nfs_in_aai is not None: + nf_filter_module = NetworkFunctionFilter(**nf_filter) + filtered_nfs = nf_filter_module.filter_nfs(nfs_in_aai, app_conf) + return filtered_nfs + + +def create_nf_event_body(nf, change_type): + """ + Creates a network function event body to publish on MR + + Args: + nf (dict): the Network function to include in the event. + change_type (string): define the change type to be applied on node + Returns: + NetworkFunctionEvent (dict): etwork function event body to publish on MR. + """ + app_conf = current_app.config['app_config'] + return {'networkFunction': {'nfName': nf.nf_name, + 'ipv4Address': nf.ipv4_address, + 'ipv6Address': nf.ipv6_address, + 'blueprintName': nf.sdnc_model_name, + 'blueprintVersion': nf.sdnc_model_version, + 'policyName': app_conf.operational_policy_name, + 'changeType': change_type, + 'closedLoopControlName': app_conf.control_loop_name}} + + +def save_nf(nf): + """ + Saves the network function request + and also updates model names if missing + Args: + nf (dict) : requested network function to save + """ + network_function = NetworkFunctionModel.query.filter( + NetworkFunctionModel.nf_name == nf.nf_name).one_or_none() + if network_function is None: + network_function = NetworkFunctionModel(nf_name=nf.nf_name, + ipv4_address=nf.ipv4_address, + ipv6_address=nf.ipv6_address, + model_invariant_id=nf.model_invariant_id, + model_version_id=nf.model_version_id, + model_name=nf.model_name, + sdnc_model_name=nf.sdnc_model_name, + sdnc_model_version=nf.sdnc_model_version) + db.session.add(network_function) + elif network_function.model_name is None: + NetworkFunctionModel.query.filter(NetworkFunctionModel.nf_name == nf.nf_name)\ + .update({NetworkFunctionModel.sdnc_model_name: nf.sdnc_model_name, + NetworkFunctionModel.sdnc_model_version: nf.sdnc_model_version, + NetworkFunctionModel.model_name: nf.model_name}, + synchronize_session='evaluate') + + +def validate_nf_filter(nf_filter): + invalid_info = [] + if not [filter_name for filter_name, val in nf_filter.items() if len(val) > 0]: + invalid_info.append("At least one filter within nfFilter must not be empty") + return invalid_info diff --git a/components/pm-subscription-handler/pmsh_service/mod/api/services/subscription_service.py b/components/pm-subscription-handler/pmsh_service/mod/api/services/subscription_service.py new file mode 100644 index 00000000..5babe710 --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/services/subscription_service.py @@ -0,0 +1,171 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2020-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== + +from mod import db, logger +from mod.api.db_models import SubscriptionModel, NfSubRelationalModel +from mod.api.services import measurement_group_service, nf_service +from mod.api.custom_exception import InvalidDataException, DuplicateDataException +from mod.subscription import AdministrativeState + + +def create_subscription(subscription): + """ + Creates a subscription + + Args: + subscription (dict): subscription to save. + + Raises: + Error: If anything fails in the server. + """ + perform_validation(subscription) + try: + save_subscription_request(subscription) + filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"]) + if len(filtered_nfs) > 0: + save_filtered_nfs(filtered_nfs) + apply_subscription_to_nfs(filtered_nfs, subscription["subscriptionName"]) + apply_measurement_grp_to_nfs(subscription["subscriptionName"], + filtered_nfs, subscription.get('measurementGroups')) + db.session.commit() + except Exception as e: + db.session.rollback() + logger.error(f'Failed to create subscription ' + f'{subscription["subscriptionName"]} in the DB: {e}', exc_info=True) + raise e + finally: + db.session.remove() + + +def save_filtered_nfs(filtered_nfs): + """ + Saves a network function + + Args: + filtered_nfs (dict): list of filtered network functions to save. + """ + for nf in filtered_nfs: + nf_service.save_nf(nf) + + +def apply_subscription_to_nfs(filtered_nfs, subscription_name): + """ + Associate and saves the subscription with Network functions + + Args: + filtered_nfs (dict): list of filtered network functions to save. + subscription_name (string): subscription name to save against nfs + """ + for nf in filtered_nfs: + new_nf_sub_rel = NfSubRelationalModel(subscription_name=subscription_name, + nf_name=nf.nf_name) + db.session.add(new_nf_sub_rel) + + +def apply_measurement_grp_to_nfs(subscription_name, filtered_nfs, measurement_groups): + """ + Publishes an event for measurement groups against nfs + And saves the successful trigger action as PENDING_CREATE + + Args: + subscription_name (string): subscription name to publish against nfs + filtered_nfs (dict): list of filtered network functions to publish. + measurement_groups (dict): list of measurement group to publish + """ + if measurement_groups: + for measurement_group in measurement_groups: + measurement_group_details = measurement_group['measurementGroup'] + if measurement_group_details['administrativeState'] \ + == AdministrativeState.UNLOCKED.value: + measurement_group_service.publish_measurement_group( + subscription_name, measurement_group_details, filtered_nfs) + for nf in filtered_nfs: + measurement_group_service.apply_nf(nf, measurement_group_details) + + +def perform_validation(subscription): + """ + validates the subscription and if invalid raises an exception + to indicate duplicate/invalid request + + Args: + subscription (Subscription): subscription to validate + + Raises: + DuplicateDataException: exception containing the list of duplicate data fields. + InvalidDataException: exception containing the list of invalid data. + """ + duplicate_messages = check_duplicate_fields(subscription) + if duplicate_messages: + raise DuplicateDataException(duplicate_messages) + invalid_messages = nf_service.validate_nf_filter(subscription["nfFilter"]) + if invalid_messages: + raise InvalidDataException(invalid_messages) + + +def save_subscription_request(subscription): + """ + Saves the subscription request consisting of: + network function filter and measurement groups + + Args: + subscription (dict): subscription request to be saved. + """ + save_subscription(subscription) + nf_service.save_nf_filter(subscription["nfFilter"], subscription["subscriptionName"]) + if subscription.get('measurementGroups'): + for measurement_group in subscription['measurementGroups']: + measurement_group_service \ + .save_measurement_group(measurement_group['measurementGroup'], + subscription["subscriptionName"]) + + +def check_duplicate_fields(subscription): + """ + validates the subscription content if already present + and captures duplicate fields + + Args: + subscription (Subscription): subscription to validate + + Returns: + invalid_messages: list of invalid data details. + """ + duplicate_field_details = [] + existing_subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == subscription['subscriptionName']).one_or_none()) + if existing_subscription is not None: + duplicate_field_details.append(f'subscription Name: {subscription["subscriptionName"]}' + f' already exists.') + if subscription.get('measurementGroups'): + for measurement_group in subscription['measurementGroups']: + duplicate_field_details.extend(measurement_group_service.check_duplicate_fields( + measurement_group['measurementGroup'], subscription['subscriptionName'])) + return duplicate_field_details + + +def save_subscription(subscription): + """ + Saves the subscription data + + Args: + subscription (dict): subscription request to be saved. + """ + new_subscription = SubscriptionModel(subscription_name=subscription["subscriptionName"], + status='LOCKED') + db.session.add(new_subscription) diff --git a/components/pm-subscription-handler/pmsh_service/mod/network_function.py b/components/pm-subscription-handler/pmsh_service/mod/network_function.py index 83130a8e..9a08249f 100755 --- a/components/pm-subscription-handler/pmsh_service/mod/network_function.py +++ b/components/pm-subscription-handler/pmsh_service/mod/network_function.py @@ -26,7 +26,8 @@ class NetworkFunction: def __init__(self, sdnc_model_name=None, sdnc_model_version=None, **kwargs): """ Object representation of the NetworkFunction. """ self.nf_name = kwargs.get('nf_name') - self.ip_address = kwargs.get('ip_address') + self.ipv4_address = kwargs.get('ipv4_address') + self.ipv6_address = kwargs.get('ipv6_address') self.model_invariant_id = kwargs.get('model_invariant_id') self.model_version_id = kwargs.get('model_version_id') self.model_name = kwargs.get('model_name') @@ -35,13 +36,14 @@ class NetworkFunction: @classmethod def nf_def(cls): - return cls(nf_name=None, ip_address=None, model_invariant_id=None, - model_version_id=None, model_name=None, + return cls(nf_name=None, ipv4_address=None, ipv6_address=None, + model_invariant_id=None, model_version_id=None, model_name=None, sdnc_model_name=None, sdnc_model_version=None) def __str__(self): return f'nf-name: {self.nf_name}, ' \ - f'ipaddress-v4-oam: {self.ip_address}, ' \ + f'ipaddress-v4-oam: {self.ipv4_address}, ' \ + f'ipaddress-v6-oam: {self.ipv6_address}, ' \ f'model-invariant-id: {self.model_invariant_id}, ' \ f'model-version-id: {self.model_version_id}, ' \ f'model-name: {self.model_name}, ' \ @@ -51,7 +53,8 @@ class NetworkFunction: def __eq__(self, other): return \ self.nf_name == other.nf_name and \ - self.ip_address == other.ip_address and \ + self.ipv4_address == other.ipv4_address and \ + self.ipv6_address == other.ipv6_address and \ self.model_invariant_id == other.model_invariant_id and \ self.model_version_id == other.model_version_id and \ self.model_name == other.model_name and \ @@ -59,8 +62,8 @@ class NetworkFunction: self.sdnc_model_version == other.sdnc_model_version def __hash__(self): - return hash((self.nf_name, self.ip_address, self.model_invariant_id, - self.model_version_id, self.model_name, + return hash((self.nf_name, self.ipv4_address, self.ipv6_address, + self.model_invariant_id, self.model_version_id, self.model_name, self.sdnc_model_name, self.sdnc_model_version)) def create(self): @@ -70,7 +73,8 @@ class NetworkFunction: if existing_nf is None: new_nf = NetworkFunctionModel(nf_name=self.nf_name, - ip_address=self.ip_address, + ipv4_address=self.ipv6_address, + ipv6_address=self.ipv6_address, model_invariant_id=self.model_invariant_id, model_version_id=self.model_version_id, model_name=self.model_name, diff --git a/components/pm-subscription-handler/pmsh_service/mod/network_function_filter.py b/components/pm-subscription-handler/pmsh_service/mod/network_function_filter.py new file mode 100644 index 00000000..893fc634 --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/network_function_filter.py @@ -0,0 +1,118 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== + +import re +from mod import logger, aai_client +from mod.network_function import NetworkFunction +from mod.api.db_models import NetworkFunctionModel + + +class NetworkFunctionFilter: + def __init__(self, **kwargs): + self.nf_names = kwargs.get('nfNames') + self.model_invariant_ids = kwargs.get('modelInvariantIDs') + self.model_version_ids = kwargs.get('modelVersionIDs') + self.model_names = kwargs.get('modelNames') + self.regex_matcher = re.compile('|'.join(raw_regex for raw_regex in self.nf_names)) + + def filter_nfs(self, nf_data, app_conf): + """ + Returns a list of filtered NetworkFunctions using the Filter initialised in the class + + Args: + nf_data (dict): the nf json data from AAI. + app_conf (App_config): the config for making AAI call. + Returns: + NetworkFunction (list): a list of filtered NetworkFunction Objects. + + Raises: + KeyError: if AAI data cannot be parsed. + """ + nf_list = [] + try: + for nf in nf_data['results']: + if nf['properties'].get('orchestration-status') != 'Active': + continue + name_identifier = 'pnf-name' if nf['node-type'] == 'pnf' else 'vnf-name' + new_nf = NetworkFunction( + nf_name=nf['properties'].get(name_identifier), + ipv4_address=nf['properties'].get('ipaddress-v4-oam'), + ipv6_address=nf['properties'].get('ipaddress-v6-oam'), + model_invariant_id=nf['properties'].get('model-invariant-id'), + model_version_id=nf['properties'].get('model-version-id')) + if self.is_nf_in_filter(new_nf) \ + and self.is_sdnc_model_in_filter(new_nf, app_conf): + nf_list.append(new_nf) + except KeyError as e: + logger.error(f'Failed to parse AAI data: {e}', exc_info=True) + raise + return nf_list + + def is_nf_in_filter(self, nf): + """Match the nf fields against the Filter values initialised in the class + + Args: + nf (NetworkFunction): The NF to be filtered. + + Returns: + bool: True if matched, else False. + """ + match = True + if self.nf_names and self.regex_matcher.search(nf.nf_name) is None: + match = False + if self.model_invariant_ids and nf.model_invariant_id not in self.model_invariant_ids: + match = False + if self.model_version_ids and nf.model_version_id not in self.model_version_ids: + match = False + return match + + def is_sdnc_model_in_filter(self, new_nf, app_conf): + """ + saves NetworkFunction model details and confirms. + + Args: + new_nf (NetworkFunction): the network function to check. + app_conf (App_config): the config for making AAI call. + Returns: + Boolean : true if model name satisfies + + Raises: + KeyError: if AAI data cannot be parsed and logs it. + """ + match = True + network_function = NetworkFunctionModel.query.filter( + NetworkFunctionModel.nf_name == new_nf.nf_name).one_or_none() + + if network_function is None or network_function.model_name is None: + sdnc_model_data = aai_client.get_aai_model_data(app_conf, new_nf.model_invariant_id, + new_nf.model_version_id, new_nf.nf_name) + try: + new_nf.sdnc_model_name = sdnc_model_data['sdnc-model-name'] + new_nf.sdnc_model_version = sdnc_model_data['sdnc-model-version'] + new_nf.model_name = sdnc_model_data['model-name'] + except KeyError as e: + logger.info(f'Skipping NF {new_nf.nf_name} as there is no ' + f'sdnc-model data associated in AAI: {e}', exc_info=True) + else: + new_nf.sdnc_model_name = network_function.sdnc_model_name + new_nf.sdnc_model_version = network_function.sdnc_model_name + new_nf.model_name = network_function.model_name + + if self.model_names and new_nf.model_name not in self.model_names: + match = False + return match diff --git a/components/pm-subscription-handler/pmsh_service/mod/subscription.py b/components/pm-subscription-handler/pmsh_service/mod/subscription.py index fdc1394c..225beddc 100755 --- a/components/pm-subscription-handler/pmsh_service/mod/subscription.py +++ b/components/pm-subscription-handler/pmsh_service/mod/subscription.py @@ -134,7 +134,8 @@ class Subscription: else: change_type = 'CREATE' sub_event = {'nfName': nf.nf_name, - 'ipv4Address': nf.ip_address, + 'ipv4Address': nf.ipv4_address, + 'ipv6Address': nf.ipv6_address, 'blueprintName': nf.sdnc_model_name, 'blueprintVersion': nf.sdnc_model_version, 'policyName': app_conf.operational_policy_name, diff --git a/components/pm-subscription-handler/pom.xml b/components/pm-subscription-handler/pom.xml index fbf44977..9f92d902 100644 --- a/components/pm-subscription-handler/pom.xml +++ b/components/pm-subscription-handler/pom.xml @@ -32,7 +32,7 @@ org.onap.dcaegen2.services pmsh dcaegen2-services-pm-subscription-handler - 1.3.1-SNAPSHOT + 1.4.0-SNAPSHOT UTF-8 . diff --git a/components/pm-subscription-handler/setup.py b/components/pm-subscription-handler/setup.py index 2b8d24a9..b0028e0e 100644 --- a/components/pm-subscription-handler/setup.py +++ b/components/pm-subscription-handler/setup.py @@ -22,7 +22,7 @@ from setuptools import setup, find_packages setup( name="pm_subscription_handler", - version="1.3.2", + version="1.4.0", packages=find_packages(), author="lego@est.tech", author_email="lego@est.tech", diff --git a/components/pm-subscription-handler/tests/controllers/test_subscription_controller.py b/components/pm-subscription-handler/tests/controllers/test_subscription_controller.py new file mode 100644 index 00000000..d8cf7f3a --- /dev/null +++ b/components/pm-subscription-handler/tests/controllers/test_subscription_controller.py @@ -0,0 +1,64 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2019-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== + +import json +import os +from unittest.mock import patch, MagicMock + +from mod import aai_client +from tests.base_setup import BaseClassSetup +from mod.api.controllers import subscription_controller +from flask import current_app + + +class SubscriptionControllerTestCase(BaseClassSetup): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + def setUp(self): + super().setUp() + current_app.config['app_config'] = self.app_conf + with open(os.path.join(os.path.dirname(__file__), + '../data/create_subscription_request.json'), 'r') as data: + self.subscription_request = data.read() + with open(os.path.join(os.path.dirname(__file__), '../data/aai_xnfs.json'), + 'r') as data: + self.aai_response_data = data.read() + + def tearDown(self): + super().tearDown() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + @patch('mod.api.services.subscription_service.create_subscription', + MagicMock(return_value=None)) + def test_post_subscription(self): + response = subscription_controller.post_subscription(json.loads(self.subscription_request)) + self.assertEqual(response[1], 201) + + @patch.object(aai_client, + '_get_all_aai_nf_data') + def test_post_subscription_duplicate_sub(self, mock_aai): + mock_aai.return_value = json.loads(self.aai_response_data) + response = subscription_controller.post_subscription(json.loads(self.subscription_request)) + self.assertEqual(response[1], 409) + self.assertEqual(response[0][0], 'subscription Name: ExtraPM-All-gNB-R2B already exists.') diff --git a/components/pm-subscription-handler/tests/data/create_subscription_request.json b/components/pm-subscription-handler/tests/data/create_subscription_request.json new file mode 100644 index 00000000..e825c30e --- /dev/null +++ b/components/pm-subscription-handler/tests/data/create_subscription_request.json @@ -0,0 +1,42 @@ +{ + "subscription": { + "subscriptionName": "ExtraPM-All-gNB-R2B", + "nfFilter": { + "nfNames": [ + "^pnf.*", + "^vnf.*" + ], + "modelInvariantIDs": [ + "8lk4578-d396-4efb-af02-6b83499b12f8", + "687kj45-d396-4efb-af02-6b83499b12f8" + + ], + "modelVersionIDs": [ + "e80a6ae3-cafd-4d24-850d-e14c084a5ca9" + ], + "modelNames": [ + "PNF102" + ] + }, + "measurementGroups": [ + { + "measurementGroup": { + "measurementGroupName": "msrmt_grp_name", + "fileBasedGP":15, + "fileLocation":"pm.xml", + "administrativeState": "UNLOCKED", + "measurementTypes": [ + { + "measurementType": "counter_a" + } + ], + "managedObjectDNsBasic": [ + { + "DN": "string" + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/components/pm-subscription-handler/tests/data/pm_subscription_event.json b/components/pm-subscription-handler/tests/data/pm_subscription_event.json index cd547d9d..2ccb54e5 100755 --- a/components/pm-subscription-handler/tests/data/pm_subscription_event.json +++ b/components/pm-subscription-handler/tests/data/pm_subscription_event.json @@ -6,6 +6,7 @@ "changeType":"CREATE", "closedLoopControlName":"pmsh-control-loop", "ipv4Address": "1.2.3.4", + "ipv6Address": "1.2.3.4.5.6", "subscription":{ "subscriptionName":"ExtraPM-All-gNB-R2B", "administrativeState":"UNLOCKED", diff --git a/components/pm-subscription-handler/tests/services/test_measurement_group_service.py b/components/pm-subscription-handler/tests/services/test_measurement_group_service.py new file mode 100644 index 00000000..8499618d --- /dev/null +++ b/components/pm-subscription-handler/tests/services/test_measurement_group_service.py @@ -0,0 +1,86 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2020-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== + +import json +import os +from unittest.mock import patch +from flask import current_app +from mod import aai_client +from tests.base_setup import BaseClassSetup +from mod.api.services import subscription_service, nf_service, measurement_group_service + + +class MeasurementGroupServiceTestCase(BaseClassSetup): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def setUp(self): + super().setUp() + current_app.config['app_config'] = self.app_conf + with open(os.path.join(os.path.dirname(__file__), + '../data/create_subscription_request.json'), 'r') as data: + self.subscription_request = data.read() + with open(os.path.join(os.path.dirname(__file__), '../data/aai_xnfs.json'), 'r') as data: + self.aai_response_data = data.read() + with open(os.path.join(os.path.dirname(__file__), '../data/aai_model_info.json'), + 'r') as data: + self.good_model_info = data.read() + + def tearDown(self): + super().tearDown() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def create_test_subs(self, new_sub_name, new_msrmt_grp_name): + subscription = self.subscription_request.replace('ExtraPM-All-gNB-R2B', new_sub_name) + subscription = subscription.replace('msrmt_grp_name', new_msrmt_grp_name) + return subscription + + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + def test_capture_filtered_nfs(self, mock_model_aai, mock_aai): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + subscription = json.loads(self.subscription_request)['subscription'] + filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"]) + self.assertEqual(len(filtered_nfs), 2) + self.assertEqual(filtered_nfs[0].nf_name, 'pnf201') + self.assertEqual(filtered_nfs[1].nf_name, 'pnf_33_ericsson') + + def test_validate_measurement_group(self): + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new2', 'msrmt_grp_name-new2') + subscription = json.loads(subscription)['subscription'] + measurement1 = subscription['measurementGroups'][0] + msg = measurement_group_service.check_duplicate_fields( + measurement1['measurementGroup'], subscription["subscriptionName"]) + self.assertEqual(len(msg), 0) + + @patch.object(nf_service, 'save_nf_filter') + def test_validate_measurement_group_invalid(self, mock_save_filter): + mock_save_filter.return_value = None + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new2', 'msrmt_grp_name-new2') + subscription = json.loads(subscription)['subscription'] + subscription_service.save_subscription_request(subscription) + measurement1 = subscription['measurementGroups'][0] + msg = measurement_group_service.check_duplicate_fields( + measurement1['measurementGroup'], subscription["subscriptionName"]) + self.assertEqual(msg[0], 'Measurement Group: msrmt_grp_name-new2 for ' + 'Subscription: xtraPM-All-gNB-R2B-new2 already exists.') diff --git a/components/pm-subscription-handler/tests/services/test_nf_service.py b/components/pm-subscription-handler/tests/services/test_nf_service.py new file mode 100644 index 00000000..0523b102 --- /dev/null +++ b/components/pm-subscription-handler/tests/services/test_nf_service.py @@ -0,0 +1,123 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2020-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== + +import json +import os +from unittest.mock import patch +from flask import current_app +from mod.api.db_models import NetworkFunctionModel +from mod import aai_client +from tests.base_setup import BaseClassSetup +from mod.api.services import nf_service + + +class NetworkFunctionServiceTestCase(BaseClassSetup): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def setUp(self): + super().setUp() + current_app.config['app_config'] = self.app_conf + with open(os.path.join(os.path.dirname(__file__), + '../data/create_subscription_request.json'), 'r') as data: + self.subscription_request = data.read() + with open(os.path.join(os.path.dirname(__file__), '../data/aai_xnfs.json'), 'r') as data: + self.aai_response_data = data.read() + with open(os.path.join(os.path.dirname(__file__), '../data/aai_model_info.json'), + 'r') as data: + self.good_model_info = data.read() + + def tearDown(self): + super().tearDown() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def create_test_subs(self, new_sub_name, new_msrmt_grp_name): + subscription = self.subscription_request.replace('ExtraPM-All-gNB-R2B', new_sub_name) + subscription = subscription.replace('msrmt_grp_name', new_msrmt_grp_name) + return subscription + + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + def test_capture_filtered_nfs(self, mock_model_aai, mock_aai): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + subscription = json.loads(self.subscription_request)['subscription'] + filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"]) + self.assertEqual(len(filtered_nfs), 2) + self.assertEqual(filtered_nfs[0].nf_name, 'pnf201') + self.assertEqual(filtered_nfs[1].nf_name, 'pnf_33_ericsson') + + @patch.object(aai_client, '_get_all_aai_nf_data') + def test_capture_filtered_nfs_with_no_aai_nodes(self, mock_aai): + mock_aai.return_value = None + subscription = json.loads(self.subscription_request)['subscription'] + filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"]) + self.assertIsNone(filtered_nfs) + + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + def test_create_nf_event_body(self, mock_model_aai, mock_aai): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + subscription = json.loads(self.subscription_request)['subscription'] + nf = nf_service.capture_filtered_nfs(subscription["nfFilter"])[0] + event_body = nf_service.create_nf_event_body(nf, 'CREATE') + self.assertEqual(event_body['networkFunction']['nfName'], nf.nf_name) + self.assertEqual(event_body['networkFunction']['ipv4Address'], nf.ipv4_address) + self.assertEqual(event_body['networkFunction']['ipv6Address'], nf.ipv6_address) + self.assertEqual(event_body['networkFunction']['blueprintName'], nf.sdnc_model_name) + self.assertEqual(event_body['networkFunction']['blueprintVersion'], nf.sdnc_model_version) + self.assertEqual(event_body['networkFunction']['policyName'], + self.app_conf.operational_policy_name) + self.assertEqual(event_body['networkFunction']['changeType'], 'CREATE') + self.assertEqual(event_body['networkFunction']['closedLoopControlName'], + self.app_conf.control_loop_name) + + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + def test_save_nf_new_nf(self, mock_model_aai, mock_aai): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + subscription = json.loads(self.subscription_request)['subscription'] + nf = nf_service.capture_filtered_nfs(subscription["nfFilter"])[0] + nf.nf_name = 'newnf1' + nf_service.save_nf(nf) + network_function = NetworkFunctionModel.query.filter( + NetworkFunctionModel.nf_name == nf.nf_name).one_or_none() + self.assertIsNotNone(network_function) + + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + def test_save_nf_missing_model(self, mock_model_aai, mock_aai): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + subscription = json.loads(self.subscription_request)['subscription'] + nf = nf_service.capture_filtered_nfs(subscription["nfFilter"])[0] + nf.nf_name = 'newnf2' + nf.model_name = None + nf_service.save_nf(nf) + nf.model_name = 'new_model_name' + nf_service.save_nf(nf) + network_function = NetworkFunctionModel.query.filter( + NetworkFunctionModel.nf_name == nf.nf_name).one_or_none() + self.assertIsNotNone(network_function) + self.assertEqual(network_function.model_name, 'new_model_name') diff --git a/components/pm-subscription-handler/tests/services/test_subscription_service.py b/components/pm-subscription-handler/tests/services/test_subscription_service.py new file mode 100644 index 00000000..9627e9c0 --- /dev/null +++ b/components/pm-subscription-handler/tests/services/test_subscription_service.py @@ -0,0 +1,165 @@ +# ============LICENSE_START=================================================== +# Copyright (C) 2020-2021 Nordix Foundation. +# ============================================================================ +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# ============LICENSE_END===================================================== +import copy +import json +import os +from unittest.mock import patch, MagicMock +from flask import current_app +from mod.api.db_models import SubscriptionModel, MeasurementGroupModel, \ + NfMeasureGroupRelationalModel +from mod.subscription import SubNfState +from mod import aai_client +from mod.api.custom_exception import DuplicateDataException, InvalidDataException +from mod.pmsh_utils import _MrPub +from tests.base_setup import BaseClassSetup +from mod.api.services import subscription_service, nf_service, measurement_group_service + + +class SubscriptionServiceTestCase(BaseClassSetup): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def setUp(self): + super().setUp() + current_app.config['app_config'] = self.app_conf + with open(os.path.join(os.path.dirname(__file__), + '../data/create_subscription_request.json'), 'r') as data: + self.subscription_request = data.read() + with open(os.path.join(os.path.dirname(__file__), '../data/aai_xnfs.json'), 'r') as data: + self.aai_response_data = data.read() + with open(os.path.join(os.path.dirname(__file__), '../data/aai_model_info.json'), + 'r') as data: + self.good_model_info = data.read() + + def tearDown(self): + super().tearDown() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def create_test_subs(self, new_sub_name, new_msrmt_grp_name): + subscription = self.subscription_request.replace('ExtraPM-All-gNB-R2B', new_sub_name) + subscription = subscription.replace('msrmt_grp_name', new_msrmt_grp_name) + return subscription + + @patch('mod.api.services.nf_service.save_nf_filter', MagicMock(return_value=None)) + @patch('mod.pmsh_utils._MrPub.publish_to_topic', MagicMock(return_value=None)) + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + def test_create_subscription(self, mock_model_aai, mock_aai): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new', 'msrmt_grp_name-new') + subscription_service.create_subscription(json.loads(subscription)['subscription']) + existing_subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-new').one_or_none()) + self.assertIsNotNone(existing_subscription) + existing_measurement_grp = (MeasurementGroupModel.query.filter( + MeasurementGroupModel.measurement_group_name == 'msrmt_grp_name-new', + MeasurementGroupModel.subscription_name == 'xtraPM-All-gNB-R2B-new').one_or_none()) + self.assertIsNotNone(existing_measurement_grp) + msr_grp_nf_rel = (NfMeasureGroupRelationalModel.query.filter( + NfMeasureGroupRelationalModel.measurement_grp_name == 'msrmt_grp_name-new')).all() + for pubslished_event in msr_grp_nf_rel: + self.assertEqual(pubslished_event.nf_measure_grp_status, + SubNfState.PENDING_CREATE.value) + + @patch('mod.api.services.nf_service.save_nf_filter', MagicMock(return_value=None)) + @patch.object(aai_client, '_get_all_aai_nf_data') + def test_create_subscription_service_failed_rollback(self, mock_aai): + mock_aai.side_effect = InvalidDataException(["AAI call failed"]) + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-fail', 'msrmt_grp_name-fail') + try: + subscription_service.create_subscription(json.loads(subscription)['subscription']) + except InvalidDataException as exception: + self.assertEqual(exception.invalid_messages, ["AAI call failed"]) + + # Checking Rollback + existing_subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-fail').one_or_none()) + self.assertIsNone(existing_subscription) + + def test_perform_validation_existing_sub(self): + try: + subscription_service.create_subscription(json.loads(self.subscription_request) + ['subscription']) + except DuplicateDataException as exception: + self.assertEqual(exception.duplicate_fields_info[0], + "subscription Name: ExtraPM-All-gNB-R2B already exists.") + + @patch.object(nf_service, 'save_nf_filter') + def test_save_subscription_request(self, mock_save_filter): + mock_save_filter.return_value = None + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new1', 'msrmt_grp_name-new1') + subscription_service.save_subscription_request(json.loads(subscription)['subscription']) + existing_subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-new1').one_or_none()) + self.assertIsNotNone(existing_subscription) + self.assertTrue(mock_save_filter.called) + existing_measurement_grp = (MeasurementGroupModel.query.filter( + MeasurementGroupModel.measurement_group_name == 'msrmt_grp_name-new1', + MeasurementGroupModel.subscription_name == 'xtraPM-All-gNB-R2B-new1').one_or_none()) + self.assertIsNotNone(existing_measurement_grp) + + @patch.object(nf_service, 'save_nf_filter') + def test_save_subscription_request_no_measure_grp(self, mock_save_filter): + mock_save_filter.return_value = None + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new1', 'msrmt_grp_name-new1') + subscription = json.loads(subscription)['subscription'] + del subscription['measurementGroups'] + subscription_service.save_subscription_request(subscription) + existing_subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-new1').one_or_none()) + self.assertIsNotNone(existing_subscription) + self.assertTrue(mock_save_filter.called) + existing_measurement_grp = (MeasurementGroupModel.query.filter( + MeasurementGroupModel.measurement_group_name == 'msrmt_grp_name-new1', + MeasurementGroupModel.subscription_name == 'xtraPM-All-gNB-R2B-new1').one_or_none()) + self.assertIsNone(existing_measurement_grp) + + @patch.object(measurement_group_service, 'apply_nf') + @patch.object(_MrPub, 'publish_to_topic') + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + def test_apply_measurement_grp_to_nfs(self, mock_model_aai, mock_aai, + mock_publish, mock_apply_nf): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + mock_publish.return_value = None + mock_apply_nf.return_value = None + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new2', 'msrmt_grp_name-new2') + subscription = json.loads(subscription)['subscription'] + measurement1 = subscription['measurementGroups'][0] + measurement2 = self.create_measurement_grp(measurement1, 'meas2', 'UNLOCKED') + measurement3 = self.create_measurement_grp(measurement1, 'meas3', 'LOCKED') + subscription['measurementGroups'].extend([measurement2, measurement3]) + filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"]) + subscription_service.apply_measurement_grp_to_nfs( + subscription["subscriptionName"], filtered_nfs, subscription.get('measurementGroups')) + # Two unlocked measurement Group published + self.assertEqual(mock_publish.call_count, 2) + # 2 measurement group with 2 nfs each contribute 4 calls + self.assertEqual(mock_apply_nf.call_count, 4) + + def create_measurement_grp(self, measurement, measurement_name, admin_status): + new_measurement = copy.deepcopy(measurement) + new_measurement['measurementGroup']['measurementGroupName'] = measurement_name + new_measurement['measurementGroup']['administrativeState'] = admin_status + return new_measurement diff --git a/components/pm-subscription-handler/tests/test_network_function.py b/components/pm-subscription-handler/tests/test_network_function.py index 5a1a6ba8..990cbbb6 100755 --- a/components/pm-subscription-handler/tests/test_network_function.py +++ b/components/pm-subscription-handler/tests/test_network_function.py @@ -33,12 +33,14 @@ class NetworkFunctionTests(BaseClassSetup): super().setUp() self.nf_1 = NetworkFunction(sdnc_model_name='blah', sdnc_model_version=1.0, **{'nf_name': 'pnf_1', - 'ip_address': '1.2.3.4', + 'ipv4_address': '1.2.3.4', + 'ipv6_address': '1.2.3.4.5.6', 'model_invariant_id': 'some_id', 'model_version_id': 'some_other_id'}) self.nf_2 = NetworkFunction(sdnc_model_name='blah', sdnc_model_version=2.0, **{'nf_name': 'pnf_2', - 'ip_address': '1.2.3.4', + 'ipv4_address': '1.2.3.4', + 'ipv6_address': '1.2.3.4.5.6', 'model_invariant_id': 'some_id', 'model_version_id': 'some_other_id'}) with open(os.path.join(os.path.dirname(__file__), 'data/aai_model_info.json'), 'r') as data: diff --git a/components/pm-subscription-handler/tests/test_subscription.py b/components/pm-subscription-handler/tests/test_subscription.py index b18f41e8..d45c273d 100755 --- a/components/pm-subscription-handler/tests/test_subscription.py +++ b/components/pm-subscription-handler/tests/test_subscription.py @@ -140,7 +140,8 @@ class SubscriptionTest(BaseClassSetup): 'data/pm_subscription_event.json'), 'r') as data: expected_sub_event = json.load(data) nf = NetworkFunction(nf_name='pnf_1', - ip_address='1.2.3.4', + ipv4_address='1.2.3.4', + ipv6_address='1.2.3.4.5.6', model_invariant_id='some-id', model_version_id='some-id') nf.sdnc_model_name = 'some-name' diff --git a/components/pm-subscription-handler/tests/test_subscription_handler.py b/components/pm-subscription-handler/tests/test_subscription_handler.py index 2293ee50..ecc45f66 100644 --- a/components/pm-subscription-handler/tests/test_subscription_handler.py +++ b/components/pm-subscription-handler/tests/test_subscription_handler.py @@ -109,7 +109,8 @@ class SubscriptionHandlerTest(BaseClassSetup): MagicMock(return_value=NetworkFunctionModel(nf_name='pnf_1', model_invariant_id='some-id', model_version_id='some-id', - ip_address='ip_address', + ipv4_address='ip_address4', + ipv6_address='ip_address6', model_name='model_name', sdnc_model_name='sdnc_model_name', sdnc_model_version='sdnc_model_version'))) @@ -145,7 +146,8 @@ class SubscriptionHandlerTest(BaseClassSetup): MagicMock(return_value=NetworkFunctionModel(nf_name='pnf_1', model_invariant_id='some-id', model_version_id='some-id', - ip_address='ip_address', + ipv4_address='ip_address4', + ipv6_address='ip_address6', model_name='model_name', sdnc_model_name='sdnc_model_name', sdnc_model_version='sdnc_model_version', diff --git a/components/pm-subscription-handler/version.properties b/components/pm-subscription-handler/version.properties index ef20baaf..9e0d73d4 100644 --- a/components/pm-subscription-handler/version.properties +++ b/components/pm-subscription-handler/version.properties @@ -1,6 +1,6 @@ major=1 -minor=3 -patch=2 +minor=4 +patch=0 base_version=${major}.${minor}.${patch} release_version=${base_version} snapshot_version=${base_version}-SNAPSHOT