aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/__init__.py0
-rw-r--r--apps/license/__init__.py0
-rw-r--r--apps/license/optimizers/__init__.py0
-rw-r--r--apps/license/optimizers/simple_license_allocation.py43
-rw-r--r--apps/nsst/__init__.py0
-rw-r--r--apps/nsst/models/api/nsstSelectionRequest.py43
-rw-r--r--apps/nsst/optimizers/__init__.py0
-rw-r--r--apps/nsst/optimizers/conf/configIinputs.json53
-rw-r--r--apps/nsst/optimizers/nsst_select_processor.py155
-rw-r--r--apps/nst/__init__.py0
-rw-r--r--apps/nst/models/api/nstSelectionRequest.py43
-rw-r--r--apps/nst/optimizers/__init__.py0
-rw-r--r--apps/nst/optimizers/conf/configIinputs.json53
-rw-r--r--apps/nst/optimizers/nst_select_processor.py155
-rw-r--r--apps/nxi_termination/__init__.py0
-rw-r--r--apps/nxi_termination/models/api/_init_.py0
-rw-r--r--apps/nxi_termination/models/api/nxi_termination_request.py45
-rw-r--r--apps/nxi_termination/optimizers/__init__.py0
-rw-r--r--apps/nxi_termination/optimizers/remote_opt_processor.py107
-rw-r--r--apps/nxi_termination/optimizers/response_processor.py32
-rw-r--r--apps/pci/__init__.py0
-rw-r--r--apps/pci/models/__init__.py0
-rw-r--r--apps/pci/models/api/__init__.py0
-rw-r--r--apps/pci/models/api/pciOptimizationRequest.py56
-rw-r--r--apps/pci/models/api/pciOptimizationResponse.py46
-rw-r--r--apps/pci/optimizers/__init__.py2
-rw-r--r--apps/pci/optimizers/config/__init__.py0
-rw-r--r--apps/pci/optimizers/config/config_client.py37
-rw-r--r--apps/pci/optimizers/config/configdb.py51
-rw-r--r--apps/pci/optimizers/config/cps.py72
-rw-r--r--apps/pci/optimizers/config_request.py55
-rw-r--r--apps/pci/optimizers/pci_opt_processor.py132
-rw-r--r--apps/pci/optimizers/solver/__init__.py0
-rw-r--r--apps/pci/optimizers/solver/min_confusion.mzn98
-rw-r--r--apps/pci/optimizers/solver/min_confusion_inl.mzn156
-rw-r--r--apps/pci/optimizers/solver/ml_model.py72
-rw-r--r--apps/pci/optimizers/solver/no_conflicts_no_confusion.mzn103
-rw-r--r--apps/pci/optimizers/solver/optimizer.py179
-rw-r--r--apps/pci/optimizers/solver/pci_utils.py47
-rw-r--r--apps/placement/__init__.py0
-rw-r--r--apps/placement/models/__init__.py0
-rw-r--r--apps/placement/models/api/__init__.py0
-rw-r--r--apps/placement/models/api/placementRequest.py105
-rw-r--r--apps/placement/models/api/placementResponse.py64
-rw-r--r--apps/placement/optimizers/__init__.py0
-rw-r--r--apps/placement/optimizers/conductor/__init__.py17
-rw-r--r--apps/placement/optimizers/conductor/remote_opt_processor.py178
-rwxr-xr-xapps/placement/templates/plc_opt_request.jsont142
-rwxr-xr-xapps/placement/templates/plc_opt_response.jsont10
-rwxr-xr-xapps/placement/templates/policy_request.jsont3
-rw-r--r--apps/route/__init__.py0
-rw-r--r--apps/route/optimizers/__init__.py0
-rw-r--r--apps/route/optimizers/inter_domain_route_opt.py370
-rw-r--r--apps/route/optimizers/route_opt.mzn53
-rw-r--r--apps/route/optimizers/simple_route_opt.py266
-rw-r--r--apps/slice_selection/__init__.py0
-rw-r--r--apps/slice_selection/models/api/__init__.py17
-rw-r--r--apps/slice_selection/models/api/nsi_selection_request.py62
-rw-r--r--apps/slice_selection/models/api/nsi_selection_response.py54
-rw-r--r--apps/slice_selection/models/api/nssi_selection_request.py41
-rw-r--r--apps/slice_selection/models/api/nssi_selection_response.py40
-rw-r--r--apps/slice_selection/optimizers/__init__.py17
-rw-r--r--apps/slice_selection/optimizers/conductor/__init__.py0
-rw-r--r--apps/slice_selection/optimizers/conductor/remote_opt_processor.py134
-rw-r--r--apps/slice_selection/optimizers/conductor/response_processor.py108
-rwxr-xr-xapps/templates/cms_opt_request.jsont35
-rwxr-xr-xapps/templates/cms_opt_request.jsont_1707_v167
-rwxr-xr-xapps/templates/cms_opt_request_1702.jsont63
-rw-r--r--apps/templates/cms_opt_response.jsont8
-rw-r--r--apps/templates/license_opt_request.jsont6
-rwxr-xr-xapps/templates/test_cms_nb_req_from_client.jsont19
-rwxr-xr-xapps/templates/test_plc_nb_req_from_client.jsont52
72 files changed, 3766 insertions, 0 deletions
diff --git a/apps/__init__.py b/apps/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/__init__.py
diff --git a/apps/license/__init__.py b/apps/license/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/license/__init__.py
diff --git a/apps/license/optimizers/__init__.py b/apps/license/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/license/optimizers/__init__.py
diff --git a/apps/license/optimizers/simple_license_allocation.py b/apps/license/optimizers/simple_license_allocation.py
new file mode 100644
index 0000000..b2aaba4
--- /dev/null
+++ b/apps/license/optimizers/simple_license_allocation.py
@@ -0,0 +1,43 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2015-2017 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+from osdf.utils.mdc_utils import mdc_from_json
+
+
+def license_optim(request_json):
+ """
+ Fetch license artifacts associated with the service model and search licensekey-group-UUID and entitlement-pool-uuid
+ associated with the given att part number and nominal throughput in a request
+ :param request_json: Request in a JSON format
+ :return: A tuple of licensekey-group-uuid-list and entitlement-group-uuid-list
+ """
+ mdc_from_json(request_json)
+ req_id = request_json["requestInfo"]["requestId"]
+
+ model_name = request_json.get('placementInfo', {}).get('serviceInfo', {}).get('modelInfo', {}).get('modelName')
+ service_name = model_name
+
+ license_info = []
+
+ for demand in request_json.get('placementInfo', {}).get('demandInfo', {}).get('licenseDemands', []):
+ license_info.append(
+ {'serviceResourceId': demand['serviceResourceId'],
+ 'resourceModuleName': demand['resourceModuleName'],
+ 'entitlementPoolList': "NOT SUPPORTED",
+ 'licenseKeyGroupList': "NOT SUPPORTED"
+ })
+ return license_info
diff --git a/apps/nsst/__init__.py b/apps/nsst/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/nsst/__init__.py
diff --git a/apps/nsst/models/api/nsstSelectionRequest.py b/apps/nsst/models/api/nsstSelectionRequest.py
new file mode 100644
index 0000000..3355ea2
--- /dev/null
+++ b/apps/nsst/models/api/nsstSelectionRequest.py
@@ -0,0 +1,43 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2020 Huawei Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType
+from schematics.types.compound import DictType
+from schematics.types.compound import ModelType
+from schematics.types import IntType
+from schematics.types import StringType
+from schematics.types import URLType
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ callbackHeader = DictType(BaseType)
+ sourceId = StringType(required=True)
+ timeout = IntType()
+
+
+class NSSTSelectionAPI(OSDFModel):
+ """Request for NST selection """
+
+ requestInfo = ModelType(RequestInfo, required=True)
+ sliceProfile = DictType(BaseType)
diff --git a/apps/nsst/optimizers/__init__.py b/apps/nsst/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/nsst/optimizers/__init__.py
diff --git a/apps/nsst/optimizers/conf/configIinputs.json b/apps/nsst/optimizers/conf/configIinputs.json
new file mode 100644
index 0000000..11247bb
--- /dev/null
+++ b/apps/nsst/optimizers/conf/configIinputs.json
@@ -0,0 +1,53 @@
+{
+ "NST": [{
+ "NST1 ": {
+ "name": "EmbbNst",
+ "id": "EmbbNst_1",
+ "latency": 20,
+ "uplink": 5,
+ "downlink": 8,
+ "reliability": 95,
+ "areaTrafficCapDL": 10,
+ "areaTrafficCapUL": 100,
+ "maxNumberofUEs": 10000,
+ "areas": " area1|area2",
+ "expDataRateDL": 10,
+ "expDataRateUL": 1000,
+ "uEMobilityLevel": "stationary",
+ "resourceSharingLevel": "shared",
+ "skip_post_instantiation_configuration": "true",
+ "controller_actor": "SO-REF-DATA",
+ "sNSSAI": "01-3226E7D1",
+ "pLMNIdList": "39-00",
+ "sST": "embb",
+ "uEMobilityLevel": "stationary",
+ "activityFactor": "0",
+ "coverageAreaTAList": "Beijing;Beijing;HaidanDistrict;WanshouluStreet",
+ "modeluuid": "fe6c82b9-4e53-4322-a671-e2d8637bfbb7",
+ "modelinvariantuuid": "7d7df980-cb81-45f8-bad9-4e5ad2876393"
+
+ }
+ },
+ {
+ "NST2 ": {
+ "name": "NST_2",
+ "id": "NST_2_id",
+ "latency": 3,
+ "uplink": 7,
+ "downlink": 1,
+ "areaTrafficCapDL": 100,
+ "areaTrafficCapUL": 100,
+ "maxNumberofUEs": 300,
+ "areas": " area1|area2",
+ "expDataRateDL": 10,
+ "expDataRateUL": 30,
+ "uEMobilityLevel": "stationary",
+ "resourceSharingLevel": "shared",
+ "skip_post_instantiation_configuration": "true",
+ "controller_actor": "SO-REF-DATA",
+ "modeluuid": "7981375e-5e0a-4bf5-93fa-f3e3c02f2b15",
+ "modelinvariantuuid": "087f11b4-aca0-4341-8104-e5bb2b73285g"
+ }
+ }
+ ]
+}
diff --git a/apps/nsst/optimizers/nsst_select_processor.py b/apps/nsst/optimizers/nsst_select_processor.py
new file mode 100644
index 0000000..90f40f2
--- /dev/null
+++ b/apps/nsst/optimizers/nsst_select_processor.py
@@ -0,0 +1,155 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2020 Huawei Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+
+"""
+This application generates NST SELECTION API calls using the information received from SO
+"""
+import os
+from osdf.adapters.conductor import conductor
+from osdf.adapters.policy.interface import get_policies
+from osdf.logging.osdf_logging import debug_log
+from osdf.logging.osdf_logging import error_log
+from osdf.utils.interfaces import get_rest_client
+from requests import RequestException
+from threading import Thread
+import traceback
+BASE_DIR = os.path.dirname(__file__)
+
+
+# This is the class for NST Selection
+
+
+class NsstSelection(Thread):
+
+ def __init__(self, osdf_config, request_json):
+ super().__init__()
+ self.osdf_config = osdf_config
+ self.request_json = request_json
+ self.request_info = self.request_json['requestInfo']
+ self.request_info['numSolutions'] = 1
+
+ def run(self):
+ self.process_nsst_selection()
+
+ def process_nsst_selection(self):
+ """Process a PCI request from a Client (build config-db, policy and API call, make the call, return result)
+
+ :param req_object: Request parameters from the client
+ :param osdf_config: Configuration specific to OSDF application (core + deployment)
+ :return: response from NST Opt
+ """
+ try:
+ rest_client = get_rest_client(self.request_json, service='so')
+ solution = self.get_nsst_solution()
+ except Exception as err:
+ error_log.error("Error for {} {}".format(self.request_info.get('requestId'),
+ traceback.format_exc()))
+ error_message = str(err)
+ solution = self.error_response(error_message)
+
+ try:
+ rest_client.request(json=solution, noresponse=True)
+ except RequestException:
+ error_log.error("Error sending asynchronous notification for {} {}".
+ format(self.request_info['requestId'], traceback.format_exc()))
+
+ def get_nsst_solution(self):
+ """the file is in the same folder for now will move it to the conf folder of the has once its
+
+ integrated there...
+ """
+ req_info = self.request_json['requestInfo']
+ requirements = self.request_json['sliceProfile']
+ model_name = "nsst"
+ policies = self.get_app_policies(model_name, "nsst_selection")
+ conductor_response = self.get_conductor(req_info, requirements, policies, model_name)
+ return conductor_response
+
+ def get_nsst_selection_response(self, solutions):
+ """Get NST selection response from final solution
+
+ :param solutions: final solutions
+ :return: NST selection response to send back as dictionary
+ """
+ return {'requestId': self.request_info['requestId'],
+ 'transactionId': self.request_info['transactionId'],
+ 'requestStatus': 'completed',
+ 'statusMessage': '',
+ 'solutions': solutions}
+
+ def error_response(self, error_message):
+ """Form response message from the error message
+
+ :param error_message: error message while processing the request
+ :return: response json as dictionary
+ """
+ return {'requestId': self.request_info['requestId'],
+ 'transactionId': self.request_info['transactionId'],
+ 'requestStatus': 'error',
+ 'statusMessage': error_message}
+
+ def get_app_policies(self, model_name, app_name):
+ policy_request_json = self.request_json.copy()
+ policy_request_json['serviceInfo'] = {'serviceName': model_name}
+ debug_log.debug("policy_request_json {}".format(str(policy_request_json)))
+ return get_policies(policy_request_json, app_name) # app_name: nsst_selection
+
+ def get_conductor(self, req_info, request_parameters, policies, model_name):
+ demands = [
+ {
+ "resourceModuleName": model_name,
+ "resourceModelInfo": {}
+ }
+ ]
+
+ try:
+ template_fields = {
+ 'location_enabled': False,
+ 'version': '2020-08-13'
+ }
+ resp = conductor.request(req_info, demands, request_parameters, {}, template_fields,
+ self.osdf_config, policies)
+ except RequestException as e:
+ resp = e.response.json()
+ error = resp['plans'][0]['message']
+ if "Unable to find any" in error:
+ return self.get_nsst_selection_response([])
+ error_log.error('Error from conductor {}'.format(error))
+ return self.error_response(error)
+ debug_log.debug("Response from conductor in get_conductor method {}".format(str(resp)))
+ recommendations = resp["plans"][0].get("recommendations")
+ return self.process_response(recommendations, model_name)
+
+ def process_response(self, recommendations, model_name):
+ """Process conductor response to form the response for the API request
+
+ :param recommendations: recommendations from conductor
+ :return: response json as a dictionary
+ """
+ if not recommendations:
+ return self.get_nsst_selection_response([])
+ solutions = [self.get_solution_from_candidate(rec[model_name]['candidate'])
+ for rec in recommendations]
+ return self.get_nsst_selection_response(solutions)
+
+ def get_solution_from_candidate(self, candidate):
+ if candidate['inventory_type'] == 'nsst':
+ return {
+ 'UUID': candidate['model_version_id'],
+ 'invariantUUID': candidate['model_invariant_id'],
+ 'NSSTName': candidate['model_name'],
+ }
diff --git a/apps/nst/__init__.py b/apps/nst/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/nst/__init__.py
diff --git a/apps/nst/models/api/nstSelectionRequest.py b/apps/nst/models/api/nstSelectionRequest.py
new file mode 100644
index 0000000..99c5df6
--- /dev/null
+++ b/apps/nst/models/api/nstSelectionRequest.py
@@ -0,0 +1,43 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2020 Huawei Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType
+from schematics.types.compound import DictType
+from schematics.types.compound import ModelType
+from schematics.types import IntType
+from schematics.types import StringType
+from schematics.types import URLType
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ callbackHeader = DictType(BaseType)
+ sourceId = StringType(required=True)
+ timeout = IntType()
+
+
+class NSTSelectionAPI(OSDFModel):
+ """Request for NST selection """
+
+ requestInfo = ModelType(RequestInfo, required=True)
+ serviceProfile = DictType(BaseType)
diff --git a/apps/nst/optimizers/__init__.py b/apps/nst/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/nst/optimizers/__init__.py
diff --git a/apps/nst/optimizers/conf/configIinputs.json b/apps/nst/optimizers/conf/configIinputs.json
new file mode 100644
index 0000000..11247bb
--- /dev/null
+++ b/apps/nst/optimizers/conf/configIinputs.json
@@ -0,0 +1,53 @@
+{
+ "NST": [{
+ "NST1 ": {
+ "name": "EmbbNst",
+ "id": "EmbbNst_1",
+ "latency": 20,
+ "uplink": 5,
+ "downlink": 8,
+ "reliability": 95,
+ "areaTrafficCapDL": 10,
+ "areaTrafficCapUL": 100,
+ "maxNumberofUEs": 10000,
+ "areas": " area1|area2",
+ "expDataRateDL": 10,
+ "expDataRateUL": 1000,
+ "uEMobilityLevel": "stationary",
+ "resourceSharingLevel": "shared",
+ "skip_post_instantiation_configuration": "true",
+ "controller_actor": "SO-REF-DATA",
+ "sNSSAI": "01-3226E7D1",
+ "pLMNIdList": "39-00",
+ "sST": "embb",
+ "uEMobilityLevel": "stationary",
+ "activityFactor": "0",
+ "coverageAreaTAList": "Beijing;Beijing;HaidanDistrict;WanshouluStreet",
+ "modeluuid": "fe6c82b9-4e53-4322-a671-e2d8637bfbb7",
+ "modelinvariantuuid": "7d7df980-cb81-45f8-bad9-4e5ad2876393"
+
+ }
+ },
+ {
+ "NST2 ": {
+ "name": "NST_2",
+ "id": "NST_2_id",
+ "latency": 3,
+ "uplink": 7,
+ "downlink": 1,
+ "areaTrafficCapDL": 100,
+ "areaTrafficCapUL": 100,
+ "maxNumberofUEs": 300,
+ "areas": " area1|area2",
+ "expDataRateDL": 10,
+ "expDataRateUL": 30,
+ "uEMobilityLevel": "stationary",
+ "resourceSharingLevel": "shared",
+ "skip_post_instantiation_configuration": "true",
+ "controller_actor": "SO-REF-DATA",
+ "modeluuid": "7981375e-5e0a-4bf5-93fa-f3e3c02f2b15",
+ "modelinvariantuuid": "087f11b4-aca0-4341-8104-e5bb2b73285g"
+ }
+ }
+ ]
+}
diff --git a/apps/nst/optimizers/nst_select_processor.py b/apps/nst/optimizers/nst_select_processor.py
new file mode 100644
index 0000000..9e44522
--- /dev/null
+++ b/apps/nst/optimizers/nst_select_processor.py
@@ -0,0 +1,155 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2020 Huawei Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+
+"""
+This application generates NST SELECTION API calls using the information received from SO
+"""
+import os
+from osdf.adapters.conductor import conductor
+from osdf.adapters.policy.interface import get_policies
+from osdf.logging.osdf_logging import debug_log
+from osdf.logging.osdf_logging import error_log
+from osdf.utils.interfaces import get_rest_client
+from requests import RequestException
+from threading import Thread
+import traceback
+BASE_DIR = os.path.dirname(__file__)
+
+
+# This is the class for NST Selection
+
+
+class NstSelection(Thread):
+
+ def __init__(self, osdf_config, request_json):
+ super().__init__()
+ self.osdf_config = osdf_config
+ self.request_json = request_json
+ self.request_info = self.request_json['requestInfo']
+ self.request_info['numSolutions'] = 1
+
+ def run(self):
+ self.process_nst_selection()
+
+ def process_nst_selection(self):
+ """Process a PCI request from a Client (build config-db, policy and API call, make the call, return result)
+
+ :param req_object: Request parameters from the client
+ :param osdf_config: Configuration specific to OSDF application (core + deployment)
+ :return: response from NST Opt
+ """
+ try:
+ rest_client = get_rest_client(self.request_json, service='so')
+ solution = self.get_nst_solution()
+ except Exception as err:
+ error_log.error("Error for {} {}".format(self.request_info.get('requestId'),
+ traceback.format_exc()))
+ error_message = str(err)
+ solution = self.error_response(error_message)
+
+ try:
+ rest_client.request(json=solution, noresponse=True)
+ except RequestException:
+ error_log.error("Error sending asynchronous notification for {} {}".
+ format(self.request_info['requestId'], traceback.format_exc()))
+
+ def get_nst_solution(self):
+ """the file is in the same folder for now will move it to the conf folder of the has once its
+
+ integrated there...
+ """
+ req_info = self.request_json['requestInfo']
+ requirements = self.request_json['serviceProfile']
+ model_name = "nst"
+ policies = self.get_app_policies(model_name, "nst_selection")
+ conductor_response = self.get_conductor(req_info, requirements, policies, model_name)
+ return conductor_response
+
+ def get_nst_selection_response(self, solutions):
+ """Get NST selection response from final solution
+
+ :param solutions: final solutions
+ :return: NST selection response to send back as dictionary
+ """
+ return {'requestId': self.request_info['requestId'],
+ 'transactionId': self.request_info['transactionId'],
+ 'requestStatus': 'completed',
+ 'statusMessage': '',
+ 'solutions': solutions}
+
+ def error_response(self, error_message):
+ """Form response message from the error message
+
+ :param error_message: error message while processing the request
+ :return: response json as dictionary
+ """
+ return {'requestId': self.request_info['requestId'],
+ 'transactionId': self.request_info['transactionId'],
+ 'requestStatus': 'error',
+ 'statusMessage': error_message}
+
+ def get_app_policies(self, model_name, app_name):
+ policy_request_json = self.request_json.copy()
+ policy_request_json['serviceInfo'] = {'serviceName': model_name}
+ debug_log.debug("policy_request_json {}".format(str(policy_request_json)))
+ return get_policies(policy_request_json, app_name) # app_name: nst_selection
+
+ def get_conductor(self, req_info, request_parameters, policies, model_name):
+ demands = [
+ {
+ "resourceModuleName": model_name,
+ "resourceModelInfo": {}
+ }
+ ]
+
+ try:
+ template_fields = {
+ 'location_enabled': False,
+ 'version': '2020-08-13'
+ }
+ resp = conductor.request(req_info, demands, request_parameters, {}, template_fields,
+ self.osdf_config, policies)
+ except RequestException as e:
+ resp = e.response.json()
+ error = resp['plans'][0]['message']
+ if "Unable to find any" in error:
+ return self.get_nst_selection_response([])
+ error_log.error('Error from conductor {}'.format(error))
+ return self.error_response(error)
+ debug_log.debug("Response from conductor in get_conductor method {}".format(str(resp)))
+ recommendations = resp["plans"][0].get("recommendations")
+ return self.process_response(recommendations, model_name)
+
+ def process_response(self, recommendations, model_name):
+ """Process conductor response to form the response for the API request
+
+ :param recommendations: recommendations from conductor
+ :return: response json as a dictionary
+ """
+ if not recommendations:
+ return self.get_nst_selection_response([])
+ solutions = [self.get_solution_from_candidate(rec[model_name]['candidate'])
+ for rec in recommendations]
+ return self.get_nst_selection_response(solutions)
+
+ def get_solution_from_candidate(self, candidate):
+ if candidate['inventory_type'] == 'nst':
+ return {
+ 'UUID': candidate['model_version_id'],
+ 'invariantUUID': candidate['model_invariant_id'],
+ 'NSTName': candidate['model_name'],
+ }
diff --git a/apps/nxi_termination/__init__.py b/apps/nxi_termination/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/nxi_termination/__init__.py
diff --git a/apps/nxi_termination/models/api/_init_.py b/apps/nxi_termination/models/api/_init_.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/nxi_termination/models/api/_init_.py
diff --git a/apps/nxi_termination/models/api/nxi_termination_request.py b/apps/nxi_termination/models/api/nxi_termination_request.py
new file mode 100644
index 0000000..45456cf
--- /dev/null
+++ b/apps/nxi_termination/models/api/nxi_termination_request.py
@@ -0,0 +1,45 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType
+from schematics.types.compound import DictType
+from schematics.types.compound import ModelType
+from schematics.types import IntType
+from schematics.types import StringType
+from schematics.types import URLType
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ callbackHeader = DictType(BaseType)
+ sourceId = StringType(required=True)
+ timeout = IntType()
+ addtnlArgs = DictType(BaseType)
+
+
+class NxiTerminationApi(OSDFModel):
+ """Request for nxi termination (specific to optimization and additional metadata"""
+ requestInfo = ModelType(RequestInfo, required=True)
+ type = StringType(required=True, choices=['NSI', 'NSSI'])
+ NxIId = StringType(required=True)
+ UUID = StringType()
+ invariantUUID = StringType()
diff --git a/apps/nxi_termination/optimizers/__init__.py b/apps/nxi_termination/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/nxi_termination/optimizers/__init__.py
diff --git a/apps/nxi_termination/optimizers/remote_opt_processor.py b/apps/nxi_termination/optimizers/remote_opt_processor.py
new file mode 100644
index 0000000..fc3bc17
--- /dev/null
+++ b/apps/nxi_termination/optimizers/remote_opt_processor.py
@@ -0,0 +1,107 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from jinja2 import Template
+
+from apps.nxi_termination.optimizers.response_processor import get_nxi_termination_response
+from osdf.adapters.aai.fetch_aai_data import AAIException
+from osdf.adapters.aai.fetch_aai_data import execute_dsl_query
+from osdf.adapters.aai.fetch_aai_data import get_aai_data
+from osdf.logging.osdf_logging import debug_log
+
+
+def process_nxi_termination_opt(request_json, osdf_config):
+ """Process the nxi Termination request from API layer
+
+ :param request_json: api request
+ :param osdf_config: configuration specific to OSDF app
+ :return: response as a success,failure
+ """
+
+ request_type = request_json["type"]
+ request_info = request_json.get("requestInfo", {})
+ addtnl_args = request_info.get("addtnlArgs", {})
+ query_templates = osdf_config.core["nxi_termination"]["query_templates"]
+
+ inputs = {
+ "instance_id": request_json["NxIId"]
+ }
+
+ try:
+ if request_type == "NSSI":
+ templates = query_templates["nssi"]
+ for template in templates:
+ resource_count = get_resource_count(template, inputs, osdf_config)
+ if resource_count == -1:
+ continue
+ elif resource_count > 1 or (resource_count == 1 and not addtnl_args.get("serviceInstanceId")):
+ terminate_response = False
+ elif resource_count == 0:
+ terminate_response = True
+ elif resource_count == 1 and addtnl_args.get("serviceInstanceId"):
+ new_template = template + "('service-instance-id','{}')".format(addtnl_args["serviceInstanceId"])
+ terminate_response = get_resource_count(new_template, inputs, osdf_config) == 1
+ return set_response("success", "", request_info, terminate_response)
+
+ if request_type == "NSI":
+ allotted_resources = get_allotted_resources(request_json, osdf_config)
+ resource_count = len(allotted_resources)
+ if resource_count == 1 and addtnl_args.get("serviceInstanceId"):
+ debug_log.debug("resource count {}".format(resource_count))
+ terminate_response = False
+ properties = allotted_resources[0]["relationship-data"]
+ for property in properties:
+ if property["relationship-key"] == "service-instance.service-instance-id" \
+ and property["relationship-value"] == addtnl_args.get("serviceInstanceId"):
+ terminate_response = True
+ elif resource_count > 1 or (resource_count == 1 and not addtnl_args.get("serviceInstanceId")):
+ terminate_response = False
+ elif resource_count == 0:
+ terminate_response = True
+
+ return set_response("success", "", request_info, terminate_response)
+ except AAIException as e:
+ reason = str(e)
+ return set_response("failure", reason, request_info)
+
+ except Exception as e:
+ reason = "{} Exception Occurred while processing".format(str(e))
+ return set_response("failure", reason, request_info)
+
+
+def set_response(status, reason, request_info, terminate_response=None):
+ res = dict()
+ res["requestStatus"] = status
+ res["terminateResponse"] = terminate_response
+ res["reason"] = reason
+ return get_nxi_termination_response(request_info, res)
+
+
+def get_resource_count(query_template, inputs, osdf_config):
+ query = Template(query_template).render(inputs)
+ dsl_response = execute_dsl_query(query, "count", osdf_config)
+ debug_log.debug("dsl_response {}".format(dsl_response))
+ # the dsl query with format "count" includes the original service-instance, hence reducing one from the result
+ count = dsl_response["results"][0]
+ return count.get("service-instance", 0) - 1
+
+
+def get_allotted_resources(request_json, osdf_config):
+ response = get_aai_data(request_json, osdf_config)
+ rel_list = response["relationship-list"]["relationship"]
+ return [rel for rel in rel_list if rel["related-to"] == "allotted-resource"]
diff --git a/apps/nxi_termination/optimizers/response_processor.py b/apps/nxi_termination/optimizers/response_processor.py
new file mode 100644
index 0000000..3ea35c0
--- /dev/null
+++ b/apps/nxi_termination/optimizers/response_processor.py
@@ -0,0 +1,32 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+
+def get_nxi_termination_response(request_info, response):
+
+ """Get NXI termination response from final solution
+
+ :param request_info: request info
+ :param response: response to be send
+ :return: NxI Termination response to send back as dictionary
+ """
+ return {'requestId': request_info['requestId'],
+ 'transactionId': request_info['transactionId'],
+ 'requestStatus': response["requestStatus"],
+ 'terminateResponse': response["terminateResponse"],
+ 'reason': response['reason']}
diff --git a/apps/pci/__init__.py b/apps/pci/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/__init__.py
diff --git a/apps/pci/models/__init__.py b/apps/pci/models/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/models/__init__.py
diff --git a/apps/pci/models/api/__init__.py b/apps/pci/models/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/models/api/__init__.py
diff --git a/apps/pci/models/api/pciOptimizationRequest.py b/apps/pci/models/api/pciOptimizationRequest.py
new file mode 100644
index 0000000..2aa22f1
--- /dev/null
+++ b/apps/pci/models/api/pciOptimizationRequest.py
@@ -0,0 +1,56 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from schematics.types import BaseType, StringType, URLType, IntType
+from schematics.types.compound import ModelType, ListType, DictType
+
+from osdf.models.api.common import OSDFModel
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ callbackHeader = DictType(BaseType)
+ sourceId = StringType(required=True)
+ requestType = StringType(required=True)
+ numSolutions = IntType()
+ optimizers = ListType(StringType(required=True))
+ timeout = IntType()
+
+
+class ANRInfo(OSDFModel):
+ cellId = StringType(required=True)
+ removeableNeighbors = ListType(StringType())
+
+
+class CellInfo(OSDFModel):
+ """Information specific to CellInfo """
+ networkId = StringType(required=True)
+ cellIdList = ListType(StringType(required=True))
+ anrInputList = ListType(ModelType(ANRInfo))
+ fixedPCICells = ListType(StringType())
+ priorityTreatmentCells = ListType(StringType())
+ trigger = StringType()
+
+
+class PCIOptimizationAPI(OSDFModel):
+ """Request for PCI optimization """
+ requestInfo = ModelType(RequestInfo, required=True)
+ cellInfo = ModelType(CellInfo, required=True)
diff --git a/apps/pci/models/api/pciOptimizationResponse.py b/apps/pci/models/api/pciOptimizationResponse.py
new file mode 100644
index 0000000..019a43a
--- /dev/null
+++ b/apps/pci/models/api/pciOptimizationResponse.py
@@ -0,0 +1,46 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from schematics.types import StringType, IntType
+from schematics.types.compound import ModelType, ListType
+
+from osdf.models.api.common import OSDFModel
+
+
+class PCISolution(OSDFModel):
+ cellId = StringType(required=True)
+ pci = IntType(required=True)
+
+
+class ANRSolution(OSDFModel):
+ cellId = StringType(required=True)
+ removeableNeighbors = ListType(StringType())
+
+
+class Solution(OSDFModel):
+ networkId = StringType(required=True)
+ pciSolutions = ListType(ListType(ModelType(PCISolution), min_size=1))
+ anrSolutions = ListType(ListType(ModelType(ANRSolution), min_size=1))
+
+
+class PCIOptimizationResponse(OSDFModel):
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ requestStatus = StringType(required=True)
+ statusMessage = StringType()
+ solutions = ModelType(Solution, required=True)
diff --git a/apps/pci/optimizers/__init__.py b/apps/pci/optimizers/__init__.py
new file mode 100644
index 0000000..35bc5a0
--- /dev/null
+++ b/apps/pci/optimizers/__init__.py
@@ -0,0 +1,2 @@
+from apps.pci.optimizers.config import configdb
+from apps.pci.optimizers.config import cps
diff --git a/apps/pci/optimizers/config/__init__.py b/apps/pci/optimizers/config/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/optimizers/config/__init__.py
diff --git a/apps/pci/optimizers/config/config_client.py b/apps/pci/optimizers/config/config_client.py
new file mode 100644
index 0000000..7e5a737
--- /dev/null
+++ b/apps/pci/optimizers/config/config_client.py
@@ -0,0 +1,37 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2021 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+
+class ConfigClient(object):
+
+ subclasses = {}
+
+ @classmethod
+ def register_subclass(cls, type):
+ def decorator(subclass):
+ cls.subclasses[type] = subclass
+ return subclass
+
+ return decorator
+
+ @classmethod
+ def create(cls, type):
+ if type not in cls.subclasses:
+ raise ValueError('Bad config client type {}'.format(type))
+
+ return cls.subclasses[type]()
diff --git a/apps/pci/optimizers/config/configdb.py b/apps/pci/optimizers/config/configdb.py
new file mode 100644
index 0000000..cfc7ce1
--- /dev/null
+++ b/apps/pci/optimizers/config/configdb.py
@@ -0,0 +1,51 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2021 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from datetime import datetime as dt
+import uuid
+
+from apps.pci.optimizers.config.config_client import ConfigClient
+from osdf.config.base import osdf_config
+from osdf.logging.osdf_logging import debug_log
+from osdf.utils.interfaces import RestClient
+
+
+@ConfigClient.register_subclass('configdb')
+class ConfigDb(ConfigClient):
+
+ def __init__(self):
+ self.config = osdf_config.deployment
+ uid, passwd = self.config['configDbUserName'], self.config['configDbPassword']
+ headers = dict(transaction_id=str(uuid.uuid4()))
+ self.rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers)
+
+ def get_cell_list(self, network_id):
+ ts = dt.strftime(dt.now(), '%Y-%m-%d %H:%M:%S%z')
+ cell_list_url = '{}/{}/{}/{}'.format(self.config['configDbUrl'],
+ self.config['configDbGetCellListUrl'], network_id, ts)
+ return self.rc.request(raw_response=True, url=cell_list_url).json()
+
+ def get_nbr_list(self, network_id, cell_id):
+ ts = dt.strftime(dt.now(), '%Y-%m-%d %H:%M:%S%z')
+ nbr_list_url = '{}/{}/{}/{}'.format(self.config['configDbUrl'],
+ self.config['configDbGetNbrListUrl'], cell_id, ts)
+ response = self.rc.request(url=nbr_list_url, raw_response=True).json()
+
+ debug_log.debug("cell_id {} nbr_list {}".format(cell_id, response.get('nbrList')))
+
+ return response.get('nbrList', [])
diff --git a/apps/pci/optimizers/config/cps.py b/apps/pci/optimizers/config/cps.py
new file mode 100644
index 0000000..9cf1b1f
--- /dev/null
+++ b/apps/pci/optimizers/config/cps.py
@@ -0,0 +1,72 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2021 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import json
+
+from apps.pci.optimizers.config.config_client import ConfigClient
+from osdf.config.base import osdf_config
+from osdf.logging.osdf_logging import debug_log
+from osdf.utils.interfaces import RestClient
+
+
+@ConfigClient.register_subclass('cps')
+class Cps(ConfigClient):
+
+ def __init__(self):
+ self.config = osdf_config.deployment
+ username, password = self.config['cpsUsername'], self.config['cpsPassword']
+ headers = {
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+ }
+ self.rc = RestClient(userid=username, passwd=password, method="POST",
+ log_func=debug_log.debug, headers=headers)
+
+ def get_cell_list(self, network_id):
+ cell_list_url = '{}/{}'.format(self.config['cpsUrl'], self.config['cpsCellListUrl'])
+ data = {
+ 'inputParameters': {
+ 'regionId': network_id
+ }
+ }
+ response = self.rc.request(url=cell_list_url, data=json.dumps(data))
+ debug_log.debug("cell list response {}".format(response))
+ return sorted([x['idNRCellCU'] for x in response.get('NRCellCU')])
+
+ def get_nbr_list(self, network_id, cell_id):
+ nbr_list_url = '{}/{}'.format(self.config['cpsUrl'], self.config['cpsNbrListUrl'])
+ data = {
+ 'inputParameters': {
+ 'regionId': network_id,
+ 'idNRCellCU': cell_id
+ }
+ }
+ response = self.rc.request(url=nbr_list_url, data=json.dumps(data))
+ debug_log.debug("nbr list response {}".format(response))
+ nbr_list = []
+ for cell_relation in response.get('NRCellRelation'):
+ nbr = {
+ 'targetCellId': cell_relation['attributes']['nRTCI'],
+ 'pciValue': int(cell_relation['attributes']['nRPCI']),
+ 'ho': cell_relation['attributes']['isHOAllowed']
+ }
+ nbr_list.append(nbr)
+
+ debug_log.debug("cell_id {} nbr_list {}".format(cell_id, nbr_list))
+
+ return nbr_list
diff --git a/apps/pci/optimizers/config_request.py b/apps/pci/optimizers/config_request.py
new file mode 100644
index 0000000..f62641d
--- /dev/null
+++ b/apps/pci/optimizers/config_request.py
@@ -0,0 +1,55 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from apps.pci.optimizers.config.config_client import ConfigClient
+
+
+def request(req_object, osdf_config, flat_policies):
+ """Process a configdb request from a Client (build Conductor API call, make the call, return result)
+
+ :param req_object: Request parameters from the client
+ :param osdf_config: Configuration specific to OSDF application (core + deployment)
+ :param flat_policies: policies related to PCI Opt (fetched based on request)
+ :return: response from ConfigDB (accounting for redirects from Conductor service
+ """
+ cell_list_response = {}
+
+ network_id = req_object['cellInfo']['networkId']
+
+ cell_list_response['network_id'] = network_id
+
+ config = osdf_config.deployment
+
+ config_client = ConfigClient.create(config['configClientType'])
+
+ cell_resp = config_client.get_cell_list(network_id)
+
+ cell_list = []
+ count = 0
+ for cell_id in cell_resp:
+ cell_info = {
+ 'cell_id': cell_id,
+ 'id': count,
+ 'nbr_list': config_client.get_nbr_list(network_id, cell_id)
+ }
+ cell_list.append(cell_info)
+ count += 1
+
+ cell_list_response['cell_list'] = cell_list
+
+ return cell_resp, cell_list_response
diff --git a/apps/pci/optimizers/pci_opt_processor.py b/apps/pci/optimizers/pci_opt_processor.py
new file mode 100644
index 0000000..a58d2f4
--- /dev/null
+++ b/apps/pci/optimizers/pci_opt_processor.py
@@ -0,0 +1,132 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import traceback
+
+from onaplogging.mdcContext import MDC
+from requests import RequestException
+
+from apps.pci.optimizers.config_request import request as config_request
+from apps.pci.optimizers.solver.optimizer import pci_optimize as optimize
+from apps.pci.optimizers.solver.pci_utils import get_cell_id
+from apps.pci.optimizers.solver.pci_utils import get_pci_value
+from osdf.logging.osdf_logging import error_log
+from osdf.logging.osdf_logging import metrics_log
+from osdf.logging.osdf_logging import MH
+from osdf.operation.error_handling import build_json_error_body
+from osdf.utils.interfaces import get_rest_client
+from osdf.utils.mdc_utils import mdc_from_json
+
+"""
+This application generates PCI Optimization API calls using the information received from PCI-Handler-MS, SDN-C
+and Policy.
+"""
+
+
+def process_pci_optimation(request_json, osdf_config, flat_policies):
+ """Process a PCI request from a Client (build config-db, policy and API call, make the call, return result)
+
+ :param req_object: Request parameters from the client
+ :param osdf_config: Configuration specific to OSDF application (core + deployment)
+ :param flat_policies: policies related to pci (fetched based on request)
+ :return: response from PCI Opt
+ """
+ try:
+ mdc_from_json(request_json)
+ rc = get_rest_client(request_json, service="pcih")
+ req_id = request_json["requestInfo"]["requestId"]
+ cell_info_list, network_cell_info = config_request(request_json, osdf_config, flat_policies)
+ pci_response = get_solutions(cell_info_list, network_cell_info, request_json)
+
+ metrics_log.info(MH.inside_worker_thread(req_id))
+ except Exception as err:
+ error_log.error("Error for {} {}".format(req_id, traceback.format_exc()))
+
+ try:
+ body = build_json_error_body(err)
+ metrics_log.info(MH.sending_response(req_id, "ERROR"))
+ rc.request(json=body, noresponse=True)
+ except RequestException:
+ MDC.put('requestID', req_id)
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
+ raise err
+
+ try:
+ metrics_log.info(MH.calling_back_with_body(req_id, rc.url, pci_response))
+ error_log.error("pci response: {}".format(str(pci_response)))
+ rc.request(json=pci_response, noresponse=True)
+ except RequestException: # can't do much here but log it and move on
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
+
+
+def get_solutions(cell_info_list, network_cell_info, request_json):
+ status, pci_solutions, anr_solutions = build_solution_list(cell_info_list, network_cell_info, request_json)
+ return {
+ "transactionId": request_json['requestInfo']['transactionId'],
+ "requestId": request_json["requestInfo"]["requestId"],
+ "requestStatus": "completed",
+ "statusMessage": status,
+ "solutions": {
+ 'networkId': request_json['cellInfo']['networkId'],
+ 'pciSolutions': pci_solutions,
+ 'anrSolutions': anr_solutions
+ }
+ }
+
+
+def build_solution_list(cell_info_list, network_cell_info, request_json):
+ status = "success"
+ req_id = request_json["requestInfo"]["requestId"]
+ pci_solutions = []
+ anr_solutions = []
+ try:
+ opt_solution = optimize(network_cell_info, cell_info_list, request_json)
+ if opt_solution == 'UNSATISFIABLE':
+ status = 'inconsistent input'
+ return status, pci_solutions, anr_solutions
+ else:
+ pci_solutions = build_pci_solution(network_cell_info, opt_solution['pci'])
+ anr_solutions = build_anr_solution(network_cell_info, opt_solution.get('removables', {}))
+ except RuntimeError:
+ error_log.error("Failed finding solution for {} {}".format(req_id, traceback.format_exc()))
+ status = "failed"
+ return status, pci_solutions, anr_solutions
+
+
+def build_pci_solution(network_cell_info, pci_solution):
+ pci_solutions = []
+ for k, v in pci_solution.items():
+ old_pci = get_pci_value(network_cell_info, k)
+ if old_pci != v:
+ response = {
+ 'cellId': get_cell_id(network_cell_info, k),
+ 'pci': v
+ }
+ pci_solutions.append(response)
+ return pci_solutions
+
+
+def build_anr_solution(network_cell_info, removables):
+ anr_solutions = []
+ for k, v in removables.items():
+ response = {
+ 'cellId': get_cell_id(network_cell_info, k),
+ 'removeableNeighbors': list(map(lambda x: get_cell_id(network_cell_info, x), v))
+ }
+ anr_solutions.append(response)
+ return anr_solutions
diff --git a/apps/pci/optimizers/solver/__init__.py b/apps/pci/optimizers/solver/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/optimizers/solver/__init__.py
diff --git a/apps/pci/optimizers/solver/min_confusion.mzn b/apps/pci/optimizers/solver/min_confusion.mzn
new file mode 100644
index 0000000..ff56c18
--- /dev/null
+++ b/apps/pci/optimizers/solver/min_confusion.mzn
@@ -0,0 +1,98 @@
+% -------------------------------------------------------------------------
+% Copyright (c) 2018 AT&T Intellectual Property
+%
+% 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.
+%
+% -------------------------------------------------------------------------
+%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Parameters and its assertions
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Number of cells/radios.
+int: NUM_NODES;
+
+% Maximum number of Physical Cell Identifiers to be assigned to the nodes.
+int: NUM_PCIS;
+
+% Number of edges between neighbor nodes. There is a edge (i,j) if and only
+% if nodes i and j are neighbors, i.e., an user equipment (UE) can make
+% handoff between i and j. Such edges are used to avoid **COLLISION**, i.e.,
+% to guarantee that nodes i and j have different PCIs.
+int: NUM_NEIGHBORS;
+
+% Each line represents an edge between direct neighbors as defined before.
+array[1..NUM_NEIGHBORS, 1..2] of int: NEIGHBORS;
+
+% Number of undirect neighbor pairs (j, k) such that both j and k are direct
+% neighbors of node i, i.e., (j, k) exits if and only if exists (i, j) and
+% (i, k). Nodes (i, k) can generate "confunsions" in the network if they have
+% the same PCI. Such edges are used to avoid/minimize **CONFUSIONS**.
+int: NUM_SECOND_LEVEL_NEIGHBORS;
+
+% Each line represents an edge between undirect neighbors as defined before.
+array[1..NUM_SECOND_LEVEL_NEIGHBORS, 1..2] of int: SECOND_LEVEL_NEIGHBORS;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Decision variables
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Defines the PCI for each node.
+array[0..NUM_NODES-1] of var 0..NUM_PCIS-1: pci;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Constraints
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Direct neighbors must have different PCIs for avoid **COLLISION**.
+constraint
+forall(i in 1..NUM_NEIGHBORS)(
+ pci[NEIGHBORS[i, 1]] != pci[NEIGHBORS[i, 2]]
+);
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Objective function
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Total number of confusions.
+var int: total_confusions =
+ sum([bool2int(pci[SECOND_LEVEL_NEIGHBORS[i, 1]] ==
+ pci[SECOND_LEVEL_NEIGHBORS[i, 2]])
+ | i in 1..NUM_SECOND_LEVEL_NEIGHBORS]);
+
+% Minimize the total number of confusions.
+solve :: int_search(pci, smallest, indomain_min, complete)
+minimize total_confusions;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Output
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+output
+["PCI assigment"] ++
+["\nnode,pci"] ++
+[
+ "\n" ++ show(node) ++ "," ++ show(pci[node])
+| node in 0..NUM_NODES-1
+] ++
+
+["\n\nConfusions"] ++
+["\nTotal confusions: " ++ show(total_confusions)] ++
+["\nConfusion pairs"] ++
+[
+ "\n" ++ show(SECOND_LEVEL_NEIGHBORS[i, 1]) ++ "," ++
+ show(SECOND_LEVEL_NEIGHBORS[i, 2])
+| i in 1..NUM_SECOND_LEVEL_NEIGHBORS where
+ fix(pci[SECOND_LEVEL_NEIGHBORS[i, 1]] == pci[SECOND_LEVEL_NEIGHBORS[i, 2]])
+]
diff --git a/apps/pci/optimizers/solver/min_confusion_inl.mzn b/apps/pci/optimizers/solver/min_confusion_inl.mzn
new file mode 100644
index 0000000..e677e27
--- /dev/null
+++ b/apps/pci/optimizers/solver/min_confusion_inl.mzn
@@ -0,0 +1,156 @@
+% -------------------------------------------------------------------------
+% Copyright (c) 2018 AT&T Intellectual Property
+%
+% 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.
+%
+% -------------------------------------------------------------------------
+%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Parameters and its assertions
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Number of cells/radios.
+int: NUM_NODES;
+
+% Maximum number of Physical Cell Identifiers to be assigned to the nodes.
+int: NUM_PCIS;
+
+% Number of edges between neighbor nodes. There is a edge (i,j) if and only
+% if nodes i and j are neighbors, i.e., an user equipment (UE) can make
+% handoff between i and j. Such edges are used to avoid **COLLISION**, i.e.,
+% to guarantee that nodes i and j have different PCIs.
+int: NUM_NEIGHBORS;
+
+% Each line represents an edge between direct neighbors as defined before.
+array[1..NUM_NEIGHBORS, 1..2] of int: NEIGHBORS;
+
+% Number of undirect neighbor pairs (j, k) such that both j and k are direct
+% neighbors of node i, i.e., (j, k) exits if and only if exists (i, j) and
+% (i, k). Nodes (i, k) can generate "confunsions" in the network if they have
+% the same PCI. Such edges are used to avoid/minimize **CONFUSIONS**.
+int: NUM_SECOND_LEVEL_NEIGHBORS;
+
+% Each line represents an edge between undirect neighbors as defined before.
+array[1..NUM_SECOND_LEVEL_NEIGHBORS, 1..2] of int: SECOND_LEVEL_NEIGHBORS;
+
+% Number of ignorable neighbor links. Such links can be ignored during
+% optimization if needed.
+int: NUM_IGNORABLE_NEIGHBOR_LINKS;
+
+% The links that can be ignored if needed. Each line represents the two ends
+% of the links, like the previous structures.
+array[1..NUM_IGNORABLE_NEIGHBOR_LINKS, 1..2] of int: IGNORABLE_NEIGHBOR_LINKS;
+
+% ids of cells for which the pci should remain unchanged
+set of int: PCI_UNCHANGEABLE_CELLS;
+
+% This array has the original pcis of all the cells. array is indexed by the ids
+% of the cell. eg. ORIGINAL_PCIS[3] returns the pci of cell whose id is 3.
+% ids start from 0
+array[1..NUM_NODES] of 0..NUM_PCIS-1: ORIGINAL_PCIS;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Decision variables
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Defines the PCI for each node.
+array[0..NUM_NODES-1] of var 0..NUM_PCIS-1: pci;
+
+array[1..NUM_IGNORABLE_NEIGHBOR_LINKS] of var 0..1: used_ignorables;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Constraints
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% fixed pci cells
+constraint
+if(length(PCI_UNCHANGEABLE_CELLS) !=0) then
+forall(i in PCI_UNCHANGEABLE_CELLS)(
+ pci[i] == ORIGINAL_PCIS[i+1]
+)
+endif;
+
+% Direct neighbors must have different PCIs for avoid **COLLISION**.
+% Forced links.
+constraint
+forall(i in 1..NUM_NEIGHBORS, j in 1..NUM_IGNORABLE_NEIGHBOR_LINKS
+ where
+ NEIGHBORS[i, 1] != IGNORABLE_NEIGHBOR_LINKS[j, 1] \/
+ NEIGHBORS[i, 2] != IGNORABLE_NEIGHBOR_LINKS[j, 2]
+)(
+ pci[NEIGHBORS[i, 1]] != pci[NEIGHBORS[i, 2]]
+);
+
+
+% Ignorable links.
+constraint
+forall(i in 1..NUM_NEIGHBORS, j in 1..NUM_IGNORABLE_NEIGHBOR_LINKS
+ where
+ NEIGHBORS[i, 1] == IGNORABLE_NEIGHBOR_LINKS[j, 1] /\
+ NEIGHBORS[i, 2] == IGNORABLE_NEIGHBOR_LINKS[j, 2]
+)(
+ used_ignorables[j] >= bool2int(pci[NEIGHBORS[i, 1]] == pci[NEIGHBORS[i, 2]])
+);
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Objective function
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Total number of confusions.
+var int: total_confusions =
+ sum([bool2int(pci[SECOND_LEVEL_NEIGHBORS[i, 1]] ==
+ pci[SECOND_LEVEL_NEIGHBORS[i, 2]])
+ | i in 1..NUM_SECOND_LEVEL_NEIGHBORS]);
+
+% Total number of used ignorables links.
+var int: total_used_ignorables = sum(used_ignorables);
+
+solve :: int_search(pci, smallest, indomain_min, complete)
+
+% Minimize the total number of confusions.
+%minimize total_confusions;
+
+% Minimize the total number of confusions first,
+% then the number of used ignorables links.
+minimize (2 * NUM_IGNORABLE_NEIGHBOR_LINKS * total_confusions) +
+ total_used_ignorables;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Output
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+output
+["PCI assigment"] ++
+["\nnode,pci"] ++
+[
+ "\n" ++ show(node) ++ "," ++ show(pci[node])
+| node in 0..NUM_NODES-1
+] ++
+["\n\nTotal used ignorables links: " ++ show(total_used_ignorables)] ++
+["\nUsed ignorables links: "] ++
+[
+ "\n" ++ show(IGNORABLE_NEIGHBOR_LINKS[i, 1]) ++
+ "," ++ show(IGNORABLE_NEIGHBOR_LINKS[i, 2])
+ | i in 1..NUM_IGNORABLE_NEIGHBOR_LINKS where fix(used_ignorables[i] > 0)
+] ++
+["\n\nConfusions"] ++
+["\nTotal confusions: " ++ show(total_confusions)] ++
+["\nConfusion pairs"] ++
+[
+ "\n" ++ show(SECOND_LEVEL_NEIGHBORS[i, 1]) ++ "," ++
+ show(SECOND_LEVEL_NEIGHBORS[i, 2])
+ | i in 1..NUM_SECOND_LEVEL_NEIGHBORS where
+ fix(pci[SECOND_LEVEL_NEIGHBORS[i, 1]] == pci[SECOND_LEVEL_NEIGHBORS[i, 2]])
+]
+
diff --git a/apps/pci/optimizers/solver/ml_model.py b/apps/pci/optimizers/solver/ml_model.py
new file mode 100644
index 0000000..c239be8
--- /dev/null
+++ b/apps/pci/optimizers/solver/ml_model.py
@@ -0,0 +1,72 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import json
+
+from apps.pci.optimizers.solver.pci_utils import get_id
+from osdf.adapters.dcae import des
+from osdf.adapters.dcae.des import DESException
+from osdf.config.base import osdf_config
+from osdf.logging.osdf_logging import error_log
+
+
+class MlModel(object):
+ def __init__(self):
+ self.config = osdf_config.core['PCI']
+
+ def get_additional_inputs(self, dzn_data, network_cell_info):
+ """Add/update additional info to the existing models.
+
+ The method returns nothing. Instead, it modifies the dzn_data
+ :params: dzn_data: map with data for the optimization
+ """
+ self.compute_ml_model(dzn_data, network_cell_info)
+
+ def compute_ml_model(self, dzn_data, network_cell_info):
+ average_ho_threshold = self.config['ML']['average_ho_threshold']
+ latest_ho_threshold = self.config['ML']['latest_ho_threshold']
+
+ fixed_cells = set()
+ for cell in network_cell_info['cell_list']:
+ cell_id = cell['cell_id']
+ average_ho, latest_ho = self.get_ho_details(cell['cell_id'])
+ if average_ho > average_ho_threshold or latest_ho > latest_ho_threshold:
+ fixed_cells.add(get_id(network_cell_info, cell_id))
+
+ fixed_cells.update(dzn_data.get('PCI_UNCHANGEABLE_CELLS', []))
+ dzn_data['PCI_UNCHANGEABLE_CELLS'] = fixed_cells
+
+ def get_ho_details(self, cell_id):
+ service_id = self.config['DES']['service_id']
+ request_data = self.config['DES']['filter']
+ request_data['cell_id'] = cell_id
+ try:
+ result = des.extract_data(service_id, json.dumps(request_data))
+ except DESException as e:
+ error_log.error("Error while calling DES {}".format(e))
+ return 0, 0
+
+ if not result:
+ return 0, 0
+
+ ho_list = []
+ for pm_data in result:
+ ho = pm_data['overallHoAtt']
+ ho_list.append(ho)
+
+ return sum(ho_list) / len(ho_list), ho_list[0]
diff --git a/apps/pci/optimizers/solver/no_conflicts_no_confusion.mzn b/apps/pci/optimizers/solver/no_conflicts_no_confusion.mzn
new file mode 100644
index 0000000..f059d4a
--- /dev/null
+++ b/apps/pci/optimizers/solver/no_conflicts_no_confusion.mzn
@@ -0,0 +1,103 @@
+% -------------------------------------------------------------------------
+% Copyright (c) 2018 AT&T Intellectual Property
+%
+% 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.
+%
+% -------------------------------------------------------------------------
+%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Parameters and its assertions
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Number of cells/radios.
+int: NUM_NODES;
+
+% Maximum number of Physical Cell Identifiers to be assigned to the nodes.
+int: NUM_PCIS;
+
+% Number of edges between neighbor nodes. There is a edge (i,j) if and only
+% if nodes i and j are neighbors, i.e., an user equipment (UE) can make
+% handoff between i and j. Such edges are used to avoid **COLLISIONS**, i.e.,
+% to guarantee that nodes i and j have different PCIs.
+int: NUM_NEIGHBORS;
+
+% Each line represents an edge between direct neighbors as defined before.
+array[1..NUM_NEIGHBORS, 1..2] of int: NEIGHBORS;
+
+% Number of undirect neighbor pairs (j, k) such that both j and k are direct
+% neighbors of node i, i.e., (j, k) exits if and only if exists (i, j) and
+% (i, k). Nodes (i, k) can generate "confunsions" in the network if they have
+% the same PCI. Such edges are used to avoid/minimize **CONFUSIONS**.
+int: NUM_SECOND_LEVEL_NEIGHBORS;
+
+% Each line represents an edge between undirect neighbors as defined before.
+array[1..NUM_SECOND_LEVEL_NEIGHBORS, 1..2] of int: SECOND_LEVEL_NEIGHBORS;
+
+% ids of cells for which the pci should remain unchanged
+set of int: PCI_UNCHANGEABLE_CELLS;
+
+% This array has the original pcis of all the cells. array is indexed by the ids
+% of the cell. eg. ORIGINAL_PCIS[3] returns the pci of cell whose id is 3.
+% ids start from 0
+% array[0..NUM_NODES-1] of 0..NUM_PCIS-1: ORIGINAL_PCIS;
+array[1..NUM_NODES] of 0..NUM_PCIS-1: ORIGINAL_PCIS;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Decision variables
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Defines the PCI for each node.
+array[0..NUM_NODES-1] of var 0..NUM_PCIS-1: pci;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Constraints
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+constraint
+if(length(PCI_UNCHANGEABLE_CELLS) !=0) then
+forall(i in PCI_UNCHANGEABLE_CELLS)(
+ pci[i] == ORIGINAL_PCIS[i+1]
+)
+endif;
+
+
+% Direct neighbors must have different PCIs for avoid **COLLISION**.
+constraint
+forall(i in 1..NUM_NEIGHBORS)(
+ pci[NEIGHBORS[i, 1]] != pci[NEIGHBORS[i, 2]]
+);
+
+% Undirect neighbors must have different PCIs for avoid **CONFUSIONS**.
+constraint
+forall(i in 1..NUM_SECOND_LEVEL_NEIGHBORS)(
+ pci[SECOND_LEVEL_NEIGHBORS[i, 1]] != pci[SECOND_LEVEL_NEIGHBORS[i, 2]]
+);
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Objective function
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Just satisfy the problem.
+solve :: int_search(pci, smallest, indomain_min, complete) satisfy;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Output
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+output
+["node,pci\n"] ++
+[
+ show(node) ++ "," ++ show(pci[node]) ++ "\n"
+| node in 0..NUM_NODES-1
+]
diff --git a/apps/pci/optimizers/solver/optimizer.py b/apps/pci/optimizers/solver/optimizer.py
new file mode 100644
index 0000000..13298ed
--- /dev/null
+++ b/apps/pci/optimizers/solver/optimizer.py
@@ -0,0 +1,179 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from collections import defaultdict
+import itertools
+import os
+import pymzn
+
+from apps.pci.optimizers.solver.ml_model import MlModel
+from apps.pci.optimizers.solver.pci_utils import get_id
+from apps.pci.optimizers.solver.pci_utils import mapping
+from osdf.config.base import osdf_config
+
+BASE_DIR = os.path.dirname(__file__)
+cell_id_mapping = dict()
+id_cell_mapping = dict()
+
+
+def pci_optimize(network_cell_info, cell_info_list, request_json):
+ global cell_id_mapping, id_cell_mapping
+ cell_id_mapping, id_cell_mapping = mapping(network_cell_info)
+ original_pcis = get_original_pci_list(network_cell_info)
+ unchangeable_pcis = get_ids_of_fixed_pci_cells(request_json['cellInfo'].get('fixedPCICells', []))
+ neighbor_edges = get_neighbor_list(network_cell_info)
+ second_level_edges = get_second_level_neighbor(network_cell_info)
+ ignorable_links = get_ignorable_links(network_cell_info, request_json)
+ anr_flag = is_anr(request_json)
+
+ dzn_data = build_dzn_data(cell_info_list, ignorable_links, neighbor_edges, second_level_edges, anr_flag,
+ original_pcis, unchangeable_pcis)
+
+ ml_enabled = osdf_config.core['PCI']['ml_enabled']
+ if ml_enabled:
+ MlModel().get_additional_inputs(dzn_data, network_cell_info)
+
+ return build_pci_solution(dzn_data, ignorable_links, anr_flag)
+
+
+def get_ids_of_fixed_pci_cells(fixed_pci_list):
+ fixed_pci_ids = set()
+ for cell in fixed_pci_list:
+ fixed_pci_ids.add(cell_id_mapping[cell])
+ return fixed_pci_ids
+
+
+def get_cell_id_pci_mapping(network_cell_info):
+ original_pcis = dict()
+ for cell in network_cell_info['cell_list']:
+ for nbr in cell['nbr_list']:
+ if cell_id_mapping[nbr['targetCellId']] not in original_pcis:
+ original_pcis[cell_id_mapping[nbr['targetCellId']]] = nbr['pciValue']
+ return original_pcis
+
+
+def get_original_pci_list(network_cell_info):
+ cell_id_pci_mapping = get_cell_id_pci_mapping(network_cell_info)
+ original_pcis_list = []
+ for i in range(len(cell_id_pci_mapping)):
+ original_pcis_list.append(cell_id_pci_mapping.get(i))
+ return original_pcis_list
+
+
+def build_pci_solution(dzn_data, ignorable_links, anr_flag):
+ mzn_solution = solve(get_mzn_model(anr_flag), dzn_data)
+ if mzn_solution == 'UNSATISFIABLE':
+ return mzn_solution
+ solution = {'pci': mzn_solution[0]['pci']}
+
+ if anr_flag:
+ removables = defaultdict(list)
+ used_ignorables = mzn_solution[0]['used_ignorables']
+ index = 0
+ for i in ignorable_links:
+ if used_ignorables[index] > 0:
+ removables[i[0]].append(i[1])
+ index += 1
+ solution['removables'] = removables
+ return solution
+
+
+def build_dzn_data(cell_info_list, ignorable_links, neighbor_edges, second_level_edges, anr_flag, original_pcis,
+ unchangeable_pcis):
+ dzn_data = {
+ 'NUM_NODES': len(cell_info_list),
+ 'NUM_PCIS': len(cell_info_list),
+ 'NUM_NEIGHBORS': len(neighbor_edges),
+ 'NEIGHBORS': get_list(neighbor_edges),
+ 'NUM_SECOND_LEVEL_NEIGHBORS': len(second_level_edges),
+ 'SECOND_LEVEL_NEIGHBORS': get_list(second_level_edges),
+ 'PCI_UNCHANGEABLE_CELLS': unchangeable_pcis,
+ 'ORIGINAL_PCIS': original_pcis
+ }
+ if anr_flag:
+ dzn_data['NUM_IGNORABLE_NEIGHBOR_LINKS'] = len(ignorable_links)
+ dzn_data['IGNORABLE_NEIGHBOR_LINKS'] = get_list(ignorable_links)
+ return dzn_data
+
+
+def get_mzn_model(anr_flag):
+ if anr_flag:
+ mzn_model = os.path.join(BASE_DIR, 'min_confusion_inl.mzn')
+ else:
+ mzn_model = os.path.join(BASE_DIR, 'no_conflicts_no_confusion.mzn')
+ return mzn_model
+
+
+def is_anr(request_json):
+ return 'pci-anr' in request_json["requestInfo"]["optimizers"]
+
+
+def get_list(edge_list):
+ array_list = []
+ for s in edge_list:
+ array_list.append([s[0], s[1]])
+ return sorted(array_list)
+
+
+def solve(mzn_model, dzn_data):
+ return pymzn.minizinc(mzn=mzn_model, data=dzn_data)
+
+
+def get_neighbor_list(network_cell_info):
+ neighbor_list = set()
+ for cell in network_cell_info['cell_list']:
+ add_to_neighbor_list(network_cell_info, cell, neighbor_list)
+ return neighbor_list
+
+
+def add_to_neighbor_list(network_cell_info, cell, neighbor_list):
+ for nbr in cell.get('nbr_list', []):
+ host_id = cell['id']
+ nbr_id = get_id(network_cell_info, nbr['targetCellId'])
+ if nbr_id and host_id != nbr_id:
+ neighbor_list.add((host_id, nbr_id))
+
+
+def get_second_level_neighbor(network_cell_info):
+ second_neighbor_list = set()
+ for cell in network_cell_info['cell_list']:
+ comb_list = build_second_level_list(network_cell_info, cell)
+ for comb in comb_list:
+ if comb[0] and comb[1]:
+ second_neighbor_list.add((comb[0], comb[1]))
+ return sorted(second_neighbor_list)
+
+
+def build_second_level_list(network_cell_info, cell):
+ second_nbr_list = []
+ for nbr in cell.get('nbr_list', []):
+ second_nbr_list.append(get_id(network_cell_info, nbr['targetCellId']))
+ return [list(elem) for elem in list(itertools.combinations(second_nbr_list, 2))]
+
+
+def get_ignorable_links(network_cell_info, request_json):
+ ignorable_list = set()
+ anr_input_list = request_json["cellInfo"].get('anrInputList', [])
+ if anr_input_list:
+ for anr_info in anr_input_list:
+ cell_id = get_id(network_cell_info, anr_info['cellId'])
+ anr_removable = anr_info.get('removeableNeighbors', [])
+ for anr in anr_removable:
+ ignorable_list.add((cell_id, get_id(network_cell_info, anr)))
+ return ignorable_list
diff --git a/apps/pci/optimizers/solver/pci_utils.py b/apps/pci/optimizers/solver/pci_utils.py
new file mode 100644
index 0000000..7db3a6f
--- /dev/null
+++ b/apps/pci/optimizers/solver/pci_utils.py
@@ -0,0 +1,47 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+def mapping(network_cell_info):
+ cell_id_mapping= dict()
+ id_cell_mapping = dict()
+ for i in network_cell_info['cell_list']:
+ cell_id_mapping[i['cell_id']] = i['id']
+ id_cell_mapping[i['id']] = i['cell_id']
+ return cell_id_mapping, id_cell_mapping
+
+def get_id(network_cell_info, cell_id):
+ for i in network_cell_info['cell_list']:
+ if i['cell_id'] == cell_id:
+ return i['id']
+ return None
+
+
+def get_cell_id(network_cell_info, id):
+ for i in network_cell_info['cell_list']:
+ if i['id'] == id:
+ return i['cell_id']
+ return None
+
+
+def get_pci_value(network_cell_info, id):
+ cell_id = get_cell_id(network_cell_info, id)
+ for i in network_cell_info['cell_list']:
+ for j in i['nbr_list']:
+ if cell_id == j['targetCellId']:
+ return j['pciValue']
+ return None
diff --git a/apps/placement/__init__.py b/apps/placement/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/placement/__init__.py
diff --git a/apps/placement/models/__init__.py b/apps/placement/models/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/placement/models/__init__.py
diff --git a/apps/placement/models/api/__init__.py b/apps/placement/models/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/placement/models/api/__init__.py
diff --git a/apps/placement/models/api/placementRequest.py b/apps/placement/models/api/placementRequest.py
new file mode 100644
index 0000000..e04c2af
--- /dev/null
+++ b/apps/placement/models/api/placementRequest.py
@@ -0,0 +1,105 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2015-2017 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType, StringType, URLType, IntType, BooleanType
+from schematics.types.compound import ModelType, ListType, DictType
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ callbackHeader = DictType(BaseType)
+ sourceId = StringType(required=True)
+ requestType = StringType(required=True)
+ numSolutions = IntType()
+ optimizers = ListType(StringType(required=True))
+ timeout = IntType()
+
+
+class Candidates(OSDFModel):
+ """Preferred candidate for a resource (sent as part of a request from client)"""
+ identifierType = StringType(required=True)
+ identifiers = ListType(StringType(required=True))
+ cloudOwner = StringType()
+
+
+class ModelMetaData(OSDFModel):
+ """Model information for a specific resource"""
+ modelInvariantId = StringType(required=True)
+ modelVersionId = StringType(required=True)
+ modelName = StringType()
+ modelType = StringType()
+ modelVersion = StringType()
+ modelCustomizationName = StringType()
+
+
+class LicenseModel(OSDFModel):
+ entitlementPoolUUID = ListType(StringType(required=True))
+ licenseKeyGroupUUID = ListType(StringType(required=True))
+
+
+class LicenseDemands(OSDFModel):
+ resourceModuleName = StringType(required=True)
+ serviceResourceId = StringType(required=True)
+ resourceModelInfo = ModelType(ModelMetaData, required=True)
+ existingLicenses = ModelType(LicenseModel)
+
+
+class LicenseInfo(OSDFModel):
+ licenseDemands = ListType(ModelType(LicenseDemands))
+
+
+class PlacementDemand(OSDFModel):
+ resourceModuleName = StringType(required=True)
+ serviceResourceId = StringType(required=True)
+ tenantId = StringType()
+ resourceModelInfo = ModelType(ModelMetaData, required=True)
+ existingCandidates = ListType(ModelType(Candidates))
+ excludedCandidates = ListType(ModelType(Candidates))
+ requiredCandidates = ListType(ModelType(Candidates))
+
+
+class ServiceInfo(OSDFModel):
+ serviceInstanceId = StringType(required=True)
+ modelInfo = ModelType(ModelMetaData, required=True)
+ serviceName = StringType(required=True)
+
+
+class SubscriberInfo(OSDFModel):
+ """Details on the customer that subscribes to the VNFs"""
+ globalSubscriberId = StringType(required=True)
+ subscriberName = StringType()
+ subscriberCommonSiteId = StringType()
+
+
+class PlacementInfo(OSDFModel):
+ """Information specific to placement optimization"""
+ requestParameters = DictType(BaseType)
+ placementDemands = ListType(ModelType(PlacementDemand), min_size=1)
+ subscriberInfo = ModelType(SubscriberInfo)
+
+
+class PlacementAPI(OSDFModel):
+ """Request for placement optimization (specific to optimization and additional metadata"""
+ requestInfo = ModelType(RequestInfo, required=True)
+ placementInfo = ModelType(PlacementInfo, required=True)
+ licenseInfo = ModelType(LicenseInfo)
+ serviceInfo = ModelType(ServiceInfo, required=True) \ No newline at end of file
diff --git a/apps/placement/models/api/placementResponse.py b/apps/placement/models/api/placementResponse.py
new file mode 100644
index 0000000..13b8d7a
--- /dev/null
+++ b/apps/placement/models/api/placementResponse.py
@@ -0,0 +1,64 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2015-2017 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType, StringType
+from schematics.types.compound import ModelType, ListType, DictType
+
+
+# TODO: update osdf.models
+
+class LicenseSolution(OSDFModel):
+ serviceResourceId = StringType(required=True)
+ resourceModuleName = StringType(required=True)
+ entitlementPoolUUID = ListType(StringType(required=True))
+ licenseKeyGroupUUID = ListType(StringType(required=True))
+ entitlementPoolInvariantUUID = ListType(StringType(required=True))
+ licenseKeyGroupInvariantUUID = ListType(StringType(required=True))
+
+
+class Candidates(OSDFModel):
+ """Preferred candidate for a resource (sent as part of a request from client)"""
+ identifierType = StringType(required=True)
+ identifiers = ListType(StringType(required=True))
+ cloudOwner = StringType()
+
+
+class AssignmentInfo(OSDFModel):
+ key = StringType(required=True)
+ value = BaseType(required=True)
+
+
+class PlacementSolution(OSDFModel):
+ serviceResourceId = StringType(required=True)
+ resourceModuleName = StringType(required=True)
+ solution = ModelType(Candidates, required=True)
+ assignmentInfo = ListType(ModelType(AssignmentInfo))
+
+
+class Solution(OSDFModel):
+ placementSolutions = ListType(ListType(ModelType(PlacementSolution), min_size=1))
+ licenseSolutions = ListType(ModelType(LicenseSolution), min_size=1)
+
+
+class PlacementResponse(OSDFModel):
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ requestStatus = StringType(required=True)
+ statusMessage = StringType()
+ solutions = ModelType(Solution, required=True)
diff --git a/apps/placement/optimizers/__init__.py b/apps/placement/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/placement/optimizers/__init__.py
diff --git a/apps/placement/optimizers/conductor/__init__.py b/apps/placement/optimizers/conductor/__init__.py
new file mode 100644
index 0000000..4b25e5b
--- /dev/null
+++ b/apps/placement/optimizers/conductor/__init__.py
@@ -0,0 +1,17 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2017-2018 AT&T Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
diff --git a/apps/placement/optimizers/conductor/remote_opt_processor.py b/apps/placement/optimizers/conductor/remote_opt_processor.py
new file mode 100644
index 0000000..2e681be
--- /dev/null
+++ b/apps/placement/optimizers/conductor/remote_opt_processor.py
@@ -0,0 +1,178 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2015-2017 AT&T Intellectual Property
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from jinja2 import Template
+import json
+from requests import RequestException
+import traceback
+
+from apps.license.optimizers.simple_license_allocation import license_optim
+from osdf.adapters.conductor import conductor
+from osdf.logging.osdf_logging import debug_log
+from osdf.logging.osdf_logging import error_log
+from osdf.logging.osdf_logging import metrics_log
+from osdf.logging.osdf_logging import MH
+from osdf.operation.error_handling import build_json_error_body
+from osdf.utils.interfaces import get_rest_client
+from osdf.utils.mdc_utils import mdc_from_json
+
+
+def conductor_response_processor(conductor_response, req_id, transaction_id):
+ """Build a response object to be sent to client's callback URL from Conductor's response
+
+ This includes Conductor's placement optimization response, and required ASDC license artifacts
+ :param conductor_response: JSON response from Conductor
+ :param raw_response: Raw HTTP response corresponding to above
+ :param req_id: Id of a request
+ :return: JSON object that can be sent to the client's callback URL
+ """
+ composite_solutions = []
+ name_map = {"physical-location-id": "cloudClli", "host_id": "vnfHostName",
+ "cloud_version": "cloudVersion", "cloud_owner": "cloudOwner",
+ "cloud": "cloudRegionId", "service": "serviceInstanceId", "is_rehome": "isRehome",
+ "location_id": "locationId", "location_type": "locationType", "directives": "oof_directives"}
+ for reco in conductor_response['plans'][0]['recommendations']:
+ for resource in reco.keys():
+ c = reco[resource]['candidate']
+ solution = {
+ 'resourceModuleName': resource,
+ 'serviceResourceId': reco[resource].get('service_resource_id', ""),
+ 'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']),
+ 'identifiers': [c['candidate_id']],
+ 'cloudOwner': c.get('cloud_owner', "")},
+ 'assignmentInfo': []
+ }
+ for key, value in c.items():
+ if key in ["location_id", "location_type", "is_rehome", "host_id"]:
+ try:
+ solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
+ except KeyError:
+ debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info"
+ .format(key))
+
+ for key, value in reco[resource]['attributes'].items():
+ try:
+ solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
+ except KeyError:
+ debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info"
+ .format(key))
+ composite_solutions.append(solution)
+
+ request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \
+ else conductor_response['plans'][0]['status']
+ status_message = conductor_response.get('plans')[0].get('message', "")
+
+ solution_info = {}
+ if composite_solutions:
+ solution_info.setdefault('placementSolutions', [])
+ solution_info['placementSolutions'].append(composite_solutions)
+
+ resp = {
+ "transactionId": transaction_id,
+ "requestId": req_id,
+ "requestStatus": request_status,
+ "statusMessage": status_message,
+ "solutions": solution_info
+ }
+ return resp
+
+
+def conductor_no_solution_processor(conductor_response, request_id, transaction_id,
+ template_placement_response="templates/plc_opt_response.jsont"):
+ """Build a response object to be sent to client's callback URL from Conductor's response
+
+ This is for case where no solution is found
+ :param conductor_response: JSON response from Conductor
+ :param raw_response: Raw HTTP response corresponding to above
+ :param request_id: request Id associated with the client request (same as conductor response's "name")
+ :param template_placement_response: the template for generating response to client (plc_opt_response.jsont)
+ :return: JSON object that can be sent to the client's callback URL
+ """
+ status_message = conductor_response["plans"][0].get("message")
+ templ = Template(open(template_placement_response).read())
+ return json.loads(templ.render(composite_solutions=[], requestId=request_id, license_solutions=[],
+ transactionId=transaction_id,
+ requestStatus="completed", statusMessage=status_message, json=json))
+
+
+def process_placement_opt(request_json, policies, osdf_config):
+ """Perform the work for placement optimization (e.g. call SDC artifact and make conductor request)
+
+ NOTE: there is scope to make the requests to policy asynchronous to speed up overall performance
+ :param request_json: json content from original request
+ :param policies: flattened policies corresponding to this request
+ :param osdf_config: configuration specific to OSDF app
+ :param prov_status: provStatus retrieved from Subscriber policy
+ :return: None, but make a POST to callback URL
+ """
+
+ try:
+ mdc_from_json(request_json)
+ rc = get_rest_client(request_json, service="so")
+ req_id = request_json["requestInfo"]["requestId"]
+ transaction_id = request_json['requestInfo']['transactionId']
+
+ metrics_log.info(MH.inside_worker_thread(req_id))
+ license_info = None
+ if request_json.get('licenseInfo', {}).get('licenseDemands'):
+ license_info = license_optim(request_json)
+
+ # Conductor only handles placement, only call Conductor if placementDemands exist
+ if request_json.get('placementInfo', {}).get('placementDemands'):
+ metrics_log.info(MH.requesting("placement/conductor", req_id))
+ req_info = request_json['requestInfo']
+ demands = request_json['placementInfo']['placementDemands']
+ request_parameters = request_json['placementInfo']['requestParameters']
+ service_info = request_json['serviceInfo']
+ template_fields = {
+ 'location_enabled': True,
+ 'version': '2017-10-10'
+ }
+ resp = conductor.request(req_info, demands, request_parameters, service_info, template_fields,
+ osdf_config, policies)
+ if resp["plans"][0].get("recommendations"):
+ placement_response = conductor_response_processor(resp, req_id, transaction_id)
+ else: # "solved" but no solutions found
+ placement_response = conductor_no_solution_processor(resp, req_id, transaction_id)
+ if license_info: # Attach license solution if it exists
+ placement_response['solutionInfo']['licenseInfo'] = license_info
+ else: # License selection only scenario
+ placement_response = {
+ "transactionId": transaction_id,
+ "requestId": req_id,
+ "requestStatus": "completed",
+ "statusMessage": "License selection completed successfully",
+ "solutionInfo": {"licenseInfo": license_info}
+ }
+ except Exception as err:
+ error_log.error("Error for {} {}".format(req_id, traceback.format_exc()))
+
+ try:
+ body = build_json_error_body(err)
+ metrics_log.info(MH.sending_response(req_id, "ERROR"))
+ rc.request(json=body, noresponse=True)
+ except RequestException:
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
+ return
+
+ try:
+ metrics_log.info(MH.calling_back_with_body(req_id, rc.url, placement_response))
+ rc.request(json=placement_response, noresponse=True)
+ except RequestException: # can't do much here but log it and move on
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
diff --git a/apps/placement/templates/plc_opt_request.jsont b/apps/placement/templates/plc_opt_request.jsont
new file mode 100755
index 0000000..a218b8a
--- /dev/null
+++ b/apps/placement/templates/plc_opt_request.jsont
@@ -0,0 +1,142 @@
+{
+ "name": "{{ name }}",
+ "files": "{{ files }}",
+ "timeout": "{{ timeout }}",
+ "num_solution": "{{ limit }}",
+ "template": {
+ "CUST_ID": "{{ cust_id }}",
+ "E2EVPNKEY": "{{ e2evpnkey }}",
+ "UCPEHOST": "{{ ucpehost }}",
+ "WAN_PORT1_UP": "{{ wan_port1_up }}",
+ "WAN_PORT1_DOWN": "{{ wan_port1_down }}",
+ "EFFECTIVE_BANDWIDTH": "{{ effective_bandwidth }}",
+ "SERVICE_INST": "{{ service_inst }}",
+ "locations": {
+ "customer_loc": {
+ "host_name": "{{ ucpehost }}"
+ }
+ },
+ "demands": [
+ {% set comma=joiner(",") %}
+ {% for demand in demand_list %} {{ comma() }}
+ {
+ "{{ demand.vnf_name }}": [
+ {% set comma2=joiner(",") %}
+ {% for property in demand.property %}
+ "inventory_provider": {{ property.inventory_provider }},
+ "inventory_type": {{ property.inventory_type }},
+ "service_type": {{ property.service_type }},
+ "customer_id": {{ property.customer_id }},
+ "candidate_id": {{ property.candidate_id }}
+ {% endfor %}
+ ]
+ }
+ {% endfor %}
+ ],
+ "constraints": {
+ {% set comma_main=joiner(",") %}
+
+ {% if attribute_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for attribute in attribute_policy_list %} {{ comma() }}
+ attribute['identity'] : {
+ "type": {{ attribute['type'] }},
+ "demands": {{ attribute['demands'] }},
+ "properties": {
+ "evaluate": {
+ "hypervisor": {{ attribute['property']['hypervisor'] }},
+ "aic_version": {{ attribute['property']['aicVersion'] }},
+ "aic_type": {{ attribute['property']['aicType'] }},
+ "dataplane": {{ attribute['property']['datatype'] }},
+ "network_roles": {{ attribute['property']['networkRoles'] }},
+ "complex": {{ attribute['property']['complex'] }}
+ }
+ }
+ }
+ {% endfor %}
+
+ {% if distance_to_location_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for distance_location in distance_to_location_policy_list %} {{ comma() }}
+ distance_location['identity'] : {
+ "type": {{ distance_location['type'] }},
+ "demands": {{ distance_location['demands'] }},
+ "properties": {
+ "distance": {{ distance_location['property']['distance'] }},
+ "location": {{ distance_location['property']['location'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if inventory_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for inventory in inventory_policy_list %} {{ comma() }}
+ inventory['identity'] : {
+ "type": {{ inventory['type'] }},
+ "demands": {{ inventory['demands'] }}
+ }
+ {% endfor %}
+
+ {% if resource_instance_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for resource_instance in resource_instance_policy_list %} {{ comma() }}
+ resource_instance['identity'] : {
+ "type": {{ resource_instance['type'] }},
+ "demands": {{ resource_instance['demands'] }},
+ "properties": {
+ "controller": {{ resource_instance['property']['controller'] }},
+ "request": {{ resource_instance['property']['request'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if resource_region_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for resource_region in resource_region_policy_list %} {{ comma() }}
+ resource_region['identity'] : {
+ "type": {{ resource_region['type'] }},
+ "demands": {{ resource_region['demands'] }},
+ "properties": {
+ "controller": {{ resource_region['property']['controller'] }},
+ "request": {{ resource_region['property']['request'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if zone_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for zone in zone_policy_list %} {{ comma() }}
+ zone['identity'] : {
+ "type": {{ zone['type'] }},
+ "demands": {{ zone['demands'] }},
+ "properties": {
+ "qualifier": {{ resource_region['property']['qualifier'] }},
+ "category": {{ resource_region['property']['category'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if optmization_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for optimization in optimization_policy_list %} {{ comma() }}
+ "optimization" : {
+ {{ optimization['objective'] }}: {
+ "sum": [
+ {% set comma2=joiner(",") %}
+ {% for parameter in optimization['parameter'] %} {{ comma() }}
+ {
+ "product": [
+ {{ parameter['weight'] }},
+ {
+ "distance_between": [{{ parameter['customerLocation'] }},{{ parameter['demand'] }}]
+ }
+ ]
+ }
+ {% endfor %}
+ ]
+ }
+ }
+ {% endfor %}
+ }
+ }
+}
diff --git a/apps/placement/templates/plc_opt_response.jsont b/apps/placement/templates/plc_opt_response.jsont
new file mode 100755
index 0000000..e5709e7
--- /dev/null
+++ b/apps/placement/templates/plc_opt_response.jsont
@@ -0,0 +1,10 @@
+{
+ "requestId": "{{requestId}}",
+ "transactionId": "{{transacationId}}",
+ "requestStatus": "{{requestStatus}}",
+ "statusMessage": "{{statusMessage}}"
+ "solutions": {
+ "placementSolutions": {{ json.dumps(composite_solutions) }},
+ "licenseSolutions":{{ json.dumps(license_solutions) }}
+ }
+}
diff --git a/apps/placement/templates/policy_request.jsont b/apps/placement/templates/policy_request.jsont
new file mode 100755
index 0000000..3a9e201
--- /dev/null
+++ b/apps/placement/templates/policy_request.jsont
@@ -0,0 +1,3 @@
+{
+ "policyName": "{{policy_name}}" {# we currently only support query by policy name only -- policyName #}
+}
diff --git a/apps/route/__init__.py b/apps/route/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/route/__init__.py
diff --git a/apps/route/optimizers/__init__.py b/apps/route/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/route/optimizers/__init__.py
diff --git a/apps/route/optimizers/inter_domain_route_opt.py b/apps/route/optimizers/inter_domain_route_opt.py
new file mode 100644
index 0000000..253c7b2
--- /dev/null
+++ b/apps/route/optimizers/inter_domain_route_opt.py
@@ -0,0 +1,370 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2020 Fujitsu Limited Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+
+
+import os
+import itertools
+import json
+import requests
+from requests.auth import HTTPBasicAuth
+import urllib3
+
+from osdf.logging.osdf_logging import audit_log
+import pymzn
+from sklearn import preprocessing
+
+BASE_DIR = os.path.dirname(__file__)
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+
+class InterDomainRouteOpt:
+
+ """
+ This values will need to deleted..
+ only added for the debug purpose
+ """
+ aai_headers = {
+ "X-TransactionId": "9999",
+ "X-FromAppId": "OOF",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ }
+
+
+ def get_route(self, request, osdf_config):
+ """
+ This method processes the mdons route request
+ and returns an optimised path for the given
+ two ports
+ """
+
+ try:
+ route_info = request["routeInfo"]["routeRequest"]
+ src_controller_id = route_info["srcDetails"]["controllerId"]
+ src_port_id = route_info["srcDetails"]["interfaceId"]
+ dst_controller_id = route_info["dstDetails"]["controllerId"]
+ dst_port_id = route_info["dstDetails"]["interfaceId"]
+ service_rate = route_info["serviceRate"]
+ dzn_data, mapping_table = self.build_dzn_data(osdf_config, src_controller_id,
+ dst_controller_id, service_rate)
+ audit_log.info("Dzn data")
+ audit_log.info(dzn_data)
+ mzn_model = os.path.join(BASE_DIR, 'route_opt.mzn')
+ links_list = self.find_suitable_path(mzn_model, dzn_data, mapping_table)
+ ordered_list = self.get_ordered_route_list(links_list,
+ src_controller_id, dst_controller_id)
+ solution = self.get_solution_object(ordered_list, src_port_id, dst_port_id)
+ return {
+ "requestId": request["requestInfo"]["requestId"],
+ "transactionId": request["requestInfo"]["transactionId"],
+ "statusMessage": "SUCCESS",
+ "requestStatus": "accepted",
+ "solutions": solution
+ }
+ except Exception as err:
+ audit_log.info(err)
+ raise err
+
+ def get_solution_object(self, ordered_list, src_port_id, dst_port_id):
+ """
+ :param ordered_list: service_route list
+ :param src_port_id: source port id of route
+ :param dst_port_id: destination port id of route
+ :return: solution object of the route respone
+ """
+ service_route_list = []
+ link_list = []
+ for value in ordered_list:
+ service_route_object = {}
+ service_route_object["srcInterfaceId"] = src_port_id
+ service_route_object["dstInterfaceId"] = value["srcPortId"]
+ service_route_object["controllerId"] = value["srcControllerId"]
+ service_route_list.append(service_route_object)
+ link_list.append(value["linkName"])
+ src_port_id = value["dstPortId"]
+ dst_controller_id = value["dstControllerId"]
+ service_route_object = {}
+ service_route_object["srcInterfaceId"] = src_port_id
+ service_route_object["dstInterfaceId"] = dst_port_id
+ service_route_object["controllerId"] = dst_controller_id
+ service_route_list.append(service_route_object)
+ route_info_object = {
+ "serviceRoute" : service_route_list,
+ "linkList" : link_list
+ }
+ solution = {
+ "routeInfo" : route_info_object
+ }
+ return solution
+
+
+ def get_ordered_route_list(self, link_list, src_controller_id, dst_controller_id):
+ """
+ :param link_list: link list from the minizinc response
+ :param src_controller_id: source port id of route
+ :param dst_controller_id: destination port id of route
+ :return: route list in order
+ """
+ ordered_link_list = []
+ flag = True
+ while flag:
+ for item in link_list:
+ if item["srcControllerId"] == src_controller_id:
+ ordered_link_list.append(item)
+ src_controller_id = item["dstControllerId"]
+ if src_controller_id == dst_controller_id:
+ flag = False
+ return ordered_link_list
+
+
+ def find_suitable_path(self, mzn_model, dzn_data, mapping_table):
+ """
+ :param mzn_model: minizinc model details
+ :param dzn_data: minizinc data
+ :param mapping_table: list that maintains AAI link details
+ :return: list of link from after running minizinc
+ """
+ minizinc_solution = self.solve(mzn_model, dzn_data)
+ audit_log.info("Minizinc Solution ==========>")
+ routes = list(minizinc_solution)
+ audit_log.info(routes)
+ try:
+ arr = routes[0]['x']
+ except Exception as err:
+ audit_log.info("No minizinc solutions found")
+ raise err
+ links_list = []
+ for i in range(0, len(routes[0]['x'])):
+ if arr[i] == 1:
+ links_list.append(mapping_table[i])
+ return links_list
+
+
+ def process_inter_domain_link(self, logical_link, osdf_config):
+ """
+ :param logical_link: logical links from AAI
+ :param osdf_config: OSDF config details
+ :return: list of link object with src and dst controller details
+ """
+ link_details = {}
+ link_details["linkName"] = logical_link["link-name"]
+ relationship = logical_link["relationship-list"]["relationship"]
+ flag = 1
+
+ for value in relationship:
+ if value["related-to"] == "p-interface" and flag == 1:
+ src_port_id = value["relationship-data"][1]["relationship-value"]
+ src_controller_id = self.get_controller_for_interface(osdf_config, src_port_id)
+ link_details["srcPortId"] = src_port_id
+ link_details["srcControllerId"] = src_controller_id
+ flag += 1
+ elif value["related-to"] == "p-interface" and flag == 2:
+ dest_port_id = value["relationship-data"][1]["relationship-value"]
+ dest_controller_id = self.get_controller_for_interface(osdf_config, dest_port_id)
+ link_details["dstPortId"] = dest_port_id
+ link_details["dstControllerId"] = dest_controller_id
+ return link_details
+
+
+ def prepare_map_table(self, osdf_config, logical_links):
+ """
+ :param logical_links: logical links from AAI
+ :param osdf_config: OSDF config details
+ :return: list of link object with src and dst controller details
+ """
+ results = map(self.process_inter_domain_link, logical_links,
+ itertools.repeat(osdf_config, len(logical_links)))
+ new_results = list(results)
+
+ new_list = []
+ new_list += new_results
+ for i in new_results:
+ link_details = {}
+ link_details["linkName"] = i["linkName"]
+ link_details["srcPortId"] = i["dstPortId"]
+ link_details["srcControllerId"] = i["dstControllerId"]
+ link_details["dstPortId"] = i["srcPortId"]
+ link_details["dstControllerId"] = i["srcControllerId"]
+ new_list.append(link_details)
+ return new_list
+
+
+ def solve(self, mzn_model, dzn_data):
+ """
+ :param mzn_model: minizinc template
+ :param dzn_data: minizinc data model
+ :return: minizinc response
+ """
+ return pymzn.minizinc(mzn=mzn_model, data=dzn_data)
+
+
+ def get_links_based_on_bandwidth_attributes(self, logical_links_list,
+ osdf_config, service_rate):
+ """
+ This method filters the logical links based on the
+ bandwidth attribute availability of the interfaces
+ from AAI
+ :return: filtered_list[]
+ """
+ filtered_list = []
+ for logical_link in logical_links_list:
+ relationship = logical_link["relationship-list"]["relationship"]
+ count = 0
+ for value in relationship:
+ if value["related-to"] == "p-interface":
+ interface_url = value["related-link"]
+ if self.get_available_bandwidth_aai(interface_url, osdf_config, service_rate):
+ count += 1
+ if count == 2:
+ filtered_list.append(logical_link)
+
+ return filtered_list
+
+
+ def build_dzn_data(self, osdf_config, src_controller_id, dst_controller_id, service_rate):
+ """
+ :param osdf_config: OSDF config details
+ :param src_controller_id: controller Id of the source port
+ :param dst_controller_id: controller id of the destination port
+ :param service_rate: service rate
+ :return: mapping atble which maintains link details from AAI
+ and minizinc data model to be used by template
+ """
+ logical_links = self.get_inter_domain_links(osdf_config)
+ logical_links_list = logical_links["logical-link"]
+ mapping_table = self.prepare_map_table(osdf_config,
+ self.get_links_based_on_bandwidth_attributes(logical_links_list, osdf_config, service_rate))
+
+ edge_start = []
+ edge_end = []
+ for item in mapping_table:
+ edge_start.append(item["srcControllerId"])
+ edge_end.append(item["dstControllerId"])
+ link_cost = []
+ for k in range(0, len(edge_start)):
+ link_cost.append(1)
+ list_controllers = self.get_controllers_from_aai(osdf_config)
+ le = preprocessing.LabelEncoder()
+ le.fit(list_controllers)
+
+ start_edge = le.transform(edge_start)
+ end_edge = le.transform(edge_end)
+ source = le.transform([src_controller_id])
+ destination = le.transform([dst_controller_id])
+
+ final_dzn_start_arr = []
+ for i in start_edge:
+ final_dzn_start_arr.append(i)
+
+ final_dzn_end_arr = []
+ for j in end_edge:
+ final_dzn_end_arr.append(j)
+
+ contollers_length = len(list_controllers)
+ no_of_edges = len(final_dzn_start_arr)
+ dzn_data = {
+ 'N': contollers_length,
+ 'M': no_of_edges,
+ 'Edge_Start': final_dzn_start_arr,
+ 'Edge_End': final_dzn_end_arr,
+ 'L': link_cost,
+ 'Start': source[0],
+ 'End' : destination[0]
+ }
+ return dzn_data, mapping_table
+
+
+ def get_inter_domain_links(self, osdf_config):
+ """
+ This method returns list of all cross ONAP links
+ from /aai/v19/network/logical-links?link-type=inter-domain&operational-status="Up"
+ :return: logical-links[]
+ """
+
+ config = osdf_config.deployment
+ aai_url = config["aaiUrl"]
+ aai_req_url = aai_url + config["aaiGetInterDomainLinksUrl"]
+ response = requests.get(aai_req_url, headers=self.aai_headers,
+ auth=HTTPBasicAuth("AAI", "AAI"), verify=False)
+ if response.status_code == 200:
+ return response.json()
+
+
+ def get_controller_for_interface(self, osdf_config, port_id):
+ """
+ This method returns returns the controller id
+ given a p-interface from the below query
+ :return: controller_id
+ """
+ data = {
+ "start": ["external-system"],
+ "query": "query/getDomainController?portid="
+ }
+ query = data.get("query") + port_id
+ data.update(query=query)
+ config = osdf_config.deployment
+ aai_url = config["aaiUrl"]
+ aai_req_url = aai_url + config["controllerQueryUrl"]
+ response = requests.put(aai_req_url, data=json.dumps(data),
+ headers=self.aai_headers,
+ auth=HTTPBasicAuth("AAI", "AAI"),
+ verify=False)
+ if response.status_code == 200:
+ response_body = response.json()
+ return response_body["results"][0]["esr-thirdparty-sdnc"]["thirdparty-sdnc-id"]
+
+
+ def get_controllers_from_aai(self, osdf_config):
+ """
+ This method returns returns the list of
+ controller names in AAI
+ :return: controllers_list[]
+ """
+ controllers_list = []
+ config = osdf_config.deployment
+ aai_url = config["aaiUrl"]
+ aai_req_url = aai_url + config["aaiGetControllersUrl"]
+ response = requests.get(aai_req_url,
+ headers=self.aai_headers,
+ auth=HTTPBasicAuth("AAI", "AAI"),
+ verify=False)
+ if response.status_code == 200:
+ response_body = response.json()
+ esr_thirdparty_list = response_body["esr-thirdparty-sdnc"]
+
+ for item in esr_thirdparty_list:
+ controllers_list.append(item["thirdparty-sdnc-id"])
+ return controllers_list
+
+
+ def get_available_bandwidth_aai(self, interface_url, osdf_config, service_rate):
+ """
+ Checks if the given interface has the required bandwidth
+ :return: boolean flag
+ """
+ config = osdf_config.deployment
+ aai_url = config["aaiUrl"]
+ aai_req_url = aai_url + interface_url + "?depth=all"
+ response = requests.get(aai_req_url,
+ headers=self.aai_headers,
+ auth=HTTPBasicAuth("AAI", "AAI"), verify=False)
+ if response.status_code == 200:
+ response_body = response.json()
+ available_bandwidth = response_body["bandwidth-attributes"]["bandwidth-attribute"][0]["available-bandwidth-map"]["available-bandwidth"]
+ for i in available_bandwidth:
+ if i["odu-type"] == service_rate and i["number"] > 0:
+ return True
diff --git a/apps/route/optimizers/route_opt.mzn b/apps/route/optimizers/route_opt.mzn
new file mode 100644
index 0000000..7aa73cb
--- /dev/null
+++ b/apps/route/optimizers/route_opt.mzn
@@ -0,0 +1,53 @@
+
+% Number of nodes
+int: N;
+ % Start node
+0..N-1: Start;
+ % End node
+0..N-1: End;
+ % Number of edges (directed arcs)
+int: M;
+ % The actual edges
+set of int: Edges = 1..M;
+ % Edge lengths
+array[Edges] of int: L;
+ % Edge start node
+array[Edges] of 0..N-1: Edge_Start;
+array[Edges] of 0..N-1: Edge_End;
+
+ % Variable indicating if edge is used
+array[Edges] of var 0..1: x;
+
+constraint
+ forall( i in 0..N-1 ) (
+ if i = Start then
+ % outgoing flow
+ sum(e in Edges where Edge_Start[e] = i)(x[e]) -
+ % incoming flow
+ sum(e in Edges where Edge_End[e] = i)(x[e])
+ = 1
+ elseif i = End then
+ sum(e in Edges where Edge_Start[e] = i)(x[e]) -
+ sum(e in Edges where Edge_End[e] = i)(x[e])
+ = -1
+ else
+ sum(e in Edges where Edge_Start[e] = i)(x[e]) -
+ sum(e in Edges where Edge_End[e] = i)(x[e])
+ = 0
+ endif
+ );
+
+
+solve minimize sum(e in Edges)( L[e] * x[e] );
+%solve satisfy;
+
+output ["Length: ", show(sum(e in Edges)(L[e] * x[e])), "\n"] ++
+ ["Start : ", show(Start), "\n"] ++
+ ["End : ", show(End), "\n\n"] ++
+ ["Edges in shortest path:\n"] ++
+ [ if fix(x[e]) = 1
+ then show(Edge_Start[e]) ++ " -> " ++ show(Edge_End[e]) ++ "\n"
+ else ""
+ endif | e in Edges
+ ];
+
diff --git a/apps/route/optimizers/simple_route_opt.py b/apps/route/optimizers/simple_route_opt.py
new file mode 100644
index 0000000..9113516
--- /dev/null
+++ b/apps/route/optimizers/simple_route_opt.py
@@ -0,0 +1,266 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2020 Huawei Intellectual Property
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import requests
+import json
+from requests.auth import HTTPBasicAuth
+
+from osdf.utils.mdc_utils import mdc_from_json
+from osdf.logging.osdf_logging import MH, audit_log, error_log, debug_log
+import pymzn
+from sklearn import preprocessing
+
+import os
+BASE_DIR = os.path.dirname(__file__)
+
+class RouteOpt:
+
+ """
+ This values will need to deleted..
+ only added for the debug purpose
+ """
+ # DNS server and standard port of AAI..
+ # TODO: read the port from the configuration and add to DNS
+ aai_headers = {
+ "X-TransactionId": "9999",
+ "X-FromAppId": "OOF",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ }
+
+ def is_cross_onap_link(self, logical_link):
+ """
+ This method checks if cross link is cross onap
+ :param logical_link:
+ :return:
+ """
+ for relationship in logical_link["relationship-list"]["relationship"]:
+ if relationship["related-to"] == "ext-aai-network":
+ return True
+ return False
+
+ def get_links_name(self, routes,initial_start_edge,initial_end_edge, mappingTable):
+ routes=list(routes)
+ try:
+ arr=routes[0]['x']
+ except Exception as err:
+ audit_log.info("No satisfiable solutions found")
+ raise err
+ listOfLinks=[]
+ for i in range(0, len(routes[0]['x'])):
+ individual_link = {}
+ if arr[i] == 1 :
+ # listOfLinks.append(self.fetchLogicalLinks(initial_start_edge[i], initial_end_edge[i], mappingTable))
+ individual_link["link"] = mappingTable[initial_start_edge[i] + ":" + initial_end_edge[i]]
+ individual_link["start_node"] = initial_start_edge[i]
+ individual_link["end_node"] = initial_end_edge[i]
+ listOfLinks.append(individual_link)
+
+ return listOfLinks
+
+ def solve(self, mzn_model, dzn_data):
+ return pymzn.minizinc(mzn=mzn_model, data=dzn_data)
+
+ def get_links(self, mzn_model, dzn_data, initial_start_edge,initial_end_edge, mappingTable):
+ routes = self.solve(mzn_model, dzn_data)
+ audit_log.info("mocked minizinc solution====>")
+ audit_log.info(routes)
+
+ converted_links=self.get_links_name(routes, initial_start_edge,initial_end_edge, mappingTable)
+ audit_log.info("converted links===>")
+ audit_log.info(converted_links)
+ return converted_links
+
+ def addition(self, data):
+ res = ""
+ if 'relationship-list' in data.keys():
+ relationship = data["relationship-list"]["relationship"]
+ for index, eachItem in enumerate(relationship):
+ temp = eachItem["relationship-data"][0]
+ if index == len(relationship) - 1:
+ res += temp['relationship-value']
+ else:
+ res += temp['relationship-value'] + ":"
+
+ return data["link-name"], res
+ else:
+ return data["link-name"], res
+
+ def create_map_table(self, logical_links):
+ result = map(self.addition, logical_links)
+
+ parseTemplate = {}
+
+ for eachItem in result:
+ parseTemplate[eachItem[1]] = eachItem[0]
+ audit_log.info("mapping table")
+ audit_log.info(parseTemplate)
+ return parseTemplate
+
+ def build_dzn_data(self, src_access_node_id, dst_access_node_id, osdf_config):
+ Edge_Start = []
+ Edge_End = []
+ logical_links = self.get_logical_links(osdf_config)
+
+
+ logical_links = logical_links['logical-link']
+ audit_log.info("mocked response of AAI received (logical links) successful===>")
+ audit_log.info(logical_links)
+ # prepare map table
+ mappingTable = self.create_map_table(logical_links)
+ audit_log.info("mapping table created successfully====>")
+ audit_log.info(mappingTable)
+ # take the logical link where both the p-interface in same onap
+ if logical_links is not None:
+ audit_log.info('logical links not empty=====>')
+ for logical_link in logical_links:
+ audit_log.info('logical_link')
+ audit_log.info(logical_link)
+
+ if 'relationship-list' in logical_link.keys():
+ if not self.is_cross_onap_link(logical_link):
+ # link is in local ONAP
+ audit_log.info('link is inside onap===>')
+ relationship = logical_link["relationship-list"]["relationship"]
+
+ relationshipStartNode = relationship[0]
+ audit_log.info('relationshipStartNode')
+ audit_log.info(relationshipStartNode)
+ relationshipStartNodeID = relationshipStartNode["related-link"].split("/")[-4]
+ audit_log.info('relationshipStartNodeID')
+ audit_log.info(relationshipStartNodeID)
+ Edge_Start.append(relationshipStartNodeID)
+
+ relationshipEndtNode = relationship[1]
+ relationshipEndNodeID = relationshipEndtNode["related-link"].split("/")[-4]
+ audit_log.info('relationshipEndNodeID')
+ audit_log.info(relationshipEndNodeID)
+ Edge_End.append(relationshipEndNodeID)
+ else:
+ continue
+
+ audit_log.info("edge start and end array of i/p address are===>")
+ audit_log.info(Edge_Start)
+ audit_log.info(Edge_End)
+ # labeling ip to number for mapping
+ le = preprocessing.LabelEncoder()
+ le.fit(Edge_Start + Edge_End)
+ dzn_start_edge = le.transform(Edge_Start)
+
+ final_dzn_start_arr = []
+ for i in range(0, len(dzn_start_edge)):
+ final_dzn_start_arr.append(dzn_start_edge[i])
+
+ final_dzn_end_arr = []
+ dzn_end_edge = le.transform(Edge_End)
+ for j in range(0, len(dzn_end_edge)):
+ final_dzn_end_arr.append(dzn_end_edge[j])
+
+ audit_log.info("start and end array that passed in dzn_data===>")
+ audit_log.info(final_dzn_start_arr)
+ audit_log.info(final_dzn_end_arr)
+
+ link_cost = []
+ for k in range(0, len(final_dzn_start_arr)):
+ link_cost.append(1)
+
+ audit_log.info("src_access_node_id")
+ audit_log.info(src_access_node_id)
+ source= le.transform([src_access_node_id])
+ audit_log.info("vallue of source===>")
+ audit_log.info(source)
+ if source in final_dzn_start_arr :
+ start = source[0]
+ audit_log.info("source node")
+ audit_log.info(start)
+
+ audit_log.info("dst_access_node_id")
+ audit_log.info(dst_access_node_id)
+ destination= le.transform([dst_access_node_id])
+ if destination in final_dzn_end_arr :
+ end = destination[0]
+ audit_log.info("destination node")
+ audit_log.info(end)
+ # data to be prepared in the below format:
+ dzn_data = {
+ 'N': self.total_node(final_dzn_start_arr + final_dzn_end_arr),
+ 'M': len(final_dzn_start_arr),
+ 'Edge_Start': final_dzn_start_arr,
+ 'Edge_End': final_dzn_end_arr,
+ 'L': link_cost,
+ 'Start': start,
+ 'End': end,
+ }
+ # can not do reverse mapping outside of this scope, so doing here
+ audit_log.info("reverse mapping after prepared dzn_data")
+ initial_start_edge=le.inverse_transform(final_dzn_start_arr)
+ initial_end_edge=le.inverse_transform(final_dzn_end_arr)
+ audit_log.info(initial_start_edge)
+ audit_log.info(initial_end_edge)
+ return dzn_data, initial_start_edge,initial_end_edge, mappingTable
+
+ def total_node(self, node):
+ nodeSet = set()
+ for i in range(0, len(node)):
+ nodeSet.add(node[i])
+ total_node = len(nodeSet)
+ return total_node
+
+ def get_route(self, request, osdf_config):
+ """
+ This method checks
+ :param logical_link:
+ :return:
+ """
+ try:
+ routeInfo = request["routeInfo"]["routeRequests"]
+ routeRequest = routeInfo[0]
+ src_access_node_id = routeRequest["srcPort"]["accessNodeId"]
+ dst_access_node_id = routeRequest["dstPort"]["accessNodeId"]
+
+ dzn_data, initial_start_edge, initial_end_edge, mappingTable = self.build_dzn_data(src_access_node_id, dst_access_node_id, osdf_config)
+ #mzn_model = "/home/root1/Videos/projects/osdf/test/functest/simulators/osdf/optimizers/routeopt/route_opt.mzn"
+ mzn_model = os.path.join(BASE_DIR, 'route_opt.mzn')
+
+ routeSolutions = self.get_links(mzn_model, dzn_data, initial_start_edge,initial_end_edge, mappingTable)
+
+ return {
+ "requestId": request["requestInfo"]["requestId"],
+ "transactionId": request["requestInfo"]["transactionId"],
+ "statusMessage": " ",
+ "requestStatus": "accepted",
+ "solutions": routeSolutions
+ }
+ except Exception as err:
+ audit_log.info(err)
+ raise err
+
+ def get_logical_links(self, osdf_config):
+ """
+ This method returns list of all cross ONAP links
+ from /aai/v14/network/logical-links?operation-status="Up"
+ :return: logical-links[]
+ """
+
+ config = osdf_config.deployment
+ aai_url = config["aaiUrl"]
+ aai_req_url = aai_url + config["aaiGetLinksUrl"]
+
+ response = requests.get(aai_req_url,headers=self.aai_headers,auth=HTTPBasicAuth("AAI", "AAI"),verify=False)
+ if response.status_code == 200:
+ return response.json() \ No newline at end of file
diff --git a/apps/slice_selection/__init__.py b/apps/slice_selection/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/slice_selection/__init__.py
diff --git a/apps/slice_selection/models/api/__init__.py b/apps/slice_selection/models/api/__init__.py
new file mode 100644
index 0000000..b45f74d
--- /dev/null
+++ b/apps/slice_selection/models/api/__init__.py
@@ -0,0 +1,17 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
diff --git a/apps/slice_selection/models/api/nsi_selection_request.py b/apps/slice_selection/models/api/nsi_selection_request.py
new file mode 100644
index 0000000..b395012
--- /dev/null
+++ b/apps/slice_selection/models/api/nsi_selection_request.py
@@ -0,0 +1,62 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType
+from schematics.types import BooleanType
+from schematics.types.compound import DictType
+from schematics.types.compound import ListType
+from schematics.types.compound import ModelType
+from schematics.types import IntType
+from schematics.types import StringType
+from schematics.types import URLType
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ sourceId = StringType(required=True)
+ callbackHeader = DictType(BaseType)
+ timeout = IntType()
+ numSolutions = IntType()
+ addtnlArgs = DictType(BaseType)
+
+
+class NxTInfo(OSDFModel):
+ """Information about NST/NSST model"""
+ invariantUUID = StringType(required=True)
+ UUID = StringType(required=True)
+ name = StringType(required=True)
+
+
+class SubnetCapability(OSDFModel):
+ """Subnet capability of every subnet"""
+ domainType = StringType(required=True)
+ capabilityDetails = DictType(BaseType, required=True)
+
+
+class NSISelectionAPI(OSDFModel):
+ """Request for nsi selection (specific to optimization and additional metadata"""
+ requestInfo = ModelType(RequestInfo, required=True)
+ NSTInfo = ModelType(NxTInfo, required=True)
+ NSSTInfo = ListType(ModelType(NxTInfo), required=False)
+ serviceProfile = DictType(BaseType, required=True)
+ subnetCapabilities = ListType(ModelType(SubnetCapability), required=True)
+ preferReuse = BooleanType()
diff --git a/apps/slice_selection/models/api/nsi_selection_response.py b/apps/slice_selection/models/api/nsi_selection_response.py
new file mode 100644
index 0000000..3c6d35b
--- /dev/null
+++ b/apps/slice_selection/models/api/nsi_selection_response.py
@@ -0,0 +1,54 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType, StringType, BooleanType
+from schematics.types.compound import ModelType, ListType, DictType
+
+
+# TODO: update osdf.models
+class SharedNSISolution(OSDFModel):
+ """Represents the shared NSI Solution object"""
+ invariantUUID = StringType(required=True)
+ UUID = StringType(required=True)
+ NSIName = StringType(required=True)
+ NSIId = StringType(required=True)
+ matchLevel = StringType(required=True)
+
+
+class NewNSISolution(OSDFModel):
+ """Represents the New NSI Solution object containing tuple of slice profiles"""
+ sliceProfiles = ListType(DictType(BaseType), required=True)
+ matchLevel = StringType(required=True)
+
+
+class NSISolution(OSDFModel):
+ """Represents the NSI Solution object"""
+ """This solution object contains either sharedNSISolution or newNSISolution"""
+ existingNSI = BooleanType(required=True)
+ sharedNSISolution = ModelType(SharedNSISolution)
+ newNSISolution = ModelType(NewNSISolution)
+
+
+class NSISelectionResponse(OSDFModel):
+ """Response sent to NSMF(SO)"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ requestStatus = StringType(required=True)
+ solutions = ListType(ModelType(NSISolution), required=True)
+ statusMessage = StringType()
diff --git a/apps/slice_selection/models/api/nssi_selection_request.py b/apps/slice_selection/models/api/nssi_selection_request.py
new file mode 100644
index 0000000..c670abe
--- /dev/null
+++ b/apps/slice_selection/models/api/nssi_selection_request.py
@@ -0,0 +1,41 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType, StringType, URLType, IntType
+from schematics.types.compound import ModelType, DictType
+
+from apps.slice_selection.models.api.nsi_selection_request import NxTInfo
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ sourceId = StringType(required=True)
+ callbackHeader = DictType(BaseType)
+ timeout = IntType()
+ numSolutions = IntType()
+ addtnlArgs = DictType(BaseType)
+
+
+class NSSISelectionAPI(OSDFModel):
+ """Request for NSSI selection (specific to optimization and additional metadata"""
+ requestInfo = ModelType(RequestInfo, required=True)
+ NSSTInfo = ModelType(NxTInfo, required=True)
+ sliceProfile = DictType(BaseType, required=True)
diff --git a/apps/slice_selection/models/api/nssi_selection_response.py b/apps/slice_selection/models/api/nssi_selection_response.py
new file mode 100644
index 0000000..af67f65
--- /dev/null
+++ b/apps/slice_selection/models/api/nssi_selection_response.py
@@ -0,0 +1,40 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import StringType
+from schematics.types.compound import ModelType, ListType
+
+
+# TODO: update osdf.models
+class SharedNSSISolution(OSDFModel):
+ """Represents the shared NSSI Solution object"""
+ invariantUUID = StringType(required=True)
+ UUID = StringType(required=True)
+ NSSIName = StringType(required=True)
+ NSSIId = StringType(required=True)
+ matchLevel = StringType(required=True)
+
+
+class NSSISelectionResponse(OSDFModel):
+ """Response sent to NSSMF(SO)"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ requestStatus = StringType(required=True)
+ solutions = ListType(ModelType(SharedNSSISolution), required=True)
+ statusMessage = StringType()
diff --git a/apps/slice_selection/optimizers/__init__.py b/apps/slice_selection/optimizers/__init__.py
new file mode 100644
index 0000000..b45f74d
--- /dev/null
+++ b/apps/slice_selection/optimizers/__init__.py
@@ -0,0 +1,17 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
diff --git a/apps/slice_selection/optimizers/conductor/__init__.py b/apps/slice_selection/optimizers/conductor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/slice_selection/optimizers/conductor/__init__.py
diff --git a/apps/slice_selection/optimizers/conductor/remote_opt_processor.py b/apps/slice_selection/optimizers/conductor/remote_opt_processor.py
new file mode 100644
index 0000000..68c9409
--- /dev/null
+++ b/apps/slice_selection/optimizers/conductor/remote_opt_processor.py
@@ -0,0 +1,134 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+"""
+Module for processing slice selection request
+"""
+
+from requests import RequestException
+from threading import Thread
+import traceback
+
+from apps.slice_selection.optimizers.conductor.response_processor import ResponseProcessor
+from osdf.adapters.conductor import conductor
+from osdf.adapters.policy.interface import get_policies
+from osdf.logging.osdf_logging import debug_log
+from osdf.logging.osdf_logging import error_log
+from osdf.utils.interfaces import get_rest_client
+from osdf.utils.mdc_utils import mdc_from_json
+
+
+class SliceSelectionOptimizer(Thread):
+ def __init__(self, osdf_config, slice_config, request_json, model_type):
+ super().__init__()
+ self.osdf_config = osdf_config
+ self.slice_config = slice_config
+ self.request_json = request_json
+ self.model_type = model_type
+ self.response_processor = ResponseProcessor(request_json['requestInfo'], slice_config)
+
+ def run(self):
+ self.process_slice_selection_opt()
+
+ def process_slice_selection_opt(self):
+ """Process the slice selection request from the API layer"""
+ req_info = self.request_json['requestInfo']
+ rc = get_rest_client(self.request_json, service='so')
+
+ try:
+ if self.model_type == 'NSSI' \
+ and self.request_json['sliceProfile'].get('resourceSharingLevel', "") \
+ in ['not-shared', 'non-shared']:
+ final_response = self.response_processor.get_slice_selection_response([])
+
+ else:
+ final_response = self.do_slice_selection()
+
+ except Exception as ex:
+ error_log.error("Error for {} {}".format(req_info.get('requestId'),
+ traceback.format_exc()))
+ error_message = str(ex)
+ final_response = self.response_processor.process_error_response(error_message)
+
+ try:
+ rc.request(json=final_response, noresponse=True)
+ except RequestException:
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_info['request_id'],
+ traceback.format_exc()))
+
+ def do_slice_selection(self):
+ req_info = self.request_json['requestInfo']
+ app_info = self.slice_config['app_info'][self.model_type]
+ mdc_from_json(self.request_json)
+ requirements = self.request_json.get(app_info['requirements_field'], {})
+ model_info = self.request_json.get(app_info['model_info'])
+ model_name = model_info['name']
+ policies = self.get_app_policies(model_name, app_info['app_name'])
+ request_parameters = self.get_request_parameters(requirements, model_info)
+
+ demands = [
+ {
+ "resourceModuleName": model_name,
+ "resourceModelInfo": {}
+ }
+ ]
+
+ try:
+ template_fields = {
+ 'location_enabled': False,
+ 'version': '2020-08-13'
+ }
+ resp = conductor.request(req_info, demands, request_parameters, {}, template_fields,
+ self.osdf_config, policies)
+ except RequestException as e:
+ resp = e.response.json()
+ error = resp['plans'][0]['message']
+ if isinstance(error, list) and "Unable to find any" in error[0]:
+ return self.response_processor.get_slice_selection_response([])
+ error_log.error('Error from conductor {}'.format(error))
+ return self.response_processor.process_error_response(error)
+
+ debug_log.debug("Response from conductor {}".format(str(resp)))
+ recommendations = resp["plans"][0].get("recommendations")
+ subnets = [subnet['domainType'] for subnet in self.request_json['subnetCapabilities']] \
+ if self.request_json.get('subnetCapabilities') else []
+ return self.response_processor.process_response(recommendations, model_info, subnets, self.model_type)
+
+ def get_request_parameters(self, requirements, model_info):
+ camel_to_snake = self.slice_config['attribute_mapping']['camel_to_snake']
+ request_params = {camel_to_snake[key]: value for key, value in requirements.items()}
+ subnet_capabilities = self.request_json.get('subnetCapabilities')
+ if subnet_capabilities:
+ for subnet_capability in subnet_capabilities:
+ domain_type = f"{subnet_capability['domainType']}_"
+ capability_details = subnet_capability['capabilityDetails']
+ for key, value in capability_details.items():
+ request_params[f"{domain_type}{camel_to_snake[key]}"] = value
+ request_params.update(model_info)
+ return request_params
+
+ def get_app_policies(self, model_name, app_name):
+ policy_request_json = self.request_json.copy()
+ policy_request_json['serviceInfo'] = {'serviceName': model_name}
+ if 'serviceProfile' in self.request_json:
+ slice_scope = self.request_json['serviceProfile']['resourceSharingLevel']
+ if 'preferReuse' in self.request_json and slice_scope == "shared":
+ slice_scope = slice_scope + "," + ("reuse" if self.request_json['preferReuse'] else "create_new")
+ policy_request_json['slice_scope'] = slice_scope
+ debug_log.debug("policy_request_json {}".format(str(policy_request_json)))
+ return get_policies(policy_request_json, app_name)
diff --git a/apps/slice_selection/optimizers/conductor/response_processor.py b/apps/slice_selection/optimizers/conductor/response_processor.py
new file mode 100644
index 0000000..2357ab9
--- /dev/null
+++ b/apps/slice_selection/optimizers/conductor/response_processor.py
@@ -0,0 +1,108 @@
+# -------------------------------------------------------------------------
+# Copyright (C) 2020 Wipro Limited.
+#
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+"""
+Module for processing response from conductor for slice selection
+"""
+
+import re
+
+
+class ResponseProcessor(object):
+ def __init__(self, request_info, slice_config):
+ self.request_info = request_info
+ self.slice_config = slice_config
+
+ def process_response(self, recommendations, model_info, subnets, model_type):
+ """Process conductor response to form the response for the API request
+
+ :param recommendations: recommendations from conductor
+ :param model_info: model info from the request
+ :param subnets: list of subnets
+ :param model_type: NSI or NSSI
+ :return: response json as a dictionary
+ """
+ if not recommendations:
+ return self.get_slice_selection_response([])
+ model_name = model_info['name']
+ solutions = [self.get_solution_from_candidate(rec[model_name]['candidate'], model_info, subnets, model_type)
+ for rec in recommendations]
+ return self.get_slice_selection_response(solutions)
+
+ def get_solution_from_candidate(self, candidate, model_info, subnets, model_type):
+ if candidate['inventory_type'] == 'slice_profiles':
+ return {
+ 'existingNSI': False,
+ 'newNSISolution': {
+ 'sliceProfiles': self.get_slice_profiles_from_candidate(candidate, subnets)
+ }
+ }
+ elif model_type == 'NSSI':
+ return {
+ 'UUID': model_info['UUID'],
+ 'invariantUUID': model_info['invariantUUID'],
+ 'NSSIName': candidate['instance_name'],
+ 'NSSIId': candidate['instance_id']
+ }
+
+ elif model_type == 'NSI':
+ return {
+ 'existingNSI': True,
+ 'sharedNSISolution': {
+ 'UUID': model_info['UUID'],
+ 'invariantUUID': model_info['invariantUUID'],
+ 'NSIName': candidate['instance_name'],
+ 'NSIId': candidate['instance_id']
+ }
+ }
+
+ def get_slice_profiles_from_candidate(self, candidate, subnets):
+ slice_profiles = []
+ for subnet in subnets:
+ slice_profile = {self.get_profile_attribute(k, subnet): v for k, v in candidate.items()
+ if k.startswith(subnet)}
+ slice_profile['domainType'] = subnet
+ slice_profiles.append(slice_profile)
+ return slice_profiles
+
+ def get_profile_attribute(self, attribute, subnet):
+ snake_to_camel = self.slice_config['attribute_mapping']['snake_to_camel']
+ return snake_to_camel[re.sub(f'^{subnet}_', '', attribute)]
+
+ def process_error_response(self, error_message):
+ """Form response message from the error message
+
+ :param error_message: error message while processing the request
+ :return: response json as dictionary
+ """
+ return {'requestId': self.request_info['requestId'],
+ 'transactionId': self.request_info['transactionId'],
+ 'requestStatus': 'error',
+ 'statusMessage': error_message}
+
+ def get_slice_selection_response(self, solutions):
+ """Get NSI selection response from final solution
+
+ :param solutions: final solutions
+ :return: NSI selection response to send back as dictionary
+ """
+ return {'requestId': self.request_info['requestId'],
+ 'transactionId': self.request_info['transactionId'],
+ 'requestStatus': 'completed',
+ 'statusMessage': '',
+ 'solutions': solutions}
diff --git a/apps/templates/cms_opt_request.jsont b/apps/templates/cms_opt_request.jsont
new file mode 100755
index 0000000..006562b
--- /dev/null
+++ b/apps/templates/cms_opt_request.jsont
@@ -0,0 +1,35 @@
+{
+ "transaction_id": "{{ transaction_id }}",
+ "request_id": "{{ request_id }}",
+ "start_date" : "{{ start_time }}",
+ "end_date" : "{{ end_time }}",
+ "change_elements" : {{ json.dumps(change_elements) }},
+ "constraints" : [
+ {
+ "type" : "general_concurrency_limit",
+ "parameters": [{{ concurrency_limit }}]
+ },
+
+ {
+ "type" : "allowed_forbidden_periods",
+ "parameters" : {{ json.dumps(allowed_periods) }}
+ }
+
+ {% if spatial_conflicts is defined and spatial_conflicts|length > 0 %}
+ ,
+ {
+ "type" : "spatial_conflict",
+ "parameters": {{ json.dumps(spatial_conflicts) }}
+ }
+ {% endif %}
+
+
+ {% if critical_periods is defined and spatial_conflicts|length > 0 %}
+ ,
+ {
+ "type" : "critical_periods",
+ "parameters": {{ json.dumps(critical_periods) }}
+ }
+ {% endif %}
+ ]
+}
diff --git a/apps/templates/cms_opt_request.jsont_1707_v1 b/apps/templates/cms_opt_request.jsont_1707_v1
new file mode 100755
index 0000000..75ecbe5
--- /dev/null
+++ b/apps/templates/cms_opt_request.jsont_1707_v1
@@ -0,0 +1,67 @@
+{
+ "transaction_id": "{{ transaction_id }}",
+ "request_id": "{{ request_id }}",
+ "start_date" : "{{ start_time }}",
+ "end_date" : "{{ end_time }}",
+
+ "change_elements" : [
+ {% set comma = joiner(",") -%}
+ {% for element in all_upgrades -%} {{ comma() }}
+ {
+ "id" : "{{ element.id }}",
+ "failback_duration": {{ element.failback_duration }},
+ {% if element.group_id -%}
+ "group_id": "{{ element.group_id }}",
+ {% endif %}
+ {% if element.scheduled_on -%}
+ "scheduled_on": "{{ element.scheduled_on }}",
+ {% endif %}
+ "duration": {{ element.duration }}
+ }
+ {% endfor -%}
+ ],
+
+ "constraints" : [
+ {
+ "type" : "general_concurrency_limit",
+ "parameters": [{{ concurrency_limit }}]
+ },
+
+ {
+ "type" : "allowed_forbidden_periods",
+ "parameters" : [
+ {% set comma = joiner(",") -%}
+ {% for idx in all_pending -%} {{ comma() }}
+ { "id" : "{{ idx.id }}",
+ "allowed_periods": [ {{ allowed_periods }}]
+ }
+ {% endfor -%}
+ ]
+ },
+ {
+ "type" : "spatial_conflict",
+ "parameters": [
+ {% set comma = joiner(",") -%}
+ {% for pserver, vce_list in vce_pserver_mapping.items() -%} {{ comma() }}
+ {
+ "spatial_entity": "{{ pserver }}",
+ "affected_entities": {{ vce_list }}
+ }
+ {% endfor -%}
+ ]
+ },
+
+ {
+ "type" : "critical_periods",
+ "parameters": [
+ {% set comma = joiner(",") -%}
+ {% for element, conflict_period in conflict_interval.items() -%} {{ comma() }}
+ {
+ "id" : "{{ element }}",
+ "periods": [{{ conflict_period }}]
+ }
+ {% endfor -%}
+ ]
+ }
+ ]
+}
diff --git a/apps/templates/cms_opt_request_1702.jsont b/apps/templates/cms_opt_request_1702.jsont
new file mode 100755
index 0000000..bcafa45
--- /dev/null
+++ b/apps/templates/cms_opt_request_1702.jsont
@@ -0,0 +1,63 @@
+{
+ "request_id": "{{ request_id }}",
+ "startdate" : "{{ start_time }}",
+ "enddate" : "{{ end_time }}",
+
+ "change_elements" : [
+{% set comma = joiner(",") -%}
+{% for element in all_upgrades -%} {{ comma() }}
+ { "id" : "{{ element.id }}",
+ {% if element.scheduled -%} "scheduled_on": "{{ element.scheduled }}", {% endif -%}
+ "duration": {{ element.duration }}, {# duration in seconds #}
+ "failback_duration": {{ element.failback_duration }}, {# duration in seconds #}
+ "group_id": {{ element.group_id }}, {# duration in seconds #}
+ }{% endfor -%}
+ ],
+
+ "constraints" : [
+ {
+ "type" : "general_concurrency_limit",
+ "parameters" : [ {{ general_concurrency_limit }} ]
+ },
+
+ {
+ "type" : "allowed_forbidden_periods",
+ "parameters" : [
+{% set comma = joiner(",") -%}
+{% for idx in all_pending -%} {{ comma() }}
+ { "id" : "{{ idx.id }}",
+ "allowed_periods": [ {% set comma2 = joiner(",") -%}
+ {% for period in allowed_periods -%} {{ comma2() }} [{{ json.dumps(period[0]) }}, {{ json.dumps(period[1]) }}]
+ {% endfor -%} ] }{% endfor -%}
+ ]
+ }
+
+{% if p_v_conflict is defined and p_v_conflict|length > 0 %}
+ ,
+ {
+ "type" : "critical_periods",
+ "description" : "Simultaneous upgrades",
+ "parameters" : [
+{% set comma2 = joiner(",") -%}
+{% for element in p_v_conflict -%} {{ comma2() }}
+ {
+ "id" : "{{ element[0] }}",
+ "periods" : [{{ json.dumps(element[0]) }}, {{ json.dumps(element[1]) }}]
+ }
+{% endfor -%}
+{% endif %}
+
+{% for pserver, vce_group in grouped_vces.items() -%} {{ comma() }}
+ ,
+ {
+ "id" : "{{ pserver }}",
+ "name" : "VCE's on pserver {{ pserver }}",
+ "description": "Only some VCEs on a pserver can be upgraded at a time",
+ "max_num_upgrades" : {{ max_num_upgrades(vce_group) }},
+ "upgrades" : {{ json.dumps(vce_group) }}
+ }
+{% endfor -%}
+ ]
+ }
+ ]
+}
diff --git a/apps/templates/cms_opt_response.jsont b/apps/templates/cms_opt_response.jsont
new file mode 100644
index 0000000..a8817df
--- /dev/null
+++ b/apps/templates/cms_opt_response.jsont
@@ -0,0 +1,8 @@
+{
+ "transactionId": "{{transaction_id}}",
+ "scheduleId":"{{schedule_id}}",
+ "requestState": "{{request_state}}",
+ "status": "{{status}}",
+ "description": "{{description}}",
+ "schedule": {{schedule}}
+} \ No newline at end of file
diff --git a/apps/templates/license_opt_request.jsont b/apps/templates/license_opt_request.jsont
new file mode 100644
index 0000000..7baa759
--- /dev/null
+++ b/apps/templates/license_opt_request.jsont
@@ -0,0 +1,6 @@
+{
+ "transactionId": "{{transaction_id}}",
+ "requestId": "{{request_id}}",
+ "partNumber": "{{part_number}}",
+ "licenseModel" : "{{artifact}}"
+} \ No newline at end of file
diff --git a/apps/templates/test_cms_nb_req_from_client.jsont b/apps/templates/test_cms_nb_req_from_client.jsont
new file mode 100755
index 0000000..a60c8ff
--- /dev/null
+++ b/apps/templates/test_cms_nb_req_from_client.jsont
@@ -0,0 +1,19 @@
+{
+ "schedulingInfo": {
+ "change_management_id": "{{ change_management_id }}",
+ "start_time": "{{ start_time }}",
+ "end_time": "{{ end_time }}",
+ "policy_id": {{ json.dumps(policy_id) }}, {# a list of policy Ids #}
+ "service_type": "{{ service_type }}",
+ "workflow_type": "{{ workflow_type }}",
+ "upgrades": {{ json.dumps(upgrades) }} {# a list of node Ids #}
+ },
+ "requestInfo": {
+ "requestId": "{{ requestId }}",
+ "sourceId": "{{ sourceId }}",
+ "optimizer": "{{ optimizer }}",
+ "numSolutions": "{{ numSolutions }}",
+ "callbackUrl" : "{{ callbackUrl }}"
+ }
+}
+
diff --git a/apps/templates/test_plc_nb_req_from_client.jsont b/apps/templates/test_plc_nb_req_from_client.jsont
new file mode 100755
index 0000000..998ffb3
--- /dev/null
+++ b/apps/templates/test_plc_nb_req_from_client.jsont
@@ -0,0 +1,52 @@
+{
+ "requestInfo": {
+ "requestId": "{{requestId}}",
+ "sourceId": "{{sourceId}}",
+ "optimizer": "{{optimizer}}",
+ "numSolutions": {{numSolutions}},
+ "timeout": {{timeout}},
+ "callbackUrl" : "{{callbackUrl}}"
+ },
+ "placementInfo": {
+ "modelInfo": {
+ "modelType": "{{modelType}}",
+ "modelInvariant": "{{modelInvariantId}}",
+ "modelVersionId": "{{modelVersionId}}",
+ "modelName": "{{modelName}}",
+ "modelVersion": "{{modelVersion}}",
+ "modelCustomizationId": "{{modelCustomizationId}}"
+ },
+ "subscriberInfo": {
+ "globalSubscriberId": "{{globalSubscriberId}}",
+ "subscriberName": "{{subscriberName}}",
+ "subscriberCommonSiteId": "{{subscriberCommonSiteId}}",
+ "ucpeHostName": "{{ucpeHostName}}"
+ },
+ "policyId": {{json.dumps(policyId)}},
+ "vnfInfo": {
+ "vnfType": "{{vnfType}}",
+ "vnfPartNumber": "{{vnfPartNumber}}",
+ "nominalThroughput": "{{nominalThroughput}}",
+ "vnfSoftwareVersion": "{{vnfSoftwareVersion}}",
+ "vnfManagementOption": "{{vnfManagementOption}}"
+ },
+ "vpnInfo": {
+ "vpnId": "{{vpnId}}",
+ "pvcId": "{{pvcId}}"
+ },
+ "serviceInfo": {
+ "dhvServiceInfo": {
+ "serviceInstanceId": "{{serviceInstanceId}}",
+ "serviceType": "{{serviceType}}",
+ "e2evpnkey": "{{e2evpnkey}}",
+ "dhvSiteEffectiveTransportBandwidth": {{dhvSiteEffectiveTransportBandwidth}},
+ "dhvIPSecTransportBandwidthUp": {{dhvIPSecTransportBandwidthUp}},
+ "dhvIPSecTransportBandwidthDown": {{dhvIPSecTransportBandwidthDown}},
+ "dhvIPSec2TransportBandwidthUp": {{dhvIPSec2TransportBandwidthUp}},
+ "dhvIPSec2TransportBandwidthDown": {{dhvIPSec2TransportBandwidthDown}},
+ "dhvVendorName": "{{dhvVendorName}}"
+ }
+ },
+ "demandInfo": {{json.dumps(demandInfo)}}
+ }
+}