From 5f69c24ad78121a2840b5299583791e557f8b535 Mon Sep 17 00:00:00 2001 From: SagarS Date: Thu, 24 Feb 2022 17:07:01 +0000 Subject: [PMSH] Update Filter API Issue-ID: DCAEGEN2-2922 Change-Id: Ibf0ef167642027429b3ba91daea60228cf5fa4c6 Signed-off-by: SagarS --- components/pm-subscription-handler/Changelog.md | 4 + .../pmsh_service/mod/api/controller.py | 33 +++ .../pmsh_service/mod/api/pmsh_swagger.yml | 31 ++- .../mod/api/services/measurement_group_service.py | 33 +++ .../pmsh_service/mod/api/services/nf_service.py | 21 +- .../mod/api/services/subscription_service.py | 226 +++++++++++++++++---- .../pmsh_service/mod/policy_response_handler.py | 23 ++- .../pmsh_service/mod/subscription.py | 3 +- components/pm-subscription-handler/pom.xml | 2 +- components/pm-subscription-handler/setup.py | 5 +- .../tests/data/create_subscription_request.json | 3 +- .../services/test_measurement_group_service.py | 45 ++++ .../tests/services/test_subscription_service.py | 179 +++++++++++++++- .../tests/test_controller.py | 32 ++- .../tests/test_policy_response_handler.py | 71 ++++++- .../pm-subscription-handler/version.properties | 4 +- 16 files changed, 650 insertions(+), 65 deletions(-) (limited to 'components/pm-subscription-handler') diff --git a/components/pm-subscription-handler/Changelog.md b/components/pm-subscription-handler/Changelog.md index d002f229..00e97783 100755 --- a/components/pm-subscription-handler/Changelog.md +++ b/components/pm-subscription-handler/Changelog.md @@ -5,6 +5,10 @@ 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/). +## [2.2.0] +### Changed +* Update Filter API (DCAEGEN2-2922) + ## [2.1.1] ### Changed * Fixes for Flask, MarkupSafe versions + tox (DCAEGEN2-3086) 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 de3aa5f3..57d3e021 100755 --- a/components/pm-subscription-handler/pmsh_service/mod/api/controller.py +++ b/components/pm-subscription-handler/pmsh_service/mod/api/controller.py @@ -267,3 +267,36 @@ def update_admin_state(subscription_name, measurement_group_name, body): f' due to Exception : {exception}', HTTPStatus.INTERNAL_SERVER_ERROR return response + + +def put_nf_filter(subscription_name, body): + """ + Performs network function filter update for the respective subscription + + Args: + subscription_name (String): Name of the subscription. + body (dict): Request body with nf filter data to update. + Returns: + string, HTTPStatus: Successfully updated network function Filter, 200 + string, HTTPStatus: Invalid request details, 400 + string, HTTPStatus: Cannot update as Locked/Filtering request is in progress, 409 + string, HTTPStatus: Exception details of server failure, 500 + """ + logger.info('Performing network function filter update for subscription ' + f'with sub name: {subscription_name} ') + response = 'Successfully updated network function Filter', HTTPStatus.OK.value + try: + subscription_service.update_filter(subscription_name, body) + except InvalidDataException as exception: + logger.error(exception.args[0]) + response = exception.args[0], HTTPStatus.BAD_REQUEST.value + except DataConflictException as exception: + logger.error(exception.args[0]) + response = exception.args[0], HTTPStatus.CONFLICT.value + except Exception as exception: + logger.error('Update nf filter request was not processed for sub name: ' + f'{subscription_name} due to Exception : {exception}') + response = 'Update nf filter request was not processed for sub name: ' \ + f'{subscription_name} due to Exception : {exception}', \ + HTTPStatus.INTERNAL_SERVER_ERROR + return response 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 274e0ebb..1f24f171 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 @@ -131,6 +131,33 @@ paths: 500: description: Exception occurred on the server + /subscription/{subscription_name}/nfFilter: + put: + tags: + - "Subscription" + description: >- + Update a Subscription nf filter + operationId: mod.api.controller.put_nf_filter + parameters: + - name: subscription_name + in: path + required: true + description: The name of the subscription to update nf filters + type: string + - in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/nfFilter" + responses: + 201: + description: Successfully updated nf filter + 409: + description: Conflicting data + 400: + description: Invalid input + 500: + description: Exception occurred while querying database /subscription/{subscription_name}/measurementGroups/{measurement_group_name}: get: @@ -162,9 +189,9 @@ paths: delete: description: Delete a measurement group - operationId: mod.api.controller.delete_meas_group + operationId: mod.api.controller.delete_meas_group_by_name tags: - - "measurement group" + - "Measurement Group" parameters: - name : subscription_name in: path 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 index b272e5b8..07d1b642 100644 --- 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 @@ -24,6 +24,7 @@ from mod.api.services import nf_service, subscription_service from mod.network_function import NetworkFunction from mod.pmsh_config import MRTopic, AppConfig from mod.subscription import AdministrativeState, SubNfState +from sqlalchemy import or_ def save_measurement_group(measurement_group, subscription_name): @@ -318,3 +319,35 @@ def update_admin_status(measurement_group, status): deactivate_nfs(sub_model, measurement_group, nf_meas_relations) elif status == AdministrativeState.UNLOCKED.value: activate_nfs(sub_model, measurement_group) + + +def filter_nf_to_meas_grp(nf_name, measurement_group_name, status): + """ Performs successful status update for a nf under filter update + request for a particular subscription and measurement group + + Args: + nf_name (string): The network function name + measurement_group_name (string): Measurement group name + status (string): status of the network function for measurement group + """ + try: + if status == SubNfState.DELETED.value: + delete_nf_to_measurement_group(nf_name, measurement_group_name, + SubNfState.DELETED.value) + elif status == SubNfState.CREATED.value: + update_measurement_group_nf_status(measurement_group_name, + SubNfState.CREATED.value, nf_name) + nf_measurement_group_rels = NfMeasureGroupRelationalModel.query.filter( + NfMeasureGroupRelationalModel.measurement_grp_name == measurement_group_name, + or_(NfMeasureGroupRelationalModel.nf_measure_grp_status.like('PENDING_%'), + NfMeasureGroupRelationalModel.nf_measure_grp_status.like('%_FAILED')) + ).all() + if not nf_measurement_group_rels: + MeasurementGroupModel.query.filter( + MeasurementGroupModel.measurement_group_name == measurement_group_name). \ + update({MeasurementGroupModel.administrative_state: AdministrativeState. + UNLOCKED.value}, synchronize_session='evaluate') + db.session.commit() + except Exception as e: + logger.error('Failed update filter response for measurement group name: ' + f'{measurement_group_name}, nf name: {nf_name} due to: {e}') 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 index ce463ed0..a3c2a036 100644 --- 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 @@ -17,7 +17,7 @@ # ============LICENSE_END===================================================== from mod import db, aai_client, logger -from mod.api.db_models import NetworkFunctionModel +from mod.api.db_models import NetworkFunctionModel, NetworkFunctionFilterModel from mod.pmsh_config import AppConfig from mod.network_function import NetworkFunctionFilter @@ -74,3 +74,22 @@ def save_nf(nf): sdnc_model_name=nf.sdnc_model_name, sdnc_model_version=nf.sdnc_model_version) db.session.add(network_function) + + +def save_nf_filter_update(sub_name, nf_filter): + """ + Updates the network function filter for the subscription in the db + + Args: + sub_name (String): Name of the Subscription + nf_filter (dict): filter object to update in the subscription + """ + NetworkFunctionFilterModel.query.filter( + NetworkFunctionFilterModel.subscription_name == sub_name). \ + update({NetworkFunctionFilterModel.nf_names: nf_filter['nfNames'], + NetworkFunctionFilterModel.model_invariant_ids: nf_filter['modelInvariantIDs'], + NetworkFunctionFilterModel.model_version_ids: nf_filter['modelVersionIDs'], + NetworkFunctionFilterModel.model_names: nf_filter['modelNames']}, + synchronize_session='evaluate') + db.session.commit() + logger.info(f'Successfully saved filter for subscription: {sub_name}') 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 index 338ab89e..032fc4a0 100644 --- 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 @@ -18,9 +18,11 @@ from mod import db, logger from mod.api.db_models import SubscriptionModel, NfSubRelationalModel, \ - NetworkFunctionFilterModel, NetworkFunctionModel, MeasurementGroupModel + NetworkFunctionFilterModel, NetworkFunctionModel, MeasurementGroupModel, \ + NfMeasureGroupRelationalModel from mod.api.services import measurement_group_service, nf_service -from mod.api.custom_exception import InvalidDataException, DuplicateDataException +from mod.api.custom_exception import InvalidDataException, DuplicateDataException, \ + DataConflictException from mod.subscription import AdministrativeState, SubNfState from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload @@ -40,32 +42,16 @@ def create_subscription(subscription): logger.info(f'Initiating create subscription for: {subscription["subscriptionName"]}') perform_validation(subscription) try: - sub_model, measurement_groups = save_subscription_request(subscription) + sub_model = 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_model.subscription_name) - if filtered_nfs: - logger.info(f'Applying the filtered nfs for subscription: ' - f'{sub_model.subscription_name}') - save_filtered_nfs(filtered_nfs) - apply_subscription_to_nfs(filtered_nfs, sub_model.subscription_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_model, filtered_nfs, - unlocked_msmt_groups) - else: - logger.error(f'All measurement groups are locked for subscription: ' - f'{sub_model.subscription_name}, ' - f'please verify/check measurement groups.') - else: - logger.error(f'No network functions found for subscription: ' - f'{sub_model.subscription_name}, ' - f'please verify/check NetworkFunctionFilter.') + unlocked_mgs = get_unlocked_measurement_grps(sub_model) + add_new_filtered_nfs(filtered_nfs, unlocked_mgs, sub_model) except IntegrityError as e: db.session.rollback() - raise DuplicateDataException(f'DB Integrity issue encountered: {e.orig.args[0]}') + raise DuplicateDataException(f'DB Integrity issue encountered: {e.orig.args[0]}') from e except Exception as e: db.session.rollback() raise e @@ -73,6 +59,36 @@ def create_subscription(subscription): db.session.remove() +def add_new_filtered_nfs(filtered_nfs, unlocked_mgs, sub_model): + """ + Inserts the filtered nfs in measurement groups of subscription + + Args: + filtered_nfs (List[NetworkFunction]): nfs to be inserted + unlocked_mgs (List[MeasurementGroupModel]): mgs to be updated with new nfs + sub_model (SubscriptionModel): subscription model to update + """ + if filtered_nfs: + logger.info(f'Applying the filtered nfs for subscription: ' + f'{sub_model.subscription_name}') + save_filtered_nfs(filtered_nfs) + apply_subscription_to_nfs(filtered_nfs, sub_model.subscription_name) + db.session.commit() + if unlocked_mgs: + apply_measurement_grp_to_nfs(filtered_nfs, unlocked_mgs) + db.session.commit() + publish_measurement_grp_to_nfs(sub_model, filtered_nfs, + unlocked_mgs) + else: + logger.error(f'All measurement groups are locked for subscription: ' + f'{sub_model.subscription_name}, ' + f'please verify/check measurement groups.') + else: + logger.error(f'No network functions found for subscription: ' + f'{sub_model.subscription_name}, ' + f'please verify/check NetworkFunctionFilter.') + + def publish_measurement_grp_to_nfs(sub_model, filtered_nfs, measurement_groups): """ @@ -124,32 +140,22 @@ def apply_subscription_to_nfs(filtered_nfs, subscription_name): db.session.add(new_nf_sub_rel) -def apply_measurement_grp_to_nfs(filtered_nfs, measurement_groups): +def apply_measurement_grp_to_nfs(filtered_nfs, unlocked_mgs): """ 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 + unlocked_mgs (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_status_to_measurement_group( - nf.nf_name, measurement_group.measurement_group_name, - SubNfState.PENDING_CREATE.value) - else: - logger.info(f'No nfs added as measure_grp_name: ' - f'{measurement_group.measurement_group_name} is LOCKED') - return unlocked_msmt_groups + for measurement_group in unlocked_mgs: + 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_status_to_measurement_group( + nf.nf_name, measurement_group.measurement_group_name, + SubNfState.PENDING_CREATE.value) def check_missing_data(subscription): @@ -217,7 +223,7 @@ def save_subscription_request(subscription): measurement_group_service.save_measurement_group( measurement_group['measurementGroup'], subscription["subscriptionName"])) - return sub_model, measurement_groups + return sub_model def check_duplicate_fields(subscription_name): @@ -380,3 +386,139 @@ def query_to_delete_subscription_by_name(subscription_name): .filter_by(subscription_name=subscription_name).delete() db.session.commit() return effected_rows + + +def is_duplicate_filter(nf_filter, db_network_filter): + """ + Checks if the network function filter is unchanged for the subscription + + Args: + nf_filter (dict): filter object to update in the subscription + db_network_filter (NetworkFunctionFilterModel): nf filter object from db + + Returns: + (boolean) : True is nf filters are same else False + """ + return nf_filter == db_network_filter.serialize() + + +def update_filter(sub_name, nf_filter): + """ + Updates the network function filter for the subscription + + Args: + sub_name (String): Name of the Subscription + nf_filter (dict): filter object to update in the subscription + + Returns: + InvalidDataException: contains details on invalid fields + DataConflictException: contains details on conflicting state of a field + Exception: contains runtime error details + """ + sub_model = query_subscription_by_name(sub_name) + if sub_model is None: + raise InvalidDataException('Requested subscription is not available ' + f'with sub name: {sub_name} for nf filter update') + if is_duplicate_filter(nf_filter, sub_model.network_filter): + raise InvalidDataException('Duplicate nf filter update requested for subscription ' + f'with sub name: {sub_name}') + validate_sub_mgs_state(sub_model) + nf_service.save_nf_filter_update(sub_name, nf_filter) + del_nfs, new_nfs = extract_del_new_nfs(sub_model) + NfSubRelationalModel.query.filter( + NfSubRelationalModel.subscription_name == sub_name, + NfSubRelationalModel.nf_name.in_(del_nfs)).delete() + db.session.commit() + unlocked_mgs = get_unlocked_measurement_grps(sub_model) + if unlocked_mgs: + add_new_filtered_nfs(new_nfs, unlocked_mgs, sub_model) + delete_filtered_nfs(del_nfs, sub_model, unlocked_mgs) + db.session.remove() + + +def get_unlocked_measurement_grps(sub_model): + """ + Gets unlocked measurement groups and logs locked measurement groups + + Args: + sub_model (SubscriptionModel): Subscription model to perform nfs delete + + Returns: + unlocked_mgs (List[MeasurementGroupModel]): unlocked msgs in a subscription + + """ + unlocked_mgs = [] + for measurement_group in sub_model.measurement_groups: + if measurement_group.administrative_state \ + == AdministrativeState.UNLOCKED.value: + unlocked_mgs.append(measurement_group) + else: + logger.info(f'No nfs added as measure_grp_name: ' + f'{measurement_group.measurement_group_name} is LOCKED') + return unlocked_mgs + + +def delete_filtered_nfs(del_nfs, sub_model, unlocked_mgs): + """ + Removes unfiltered nfs + + Args: + del_nfs (List[String]): Names of nfs to be deleted + sub_model (SubscriptionModel): Subscription model to perform nfs delete + unlocked_mgs (List[MeasurementGroupModel]): unlocked msgs to perform nfs delete + + """ + if del_nfs: + logger.info(f'Removing nfs from subscription: ' + f'{sub_model.subscription_name}') + for mg in unlocked_mgs: + MeasurementGroupModel.query.filter( + MeasurementGroupModel.measurement_group_name == mg.measurement_group_name) \ + .update({MeasurementGroupModel.administrative_state: AdministrativeState. + FILTERING.value}, synchronize_session='evaluate') + db.session.commit() + nf_meas_relations = NfMeasureGroupRelationalModel.query.filter( + NfMeasureGroupRelationalModel.measurement_grp_name == mg. + measurement_group_name, NfMeasureGroupRelationalModel. + nf_name.in_(del_nfs)).all() + measurement_group_service.deactivate_nfs(sub_model, mg, nf_meas_relations) + + +def extract_del_new_nfs(sub_model): + """ + Captures nfs to be deleted and created for the subscription + + Args: + sub_model (SubscriptionModel): Subscription model to perform nfs delete + + Returns: + del_nfs (List[String]): Names of nfs to be deleted + new_nfs (List[NetworkFunction]): nfs to be inserted + """ + filtered_nfs = nf_service.capture_filtered_nfs(sub_model.subscription_name) + filtered_nf_names = [nf.nf_name for nf in filtered_nfs] + existing_nf_names = [nf.nf_name for nf in sub_model.nfs] + new_nfs = list(filter(lambda x: x.nf_name not in existing_nf_names, filtered_nfs)) + del_nfs = [nf.nf_name for nf in sub_model.nfs if nf.nf_name not in filtered_nf_names] + return del_nfs, new_nfs + + +def validate_sub_mgs_state(sub_model): + """ + Validates if any measurement group in subscription has + status Locking or Filtering + + Args: + sub_model (SubscriptionModel): Subscription model to perform validation before nf filter + + Returns: + DataConflictException: contains details on conflicting status in measurement group + """ + mg_names_processing = [mg for mg in sub_model.measurement_groups + if mg.administrative_state in [AdministrativeState.FILTERING.value, + AdministrativeState.LOCKING.value]] + if mg_names_processing: + raise DataConflictException('Cannot update filter as subscription: ' + f'{sub_model.subscription_name} is under ' + 'transitioning state for the following measurement ' + f'groups: {mg_names_processing}') diff --git a/components/pm-subscription-handler/pmsh_service/mod/policy_response_handler.py b/components/pm-subscription-handler/pmsh_service/mod/policy_response_handler.py index a0a7bd67..5065ce8a 100644 --- a/components/pm-subscription-handler/pmsh_service/mod/policy_response_handler.py +++ b/components/pm-subscription-handler/pmsh_service/mod/policy_response_handler.py @@ -18,8 +18,8 @@ import json from mod.pmsh_config import MRTopic, AppConfig from mod import logger -from mod.subscription import AdministrativeState, subscription_nf_states -from mod.api.db_models import MeasurementGroupModel +from mod.subscription import AdministrativeState, subscription_nf_states, SubNfState +from mod.api.db_models import MeasurementGroupModel, NfMeasureGroupRelationalModel from mod.api.services import measurement_group_service policy_response_handle_functions = { @@ -34,6 +34,10 @@ policy_response_handle_functions = { AdministrativeState.LOCKING.value: { 'success': measurement_group_service.lock_nf_to_meas_grp, 'failed': measurement_group_service.update_measurement_group_nf_status + }, + AdministrativeState.FILTERING.value: { + 'success': measurement_group_service.filter_nf_to_meas_grp, + 'failed': measurement_group_service.update_measurement_group_nf_status } } @@ -86,8 +90,19 @@ class PolicyResponseHandler: logger.info(f'Response from MR: measurement group name: {measurement_group_name} for ' f'NF: {nf_name} received, updating the DB') try: - nf_measure_grp_status = subscription_nf_states[administrative_state][response_message]\ - .value + + if administrative_state == AdministrativeState.FILTERING.value: + nf_msg_rel = NfMeasureGroupRelationalModel.query.filter( + NfMeasureGroupRelationalModel.measurement_grp_name == measurement_group_name, + NfMeasureGroupRelationalModel.nf_name == nf_name + ).one_or_none() + if nf_msg_rel.nf_measure_grp_status == SubNfState.PENDING_DELETE.value: + administrative_state = AdministrativeState.LOCKING.value + elif nf_msg_rel.nf_measure_grp_status == SubNfState.PENDING_CREATE.value: + administrative_state = AdministrativeState.UNLOCKED.value + + nf_measure_grp_status = (subscription_nf_states[administrative_state] + [response_message]).value policy_response_handle_functions[administrative_state][response_message]( measurement_group_name=measurement_group_name, status=nf_measure_grp_status, nf_name=nf_name) diff --git a/components/pm-subscription-handler/pmsh_service/mod/subscription.py b/components/pm-subscription-handler/pmsh_service/mod/subscription.py index 603343f0..ddb6e1f5 100755 --- a/components/pm-subscription-handler/pmsh_service/mod/subscription.py +++ b/components/pm-subscription-handler/pmsh_service/mod/subscription.py @@ -1,5 +1,5 @@ # ============LICENSE_START=================================================== -# Copyright (C) 2019-2021 Nordix Foundation. +# Copyright (C) 2019-2022 Nordix Foundation. # ============================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ class AdministrativeState(Enum): UNLOCKED = 'UNLOCKED' LOCKING = 'LOCKING' LOCKED = 'LOCKED' + FILTERING = 'FILTERING' subscription_nf_states = { diff --git a/components/pm-subscription-handler/pom.xml b/components/pm-subscription-handler/pom.xml index 815b2f16..88ae50d5 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 - 2.1.1-SNAPSHOT + 2.2.0-SNAPSHOT UTF-8 . diff --git a/components/pm-subscription-handler/setup.py b/components/pm-subscription-handler/setup.py index ba1b74b9..a4a80efb 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="2.1.1", + version="2.2.0", packages=find_packages(), author="lego@est.tech", author_email="lego@est.tech", @@ -43,5 +43,6 @@ setup( "onap_dcae_cbs_docker_client==2.2.1", "onappylog==1.0.9", "ruamel.yaml==0.16.10", - "jsonschema==3.2.0"] + "jsonschema==3.2.0", + "pyyaml==5.4.1"] ) diff --git a/components/pm-subscription-handler/tests/data/create_subscription_request.json b/components/pm-subscription-handler/tests/data/create_subscription_request.json index bc089a9a..74c09374 100644 --- a/components/pm-subscription-handler/tests/data/create_subscription_request.json +++ b/components/pm-subscription-handler/tests/data/create_subscription_request.json @@ -10,7 +10,8 @@ ], "modelInvariantIDs": [ "8lk4578-d396-4efb-af02-6b83499b12f8", - "687kj45-d396-4efb-af02-6b83499b12f8" + "687kj45-d396-4efb-af02-6b83499b12f8", + "597b524-d396-4efb-af02-6b83499b12f8" ], "modelVersionIDs": [ 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 index f656513e..1dbe84a9 100644 --- a/components/pm-subscription-handler/tests/services/test_measurement_group_service.py +++ b/components/pm-subscription-handler/tests/services/test_measurement_group_service.py @@ -358,3 +358,48 @@ class MeasurementGroupServiceTestCase(BaseClassSetup): self.assertEqual(meas_grp.subscription_name, 'sub') self.assertEqual(meas_grp.measurement_group_name, 'MG2') self.assertEqual(meas_grp.administrative_state, 'LOCKING') + + def test_filter_nf_to_meas_grp_for_delete(self): + sub = create_subscription_data('sub') + db.session.add(sub) + nf = NetworkFunction(nf_name='pnf_test2') + nf_service.save_nf(nf) + measurement_group_service.apply_nf_status_to_measurement_group( + "pnf_test2", "MG2", SubNfState.PENDING_DELETE.value) + db.session.commit() + measurement_group_service.filter_nf_to_meas_grp( + "pnf_test2", "MG2", SubNfState.DELETED.value) + measurement_grp_rel = (NfMeasureGroupRelationalModel.query.filter( + NfMeasureGroupRelationalModel.measurement_grp_name == 'MG2', + NfMeasureGroupRelationalModel.nf_name == 'pnf_test2').one_or_none()) + self.assertIsNone(measurement_grp_rel) + network_function = (NetworkFunctionModel.query.filter( + NetworkFunctionModel.nf_name == 'pnf_test2').one_or_none()) + self.assertIsNone(network_function) + meas_grp = measurement_group_service.query_meas_group_by_name('sub', 'MG2') + self.assertEqual(meas_grp.subscription_name, 'sub') + self.assertEqual(meas_grp.measurement_group_name, 'MG2') + self.assertEqual(meas_grp.administrative_state, 'UNLOCKED') + + def test_filter_nf_to_meas_grp_for_create(self): + sub = create_subscription_data('sub') + db.session.add(sub) + nf = NetworkFunction(nf_name='pnf_test2') + nf_service.save_nf(nf) + measurement_group_service.apply_nf_status_to_measurement_group( + "pnf_test2", "MG2", SubNfState.PENDING_CREATE.value) + db.session.commit() + measurement_group_service.filter_nf_to_meas_grp( + "pnf_test2", "MG2", SubNfState.CREATED.value) + measurement_grp_rel = (NfMeasureGroupRelationalModel.query.filter( + NfMeasureGroupRelationalModel.measurement_grp_name == 'MG2', + NfMeasureGroupRelationalModel.nf_name == 'pnf_test2').one_or_none()) + self.assertIsNotNone(measurement_grp_rel) + self.assertEqual(measurement_grp_rel.nf_measure_grp_status, 'CREATED') + network_function = (NetworkFunctionModel.query.filter( + NetworkFunctionModel.nf_name == 'pnf_test2').one_or_none()) + self.assertIsNotNone(network_function) + meas_grp = measurement_group_service.query_meas_group_by_name('sub', 'MG2') + self.assertEqual(meas_grp.subscription_name, 'sub') + self.assertEqual(meas_grp.measurement_group_name, 'MG2') + self.assertEqual(meas_grp.administrative_state, 'UNLOCKED') diff --git a/components/pm-subscription-handler/tests/services/test_subscription_service.py b/components/pm-subscription-handler/tests/services/test_subscription_service.py index 807806f8..a0f3297c 100644 --- a/components/pm-subscription-handler/tests/services/test_subscription_service.py +++ b/components/pm-subscription-handler/tests/services/test_subscription_service.py @@ -21,13 +21,14 @@ import os from unittest.mock import patch, MagicMock from mod.api.db_models import SubscriptionModel, MeasurementGroupModel, \ NfMeasureGroupRelationalModel, NetworkFunctionModel, NfSubRelationalModel, \ - convert_db_string_to_list + convert_db_string_to_list, NetworkFunctionFilterModel 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 import aai_client, db +from mod.api.custom_exception import DuplicateDataException, InvalidDataException, \ + DataConflictException from mod.pmsh_config import AppConfig -from tests.base_setup import BaseClassSetup +from tests.base_setup import BaseClassSetup, create_subscription_data from mod.api.services import subscription_service, nf_service, measurement_group_service from tests.base_setup import create_multiple_subscription_data @@ -184,11 +185,10 @@ class SubscriptionServiceTestCase(BaseClassSetup): '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] + unlocked_msgs = [measurement_grp, measurement2] 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) + subscription_service.apply_measurement_grp_to_nfs(filtered_nfs, unlocked_msgs) # 2 measurement group with 2 nfs each contribute 4 calls self.assertEqual(mock_apply_nf.call_count, 4) @@ -377,3 +377,168 @@ class SubscriptionServiceTestCase(BaseClassSetup): def test_get_subscriptions_list_empty(self): subs = subscription_service.get_subscriptions_list() self.assertEqual(subs, []) + + @patch('mod.api.services.nf_service.save_nf_filter_update') + @patch('mod.api.services.subscription_service.is_duplicate_filter', + MagicMock(return_value=False)) + @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_update_filter(self, mock_filter_call, mock_model_aai, mock_aai, mock_update_filter): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + mock_update_filter.return_value = None + subscription = self.create_test_subs('sub_01', 'msg_01') + subscription = json.loads(subscription)['subscription'] + nf_filter = subscription['nfFilter'] + mock_filter_call.return_value = NetworkFunctionFilter(**nf_filter) + subscription_service.create_subscription(subscription) + subscription_service.update_filter('sub_01', nf_filter) + self.assertTrue(mock_update_filter.called) + + @patch('mod.api.services.nf_service.save_nf_filter_update') + @patch('mod.api.services.subscription_service.is_duplicate_filter', + MagicMock(return_value=False)) + @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_update_filter_with_new_del_nfs(self, mock_filter_call, mock_model_aai, mock_aai, + mock_update_filter): + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + mock_update_filter.return_value = None + subscription = self.create_test_subs('sub_01', 'msg_01') + subscription = json.loads(subscription)['subscription'] + nf_filter = subscription['nfFilter'] + mock_filter_call.return_value = NetworkFunctionFilter(**nf_filter) + subscription_service.create_subscription(subscription) + # Check existing network functions + meas_group_nfs = db.session.query(NfMeasureGroupRelationalModel).filter( + NfMeasureGroupRelationalModel.measurement_grp_name == 'msg_01') \ + .all() + self.assertEqual(len(meas_group_nfs), 2) + self.assertEqual(meas_group_nfs[0].nf_name, 'pnf201') + self.assertEqual(meas_group_nfs[0].nf_measure_grp_status, + SubNfState.PENDING_CREATE.value) + self.assertEqual(meas_group_nfs[1].nf_name, 'pnf_33_ericsson') + self.assertEqual(meas_group_nfs[1].nf_measure_grp_status, + SubNfState.PENDING_CREATE.value) + meas_grp = measurement_group_service.query_meas_group_by_name('sub_01', 'msg_01') + self.assertEqual(meas_grp.administrative_state, 'UNLOCKED') + # Creating test data for update filter function + aai_response = self.aai_response_data.replace('pnf201', 'xnf111') + mock_aai.return_value = json.loads(aai_response) + nf_filter['nfNames'] = ["^vnf.*", "^xnf.*"] + mock_filter_call.return_value = NetworkFunctionFilter(**nf_filter) + subscription_service.update_filter('sub_01', nf_filter) + self.assertTrue(mock_update_filter.called) + # Check updated network functions after filter change + meas_group_nfs = db.session.query(NfMeasureGroupRelationalModel).filter( + NfMeasureGroupRelationalModel.measurement_grp_name == 'msg_01') \ + .all() + self.assertEqual(meas_group_nfs[0].nf_name, 'pnf201') + self.assertEqual(meas_group_nfs[0].nf_measure_grp_status, + SubNfState.PENDING_DELETE.value) + self.assertEqual(meas_group_nfs[1].nf_name, 'pnf_33_ericsson') + self.assertEqual(meas_group_nfs[1].nf_measure_grp_status, + SubNfState.PENDING_DELETE.value) + self.assertEqual(meas_group_nfs[2].nf_name, 'xnf111') + self.assertEqual(meas_group_nfs[2].nf_measure_grp_status, + SubNfState.PENDING_CREATE.value) + meas_grp = measurement_group_service.query_meas_group_by_name('sub_01', 'msg_01') + self.assertEqual(meas_grp.administrative_state, 'FILTERING') + + def test_update_filter_locking(self): + sub = create_subscription_data('sub') + sub.measurement_groups[1].administrative_state = 'LOCKING' + db.session.add(sub) + try: + subscription_service.update_filter('sub', json.loads('{"nfNames": "^pnf.*"}')) + except DataConflictException as conflictEx: + self.assertEqual(conflictEx.args[0], + 'Cannot update filter as subscription: sub is under transitioning' + ' state for the following measurement groups: [subscription_name:' + ' sub, measurement_group_name: MG2, administrative_state: LOCKING,' + ' file_based_gp: 15, file_location: /pm/pm.xml, measurement_type: ' + '[{ "measurementType": "countera" }, { "measurementType": ' + '"counterb" }], managed_object_dns_basic: [{ "DN":"dna"},' + '{"DN":"dnb"}]]') + + def test_update_filter_filtering(self): + sub = create_subscription_data('sub') + sub.measurement_groups[1].administrative_state = 'FILTERING' + db.session.add(sub) + try: + subscription_service.update_filter('sub', json.loads('{"nfNames": "^pnf.*"}')) + except DataConflictException as conflictEx: + self.assertEqual(conflictEx.args[0], + 'Cannot update filter as subscription: sub is under transitioning' + ' state for the following measurement groups: [subscription_name:' + ' sub, measurement_group_name: MG2, administrative_state: FILTERING,' + ' file_based_gp: 15, file_location: /pm/pm.xml, measurement_type: [' + '{ "measurementType": "countera" }, { "measurementType": "counterb" ' + '}], managed_object_dns_basic: [{ "DN":"dna"},{"DN":"dnb"}]]') + + def test_update_filter_invalid_request(self): + try: + subscription_service.update_filter("sub3", json.loads('{"nfNames": "^pnf.*"}')) + except InvalidDataException as invalidEx: + self.assertEqual(invalidEx.args[0], + "Requested subscription is not available with sub name: sub3 " + "for nf filter update") + + @patch('mod.api.services.nf_service.save_nf_filter_update') + @patch('mod.api.services.subscription_service.is_duplicate_filter', + MagicMock(return_value=True)) + @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_update_filter_invalid_duplicate_request(self, mock_filter_call, mock_model_aai, + mock_aai, mock_update_filter): + try: + mock_aai.return_value = json.loads(self.aai_response_data) + mock_model_aai.return_value = json.loads(self.good_model_info) + mock_update_filter.return_value = None + subscription = self.create_test_subs('sub_01', 'msg_01') + subscription = json.loads(subscription)['subscription'] + nf_filter = subscription['nfFilter'] + mock_filter_call.return_value = NetworkFunctionFilter(**nf_filter) + subscription_service.create_subscription(subscription) + subscription_service.update_filter("sub_01", json.loads('{"nfNames": "^pnf.*"}')) + except InvalidDataException as invalidEx: + self.assertEqual(invalidEx.args[0], + "Duplicate nf filter update requested for subscription " + "with sub name: sub_01") + + def test_is_duplicate_filter_true(self): + subscription = self.create_test_subs('sub_01', 'msg_01') + subscription = json.loads(subscription)['subscription'] + nf_filter = subscription['nfFilter'] + db_network_filter = NetworkFunctionFilterModel('sub_01', + '{^pnf.*,^vnf.*}', + '{8lk4578-d396-4efb-af02-6b83499b12f8,' + '687kj45-d396-4efb-af02-6b83499b12f8,' + '597b524-d396-4efb-af02-6b83499b12f8}', + '{e80a6ae3-cafd-4d24-850d-e14c084a5ca9}', + '{PNF102}') + similar = subscription_service.is_duplicate_filter(nf_filter, db_network_filter) + self.assertTrue(similar) + + def test_is_duplicate_filter_false(self): + subscription = self.create_test_subs('sub_01', 'msg_01') + subscription = json.loads(subscription)['subscription'] + nf_filter = subscription['nfFilter'] + db_network_filter = NetworkFunctionFilterModel('sub_01', + '{^pnf.*,^vnf.*}', + '{8lk4578-d396-4efb-af02-6b83499b12f8,' + '687kj45-d396-4efb-af02-6b83499b12f8}', + '{e80a6ae3-cafd-4d24-850d-e14c084a5ca9}', + '{PNF102}') + similar = subscription_service.is_duplicate_filter(nf_filter, db_network_filter) + self.assertFalse(similar) diff --git a/components/pm-subscription-handler/tests/test_controller.py b/components/pm-subscription-handler/tests/test_controller.py index 42c52c09..7b0a8b19 100755 --- a/components/pm-subscription-handler/tests/test_controller.py +++ b/components/pm-subscription-handler/tests/test_controller.py @@ -23,7 +23,7 @@ from http import HTTPStatus from mod import aai_client, db from mod.api.controller import status, post_subscription, get_subscription_by_name, \ get_subscriptions, get_meas_group_with_nfs, delete_subscription_by_name, update_admin_state, \ - delete_meas_group_by_name + delete_meas_group_by_name, put_nf_filter from mod.api.services.measurement_group_service import query_meas_group_by_name from tests.base_setup import BaseClassSetup from mod.api.custom_exception import InvalidDataException, DataConflictException @@ -366,3 +366,33 @@ class ControllerTestCase(BaseClassSetup): self.assertEqual(status_code, HTTPStatus.INTERNAL_SERVER_ERROR.value) self.assertEqual(error, 'Update admin status request was not processed for sub name: sub4 ' 'and meas group name: MG2 due to Exception : Server Error') + + @patch('mod.api.services.subscription_service.update_filter', MagicMock(return_value=None)) + def test_put_nf_filter(self): + response = put_nf_filter('sub1', json.loads('{"nfNames": ["^pnf.*", "^vnf.*"]}')) + self.assertEqual(response[0], 'Successfully updated network function Filter') + self.assertEqual(response[1], HTTPStatus.OK.value) + + @patch('mod.api.services.subscription_service.update_filter', + MagicMock(side_effect=InvalidDataException('Bad request'))) + def test_put_nf_filter_api_invalid_data_exception(self): + error, status_code = put_nf_filter('sub1', + json.loads('{"nfNames": ["^pnf.*", "^vnf.*"]}')) + self.assertEqual(status_code, HTTPStatus.BAD_REQUEST.value) + self.assertEqual(error, 'Bad request') + + @patch('mod.api.services.subscription_service.update_filter', + MagicMock(side_effect=DataConflictException('Data conflict'))) + def test_put_nf_filter_api_data_conflict_exception(self): + error, status_code = put_nf_filter('sub1', + json.loads('{"nfNames": ["^pnf.*", "^vnf.*"]}')) + self.assertEqual(status_code, HTTPStatus.CONFLICT.value) + self.assertEqual(error, 'Data conflict') + + @patch('mod.api.services.subscription_service.update_filter', + MagicMock(side_effect=Exception('Server Error'))) + def test_put_nf_filter_api_exception(self): + error, status_code = put_nf_filter('sub1', json.loads('{"nfNames": ["^pnf.*", "^vnf.*"]}')) + self.assertEqual(status_code, HTTPStatus.INTERNAL_SERVER_ERROR.value) + self.assertEqual(error, 'Update nf filter request was not processed for sub name: sub1 ' + 'due to Exception : Server Error') diff --git a/components/pm-subscription-handler/tests/test_policy_response_handler.py b/components/pm-subscription-handler/tests/test_policy_response_handler.py index 3e6abf94..d5ae5ce1 100644 --- a/components/pm-subscription-handler/tests/test_policy_response_handler.py +++ b/components/pm-subscription-handler/tests/test_policy_response_handler.py @@ -1,5 +1,5 @@ # ============LICENSE_START=================================================== -# Copyright (C) 2019-2021 Nordix Foundation. +# Copyright (C) 2019-2022 Nordix Foundation. # ============================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ from unittest.mock import patch, MagicMock from mod import db +from mod.api.services import measurement_group_service from mod.network_function import NetworkFunction from mod.policy_response_handler import PolicyResponseHandler, policy_response_handle_functions from mod.subscription import AdministrativeState, SubNfState @@ -99,6 +100,40 @@ class PolicyResponseHandlerTest(BaseClassSetup): measurement_group_name='msr_grp_name', status=SubNfState.CREATED.value, nf_name=self.nf.nf_name) + @patch('mod.api.services.measurement_group_service.update_measurement_group_nf_status') + def test_handle_response_unlocked_success_filtering(self, mock_update_sub_nf): + with patch.dict(policy_response_handle_functions, + {AdministrativeState.UNLOCKED.value: {'success': mock_update_sub_nf}}): + sub = create_subscription_data('sub') + db.session.add(sub) + measurement_group_service.apply_nf_status_to_measurement_group( + self.nf.nf_name, "MG2", SubNfState.PENDING_CREATE.value) + db.session.commit() + self.policy_response_handler._handle_response( + 'MG2', + AdministrativeState.FILTERING.value, + self.nf.nf_name, 'success') + mock_update_sub_nf.assert_called_with( + measurement_group_name='MG2', + status=SubNfState.CREATED.value, nf_name=self.nf.nf_name) + + @patch('mod.api.services.measurement_group_service.update_measurement_group_nf_status') + def test_handle_response_locking_success_filtering(self, mock_update_sub_nf): + with patch.dict(policy_response_handle_functions, + {AdministrativeState.LOCKING.value: {'success': mock_update_sub_nf}}): + sub = create_subscription_data('sub') + db.session.add(sub) + measurement_group_service.apply_nf_status_to_measurement_group( + self.nf.nf_name, "MG2", SubNfState.PENDING_DELETE.value) + db.session.commit() + self.policy_response_handler._handle_response( + 'MG2', + AdministrativeState.FILTERING.value, + self.nf.nf_name, 'success') + mock_update_sub_nf.assert_called_with( + measurement_group_name='MG2', + status=SubNfState.DELETED.value, nf_name=self.nf.nf_name) + @patch('mod.api.services.measurement_group_service.update_measurement_group_nf_status') def test_handle_response_unlocked_failed(self, mock_update_sub_nf): with patch.dict(policy_response_handle_functions, @@ -111,6 +146,40 @@ class PolicyResponseHandlerTest(BaseClassSetup): measurement_group_name='msr_grp_name', status=SubNfState.CREATE_FAILED.value, nf_name=self.nf.nf_name) + @patch('mod.api.services.measurement_group_service.update_measurement_group_nf_status') + def test_handle_response_create_failed_filtering(self, mock_update_sub_nf): + with patch.dict(policy_response_handle_functions, + {AdministrativeState.UNLOCKED.value: {'failed': mock_update_sub_nf}}): + sub = create_subscription_data('sub') + db.session.add(sub) + measurement_group_service.apply_nf_status_to_measurement_group( + self.nf.nf_name, "MG2", SubNfState.PENDING_CREATE.value) + db.session.commit() + self.policy_response_handler._handle_response( + 'MG2', + AdministrativeState.FILTERING.value, + self.nf.nf_name, 'failed') + mock_update_sub_nf.assert_called_with( + measurement_group_name='MG2', + status=SubNfState.CREATE_FAILED.value, nf_name=self.nf.nf_name) + + @patch('mod.api.services.measurement_group_service.update_measurement_group_nf_status') + def test_handle_response_delete_failed_filtering(self, mock_update_sub_nf): + with patch.dict(policy_response_handle_functions, + {AdministrativeState.LOCKING.value: {'failed': mock_update_sub_nf}}): + sub = create_subscription_data('sub') + db.session.add(sub) + measurement_group_service.apply_nf_status_to_measurement_group( + self.nf.nf_name, "MG2", SubNfState.PENDING_DELETE.value) + db.session.commit() + self.policy_response_handler._handle_response( + 'MG2', + AdministrativeState.FILTERING.value, + self.nf.nf_name, 'failed') + mock_update_sub_nf.assert_called_with( + measurement_group_name='MG2', + status=SubNfState.DELETE_FAILED.value, nf_name=self.nf.nf_name) + def test_handle_response_exception(self): self.assertRaises(Exception, self.policy_response_handler._handle_response, 'sub1', 'wrong_state', 'nf1', 'wrong_message') diff --git a/components/pm-subscription-handler/version.properties b/components/pm-subscription-handler/version.properties index 3c5fba7f..3ad2137c 100644 --- a/components/pm-subscription-handler/version.properties +++ b/components/pm-subscription-handler/version.properties @@ -1,6 +1,6 @@ major=2 -minor=1 -patch=1 +minor=2 +patch=0 base_version=${major}.${minor}.${patch} release_version=${base_version} snapshot_version=${base_version}-SNAPSHOT -- cgit 1.2.3-korg