[PMSH] Validate schema of PMSH monitoring policy 58/116958/8
authorshivasubedi <shiva.subedi@est.tech>
Mon, 18 Jan 2021 13:59:18 +0000 (13:59 +0000)
committershivasubedi <shiva.subedi@est.tech>
Tue, 9 Feb 2021 10:57:23 +0000 (10:57 +0000)
Change-Id: I42b002f855a03b39ab85cfcb20d7857d30447e40
Signed-off-by: shivasubedi <shiva.subedi@est.tech>
Issue-ID: DCAEGEN2-2152

components/pm-subscription-handler/Changelog.md
components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py
components/pm-subscription-handler/pmsh_service/mod/sub_schema.json [new file with mode: 0644]
components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py
components/pm-subscription-handler/setup.py
components/pm-subscription-handler/tests/base_setup.py
components/pm-subscription-handler/tests/data/cbs_data_1.json
components/pm-subscription-handler/tests/data/cbs_invalid_data.json [new file with mode: 0644]
components/pm-subscription-handler/tests/test_pmsh_utils.py
components/pm-subscription-handler/tests/test_subscription_handler.py

index 8988508..9190007 100755 (executable)
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 * Added Resource Name (model-name) to filter (DCAEGEN2-2402)
 * Added retry mechanism for DELETE_FAILED subscriptions on given NFs (DCAEGEN2-2152)
 * Added func to update the subscription object on ACTIVATE/UNLOCK (DCAEGEN2-2152)
+* Added validation for schema of PMSH monitoring policy (DCAEGEN2-2152)
 
 ## [1.1.2]
 ### Changed
index 1883413..7b91a30 100755 (executable)
 #
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
+import json
+import os
 import uuid
+from json import JSONDecodeError
 from os import getenv
 from threading import Timer
 
@@ -24,8 +27,8 @@ from onap_dcae_cbs_docker_client.client import get_all
 from onaplogging.mdcContext import MDC
 from requests.auth import HTTPBasicAuth
 from tenacity import wait_fixed, stop_after_attempt, retry, retry_if_exception_type
+from jsonschema import validate, ValidationError
 
-import mod.network_function
 from mod import logger
 from mod.subscription import Subscription
 
@@ -41,6 +44,7 @@ def mdc_handler(function):
         kwargs['request_id'] = request_id
         kwargs['invocation_id'] = invocation_id
         return function(*args, **kwargs)
+
     return decorator
 
 
@@ -58,6 +62,16 @@ class MySingleton(object):
         return type(clz.__name__, (MySingleton,), dict(clz.__dict__))
 
 
+def _load_sub_schema_from_file():
+    try:
+        with open(os.path.join(os.path.dirname(__file__), 'sub_schema.json')) as sub:
+            return json.load(sub)
+    except OSError as err:
+        logger.error(f'Failed to read sub schema file: {err}', exc_info=True)
+    except JSONDecodeError as json_err:
+        logger.error(f'sub schema file is not a valid JSON file: {json_err}', exc_info=True)
+
+
 class AppConfig:
     INSTANCE = None
 
@@ -73,8 +87,9 @@ class AppConfig:
         self.streams_publishes = conf['config'].get('streams_publishes')
         self.operational_policy_name = conf['config'].get('operational_policy_name')
         self.control_loop_name = conf['config'].get('control_loop_name')
+        self.sub_schema = _load_sub_schema_from_file()
         self.subscription = Subscription(**conf['policy']['subscription'])
-        self.nf_filter = mod.network_function.NetworkFunctionFilter(**self.subscription.nfFilter)
+        self.nf_filter = None
 
     def __new__(cls, *args, **kwargs):
         if AppConfig.INSTANCE is None:
@@ -103,6 +118,23 @@ class AppConfig:
             logger.error(f'Failed to get config from CBS: {e}', exc_info=True)
             raise ValueError(e)
 
+    def validate_sub_schema(self):
+        """
+        Validates schema of PMSH subscription
+
+        Raises:
+            ValidationError: If the PMSH subscription schema is invalid
+        """
+        sub_data = self.subscription.__dict__
+        validate(instance=sub_data, schema=self.sub_schema)
+        nf_filter = sub_data["nfFilter"]
+        for filter_name in nf_filter:
+            if len(nf_filter[filter_name]) > 0:
+                break
+        else:
+            raise ValidationError("At least one filter within nfFilter must not be empty")
+        logger.debug("Subscription schema is valid.")
+
     def refresh_config(self):
         """
         Update the relevant attributes of the AppConfig object.
diff --git a/components/pm-subscription-handler/pmsh_service/mod/sub_schema.json b/components/pm-subscription-handler/pmsh_service/mod/sub_schema.json
new file mode 100644 (file)
index 0000000..7a1da5b
--- /dev/null
@@ -0,0 +1,122 @@
+{
+   "type":"object",
+   "properties":{
+      "subscriptionName":{
+         "type":"string"
+      },
+      "administrativeState":{
+         "allOf":[
+            {
+               "type":"string"
+            },
+            {
+               "enum":[
+                  "UNLOCKED",
+                  "LOCKED"
+               ]
+            }
+         ]
+      },
+      "fileBasedGP":{
+         "type":"integer"
+      },
+      "fileLocation":{
+         "type":"string"
+      },
+      "nfFilter":{
+         "type":"object",
+         "properties":{
+            "nfNames":{
+               "type":"array",
+               "items":{
+                  "type":"string"
+               }
+            },
+            "modelInvariantIDs":{
+               "type":"array",
+               "items":{
+                  "type":"string"
+               }
+            },
+            "modelVersionIDs":{
+               "type":"array",
+               "items":{
+                  "type":"string"
+               }
+            },
+            "modelNames":{
+               "type":"array",
+               "items":{
+                  "type":"string"
+               }
+            }
+         },
+         "required":[
+            "nfNames",
+            "modelInvariantIDs",
+            "modelVersionIDs",
+            "modelNames"
+         ]
+      },
+      "measurementGroups":{
+         "type":"array",
+         "minItems": 1,
+         "items":{
+            "type":"object",
+            "properties":{
+               "measurementGroup":{
+                  "type":"object",
+                  "properties":{
+                     "measurementTypes":{
+                        "type":"array",
+                        "minItems": 1,
+                        "items":{
+                           "type":"object",
+                           "properties":{
+                              "measurementType":{
+                                 "type":"string"
+                              }
+                           },
+                           "required":[
+                              "measurementType"
+                           ]
+                        }
+                     },
+                     "managedObjectDNsBasic":{
+                        "type":"array",
+                        "minItems": 1,
+                        "items":{
+                           "type":"object",
+                           "properties":{
+                              "DN":{
+                                 "type":"string"
+                              }
+                           },
+                           "required":[
+                              "DN"
+                           ]
+                        }
+                     }
+                  },
+                  "required":[
+                     "measurementTypes",
+                     "managedObjectDNsBasic"
+                  ]
+               }
+            },
+            "required":[
+               "measurementGroup"
+            ]
+         }
+      }
+   },
+   "required":[
+      "subscriptionName",
+      "administrativeState",
+      "fileBasedGP",
+      "fileLocation",
+      "nfFilter",
+      "measurementGroups"
+   ]
+
+}
\ No newline at end of file
index f50f5ab..6238a29 100644 (file)
 #
 # SPDX-License-Identifier: Apache-2.0
 # ============LICENSE_END=====================================================
+from jsonschema import ValidationError
 
 from mod import logger, aai_client
 from mod.aai_event_handler import process_aai_events
+from mod.network_function import NetworkFunctionFilter
 from mod.pmsh_utils import PeriodicTask
 from mod.subscription import AdministrativeState
 
@@ -42,12 +44,16 @@ class SubscriptionHandler:
                 self._check_for_failed_nfs()
             else:
                 self.app_conf.refresh_config()
+                self.app_conf.validate_sub_schema()
                 new_administrative_state = self.app_conf.subscription.administrativeState
                 if local_admin_state == new_administrative_state:
                     logger.info(f'Administrative State did not change in the app config: '
                                 f'{new_administrative_state}')
                 else:
                     self._check_state_change(local_admin_state, new_administrative_state)
+        except (ValidationError, TypeError) as err:
+            logger.error(f'Error occurred during validation of subscription schema {err}',
+                         exc_info=True)
         except Exception as err:
             logger.error(f'Error occurred during the activation/deactivation process {err}',
                          exc_info=True)
@@ -65,6 +71,8 @@ class SubscriptionHandler:
             raise Exception(f'Invalid AdministrativeState: {new_administrative_state}')
 
     def _activate(self, new_administrative_state):
+        if not self.app_conf.nf_filter:
+            self.app_conf.nf_filter = NetworkFunctionFilter(**self.app_conf.subscription.nfFilter)
         self._start_aai_event_thread()
         self.app_conf.subscription.update_sub_params(new_administrative_state,
                                                      self.app_conf.subscription.fileBasedGP,
index 65a0770..c8d9078 100644 (file)
@@ -1,5 +1,5 @@
 # ============LICENSE_START=======================================================
-#  Copyright (C) 2019-2020 Nordix Foundation.
+#  Copyright (C) 2019-2021 Nordix Foundation.
 #  Copyright 2020 Deutsche Telekom. All rights reserved.
 # ================================================================================
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -39,5 +39,6 @@ setup(
         "psycopg2-binary==2.8.4",
         "onap_dcae_cbs_docker_client==2.1.1",
         "onappylog==1.0.9",
-        "ruamel.yaml==0.16.10"]
+        "ruamel.yaml==0.16.10",
+        "jsonschema==3.2.0"]
 )
index 2e50dde..9e12f96 100755 (executable)
@@ -21,11 +21,12 @@ from unittest import TestCase
 from unittest.mock import patch, MagicMock
 
 from mod import create_app, db
+from mod.network_function import NetworkFunctionFilter
 from mod.pmsh_utils import AppConfig
 
 
-def get_pmsh_config():
-    with open(os.path.join(os.path.dirname(__file__), 'data/cbs_data_1.json'), 'r') as data:
+def get_pmsh_config(file_path='data/cbs_data_1.json'):
+    with open(os.path.join(os.path.dirname(__file__), file_path), 'r') as data:
         return json.load(data)
 
 
@@ -48,6 +49,7 @@ class BaseClassSetup(TestCase):
         os.environ['AAI_SERVICE_PORT'] = '8443'
         db.create_all()
         self.app_conf = AppConfig()
+        self.app_conf.nf_filter = NetworkFunctionFilter(**self.app_conf.subscription.nfFilter)
 
     def tearDown(self):
         db.drop_all()
index 8651534..2e405d0 100644 (file)
@@ -15,6 +15,9 @@
             ],
             "modelVersionIDs": [
 
+            ],
+            "modelNames": [
+
             ]
          },
          "measurementGroups":[
diff --git a/components/pm-subscription-handler/tests/data/cbs_invalid_data.json b/components/pm-subscription-handler/tests/data/cbs_invalid_data.json
new file mode 100644 (file)
index 0000000..92da2b9
--- /dev/null
@@ -0,0 +1,116 @@
+{
+   "policy":{
+      "subscription":{
+         "subscriptionName":"ExtraPM-All-gNB-R2B",
+         "administrativeState":"UNLOCKED",
+         "fileBasedGP":15,
+         "fileLocation":"\/pm\/pm.xml",
+         "nfFilter":{
+            "nfNames":[
+
+            ],
+            "modelInvariantIDs": [
+
+            ],
+            "modelVersionIDs": [
+
+            ],
+            "modelNames": [
+
+            ]
+         },
+         "measurementGroups":[
+            {
+               "measurementGroup":{
+                  "measurementTypes":[
+                     {
+                        "measurementType":"countera"
+                     },
+                     {
+                        "measurementType":"counterb"
+                     }
+                  ],
+                  "managedObjectDNsBasic":[
+                     {
+                        "DN":"dna"
+                     },
+                     {
+                        "DN":"dnb"
+                     }
+                  ]
+               }
+            },
+            {
+               "measurementGroup":{
+                  "measurementTypes":[
+                     {
+                        "measurementType":"counterc"
+                     },
+                     {
+                        "measurementType":"counterd"
+                     }
+                  ],
+                  "managedObjectDNsBasic":[
+                     {
+                        "DN":"dnc"
+                     },
+                     {
+                        "DN":"dnd"
+                     }
+                  ]
+               }
+            }
+         ]
+      }
+   },
+   "config":{
+      "control_loop_name": "pmsh-control-loop",
+      "operational_policy_name": "pmsh-operational-policy",
+      "aaf_password":"demo123456!",
+      "aaf_identity":"dcae@dcae.onap.org",
+      "cert_path":"/opt/app/pmsh/etc/certs/cert.pem",
+      "key_path":"/opt/app/pmsh/etc/certs/key.pem",
+      "ca_cert_path":"/opt/app/pmsh/etc/certs/cacert.pem",
+      "enable_tls":"true",
+      "streams_subscribes":{
+         "aai_subscriber":{
+            "type":"message_router",
+            "dmaap_info":{
+               "topic_url":"https://message-router:3905/events/AAI_EVENT",
+               "client_role":"org.onap.dcae.aaiSub",
+               "location":"san-francisco",
+               "client_id":"1575976809466"
+            }
+         },
+         "policy_pm_subscriber":{
+            "type":"message_router",
+            "dmaap_info":{
+               "topic_url":"https://message-router:3905/events/org.onap.dmaap.mr.PM_SUBSCRIPTIONS",
+               "client_role":"org.onap.dcae.pmSubscriber",
+               "location":"san-francisco",
+               "client_id":"1575876809456"
+            }
+         }
+      },
+      "streams_publishes":{
+         "policy_pm_publisher":{
+            "type":"message_router",
+            "dmaap_info":{
+               "topic_url":"https://message-router:3905/events/org.onap.dmaap.mr.PM_SUBSCRIPTIONS",
+               "client_role":"org.onap.dcae.pmPublisher",
+               "location":"san-francisco",
+               "client_id":"1475976809466"
+            }
+         },
+         "other_publisher":{
+            "type":"message_router",
+            "dmaap_info":{
+               "topic_url":"https://message-router:3905/events/org.onap.dmaap.mr.SOME_OTHER_TOPIC",
+               "client_role":"org.onap.dcae.pmControlPub",
+               "location":"san-francisco",
+               "client_id":"1875976809466"
+            }
+         }
+      }
+   }
+}
\ No newline at end of file
index 602253b..1711e01 100644 (file)
@@ -1,5 +1,5 @@
 # ============LICENSE_START===================================================
-#  Copyright (C) 2019-2020 Nordix Foundation.
+#  Copyright (C) 2019-2021 Nordix Foundation.
 # ============================================================================
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ from test.support import EnvironmentVarGuard
 from unittest.mock import patch, Mock
 
 import responses
+from jsonschema import ValidationError
 from requests import Session
 from tenacity import RetryError
 
@@ -142,3 +143,66 @@ class PmshUtilsTestCase(BaseClassSetup):
         with self.assertRaises(RetryError):
             self.app_conf.refresh_config()
         mock_logger.assert_called_with('Failed to refresh PMSH AppConfig')
+
+    @patch('mod.logger.debug')
+    def test_utils_validate_config_subscription(self, mock_logger):
+        self.app_conf.validate_sub_schema()
+        mock_logger.assert_called_with("Subscription schema is valid.")
+
+    @patch('mod.logger.debug')
+    def test_utils_validate_config_subscription_administrativeState_locked(self, mock_logger):
+        self.app_conf.subscription.administrativeState = "LOCKED"
+        self.app_conf.validate_sub_schema()
+        mock_logger.assert_called_with("Subscription schema is valid.")
+
+    def test_utils_validate_config_subscription_administrativeState_invalid_value(self):
+        self.app_conf.subscription.administrativeState = "FAILED"
+        with self.assertRaises(ValidationError):
+            self.app_conf.validate_sub_schema()
+
+    def test_utils_validate_config_subscription_nfFilter_failed(self):
+        self.app_conf.subscription.nfFilter = {}
+        with self.assertRaises(ValidationError):
+            self.app_conf.validate_sub_schema()
+
+    def test_utils_validate_config_subscription_where_measurementTypes_is_empty(self):
+        self.app_conf.subscription.measurementGroups = [{
+            "measurementGroup": {
+                "measurementTypes": [
+                ],
+                "managedObjectDNsBasic": [
+                    {
+                        "DN": "dna"
+                    },
+                    {
+                        "DN": "dnb"
+                    }
+                ]
+            }
+        }]
+        with self.assertRaises(ValidationError):
+            self.app_conf.validate_sub_schema()
+
+    def test_utils_validate_config_subscription_where_managedObjectDNsBasic_is_empty(self):
+        self.app_conf.subscription.measurementGroups = [{
+            "measurementGroup": {
+                "measurementTypes": [
+                    {
+                        "measurementType": "countera"
+                    },
+                    {
+                        "measurementType": "counterb"
+                    }
+                ],
+                "managedObjectDNsBasic": [
+
+                ]
+            }
+        }]
+        with self.assertRaises(ValidationError):
+            self.app_conf.validate_sub_schema()
+
+    def test_utils_validate_config_subscription_where_measurementGroups_is_empty(self):
+        self.app_conf.subscription.measurementGroups = []
+        with self.assertRaises(ValidationError):
+            self.app_conf.validate_sub_schema()
index 31dd094..2293ee5 100644 (file)
@@ -162,3 +162,21 @@ class SubscriptionHandlerTest(BaseClassSetup):
                                           self.app_conf)
         sub_handler.execute()
         mock_nf_del.assert_called_once()
+
+    @patch('mod.pmsh_utils.AppConfig._get_pmsh_config',
+           MagicMock(return_value=get_pmsh_config('data/cbs_invalid_data.json')))
+    @patch('mod.subscription_handler.SubscriptionHandler._check_state_change')
+    def test_execute_invalid_schema(self, mock_change_state_check):
+        sub_handler = SubscriptionHandler(self.mock_mr_pub, self.mock_mr_sub, self.app,
+                                          self.app_conf)
+        sub_handler.execute()
+        mock_change_state_check.assert_not_called()
+
+    @patch('mod.pmsh_utils.AppConfig._get_pmsh_config',
+           MagicMock(return_value=get_pmsh_config()))
+    @patch('mod.subscription_handler.SubscriptionHandler._check_state_change')
+    def test_execute_valid_schema(self, mock_change_state_check):
+        sub_handler = SubscriptionHandler(self.mock_mr_pub, self.mock_mr_sub, self.app,
+                                          self.app_conf)
+        sub_handler.execute()
+        mock_change_state_check.assert_called_once()