diff options
Diffstat (limited to 'apps')
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)}}
+ }
+}
|