The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [1.4.0]
+### Changed
+* Enhanced API for PMSH subscription management (DCAEGEN2-2802)
## [1.3.2]
### Changed
# ============LICENSE_START===================================================
-# Copyright (C) 2019-2021 Nordix Foundation.
+# Copyright (C) 2020-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.
def launch_api_server(app_config):
connex_app = _get_app()
+ connex_app.app.config['app_config'] = app_config
connex_app.add_api('api/pmsh_swagger.yml')
if app_config.enable_tls:
logger.info('Launching secure http API server')
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 2020-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+
+from http import HTTPStatus
+from mod.api.services import subscription_service
+from connexion import NoContent
+from mod.api.custom_exception import InvalidDataException, DuplicateDataException
+
+
+def post_subscription(body):
+ """
+ Creates a subscription
+
+ Args:
+ body (dict): subscription request body to save.
+
+ Returns:
+ Success : NoContent, 201
+ Invalid Data : List of Invalid messages, 400
+
+ Raises:
+ Error: If anything fails in the server.
+ """
+ response = NoContent, HTTPStatus.CREATED.value
+ try:
+ subscription_service.create_subscription(body['subscription'])
+ except DuplicateDataException as exception:
+ response = exception.duplicate_fields_info, HTTPStatus.CONFLICT.value
+ except InvalidDataException as exception:
+ response = exception.invalid_messages, HTTPStatus.BAD_REQUEST.value
+ return response
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 2020-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+
+class InvalidDataException(Exception):
+ """Exception raised for invalid inputs.
+
+ Attributes:
+ message -- detail on invalid fields
+ """
+
+ def __init__(self, invalid_messages):
+ self.invalid_messages = invalid_messages
+
+
+class DuplicateDataException(Exception):
+ """Exception raised for invalid inputs.
+
+ Attributes:
+ message -- detail on duplicate fields
+ """
+
+ def __init__(self, duplicate_fields_info):
+ self.duplicate_fields_info = duplicate_fields_info
# ============LICENSE_START===================================================
-# Copyright (C) 2019-2020 Nordix Foundation.
+# Copyright (C) 2020-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.
# SPDX-License-Identifier: Apache-2.0
# ============LICENSE_END=====================================================
-from sqlalchemy import Column, Integer, String, ForeignKey
+from sqlalchemy import Column, Integer, String, ForeignKey, JSON
from sqlalchemy.orm import relationship
from mod import db
cascade='all, delete-orphan',
backref='subscription')
+ network_filter = relationship(
+ 'NetworkFunctionFilterModel',
+ cascade='all, delete-orphan',
+ backref='subscription')
+
+ measurement_groups = relationship(
+ 'MeasurementGroupModel',
+ cascade='all, delete-orphan',
+ backref='subscription')
+
def __init__(self, subscription_name, status):
self.subscription_name = subscription_name
self.status = status
__tablename__ = 'network_functions'
id = Column(Integer, primary_key=True, autoincrement=True)
nf_name = Column(String(100), unique=True)
- ip_address = Column(String(50))
+ ipv4_address = Column(String(50))
+ ipv6_address = Column(String(50))
model_invariant_id = Column(String(100))
model_version_id = Column(String(100))
model_name = Column(String(100))
cascade='all, delete-orphan',
backref='nf')
- def __init__(self, nf_name, ip_address, model_invariant_id,
+ def __init__(self, nf_name, ipv4_address, ipv6_address, model_invariant_id,
model_version_id, model_name, sdnc_model_name,
sdnc_model_version, retry_count=0):
self.nf_name = nf_name
- self.ip_address = ip_address
+ self.ipv4_address = ipv4_address
+ self.ipv6_address = ipv6_address
self.model_invariant_id = model_invariant_id
self.model_version_id = model_version_id
self.model_name = model_name
return NetworkFunction(sdnc_model_name=self.sdnc_model_name,
sdnc_model_version=self.sdnc_model_version,
**{'nf_name': self.nf_name,
- 'ip_address': self.ip_address,
+ 'ipv4_address': self.ipv4_address,
+ 'ipv6_address': self.ipv6_address,
'model_invariant_id': self.model_invariant_id,
'model_version_id': self.model_version_id})
NetworkFunctionModel.nf_name == self.nf_name).one_or_none()
db.session.remove()
return {'nf_name': self.nf_name,
- 'ip_address': nf.ip_address,
+ 'ipv4_address': nf.ipv4_address,
+ 'ipv6_address': nf.ipv6_address,
'nf_sub_status': self.nf_sub_status,
'model_invariant_id': nf.model_invariant_id,
'model_version_id': nf.model_version_id,
'model_name': nf.model_name,
'sdnc_model_name': nf.sdnc_model_name,
'sdnc_model_version': nf.sdnc_model_version}
+
+
+class NetworkFunctionFilterModel(db.Model):
+ __tablename__ = 'nf_filter'
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ subscription_name = Column(
+ String,
+ ForeignKey(SubscriptionModel.subscription_name, ondelete='cascade', onupdate='cascade'),
+ unique=True
+ )
+ nf_names = Column(String(100))
+ model_invariant_ids = Column(String(100))
+ model_version_ids = Column(String(100))
+ model_names = Column(String(100))
+
+ def __init__(self, subscription_name, nf_names, model_invariant_ids, model_version_ids,
+ model_names):
+ self.subscription_name = subscription_name
+ self.nf_names = nf_names
+ self.model_invariant_ids = model_invariant_ids
+ self.model_version_ids = model_version_ids
+ 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}'
+
+ def serialize(self):
+ return {'subscription_name': self.subscription_name, 'nf_names': self.nf_names,
+ 'model_invariant_ids': self.model_invariant_ids,
+ 'model_version_ids': self.model_version_ids, 'model_names': self.model_names}
+
+
+class MeasurementGroupModel(db.Model):
+ __tablename__ = 'measurement_group'
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ subscription_name = Column(
+ String,
+ ForeignKey(SubscriptionModel.subscription_name, ondelete='cascade', onupdate='cascade')
+ )
+ measurement_group_name = Column(String(100), unique=True)
+ administrative_state = Column(String(20))
+ file_based_gp = Column(Integer)
+ file_location = Column(String(100))
+ measurement_type = Column(JSON)
+ managed_object_dns_basic = Column(JSON)
+
+ def __init__(self, subscription_name, measurement_group_name,
+ administrative_state, file_based_gp, file_location,
+ measurement_type, managed_object_dns_basic):
+ self.subscription_name = subscription_name
+ self.measurement_group_name = measurement_group_name
+ self.administrative_state = administrative_state
+ self.file_based_gp = file_based_gp
+ self.file_location = file_location
+ self.measurement_type = measurement_type
+ 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}'
+
+ def serialize(self):
+ return {'subscription_name': self.subscription_name,
+ 'measurement_group_name': self.measurement_group_name,
+ 'administrative_state': self.administrative_state,
+ 'file_based_gp': self.file_based_gp,
+ 'file_location': self.file_location,
+ 'measurement_type': self.measurement_type,
+ 'managed_object_dns_basic': self.managed_object_dns_basic}
+
+
+class NfMeasureGroupRelationalModel(db.Model):
+ __tablename__ = 'nf_to_measure_grp_rel'
+ __mapper_args__ = {
+ 'confirm_deleted_rows': False
+ }
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ measurement_grp_name = Column(
+ String,
+ ForeignKey(MeasurementGroupModel.measurement_group_name, ondelete='cascade',
+ onupdate='cascade')
+ )
+ nf_name = Column(
+ String,
+ ForeignKey(NetworkFunctionModel.nf_name, ondelete='cascade', onupdate='cascade')
+ )
+ nf_measure_grp_status = Column(String(20))
+ retry_count = Column(Integer)
+
+ def __init__(self, measurement_grp_name, nf_name, nf_measure_grp_status=None,
+ retry_count=0):
+ self.measurement_grp_name = measurement_grp_name
+ self.nf_name = nf_name
+ self.nf_measure_grp_status = nf_measure_grp_status
+ 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}'
# ============LICENSE_START=======================================================
-# Copyright (C) 2020 Nordix Foundation.
+# Copyright (C) 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.
enum: [healthy, unhealthy]
503:
description: the pmsh service is unavailable
+
+ /subscription:
+ post:
+ tags:
+ - "Subscriptions"
+ description: >-
+ Create a PM Subscription
+ operationId: mod.api.controllers.subscription_controller.post_subscription
+ consumes:
+ - "application/json"
+ produces:
+ - "application/json"
+ parameters:
+ - in: "body"
+ name: "body"
+ description: "Subscription object to be created"
+ required: true
+ schema:
+ type: object
+ properties:
+ subscription:
+ type: object
+ properties:
+ subscriptionName:
+ 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
+ additionalProperties: false
+ measurementGroups:
+ type: array
+ minItems: 1
+ items:
+ type: object
+ properties:
+ measurementGroup:
+ type: object
+ properties:
+ administrativeState:
+ allOf:
+ - type: string
+ - enum:
+ - UNLOCKED
+ - LOCKED
+ - FILTERING
+ fileBasedGP:
+ type: integer
+ fileLocation:
+ type: string
+ 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:
+ - administrativeState
+ - fileBasedGP
+ - fileLocation
+ - measurementTypes
+ - managedObjectDNsBasic
+ required:
+ - measurementGroup
+ required:
+ - subscriptionName
+ - nfFilter
+ - measurementGroups
+ responses:
+ 201:
+ description: successfully created PM Subscription
+ 409:
+ description: Duplicate data
+ 400:
+ description: Invalid input
+
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 2020-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+
+from mod.api.db_models import MeasurementGroupModel, NfMeasureGroupRelationalModel
+from mod import db
+from mod.subscription import SubNfState
+from mod.api.services import nf_service
+from flask import current_app
+
+
+def check_duplicate_fields(measurement_group, subscription_name):
+ """
+ validates the measurement group content if already present
+ and if present raises an exception to indicate duplicate request
+
+ Args:
+ measurement_group (dict): measurement group to validate
+ subscription_name (string): subscription name to check
+ Returns:
+ invalid_messages: list of duplicate data details.
+ """
+ duplicate_field_details = []
+ existing_measurement_grp = (MeasurementGroupModel.query.filter(
+ MeasurementGroupModel.measurement_group_name == measurement_group['measurementGroupName'],
+ MeasurementGroupModel.subscription_name == subscription_name)
+ .one_or_none())
+ if existing_measurement_grp is not None:
+ duplicate_field_details.append(f'Measurement Group: '
+ f'{measurement_group["measurementGroupName"]} '
+ f' for Subscription: {subscription_name} '
+ f'already exists.')
+ return duplicate_field_details
+
+
+def save_measurement_group(measurement_group, subscription_name):
+ """
+ Saves the measurement_group data request
+
+ Args:
+ measurement_group (dict) : measurement group to save
+ subscription_name (string) : subscription name to associate with measurement group.
+ """
+ new_measurement_group = MeasurementGroupModel(
+ subscription_name=subscription_name,
+ measurement_group_name=measurement_group['measurementGroupName'],
+ administrative_state=measurement_group['administrativeState'],
+ file_based_gp=measurement_group['fileBasedGP'],
+ file_location=measurement_group['fileLocation'],
+ measurement_type=measurement_group['measurementTypes'],
+ managed_object_dns_basic=measurement_group['managedObjectDNsBasic'])
+ db.session.add(new_measurement_group)
+
+
+def apply_nf(nf, measurement_group):
+ """
+ Associate and saves the measurement group with Network function
+
+ Args:
+ nf (dict): list of filtered network functions to save.
+ measurement_group (string): measurement group to associate with nf
+ """
+ new_nf_measure_grp_rel = NfMeasureGroupRelationalModel(
+ measurement_grp_name=measurement_group['measurementGroupName'],
+ nf_name=nf.nf_name,
+ nf_measure_grp_status=SubNfState.PENDING_CREATE.value
+ )
+ db.session.add(new_nf_measure_grp_rel)
+
+
+def publish_measurement_group(subscription_name, measurement_group, nfs):
+ """
+ Publishes an event for measurement groups against nfs to MR
+
+ Args:
+ subscription_name (string): subscription name to publish against nfs
+ measurement_group (dict): measurement group to publish
+ nfs (dict): list of filtered network functions to publish.
+ """
+ app_conf = current_app.config['app_config']
+ event_body = {"subscriptionName": subscription_name,
+ "measurementGroup": measurement_group,
+ "networkFunctions": [nf_service.create_nf_event_body(nf, 'CREATE')
+ for nf in nfs]}
+ policy_mr_pub = app_conf.get_mr_pub('policy_pm_publisher')
+ policy_mr_pub.publish_to_topic(event_body)
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 2020-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+from mod import db, aai_client
+from mod.api.db_models import NetworkFunctionFilterModel, NetworkFunctionModel
+from flask import current_app
+from mod.network_function_filter import NetworkFunctionFilter
+
+
+def save_nf_filter(nf_filter, subscription_name):
+ """
+ Saves the nf_filter data request
+
+ Args:
+ nf_filter (dict) : network unction filter to save
+ subscription_name (string) : subscription name to associate with nf filter.
+ """
+ new_filter = NetworkFunctionFilterModel(subscription_name=subscription_name,
+ nf_names=nf_filter['nfNames'],
+ model_invariant_ids=nf_filter['modelInvariantIDs'],
+ model_version_ids=nf_filter['modelVersionIDs'],
+ model_names=nf_filter['modelNames'])
+ db.session.add(new_filter)
+
+
+def capture_filtered_nfs(nf_filter):
+ """
+ Retrieves network functions from AAI client and
+ returns a list of filtered NetworkFunctions using the Filter
+
+ Args:
+ nf_filter (dict): the nf json data from AAI.
+ Returns:
+ NetworkFunction (list): a list of filtered NetworkFunction Objects.
+ """
+ filtered_nfs = None
+ app_conf = current_app.config['app_config']
+ nfs_in_aai = aai_client._get_all_aai_nf_data(app_conf)
+ if nfs_in_aai is not None:
+ nf_filter_module = NetworkFunctionFilter(**nf_filter)
+ filtered_nfs = nf_filter_module.filter_nfs(nfs_in_aai, app_conf)
+ return filtered_nfs
+
+
+def create_nf_event_body(nf, change_type):
+ """
+ Creates a network function event body to publish on MR
+
+ Args:
+ nf (dict): the Network function to include in the event.
+ change_type (string): define the change type to be applied on node
+ Returns:
+ NetworkFunctionEvent (dict): etwork function event body to publish on MR.
+ """
+ app_conf = current_app.config['app_config']
+ return {'networkFunction': {'nfName': nf.nf_name,
+ 'ipv4Address': nf.ipv4_address,
+ 'ipv6Address': nf.ipv6_address,
+ 'blueprintName': nf.sdnc_model_name,
+ 'blueprintVersion': nf.sdnc_model_version,
+ 'policyName': app_conf.operational_policy_name,
+ 'changeType': change_type,
+ 'closedLoopControlName': app_conf.control_loop_name}}
+
+
+def save_nf(nf):
+ """
+ Saves the network function request
+ and also updates model names if missing
+ Args:
+ nf (dict) : requested network function to save
+ """
+ network_function = NetworkFunctionModel.query.filter(
+ NetworkFunctionModel.nf_name == nf.nf_name).one_or_none()
+ if network_function is None:
+ network_function = NetworkFunctionModel(nf_name=nf.nf_name,
+ ipv4_address=nf.ipv4_address,
+ ipv6_address=nf.ipv6_address,
+ model_invariant_id=nf.model_invariant_id,
+ model_version_id=nf.model_version_id,
+ model_name=nf.model_name,
+ sdnc_model_name=nf.sdnc_model_name,
+ sdnc_model_version=nf.sdnc_model_version)
+ db.session.add(network_function)
+ elif network_function.model_name is None:
+ NetworkFunctionModel.query.filter(NetworkFunctionModel.nf_name == nf.nf_name)\
+ .update({NetworkFunctionModel.sdnc_model_name: nf.sdnc_model_name,
+ NetworkFunctionModel.sdnc_model_version: nf.sdnc_model_version,
+ NetworkFunctionModel.model_name: nf.model_name},
+ synchronize_session='evaluate')
+
+
+def validate_nf_filter(nf_filter):
+ invalid_info = []
+ if not [filter_name for filter_name, val in nf_filter.items() if len(val) > 0]:
+ invalid_info.append("At least one filter within nfFilter must not be empty")
+ return invalid_info
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 2020-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+
+from mod import db, logger
+from mod.api.db_models import SubscriptionModel, NfSubRelationalModel
+from mod.api.services import measurement_group_service, nf_service
+from mod.api.custom_exception import InvalidDataException, DuplicateDataException
+from mod.subscription import AdministrativeState
+
+
+def create_subscription(subscription):
+ """
+ Creates a subscription
+
+ Args:
+ subscription (dict): subscription to save.
+
+ Raises:
+ Error: If anything fails in the server.
+ """
+ perform_validation(subscription)
+ try:
+ save_subscription_request(subscription)
+ filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"])
+ if len(filtered_nfs) > 0:
+ save_filtered_nfs(filtered_nfs)
+ apply_subscription_to_nfs(filtered_nfs, subscription["subscriptionName"])
+ apply_measurement_grp_to_nfs(subscription["subscriptionName"],
+ filtered_nfs, subscription.get('measurementGroups'))
+ db.session.commit()
+ except Exception as e:
+ db.session.rollback()
+ logger.error(f'Failed to create subscription '
+ f'{subscription["subscriptionName"]} in the DB: {e}', exc_info=True)
+ raise e
+ finally:
+ db.session.remove()
+
+
+def save_filtered_nfs(filtered_nfs):
+ """
+ Saves a network function
+
+ Args:
+ filtered_nfs (dict): list of filtered network functions to save.
+ """
+ for nf in filtered_nfs:
+ nf_service.save_nf(nf)
+
+
+def apply_subscription_to_nfs(filtered_nfs, subscription_name):
+ """
+ Associate and saves the subscription with Network functions
+
+ Args:
+ filtered_nfs (dict): list of filtered network functions to save.
+ subscription_name (string): subscription name to save against nfs
+ """
+ for nf in filtered_nfs:
+ new_nf_sub_rel = NfSubRelationalModel(subscription_name=subscription_name,
+ nf_name=nf.nf_name)
+ db.session.add(new_nf_sub_rel)
+
+
+def apply_measurement_grp_to_nfs(subscription_name, filtered_nfs, measurement_groups):
+ """
+ Publishes an event for measurement groups against nfs
+ And saves the successful trigger action as PENDING_CREATE
+
+ Args:
+ subscription_name (string): subscription name to publish against nfs
+ filtered_nfs (dict): list of filtered network functions to publish.
+ measurement_groups (dict): list of measurement group to publish
+ """
+ if measurement_groups:
+ for measurement_group in measurement_groups:
+ measurement_group_details = measurement_group['measurementGroup']
+ if measurement_group_details['administrativeState'] \
+ == AdministrativeState.UNLOCKED.value:
+ measurement_group_service.publish_measurement_group(
+ subscription_name, measurement_group_details, filtered_nfs)
+ for nf in filtered_nfs:
+ measurement_group_service.apply_nf(nf, measurement_group_details)
+
+
+def perform_validation(subscription):
+ """
+ validates the subscription and if invalid raises an exception
+ to indicate duplicate/invalid request
+
+ Args:
+ subscription (Subscription): subscription to validate
+
+ Raises:
+ DuplicateDataException: exception containing the list of duplicate data fields.
+ InvalidDataException: exception containing the list of invalid data.
+ """
+ duplicate_messages = check_duplicate_fields(subscription)
+ if duplicate_messages:
+ raise DuplicateDataException(duplicate_messages)
+ invalid_messages = nf_service.validate_nf_filter(subscription["nfFilter"])
+ if invalid_messages:
+ raise InvalidDataException(invalid_messages)
+
+
+def save_subscription_request(subscription):
+ """
+ Saves the subscription request consisting of:
+ network function filter and measurement groups
+
+ Args:
+ subscription (dict): subscription request to be saved.
+ """
+ save_subscription(subscription)
+ nf_service.save_nf_filter(subscription["nfFilter"], subscription["subscriptionName"])
+ if subscription.get('measurementGroups'):
+ for measurement_group in subscription['measurementGroups']:
+ measurement_group_service \
+ .save_measurement_group(measurement_group['measurementGroup'],
+ subscription["subscriptionName"])
+
+
+def check_duplicate_fields(subscription):
+ """
+ validates the subscription content if already present
+ and captures duplicate fields
+
+ Args:
+ subscription (Subscription): subscription to validate
+
+ Returns:
+ invalid_messages: list of invalid data details.
+ """
+ duplicate_field_details = []
+ existing_subscription = (SubscriptionModel.query.filter(
+ SubscriptionModel.subscription_name == subscription['subscriptionName']).one_or_none())
+ if existing_subscription is not None:
+ duplicate_field_details.append(f'subscription Name: {subscription["subscriptionName"]}'
+ f' already exists.')
+ if subscription.get('measurementGroups'):
+ for measurement_group in subscription['measurementGroups']:
+ duplicate_field_details.extend(measurement_group_service.check_duplicate_fields(
+ measurement_group['measurementGroup'], subscription['subscriptionName']))
+ return duplicate_field_details
+
+
+def save_subscription(subscription):
+ """
+ Saves the subscription data
+
+ Args:
+ subscription (dict): subscription request to be saved.
+ """
+ new_subscription = SubscriptionModel(subscription_name=subscription["subscriptionName"],
+ status='LOCKED')
+ db.session.add(new_subscription)
def __init__(self, sdnc_model_name=None, sdnc_model_version=None, **kwargs):
""" Object representation of the NetworkFunction. """
self.nf_name = kwargs.get('nf_name')
- self.ip_address = kwargs.get('ip_address')
+ self.ipv4_address = kwargs.get('ipv4_address')
+ self.ipv6_address = kwargs.get('ipv6_address')
self.model_invariant_id = kwargs.get('model_invariant_id')
self.model_version_id = kwargs.get('model_version_id')
self.model_name = kwargs.get('model_name')
@classmethod
def nf_def(cls):
- return cls(nf_name=None, ip_address=None, model_invariant_id=None,
- model_version_id=None, model_name=None,
+ return cls(nf_name=None, ipv4_address=None, ipv6_address=None,
+ model_invariant_id=None, model_version_id=None, model_name=None,
sdnc_model_name=None, sdnc_model_version=None)
def __str__(self):
return f'nf-name: {self.nf_name}, ' \
- f'ipaddress-v4-oam: {self.ip_address}, ' \
+ f'ipaddress-v4-oam: {self.ipv4_address}, ' \
+ f'ipaddress-v6-oam: {self.ipv6_address}, ' \
f'model-invariant-id: {self.model_invariant_id}, ' \
f'model-version-id: {self.model_version_id}, ' \
f'model-name: {self.model_name}, ' \
def __eq__(self, other):
return \
self.nf_name == other.nf_name and \
- self.ip_address == other.ip_address and \
+ self.ipv4_address == other.ipv4_address and \
+ self.ipv6_address == other.ipv6_address and \
self.model_invariant_id == other.model_invariant_id and \
self.model_version_id == other.model_version_id and \
self.model_name == other.model_name and \
self.sdnc_model_version == other.sdnc_model_version
def __hash__(self):
- return hash((self.nf_name, self.ip_address, self.model_invariant_id,
- self.model_version_id, self.model_name,
+ return hash((self.nf_name, self.ipv4_address, self.ipv6_address,
+ self.model_invariant_id, self.model_version_id, self.model_name,
self.sdnc_model_name, self.sdnc_model_version))
def create(self):
if existing_nf is None:
new_nf = NetworkFunctionModel(nf_name=self.nf_name,
- ip_address=self.ip_address,
+ ipv4_address=self.ipv6_address,
+ ipv6_address=self.ipv6_address,
model_invariant_id=self.model_invariant_id,
model_version_id=self.model_version_id,
model_name=self.model_name,
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+
+import re
+from mod import logger, aai_client
+from mod.network_function import NetworkFunction
+from mod.api.db_models import NetworkFunctionModel
+
+
+class NetworkFunctionFilter:
+ def __init__(self, **kwargs):
+ self.nf_names = kwargs.get('nfNames')
+ self.model_invariant_ids = kwargs.get('modelInvariantIDs')
+ self.model_version_ids = kwargs.get('modelVersionIDs')
+ self.model_names = kwargs.get('modelNames')
+ self.regex_matcher = re.compile('|'.join(raw_regex for raw_regex in self.nf_names))
+
+ def filter_nfs(self, nf_data, app_conf):
+ """
+ Returns a list of filtered NetworkFunctions using the Filter initialised in the class
+
+ Args:
+ nf_data (dict): the nf json data from AAI.
+ app_conf (App_config): the config for making AAI call.
+ Returns:
+ NetworkFunction (list): a list of filtered NetworkFunction Objects.
+
+ Raises:
+ KeyError: if AAI data cannot be parsed.
+ """
+ nf_list = []
+ try:
+ for nf in nf_data['results']:
+ if nf['properties'].get('orchestration-status') != 'Active':
+ continue
+ name_identifier = 'pnf-name' if nf['node-type'] == 'pnf' else 'vnf-name'
+ new_nf = NetworkFunction(
+ nf_name=nf['properties'].get(name_identifier),
+ ipv4_address=nf['properties'].get('ipaddress-v4-oam'),
+ ipv6_address=nf['properties'].get('ipaddress-v6-oam'),
+ model_invariant_id=nf['properties'].get('model-invariant-id'),
+ model_version_id=nf['properties'].get('model-version-id'))
+ if self.is_nf_in_filter(new_nf) \
+ and self.is_sdnc_model_in_filter(new_nf, app_conf):
+ nf_list.append(new_nf)
+ except KeyError as e:
+ logger.error(f'Failed to parse AAI data: {e}', exc_info=True)
+ raise
+ return nf_list
+
+ def is_nf_in_filter(self, nf):
+ """Match the nf fields against the Filter values initialised in the class
+
+ Args:
+ nf (NetworkFunction): The NF to be filtered.
+
+ Returns:
+ bool: True if matched, else False.
+ """
+ match = True
+ if self.nf_names and self.regex_matcher.search(nf.nf_name) is None:
+ match = False
+ if self.model_invariant_ids and nf.model_invariant_id not in self.model_invariant_ids:
+ match = False
+ if self.model_version_ids and nf.model_version_id not in self.model_version_ids:
+ match = False
+ return match
+
+ def is_sdnc_model_in_filter(self, new_nf, app_conf):
+ """
+ saves NetworkFunction model details and confirms.
+
+ Args:
+ new_nf (NetworkFunction): the network function to check.
+ app_conf (App_config): the config for making AAI call.
+ Returns:
+ Boolean : true if model name satisfies
+
+ Raises:
+ KeyError: if AAI data cannot be parsed and logs it.
+ """
+ match = True
+ network_function = NetworkFunctionModel.query.filter(
+ NetworkFunctionModel.nf_name == new_nf.nf_name).one_or_none()
+
+ if network_function is None or network_function.model_name is None:
+ sdnc_model_data = aai_client.get_aai_model_data(app_conf, new_nf.model_invariant_id,
+ new_nf.model_version_id, new_nf.nf_name)
+ try:
+ new_nf.sdnc_model_name = sdnc_model_data['sdnc-model-name']
+ new_nf.sdnc_model_version = sdnc_model_data['sdnc-model-version']
+ new_nf.model_name = sdnc_model_data['model-name']
+ except KeyError as e:
+ logger.info(f'Skipping NF {new_nf.nf_name} as there is no '
+ f'sdnc-model data associated in AAI: {e}', exc_info=True)
+ else:
+ new_nf.sdnc_model_name = network_function.sdnc_model_name
+ new_nf.sdnc_model_version = network_function.sdnc_model_name
+ new_nf.model_name = network_function.model_name
+
+ if self.model_names and new_nf.model_name not in self.model_names:
+ match = False
+ return match
else:
change_type = 'CREATE'
sub_event = {'nfName': nf.nf_name,
- 'ipv4Address': nf.ip_address,
+ 'ipv4Address': nf.ipv4_address,
+ 'ipv6Address': nf.ipv6_address,
'blueprintName': nf.sdnc_model_name,
'blueprintVersion': nf.sdnc_model_version,
'policyName': app_conf.operational_policy_name,
<groupId>org.onap.dcaegen2.services</groupId>
<artifactId>pmsh</artifactId>
<name>dcaegen2-services-pm-subscription-handler</name>
- <version>1.3.1-SNAPSHOT</version>
+ <version>1.4.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sonar.sources>.</sonar.sources>
setup(
name="pm_subscription_handler",
- version="1.3.2",
+ version="1.4.0",
packages=find_packages(),
author="lego@est.tech",
author_email="lego@est.tech",
--- /dev/null
+# ============LICENSE_START===================================================
+# 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+
+import json
+import os
+from unittest.mock import patch, MagicMock
+
+from mod import aai_client
+from tests.base_setup import BaseClassSetup
+from mod.api.controllers import subscription_controller
+from flask import current_app
+
+
+class SubscriptionControllerTestCase(BaseClassSetup):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ def setUp(self):
+ super().setUp()
+ current_app.config['app_config'] = self.app_conf
+ with open(os.path.join(os.path.dirname(__file__),
+ '../data/create_subscription_request.json'), 'r') as data:
+ self.subscription_request = data.read()
+ with open(os.path.join(os.path.dirname(__file__), '../data/aai_xnfs.json'),
+ 'r') as data:
+ self.aai_response_data = data.read()
+
+ def tearDown(self):
+ super().tearDown()
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ @patch('mod.api.services.subscription_service.create_subscription',
+ MagicMock(return_value=None))
+ def test_post_subscription(self):
+ response = subscription_controller.post_subscription(json.loads(self.subscription_request))
+ self.assertEqual(response[1], 201)
+
+ @patch.object(aai_client,
+ '_get_all_aai_nf_data')
+ def test_post_subscription_duplicate_sub(self, mock_aai):
+ mock_aai.return_value = json.loads(self.aai_response_data)
+ response = subscription_controller.post_subscription(json.loads(self.subscription_request))
+ self.assertEqual(response[1], 409)
+ self.assertEqual(response[0][0], 'subscription Name: ExtraPM-All-gNB-R2B already exists.')
--- /dev/null
+{
+ "subscription": {
+ "subscriptionName": "ExtraPM-All-gNB-R2B",
+ "nfFilter": {
+ "nfNames": [
+ "^pnf.*",
+ "^vnf.*"
+ ],
+ "modelInvariantIDs": [
+ "8lk4578-d396-4efb-af02-6b83499b12f8",
+ "687kj45-d396-4efb-af02-6b83499b12f8"
+
+ ],
+ "modelVersionIDs": [
+ "e80a6ae3-cafd-4d24-850d-e14c084a5ca9"
+ ],
+ "modelNames": [
+ "PNF102"
+ ]
+ },
+ "measurementGroups": [
+ {
+ "measurementGroup": {
+ "measurementGroupName": "msrmt_grp_name",
+ "fileBasedGP":15,
+ "fileLocation":"pm.xml",
+ "administrativeState": "UNLOCKED",
+ "measurementTypes": [
+ {
+ "measurementType": "counter_a"
+ }
+ ],
+ "managedObjectDNsBasic": [
+ {
+ "DN": "string"
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
"changeType":"CREATE",\r
"closedLoopControlName":"pmsh-control-loop",\r
"ipv4Address": "1.2.3.4",\r
+ "ipv6Address": "1.2.3.4.5.6",\r
"subscription":{\r
"subscriptionName":"ExtraPM-All-gNB-R2B",\r
"administrativeState":"UNLOCKED",\r
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 2020-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+
+import json
+import os
+from unittest.mock import patch
+from flask import current_app
+from mod import aai_client
+from tests.base_setup import BaseClassSetup
+from mod.api.services import subscription_service, nf_service, measurement_group_service
+
+
+class MeasurementGroupServiceTestCase(BaseClassSetup):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ def setUp(self):
+ super().setUp()
+ current_app.config['app_config'] = self.app_conf
+ with open(os.path.join(os.path.dirname(__file__),
+ '../data/create_subscription_request.json'), 'r') as data:
+ self.subscription_request = data.read()
+ with open(os.path.join(os.path.dirname(__file__), '../data/aai_xnfs.json'), 'r') as data:
+ self.aai_response_data = data.read()
+ with open(os.path.join(os.path.dirname(__file__), '../data/aai_model_info.json'),
+ 'r') as data:
+ self.good_model_info = data.read()
+
+ def tearDown(self):
+ super().tearDown()
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ def create_test_subs(self, new_sub_name, new_msrmt_grp_name):
+ 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(aai_client, '_get_all_aai_nf_data')
+ @patch.object(aai_client, 'get_aai_model_data')
+ def test_capture_filtered_nfs(self, 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)
+ subscription = json.loads(self.subscription_request)['subscription']
+ filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"])
+ self.assertEqual(len(filtered_nfs), 2)
+ self.assertEqual(filtered_nfs[0].nf_name, 'pnf201')
+ self.assertEqual(filtered_nfs[1].nf_name, 'pnf_33_ericsson')
+
+ def test_validate_measurement_group(self):
+ subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new2', 'msrmt_grp_name-new2')
+ subscription = json.loads(subscription)['subscription']
+ measurement1 = subscription['measurementGroups'][0]
+ msg = measurement_group_service.check_duplicate_fields(
+ measurement1['measurementGroup'], subscription["subscriptionName"])
+ self.assertEqual(len(msg), 0)
+
+ @patch.object(nf_service, 'save_nf_filter')
+ def test_validate_measurement_group_invalid(self, mock_save_filter):
+ mock_save_filter.return_value = None
+ subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new2', 'msrmt_grp_name-new2')
+ subscription = json.loads(subscription)['subscription']
+ subscription_service.save_subscription_request(subscription)
+ measurement1 = subscription['measurementGroups'][0]
+ msg = measurement_group_service.check_duplicate_fields(
+ measurement1['measurementGroup'], subscription["subscriptionName"])
+ self.assertEqual(msg[0], 'Measurement Group: msrmt_grp_name-new2 for '
+ 'Subscription: xtraPM-All-gNB-R2B-new2 already exists.')
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 2020-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+
+import json
+import os
+from unittest.mock import patch
+from flask import current_app
+from mod.api.db_models import NetworkFunctionModel
+from mod import aai_client
+from tests.base_setup import BaseClassSetup
+from mod.api.services import nf_service
+
+
+class NetworkFunctionServiceTestCase(BaseClassSetup):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ def setUp(self):
+ super().setUp()
+ current_app.config['app_config'] = self.app_conf
+ with open(os.path.join(os.path.dirname(__file__),
+ '../data/create_subscription_request.json'), 'r') as data:
+ self.subscription_request = data.read()
+ with open(os.path.join(os.path.dirname(__file__), '../data/aai_xnfs.json'), 'r') as data:
+ self.aai_response_data = data.read()
+ with open(os.path.join(os.path.dirname(__file__), '../data/aai_model_info.json'),
+ 'r') as data:
+ self.good_model_info = data.read()
+
+ def tearDown(self):
+ super().tearDown()
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ def create_test_subs(self, new_sub_name, new_msrmt_grp_name):
+ 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(aai_client, '_get_all_aai_nf_data')
+ @patch.object(aai_client, 'get_aai_model_data')
+ def test_capture_filtered_nfs(self, 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)
+ subscription = json.loads(self.subscription_request)['subscription']
+ filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"])
+ self.assertEqual(len(filtered_nfs), 2)
+ self.assertEqual(filtered_nfs[0].nf_name, 'pnf201')
+ self.assertEqual(filtered_nfs[1].nf_name, 'pnf_33_ericsson')
+
+ @patch.object(aai_client, '_get_all_aai_nf_data')
+ def test_capture_filtered_nfs_with_no_aai_nodes(self, mock_aai):
+ mock_aai.return_value = None
+ subscription = json.loads(self.subscription_request)['subscription']
+ filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"])
+ self.assertIsNone(filtered_nfs)
+
+ @patch.object(aai_client, '_get_all_aai_nf_data')
+ @patch.object(aai_client, 'get_aai_model_data')
+ def test_create_nf_event_body(self, 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)
+ subscription = json.loads(self.subscription_request)['subscription']
+ nf = nf_service.capture_filtered_nfs(subscription["nfFilter"])[0]
+ event_body = nf_service.create_nf_event_body(nf, 'CREATE')
+ self.assertEqual(event_body['networkFunction']['nfName'], nf.nf_name)
+ self.assertEqual(event_body['networkFunction']['ipv4Address'], nf.ipv4_address)
+ self.assertEqual(event_body['networkFunction']['ipv6Address'], nf.ipv6_address)
+ self.assertEqual(event_body['networkFunction']['blueprintName'], nf.sdnc_model_name)
+ self.assertEqual(event_body['networkFunction']['blueprintVersion'], nf.sdnc_model_version)
+ self.assertEqual(event_body['networkFunction']['policyName'],
+ self.app_conf.operational_policy_name)
+ self.assertEqual(event_body['networkFunction']['changeType'], 'CREATE')
+ self.assertEqual(event_body['networkFunction']['closedLoopControlName'],
+ self.app_conf.control_loop_name)
+
+ @patch.object(aai_client, '_get_all_aai_nf_data')
+ @patch.object(aai_client, 'get_aai_model_data')
+ def test_save_nf_new_nf(self, 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)
+ subscription = json.loads(self.subscription_request)['subscription']
+ nf = nf_service.capture_filtered_nfs(subscription["nfFilter"])[0]
+ nf.nf_name = 'newnf1'
+ nf_service.save_nf(nf)
+ network_function = NetworkFunctionModel.query.filter(
+ NetworkFunctionModel.nf_name == nf.nf_name).one_or_none()
+ self.assertIsNotNone(network_function)
+
+ @patch.object(aai_client, '_get_all_aai_nf_data')
+ @patch.object(aai_client, 'get_aai_model_data')
+ def test_save_nf_missing_model(self, 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)
+ subscription = json.loads(self.subscription_request)['subscription']
+ nf = nf_service.capture_filtered_nfs(subscription["nfFilter"])[0]
+ nf.nf_name = 'newnf2'
+ nf.model_name = None
+ nf_service.save_nf(nf)
+ nf.model_name = 'new_model_name'
+ nf_service.save_nf(nf)
+ network_function = NetworkFunctionModel.query.filter(
+ NetworkFunctionModel.nf_name == nf.nf_name).one_or_none()
+ self.assertIsNotNone(network_function)
+ self.assertEqual(network_function.model_name, 'new_model_name')
--- /dev/null
+# ============LICENSE_START===================================================
+# Copyright (C) 2020-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END=====================================================
+import copy
+import json
+import os
+from unittest.mock import patch, MagicMock
+from flask import current_app
+from mod.api.db_models import SubscriptionModel, MeasurementGroupModel, \
+ NfMeasureGroupRelationalModel
+from mod.subscription import SubNfState
+from mod import aai_client
+from mod.api.custom_exception import DuplicateDataException, InvalidDataException
+from mod.pmsh_utils import _MrPub
+from tests.base_setup import BaseClassSetup
+from mod.api.services import subscription_service, nf_service, measurement_group_service
+
+
+class SubscriptionServiceTestCase(BaseClassSetup):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ def setUp(self):
+ super().setUp()
+ current_app.config['app_config'] = self.app_conf
+ with open(os.path.join(os.path.dirname(__file__),
+ '../data/create_subscription_request.json'), 'r') as data:
+ self.subscription_request = data.read()
+ with open(os.path.join(os.path.dirname(__file__), '../data/aai_xnfs.json'), 'r') as data:
+ self.aai_response_data = data.read()
+ with open(os.path.join(os.path.dirname(__file__), '../data/aai_model_info.json'),
+ 'r') as data:
+ self.good_model_info = data.read()
+
+ def tearDown(self):
+ super().tearDown()
+
+ @classmethod
+ def tearDownClass(cls):
+ super().tearDownClass()
+
+ def create_test_subs(self, new_sub_name, new_msrmt_grp_name):
+ 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('mod.api.services.nf_service.save_nf_filter', MagicMock(return_value=None))
+ @patch('mod.pmsh_utils._MrPub.publish_to_topic', MagicMock(return_value=None))
+ @patch.object(aai_client, '_get_all_aai_nf_data')
+ @patch.object(aai_client, 'get_aai_model_data')
+ def test_create_subscription(self, 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)
+ subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new', 'msrmt_grp_name-new')
+ subscription_service.create_subscription(json.loads(subscription)['subscription'])
+ existing_subscription = (SubscriptionModel.query.filter(
+ SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-new').one_or_none())
+ self.assertIsNotNone(existing_subscription)
+ existing_measurement_grp = (MeasurementGroupModel.query.filter(
+ MeasurementGroupModel.measurement_group_name == 'msrmt_grp_name-new',
+ MeasurementGroupModel.subscription_name == 'xtraPM-All-gNB-R2B-new').one_or_none())
+ self.assertIsNotNone(existing_measurement_grp)
+ msr_grp_nf_rel = (NfMeasureGroupRelationalModel.query.filter(
+ NfMeasureGroupRelationalModel.measurement_grp_name == 'msrmt_grp_name-new')).all()
+ for pubslished_event in msr_grp_nf_rel:
+ self.assertEqual(pubslished_event.nf_measure_grp_status,
+ SubNfState.PENDING_CREATE.value)
+
+ @patch('mod.api.services.nf_service.save_nf_filter', MagicMock(return_value=None))
+ @patch.object(aai_client, '_get_all_aai_nf_data')
+ def test_create_subscription_service_failed_rollback(self, mock_aai):
+ mock_aai.side_effect = InvalidDataException(["AAI call failed"])
+ subscription = self.create_test_subs('xtraPM-All-gNB-R2B-fail', 'msrmt_grp_name-fail')
+ try:
+ subscription_service.create_subscription(json.loads(subscription)['subscription'])
+ except InvalidDataException as exception:
+ self.assertEqual(exception.invalid_messages, ["AAI call failed"])
+
+ # Checking Rollback
+ existing_subscription = (SubscriptionModel.query.filter(
+ SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-fail').one_or_none())
+ self.assertIsNone(existing_subscription)
+
+ def test_perform_validation_existing_sub(self):
+ try:
+ subscription_service.create_subscription(json.loads(self.subscription_request)
+ ['subscription'])
+ except DuplicateDataException as exception:
+ self.assertEqual(exception.duplicate_fields_info[0],
+ "subscription Name: ExtraPM-All-gNB-R2B already exists.")
+
+ @patch.object(nf_service, 'save_nf_filter')
+ def test_save_subscription_request(self, mock_save_filter):
+ mock_save_filter.return_value = None
+ subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new1', 'msrmt_grp_name-new1')
+ subscription_service.save_subscription_request(json.loads(subscription)['subscription'])
+ existing_subscription = (SubscriptionModel.query.filter(
+ SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-new1').one_or_none())
+ self.assertIsNotNone(existing_subscription)
+ self.assertTrue(mock_save_filter.called)
+ existing_measurement_grp = (MeasurementGroupModel.query.filter(
+ MeasurementGroupModel.measurement_group_name == 'msrmt_grp_name-new1',
+ MeasurementGroupModel.subscription_name == 'xtraPM-All-gNB-R2B-new1').one_or_none())
+ self.assertIsNotNone(existing_measurement_grp)
+
+ @patch.object(nf_service, 'save_nf_filter')
+ def test_save_subscription_request_no_measure_grp(self, mock_save_filter):
+ mock_save_filter.return_value = None
+ subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new1', 'msrmt_grp_name-new1')
+ subscription = json.loads(subscription)['subscription']
+ del subscription['measurementGroups']
+ subscription_service.save_subscription_request(subscription)
+ existing_subscription = (SubscriptionModel.query.filter(
+ SubscriptionModel.subscription_name == 'xtraPM-All-gNB-R2B-new1').one_or_none())
+ self.assertIsNotNone(existing_subscription)
+ self.assertTrue(mock_save_filter.called)
+ existing_measurement_grp = (MeasurementGroupModel.query.filter(
+ MeasurementGroupModel.measurement_group_name == 'msrmt_grp_name-new1',
+ MeasurementGroupModel.subscription_name == 'xtraPM-All-gNB-R2B-new1').one_or_none())
+ self.assertIsNone(existing_measurement_grp)
+
+ @patch.object(measurement_group_service, 'apply_nf')
+ @patch.object(_MrPub, 'publish_to_topic')
+ @patch.object(aai_client, '_get_all_aai_nf_data')
+ @patch.object(aai_client, 'get_aai_model_data')
+ def test_apply_measurement_grp_to_nfs(self, mock_model_aai, mock_aai,
+ mock_publish, mock_apply_nf):
+ mock_aai.return_value = json.loads(self.aai_response_data)
+ mock_model_aai.return_value = json.loads(self.good_model_info)
+ mock_publish.return_value = None
+ mock_apply_nf.return_value = None
+ subscription = self.create_test_subs('xtraPM-All-gNB-R2B-new2', 'msrmt_grp_name-new2')
+ subscription = json.loads(subscription)['subscription']
+ measurement1 = subscription['measurementGroups'][0]
+ measurement2 = self.create_measurement_grp(measurement1, 'meas2', 'UNLOCKED')
+ measurement3 = self.create_measurement_grp(measurement1, 'meas3', 'LOCKED')
+ subscription['measurementGroups'].extend([measurement2, measurement3])
+ filtered_nfs = nf_service.capture_filtered_nfs(subscription["nfFilter"])
+ subscription_service.apply_measurement_grp_to_nfs(
+ subscription["subscriptionName"], filtered_nfs, subscription.get('measurementGroups'))
+ # Two unlocked measurement Group published
+ self.assertEqual(mock_publish.call_count, 2)
+ # 2 measurement group with 2 nfs each contribute 4 calls
+ self.assertEqual(mock_apply_nf.call_count, 4)
+
+ def create_measurement_grp(self, measurement, measurement_name, admin_status):
+ new_measurement = copy.deepcopy(measurement)
+ new_measurement['measurementGroup']['measurementGroupName'] = measurement_name
+ new_measurement['measurementGroup']['administrativeState'] = admin_status
+ return new_measurement
super().setUp()
self.nf_1 = NetworkFunction(sdnc_model_name='blah', sdnc_model_version=1.0,
**{'nf_name': 'pnf_1',
- 'ip_address': '1.2.3.4',
+ 'ipv4_address': '1.2.3.4',
+ 'ipv6_address': '1.2.3.4.5.6',
'model_invariant_id': 'some_id',
'model_version_id': 'some_other_id'})
self.nf_2 = NetworkFunction(sdnc_model_name='blah', sdnc_model_version=2.0,
**{'nf_name': 'pnf_2',
- 'ip_address': '1.2.3.4',
+ 'ipv4_address': '1.2.3.4',
+ 'ipv6_address': '1.2.3.4.5.6',
'model_invariant_id': 'some_id',
'model_version_id': 'some_other_id'})
with open(os.path.join(os.path.dirname(__file__), 'data/aai_model_info.json'), 'r') as data:
'data/pm_subscription_event.json'), 'r') as data:
expected_sub_event = json.load(data)
nf = NetworkFunction(nf_name='pnf_1',
- ip_address='1.2.3.4',
+ ipv4_address='1.2.3.4',
+ ipv6_address='1.2.3.4.5.6',
model_invariant_id='some-id',
model_version_id='some-id')
nf.sdnc_model_name = 'some-name'
MagicMock(return_value=NetworkFunctionModel(nf_name='pnf_1',
model_invariant_id='some-id',
model_version_id='some-id',
- ip_address='ip_address',
+ ipv4_address='ip_address4',
+ ipv6_address='ip_address6',
model_name='model_name',
sdnc_model_name='sdnc_model_name',
sdnc_model_version='sdnc_model_version')))
MagicMock(return_value=NetworkFunctionModel(nf_name='pnf_1',
model_invariant_id='some-id',
model_version_id='some-id',
- ip_address='ip_address',
+ ipv4_address='ip_address4',
+ ipv6_address='ip_address6',
model_name='model_name',
sdnc_model_name='sdnc_model_name',
sdnc_model_version='sdnc_model_version',
major=1
-minor=3
-patch=2
+minor=4
+patch=0
base_version=${major}.${minor}.${patch}
release_version=${base_version}
snapshot_version=${base_version}-SNAPSHOT