diff options
27 files changed, 1154 insertions, 242 deletions
diff --git a/config/osdf_config.yaml b/config/osdf_config.yaml index 4ef11f8..636b6ad 100755 --- a/config/osdf_config.yaml +++ b/config/osdf_config.yaml @@ -1,3 +1,15 @@ +placementVersioningEnabled: False + +# Placement API latest version numbers to be set in HTTP header +placementMajorVersion: "1" +placementMinorVersion: "0" +placementPatchVersion: "0" + +# Placement API default version numbers to be set in HTTP header +placementDefaultMajorVersion: "1" +placementDefaultMinorVersion: "0" +placementDefaultPatchVersion: "0" + # Credentials for SO soUsername: "" # SO username for call back. soPassword: "" # SO password for call back. @@ -8,6 +20,8 @@ conductorUsername: admin1 conductorPassword: plan.15 conductorPingWaitTime: 60 # seconds to wait before calling the conductor retry URL conductorMaxRetries: 30 # if we don't get something in 30 minutes, give up +# versions to be set in HTTP header +conductorMinorVersion: 0 # Policy Platform -- requires ClientAuth, Authorization, and Environment policyPlatformUrl: http://policy.api.simpledemo.onap.org:8081/pdp/api/getConfig # Policy Dev platform URL @@ -56,3 +70,14 @@ aaf_sms_url: https://aaf-sms.onap:10443 aaf_sms_timeout: 30 secret_domain: osdf #Replace with the UUID aaf_ca_certs: ssl_certs/aaf_root_ca.cer + +# config db api +configDbUrl: http://config.db.url:8080 +configDbUserName: osdf +configDbPassword: passwd +configDbGetCellListUrl: 'SDNCConfigDBAPI/getCellList' +configDbGetNbrListUrl: 'SDNCConfigDBAPI/getNbrList' + +# Credentials for PCIHandler +pciHMSUsername: "" # pcihandler username for call back. +pciHMSPassword: "" # pcihandler password for call back. diff --git a/osdf/config/base.py b/osdf/config/base.py index 29376a5..fbe9315 100644 --- a/osdf/config/base.py +++ b/osdf/config/base.py @@ -18,14 +18,14 @@ import os -import osdf.config.loader as config_loader import osdf.config.credentials as creds +import osdf.config.loader as config_loader from osdf.utils.programming_utils import DotDict config_spec = { "deployment": os.environ.get("OSDF_CONFIG_FILE", "config/osdf_config.yaml"), "core": "config/common_config.yaml" - } +} osdf_config = DotDict(config_loader.all_configs(**config_spec)) @@ -33,4 +33,4 @@ http_basic_auth_credentials = creds.load_credentials(osdf_config) dmaap_creds = creds.dmaap_creds() -creds_prefixes = {"so": "so", "cm": "cmPortal"} +creds_prefixes = {"so": "so", "cm": "cmPortal", "pcih": "pciHMS"} diff --git a/osdf/models/api/pciOptimizationRequest.py b/osdf/models/api/pciOptimizationRequest.py new file mode 100644 index 0000000..47b4eba --- /dev/null +++ b/osdf/models/api/pciOptimizationRequest.py @@ -0,0 +1,48 @@ +# ------------------------------------------------------------------------- +# 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 .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 CellInfo(OSDFModel): + """Information specific to CellInfo """ + networkId = StringType(required=True) + cellIdList = ListType(StringType(required=True)) + trigger = StringType() + + +class PCIOptimizationAPI(OSDFModel): + """Request for PCI optimization """ + requestInfo = ModelType(RequestInfo, required=True) + cellInfo = ModelType(CellInfo, required=True) diff --git a/osdf/models/api/pciOptimizationResponse.py b/osdf/models/api/pciOptimizationResponse.py new file mode 100644 index 0000000..876c380 --- /dev/null +++ b/osdf/models/api/pciOptimizationResponse.py @@ -0,0 +1,40 @@ +# ------------------------------------------------------------------------- +# 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 .common import OSDFModel + + +class PCISolution(OSDFModel): + cellId = StringType(required=True) + pci = IntType(required=True) + + +class Solution(OSDFModel): + networkId = StringType(required=True) + pciSolutions = ListType(ListType(ModelType(PCISolution), 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/osdf/operation/responses.py b/osdf/operation/responses.py index 84bb2cc..a3623ba 100644..100755 --- a/osdf/operation/responses.py +++ b/osdf/operation/responses.py @@ -1,43 +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 flask import Response - -from osdf import ACCEPTED_MESSAGE_TEMPLATE - - -def osdf_response_for_request_accept(request_id="", transaction_id="", request_status="", status_message="", - response_code=202, as_http=True): - """Helper method to create a response object for request acceptance, so that the object can be sent to a client - :param request_id: request ID provided by the caller - :param transaction_id: transaction ID provided by the caller - :param request_status: the status of a request - :param status_message: details on the status of a request - :param response_code: the HTTP status code to send -- default is 202 (accepted) - :param as_http: whether to send response as HTTP response object or as a string - :return: if as_http is True, return a HTTP Response object. Otherwise, return json-encoded-message - """ - response_message = ACCEPTED_MESSAGE_TEMPLATE.render(request_id=request_id, transaction_id=transaction_id, - request_status=request_status, status_message=status_message) - if not as_http: - return response_message - - response = Response(response_message, content_type='application/json; charset=utf-8') - response.headers.add('content-length', len(response_message)) - response.status_code = response_code - return response +# -------------------------------------------------------------------------
+# 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 flask import Response
+
+from osdf import ACCEPTED_MESSAGE_TEMPLATE
+from osdf.logging.osdf_logging import debug_log
+
+def osdf_response_for_request_accept(request_id="", transaction_id="", request_status="", status_message="",
+ version_info = {
+ 'placementVersioningEnabled': False,
+ 'placementMajorVersion': '1',
+ 'placementMinorVersion': '0',
+ 'placementPatchVersion': '0'
+ },
+ response_code=202, as_http=True):
+ """Helper method to create a response object for request acceptance, so that the object can be sent to a client
+ :param request_id: request ID provided by the caller
+ :param transaction_id: transaction ID provided by the caller
+ :param request_status: the status of a request
+ :param status_message: details on the status of a request
+ :param response_code: the HTTP status code to send -- default is 202 (accepted)
+ :param as_http: whether to send response as HTTP response object or as a string
+ :return: if as_http is True, return a HTTP Response object. Otherwise, return json-encoded-message
+ """
+ response_message = ACCEPTED_MESSAGE_TEMPLATE.render(request_id=request_id, transaction_id=transaction_id,
+ request_status=request_status, status_message=status_message)
+ if not as_http:
+ return response_message
+
+ response = Response(response_message, content_type='application/json; charset=utf-8')
+ response.headers.add('content-length', len(response_message))
+
+ placement_ver_enabled = version_info['placementVersioningEnabled']
+
+ if placement_ver_enabled:
+ placement_minor_version = version_info['placementMinorVersion']
+ placement_patch_version = version_info['placementPatchVersion']
+ placement_major_version = version_info['placementMajorVersion']
+ x_latest_version = placement_major_version+'.'+placement_minor_version+'.'+placement_patch_version
+ response.headers.add('X-MinorVersion', placement_minor_version)
+ response.headers.add('X-PatchVersion', placement_patch_version)
+ response.headers.add('X-LatestVersion', x_latest_version)
+
+ debug_log.debug("Versions set in HTTP header for synchronous response: X-MinorVersion: {} X-PatchVersion: {} X-LatestVersion: {}"
+ .format(placement_minor_version, placement_patch_version, x_latest_version))
+
+ response.status_code = response_code
+ return response
diff --git a/osdf/optimizers/pciopt/__init__.py b/osdf/optimizers/pciopt/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/osdf/optimizers/pciopt/__init__.py diff --git a/osdf/optimizers/pciopt/configdb.py b/osdf/optimizers/pciopt/configdb.py new file mode 100644 index 0000000..bebc5c0 --- /dev/null +++ b/osdf/optimizers/pciopt/configdb.py @@ -0,0 +1,65 @@ +# ------------------------------------------------------------------------- +# 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 datetime import datetime as dt + +from osdf.logging.osdf_logging import debug_log +from osdf.utils.interfaces import RestClient + + +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 = {} + config = osdf_config.deployment + local_config = osdf_config.core + uid, passwd = config['configDbUserName'], config['configDbPassword'] + req_id = req_object['requestInfo']['requestId'] + transaction_id = req_object['requestInfo']['transactionId'] + headers = dict(transaction_id=transaction_id) + + network_id = req_object['cellInfo']['networkId'] + + cell_list_response['network_id'] = network_id + + rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers) + + cell_list_url = '{}/{}?networkId={}'.format(config['configDbUrl'], config['configDbGetCellListUrl'], network_id) + + cell_list_resp = rc.request(raw_response=True, url=cell_list_url) + cell_resp = cell_list_resp.json() + ts = dt.strftime(dt.now(), '%Y-%m-%dT%H:%M:%S%z') + + cell_list = [] + count = 0 + for cell_id in cell_resp: + cell_info = {'cell_id': cell_id, 'id': count} + nbr_list_url = '{}/{}?cellId={}&ts={}'.format(config['configDbUrl'], config['configDbGetNbrListUrl'], cell_id, + ts) + nbr_list_raw = rc.request(url=nbr_list_url, raw_response=True) + cell_info['nbr_list'] = nbr_list_raw.json() + cell_list.append(cell_info) + count += 1 + + cell_list_response['cell_list'] = cell_list + return cell_resp, cell_list_response diff --git a/osdf/optimizers/pciopt/pci_opt_processor.py b/osdf/optimizers/pciopt/pci_opt_processor.py new file mode 100644 index 0000000..030128e --- /dev/null +++ b/osdf/optimizers/pciopt/pci_opt_processor.py @@ -0,0 +1,84 @@ +# ------------------------------------------------------------------------- +# 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 requests import RequestException + +from osdf.logging.osdf_logging import metrics_log, MH, error_log +from osdf.models.api.pciOptimizationResponse import PCIOptimizationResponse, Solution, PCISolution +from osdf.operation.error_handling import build_json_error_body +from osdf.utils.interfaces import get_rest_client +from .configdb import request as config_request +from .solver.optimizer import pci_optimize as optimize +from .solver.pci_utils import get_cell_id, get_pci_value + +""" +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: + rc = get_rest_client(request_json, service="pcih") + req_id = request_json["requestInfo"]["requestId"] + transaction_id = request_json['requestInfo']['transactionId'] + cell_info_list, network_cell_info = config_request(request_json, osdf_config, flat_policies) + + pci_response = PCIOptimizationResponse() + pci_response.transactionId = transaction_id + pci_response.requestId = req_id + pci_response.requestStatus = 'success' + pci_response.solutions = Solution() + pci_response.solutions.networkId = request_json['cellInfo']['networkId'] + pci_response.solutions.pciSolutions = [] + + for cell in request_json['cellInfo']['cellIdList']: + pci_solution = optimize(cell['cellId'], network_cell_info, cell_info_list) + error_log.error(pci_solution) + sol = pci_solution[0]['pci'] + for k, v in sol.items(): + response = PCISolution() + response.cellId = get_cell_id(network_cell_info, k) + response.pci = get_pci_value(network_cell_info, v) + pci_response.solutions.pciSolutions.append(response) + + 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: + 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, 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())) diff --git a/osdf/optimizers/pciopt/solver/__init__.py b/osdf/optimizers/pciopt/solver/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/osdf/optimizers/pciopt/solver/__init__.py diff --git a/osdf/optimizers/pciopt/solver/min_confusion.mzn b/osdf/optimizers/pciopt/solver/min_confusion.mzn new file mode 100644 index 0000000..803f914 --- /dev/null +++ b/osdf/optimizers/pciopt/solver/min_confusion.mzn @@ -0,0 +1,95 @@ +% ------------------------------------------------------------------------- +% 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 **CONFLICTS**, i.e., +% to guarantee that nodes i and j have different PCIs. +int: NUM_CONFLICT_EDGES; + +% Each line represents an edge between direct neighbors as defined before. +array[1..NUM_CONFLICT_EDGES, 1..2] of int: CONFLICT_EDGES; + +% 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_CONFUSION_EDGES; + +% Each line represents an edge between undirect neighbors as defined before. +array[1..NUM_CONFUSION_EDGES, 1..2] of int: CONFUSION_EDGES; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% 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 **CONFLICTS**. +constraint +forall(i in 1..NUM_CONFLICT_EDGES)( + pci[CONFLICT_EDGES[i, 1]] != pci[CONFLICT_EDGES[i, 2]] +); + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Objective function +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% Total number of confusions. +var int: total_confusions = + sum([bool2int(pci[CONFUSION_EDGES[i, 1]] == pci[CONFUSION_EDGES[i, 2]]) + | i in 1..NUM_CONFUSION_EDGES]); + +% 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(CONFUSION_EDGES[i, 1]) ++ "," ++ show(CONFUSION_EDGES[i, 2]) +| i in 1..NUM_CONFUSION_EDGES where + fix(pci[CONFUSION_EDGES[i, 1]] == pci[CONFUSION_EDGES[i, 2]]) +] diff --git a/osdf/optimizers/pciopt/solver/no_conflicts_no_confusion.mzn b/osdf/optimizers/pciopt/solver/no_conflicts_no_confusion.mzn new file mode 100644 index 0000000..19fabb9 --- /dev/null +++ b/osdf/optimizers/pciopt/solver/no_conflicts_no_confusion.mzn @@ -0,0 +1,86 @@ +% ------------------------------------------------------------------------- +% 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 **CONFLICTS**, i.e., +% to guarantee that nodes i and j have different PCIs. +int: NUM_CONFLICT_EDGES; + +% Each line represents an edge between direct neighbors as defined before. +array[1..NUM_CONFLICT_EDGES, 1..2] of int: CONFLICT_EDGES; + +% 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_CONFUSION_EDGES; + +% Each line represents an edge between undirect neighbors as defined before. +array[1..NUM_CONFUSION_EDGES, 1..2] of int: CONFUSION_EDGES; + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% 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 **CONFLICTS**. +constraint +forall(i in 1..NUM_CONFLICT_EDGES)( + pci[CONFLICT_EDGES[i, 1]] != pci[CONFLICT_EDGES[i, 2]] +); + +% Undirect neighbors must have different PCIs for avoid **CONFUSIONS**. +constraint +forall(i in 1..NUM_CONFUSION_EDGES)( + pci[CONFUSION_EDGES[i, 1]] != pci[CONFUSION_EDGES[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/osdf/optimizers/pciopt/solver/optimizer.py b/osdf/optimizers/pciopt/solver/optimizer.py new file mode 100644 index 0000000..e9fcb0d --- /dev/null +++ b/osdf/optimizers/pciopt/solver/optimizer.py @@ -0,0 +1,82 @@ +# ------------------------------------------------------------------------- +# 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 itertools + +import os +import pymzn + +from osdf.logging.osdf_logging import debug_log +from .pci_utils import get_id + +BASE_DIR = os.path.dirname(__file__) +MZN_FILE_NAME = os.path.join(BASE_DIR, 'no_conflicts_no_confusion.mzn') + + +def pci_optimize(cell_id, network_cell_info, cell_info_list): + debug_log.debug("Cell ID {} ".format(cell_id)) + dzn_data = {} + dzn_data['NUM_NODES'] = len(cell_info_list) + dzn_data['NUM_PCIS'] = len(cell_info_list) + + conflict_edges = get_conflict_edges(cell_id, network_cell_info) + + dzn_data['NUM_CONFLICT_EDGES'] = len(conflict_edges) + dzn_data['CONFLICT_EDGES'] = conflict_edges + + confusion_edges = get_confusion_edges(cell_id, network_cell_info) + + dzn_data['NUM_CONFUSION_EDGES'] = len(confusion_edges) + dzn_data['CONFUSION_EDGES'] = confusion_edges + + return solve(dzn_data) + +def solve(dzn_data): + return pymzn.minizinc(MZN_FILE_NAME, data=dzn_data) + + +def get_conflict_edges(cell_id, network_cell_info): + conflict_edges = [] + for cell in network_cell_info['cell_list']: + + if cell_id == cell['cell_id']: + add_to_conflict_edges(network_cell_info, cell, conflict_edges) + return conflict_edges + + +def add_to_conflict_edges(network_cell_info, cell, conflict_edges): + cell_id = cell['cell_id'] + for nbr in cell.get('nbr_list', []): + conflict_edges.append([get_id(network_cell_info, cell_id), get_id(network_cell_info, nbr['cellId'])]) + + + +def get_confusion_edges(cell_id, network_cell_info): + confusion_edges = [] + for cell in network_cell_info['cell_list']: + if cell_id == cell['cell_id']: + return add_to_confusion_edges(network_cell_info, cell) + return confusion_edges + + +def add_to_confusion_edges(network_cell_info, cell): + cell_id = cell['cell_id'] + nbr_list = [] + for nbr in cell.get('nbr_list', []): + nbr_list.append(get_id(network_cell_info, nbr['cellId'])) + return [list(elem) for elem in list(itertools.combinations(nbr_list, 2))] diff --git a/osdf/optimizers/pciopt/solver/pci_utils.py b/osdf/optimizers/pciopt/solver/pci_utils.py new file mode 100644 index 0000000..71b5dd2 --- /dev/null +++ b/osdf/optimizers/pciopt/solver/pci_utils.py @@ -0,0 +1,39 @@ +# ------------------------------------------------------------------------- +# 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 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['cellId']: + return j['pciValue'] + return None diff --git a/osdf/optimizers/placementopt/conductor/conductor.py b/osdf/optimizers/placementopt/conductor/conductor.py index f40ee95..29f0bbc 100644..100755 --- a/osdf/optimizers/placementopt/conductor/conductor.py +++ b/osdf/optimizers/placementopt/conductor/conductor.py @@ -1,194 +1,202 @@ -# ------------------------------------------------------------------------- -# 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. -# -# ------------------------------------------------------------------------- -# - -""" -This application generates conductor API calls using the information received from SO and Policy platform. -""" - -import json -import time - -from jinja2 import Template -from requests import RequestException - -from osdf.logging.osdf_logging import debug_log -from osdf.optimizers.placementopt.conductor.api_builder import conductor_api_builder -from osdf.utils.interfaces import RestClient -from osdf.operation.exceptions import BusinessException - - -def request(req_object, osdf_config, flat_policies): - """ - Process a placement 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 SNIRO application (core + deployment) - :param flat_policies: policies related to placement (fetched based on request) - :param prov_status: provStatus retrieved from Subscriber policy - :return: response from Conductor (accounting for redirects from Conductor service - """ - config = osdf_config.deployment - local_config = osdf_config.core - uid, passwd = config['conductorUsername'], config['conductorPassword'] - conductor_url = config['conductorUrl'] - req_id = req_object['requestInfo']['requestId'] - transaction_id = req_object['requestInfo']['transactionId'] - headers = dict(transaction_id=transaction_id) - - max_retries = config.get('conductorMaxRetries', 30) - ping_wait_time = config.get('conductorPingWaitTime', 60) - - rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers) - conductor_req_json_str = conductor_api_builder(req_object, flat_policies, local_config) - conductor_req_json = json.loads(conductor_req_json_str) - - debug_log.debug("Sending first Conductor request for request_id {}".format(req_id)) - resp, raw_resp = initial_request_to_conductor(rc, conductor_url, conductor_req_json) - # Very crude way of keeping track of time. - # We are not counting initial request time, first call back, or time for HTTP request - total_time, ctr = 0, 2 - client_timeout = req_object['requestInfo']['timeout'] - configured_timeout = max_retries * ping_wait_time - max_timeout = min(client_timeout, configured_timeout) - - while True: # keep requesting conductor till we get a result or we run out of time - if resp is not None: - if resp["plans"][0].get("status") in ["error"]: - raise RequestException(response=raw_resp, request=raw_resp.request) - - if resp["plans"][0].get("status") in ["done", "not found"]: - if resp["plans"][0].get("recommendations"): - return conductor_response_processor(resp, raw_resp, req_id) - else: # "solved" but no solutions found - return conductor_no_solution_processor(resp, raw_resp, req_id) - new_url = resp['plans'][0]['links'][0][0]['href'] # TODO: check why a list of lists - - if total_time >= max_timeout: - raise BusinessException("Conductor could not provide a solution within {} seconds," - "this transaction is timing out".format(max_timeout)) - time.sleep(ping_wait_time) - ctr += 1 - debug_log.debug("Attempt number {} url {}; prior status={}".format(ctr, new_url, resp['plans'][0]['status'])) - total_time += ping_wait_time - - try: - raw_resp = rc.request(new_url, raw_response=True) - resp = raw_resp.json() - except RequestException as e: - debug_log.debug("Conductor attempt {} for request_id {} has failed because {}".format(ctr, req_id, str(e))) - - -def initial_request_to_conductor(rc, conductor_url, conductor_req_json): - """First steps in the request-redirect chain in making a call to Conductor - :param rc: REST client object for calling conductor - :param conductor_url: conductor's base URL to submit a placement request - :param conductor_req_json: request json object to send to Conductor - :return: URL to check for follow up (similar to redirects); we keep checking these till we get a result/error - """ - debug_log.debug("Payload to Conductor: {}".format(json.dumps(conductor_req_json))) - raw_resp = rc.request(url=conductor_url, raw_response=True, method="POST", json=conductor_req_json) - resp = raw_resp.json() - if resp["status"] != "template": - raise RequestException(response=raw_resp, request=raw_resp.request) - time.sleep(10) # 10 seconds wait time to avoid being too quick! - plan_url = resp["links"][0][0]["href"] - debug_log.debug("Attempting to read the plan from the conductor provided url {}".format(plan_url)) - raw_resp = rc.request(raw_response=True, url=plan_url) # TODO: check why a list of lists for links - resp = raw_resp.json() - - if resp["plans"][0]["status"] in ["error"]: - raise RequestException(response=raw_resp, request=raw_resp.request) - return resp, raw_resp # now the caller of this will handle further follow-ups - - -def conductor_response_processor(conductor_response, raw_response, req_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"} - 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'] - transaction_id = raw_response.headers.get('transaction_id', "") - 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, raw_response, request_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=raw_response.headers.get('transaction_id', ""), - requestStatus="completed", statusMessage=status_message, json=json)) - - +# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+"""
+This application generates conductor API calls using the information received from SO and Policy platform.
+"""
+
+import json
+import time
+
+from jinja2 import Template
+from requests import RequestException
+
+from osdf.logging.osdf_logging import debug_log
+from osdf.optimizers.placementopt.conductor.api_builder import conductor_api_builder
+from osdf.utils.interfaces import RestClient
+from osdf.operation.exceptions import BusinessException
+
+
+def request(req_object, osdf_config, flat_policies):
+ """
+ Process a placement 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 SNIRO application (core + deployment)
+ :param flat_policies: policies related to placement (fetched based on request)
+ :param prov_status: provStatus retrieved from Subscriber policy
+ :return: response from Conductor (accounting for redirects from Conductor service
+ """
+ config = osdf_config.deployment
+ local_config = osdf_config.core
+ uid, passwd = config['conductorUsername'], config['conductorPassword']
+ conductor_url = config['conductorUrl']
+ req_id = req_object['requestInfo']['requestId']
+ transaction_id = req_object['requestInfo']['transactionId']
+ headers = dict(transaction_id=transaction_id)
+ placement_ver_enabled = config.get('placementVersioningEnabled', False)
+
+ if placement_ver_enabled:
+ cond_minor_version = config.get('conductorMinorVersion', None)
+ if cond_minor_version is not None:
+ x_minor_version = str(cond_minor_version)
+ headers.update({'X-MinorVersion': x_minor_version})
+ debug_log.debug("Versions set in HTTP header to conductor: X-MinorVersion: {} ".format(x_minor_version))
+
+ max_retries = config.get('conductorMaxRetries', 30)
+ ping_wait_time = config.get('conductorPingWaitTime', 60)
+
+ rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers)
+ conductor_req_json_str = conductor_api_builder(req_object, flat_policies, local_config)
+ conductor_req_json = json.loads(conductor_req_json_str)
+
+ debug_log.debug("Sending first Conductor request for request_id {}".format(req_id))
+ resp, raw_resp = initial_request_to_conductor(rc, conductor_url, conductor_req_json)
+ # Very crude way of keeping track of time.
+ # We are not counting initial request time, first call back, or time for HTTP request
+ total_time, ctr = 0, 2
+ client_timeout = req_object['requestInfo']['timeout']
+ configured_timeout = max_retries * ping_wait_time
+ max_timeout = min(client_timeout, configured_timeout)
+
+ while True: # keep requesting conductor till we get a result or we run out of time
+ if resp is not None:
+ if resp["plans"][0].get("status") in ["error"]:
+ raise RequestException(response=raw_resp, request=raw_resp.request)
+
+ if resp["plans"][0].get("status") in ["done", "not found"]:
+ if resp["plans"][0].get("recommendations"):
+ return conductor_response_processor(resp, raw_resp, req_id)
+ else: # "solved" but no solutions found
+ return conductor_no_solution_processor(resp, raw_resp, req_id)
+ new_url = resp['plans'][0]['links'][0][0]['href'] # TODO: check why a list of lists
+
+ if total_time >= max_timeout:
+ raise BusinessException("Conductor could not provide a solution within {} seconds,"
+ "this transaction is timing out".format(max_timeout))
+ time.sleep(ping_wait_time)
+ ctr += 1
+ debug_log.debug("Attempt number {} url {}; prior status={}".format(ctr, new_url, resp['plans'][0]['status']))
+ total_time += ping_wait_time
+
+ try:
+ raw_resp = rc.request(new_url, raw_response=True)
+ resp = raw_resp.json()
+ except RequestException as e:
+ debug_log.debug("Conductor attempt {} for request_id {} has failed because {}".format(ctr, req_id, str(e)))
+
+
+def initial_request_to_conductor(rc, conductor_url, conductor_req_json):
+ """First steps in the request-redirect chain in making a call to Conductor
+ :param rc: REST client object for calling conductor
+ :param conductor_url: conductor's base URL to submit a placement request
+ :param conductor_req_json: request json object to send to Conductor
+ :return: URL to check for follow up (similar to redirects); we keep checking these till we get a result/error
+ """
+ debug_log.debug("Payload to Conductor: {}".format(json.dumps(conductor_req_json)))
+ raw_resp = rc.request(url=conductor_url, raw_response=True, method="POST", json=conductor_req_json)
+ resp = raw_resp.json()
+ if resp["status"] != "template":
+ raise RequestException(response=raw_resp, request=raw_resp.request)
+ time.sleep(10) # 10 seconds wait time to avoid being too quick!
+ plan_url = resp["links"][0][0]["href"]
+ debug_log.debug("Attempting to read the plan from the conductor provided url {}".format(plan_url))
+ raw_resp = rc.request(raw_response=True, url=plan_url) # TODO: check why a list of lists for links
+ resp = raw_resp.json()
+
+ if resp["plans"][0]["status"] in ["error"]:
+ raise RequestException(response=raw_resp, request=raw_resp.request)
+ return resp, raw_resp # now the caller of this will handle further follow-ups
+
+
+def conductor_response_processor(conductor_response, raw_response, req_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"}
+ 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']
+ transaction_id = raw_response.headers.get('transaction_id', "")
+ 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, raw_response, request_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=raw_response.headers.get('transaction_id', ""),
+ requestStatus="completed", statusMessage=status_message, json=json))
+
+
diff --git a/osdf/utils/api_data_utils.py b/osdf/utils/api_data_utils.py new file mode 100755 index 0000000..a7a4c12 --- /dev/null +++ b/osdf/utils/api_data_utils.py @@ -0,0 +1,59 @@ +# ------------------------------------------------------------------------- +# 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 collections import defaultdict +from osdf.logging.osdf_logging import debug_log +from osdf.config.base import osdf_config + + +def retrieve_version_info(request, request_id): + version_info_dict = defaultdict(dict) + config = osdf_config.deployment + placement_ver_enabled = config.get('placementVersioningEnabled', False) + + if placement_ver_enabled: + placement_major_version = config.get('placementMajorVersion', None) + placement_minor_version = config.get('placementMinorVersion', None) + placement_patch_version = config.get('placementPatchVersion', None) + + http_header = request.headers.environ + http_x_minorversion = http_header.get("HTTP_X_MINORVERSION") + http_x_patchversion = http_header.get("HTTP_X_PATCHVERSION") + http_x_latestversion = http_header.get("HTTP_X_LATESTVERSION") + + debug_log.debug("Versions sent in HTTP header for request ID {} are: X-MinorVersion: {} X-PatchVersion: {} X-LatestVersion: {}" + .format(request_id, http_x_minorversion, http_x_patchversion, http_x_latestversion)) + debug_log.debug("latest versions specified in osdf config file are: placementMajorVersion: {} placementMinorVersion: {} placementPatchVersion: {}" + .format(placement_major_version, placement_minor_version, placement_patch_version)) + else: + placement_major_version = config.get('placementDefaultMajorVersion', "1") + placement_minor_version = config.get('placementDefaultMinorVersion', "0") + placement_patch_version = config.get('placementDefaultPatchVersion', "0") + + debug_log.debug("Default versions specified in osdf config file are: placementDefaultMajorVersion: {} placementDefaultMinorVersion: {} placementDefaultPatchVersion: {}" + .format(placement_major_version, placement_minor_version, placement_patch_version)) + + version_info_dict.update({ + 'placementVersioningEnabled': placement_ver_enabled, + 'placementMajorVersion': str(placement_major_version), + 'placementMinorVersion': str(placement_minor_version), + 'placementPatchVersion': str( placement_patch_version) + }) + + return version_info_dict +
\ No newline at end of file @@ -45,8 +45,11 @@ from requests import RequestException from schematics.exceptions import DataError from osdf.logging.osdf_logging import MH, audit_log, error_log, debug_log from osdf.models.api.placementRequest import PlacementAPI +from osdf.models.api.pciOptimizationRequest import PCIOptimizationAPI from osdf.operation.responses import osdf_response_for_request_accept as req_accept from osdf.optimizers.routeopt.simple_route_opt import RouteOpt +from osdf.optimizers.pciopt.pci_opt_processor import process_pci_optimation +from osdf.utils import api_data_utils ERROR_TEMPLATE = osdf.ERROR_TEMPLATE @@ -105,6 +108,16 @@ def do_osdf_health_check(): @app.route("/api/oof/v1/placement", methods=["POST"]) @auth_basic.login_required def do_placement_opt(): + return placement_rest_api() + + +@app.route("/api/oof/placement/v1", methods=["POST"]) +@auth_basic.login_required +def do_placement_opt_common_versioning(): + return placement_rest_api() + + +def placement_rest_api(): """Perform placement optimization after validating the request and fetching policies Make a call to the call-back URL with the output of the placement request. Note: Call to Conductor for placement optimization may have redirects, so account for them @@ -113,6 +126,7 @@ def do_placement_opt(): req_id = request_json['requestInfo']['requestId'] g.request_id = req_id audit_log.info(MH.received_request(request.url, request.remote_addr, json.dumps(request_json))) + api_version_info = api_data_utils.retrieve_version_info(request, req_id) PlacementAPI(request_json).validate() policies = get_policies(request_json, "placement") audit_log.info(MH.new_worker_thread(req_id, "[for placement]")) @@ -121,7 +135,7 @@ def do_placement_opt(): audit_log.info(MH.accepted_valid_request(req_id, request)) return req_accept(request_id=req_id, transaction_id=request_json['requestInfo']['transactionId'], - request_status="accepted", status_message="") + version_info=api_version_info, request_status="accepted", status_message="") @app.route("/api/oof/v1/route", methods=["POST"]) @@ -158,6 +172,23 @@ def do_route_calc(): else: return RouteOpt.getRoute(request_json) +@app.route("/api/oof/v1/pci", methods=["POST"]) +@auth_basic.login_required +def do_pci_optimization(): + request_json = request.get_json() + req_id = request_json['requestInfo']['requestId'] + g.request_id = req_id + audit_log.info(MH.received_request(request.url, request.remote_addr, json.dumps(request_json))) + PCIOptimizationAPI(request_json).validate() + policies = get_policies(request_json, "pciopt") + audit_log.info(MH.new_worker_thread(req_id, "[for pciopt]")) + t = Thread(target=process_pci_optimation, args=(request_json, policies, osdf_config)) + t.start() + audit_log.info(MH.accepted_valid_request(req_id, request)) + return req_accept(request_id=req_id, + transaction_id=request_json['requestInfo']['transactionId'], + request_status="accepted", status_message="") + @app.errorhandler(500) def internal_failure(error): """Returned when unexpected coding errors occur during initial synchronous processing""" diff --git a/requirements.txt b/requirements.txt index f05dadf..2cf5358 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ schematics>=2.0.0 docopt>=0.6.2 pydevd>=1.0.0 onapsmsclient>=0.0.3 +pymzn>=0.17.0 diff --git a/test/configdb/test_configdb_calls.py b/test/configdb/test_configdb_calls.py new file mode 100644 index 0000000..eb799e7 --- /dev/null +++ b/test/configdb/test_configdb_calls.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 osdf.optimizers.pciopt.configdb import request +import osdf.config.loader as config_loader +from osdf.utils.interfaces import json_from_file +from osdf.utils.programming_utils import DotDict +from osdf.adapters.policy import interface as pol + + +class TestConfigDbCalls(): + + def setUp(self): + 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)) + self.lp = self.osdf_config.core.get('osdf_temp', {}).get('local_policies', {} + ).get('placement_policy_files_vcpe') + + def tearDown(self): + pass + + def test_request(self): + self.setUp() + req_json = json_from_file("./test/pci-optimization-tests/request.json") + policies = pol.get_local_policies("test/policy-local-files/", self.lp) + cell_list = request(req_json, self.osdf_config, policies) + + diff --git a/test/functest/simulators/configdb/response-payloads/getCellList-1000.json b/test/functest/simulators/configdb/response-payloads/getCellList-1000.json new file mode 100644 index 0000000..df23f6e --- /dev/null +++ b/test/functest/simulators/configdb/response-payloads/getCellList-1000.json @@ -0,0 +1 @@ +["cell0","cell1","cell2"]
\ No newline at end of file diff --git a/test/functest/simulators/configdb/response-payloads/getNbrList-cell0.json b/test/functest/simulators/configdb/response-payloads/getNbrList-cell0.json new file mode 100644 index 0000000..e0986d8 --- /dev/null +++ b/test/functest/simulators/configdb/response-payloads/getNbrList-cell0.json @@ -0,0 +1,10 @@ +[ + { + "cellId": "cell1", + "pciValue": 1 + }, + { + "cellId": "cell2", + "pciValue": 2 + } +]
\ No newline at end of file diff --git a/test/functest/simulators/configdb/response-payloads/getNbrList-cell1.json b/test/functest/simulators/configdb/response-payloads/getNbrList-cell1.json new file mode 100644 index 0000000..d6ed353 --- /dev/null +++ b/test/functest/simulators/configdb/response-payloads/getNbrList-cell1.json @@ -0,0 +1,10 @@ +[ + { + "cellId": "cell0", + "pciValue": 0 + }, + { + "cellId": "cell2", + "pciValue": 2 + } +]
\ No newline at end of file diff --git a/test/functest/simulators/configdb/response-payloads/getNbrList-cell2.json b/test/functest/simulators/configdb/response-payloads/getNbrList-cell2.json new file mode 100644 index 0000000..1ea80be --- /dev/null +++ b/test/functest/simulators/configdb/response-payloads/getNbrList-cell2.json @@ -0,0 +1,10 @@ +[ + { + "cellId": "cell0", + "pciValue": 0 + }, + { + "cellId": "cell1", + "pciValue": 1 + } +]
\ No newline at end of file diff --git a/test/functest/simulators/oof_dependencies_simulators.py b/test/functest/simulators/oof_dependencies_simulators.py index bdb552d..9c20e79 100755 --- a/test/functest/simulators/oof_dependencies_simulators.py +++ b/test/functest/simulators/oof_dependencies_simulators.py @@ -20,9 +20,9 @@ Simulators for dependencies of OSDF (e.g. HAS-API, Policy, SO-callback, etc.) """ import glob +from flask import Flask, jsonify, request from osdf.utils.interfaces import json_from_file -from flask import Flask, jsonify, request app = Flask(__name__) @@ -80,5 +80,22 @@ def get_policies(sub_component): return jsonify([json_from_file(x) for x in files]) +@app.route("/simulated/configdb/getCellList", methods=["GET"]) +def get_cell_list(): + data, status = get_payload_for_simulated_component('configdb', + 'getCellList-' + request.args.get('networkId') + '.json') + if not status: + return jsonify(data) + return jsonify(data), 503 + + +@app.route("/simulated/configdb/getNbrList", methods=["GET"]) +def get_nbr_list(): + data, status = get_payload_for_simulated_component('configdb', 'getNbrList-' + request.args.get('cellId') + '.json') + if not status: + return jsonify(data) + return jsonify(data), 503 + + if __name__ == "__main__": app.run(debug=True) diff --git a/test/functest/simulators/simulated-config/osdf_config.yaml b/test/functest/simulators/simulated-config/osdf_config.yaml index 0a77fe2..dbdcc91 100755 --- a/test/functest/simulators/simulated-config/osdf_config.yaml +++ b/test/functest/simulators/simulated-config/osdf_config.yaml @@ -32,3 +32,15 @@ sdcONAPInstanceID: ONAP-OSDF osdfPlacementUrl: "http://127.0.0.1:24699/osdf/api/v2/placement" osdfPlacementUsername: "test" osdfPlacementPassword: "testpwd" + +# config db api +configDbUrl: http://127.0.0.1:5000/simulated/configdb +configDbUserName: osdf +configDbPassword: passwd +configDbGetCellListUrl: 'getCellList' +configDbGetNbrListUrl: 'getNbrList' + +# Credentials for PCIHandler +pciHMSUsername: "" # pcihandler username for call back. +pciHMSPassword: "" # pcihandler password for call back. + diff --git a/test/pci-optimization-tests/request.json b/test/pci-optimization-tests/request.json new file mode 100644 index 0000000..79c98c3 --- /dev/null +++ b/test/pci-optimization-tests/request.json @@ -0,0 +1,22 @@ +{ + "requestInfo": { + "transactionId": "xxx-xxx-xxxx", + "requestId": "yyy-yyy-yyyy", + "callbackUrl": "https://wiki.onap.org:5000/callbackUrl/", + "sourceId": "SO", + "requestType": "create", + "numSolutions": 1, + "optimizers": [ + "placement" + ], + "timeout": 600 + }, + "cellInfo": { + "networkId": "1000", + "cellIdList": [ + { + "cellId": "cell0" + } + ] + } +}
\ No newline at end of file diff --git a/test/test_api_data_utils.py b/test/test_api_data_utils.py new file mode 100755 index 0000000..99d7a2e --- /dev/null +++ b/test/test_api_data_utils.py @@ -0,0 +1,21 @@ +import json +import os +from osdf.utils import api_data_utils +from collections import defaultdict + + +BASE_DIR = os.path.dirname(__file__) + +with open(os.path.join(BASE_DIR, "placement-tests/request.json")) as json_data: + req_json = json.load(json_data) + +class TestVersioninfo(): +# +# Tests for api_data_utils.py +# + def test_retrieve_version_info(self): + request_id = 'test12345' + test_dict = {'placementVersioningEnabled': False, 'placementMajorVersion': '1', 'placementPatchVersion': '0', 'placementMinorVersion': '0'} + test_verison_info_dict = defaultdict(dict ,test_dict ) + verison_info_dict = api_data_utils.retrieve_version_info(req_json, request_id) + assert verison_info_dict == test_verison_info_dict
\ No newline at end of file diff --git a/test/test_process_pci_opt.py b/test/test_process_pci_opt.py new file mode 100644 index 0000000..31aa5fb --- /dev/null +++ b/test/test_process_pci_opt.py @@ -0,0 +1,79 @@ +# ------------------------------------------------------------------------- +# 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 mock +import unittest + +from flask import Response +from mock import patch +from osdf.adapters.local_data import local_policies +from osdf.optimizers.pciopt.pci_opt_processor import process_pci_optimation +import osdf.config.loader as config_loader +from osdf.utils.interfaces import json_from_file +from osdf.utils.programming_utils import DotDict + + +class TestProcessPlacementOpt(unittest.TestCase): + + def setUp(self): + mock_req_accept_message = Response("Accepted Request", content_type='application/json; charset=utf-8') + self.patcher_req = patch('osdf.optimizers.pciopt.configdb.request', + return_value={"solutionInfo": {"placementInfo": "dummy"}}) + self.patcher_req_accept = patch('osdf.operation.responses.osdf_response_for_request_accept', + return_value=mock_req_accept_message) + self.patcher_callback = patch( + 'osdf.optimizers.pciopt.pci_opt_processor.process_pci_optimation', + return_value=mock_req_accept_message) + + mock_mzn_response = [{'pci': {0: 0, 1: 1, 2: 2}}] + + self.patcher_minizinc_callback = patch( + 'osdf.optimizers.pciopt.solver.optimizer.solve', + return_value=mock_mzn_response ) + self.patcher_RestClient = patch( + 'osdf.utils.interfaces.RestClient', return_value=mock.MagicMock()) + self.Mock_req = self.patcher_req.start() + self.Mock_req_accept = self.patcher_req_accept.start() + self.Mock_callback = self.patcher_callback.start() + self.Mock_RestClient = self.patcher_RestClient.start() + self.Mock_mzn_callback = self.patcher_minizinc_callback.start() + + def tearDown(self): + patch.stopall() + + def test_process_pci_opt_solutions(self): + main_dir = "" + parameter_data_file = main_dir + "test/pci-optimization-tests/request.json" + policy_data_path = main_dir + "test/policy-local-files/" + 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)) + + valid_policies_list_file = policy_data_path + '/' + 'meta-valid-policies.txt' + valid_policies_files = local_policies.get_policy_names_from_file(valid_policies_list_file) + + request_json = json_from_file(parameter_data_file) + policies = [json_from_file(policy_data_path + '/' + name) for name in valid_policies_files] + + templ_string = process_pci_optimation(request_json, self.osdf_config,policies) + + +if __name__ == "__main__": + unittest.main() + |