From 70de6a27b7722e3ed02d8e8a8c7933e053eacabb Mon Sep 17 00:00:00 2001 From: SagarS Date: Wed, 8 Sep 2021 14:46:32 +0100 Subject: [DCAEGEN2] PMSH Create Subscription public API Issue-ID: DCAEGEN2-2819 Change-Id: I80636be25dc4f7b1c5ce7470c7a38c010cb339a1 Signed-off-by: SagarS --- components/pm-subscription-handler/Changelog.md | 1 + .../pmsh_service/mod/aai_client.py | 14 +- .../pmsh_service/mod/api/controller.py | 38 ++- .../pmsh_service/mod/api/custom_exception.py | 38 +++ .../pmsh_service/mod/api/db_models.py | 21 +- .../pmsh_service/mod/api/pmsh_swagger.yml | 34 +- .../mod/api/services/measurement_group_service.py | 87 +++++ .../pmsh_service/mod/api/services/nf_service.py | 75 +++++ .../mod/api/services/subscription_service.py | 275 ++++++++++++++++ .../pmsh_service/mod/network_function.py | 16 +- .../pmsh_service/mod/pmsh_config.py | 3 + .../pmsh_service/mod/subscription_handler.py | 2 +- .../tests/data/create_subscription_request.json | 60 ++++ .../services/test_measurement_group_service.py | 113 +++++++ .../tests/services/test_nf_service.py | 105 ++++++ .../tests/services/test_subscription_service.py | 353 +++++++++++++++++++++ .../tests/test_aai_service.py | 6 +- .../tests/test_controller.py | 72 ++++- .../tests/test_subscription.py | 2 +- 19 files changed, 1286 insertions(+), 29 deletions(-) create mode 100644 components/pm-subscription-handler/pmsh_service/mod/api/custom_exception.py create mode 100644 components/pm-subscription-handler/pmsh_service/mod/api/services/measurement_group_service.py create mode 100644 components/pm-subscription-handler/pmsh_service/mod/api/services/nf_service.py create mode 100644 components/pm-subscription-handler/pmsh_service/mod/api/services/subscription_service.py create mode 100644 components/pm-subscription-handler/tests/data/create_subscription_request.json create mode 100644 components/pm-subscription-handler/tests/services/test_measurement_group_service.py create mode 100644 components/pm-subscription-handler/tests/services/test_nf_service.py create mode 100644 components/pm-subscription-handler/tests/services/test_subscription_service.py (limited to 'components/pm-subscription-handler') diff --git a/components/pm-subscription-handler/Changelog.md b/components/pm-subscription-handler/Changelog.md index f98d2b8d..eff84191 100755 --- a/components/pm-subscription-handler/Changelog.md +++ b/components/pm-subscription-handler/Changelog.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed * Updated PMSH app configuration, simplified existing config (DCAEGEN2-2814) * Created Schema definitions in swagger file according to the new structure (DCAEGEN2-2889) +* Implemented Create Subscription public API (DCAEGEN2-2819) ## [1.3.2] ### Changed diff --git a/components/pm-subscription-handler/pmsh_service/mod/aai_client.py b/components/pm-subscription-handler/pmsh_service/mod/aai_client.py index 39adba46..d2aeb0f0 100755 --- a/components/pm-subscription-handler/pmsh_service/mod/aai_client.py +++ b/components/pm-subscription-handler/pmsh_service/mod/aai_client.py @@ -17,22 +17,20 @@ # ============LICENSE_END===================================================== import json from os import environ - import requests from requests.auth import HTTPBasicAuth - import mod.network_function import mod.pmsh_utils from mod import logger -def get_pmsh_nfs_from_aai(app_conf): +def get_pmsh_nfs_from_aai(app_conf, nf_filter): """ Returns the Network Functions from AAI related to the Subscription. Args: app_conf (AppConfig): the AppConfig object. - + nf_filter (NetworkFunctionFilter): the filter to apply on nf from aai Returns: NetworkFunctions (list): list of NetworkFunctions. @@ -41,7 +39,7 @@ def get_pmsh_nfs_from_aai(app_conf): """ aai_nf_data = _get_all_aai_nf_data(app_conf) if aai_nf_data: - nfs = _filter_nf_data(aai_nf_data, app_conf) + nfs = _filter_nf_data(aai_nf_data, app_conf, nf_filter) else: raise RuntimeError('Failed to get data from AAI') return nfs @@ -114,14 +112,14 @@ def _get_aai_request_headers(**kwargs): 'RequestID': kwargs['request_id']} -def _filter_nf_data(nf_data, app_conf): +def _filter_nf_data(nf_data, app_conf, nf_filter): """ Returns a list of filtered NetworkFunctions using the nf_filter. Args: nf_data (dict): the nf json data from AAI. app_conf (AppConfig): the AppConfig object. - + nf_filter (NetworkFunctionFilter): filter data to apply on network functions Returns: NetworkFunction (list): a list of filtered NetworkFunction Objects. @@ -142,7 +140,7 @@ def _filter_nf_data(nf_data, app_conf): model_version_id=nf['properties'].get('model-version-id')) if not new_nf.set_nf_model_params(app_conf): continue - if app_conf.nf_filter.is_nf_in_filter(new_nf): + if nf_filter.is_nf_in_filter(new_nf): nf_list.append(new_nf) except KeyError as e: logger.error(f'Failed to parse AAI data: {e}', exc_info=True) diff --git a/components/pm-subscription-handler/pmsh_service/mod/api/controller.py b/components/pm-subscription-handler/pmsh_service/mod/api/controller.py index 21d29caf..8af6c777 100755 --- a/components/pm-subscription-handler/pmsh_service/mod/api/controller.py +++ b/components/pm-subscription-handler/pmsh_service/mod/api/controller.py @@ -1,5 +1,5 @@ # ============LICENSE_START=================================================== -# Copyright (C) 2019-2020 Nordix Foundation. +# 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. @@ -17,6 +17,11 @@ # ============LICENSE_END===================================================== from mod.subscription import Subscription +from http import HTTPStatus +from mod import logger +from mod.api.services import subscription_service +from connexion import NoContent +from mod.api.custom_exception import InvalidDataException, DuplicateDataException def status(): @@ -40,3 +45,34 @@ def get_all_sub_to_nf_relations(): """ subs_dict = [s.serialize() for s in Subscription.get_all()] return subs_dict + + +def post_subscription(body): + """ + Creates a subscription + + Args: + body (dict): subscription request body to save. + + Returns: + Success : NoContent, 201 + Invalid Data : Invalid message, 400 + Duplicate Data : Duplicate field detail, 409 + + Raises: + Error: If anything fails in the server. + """ + response = NoContent, HTTPStatus.CREATED.value + try: + subscription_service.create_subscription(body['subscription']) + except DuplicateDataException as e: + logger.error(f'Failed to create subscription for ' + f'{body["subscription"]["subscriptionName"]} due to duplicate data: {e}', + exc_info=True) + response = e.duplicate_field_info, HTTPStatus.CONFLICT.value + except InvalidDataException as e: + logger.error(f'Failed to create subscription for ' + f'{body["subscription"]["subscriptionName"]} due to invalid data: {e}', + exc_info=True) + response = e.invalid_message, 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..606d500c --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/custom_exception.py @@ -0,0 +1,38 @@ +# ============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===================================================== + +class InvalidDataException(Exception): + """Exception raised for invalid inputs. + + Attributes: + message -- detail on invalid data + """ + + def __init__(self, invalid_message): + self.invalid_message = invalid_message + + +class DuplicateDataException(Exception): + """Exception raised for duplicate inputs. + + Attributes: + message -- detail on duplicate field + """ + + def __init__(self, duplicate_field_info): + self.duplicate_field_info = duplicate_field_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 5c87fa55..2b340e24 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 @@ -179,9 +179,11 @@ class NetworkFunctionFilterModel(db.Model): 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} + return {'subscriptionName': self.subscription_name, + 'nfNames': convert_db_string_to_list(self.nf_names), + 'modelInvariantIDs': convert_db_string_to_list(self.model_invariant_ids), + 'modelVersionIDs': convert_db_string_to_list(self.model_version_ids), + 'modelNames': convert_db_string_to_list(self.model_names)} class MeasurementGroupModel(db.Model): @@ -256,3 +258,16 @@ class NfMeasureGroupRelationalModel(db.Model): 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}' + + +def convert_db_string_to_list(db_string): + """ + Converts a db string to array and + removes empty strings + Args: + db_string (string): The db string to convert into an array + Returns: + list[string]: converted list of strings else empty + """ + array_format = db_string.strip('{}').split(',') + return [x for x in array_format if x.strip() != ""] 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 2a6137cf..11cea4ee 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 @@ -21,6 +21,8 @@ info: title: PM Subscription Handler Service version: "2.0.0" description: PM subscription handler enables control of performance management jobs on network functions in ONAP +consumes: + - "application/json" produces: - "application/json" basePath: "/" @@ -89,6 +91,27 @@ paths: 503: description: the pmsh service is unavailable + /subscription: + post: + tags: + - "Subscription" + description: >- + Create a PM Subscription + operationId: mod.api.controller.post_subscription + parameters: + - in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/subscription" + responses: + 201: + description: successfully created PM Subscription + 409: + description: Duplicate data + 400: + description: Invalid input + definitions: subscription: type: object @@ -119,29 +142,30 @@ definitions: nfFilter: type: object - minProperties: 1 + description: "At least one valid filter value within nfFilter is required" additionalProperties: false properties: nfNames: type: array - minItems: 1 items: type: string modelInvariantIDs: type: array - minItems: 1 items: type: string modelVersionIDs: type: array - minItems: 1 items: type: string modelNames: type: array - minItems: 1 items: type: string + required: + - nfNames + - modelInvariantIDs + - modelVersionIDs + - modelNames measurementGroup: type: object 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..329dc857 --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/services/measurement_group_service.py @@ -0,0 +1,87 @@ +# ============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===================================================== + +from mod.api.db_models import MeasurementGroupModel, NfMeasureGroupRelationalModel +from mod import db, logger +from mod.subscription import SubNfState +from mod.api.services import nf_service +from mod.pmsh_config import MRTopic, AppConfig + + +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. + + Returns: + MeasurementGroupModel : measurement group saved in the database + """ + logger.info(f'Saving measurement group for subscription request: {subscription_name}') + 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) + return new_measurement_group + + +def apply_nf_to_measgroup(nf_name, measurement_group_name): + """ + Associate and saves the measurement group with Network function + + Args: + nf_name (string): Network function name. + measurement_group_name (string): Measurement group name + """ + new_nf_measure_grp_rel = NfMeasureGroupRelationalModel( + measurement_grp_name=measurement_group_name, + nf_name=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, nf): + """ + Publishes an event for measurement group against nfs to MR + + Args: + subscription_name (string): Subscription name to publish against nf + measurement_group (MeasurementGroupModel): Measurement group to publish + nf (NetworkFunction): Network function to publish. + """ + event_body = nf_service.create_nf_event_body(nf, 'CREATE') + event_body['subscription'] = { + "administrativeState": measurement_group.administrative_state, + "subscriptionName": subscription_name, + "fileBasedGP": measurement_group.file_based_gp, + "fileLocation": measurement_group.file_location, + "measurementGroup": { + "measurementGroupName": measurement_group.measurement_group_name, + "measurementTypes": measurement_group.measurement_type, + "managedObjectDNsBasic": measurement_group.managed_object_dns_basic + } + } + AppConfig.get_instance().publish_to_topic(MRTopic.POLICY_PM_PUBLISHER.value, 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..1fca766a --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/services/nf_service.py @@ -0,0 +1,75 @@ +# ============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===================================================== + +from mod import db, aai_client, logger +from mod.api.db_models import NetworkFunctionModel +from mod.pmsh_config import AppConfig +from mod.network_function import NetworkFunctionFilter + + +def capture_filtered_nfs(sub_name): + """ + Retrieves network functions from AAI client and + returns a list of filtered NetworkFunctions using the Filter + + Args: + sub_name (string): The name of subscription inorder to perform filtering + Returns: + list[NetworkFunction]: a list of filtered NetworkFunction Objects. + """ + logger.info(f'Getting filtered nfs for subscription: {sub_name}') + nf_filter = NetworkFunctionFilter.get_network_function_filter(sub_name) + return aai_client.get_pmsh_nfs_from_aai(AppConfig.get_instance(), nf_filter) + + +def create_nf_event_body(nf, change_type): + """ + Creates a network function event body to publish on MR + + Args: + nf (NetworkFunction): the Network function to include in the event. + change_type (string): define the change type to be applied on node + Returns: + dict: network function event body to publish on MR. + """ + app_conf = AppConfig.get_instance() + return {'nfName': nf.nf_name, + 'ipAddress': nf.ipv4_address if nf.ipv6_address in (None, '') + else 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 to the db + Args: + nf (NetworkFunction) : requested network function to save + """ + 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) 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..1485bebe --- /dev/null +++ b/components/pm-subscription-handler/pmsh_service/mod/api/services/subscription_service.py @@ -0,0 +1,275 @@ +# ============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===================================================== + +from mod import db, logger +from mod.api.db_models import SubscriptionModel, NfSubRelationalModel, \ + NetworkFunctionFilterModel, NetworkFunctionModel +from mod.api.services import measurement_group_service, nf_service +from mod.api.custom_exception import InvalidDataException, DuplicateDataException +from mod.subscription import AdministrativeState +from sqlalchemy.exc import IntegrityError + + +def create_subscription(subscription): + """ + Creates a subscription + + Args: + subscription (dict): subscription to save. + + Raises: + DuplicateDataException: contains details on duplicate fields + Exception: contains runtime error details + """ + logger.info(f'Initiating create subscription for: {subscription["subscriptionName"]}') + perform_validation(subscription) + try: + sub_name, measurement_groups = save_subscription_request(subscription) + db.session.commit() + logger.info(f'Successfully saved subscription request for: ' + f'{subscription["subscriptionName"]}') + filtered_nfs = nf_service.capture_filtered_nfs(sub_name) + if filtered_nfs: + logger.info(f'Applying the filtered nfs for subscription: {sub_name}') + save_filtered_nfs(filtered_nfs) + apply_subscription_to_nfs(filtered_nfs, sub_name) + unlocked_msmt_groups = apply_measurement_grp_to_nfs(filtered_nfs, measurement_groups) + db.session.commit() + if unlocked_msmt_groups: + publish_measurement_grp_to_nfs(sub_name, filtered_nfs, unlocked_msmt_groups) + else: + logger.error(f'All measurement groups are locked for subscription: {sub_name}, ' + f'please verify/check measurement groups.') + else: + logger.error(f'No network functions found for subscription: {sub_name}, ' + f'please verify/check NetworkFunctionFilter.') + except IntegrityError as e: + db.session.rollback() + raise DuplicateDataException(f'DB Integrity issue encountered: {e.orig.args[0]}') + except Exception as e: + db.session.rollback() + raise e + finally: + db.session.remove() + + +def publish_measurement_grp_to_nfs(subscription_name, filtered_nfs, measurement_groups): + """ + Publishes an event for measurement groups against nfs + + Args: + subscription_name (string): subscription name against nfs + filtered_nfs (list[NetworkFunction])): list of filtered network functions + measurement_groups (list[MeasurementGroupModel]): list of unlocked measurement group + """ + for measurement_group in measurement_groups: + for nf in filtered_nfs: + try: + logger.info(f'Publishing event for nf name, measure_grp_name: {nf.nf_name},' + f'{measurement_group.measurement_group_name}') + measurement_group_service.publish_measurement_group( + subscription_name, measurement_group, nf) + except Exception as ex: + logger.error(f'Publish event failed for nf name, measure_grp_name, sub_name: ' + f'{nf.nf_name},{measurement_group.measurement_group_name}, ' + f'{subscription_name} with error: {ex}') + + +def save_filtered_nfs(filtered_nfs): + """ + Saves a network function + + Args: + filtered_nfs (list[NetworkFunction]): list of filtered network functions to save. + """ + pmsh_nf_names = list(nf.nf_name for nf in NetworkFunctionModel.query.all()) + for nf in filtered_nfs: + if nf.nf_name not in pmsh_nf_names: + 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 (list[NetworkFunction]): list of filtered network functions to save. + subscription_name (string): subscription name to save against nfs + """ + logger.info(f'Saving filtered nfs for subscription: {subscription_name}') + 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(filtered_nfs, measurement_groups): + """ + Saves measurement groups against nfs with status as PENDING_CREATE + + Args: + filtered_nfs (list[NetworkFunction])): list of filtered network functions + measurement_groups (list[MeasurementGroupModel]): list of measurement group + + Returns: + list[MeasurementGroupModel]: list of Unlocked measurement groups + """ + unlocked_msmt_groups = [] + for measurement_group in measurement_groups: + if measurement_group.administrative_state \ + == AdministrativeState.UNLOCKED.value: + unlocked_msmt_groups.append(measurement_group) + for nf in filtered_nfs: + logger.info(f'Saving measurement group to nf name, measure_grp_name: {nf.nf_name},' + f'{measurement_group.measurement_group_name}') + measurement_group_service.apply_nf_to_measgroup( + nf.nf_name, measurement_group.measurement_group_name) + else: + logger.info(f'No nfs added as measure_grp_name: ' + f'{measurement_group.measurement_group_name} is LOCKED') + return unlocked_msmt_groups + + +def check_missing_data(subscription): + """ + checks if the subscription request has missing data + + Args: + subscription (dict): subscription to validate + + Raises: + InvalidDataException: exception containing the list of invalid data. + """ + if subscription['subscriptionName'].strip() in (None, ''): + raise InvalidDataException("No value provided in subscription name") + + for measurement_group in subscription.get('measurementGroups'): + measurement_group_details = measurement_group['measurementGroup'] + if measurement_group_details['administrativeState'].strip() in (None, ''): + raise InvalidDataException("No value provided for administrative state") + if measurement_group_details['measurementGroupName'].strip() in (None, ''): + raise InvalidDataException("No value provided for measurement group name") + + +def perform_validation(subscription): + """ + validates the subscription and if invalid raises an exception + to indicate duplicate/invalid request + + Args: + subscription (dict): subscription to validate + + Raises: + DuplicateDataException: exception containing the detail on duplicate data field. + InvalidDataException: exception containing the detail on invalid data. + """ + logger.info(f'Performing subscription validation for: {subscription["subscriptionName"]}') + check_missing_data(subscription) + logger.info(f'No missing data found for: {subscription["subscriptionName"]}') + check_duplicate_fields(subscription["subscriptionName"]) + logger.info(f'No duplicate data found for: {subscription["subscriptionName"]}') + validate_nf_filter(subscription["nfFilter"]) + logger.info(f'Filter data is valid for: {subscription["subscriptionName"]}') + + +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. + + Returns: + string: Subscription name + list[MeasurementGroupModel]: list of measurement groups + """ + logger.info(f'Saving subscription request for: {subscription["subscriptionName"]}') + sub_name = save_subscription(subscription).subscription_name + save_nf_filter(subscription["nfFilter"], subscription["subscriptionName"]) + measurement_groups = [] + for measurement_group in subscription['measurementGroups']: + measurement_groups.append( + measurement_group_service.save_measurement_group( + measurement_group['measurementGroup'], + subscription["subscriptionName"])) + return sub_name, measurement_groups + + +def check_duplicate_fields(subscription_name): + """ + validates the subscription content if already present + and captures duplicate fields + + Args: + subscription_name (string): subscription name + + Raises: + InvalidDataException: exception containing the list of invalid data. + """ + + existing_subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == subscription_name).one_or_none()) + if existing_subscription is not None: + raise DuplicateDataException(f'subscription Name: {subscription_name} already exists.') + + +def save_subscription(subscription): + """ + Saves the subscription data + + Args: + subscription (dict): subscription model to be saved. + """ + subscription_model = SubscriptionModel(subscription_name=subscription["subscriptionName"], + status=AdministrativeState.LOCKED.value) + db.session.add(subscription_model) + return subscription_model + + +def validate_nf_filter(nf_filter): + """ + checks if the nf filter is valid + + Args: + nf_filter (dict): nf filter to validate + + Raises: + InvalidDataException: if no field is available in nf_filter + """ + for filter_name, filter_values in nf_filter.items(): + filter_values[:] = [x for x in filter_values if x.strip()] + if not [filter_name for filter_name, val in nf_filter.items() if len(val) > 0]: + raise InvalidDataException("At least one filter within nfFilter must not be empty") + + +def save_nf_filter(nf_filter, subscription_name): + """ + Saves the nf_filter data request + + Args: + nf_filter (dict) : network function filter to save + subscription_name (string) : subscription name to associate with nf filter. + """ + logger.info(f'Saving nf filter for subscription request: {subscription_name}') + 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) 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 f7b682d4..2404c8b8 100755 --- a/components/pm-subscription-handler/pmsh_service/mod/network_function.py +++ b/components/pm-subscription-handler/pmsh_service/mod/network_function.py @@ -19,7 +19,7 @@ import re from mod import logger, db -from mod.api.db_models import NetworkFunctionModel +from mod.api.db_models import NetworkFunctionModel, NetworkFunctionFilterModel class NetworkFunction: @@ -167,6 +167,20 @@ class NetworkFunctionFilter: self.model_names = kwargs.get('modelNames') self.regex_matcher = re.compile('|'.join(raw_regex for raw_regex in self.nf_names)) + @staticmethod + def get_network_function_filter(sub_name): + """Gets the network function filter from the Database + + Args: + sub_name (string): The name of the subscription + + Returns: + NetworkFunctionFilter: Returns network function filter for sub_name + """ + nf_filter = NetworkFunctionFilterModel.query.filter( + NetworkFunctionFilterModel.subscription_name == sub_name).one_or_none() + return NetworkFunctionFilter(**nf_filter.serialize()) + def is_nf_in_filter(self, nf): """Match the nf fields against values in Subscription.nfFilter diff --git a/components/pm-subscription-handler/pmsh_service/mod/pmsh_config.py b/components/pm-subscription-handler/pmsh_service/mod/pmsh_config.py index 9c282ab7..a6fe38ad 100644 --- a/components/pm-subscription-handler/pmsh_service/mod/pmsh_config.py +++ b/components/pm-subscription-handler/pmsh_service/mod/pmsh_config.py @@ -69,6 +69,9 @@ class AppConfig(metaclass=MetaSingleton): self.streams_subscribes = app_config['config'].get('streams_subscribes') # TODO: aaf_creds variable should be removed on code cleanup self.aaf_creds = {'aaf_id': self.aaf_id, 'aaf_pass': self.aaf_pass} + # TODO: changes under discussion once resolve is confirmed will be removed + self.operational_policy_name = 'pmsh-operational-policy' + self.control_loop_name = 'pmsh-control-loop' @staticmethod def get_instance(): diff --git a/components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py b/components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py index 6238a298..22654b82 100644 --- a/components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py +++ b/components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py @@ -78,7 +78,7 @@ class SubscriptionHandler: self.app_conf.subscription.fileBasedGP, self.app_conf.subscription.fileLocation, self.app_conf.subscription.measurementGroups) - nfs_in_aai = aai_client.get_pmsh_nfs_from_aai(self.app_conf) + nfs_in_aai = aai_client.get_pmsh_nfs_from_aai(self.app_conf, self.app_conf.nf_filter) self.app_conf.subscription.create_subscription_on_nfs(nfs_in_aai, self.mr_pub, self.app_conf) self.app_conf.subscription.update_subscription_status() 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..0b2f86f7 --- /dev/null +++ b/components/pm-subscription-handler/tests/data/create_subscription_request.json @@ -0,0 +1,60 @@ +{ + "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" + } + ] + } + }, + { + "measurementGroup": { + "measurementGroupName": "msrmt_grp_name1", + "fileBasedGP":15, + "fileLocation":"pm.xml", + "administrativeState": "UNLOCKED", + "measurementTypes": [ + { + "measurementType": "counter_a" + } + ], + "managedObjectDNsBasic": [ + { + "DN": "string" + } + ] + } + } + ] + } +} 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..e22b2303 --- /dev/null +++ b/components/pm-subscription-handler/tests/services/test_measurement_group_service.py @@ -0,0 +1,113 @@ +# ============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 json +import os +from unittest.mock import patch +from mod.network_function import NetworkFunction +from mod.pmsh_config import AppConfig +from mod import db +from tests.base_setup import BaseClassSetup +from mod.api.services import measurement_group_service +from mod.api.db_models import MeasurementGroupModel, NfMeasureGroupRelationalModel +from mod.subscription import SubNfState + + +class MeasurementGroupServiceTestCase(BaseClassSetup): + @classmethod + def setUpClass(cls): + super().setUpClass() + + def setUp(self): + super().setUp() + 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() + + @patch.object(AppConfig, 'publish_to_topic') + def test_publish_measurement_group(self, mock_mr): + super().setUpAppConf() + nf_1 = NetworkFunction(**{'nf_name': 'pnf_1', + 'ipv4_address': '204.120.0.15', + 'ipv6_address': '2001:db8:3333:4444:5555:6666:7777:8888', + 'model_invariant_id': 'some_id', + 'model_version_id': 'some_other_id'}, + sdnc_model_name='blah', + sdnc_model_version=1.0, ) + measurement_grp = MeasurementGroupModel('sub_publish', + 'msg_publish', 'UNLOCKED', + 15, 'pm.xml', [{"measurementType": "counter_a"}], + [{"DN": "string"}]) + measurement_group_service.publish_measurement_group( + 'sub_publish', measurement_grp, nf_1) + mock_mr.assert_called_once_with('policy_pm_publisher', + {'nfName': 'pnf_1', + 'ipAddress': '2001:db8:3333:4444:5555:6666:7777:8888', + 'blueprintName': 'blah', + 'blueprintVersion': 1.0, + 'policyName': 'pmsh-operational-policy', + 'changeType': 'CREATE', + 'closedLoopControlName': 'pmsh-control-loop', + 'subscription': + {'administrativeState': 'UNLOCKED', + 'subscriptionName': 'sub_publish', + 'fileBasedGP': 15, + 'fileLocation': 'pm.xml', + 'measurementGroup': + {'measurementGroupName': 'msg_publish', + 'measurementTypes': + [{"measurementType": "counter_a"}], + 'managedObjectDNsBasic': [{"DN": "string"}]}}}) + + def test_save_measurement_group(self): + subscription = json.loads(self.subscription_request)['subscription'] + mes_grp = subscription['measurementGroups'][0]['measurementGroup'] + measurement_group_service.save_measurement_group(mes_grp, "ExtraPM-All-gNB-R2B") + db.session.commit() + measurement_grp = (MeasurementGroupModel.query.filter( + MeasurementGroupModel.measurement_group_name == mes_grp['measurementGroupName'], + MeasurementGroupModel.subscription_name == 'ExtraPM-All-gNB-R2B').one_or_none()) + self.assertIsNotNone(measurement_grp) + + def test_apply_nf_to_measgroup(self): + measurement_group_service.apply_nf_to_measgroup("pnf_test", "measure_grp_name") + db.session.commit() + measurement_grp_rel = (NfMeasureGroupRelationalModel.query.filter( + NfMeasureGroupRelationalModel.measurement_grp_name == 'measure_grp_name', + NfMeasureGroupRelationalModel.nf_name == 'pnf_test').one_or_none()) + db.session.commit() + self.assertIsNotNone(measurement_grp_rel) + self.assertEqual(measurement_grp_rel.nf_measure_grp_status, + SubNfState.PENDING_CREATE.value) + + 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 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..6063a8bd --- /dev/null +++ b/components/pm-subscription-handler/tests/services/test_nf_service.py @@ -0,0 +1,105 @@ +# ============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 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 +from mod.network_function import NetworkFunctionFilter + + +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') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_capture_filtered_nfs(self, mock_filter_call, 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'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + filtered_nfs = nf_service.capture_filtered_nfs(subscription["subscriptionName"]) + 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') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_create_nf_event_body(self, mock_filter_call, 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'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + nf = nf_service.capture_filtered_nfs(subscription["subscriptionName"])[0] + event_body = nf_service.create_nf_event_body(nf, 'CREATE') + self.assertEqual(event_body['nfName'], nf.nf_name) + self.assertEqual(event_body['ipAddress'], nf.ipv6_address) + self.assertEqual(event_body['blueprintName'], nf.sdnc_model_name) + self.assertEqual(event_body['blueprintVersion'], nf.sdnc_model_version) + self.assertEqual(event_body['policyName'], + self.app_conf.operational_policy_name) + self.assertEqual(event_body['changeType'], 'CREATE') + self.assertEqual(event_body['closedLoopControlName'], + self.app_conf.control_loop_name) + + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_save_nf_new_nf(self, mock_filter_call, 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'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + nf = nf_service.capture_filtered_nfs(subscription["subscriptionName"])[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) 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..dca6d871 --- /dev/null +++ b/components/pm-subscription-handler/tests/services/test_subscription_service.py @@ -0,0 +1,353 @@ +# ============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 copy +import json +import os +from unittest.mock import patch, MagicMock +from mod.api.db_models import SubscriptionModel, MeasurementGroupModel, \ + NfMeasureGroupRelationalModel, NetworkFunctionModel, NfSubRelationalModel, \ + convert_db_string_to_list +from mod.network_function import NetworkFunctionFilter +from mod.subscription import SubNfState +from mod import aai_client +from mod.api.custom_exception import DuplicateDataException, InvalidDataException +from mod.pmsh_config import AppConfig +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() + 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.subscription_service.save_nf_filter', MagicMock(return_value=None)) + @patch('mod.pmsh_config.AppConfig.publish_to_topic', MagicMock(return_value=None)) + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_create_subscription(self, mock_filter_call, 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 = json.loads(subscription)['subscription'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + subscription_service.create_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.subscription_service.save_nf_filter', MagicMock(return_value=None)) + @patch.object(AppConfig, 'publish_to_topic') + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_create_subscription_service_failed_rollback(self, mock_filter_call, mock_model_aai, + mock_aai, mock_publish): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + mock_publish.side_effect = InvalidDataException(["publish failed"]) + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-fail1', 'msrmt_grp_name-fail1') + subscription = json.loads(subscription)['subscription'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + try: + subscription_service.create_subscription(subscription) + except InvalidDataException as exception: + self.assertEqual(exception.invalid_message, ["AAI call failed"]) + + # Checking Rollback on publish failure with subscription and nfs captured + existing_subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-fail1').one_or_none()) + self.assertIsNotNone(existing_subscription) + saved_nf_sub_rel = (NfSubRelationalModel.query.filter( + NfSubRelationalModel.subscription_name == 'xtraPM-All-gNB-R2B-fail1')) + self.assertIsNotNone(saved_nf_sub_rel) + + @patch('mod.api.services.subscription_service.save_nf_filter', MagicMock(return_value=None)) + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_create_subscription_service_on_aai_failed(self, mock_filter_call, mock_aai): + mock_aai.side_effect = InvalidDataException(["AAI call failed"]) + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-fail', 'msrmt_grp_name-fail') + subscription = json.loads(subscription)['subscription'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + try: + subscription_service.create_subscription(subscription) + except InvalidDataException as exception: + self.assertEqual(exception.invalid_message, ["AAI call failed"]) + + # Checking Rollback on AAI failure with subscription request saved + existing_subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-fail').one_or_none()) + self.assertIsNotNone(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_field_info, + "subscription Name: ExtraPM-All-gNB-R2B already exists.") + + def test_missing_measurement_grp_name(self): + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-fail', '') + try: + subscription_service.create_subscription(json.loads(subscription)['subscription']) + except InvalidDataException as exception: + self.assertEqual(exception.invalid_message, + "No value provided for measurement group name") + + def test_missing_administrative_state(self): + subscription = json.loads(self.create_test_subs('sub-fail', 'measurement_grp_name-fail')) + mes_grp = subscription['subscription']['measurementGroups'][0]['measurementGroup'] + mes_grp['administrativeState'] = '' + try: + subscription_service.create_subscription(subscription['subscription']) + except InvalidDataException as exception: + self.assertEqual(exception.invalid_message, + "No value provided for administrative state") + + @patch.object(subscription_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(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(measurement_group_service, 'apply_nf_to_measgroup') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_apply_measurement_grp_to_nfs(self, mock_filter_call, mock_apply_nf, + 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) + 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'] + measurement_grp = MeasurementGroupModel('subscription_name_1', + 'msrmt_grp_name', 'UNLOCKED', + 15, 'pm.xml', [], []) + measurement2 = self.create_measurement_grp(measurement_grp, 'meas2', 'UNLOCKED') + measurement3 = self.create_measurement_grp(measurement_grp, 'meas3', 'LOCKED') + measurement_grps = [measurement_grp, measurement2, measurement3] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + filtered_nfs = nf_service.capture_filtered_nfs(subscription["subscriptionName"]) + subscription_service.apply_measurement_grp_to_nfs(filtered_nfs, measurement_grps) + # 2 measurement group with 2 nfs each contribute 4 calls + self.assertEqual(mock_apply_nf.call_count, 4) + + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(AppConfig, 'publish_to_topic') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_publish_measurement_grp_to_nfs(self, mock_filter_call, mock_publish, + 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) + mock_publish.return_value = None + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new2', 'msrmt_grp_name-new2') + subscription = json.loads(subscription)['subscription'] + measurement_grp = MeasurementGroupModel('subscription_name_1', + 'msrmt_grp_name', 'UNLOCKED', + 15, 'pm.xml', [], []) + measurement2 = self.create_measurement_grp(measurement_grp, 'meas2', 'UNLOCKED') + measurement3 = self.create_measurement_grp(measurement_grp, 'meas3', 'UNLOCKED') + measurement_grps = [measurement_grp, measurement2, measurement3] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + filtered_nfs = nf_service.capture_filtered_nfs(subscription["subscriptionName"]) + subscription_service.publish_measurement_grp_to_nfs( + subscription["subscriptionName"], filtered_nfs, measurement_grps) + # Two unlocked measurement Group published + self.assertEqual(mock_publish.call_count, 6) + + patch.object(aai_client, 'get_aai_model_data') + + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(AppConfig, 'publish_to_topic') + @patch('mod.logger.error') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_publish_measurement_grp_to_nfs_failed(self, mock_filter_call, mock_logger, + mock_publish, 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) + mock_publish.side_effect = Exception('Publish failed') + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new2', 'msrmt_grp_name-new2') + subscription = json.loads(subscription)['subscription'] + measurement_grp = MeasurementGroupModel('subscription_name_1', + 'msrmt_grp_name', 'UNLOCKED', + 15, 'pm.xml', [], []) + measurement2 = self.create_measurement_grp(measurement_grp, 'meas2', 'UNLOCKED') + measurement3 = self.create_measurement_grp(measurement_grp, 'meas3', 'LOCKED') + measurement_grps = [measurement_grp, measurement2, measurement3] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + filtered_nfs = nf_service.capture_filtered_nfs(subscription["subscriptionName"]) + subscription_service.publish_measurement_grp_to_nfs( + subscription["subscriptionName"], filtered_nfs, measurement_grps) + mock_logger.assert_called_with('Publish event failed for nf name, measure_grp_name, ' + 'sub_name: pnf_33_ericsson,meas2, xtraPM-All-gNB-R2B-new2 ' + 'with error: Publish failed') + + @patch('mod.api.services.subscription_service.save_nf_filter', MagicMock(return_value=None)) + @patch('mod.pmsh_config.AppConfig.publish_to_topic', MagicMock(return_value=None)) + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + @patch('mod.logger.error') + def test_create_subscription_all_locked_msg_grp(self, mock_logger, mock_filter_call, + 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-new2', 'msrmt_grp_name-new2') + subscription = subscription.replace('UNLOCKED', 'LOCKED') + subscription = json.loads(subscription)['subscription'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + subscription_service.create_subscription(subscription) + mock_logger.assert_called_with('All measurement groups are locked for subscription: ' + 'xtraPM-All-gNB-R2B-new2, please verify/check' + ' measurement groups.') + + def create_measurement_grp(self, measurement, measurement_name, admin_status): + new_measurement = copy.deepcopy(measurement) + measurement.measurement_group_name = measurement_name + new_measurement.administrative_state = admin_status + return new_measurement + + @patch('mod.api.services.subscription_service.save_nf_filter', MagicMock(return_value=None)) + @patch('mod.pmsh_config.AppConfig.publish_to_topic', MagicMock(return_value=None)) + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_save_filtered_nfs(self, mock_filter_call, 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 = json.loads(subscription)['subscription'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + filtered_nfs = nf_service.capture_filtered_nfs(subscription["subscriptionName"]) + subscription_service.save_filtered_nfs(filtered_nfs) + + for nf in filtered_nfs: + saved_nf = (NetworkFunctionModel.query.filter( + NetworkFunctionModel.nf_name == nf.nf_name).one_or_none()) + self.assertIsNotNone(saved_nf) + + @patch('mod.api.services.subscription_service.save_nf_filter', MagicMock(return_value=None)) + @patch('mod.pmsh_config.AppConfig.publish_to_topic', MagicMock(return_value=None)) + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_apply_subscription_to_nfs(self, mock_filter_call, 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'] + mock_filter_call.return_value = NetworkFunctionFilter(**subscription["nfFilter"]) + filtered_nfs = nf_service.capture_filtered_nfs(subscription["subscriptionName"]) + subscription_service.apply_subscription_to_nfs(filtered_nfs, 'xtraPM-All-gNB-R2B') + + for nf in filtered_nfs: + saved_nf_sub_rel = (NfSubRelationalModel.query.filter( + NfSubRelationalModel.subscription_name == 'xtraPM-All-gNB-R2B', + NfSubRelationalModel.nf_name == nf.nf_name).one_or_none()) + self.assertIsNotNone(saved_nf_sub_rel) + + def test_check_missing_data_sub_name_missing(self): + subscription = self.create_test_subs('', 'msrmt_grp_name-new') + subscription = json.loads(subscription)['subscription'] + try: + subscription_service.check_missing_data(subscription) + except InvalidDataException as invalidEx: + self.assertEqual(invalidEx.invalid_message, "No value provided in subscription name") + + def test_check_missing_data_admin_status_missing(self): + subscription = self.subscription_request.replace( + 'UNLOCKED', '') + subscription = json.loads(subscription)['subscription'] + try: + subscription_service.check_missing_data(subscription) + except InvalidDataException as invalidEx: + self.assertEqual(invalidEx.invalid_message, + "No value provided for administrative state") + + def test_check_missing_data_msr_grp_name(self): + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new', '') + subscription = json.loads(subscription)['subscription'] + try: + subscription_service.check_missing_data(subscription) + except InvalidDataException as invalidEx: + self.assertEqual(invalidEx.invalid_message, + "No value provided for measurement group name") + + def test_validate_nf_filter_with_no_filter_values(self): + nfFilter = '{"nfNames": [],"modelInvariantIDs": [], ' \ + '"modelVersionIDs": [],"modelNames": []}' + try: + subscription_service.validate_nf_filter(json.loads(nfFilter)) + except InvalidDataException as invalidEx: + self.assertEqual(invalidEx.invalid_message, + "At least one filter within nfFilter must not be empty") + + def test_db_string_to_list(self): + db_string = '{"*pnf","*vnf"}' + db_array = convert_db_string_to_list(db_string) + self.assertEqual(len(db_array), 2) + + def test_db_string_to_list_empty(self): + db_string = '{}' + db_array = convert_db_string_to_list(db_string) + self.assertEqual(len(db_array), 0) diff --git a/components/pm-subscription-handler/tests/test_aai_service.py b/components/pm-subscription-handler/tests/test_aai_service.py index 27c0f402..97f400c1 100644 --- a/components/pm-subscription-handler/tests/test_aai_service.py +++ b/components/pm-subscription-handler/tests/test_aai_service.py @@ -1,5 +1,5 @@ # ============LICENSE_START=================================================== -# Copyright (C) 2019-2020 Nordix Foundation. +# 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. @@ -57,7 +57,7 @@ class AaiClientTestCase(BaseClassSetup): mock_get_session.return_value.status_code = 200 mock_get_session.return_value.text = self.good_model_info mock_get_sdnc_params.return_value = True - xnfs = aai_client.get_pmsh_nfs_from_aai(self.app_conf) + xnfs = aai_client.get_pmsh_nfs_from_aai(self.app_conf, self.app_conf.nf_filter) self.assertEqual(self.app_conf.subscription.subscriptionName, 'ExtraPM-All-gNB-R2B') self.assertEqual(self.app_conf.subscription.administrativeState, 'UNLOCKED') self.assertEqual(len(xnfs), 3) @@ -67,7 +67,7 @@ class AaiClientTestCase(BaseClassSetup): mock_session.return_value.status_code = 404 with mock.patch('mod.aai_client._get_all_aai_nf_data', return_value=None): with self.assertRaises(RuntimeError): - aai_client.get_pmsh_nfs_from_aai(self.app_conf) + aai_client.get_pmsh_nfs_from_aai(self.app_conf, self.app_conf.nf_filter) @responses.activate def test_aai_client_get_all_aai_xnf_data_not_found(self): diff --git a/components/pm-subscription-handler/tests/test_controller.py b/components/pm-subscription-handler/tests/test_controller.py index c38cd976..a3a28163 100755 --- a/components/pm-subscription-handler/tests/test_controller.py +++ b/components/pm-subscription-handler/tests/test_controller.py @@ -1,5 +1,5 @@ # ============LICENSE_START=================================================== -# Copyright (C) 2019-2020 Nordix Foundation. +# 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. @@ -17,14 +17,15 @@ # ============LICENSE_END===================================================== import json import os -from unittest.mock import patch - +from unittest.mock import patch, MagicMock import responses from requests import Session - from mod import aai_client -from mod.api.controller import status, get_all_sub_to_nf_relations +from mod.api.controller import status, get_all_sub_to_nf_relations, post_subscription from tests.base_setup import BaseClassSetup +from mod.api.db_models import SubscriptionModel, NfMeasureGroupRelationalModel +from mod.subscription import SubNfState +from mod.network_function import NetworkFunctionFilter class ControllerTestCase(BaseClassSetup): @@ -35,10 +36,14 @@ class ControllerTestCase(BaseClassSetup): def setUp(self): super().setUp() + super().setUpAppConf() 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() + with open(os.path.join(os.path.dirname(__file__), + 'data/create_subscription_request.json'), 'r') as data: + self.subscription_request = data.read() def tearDown(self): super().tearDown() @@ -62,10 +67,65 @@ class ControllerTestCase(BaseClassSetup): '7129e420-d396-4efb-af02-6b83499b12f8/model-vers/model-ver/' 'e80a6ae3-cafd-4d24-850d-e14c084a5ca9', json=json.loads(self.good_model_info), status=200) - self.xnfs = aai_client.get_pmsh_nfs_from_aai(self.app_conf) + self.xnfs = aai_client.get_pmsh_nfs_from_aai(self.app_conf, self.app_conf.nf_filter) sub_model = self.app_conf.subscription.get() for nf in self.xnfs: self.app_conf.subscription.add_network_function_to_subscription(nf, sub_model) all_subs = get_all_sub_to_nf_relations() self.assertEqual(len(all_subs[0]['network_functions']), 3) self.assertEqual(all_subs[0]['subscription_name'], 'ExtraPM-All-gNB-R2B') + + 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.subscription_service.save_nf_filter', MagicMock(return_value=None)) + @patch('mod.pmsh_config.AppConfig.publish_to_topic', MagicMock(return_value=None)) + @patch.object(aai_client, '_get_all_aai_nf_data') + @patch.object(aai_client, 'get_aai_model_data') + @patch.object(NetworkFunctionFilter, 'get_network_function_filter') + def test_post_subscription(self, mock_filter_call, 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-post', 'msrmt_grp_name-post') + subscription = json.loads(subscription) + mock_filter_call.return_value = NetworkFunctionFilter( + **subscription['subscription']["nfFilter"]) + sub_name = subscription['subscription']['subscriptionName'] + mes_grp = subscription['subscription']['measurementGroups'][0]['measurementGroup'] + mes_grp_name = mes_grp['measurementGroupName'] + response = post_subscription(subscription) + subscription = (SubscriptionModel.query.filter( + SubscriptionModel.subscription_name == sub_name).one_or_none()) + self.assertIsNotNone(subscription) + msr_grp_nf_rel = (NfMeasureGroupRelationalModel.query.filter( + NfMeasureGroupRelationalModel.measurement_grp_name == mes_grp_name)).all() + for published_event in msr_grp_nf_rel: + self.assertEqual(published_event.nf_measure_grp_status, + SubNfState.PENDING_CREATE.value) + self.assertEqual(response[1], 201) + + def test_post_subscription_duplicate_sub(self): + # Posting the same subscription request stored in previous test to get duplicate response + response = post_subscription(json.loads(self.subscription_request)) + self.assertEqual(response[1], 409) + self.assertEqual(response[0], 'subscription Name: ExtraPM-All-gNB-R2B already exists.') + + def test_post_subscription_invalid_filter(self): + subscription = self.create_test_subs('xtraPM-All-gNB-R2B-invalid', 'msrmt_grp_name-invalid') + subscription = json.loads(subscription) + subscription['subscription']['nfFilter']['nfNames'] = [] + subscription['subscription']['nfFilter']['modelInvariantIDs'] = [] + subscription['subscription']['nfFilter']['modelVersionIDs'] = [] + subscription['subscription']['nfFilter']['modelNames'] = [] + response = post_subscription(subscription) + self.assertEqual(response[1], 400) + self.assertEqual(response[0], 'At least one filter within nfFilter must not be empty') + + def test_post_subscription_missing(self): + subscription = json.loads(self.subscription_request) + subscription['subscription']['subscriptionName'] = '' + response = post_subscription(subscription) + self.assertEqual(response[1], 400) + self.assertEqual(response[0], 'No value provided in subscription name') diff --git a/components/pm-subscription-handler/tests/test_subscription.py b/components/pm-subscription-handler/tests/test_subscription.py index 01c573e3..3921aa9b 100755 --- a/components/pm-subscription-handler/tests/test_subscription.py +++ b/components/pm-subscription-handler/tests/test_subscription.py @@ -48,7 +48,7 @@ class SubscriptionTest(BaseClassSetup): self.mock_mr_sub = Mock() self.mock_mr_pub = Mock() self.app_conf.subscription.create() - self.xnfs = aai_client.get_pmsh_nfs_from_aai(self.app_conf) + self.xnfs = aai_client.get_pmsh_nfs_from_aai(self.app_conf, self.app_conf.nf_filter) self.sub_model = self.app_conf.subscription.get() def tearDown(self): -- cgit 1.2.3-korg