diff options
-rw-r--r-- | apps/route/optimizers/inter_domain_route_opt.py | 370 | ||||
-rwxr-xr-x | config/osdf_config.yaml | 4 | ||||
-rw-r--r-- | docs/sections/offeredapis.rst | 14 | ||||
-rw-r--r-- | docs/sections/swaggerdoc/oof-optf-opteng-api.json | 584 | ||||
-rwxr-xr-x | osdfapp.py | 11 | ||||
-rwxr-xr-x | test/functest/simulators/simulated-config/osdf_config.yaml | 4 | ||||
-rw-r--r-- | test/inter_domain_route_opt/bandwidth_attributes.json | 176 | ||||
-rw-r--r-- | test/inter_domain_route_opt/controllers_for_interfaces.json | 62 | ||||
-rw-r--r-- | test/inter_domain_route_opt/controllers_list.json | 16 | ||||
-rw-r--r-- | test/inter_domain_route_opt/get_links.json | 157 | ||||
-rw-r--r-- | test/inter_domain_route_opt/request.json | 30 | ||||
-rw-r--r-- | test/test_inter_domain_route_opt.py | 151 | ||||
-rw-r--r-- | tox.ini | 14 |
13 files changed, 1591 insertions, 2 deletions
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/config/osdf_config.yaml b/config/osdf_config.yaml index 4802a67..19f5574 100755 --- a/config/osdf_config.yaml +++ b/config/osdf_config.yaml @@ -53,6 +53,10 @@ configDbGetNbrListUrl: 'SDNCConfigDBAPI/getNbrList' #aai api aaiUrl: "https://aai.url:30233" aaiGetLinksUrl: "/aai/v16/network/logical-links" +aaiGetControllersUrl: /aai/v19/external-system/esr-thirdparty-sdnc-list +controllerQueryUrl: /aai/v19/query?format=resource +aaiGetInterDomainLinksUrl: /aai/v19/network/logical-links?link-type=inter-domain&operational-status=up + pciHMSUsername: test pciHMSPassword: passwd diff --git a/docs/sections/offeredapis.rst b/docs/sections/offeredapis.rst index f50831f..8269dea 100644 --- a/docs/sections/offeredapis.rst +++ b/docs/sections/offeredapis.rst @@ -11,8 +11,20 @@ This document describes the OSDF HAS (Homing and Allocation Service) API To view API documentation in the interactive swagger UI download the following and paste into the swagger tool here: https://editor.swagger.io -:download:`oof-osdf-has-api.json <./swaggerdoc/oof-osdf-has-api.json>` +.. csv-table:: + :header: "API name", "Swagger JSON" + :widths: 10,5 + "OOF OSDF HAS API", ":download:`link <./swaggerdoc/oof-osdf-has-api.json>`" + "OOF OPTENG API", ":download:`link <./swaggerdoc/oof-optf-opteng-api.json>`" + +OOF OSDF HAS API +................ .. swaggerv2doc:: ./swaggerdoc/oof-osdf-has-api.json +OOF OPTENG API +.............. +.. swaggerv2doc:: ./swaggerdoc/oof-optf-opteng-api.json + + diff --git a/docs/sections/swaggerdoc/oof-optf-opteng-api.json b/docs/sections/swaggerdoc/oof-optf-opteng-api.json new file mode 100644 index 0000000..4e77f76 --- /dev/null +++ b/docs/sections/swaggerdoc/oof-optf-opteng-api.json @@ -0,0 +1,584 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is the ONAP Optimization Engine (Generic Solver) API", + "version": "1.0.0", + "title": "ONAP Optimization ENGINE API", + "contact": { + "email": "vikas.varma@att.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "securityDefinitions": { + "basicAuth": { + "type": "basic", + "description": "HTTP Basic Auth" + } + }, + "security": [ + { + "basicAuth": [] + } + ], + "paths": { + "/optengine/v1": { + "post": { + "tags": [ + "Generic Solver Optimization" + ], + "summary": "Call the Generic Optimization engine", + "operationId": "optimizationRequest", + "description": "call optimization engine", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "optimizationRequest", + "description": "optimization request", + "schema": { + "$ref": "#/definitions/OptimizationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/OptimizationResponse" + } + }, + "202": { + "description": "An optimization request is accepted" + }, + "400": { + "description": "bad request" + }, + "401": { + "description": "Request body is not compliant with the API definition" + }, + "404": { + "description": "The server cannot find the requested URI" + }, + "405": { + "description": "The requested method is not supported by a server." + }, + "500": { + "description": "The server encountered an internal server error or timed out" + } + } + } + }, + "/optmodel/v1": { + "post": { + "tags": [ + "Request to add the Optimizer model, metadata" + ], + "summary": "Add/Insert the optimization models in the database", + "operationId": "optimModelRequestAPI", + "description": "Request to add update the Optimizer model, metadata", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "optimModelRequest", + "description": "optimization model request", + "schema": { + "$ref": "#/definitions/OptimModelRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/OptimModelResponse" + } + }, + "400": { + "description": "bad request" + }, + "401": { + "description": "Request body is not compliant with the API definition" + }, + "404": { + "description": "The server cannot find the requested URI" + }, + "405": { + "description": "The requested method is not supported by a server." + }, + "500": { + "description": "The server encountered an internal server error or timed out" + } + } + }, + "put": { + "tags": [ + "Request to update the Optimizer model, metadata" + ], + "summary": "Add/update the optimization models in the database", + "operationId": "updateModelRequestAPI", + "description": "Request to add update the Optimizer model, metadata", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "optimModelRequest", + "description": "optimization model request", + "schema": { + "$ref": "#/definitions/OptimModelRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/OptimModelResponse" + } + }, + "400": { + "description": "bad request" + }, + "401": { + "description": "Request body is not compliant with the API definition" + }, + "404": { + "description": "The server cannot find the requested URI" + }, + "405": { + "description": "The requested method is not supported by a server." + }, + "500": { + "description": "The server encountered an internal server error or timed out" + } + } + }, + "get": { + "tags": [ + "Retrieve all models" + ], + "summary": "Gets all Optim Model data", + "description": "Retrieves all Optim Models", + "operationId": "getAllOptModelData", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ArrayOfOptimModelResponse" + } + }, + "400": { + "description": "bad request" + }, + "401": { + "description": "Request body is not compliant with the API definition" + }, + "404": { + "description": "The server cannot find the requested URI" + }, + "405": { + "description": "The requested method is not supported by a server." + }, + "500": { + "description": "The server encountered an internal server error or timed out" + } + } + } + }, + "/optmodel/v1/{model_id}": { + "get": { + "tags": [ + "Retrieve Model Data" + ], + "summary": "Gets the Optim Model data", + "description": "Retrieves the Optim Model data given modelId", + "operationId": "getOptModelById", + "parameters": [ + { + "in": "path", + "name": "model_id", + "description": "Model ID", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/OptimModelResponse" + } + }, + "400": { + "description": "bad request" + }, + "401": { + "description": "Request body is not compliant with the API definition" + }, + "404": { + "description": "The server cannot find the requested URI" + }, + "405": { + "description": "The requested method is not supported by a server." + }, + "500": { + "description": "The server encountered an internal server error or timed out" + } + } + }, + "delete": { + "tags": [ + "Delete Model Data" + ], + "summary": "Delete the Optim Model data", + "description": "Deletes the Optim Model data given modelId", + "operationId": "deleteOptModelById", + "parameters": [ + { + "in": "path", + "name": "model_id", + "description": "Model ID", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/DeleteModelResponse" + } + }, + "400": { + "description": "bad request" + }, + "401": { + "description": "Request body is not compliant with the API definition" + }, + "404": { + "description": "The server cannot find the requested URI" + }, + "405": { + "description": "The requested method is not supported by a server." + }, + "500": { + "description": "The server encountered an internal server error or timed out" + } + } + } + } + }, + "definitions": { + "OptimizationResponse": { + "type": "object", + "required": [ + "transactionId", + "requestID", + "requestStatus" + ], + "properties": { + "transactionId": { + "type": "string", + "format": "uuid", + "description": "unique ID to track an ONAP transaction", + "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "requestID": { + "type": "string", + "format": "uuid", + "description": "A unique ID to track multiple requests associated with a transaction", + "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "requestStatus": { + "type": "string", + "description": "request status (accepted, done, completed,failed)", + "example": "done" + }, + "statusMessage": { + "type": "string", + "description": "Status message (incomplete, complete, unsatisfiable, unknown, unbounded, unsat_or_unbounded, error)", + "example": "complete" + }, + "solutions": { + "additionalProperties": { + "type": "object" + }, + "example": { + "SCHEDULED": [ + [ + 0, + 1 + ], + [ + 0, + 1 + ] + ], + "OPTIMIZED": 2 + } + } + } + }, + "OptimizationRequest": { + "type": "object", + "required": [ + "requestInfo", + "optimInfo" + ], + "properties": { + "requestInfo": { + "$ref": "#/definitions/RequestInfo" + }, + "optimInfo": { + "$ref": "#/definitions/OptimInfo" + } + } + }, + "RequestInfo": { + "type": "object", + "required": [ + "transactionId", + "requestID", + "sourceId" + ], + "properties": { + "transactionId": { + "type": "string", + "format": "uuid", + "description": "unique ID to track an ONAP transaction", + "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "requestID": { + "type": "string", + "format": "uuid", + "description": "A unique ID to track multiple requests associated with a transaction", + "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "callbackUrl": { + "type": "string", + "format": "url", + "description": "The end point of a callback service where recommendations are posted.", + "example": "myDomain.com/myCallback" + }, + "sourceId": { + "type": "string", + "description": "The unique ID of a client making an optimization call.", + "example": "son-handler" + }, + "timeout": { + "type": "integer", + "description": "A tolerance window (in second) for expecting solutions", + "example": 5 + } + } + }, + "OptimInfo": { + "type": "object", + "properties": { + "modelId": { + "type": "string", + "description": "ModelId from the database, if its not populated, assume that solverModel will be populated", + "example": "pci_model1" + }, + "solver": { + "type": "string", + "description": "type of solver (mzn, py, etc.)", + "example": "mzn" + }, + "solverArgs": { + "type": "object", + "description": "Arguments for solver", + "additionalProperties": { + "type": "object" + }, + "example": { + "solver": "cbc", + "timeout": 5 + } + }, + "modelContent": { + "type": "string", + "description": "a large blob string containing the model (which is not that problematic since models are fairly small)." + }, + "optData": { + "$ref": "#/definitions/DataInfo" + } + } + }, + "DataInfo": { + "type": "object", + "description": "Data Payload, input data for the solver, either text or json", + "properties": { + "text": { + "type": "string", + "description": "Solver data as a string", + "example": "flour = 8000; \r\nbanana = 11;\r\n " + }, + "json": { + "type": "object", + "description": "Solver data as a json", + "additionalProperties": { + "type": "object" + }, + "example": { + "flour": 8000, + "banana": 11 + } + } + } + }, + "OptimModelRequest": { + "type": "object", + "required": [ + "requestInfo", + "modelInfo" + ], + "properties": { + "requestInfo": { + "$ref": "#/definitions/ModelRequestInfo" + }, + "modelInfo": { + "$ref": "#/definitions/OptimModelInfo" + } + } + }, + "ModelRequestInfo": { + "type": "object", + "required": [ + "transactionId", + "requestID", + "sourceId" + ], + "properties": { + "transactionId": { + "type": "string", + "format": "uuid", + "description": "unique ID to track an ONAP transaction", + "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "requestID": { + "type": "string", + "format": "uuid", + "description": "A unique ID to track multiple requests associated with a transaction", + "example": "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "sourceId": { + "type": "string", + "description": "The unique ID of a client making an optimization call.", + "example": "optf-osdf" + } + } + }, + "OptimModelInfo": { + "type": "object", + "required": [ + "modelId", + "solver", + "description", + "modelContent" + ], + "properties": { + "modelId": { + "type": "string", + "description": "ModelId from the database", + "example": "pci_anr_model1" + }, + "solver": { + "type": "string", + "description": "type of solver (mzn, py, etc.)", + "example": "mzn" + }, + "description": { + "type": "string", + "description": "Description of the model", + "example": "mzn model to optimize pci/anr models" + }, + "modelContent": { + "type": "string", + "description": "a large blob string containing the model (which is not that problematic since models are fairly small).", + "example": "mzn content" + } + } + }, + "ArrayOfOptimModelResponse": { + "type": "array", + "items": { + "$ref": "#/definitions/OptimModelResponse" + } + }, + "OptimModelResponse": { + "type": "object", + "required": [ + "modelId", + "solver", + "modelContent" + ], + "properties": { + "modelId": { + "type": "string", + "description": "ModelId from the database", + "example": "pci_anr_model1" + }, + "solver": { + "type": "string", + "description": "type of solver (mzn, py, etc.)", + "example": "mzn" + }, + "description": { + "type": "string", + "description": "Description of the model", + "example": "mzn model to optimize pci/anr models" + }, + "modelContent": { + "type": "string", + "description": "a large blob string containing the model (which is not that problematic since models are fairly small).", + "example": "mzn content" + }, + "statusMessage": { + "type": "string", + "description": "status message.", + "example": "mzn content" + } + } + }, + "DeleteModelResponse": { + "type": "object", + "required": [ + "statusMessage" + ], + "properties": { + "statusMessage": { + "type": "string", + "description": "status message.", + "example": "model data for modelId pci_anr_model1 deleted" + } + } + } + }, + "schemes": [ + "https" + ], + "host": "virtserver.swaggerhub.com", + "basePath": "/api/oof/" +} @@ -33,6 +33,7 @@ from apps.nst.optimizers.nst_select_processor import process_nst_selection from apps.pci.optimizers.pci_opt_processor import process_pci_optimation from apps.placement.models.api.placementRequest import PlacementAPI from apps.placement.optimizers.conductor.remote_opt_processor import process_placement_opt +from apps.route.optimizers.inter_domain_route_opt import InterDomainRouteOpt from apps.route.optimizers.simple_route_opt import RouteOpt from apps.slice_selection.models.api.nsi_selection_request import NSISelectionAPI from apps.slice_selection.optimizers.conductor.remote_opt_processor import process_nsi_selection_opt @@ -104,6 +105,16 @@ def do_route_calc(): response = RouteOpt().get_route(request_json, osdf_config) return response +@app.route("/api/oof/mdons/route/v1", methods=["POST"]) +def do_mdons_route_calc(): + """ + Perform the inter domain route calculation + """ + request_json = request.get_json() + audit_log.info("Inter Domain Calculation Route request received!") + response = InterDomainRouteOpt().get_route(request_json, osdf_config) + return response + @app.route("/api/oof/v1/selection/nst", methods=["POST"]) def do_nst_selection(): request_json = request.get_json() diff --git a/test/functest/simulators/simulated-config/osdf_config.yaml b/test/functest/simulators/simulated-config/osdf_config.yaml index 414d7c7..d4d20c9 100755 --- a/test/functest/simulators/simulated-config/osdf_config.yaml +++ b/test/functest/simulators/simulated-config/osdf_config.yaml @@ -73,4 +73,8 @@ pciHMSPassword: "" # pcihandler password for call back. aaiUrl: "https://api.url:30233" aaiGetLinksUrl: "/aai/v16/network/logical-links" +aaiGetControllersUrl: /aai/v19/external-system/esr-thirdparty-sdnc-list +controllerQueryUrl: /aai/v19/query?format=resource +aaiGetInterDomainLinksUrl: /aai/v19/network/logical-links?link-type=inter-domain&operational-status=up + diff --git a/test/inter_domain_route_opt/bandwidth_attributes.json b/test/inter_domain_route_opt/bandwidth_attributes.json new file mode 100644 index 0000000..0de7e51 --- /dev/null +++ b/test/inter_domain_route_opt/bandwidth_attributes.json @@ -0,0 +1,176 @@ +{ + "int-1-bw":{ + "interface-name":"int1", + "bandwidth-attributes":{ + "bandwidth-attribute":[ + { + "bwa-id":"bw6", + "resource-version":"1596387588545", + "available-bandwidth-map":{ + "available-bandwidth":[ + { + "ab-id":"ab226", + "odu-type":"ODU2", + "number":1, + "resource-version":"1596387588545" + }, + { + "ab-id":"ab112", + "odu-type":"ODU4", + "number":8, + "resource-version":"1596387588545" + } + ] + } + } + ] + }, + "resource-version":"1596387588545", + "in-maint":false + }, + "int-3-bw":{ + "interface-name":"int3", + "bandwidth-attributes":{ + "bandwidth-attribute":[ + { + "bwa-id":"bw6", + "resource-version":"1596387588545", + "available-bandwidth-map":{ + "available-bandwidth":[ + { + "ab-id":"ab226", + "odu-type":"ODU2", + "number":1, + "resource-version":"1596387588545" + }, + { + "ab-id":"ab112", + "odu-type":"ODU4", + "number":8, + "resource-version":"1596387588545" + } + ] + } + } + ] + }, + "resource-version":"1596387588545", + "in-maint":false + }, + "int-4-bw":{ + "interface-name":"int4", + "bandwidth-attributes":{ + "bandwidth-attribute":[ + { + "bwa-id":"bw6", + "resource-version":"1596387588545", + "available-bandwidth-map":{ + "available-bandwidth":[ + { + "ab-id":"ab226", + "odu-type":"ODU2", + "number":1, + "resource-version":"1596387588545" + }, + { + "ab-id":"ab112", + "odu-type":"ODU4", + "number":8, + "resource-version":"1596387588545" + } + ] + } + } + ] + }, + "resource-version":"1596387588545", + "in-maint":false + }, + "int-5-bw":{ + "interface-name":"int5", + "bandwidth-attributes":{ + "bandwidth-attribute":[ + { + "bwa-id":"bw6", + "resource-version":"1596387588545", + "available-bandwidth-map":{ + "available-bandwidth":[ + { + "ab-id":"ab226", + "odu-type":"ODU2", + "number":1, + "resource-version":"1596387588545" + }, + { + "ab-id":"ab112", + "odu-type":"ODU4", + "number":8, + "resource-version":"1596387588545" + } + ] + } + } + ] + }, + "resource-version":"1596387588545", + "in-maint":false + }, + "int-6-bw":{ + "interface-name":"int6", + "bandwidth-attributes":{ + "bandwidth-attribute":[ + { + "bwa-id":"bw6", + "resource-version":"1596387588545", + "available-bandwidth-map":{ + "available-bandwidth":[ + { + "ab-id":"ab226", + "odu-type":"ODU2", + "number":1, + "resource-version":"1596387588545" + }, + { + "ab-id":"ab112", + "odu-type":"ODU4", + "number":8, + "resource-version":"1596387588545" + } + ] + } + } + ] + }, + "resource-version":"1596387588545", + "in-maint":false + }, + "int-7-bw":{ + "interface-name":"int7", + "bandwidth-attributes":{ + "bandwidth-attribute":[ + { + "bwa-id":"bw6", + "resource-version":"1596387588545", + "available-bandwidth-map":{ + "available-bandwidth":[ + { + "ab-id":"ab226", + "odu-type":"ODU2", + "number":1, + "resource-version":"1596387588545" + }, + { + "ab-id":"ab112", + "odu-type":"ODU4", + "number":8, + "resource-version":"1596387588545" + } + ] + } + } + ] + }, + "resource-version":"1596387588545", + "in-maint":false + } +} diff --git a/test/inter_domain_route_opt/controllers_for_interfaces.json b/test/inter_domain_route_opt/controllers_for_interfaces.json new file mode 100644 index 0000000..3de47d1 --- /dev/null +++ b/test/inter_domain_route_opt/controllers_for_interfaces.json @@ -0,0 +1,62 @@ +{ + "int-1-cont":{ + "results":[ + { + "esr-thirdparty-sdnc":{ + "thirdparty-sdnc-id":"Controller1", + "resource-version":"1593421890494" + } + } + ] + }, + "int-3-cont":{ + "results":[ + { + "esr-thirdparty-sdnc":{ + "thirdparty-sdnc-id":"Controller2", + "resource-version":"1593421890494" + } + } + ] + }, + "int-4-cont":{ + "results":[ + { + "esr-thirdparty-sdnc":{ + "thirdparty-sdnc-id":"Controller2", + "resource-version":"1593421890494" + } + } + ] + }, + "int-5-cont":{ + "results":[ + { + "esr-thirdparty-sdnc":{ + "thirdparty-sdnc-id":"Controller3", + "resource-version":"1593421890494" + } + } + ] + }, + "int-6-cont":{ + "results":[ + { + "esr-thirdparty-sdnc":{ + "thirdparty-sdnc-id":"Controller3", + "resource-version":"1593421890494" + } + } + ] + }, + "int-7-cont":{ + "results":[ + { + "esr-thirdparty-sdnc":{ + "thirdparty-sdnc-id":"Controller4", + "resource-version":"1593421890494" + } + } + ] + } +} diff --git a/test/inter_domain_route_opt/controllers_list.json b/test/inter_domain_route_opt/controllers_list.json new file mode 100644 index 0000000..158f530 --- /dev/null +++ b/test/inter_domain_route_opt/controllers_list.json @@ -0,0 +1,16 @@ +{ + "esr-thirdparty-sdnc":[ + { + "thirdparty-sdnc-id":"Controller1" + }, + { + "thirdparty-sdnc-id":"Controller2" + }, + { + "thirdparty-sdnc-id":"Controller3" + }, + { + "thirdparty-sdnc-id":"Controller4" + } + ] +} diff --git a/test/inter_domain_route_opt/get_links.json b/test/inter_domain_route_opt/get_links.json new file mode 100644 index 0000000..0e70523 --- /dev/null +++ b/test/inter_domain_route_opt/get_links.json @@ -0,0 +1,157 @@ +{ + "logical-link":[ + { + "link-name":"link1", + "in-maint":false, + "link-type":"inter-domain", + "resource-version":"1588952379221", + "operational-status":"up", + "relationship-list":{ + "relationship":[ + { + "related-to":"p-interface", + "relationship-label":"tosca.relationships.network.LinksTo", + "related-link":"/aai/v19/network/pnfs/pnf/pnf1/p-interfaces/p-interface/int1", + "relationship-data":[ + { + "relationship-key":"pnf.pnf-name", + "relationship-value":"pnf1" + }, + { + "relationship-key":"p-interface.interface-name", + "relationship-value":"int1" + } + ], + "related-to-property":[ + { + "property-key":"p-interface.prov-status" + } + ] + }, + { + "related-to":"p-interface", + "relationship-label":"tosca.relationships.network.LinksTo", + "related-link":"/aai/v19/network/pnfs/pnf/pnf2/p-interfaces/p-interface/int3", + "relationship-data":[ + { + "relationship-key":"pnf.pnf-name", + "relationship-value":"pnf2" + }, + { + "relationship-key":"p-interface.interface-name", + "relationship-value":"int3" + } + ], + "related-to-property":[ + { + "property-key":"p-interface.prov-status" + } + ] + } + ] + } + }, + { + "link-name":"link2", + "in-maint":false, + "link-type":"inter-domain", + "resource-version":"1588952379221", + "operational-status":"up", + "relationship-list":{ + "relationship":[ + { + "related-to":"p-interface", + "relationship-label":"tosca.relationships.network.LinksTo", + "related-link":"/aai/v19/network/pnfs/pnf/pnf2/p-interfaces/p-interface/int4", + "relationship-data":[ + { + "relationship-key":"pnf.pnf-name", + "relationship-value":"pnf2" + }, + { + "relationship-key":"p-interface.interface-name", + "relationship-value":"int4" + } + ], + "related-to-property":[ + { + "property-key":"p-interface.prov-status" + } + ] + }, + { + "related-to":"p-interface", + "relationship-label":"tosca.relationships.network.LinksTo", + "related-link":"/aai/v19/network/pnfs/pnf/pnf3/p-interfaces/p-interface/int5", + "relationship-data":[ + { + "relationship-key":"pnf.pnf-name", + "relationship-value":"pnf3" + }, + { + "relationship-key":"p-interface.interface-name", + "relationship-value":"int5" + } + ], + "related-to-property":[ + { + "property-key":"p-interface.prov-status" + } + ] + } + ] + } + }, + { + "link-name":"link3", + "in-maint":false, + "link-type":"inter-domain", + "resource-version":"1588952379221", + "operational-status":"up", + "relationship-list":{ + "relationship":[ + { + "related-to":"p-interface", + "relationship-label":"tosca.relationships.network.LinksTo", + "related-link":"/aai/v19/network/pnfs/pnf/pnf3/p-interfaces/p-interface/int6", + "relationship-data":[ + { + "relationship-key":"pnf.pnf-name", + "relationship-value":"pnf3" + }, + { + "relationship-key":"p-interface.interface-name", + "relationship-value":"int6" + } + ], + "related-to-property":[ + { + "property-key":"p-interface.prov-status" + } + ] + }, + { + "related-to":"p-interface", + "relationship-label":"tosca.relationships.network.LinksTo", + "related-link":"/aai/v19/network/pnfs/pnf/pnf4/p-interfaces/p-interface/int7", + "relationship-data":[ + { + "relationship-key":"pnf.pnf-name", + "relationship-value":"pnf4" + }, + { + "relationship-key":"p-interface.interface-name", + "relationship-value":"int7" + } + ], + "related-to-property":[ + { + "property-key":"p-interface.prov-status" + } + ] + } + ] + } + } + ] +} diff --git a/test/inter_domain_route_opt/request.json b/test/inter_domain_route_opt/request.json new file mode 100644 index 0000000..041a32f --- /dev/null +++ b/test/inter_domain_route_opt/request.json @@ -0,0 +1,30 @@ +{ + "requestInfo":{ + "transactionId":"123456", + "requestId":"789456", + "callbackUrl":"", + "callbackHeader": "", + "sourceId":"SDNC", + "requestType":"create", + "numSolutions":1, + "optimizers":[ + "route" + ], + "timeout":600 + }, + "routeInfo":{ + "routeRequest":{ + "srcDetails":{ + "interfaceId":"int19", + "nodeId":"pnf1", + "controllerId":"Controller1" + }, + "dstDetails":{ + "interfaceId":"int20", + "nodeId":"pnf4", + "controllerId":"Controller3" + }, + "serviceRate":"ODU2" + } + } +} diff --git a/test/test_inter_domain_route_opt.py b/test/test_inter_domain_route_opt.py new file mode 100644 index 0000000..3d18abc --- /dev/null +++ b/test/test_inter_domain_route_opt.py @@ -0,0 +1,151 @@ +# ------------------------------------------------------------------------- +# 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 unittest + +from unittest.mock import patch +from apps.route.optimizers.inter_domain_route_opt import InterDomainRouteOpt +import osdf.config.loader as config_loader +from osdf.utils.interfaces import json_from_file +from osdf.utils.programming_utils import DotDict + +count = 1 + +def mocked_requests_get(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + main_dir = "" + response_data_file = main_dir + "test/inter_domain_route_opt/get_links.json" + bandwidth_attributes = main_dir + "test/inter_domain_route_opt/bandwidth_attributes.json" + bandwidth_attribute_values = json_from_file(bandwidth_attributes) + + controllers_list = main_dir + "test/inter_domain_route_opt/controllers_list.json" + + if args[0] == 'https://api.url:30233/aai/v19/network/logical-links?link-type=inter-domain&operational-status=up': + return MockResponse(json_from_file(response_data_file), 200) + elif args[0] == 'https://api.url:30233/aai/v19/network/pnfs/pnf/pnf1/p-interfaces/p-interface/int1?depth=all': + return MockResponse(bandwidth_attribute_values["int-1-bw"], 200) + elif args[0] == 'https://api.url:30233/aai/v19/network/pnfs/pnf/pnf2/p-interfaces/p-interface/int3?depth=all': + return MockResponse(bandwidth_attribute_values["int-3-bw"], 200) + elif args[0] == 'https://api.url:30233/aai/v19/network/pnfs/pnf/pnf2/p-interfaces/p-interface/int4?depth=all': + return MockResponse(bandwidth_attribute_values["int-4-bw"], 200) + elif args[0] == 'https://api.url:30233/aai/v19/network/pnfs/pnf/pnf3/p-interfaces/p-interface/int5?depth=all': + return MockResponse(bandwidth_attribute_values["int-5-bw"], 200) + elif args[0] == 'https://api.url:30233/aai/v19/network/pnfs/pnf/pnf3/p-interfaces/p-interface/int6?depth=all': + return MockResponse(bandwidth_attribute_values["int-6-bw"], 200) + elif args[0] == 'https://api.url:30233/aai/v19/network/pnfs/pnf/pnf4/p-interfaces/p-interface/int7?depth=all': + return MockResponse(bandwidth_attribute_values["int-7-bw"], 200) + elif args[0] == 'https://api.url:30233/aai/v19/external-system/esr-thirdparty-sdnc-list': + return MockResponse(json_from_file(controllers_list), 200) + return MockResponse(None, 404) + + +def mocked_requests_put(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + main_dir = "" + controllers_for_interfaces = main_dir + "test/inter_domain_route_opt/controllers_for_interfaces.json" + controllers_for_interfaces_values = json_from_file(controllers_for_interfaces) + + global count + + if count == 1: + count += 1 + return MockResponse(controllers_for_interfaces_values["int-1-cont"], 200) + elif count == 2: + count += 1 + return MockResponse(controllers_for_interfaces_values["int-3-cont"], 200) + elif count == 3: + count += 1 + return MockResponse(controllers_for_interfaces_values["int-4-cont"], 200) + elif count == 4: + count += 1 + return MockResponse(controllers_for_interfaces_values["int-5-cont"], 200) + elif count == 5: + count += 1 + return MockResponse(controllers_for_interfaces_values["int-6-cont"], 200) + elif count == 6: + count += 1 + return MockResponse(controllers_for_interfaces_values["int-7-cont"], 200) + + return MockResponse(None, 404) + + + +class TestInterDomainRouteOpt(unittest.TestCase): + @patch('apps.route.optimizers.inter_domain_route_opt.requests.get', side_effect=mocked_requests_get) + @patch('apps.route.optimizers.inter_domain_route_opt.requests.put', side_effect=mocked_requests_put) + @patch('apps.route.optimizers.simple_route_opt.pymzn.minizinc') + def test_process_get_route(self, mock_solve , mock_put, mock_get): + main_dir = "" + mock_solve.return_value = [{'x': [1, 1, 0, 0, 0, 0]}] + self.config_spec = { + "deployment": "test/functest/simulators/simulated-config/osdf_config.yaml", + "core": "test/functest/simulators/simulated-config/common_config.yaml" + } + self.osdf_config = DotDict(config_loader.all_configs(**self.config_spec)) + parameter_data_file = main_dir + "test/inter_domain_route_opt/request.json" + request_json = json_from_file(parameter_data_file) + routopt = InterDomainRouteOpt() + actual_response = routopt.get_route(request_json,self.osdf_config) + mock_response = { + "requestId":"789456", + "transactionId":"123456", + "statusMessage":"SUCCESS", + "requestStatus":"accepted", + "solutions":{ + "routeInfo":{ + "serviceRoute":[ + { + "srcInterfaceId":"int19", + "dstInterfaceId":"int1", + "controllerId":"Controller1" + }, + { + "srcInterfaceId":"int3", + "dstInterfaceId":"int4", + "controllerId":"Controller2" + }, + { + "srcInterfaceId":"int5", + "dstInterfaceId":"int20", + "controllerId":"Controller3" + } + ], + "linkList":[ + "link1", + "link2" + ] + } + } + } + self.assertEqual(mock_response, actual_response) + + +if __name__ == '__main__': + unittest.main() +
\ No newline at end of file @@ -1,7 +1,7 @@ [tox] skipsdist=True -envlist = py3, pylint +envlist = py3, pylint, flake8diff [testenv] distribute = False @@ -29,3 +29,15 @@ commands = bash -c "pylint --reports=y osdf apps runtime| tee pylint.out" [testenv:py3] basepython=python3.6 + +[testenv:flake8diff] +whitelist_externals=bash +deps = hacking>=2.0.0 +commands = + bash -c "files=$(git diff HEAD^ HEAD --diff-filter=d --name-only | grep -E '(^apps\/|osdf\/|runtime\/)'| grep -E '*\.py$'); if [[ -z $files ]]; then exit 0; else flake8 $files; fi" + +[flake8] +select = E,H,W,F +max-line-length = 119 +ignore = +per-file-ignores= |