summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xcomponents/pm-subscription-handler/Changelog.md1
-rwxr-xr-xcomponents/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py36
-rw-r--r--components/pm-subscription-handler/pmsh_service/mod/sub_schema.json122
-rw-r--r--components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py8
-rw-r--r--components/pm-subscription-handler/setup.py5
-rwxr-xr-xcomponents/pm-subscription-handler/tests/base_setup.py6
-rw-r--r--components/pm-subscription-handler/tests/data/cbs_data_1.json3
-rw-r--r--components/pm-subscription-handler/tests/data/cbs_invalid_data.json116
-rw-r--r--components/pm-subscription-handler/tests/test_pmsh_utils.py66
-rw-r--r--components/pm-subscription-handler/tests/test_subscription_handler.py18
10 files changed, 374 insertions, 7 deletions
diff --git a/components/pm-subscription-handler/Changelog.md b/components/pm-subscription-handler/Changelog.md
index 8988508c..91900070 100755
--- a/components/pm-subscription-handler/Changelog.md
+++ b/components/pm-subscription-handler/Changelog.md
@@ -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
diff --git a/components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py b/components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py
index 18834134..7b91a307 100755
--- a/components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/pmsh_utils.py
@@ -15,7 +15,10 @@
#
# 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
index 00000000..7a1da5bb
--- /dev/null
+++ b/components/pm-subscription-handler/pmsh_service/mod/sub_schema.json
@@ -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
diff --git a/components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py b/components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py
index f50f5ab2..6238a298 100644
--- a/components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py
+++ b/components/pm-subscription-handler/pmsh_service/mod/subscription_handler.py
@@ -15,9 +15,11 @@
#
# 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,
diff --git a/components/pm-subscription-handler/setup.py b/components/pm-subscription-handler/setup.py
index 65a07703..c8d90783 100644
--- a/components/pm-subscription-handler/setup.py
+++ b/components/pm-subscription-handler/setup.py
@@ -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"]
)
diff --git a/components/pm-subscription-handler/tests/base_setup.py b/components/pm-subscription-handler/tests/base_setup.py
index 2e50dde9..9e12f96e 100755
--- a/components/pm-subscription-handler/tests/base_setup.py
+++ b/components/pm-subscription-handler/tests/base_setup.py
@@ -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()
diff --git a/components/pm-subscription-handler/tests/data/cbs_data_1.json b/components/pm-subscription-handler/tests/data/cbs_data_1.json
index 86515343..2e405d09 100644
--- a/components/pm-subscription-handler/tests/data/cbs_data_1.json
+++ b/components/pm-subscription-handler/tests/data/cbs_data_1.json
@@ -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
index 00000000..92da2b9c
--- /dev/null
+++ b/components/pm-subscription-handler/tests/data/cbs_invalid_data.json
@@ -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
diff --git a/components/pm-subscription-handler/tests/test_pmsh_utils.py b/components/pm-subscription-handler/tests/test_pmsh_utils.py
index 602253b8..1711e013 100644
--- a/components/pm-subscription-handler/tests/test_pmsh_utils.py
+++ b/components/pm-subscription-handler/tests/test_pmsh_utils.py
@@ -1,5 +1,5 @@
# ============LICENSE_START===================================================
-# Copyright (C) 2019-2020 Nordix Foundation.
+# Copyright (C) 2019-2021 Nordix Foundation.
# ============================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -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()
diff --git a/components/pm-subscription-handler/tests/test_subscription_handler.py b/components/pm-subscription-handler/tests/test_subscription_handler.py
index 31dd0943..2293ee50 100644
--- a/components/pm-subscription-handler/tests/test_subscription_handler.py
+++ b/components/pm-subscription-handler/tests/test_subscription_handler.py
@@ -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()