[DCAEGEN2] Update Administrative status for measurement group 32/126832/8 2.0.0-pmsh
authorSagarS <sagar.shetty@est.tech>
Thu, 27 Jan 2022 15:05:50 +0000 (15:05 +0000)
committerSagarS <sagar.shetty@est.tech>
Mon, 7 Feb 2022 18:04:39 +0000 (18:04 +0000)
Issue-ID: DCAEGEN2-2820
Change-Id: I290693edc5061c21bab6e0706eda02acb52e38e1
Signed-off-by: SagarS <sagar.shetty@est.tech>
components/pm-subscription-handler/Changelog.md
components/pm-subscription-handler/pmsh_service/mod/api/controller.py
components/pm-subscription-handler/pmsh_service/mod/api/custom_exception.py
components/pm-subscription-handler/pmsh_service/mod/api/db_models.py
components/pm-subscription-handler/pmsh_service/mod/api/pmsh_swagger.yml
components/pm-subscription-handler/pmsh_service/mod/api/services/measurement_group_service.py
components/pm-subscription-handler/pmsh_service/mod/api/services/subscription_service.py
components/pm-subscription-handler/pmsh_service/mod/policy_response_handler.py
components/pm-subscription-handler/tests/services/test_measurement_group_service.py
components/pm-subscription-handler/tests/services/test_subscription_service.py
components/pm-subscription-handler/tests/test_controller.py

index db8d575..d2630ae 100755 (executable)
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 * Read NFS associated with MG by using MGName and subName(DCAEGEN2-2993)
 * Lazy loading error for nfs in read API (DCAEGEN2-3029)
 * Delete subscription API by Name(DCAEGEN2-2821)
+* Update Administrative status for measurement group(DCAEGEN2-2820)
 
 ## [1.3.2]
 ### Changed
index d887187..96fb1b7 100755 (executable)
@@ -20,7 +20,8 @@ from http import HTTPStatus
 from mod import logger
 from mod.api.services import subscription_service, measurement_group_service
 from connexion import NoContent
-from mod.api.custom_exception import InvalidDataException, DuplicateDataException
+from mod.api.custom_exception import InvalidDataException, DuplicateDataException, \
+    DataConflictException
 
 
 def status():
@@ -58,12 +59,12 @@ def post_subscription(body):
         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
+        response = e.args[0], 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
+        response = e.args[0], HTTPStatus.BAD_REQUEST.value
     return response
 
 
@@ -186,3 +187,43 @@ def delete_subscription_by_name(subscription_name):
         return {'error': f'Try again, subscription with name {subscription_name}'
                          f'is not deleted due to following exception: {exception}'}, \
             HTTPStatus.INTERNAL_SERVER_ERROR.value
+
+
+def update_admin_state(subscription_name, measurement_group_name, body):
+    """
+    Performs administrative state update for the respective subscription
+    and measurement group name
+
+    Args:
+        subscription_name (String): Name of the subscription.
+        measurement_group_name (String): Name of the measurement group
+        body (dict): Request body with admin state to update.
+    Returns:
+       string, HTTPStatus: Successfully updated admin state, 200
+       string, HTTPStatus: Invalid request details, 400
+       string, HTTPStatus: Cannot update as Locked request is in progress, 409
+       string, HTTPStatus: Exception details of server failure, 500
+    """
+    logger.info('Performing administration status update for measurement group '
+                f'with sub name: {subscription_name} and measurement '
+                f'group name: {measurement_group_name} to {body["administrativeState"]} status')
+    response = 'Successfully updated admin state', HTTPStatus.OK.value
+    try:
+        meas_group = measurement_group_service.query_meas_group_by_name(subscription_name,
+                                                                        measurement_group_name)
+        measurement_group_service.update_admin_status(meas_group, body["administrativeState"])
+    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 admin status request was not processed for sub name: '
+                     f'{subscription_name} and meas group name: '
+                     f'{measurement_group_name} due to Exception : {exception}')
+        response = 'Update admin status request was not processed for sub name: '\
+                   f'{subscription_name} and meas group name: {measurement_group_name}'\
+                   f' due to Exception : {exception}', HTTPStatus.INTERNAL_SERVER_ERROR
+
+    return response
index 606d500..2bee3ff 100644 (file)
@@ -1,5 +1,5 @@
 # ============LICENSE_START===================================================
-#  Copyright (C) 2021 Nordix Foundation.
+#  Copyright (C) 2021-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.
@@ -24,7 +24,7 @@ class InvalidDataException(Exception):
     """
 
     def __init__(self, invalid_message):
-        self.invalid_message = invalid_message
+        super().__init__(invalid_message)
 
 
 class DuplicateDataException(Exception):
@@ -35,4 +35,15 @@ class DuplicateDataException(Exception):
     """
 
     def __init__(self, duplicate_field_info):
-        self.duplicate_field_info = duplicate_field_info
+        super().__init__(duplicate_field_info)
+
+
+class DataConflictException(Exception):
+    """Exception raised for conflicting data state in PMSH.
+
+    Attributes:
+        message -- detail on conflicting data
+    """
+
+    def __init__(self, data_conflict_message):
+        super().__init__(data_conflict_message)
index 548e4f2..2eccbac 100755 (executable)
@@ -1,5 +1,5 @@
 # ============LICENSE_START===================================================
-#  Copyright (C) 2020-2021 Nordix Foundation.
+#  Copyright (C) 2020-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.
@@ -149,7 +149,6 @@ class NfSubRelationalModel(db.Model):
     def serialize_nf(self):
         nf = NetworkFunctionModel.query.filter(
             NetworkFunctionModel.nf_name == self.nf_name).one_or_none()
-        db.session.remove()
         return {'nf_name': self.nf_name,
                 'ipv4_address': nf.ipv4_address,
                 'ipv6_address': nf.ipv6_address,
@@ -297,7 +296,6 @@ class NfMeasureGroupRelationalModel(db.Model):
         """
         nf = db.session.query(NetworkFunctionModel).filter(
             NetworkFunctionModel.nf_name == self.nf_name).one_or_none()
-        db.session.remove()
         return {'nfName': self.nf_name,
                 'ipv4Address': nf.ipv4_address,
                 'ipv6Address': nf.ipv6_address,
index 3319b7e..1c4c792 100644 (file)
@@ -112,6 +112,8 @@ paths:
     delete:
       description: Deletes the Subscription from PMSH specified by Name
       operationId: mod.api.controller.delete_subscription_by_name
+      tags:
+        - "Subscription"
       parameters:
         - name: subscription_name
           in: path
@@ -136,7 +138,7 @@ paths:
                   from PMSH by using sub name and meas group name
       operationId: mod.api.controller.get_meas_group_with_nfs
       tags:
-        - "measurement group"
+        - "Measurement Group"
       parameters:
         - name : subscription_name
           in: path
@@ -158,6 +160,41 @@ paths:
         500:
           description: Exception occurred while querying database
 
+  /subscription/{subscription_name}/measurementGroups/{measurement_group_name}/adminState:
+    put:
+      description: Update the admin status of the Measurement Group by using sub name and measurement group name
+      operationId: mod.api.controller.update_admin_state
+      tags:
+        - "Measurement Group"
+      parameters:
+        - name: subscription_name
+          in: path
+          required: true
+          description: Name of the subscription
+          type: string
+        - name: measurement_group_name
+          in: path
+          required: true
+          description: Name of the measurement group
+          type: string
+        - in: "body"
+          name: "body"
+          required: true
+          schema:
+            properties:
+              administrativeState:
+                type: string
+                enum: [ LOCKED, UNLOCKED ]
+      responses:
+        200:
+          description: Successfully updated admin state
+        409:
+          description: Cannot update as Locked request is in progress
+        400:
+          description: Invalid input request details
+        500:
+          description: Exception details of server failure
+
 definitions:
   subscription:
     type: object
index 692efe2..a1c141f 100644 (file)
@@ -1,5 +1,5 @@
 # ============LICENSE_START===================================================
-#  Copyright (C) 2021 Nordix Foundation.
+#  Copyright (C) 2021-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.
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
 
-from mod.api.db_models import MeasurementGroupModel, NfMeasureGroupRelationalModel
+from mod.api.custom_exception import InvalidDataException, DataConflictException
+from mod.api.db_models import MeasurementGroupModel, NfMeasureGroupRelationalModel, \
+    SubscriptionModel
 from mod import db, logger
-from mod.api.services import nf_service
+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
 
 
 def save_measurement_group(measurement_group, subscription_name):
@@ -64,16 +67,17 @@ def apply_nf_status_to_measurement_group(nf_name, measurement_group_name, status
     db.session.add(new_nf_measure_grp_rel)
 
 
-def publish_measurement_group(sub_model, measurement_group, nf):
+def publish_measurement_group(sub_model, measurement_group, nf, change_type):
     """
     Publishes an event for measurement group against nfs to MR
 
     Args:
         sub_model(SubscriptionModel): Subscription model object
         measurement_group (MeasurementGroupModel): Measurement group to publish
-        nf (NetworkFunction): Network function to publish.
+        nf (NetworkFunction): Network function to publish
+        change_type (string): defines event type like CREATE or DELETE
    """
-    event_body = nf_service.create_nf_event_body(nf, 'CREATE', sub_model)
+    event_body = nf_service.create_nf_event_body(nf, change_type, sub_model)
     event_body['subscription'] = {
         "administrativeState": measurement_group.administrative_state,
         "subscriptionName": sub_model.subscription_name,
@@ -152,3 +156,132 @@ def query_meas_group_by_name(subscription_name, measurement_group_name):
         MeasurementGroupModel.subscription_name == subscription_name,
         MeasurementGroupModel.measurement_group_name == measurement_group_name).one_or_none()
     return meas_group
+
+
+def lock_nf_to_meas_grp(nf_name, measurement_group_name, status):
+    """ Deletes a particular nf related to a measurement group name and
+        if no more relations of nf exist to measurement group then delete nf from PMSH
+
+    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:
+        delete_nf_to_measurement_group(nf_name, measurement_group_name, status)
+        nf_measurement_group_rels = NfMeasureGroupRelationalModel.query.filter(
+            NfMeasureGroupRelationalModel.measurement_grp_name == measurement_group_name).all()
+        if not nf_measurement_group_rels:
+            MeasurementGroupModel.query.filter(
+                MeasurementGroupModel.measurement_group_name == measurement_group_name). \
+                update({MeasurementGroupModel.administrative_state: AdministrativeState.
+                       LOCKED.value}, synchronize_session='evaluate')
+            db.session.commit()
+    except Exception as e:
+        logger.error('Failed update LOCKED status for measurement group name: '
+                     f'{measurement_group_name} due to: {e}')
+
+
+def deactivate_nfs(sub_model, measurement_group, nf_meas_relations):
+    """
+    Deactivates network functions associated with measurement group
+
+    Args:
+        sub_model (SubscriptionModel): Subscription model
+        measurement_group (MeasurementGroupModel): Measurement group to update
+        nf_meas_relations (list[NfMeasureGroupRelationalModel]): nf to measurement grp relations
+    """
+    for nf in nf_meas_relations:
+        logger.info(f'Saving measurement group to nf name, measure_grp_name: {nf.nf_name},'
+                    f'{measurement_group.measurement_group_name}  with DELETE request')
+        update_measurement_group_nf_status(measurement_group.measurement_group_name,
+                                           SubNfState.PENDING_DELETE.value, nf.nf_name)
+        try:
+            network_function = NetworkFunction(**nf.serialize_meas_group_nfs())
+            logger.info(f'Publishing event for nf name, measure_grp_name: {nf.nf_name},'
+                        f'{measurement_group.measurement_group_name} with DELETE request')
+            publish_measurement_group(sub_model, measurement_group, network_function, 'DELETE')
+        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'{sub_model.subscription_name} with error: {ex}')
+
+
+def activate_nfs(sub_model, measurement_group):
+    """
+    Activates network functions associated with measurement group
+
+    Args:
+        sub_model (SubscriptionModel): Subscription model
+        measurement_group (MeasurementGroupModel): Measurement group to update
+    """
+    new_nfs = []
+    sub_nf_names = [nf.nf_name for nf in sub_model.nfs]
+    filtered_nfs = nf_service.capture_filtered_nfs(sub_model.subscription_name)
+    for nf in filtered_nfs:
+        if nf.nf_name not in sub_nf_names:
+            new_nfs.append(nf)
+    if new_nfs:
+        logger.info(f'Adding new nfs to the subscription: '
+                    f'{sub_model.subscription_name}')
+        subscription_service.save_filtered_nfs(new_nfs)
+        subscription_service.apply_subscription_to_nfs(new_nfs,
+                                                       sub_model.subscription_name)
+    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} with CREATE request')
+
+        apply_nf_status_to_measurement_group(nf.nf_name,
+                                             measurement_group.measurement_group_name,
+                                             SubNfState.PENDING_CREATE.value)
+        db.session.commit()
+        try:
+            network_function = NetworkFunction(**nf.serialize_nf())
+            logger.info(f'Publishing event for nf name, measure_grp_name: {nf.nf_name},'
+                        f'{measurement_group.measurement_group_name}  with CREATE request')
+            publish_measurement_group(sub_model, measurement_group, network_function, 'CREATE')
+        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'{sub_model.subscription_name} with error: {ex}')
+
+
+def update_admin_status(measurement_group, status):
+    """
+    Performs administrative status updates for the measurement group
+
+    Args:
+        measurement_group (MeasurementGroupModel): Measurement group to update
+        status (string): Admin status to update for measurement group
+
+    Raises:
+        InvalidDataException: contains details on invalid fields
+        DataConflictException: contains details on conflicting state of a field
+        Exception: contains runtime error details
+    """
+    if measurement_group is None:
+        raise InvalidDataException('Requested measurement group not available '
+                                   'for admin status update')
+    elif measurement_group.administrative_state == AdministrativeState.LOCKING.value:
+        raise DataConflictException('Cannot update admin status as Locked request is in progress'
+                                    f' for sub name: {measurement_group.subscription_name}  and '
+                                    f'meas group name: {measurement_group.measurement_group_name}')
+    elif measurement_group.administrative_state == status:
+        raise InvalidDataException(f'Measurement group is already in {status} state '
+                                   f'for sub name: {measurement_group.subscription_name}  and '
+                                   f'meas group name: {measurement_group.measurement_group_name}')
+    else:
+        sub_model = SubscriptionModel.query.filter(
+            SubscriptionModel.subscription_name == measurement_group.subscription_name) \
+            .one_or_none()
+        nf_meas_relations = NfMeasureGroupRelationalModel.query.filter(
+            NfMeasureGroupRelationalModel.measurement_grp_name == measurement_group.
+            measurement_group_name).all()
+        if nf_meas_relations and status == AdministrativeState.LOCKED.value:
+            status = AdministrativeState.LOCKING.value
+        measurement_group.administrative_state = status
+        db.session.commit()
+        if status == AdministrativeState.LOCKING.value:
+            deactivate_nfs(sub_model, measurement_group, nf_meas_relations)
+        elif status == AdministrativeState.UNLOCKED.value:
+            activate_nfs(sub_model, measurement_group)
index 7b31de5..338ab89 100644 (file)
@@ -89,7 +89,7 @@ def publish_measurement_grp_to_nfs(sub_model, filtered_nfs,
                 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(
-                    sub_model, measurement_group, nf)
+                    sub_model, measurement_group, nf, 'CREATE')
             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}, '
index 1bc5808..a0a7bd6 100644 (file)
@@ -1,5 +1,5 @@
 # ============LICENSE_START===================================================
-#  Copyright (C) 2020-2021 Nordix Foundation.
+#  Copyright (C) 2020-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.
@@ -32,7 +32,7 @@ policy_response_handle_functions = {
         'failed': measurement_group_service.update_measurement_group_nf_status
     },
     AdministrativeState.LOCKING.value: {
-        'success': measurement_group_service.delete_nf_to_measurement_group,
+        'success': measurement_group_service.lock_nf_to_meas_grp,
         'failed': measurement_group_service.update_measurement_group_nf_status
     }
 }
index 97353af..f656513 100644 (file)
@@ -1,5 +1,5 @@
 # ============LICENSE_START===================================================
-#  Copyright (C) 2021 Nordix Foundation.
+#  Copyright (C) 2021-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.
 import json
 import os
 from unittest.mock import patch
-from mod.network_function import NetworkFunction
+
+from mod.api.custom_exception import InvalidDataException, DataConflictException
+from mod.network_function import NetworkFunction, NetworkFunctionFilter
 from mod.pmsh_config import AppConfig
-from mod import db
-from tests.base_setup import BaseClassSetup
+from mod import db, aai_client
+from tests.base_setup import BaseClassSetup, create_subscription_data, \
+    create_multiple_network_function_data
 from mod.api.services import measurement_group_service, nf_service
 from mod.api.db_models import MeasurementGroupModel, NfMeasureGroupRelationalModel, \
     SubscriptionModel, NetworkFunctionModel
@@ -69,7 +72,7 @@ class MeasurementGroupServiceTestCase(BaseClassSetup):
         sub_model = SubscriptionModel('sub_publish', 'pmsh-operational-policy',
                                       'pmsh-control-loop', 'LOCKED')
         measurement_group_service.publish_measurement_group(
-            sub_model, measurement_grp, nf_1)
+            sub_model, measurement_grp, nf_1, 'CREATE')
         mock_mr.assert_called_once_with('policy_pm_publisher',
                                         {'nfName': 'pnf_1',
                                          'ipAddress': '2001:db8:3333:4444:5555:6666:7777:8888',
@@ -191,3 +194,167 @@ class MeasurementGroupServiceTestCase(BaseClassSetup):
         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(AppConfig, 'publish_to_topic')
+    def test_update_admin_status_to_locking(self, mock_mr):
+        super().setUpAppConf()
+        sub = create_subscription_data('sub')
+        nf_list = create_multiple_network_function_data(['pnf_101', 'pnf_102'])
+        db.session.add(sub)
+        for nf in nf_list:
+            nf_service.save_nf(nf)
+            measurement_group_service. \
+                apply_nf_status_to_measurement_group(nf.nf_name, sub.measurement_groups[0].
+                                                     measurement_group_name,
+                                                     SubNfState.CREATED.value)
+        db.session.commit()
+        measurement_group_service.update_admin_status(sub.measurement_groups[0], 'LOCKED')
+        meas_grp = measurement_group_service.query_meas_group_by_name('sub', 'MG1')
+        self.assertEqual(meas_grp.subscription_name, 'sub')
+        self.assertEqual(meas_grp.measurement_group_name, 'MG1')
+        self.assertEqual(meas_grp.administrative_state, 'LOCKING')
+        meas_group_nfs = db.session.query(NfMeasureGroupRelationalModel).filter(
+            NfMeasureGroupRelationalModel.measurement_grp_name == meas_grp.measurement_group_name)\
+            .all()
+        for nf in meas_group_nfs:
+            self.assertEqual(nf.nf_measure_grp_status, SubNfState.PENDING_DELETE.value)
+
+    @patch.object(AppConfig, 'publish_to_topic')
+    def test_update_admin_status_to_locked(self, mock_mr):
+        super().setUpAppConf()
+        sub = create_subscription_data('sub')
+        db.session.add(sub)
+        measurement_group_service.update_admin_status(sub.measurement_groups[0], 'LOCKED')
+        meas_grp = measurement_group_service.query_meas_group_by_name('sub', 'MG1')
+        self.assertEqual(meas_grp.subscription_name, 'sub')
+        self.assertEqual(meas_grp.measurement_group_name, 'MG1')
+        self.assertEqual(meas_grp.administrative_state, 'LOCKED')
+
+    @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_update_admin_status_to_unlocked_with_no_nfs(self, mock_filter_call,
+                                                         mock_model_aai, mock_aai, mock_mr):
+        mock_aai.return_value = json.loads(self.aai_response_data)
+        mock_model_aai.return_value = json.loads(self.good_model_info)
+        super().setUpAppConf()
+        sub = create_subscription_data('sub')
+        sub.nfs = []
+        db.session.add(sub)
+        db.session.commit()
+        mock_filter_call.return_value = NetworkFunctionFilter.get_network_function_filter('sub')
+        measurement_group_service.update_admin_status(sub.measurement_groups[1], 'UNLOCKED')
+        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')
+        meas_group_nfs = db.session.query(NfMeasureGroupRelationalModel).filter(
+            NfMeasureGroupRelationalModel.measurement_grp_name == meas_grp.measurement_group_name)\
+            .all()
+        for nf in meas_group_nfs:
+            self.assertEqual(nf.nf_measure_grp_status, SubNfState.PENDING_CREATE.value)
+
+    @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_update_admin_status_to_unlocking(self, mock_filter_call,
+                                              mock_model_aai, mock_aai, mock_mr):
+        mock_aai.return_value = json.loads(self.aai_response_data)
+        mock_model_aai.return_value = json.loads(self.good_model_info)
+        super().setUpAppConf()
+        sub = create_subscription_data('sub')
+        db.session.add(sub)
+        db.session.commit()
+        mock_filter_call.return_value = NetworkFunctionFilter.get_network_function_filter('sub')
+        measurement_group_service.update_admin_status(sub.measurement_groups[1], 'UNLOCKED')
+        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')
+        meas_group_nfs = db.session.query(NfMeasureGroupRelationalModel).filter(
+            NfMeasureGroupRelationalModel.measurement_grp_name == meas_grp.measurement_group_name)\
+            .all()
+        for nf in meas_group_nfs:
+            self.assertEqual(nf.nf_measure_grp_status, SubNfState.PENDING_CREATE.value)
+
+    def test_update_admin_status_for_missing_measurement_group(self):
+        try:
+            measurement_group_service.update_admin_status(None, 'UNLOCKED')
+        except InvalidDataException as e:
+            self.assertEqual(e.args[0], 'Requested measurement group not available '
+                                        'for admin status update')
+
+    def test_update_admin_status_for_data_conflict(self):
+        super().setUpAppConf()
+        sub = create_subscription_data('sub1')
+        sub.measurement_groups[0].administrative_state = 'LOCKING'
+        try:
+            measurement_group_service.update_admin_status(sub.measurement_groups[0], 'LOCKED')
+        except DataConflictException as e:
+            self.assertEqual(e.args[0], 'Cannot update admin status as Locked request'
+                                        ' is in progress for sub name: sub1  and '
+                                        'meas group name: MG1')
+
+    def test_update_admin_status_for_same_state(self):
+        super().setUpAppConf()
+        sub = create_subscription_data('sub1')
+        try:
+            measurement_group_service.update_admin_status(sub.measurement_groups[0], 'UNLOCKED')
+        except InvalidDataException as e:
+            self.assertEqual(e.args[0], 'Measurement group is already in UNLOCKED '
+                                        'state for sub name: sub1  and meas group '
+                                        'name: MG1')
+
+    def test_lock_nf_to_meas_grp_for_locking_with_LOCKED_update(self):
+        sub = create_subscription_data('sub')
+        sub.measurement_groups[1].administrative_state = 'LOCKING'
+        nf_list = create_multiple_network_function_data(['pnf_101'])
+        db.session.add(sub)
+        for nf in nf_list:
+            nf_service.save_nf(nf)
+            measurement_group_service. \
+                apply_nf_status_to_measurement_group(nf.nf_name, sub.measurement_groups[1].
+                                                     measurement_group_name,
+                                                     SubNfState.PENDING_DELETE.value)
+        db.session.commit()
+        measurement_group_service.lock_nf_to_meas_grp(
+            "pnf_101", "MG2", SubNfState.DELETED.value)
+        measurement_grp_rel = (NfMeasureGroupRelationalModel.query.filter(
+            NfMeasureGroupRelationalModel.measurement_grp_name == 'MG2',
+            NfMeasureGroupRelationalModel.nf_name == 'pnf_101').one_or_none())
+        self.assertIsNone(measurement_grp_rel)
+        network_function = (NetworkFunctionModel.query.filter(
+            NetworkFunctionModel.nf_name == 'pnf_101').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, 'LOCKED')
+
+    def test_lock_nf_to_meas_grp_with_no_LOCKED_update(self):
+        sub = create_subscription_data('sub')
+        sub.measurement_groups[1].administrative_state = 'LOCKING'
+        nf_list = create_multiple_network_function_data(['pnf_101', 'pnf_102'])
+        db.session.add(sub)
+        for nf in nf_list:
+            nf_service.save_nf(nf)
+            measurement_group_service. \
+                apply_nf_status_to_measurement_group(nf.nf_name, sub.measurement_groups[1].
+                                                     measurement_group_name,
+                                                     SubNfState.PENDING_DELETE.value)
+        db.session.commit()
+        measurement_group_service.lock_nf_to_meas_grp(
+            "pnf_101", "MG2", SubNfState.DELETED.value)
+        measurement_grp_rel = (NfMeasureGroupRelationalModel.query.filter(
+            NfMeasureGroupRelationalModel.measurement_grp_name == 'MG2',
+            NfMeasureGroupRelationalModel.nf_name == 'pnf_101').one_or_none())
+        self.assertIsNone(measurement_grp_rel)
+        network_function = (NetworkFunctionModel.query.filter(
+            NetworkFunctionModel.nf_name == 'pnf_101').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, 'LOCKING')
index 44fb4ef..807806f 100644 (file)
@@ -1,5 +1,5 @@
 # ============LICENSE_START===================================================
-#  Copyright (C) 2021 Nordix Foundation.
+#  Copyright (C) 2021-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.
@@ -101,7 +101,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
         try:
             subscription_service.create_subscription(subscription)
         except InvalidDataException as exception:
-            self.assertEqual(exception.invalid_message, ["AAI call failed"])
+            self.assertEqual(exception.args[0], ["AAI call failed"])
 
         # Checking Rollback on publish failure with subscription and nfs captured
         existing_subscription = (SubscriptionModel.query.filter(
@@ -122,7 +122,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
         try:
             subscription_service.create_subscription(subscription)
         except InvalidDataException as exception:
-            self.assertEqual(exception.invalid_message, ["AAI call failed"])
+            self.assertEqual(exception.args[0], ["AAI call failed"])
 
         # Checking Rollback on AAI failure with subscription request saved
         existing_subscription = (SubscriptionModel.query.filter(
@@ -134,7 +134,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
             subscription_service.create_subscription(json.loads(self.subscription_request)
                                                      ['subscription'])
         except DuplicateDataException as exception:
-            self.assertEqual(exception.duplicate_field_info,
+            self.assertEqual(exception.args[0],
                              "subscription Name: ExtraPM-All-gNB-R2B already exists.")
 
     def test_missing_measurement_grp_name(self):
@@ -142,7 +142,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
         try:
             subscription_service.create_subscription(json.loads(subscription)['subscription'])
         except InvalidDataException as exception:
-            self.assertEqual(exception.invalid_message,
+            self.assertEqual(exception.args[0],
                              "No value provided for measurement group name")
 
     def test_missing_administrative_state(self):
@@ -152,7 +152,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
         try:
             subscription_service.create_subscription(subscription['subscription'])
         except InvalidDataException as exception:
-            self.assertEqual(exception.invalid_message,
+            self.assertEqual(exception.args[0],
                              "No value provided for administrative state")
 
     @patch.object(subscription_service, 'save_nf_filter')
@@ -319,7 +319,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
         try:
             subscription_service.check_missing_data(subscription)
         except InvalidDataException as invalidEx:
-            self.assertEqual(invalidEx.invalid_message, "No value provided in subscription name")
+            self.assertEqual(invalidEx.args[0], "No value provided in subscription name")
 
     def test_check_missing_data_admin_status_missing(self):
         subscription = self.subscription_request.replace(
@@ -328,7 +328,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
         try:
             subscription_service.check_missing_data(subscription)
         except InvalidDataException as invalidEx:
-            self.assertEqual(invalidEx.invalid_message,
+            self.assertEqual(invalidEx.args[0],
                              "No value provided for administrative state")
 
     def test_check_missing_data_msr_grp_name(self):
@@ -337,7 +337,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
         try:
             subscription_service.check_missing_data(subscription)
         except InvalidDataException as invalidEx:
-            self.assertEqual(invalidEx.invalid_message,
+            self.assertEqual(invalidEx.args[0],
                              "No value provided for measurement group name")
 
     def test_validate_nf_filter_with_no_filter_values(self):
@@ -346,7 +346,7 @@ class SubscriptionServiceTestCase(BaseClassSetup):
         try:
             subscription_service.validate_nf_filter(json.loads(nfFilter))
         except InvalidDataException as invalidEx:
-            self.assertEqual(invalidEx.invalid_message,
+            self.assertEqual(invalidEx.args[0],
                              "At least one filter within nfFilter must not be empty")
 
     def test_db_string_to_list(self):
index 1de7c17..94bfbfd 100755 (executable)
@@ -22,8 +22,9 @@ 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
+    get_subscriptions, get_meas_group_with_nfs, delete_subscription_by_name, update_admin_state
 from tests.base_setup import BaseClassSetup
+from mod.api.custom_exception import InvalidDataException, DataConflictException
 from mod.api.db_models import SubscriptionModel, NfMeasureGroupRelationalModel
 from mod.subscription import SubNfState
 from mod.network_function import NetworkFunctionFilter
@@ -252,3 +253,75 @@ class ControllerTestCase(BaseClassSetup):
     def test_delete_sub_exception(self):
         error, status_code = delete_subscription_by_name('None')
         self.assertEqual(status_code, HTTPStatus.INTERNAL_SERVER_ERROR.value)
+
+    @patch('mod.pmsh_config.AppConfig.publish_to_topic', MagicMock(return_value=None))
+    def test_update_admin_state_api_for_locked_update(self):
+        sub = create_subscription_data('sub1')
+        nf_list = create_multiple_network_function_data(['pnf_101', 'pnf_102'])
+        db.session.add(sub)
+        for nf in nf_list:
+            nf_service.save_nf(nf)
+            measurement_group_service. \
+                apply_nf_status_to_measurement_group(nf.nf_name, sub.measurement_groups[0].
+                                                     measurement_group_name,
+                                                     SubNfState.CREATED.value)
+        db.session.commit()
+        response = update_admin_state('sub1', 'MG1', {'administrativeState': 'LOCKED'})
+        self.assertEqual(response[1], HTTPStatus.OK.value)
+        self.assertEqual(response[0], 'Successfully updated admin state')
+        mg_with_nfs, status_code = get_meas_group_with_nfs('sub1', 'MG1')
+        self.assertEqual(mg_with_nfs['subscriptionName'], 'sub1')
+        self.assertEqual(mg_with_nfs['measurementGroupName'], 'MG1')
+        self.assertEqual(mg_with_nfs['administrativeState'], 'LOCKING')
+        for nf in mg_with_nfs['networkFunctions']:
+            self.assertEqual(nf['nfMgStatus'], SubNfState.PENDING_DELETE.value)
+
+    @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_admin_state_api_for_unlocked_update(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)
+        sub = create_subscription_data('sub1')
+        db.session.add(sub)
+        nf_list = create_multiple_network_function_data(['pnf_101', 'pnf_102'])
+        for network_function in nf_list:
+            db.session.add(network_function)
+        db.session.commit()
+        mock_filter_call.return_value = NetworkFunctionFilter.get_network_function_filter('sub')
+        response = update_admin_state('sub1', 'MG2', {'administrativeState': 'UNLOCKED'})
+        self.assertEqual(response[1], HTTPStatus.OK.value)
+        self.assertEqual(response[0], 'Successfully updated admin state')
+        mg_with_nfs, status_code = get_meas_group_with_nfs('sub1', 'MG2')
+        self.assertEqual(mg_with_nfs['subscriptionName'], 'sub1')
+        self.assertEqual(mg_with_nfs['measurementGroupName'], 'MG2')
+        self.assertEqual(mg_with_nfs['administrativeState'], 'UNLOCKED')
+        for nf in mg_with_nfs['networkFunctions']:
+            self.assertEqual(nf['nfMgStatus'], SubNfState.PENDING_CREATE.value)
+
+    @patch('mod.api.services.measurement_group_service.update_admin_status',
+           MagicMock(side_effect=InvalidDataException('Bad request')))
+    def test_update_admin_state_api_invalid_data_exception(self):
+        error, status_code = update_admin_state('sub4', 'MG2',
+                                                {'administrativeState': 'UNLOCKED'})
+        self.assertEqual(status_code, HTTPStatus.BAD_REQUEST.value)
+        self.assertEqual(error, 'Bad request')
+
+    @patch('mod.api.services.measurement_group_service.update_admin_status',
+           MagicMock(side_effect=DataConflictException('Data conflict')))
+    def test_update_admin_state_api_data_conflict_exception(self):
+        error, status_code = update_admin_state('sub4', 'MG2',
+                                                {'administrativeState': 'UNLOCKED'})
+        self.assertEqual(status_code, HTTPStatus.CONFLICT.value)
+        self.assertEqual(error, 'Data conflict')
+
+    @patch('mod.api.services.measurement_group_service.update_admin_status',
+           MagicMock(side_effect=Exception('Server Error')))
+    def test_update_admin_state_api_exception(self):
+        error, status_code = update_admin_state('sub4', 'MG2',
+                                                {'administrativeState': 'UNLOCKED'})
+        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')