[PMSH] Read NFS associated with MG by using MGName and subName

Issue-ID: DCAEGEN2-2993

Signed-off-by: Raviteja, Karumuri <raviteja.karumuri@est.tech>
Change-Id: I651a687ab7480fc0d42bf976c3d1b34f00e73e98
diff --git a/components/pm-subscription-handler/Changelog.md b/components/pm-subscription-handler/Changelog.md
index 77cd827..db096b1 100755
--- a/components/pm-subscription-handler/Changelog.md
+++ b/components/pm-subscription-handler/Changelog.md
@@ -16,6 +16,7 @@
 * Response Event Handler Integration (DCAEGEN2-2915)
 * Updated to get NFs list when requesting a specific subscription (DCAEGEN2-2992)
 * AAI Event handler changes with new subscription format (DCAEGEN2-2912)
+* Read NFS associated with MG by using MGName and subName(DCAEGEN2-2993)
 
 ## [1.3.2]
 ### Changed
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 e852324..aee6df0 100755
--- a/components/pm-subscription-handler/pmsh_service/mod/api/controller.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/api/controller.py
@@ -18,7 +18,7 @@
 
 from http import HTTPStatus
 from mod import logger
-from mod.api.services import subscription_service
+from mod.api.services import subscription_service, measurement_group_service
 from connexion import NoContent
 from mod.api.custom_exception import InvalidDataException, DuplicateDataException
 
@@ -112,3 +112,38 @@
         logger.error(f'The following exception occurred while fetching subscriptions: {exception}')
         return {'error': 'Request was not processed due to Exception : '
                          f'{exception}'}, HTTPStatus.INTERNAL_SERVER_ERROR.value
+
+
+def get_meas_group_with_nfs(subscription_name, measurement_group_name):
+    """
+    Retrieves the measurement group and it's associated network functions
+
+    Args:
+        subscription_name (String): Name of the subscription.
+        measurement_group_name (String): Name of the measurement group
+
+    Returns:
+       dict, HTTPStatus: measurement group info with associated nfs, 200
+       dict, HTTPStatus: measurement group was not defined, 404
+       dict, HTTPStatus: Exception details of failure, 500
+    """
+    logger.info('API call received to query measurement group and associated network'
+                f' functions by using sub name: {subscription_name} and measurement '
+                f'group name: {measurement_group_name}')
+    try:
+        meas_group = measurement_group_service.query_meas_group_by_name(subscription_name,
+                                                                        measurement_group_name)
+        if meas_group is not None:
+            return meas_group.meas_group_with_nfs(), HTTPStatus.OK.value
+        else:
+            logger.error('measurement group was not defined with the sub name: '
+                         f'{subscription_name} and meas group name: '
+                         f'{measurement_group_name}')
+            return {'error': 'measurement group was not defined with the sub name: '
+                             f'{subscription_name} and meas group name: '
+                             f'{measurement_group_name}'}, HTTPStatus.NOT_FOUND.value
+    except Exception as exception:
+        logger.error('The following exception occurred while fetching measurement group: '
+                     f'{exception}')
+        return {'error': 'Request was not processed due to Exception : '
+                         f'{exception}'}, HTTPStatus.INTERNAL_SERVER_ERROR.value
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 9ecc80e..548e4f2 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
@@ -52,10 +52,10 @@
         self.status = status
 
     def __repr__(self):
-        return f'subscription_name: {self.subscription_name},' \
-               f'operational_policy_name: {self.operational_policy_name},' \
-               f'control_loop_name: {self.control_loop_name},' \
-               f'status: {self.status}'
+        return (f'subscription_name: {self.subscription_name}, '
+                f'operational_policy_name: {self.operational_policy_name}, '
+                f'control_loop_name: {self.control_loop_name}, '
+                f'status: {self.status}')
 
     def __eq__(self, other):
         if isinstance(self, other.__class__):
@@ -139,8 +139,8 @@
         self.nf_sub_status = nf_sub_status
 
     def __repr__(self):
-        return f'subscription_name: {self.subscription_name}, ' \
-               f'nf_name: {self.nf_name}, nf_sub_status: {self.nf_sub_status}'
+        return (f'subscription_name: {self.subscription_name}, '
+                f'nf_name: {self.nf_name}, nf_sub_status: {self.nf_sub_status}')
 
     def serialize(self):
         return {'subscription_name': self.subscription_name, 'nf_name': self.nf_name,
@@ -183,9 +183,9 @@
         self.model_names = model_names
 
     def __repr__(self):
-        return f'subscription_name: {self.subscription_name}, ' \
-               f'nf_names: {self.nf_names}, model_invariant_ids: {self.model_invariant_ids}' \
-               f'model_version_ids: {self.model_version_ids}, model_names: {self.model_names}'
+        return (f'subscription_name: {self.subscription_name}, '
+                f'nf_names: {self.nf_names}, model_invariant_ids: {self.model_invariant_ids}, '
+                f'model_version_ids: {self.model_version_ids}, model_names: {self.model_names}')
 
     def serialize(self):
         return {'nfNames': convert_db_string_to_list(self.nf_names),
@@ -220,13 +220,13 @@
         self.managed_object_dns_basic = managed_object_dns_basic
 
     def __repr__(self):
-        return f'subscription_name: {self.subscription_name}, ' \
-               f'measurement_group_name: {self.measurement_group_name},' \
-               f'administrative_state: {self.administrative_state},' \
-               f'file_based_gp: {self.file_based_gp},' \
-               f'file_location: {self.file_location},' \
-               f'measurement_type: {self.measurement_type}' \
-               f'managed_object_dns_basic: {self.managed_object_dns_basic}'
+        return (f'subscription_name: {self.subscription_name}, '
+                f'measurement_group_name: {self.measurement_group_name}, '
+                f'administrative_state: {self.administrative_state}, '
+                f'file_based_gp: {self.file_based_gp}, '
+                f'file_location: {self.file_location}, '
+                f'measurement_type: {self.measurement_type}, '
+                f'managed_object_dns_basic: {self.managed_object_dns_basic}')
 
     def serialize(self):
         return {'measurementGroup': {'measurementGroupName': self.measurement_group_name,
@@ -236,6 +236,28 @@
                                      'measurementTypes': self.measurement_type,
                                      'managedObjectDNsBasic': self.managed_object_dns_basic}}
 
+    def meas_group_with_nfs(self):
+        """
+        Generates the dictionary of subscription name, measurement group name, administrative state
+        and network functions
+
+        Returns:
+           dict: of subscription name, measurement group name, administrative state
+                 and network functions
+        """
+        meas_group_nfs = db.session.query(NfMeasureGroupRelationalModel).filter(
+            NfMeasureGroupRelationalModel.measurement_grp_name == self.measurement_group_name).all()
+        db.session.remove()
+        return {'subscriptionName': self.subscription_name,
+                'measurementGroupName': self.measurement_group_name,
+                'administrativeState': self.administrative_state,
+                'fileBasedGP': self.file_based_gp,
+                'fileLocation': self.file_location,
+                'measurementTypes': self.measurement_type,
+                'managedObjectDNsBasic': self.managed_object_dns_basic,
+                'networkFunctions':
+                    [meas_group_nf.serialize_meas_group_nfs() for meas_group_nf in meas_group_nfs]}
+
 
 class NfMeasureGroupRelationalModel(db.Model):
     __tablename__ = 'nf_to_measure_grp_rel'
@@ -263,8 +285,28 @@
         self.retry_count = retry_count
 
     def __repr__(self):
-        return f'measurement_grp_name: {self.measurement_grp_name}, ' \
-               f'nf_name: {self.nf_name}, nf_measure_grp_status: {self.nf_measure_grp_status}'
+        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 serialize_meas_group_nfs(self):
+        """
+        Generates the dictionary of all the network function properties
+
+        Returns:
+           dict: of network function properties
+        """
+        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,
+                'nfMgStatus': self.nf_measure_grp_status,
+                'modelInvariantId': nf.model_invariant_id,
+                'modelVersionId': nf.model_version_id,
+                'modelName': nf.model_name,
+                'sdncModelName': nf.sdnc_model_name,
+                'sdncModelVersion': nf.sdnc_model_version}
 
 
 def convert_db_string_to_list(db_string):
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 39f6dc3..4dd1cc7 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
@@ -109,6 +109,34 @@
         500:
           description: Exception occurred while querying database
 
+  /subscription/{subscription_name}/measurementGroups/{measurement_group_name}:
+    get:
+      description: Get the  measurement group and associated network functions
+                  from PMSH by using sub name and meas group name
+      operationId: mod.api.controller.get_meas_group_with_nfs
+      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 name
+          type: string
+      responses:
+        200:
+          description: OK; Received requested measurement group with associated NF's
+          schema:
+            $ref : "#/definitions/measGroupWithNFs"
+        404:
+          description: Measurement group with specified name not found
+        500:
+          description: Exception occurred while querying database
+
 definitions:
   subscription:
     type: object
@@ -215,3 +243,60 @@
         type: string
     required:
       - DN
+
+  measGroupWithNFs:
+    type: object
+    properties:
+      subscriptionName:
+        type: string
+      measurementGroupName:
+        type: string
+      administrativeState:
+        type: string
+        enum: [ LOCKED, UNLOCKED ]
+      fileBasedGP:
+        type: integer
+      fileLocation:
+        type: string
+      measurementTypes:
+        type: array
+        minItems: 1
+        items:
+          $ref: "#/definitions/measurementType"
+      managedObjectDNsBasic:
+        type: array
+        minItems: 1
+        items:
+          $ref: "#/definitions/managedObjectDNs"
+      network_functions:
+        type: array
+        items:
+          type: object
+          properties:
+            nfName:
+              type: string
+              description: Name of the Network Function
+            ipv4Address:
+              type: string
+              description: Address of the IPV4
+            ipv6Address:
+              type: string
+              description: Address of the IPV6
+            nfMgStatus:
+              type: string
+              description: status of network function for one meas group
+            modelInvariantId:
+              type: string
+              description: ID of the model invariant
+            modelVersionId:
+              type: string
+              description: ID of the model version
+            modelName:
+              type: string
+              description: Name of the model
+            sdncModelName:
+              type: string
+              description: Name of the sdnc model
+            sdncModelVersion:
+              type: string
+              description: Version of the sdnc model
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 cc07cc0..692efe2 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
@@ -135,3 +135,20 @@
     except Exception as e:
         logger.error(f'Failed to delete nf: {nf_name} for measurement group: '
                      f'{measurement_group_name} due to: {e}')
+
+
+def query_meas_group_by_name(subscription_name, measurement_group_name):
+    """
+    Retrieves the measurement group by using sub name and measurement group name
+
+    Args:
+        subscription_name (String): Name of the subscription.
+        measurement_group_name (String): Name of the measurement group
+
+    Returns:
+        MeasurementGroupModel: queried measurement group (or) None
+    """
+    meas_group = db.session.query(MeasurementGroupModel).filter(
+        MeasurementGroupModel.subscription_name == subscription_name,
+        MeasurementGroupModel.measurement_group_name == measurement_group_name).one_or_none()
+    return meas_group
diff --git a/components/pm-subscription-handler/tests/base_setup.py b/components/pm-subscription-handler/tests/base_setup.py
index 0005be7..560eaeb 100755
--- a/components/pm-subscription-handler/tests/base_setup.py
+++ b/components/pm-subscription-handler/tests/base_setup.py
@@ -23,7 +23,7 @@
 
 from mod import create_app, db
 from mod.api.db_models import NetworkFunctionFilterModel, MeasurementGroupModel, \
-    SubscriptionModel, NfSubRelationalModel
+    SubscriptionModel, NetworkFunctionModel, NfSubRelationalModel
 from mod.network_function import NetworkFunctionFilter
 from mod.pmsh_utils import AppConfig
 from mod.pmsh_config import AppConfig as NewAppConfig
@@ -89,6 +89,26 @@
     return subscriptions
 
 
+def create_multiple_network_function_data(nf_name_list):
+    """
+    Creates list of network function model objects
+
+    Args:
+        nf_name_list (list): Network function names
+
+    Returns
+        list: of network function model objects
+    """
+    nf_list = []
+    for nf_name in nf_name_list:
+        nf = NetworkFunctionModel(nf_name, '10.10.10.32', '2001:0db8:0:0:0:0:1428:57ab',
+                                  '687kj45-d396-4efb-af02-6b83499b12f8',
+                                  'e80a6ae3-cafd-4d24-850d-e14c084a5ca9',
+                                  'model_name', 'pm_control', '1.0.2')
+        nf_list.append(nf)
+    return nf_list
+
+
 class BaseClassSetup(TestCase):
     app = None
     app_context = None
diff --git a/components/pm-subscription-handler/tests/test_controller.py b/components/pm-subscription-handler/tests/test_controller.py
index 77ab889..962e8fb 100755
--- a/components/pm-subscription-handler/tests/test_controller.py
+++ b/components/pm-subscription-handler/tests/test_controller.py
@@ -20,14 +20,16 @@
 from unittest.mock import patch, MagicMock
 from http import HTTPStatus
 
-from mod import aai_client
-from mod.api.controller import status, post_subscription, get_subscription_by_name,\
-    get_subscriptions
+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
 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
-from tests.base_setup import create_subscription_data, create_multiple_subscription_data
+from tests.base_setup import create_subscription_data, create_multiple_subscription_data, \
+    create_multiple_network_function_data
+from mod.api.services import measurement_group_service, nf_service
 
 
 class ControllerTestCase(BaseClassSetup):
@@ -165,3 +167,35 @@
     def test_get_subscriptions_api_exception(self):
         subs, status_code = get_subscriptions()
         self.assertEqual(status_code, HTTPStatus.INTERNAL_SERVER_ERROR.value)
+
+    def test_get_meas_group_with_nfs_api(self):
+        sub = create_subscription_data('sub1')
+        nf_list = create_multiple_network_function_data(['pnf101', 'pnf102'])
+        measurement_group_service.save_measurement_group(sub.measurement_groups[0].
+                                                         serialize()['measurementGroup'],
+                                                         sub.subscription_name)
+        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.PENDING_CREATE.value)
+        db.session.commit()
+        mg_with_nfs, status_code = get_meas_group_with_nfs('sub1', 'MG1')
+        self.assertEqual(status_code, HTTPStatus.OK.value)
+        self.assertEqual(mg_with_nfs['subscriptionName'], 'sub1')
+        self.assertEqual(mg_with_nfs['measurementGroupName'], 'MG1')
+        self.assertEqual(mg_with_nfs['administrativeState'], 'UNLOCKED')
+        self.assertEqual(len(mg_with_nfs['networkFunctions']), 2)
+
+    def test_get_meas_group_with_nfs_api_none(self):
+        error, status_code = get_meas_group_with_nfs('sub1', 'MG1')
+        self.assertEqual(error['error'], 'measurement group was not defined with '
+                                         'the sub name: sub1 and meas group name: MG1')
+        self.assertEqual(status_code, HTTPStatus.NOT_FOUND.value)
+
+    @patch('mod.api.services.measurement_group_service.query_meas_group_by_name',
+           MagicMock(side_effect=Exception('something failed')))
+    def test_get_meas_group_with_nfs_api_exception(self):
+        error, status_code = get_meas_group_with_nfs('sub1', 'MG1')
+        self.assertEqual(status_code, HTTPStatus.INTERNAL_SERVER_ERROR.value)