aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorvrvarma <vv8305@att.com>2019-12-13 10:24:55 -0500
committervrvarma <vv8305@att.com>2019-12-13 10:29:45 -0500
commit430153e1a9489a86cf0ff7942eceb0bf5f1c170c (patch)
treeb22090b80d11761a24d9cbeaa6348704cce2de83 /apps
parent34ba229b31758c38a299a163de0007848b5921be (diff)
osdf rearchitecture into apps and libs
Change-Id: I2db2f8e252bd5e60807b2ffabf5a5b193a789f7a Signed-off-by: vrvarma <vv8305@att.com> Issue-ID: OPTFRA-637
Diffstat (limited to 'apps')
-rw-r--r--apps/__init__.py0
-rw-r--r--apps/license/__init__.py0
-rw-r--r--apps/license/optimizers/__init__.py0
-rw-r--r--apps/license/optimizers/simple_license_allocation.py43
-rw-r--r--apps/pci/__init__.py0
-rw-r--r--apps/pci/models/__init__.py0
-rw-r--r--apps/pci/models/api/__init__.py0
-rw-r--r--apps/pci/models/api/pciOptimizationRequest.py54
-rw-r--r--apps/pci/models/api/pciOptimizationResponse.py46
-rw-r--r--apps/pci/optimizers/__init__.py0
-rw-r--r--apps/pci/optimizers/configdb.py69
-rw-r--r--apps/pci/optimizers/pci_opt_processor.py123
-rw-r--r--apps/pci/optimizers/solver/__init__.py0
-rw-r--r--apps/pci/optimizers/solver/min_confusion.mzn98
-rw-r--r--apps/pci/optimizers/solver/min_confusion_inl.mzn140
-rw-r--r--apps/pci/optimizers/solver/no_conflicts_no_confusion.mzn86
-rw-r--r--apps/pci/optimizers/solver/optimizer.py137
-rw-r--r--apps/pci/optimizers/solver/pci_utils.py40
-rw-r--r--apps/placement/__init__.py0
-rw-r--r--apps/placement/models/__init__.py0
-rw-r--r--apps/placement/models/api/__init__.py0
-rw-r--r--apps/placement/models/api/placementRequest.py105
-rw-r--r--apps/placement/models/api/placementResponse.py64
-rw-r--r--apps/placement/optimizers/__init__.py0
-rw-r--r--apps/placement/optimizers/conductor/__init__.py17
-rw-r--r--apps/placement/optimizers/conductor/api_builder.py99
-rwxr-xr-xapps/placement/optimizers/conductor/conductor.py202
-rw-r--r--apps/placement/optimizers/conductor/remote_opt_processor.py81
-rw-r--r--apps/placement/optimizers/conductor/translation.py283
-rwxr-xr-xapps/placement/templates/conductor_interface.json39
-rwxr-xr-xapps/placement/templates/plc_opt_request.jsont142
-rwxr-xr-xapps/placement/templates/plc_opt_response.jsont10
-rwxr-xr-xapps/placement/templates/policy_request.jsont3
-rw-r--r--apps/route/__init__.py0
-rw-r--r--apps/route/optimizers/__init__.py0
-rw-r--r--apps/route/optimizers/simple_route_opt.py153
-rwxr-xr-xapps/templates/cms_opt_request.jsont35
-rwxr-xr-xapps/templates/cms_opt_request.jsont_1707_v167
-rwxr-xr-xapps/templates/cms_opt_request_1702.jsont63
-rw-r--r--apps/templates/cms_opt_response.jsont8
-rw-r--r--apps/templates/license_opt_request.jsont6
-rwxr-xr-xapps/templates/test_cms_nb_req_from_client.jsont19
-rwxr-xr-xapps/templates/test_plc_nb_req_from_client.jsont52
43 files changed, 2284 insertions, 0 deletions
diff --git a/apps/__init__.py b/apps/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/__init__.py
diff --git a/apps/license/__init__.py b/apps/license/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/license/__init__.py
diff --git a/apps/license/optimizers/__init__.py b/apps/license/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/license/optimizers/__init__.py
diff --git a/apps/license/optimizers/simple_license_allocation.py b/apps/license/optimizers/simple_license_allocation.py
new file mode 100644
index 0000000..b2aaba4
--- /dev/null
+++ b/apps/license/optimizers/simple_license_allocation.py
@@ -0,0 +1,43 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2015-2017 AT&T Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+from osdf.utils.mdc_utils import mdc_from_json
+
+
+def license_optim(request_json):
+ """
+ Fetch license artifacts associated with the service model and search licensekey-group-UUID and entitlement-pool-uuid
+ associated with the given att part number and nominal throughput in a request
+ :param request_json: Request in a JSON format
+ :return: A tuple of licensekey-group-uuid-list and entitlement-group-uuid-list
+ """
+ mdc_from_json(request_json)
+ req_id = request_json["requestInfo"]["requestId"]
+
+ model_name = request_json.get('placementInfo', {}).get('serviceInfo', {}).get('modelInfo', {}).get('modelName')
+ service_name = model_name
+
+ license_info = []
+
+ for demand in request_json.get('placementInfo', {}).get('demandInfo', {}).get('licenseDemands', []):
+ license_info.append(
+ {'serviceResourceId': demand['serviceResourceId'],
+ 'resourceModuleName': demand['resourceModuleName'],
+ 'entitlementPoolList': "NOT SUPPORTED",
+ 'licenseKeyGroupList': "NOT SUPPORTED"
+ })
+ return license_info
diff --git a/apps/pci/__init__.py b/apps/pci/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/__init__.py
diff --git a/apps/pci/models/__init__.py b/apps/pci/models/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/models/__init__.py
diff --git a/apps/pci/models/api/__init__.py b/apps/pci/models/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/models/api/__init__.py
diff --git a/apps/pci/models/api/pciOptimizationRequest.py b/apps/pci/models/api/pciOptimizationRequest.py
new file mode 100644
index 0000000..02b67a2
--- /dev/null
+++ b/apps/pci/models/api/pciOptimizationRequest.py
@@ -0,0 +1,54 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+from schematics.types import BaseType, StringType, URLType, IntType
+from schematics.types.compound import ModelType, ListType, DictType
+
+from osdf.models.api.common import OSDFModel
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ callbackHeader = DictType(BaseType)
+ sourceId = StringType(required=True)
+ requestType = StringType(required=True)
+ numSolutions = IntType()
+ optimizers = ListType(StringType(required=True))
+ timeout = IntType()
+
+
+class ANRInfo(OSDFModel):
+ cellId = StringType(required=True)
+ removeableNeighbors = ListType(StringType())
+
+
+class CellInfo(OSDFModel):
+ """Information specific to CellInfo """
+ networkId = StringType(required=True)
+ cellIdList = ListType(StringType(required=True))
+ anrInputList = ListType(ModelType(ANRInfo))
+ trigger = StringType()
+
+
+class PCIOptimizationAPI(OSDFModel):
+ """Request for PCI optimization """
+ requestInfo = ModelType(RequestInfo, required=True)
+ cellInfo = ModelType(CellInfo, required=True)
diff --git a/apps/pci/models/api/pciOptimizationResponse.py b/apps/pci/models/api/pciOptimizationResponse.py
new file mode 100644
index 0000000..019a43a
--- /dev/null
+++ b/apps/pci/models/api/pciOptimizationResponse.py
@@ -0,0 +1,46 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+from schematics.types import StringType, IntType
+from schematics.types.compound import ModelType, ListType
+
+from osdf.models.api.common import OSDFModel
+
+
+class PCISolution(OSDFModel):
+ cellId = StringType(required=True)
+ pci = IntType(required=True)
+
+
+class ANRSolution(OSDFModel):
+ cellId = StringType(required=True)
+ removeableNeighbors = ListType(StringType())
+
+
+class Solution(OSDFModel):
+ networkId = StringType(required=True)
+ pciSolutions = ListType(ListType(ModelType(PCISolution), min_size=1))
+ anrSolutions = ListType(ListType(ModelType(ANRSolution), min_size=1))
+
+
+class PCIOptimizationResponse(OSDFModel):
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ requestStatus = StringType(required=True)
+ statusMessage = StringType()
+ solutions = ModelType(Solution, required=True)
diff --git a/apps/pci/optimizers/__init__.py b/apps/pci/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/optimizers/__init__.py
diff --git a/apps/pci/optimizers/configdb.py b/apps/pci/optimizers/configdb.py
new file mode 100644
index 0000000..8f003c2
--- /dev/null
+++ b/apps/pci/optimizers/configdb.py
@@ -0,0 +1,69 @@
+# -------------------------------------------------------------------------
+# 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
+
+ ts = dt.strftime(dt.now(), '%Y-%m-%d %H:%M:%S%z')
+
+ rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug, headers=headers)
+
+ cell_list_url = '{}/{}/{}/{}'.format(config['configDbUrl'], config['configDbGetCellListUrl'], network_id, ts)
+
+ cell_list_resp = rc.request(raw_response=True, url=cell_list_url)
+ cell_resp = cell_list_resp.json()
+
+ cell_list = []
+ count = 0
+ for cell_id in cell_resp:
+ cell_info = {'cell_id': cell_id, 'id': count}
+ nbr_list_url = '{}/{}/{}/{}'.format(config['configDbUrl'], config['configDbGetNbrListUrl'], cell_id, ts)
+ nbr_list_raw = rc.request(url=nbr_list_url, raw_response=True)
+ cell_info['nbr_list'] = get_neighbor_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
+
+
+def get_neighbor_list(nbr_list_response):
+ return nbr_list_response.get('nbrList', [])
diff --git a/apps/pci/optimizers/pci_opt_processor.py b/apps/pci/optimizers/pci_opt_processor.py
new file mode 100644
index 0000000..9948d55
--- /dev/null
+++ b/apps/pci/optimizers/pci_opt_processor.py
@@ -0,0 +1,123 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 AT&T Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+import traceback
+
+from onaplogging.mdcContext import MDC
+from requests import RequestException
+
+from osdf.logging.osdf_logging import metrics_log, MH, error_log
+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
+from osdf.utils.mdc_utils import mdc_from_json
+
+"""
+This application generates PCI Optimization API calls using the information received from PCI-Handler-MS, SDN-C
+and Policy.
+"""
+
+
+def process_pci_optimation(request_json, osdf_config, flat_policies):
+ """
+ Process a PCI request from a Client (build config-db, policy and API call, make the call, return result)
+ :param req_object: Request parameters from the client
+ :param osdf_config: Configuration specific to OSDF application (core + deployment)
+ :param flat_policies: policies related to pci (fetched based on request)
+ :return: response from PCI Opt
+ """
+ try:
+ mdc_from_json(request_json)
+ rc = get_rest_client(request_json, service="pcih")
+ req_id = request_json["requestInfo"]["requestId"]
+ cell_info_list, network_cell_info = config_request(request_json, osdf_config, flat_policies)
+ pci_response = get_solutions(cell_info_list, network_cell_info, request_json)
+
+ metrics_log.info(MH.inside_worker_thread(req_id))
+ except Exception as err:
+ error_log.error("Error for {} {}".format(req_id, traceback.format_exc()))
+
+ try:
+ body = build_json_error_body(err)
+ metrics_log.info(MH.sending_response(req_id, "ERROR"))
+ rc.request(json=body, noresponse=True)
+ except RequestException as err:
+ MDC.put('requestID',req_id)
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
+ raise err
+
+
+ try:
+ metrics_log.info(MH.calling_back_with_body(req_id, rc.url, pci_response))
+ rc.request(json=pci_response, noresponse=True)
+ except RequestException: # can't do much here but log it and move on
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
+
+
+def get_solutions(cell_info_list, network_cell_info, request_json):
+ status, pci_solutions, anr_solutions = build_solution_list(cell_info_list, network_cell_info, request_json)
+ return {
+ "transactionId": request_json['requestInfo']['transactionId'],
+ "requestId": request_json["requestInfo"]["requestId"],
+ "requestStatus": "completed",
+ "statusMessage": status,
+ "solutions": {
+ 'networkId': request_json['cellInfo']['networkId'],
+ 'pciSolutions': pci_solutions,
+ 'anrSolutions': anr_solutions
+ }
+ }
+
+
+def build_solution_list(cell_info_list, network_cell_info, request_json):
+ status = "success"
+ req_id = request_json["requestInfo"]["requestId"]
+ try:
+ opt_solution = optimize(network_cell_info, cell_info_list, request_json)
+ pci_solutions = build_pci_solution(network_cell_info, opt_solution['pci'])
+ anr_solutions = build_anr_solution(network_cell_info, opt_solution.get('removables', {}))
+ except RuntimeError:
+ error_log.error("Failed finding solution for {} {}".format(req_id, traceback.format_exc()))
+ status = "failed"
+ return status, pci_solutions, anr_solutions
+
+
+def build_pci_solution(network_cell_info, pci_solution):
+ pci_solutions = []
+ for k, v in pci_solution.items():
+ old_pci = get_pci_value(network_cell_info, k)
+ if old_pci != v:
+ response = {
+ 'cellId': get_cell_id(network_cell_info, k),
+ 'pci': v
+ }
+ pci_solutions.append(response)
+ return pci_solutions
+
+
+def build_anr_solution(network_cell_info, removables):
+ anr_solutions = []
+ for k, v in removables.items():
+ response = {
+ 'cellId': get_cell_id(network_cell_info, k),
+ 'removeableNeighbors': list(map(lambda x: get_cell_id(network_cell_info, x), v))
+ }
+ anr_solutions.append(response)
+ return anr_solutions
diff --git a/apps/pci/optimizers/solver/__init__.py b/apps/pci/optimizers/solver/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/pci/optimizers/solver/__init__.py
diff --git a/apps/pci/optimizers/solver/min_confusion.mzn b/apps/pci/optimizers/solver/min_confusion.mzn
new file mode 100644
index 0000000..ff56c18
--- /dev/null
+++ b/apps/pci/optimizers/solver/min_confusion.mzn
@@ -0,0 +1,98 @@
+% -------------------------------------------------------------------------
+% Copyright (c) 2018 AT&T Intellectual Property
+%
+% Licensed under the Apache License, Version 2.0 (the "License");
+% you may not use this file except in compliance with the License.
+% You may obtain a copy of the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS,
+% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+% See the License for the specific language governing permissions and
+% limitations under the License.
+%
+% -------------------------------------------------------------------------
+%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Parameters and its assertions
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Number of cells/radios.
+int: NUM_NODES;
+
+% Maximum number of Physical Cell Identifiers to be assigned to the nodes.
+int: NUM_PCIS;
+
+% Number of edges between neighbor nodes. There is a edge (i,j) if and only
+% if nodes i and j are neighbors, i.e., an user equipment (UE) can make
+% handoff between i and j. Such edges are used to avoid **COLLISION**, i.e.,
+% to guarantee that nodes i and j have different PCIs.
+int: NUM_NEIGHBORS;
+
+% Each line represents an edge between direct neighbors as defined before.
+array[1..NUM_NEIGHBORS, 1..2] of int: NEIGHBORS;
+
+% Number of undirect neighbor pairs (j, k) such that both j and k are direct
+% neighbors of node i, i.e., (j, k) exits if and only if exists (i, j) and
+% (i, k). Nodes (i, k) can generate "confunsions" in the network if they have
+% the same PCI. Such edges are used to avoid/minimize **CONFUSIONS**.
+int: NUM_SECOND_LEVEL_NEIGHBORS;
+
+% Each line represents an edge between undirect neighbors as defined before.
+array[1..NUM_SECOND_LEVEL_NEIGHBORS, 1..2] of int: SECOND_LEVEL_NEIGHBORS;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Decision variables
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Defines the PCI for each node.
+array[0..NUM_NODES-1] of var 0..NUM_PCIS-1: pci;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Constraints
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Direct neighbors must have different PCIs for avoid **COLLISION**.
+constraint
+forall(i in 1..NUM_NEIGHBORS)(
+ pci[NEIGHBORS[i, 1]] != pci[NEIGHBORS[i, 2]]
+);
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Objective function
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Total number of confusions.
+var int: total_confusions =
+ sum([bool2int(pci[SECOND_LEVEL_NEIGHBORS[i, 1]] ==
+ pci[SECOND_LEVEL_NEIGHBORS[i, 2]])
+ | i in 1..NUM_SECOND_LEVEL_NEIGHBORS]);
+
+% Minimize the total number of confusions.
+solve :: int_search(pci, smallest, indomain_min, complete)
+minimize total_confusions;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Output
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+output
+["PCI assigment"] ++
+["\nnode,pci"] ++
+[
+ "\n" ++ show(node) ++ "," ++ show(pci[node])
+| node in 0..NUM_NODES-1
+] ++
+
+["\n\nConfusions"] ++
+["\nTotal confusions: " ++ show(total_confusions)] ++
+["\nConfusion pairs"] ++
+[
+ "\n" ++ show(SECOND_LEVEL_NEIGHBORS[i, 1]) ++ "," ++
+ show(SECOND_LEVEL_NEIGHBORS[i, 2])
+| i in 1..NUM_SECOND_LEVEL_NEIGHBORS where
+ fix(pci[SECOND_LEVEL_NEIGHBORS[i, 1]] == pci[SECOND_LEVEL_NEIGHBORS[i, 2]])
+]
diff --git a/apps/pci/optimizers/solver/min_confusion_inl.mzn b/apps/pci/optimizers/solver/min_confusion_inl.mzn
new file mode 100644
index 0000000..0f0fc91
--- /dev/null
+++ b/apps/pci/optimizers/solver/min_confusion_inl.mzn
@@ -0,0 +1,140 @@
+% -------------------------------------------------------------------------
+% Copyright (c) 2018 AT&T Intellectual Property
+%
+% Licensed under the Apache License, Version 2.0 (the "License");
+% you may not use this file except in compliance with the License.
+% You may obtain a copy of the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS,
+% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+% See the License for the specific language governing permissions and
+% limitations under the License.
+%
+% -------------------------------------------------------------------------
+%
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Parameters and its assertions
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Number of cells/radios.
+int: NUM_NODES;
+
+% Maximum number of Physical Cell Identifiers to be assigned to the nodes.
+int: NUM_PCIS;
+
+% Number of edges between neighbor nodes. There is a edge (i,j) if and only
+% if nodes i and j are neighbors, i.e., an user equipment (UE) can make
+% handoff between i and j. Such edges are used to avoid **COLLISION**, i.e.,
+% to guarantee that nodes i and j have different PCIs.
+int: NUM_NEIGHBORS;
+
+% Each line represents an edge between direct neighbors as defined before.
+array[1..NUM_NEIGHBORS, 1..2] of int: NEIGHBORS;
+
+% Number of undirect neighbor pairs (j, k) such that both j and k are direct
+% neighbors of node i, i.e., (j, k) exits if and only if exists (i, j) and
+% (i, k). Nodes (i, k) can generate "confunsions" in the network if they have
+% the same PCI. Such edges are used to avoid/minimize **CONFUSIONS**.
+int: NUM_SECOND_LEVEL_NEIGHBORS;
+
+% Each line represents an edge between undirect neighbors as defined before.
+array[1..NUM_SECOND_LEVEL_NEIGHBORS, 1..2] of int: SECOND_LEVEL_NEIGHBORS;
+
+% Number of ignorable neighbor links. Such links can be ignored during
+% optimization if needed.
+int: NUM_IGNORABLE_NEIGHBOR_LINKS;
+
+% The links that can be ignored if needed. Each line represents the two ends
+% of the links, like the previous structures.
+array[1..NUM_IGNORABLE_NEIGHBOR_LINKS, 1..2] of int: IGNORABLE_NEIGHBOR_LINKS;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Decision variables
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Defines the PCI for each node.
+array[0..NUM_NODES-1] of var 0..NUM_PCIS-1: pci;
+
+array[1..NUM_IGNORABLE_NEIGHBOR_LINKS] of var 0..1: used_ignorables;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Constraints
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Direct neighbors must have different PCIs for avoid **COLLISION**.
+% Forced links.
+constraint
+forall(i in 1..NUM_NEIGHBORS, j in 1..NUM_IGNORABLE_NEIGHBOR_LINKS
+ where
+ NEIGHBORS[i, 1] != IGNORABLE_NEIGHBOR_LINKS[j, 1] \/
+ NEIGHBORS[i, 2] != IGNORABLE_NEIGHBOR_LINKS[j, 2]
+)(
+ pci[NEIGHBORS[i, 1]] != pci[NEIGHBORS[i, 2]]
+);
+
+
+% Ignorable links.
+constraint
+forall(i in 1..NUM_NEIGHBORS, j in 1..NUM_IGNORABLE_NEIGHBOR_LINKS
+ where
+ NEIGHBORS[i, 1] == IGNORABLE_NEIGHBOR_LINKS[j, 1] /\
+ NEIGHBORS[i, 2] == IGNORABLE_NEIGHBOR_LINKS[j, 2]
+)(
+ used_ignorables[j] >= bool2int(pci[NEIGHBORS[i, 1]] == pci[NEIGHBORS[i, 2]])
+);
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Objective function
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Total number of confusions.
+var int: total_confusions =
+ sum([bool2int(pci[SECOND_LEVEL_NEIGHBORS[i, 1]] ==
+ pci[SECOND_LEVEL_NEIGHBORS[i, 2]])
+ | i in 1..NUM_SECOND_LEVEL_NEIGHBORS]);
+
+% Total number of used ignorables links.
+var int: total_used_ignorables = sum(used_ignorables);
+
+solve :: int_search(pci, smallest, indomain_min, complete)
+
+% Minimize the total number of confusions.
+%minimize total_confusions;
+
+% Minimize the total number of confusions first,
+% then the number of used ignorables links.
+minimize (2 * NUM_IGNORABLE_NEIGHBOR_LINKS * total_confusions) +
+ total_used_ignorables;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Output
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+output
+["PCI assigment"] ++
+["\nnode,pci"] ++
+[
+ "\n" ++ show(node) ++ "," ++ show(pci[node])
+| node in 0..NUM_NODES-1
+] ++
+["\n\nTotal used ignorables links: " ++ show(total_used_ignorables)] ++
+["\nUsed ignorables links: "] ++
+[
+ "\n" ++ show(IGNORABLE_NEIGHBOR_LINKS[i, 1]) ++
+ "," ++ show(IGNORABLE_NEIGHBOR_LINKS[i, 2])
+ | i in 1..NUM_IGNORABLE_NEIGHBOR_LINKS where fix(used_ignorables[i] > 0)
+] ++
+["\n\nConfusions"] ++
+["\nTotal confusions: " ++ show(total_confusions)] ++
+["\nConfusion pairs"] ++
+[
+ "\n" ++ show(SECOND_LEVEL_NEIGHBORS[i, 1]) ++ "," ++
+ show(SECOND_LEVEL_NEIGHBORS[i, 2])
+ | i in 1..NUM_SECOND_LEVEL_NEIGHBORS where
+ fix(pci[SECOND_LEVEL_NEIGHBORS[i, 1]] == pci[SECOND_LEVEL_NEIGHBORS[i, 2]])
+]
+
diff --git a/apps/pci/optimizers/solver/no_conflicts_no_confusion.mzn b/apps/pci/optimizers/solver/no_conflicts_no_confusion.mzn
new file mode 100644
index 0000000..0a9b3e3
--- /dev/null
+++ b/apps/pci/optimizers/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 **COLLISIONS**, i.e.,
+% to guarantee that nodes i and j have different PCIs.
+int: NUM_NEIGHBORS;
+
+% Each line represents an edge between direct neighbors as defined before.
+array[1..NUM_NEIGHBORS, 1..2] of int: NEIGHBORS;
+
+% Number of undirect neighbor pairs (j, k) such that both j and k are direct
+% neighbors of node i, i.e., (j, k) exits if and only if exists (i, j) and
+% (i, k). Nodes (i, k) can generate "confunsions" in the network if they have
+% the same PCI. Such edges are used to avoid/minimize **CONFUSIONS**.
+int: NUM_SECOND_LEVEL_NEIGHBORS;
+
+% Each line represents an edge between undirect neighbors as defined before.
+array[1..NUM_SECOND_LEVEL_NEIGHBORS, 1..2] of int: SECOND_LEVEL_NEIGHBORS;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Decision variables
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Defines the PCI for each node.
+array[0..NUM_NODES-1] of var 0..NUM_PCIS-1: pci;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Constraints
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Direct neighbors must have different PCIs for avoid **COLLISION**.
+constraint
+forall(i in 1..NUM_NEIGHBORS)(
+ pci[NEIGHBORS[i, 1]] != pci[NEIGHBORS[i, 2]]
+);
+
+% Undirect neighbors must have different PCIs for avoid **CONFUSIONS**.
+constraint
+forall(i in 1..NUM_SECOND_LEVEL_NEIGHBORS)(
+ pci[SECOND_LEVEL_NEIGHBORS[i, 1]] != pci[SECOND_LEVEL_NEIGHBORS[i, 2]]
+);
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Objective function
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+% Just satisfy the problem.
+solve :: int_search(pci, smallest, indomain_min, complete) satisfy;
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Output
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+output
+["node,pci\n"] ++
+[
+ show(node) ++ "," ++ show(pci[node]) ++ "\n"
+| node in 0..NUM_NODES-1
+]
diff --git a/apps/pci/optimizers/solver/optimizer.py b/apps/pci/optimizers/solver/optimizer.py
new file mode 100644
index 0000000..0a6d5a4
--- /dev/null
+++ b/apps/pci/optimizers/solver/optimizer.py
@@ -0,0 +1,137 @@
+# -------------------------------------------------------------------------
+# 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
+from collections import defaultdict
+
+import pymzn
+
+from .pci_utils import get_id
+
+BASE_DIR = os.path.dirname(__file__)
+
+
+def pci_optimize(network_cell_info, cell_info_list, request_json):
+ neighbor_edges = get_neighbor_list(network_cell_info)
+ second_level_edges = get_second_level_neighbor(network_cell_info)
+ ignorable_links = get_ignorable_links(network_cell_info, request_json)
+ anr_flag = is_anr(request_json)
+
+ dzn_data = build_dzn_data(cell_info_list, ignorable_links, neighbor_edges, second_level_edges, anr_flag)
+
+ return build_pci_solution(dzn_data, ignorable_links, anr_flag)
+
+
+def build_pci_solution(dzn_data, ignorable_links, anr_flag):
+ mzn_solution = solve(get_mzn_model(anr_flag), dzn_data)
+
+ solution = {'pci': mzn_solution[0]['pci']}
+
+ if anr_flag:
+ removables = defaultdict(list)
+ used_ignorables = mzn_solution[0]['used_ignorables']
+ index = 0
+ for i in ignorable_links:
+ if used_ignorables[index] > 0:
+ removables[i[0]].append(i[1])
+ index += 1
+ solution['removables'] = removables
+ return solution
+
+
+def build_dzn_data(cell_info_list, ignorable_links, neighbor_edges, second_level_edges, anr_flag):
+ dzn_data = {
+ 'NUM_NODES': len(cell_info_list),
+ 'NUM_PCIS': len(cell_info_list),
+ 'NUM_NEIGHBORS': len(neighbor_edges),
+ 'NEIGHBORS': get_list(neighbor_edges),
+ 'NUM_SECOND_LEVEL_NEIGHBORS': len(second_level_edges),
+ 'SECOND_LEVEL_NEIGHBORS': get_list(second_level_edges)
+ }
+ if anr_flag:
+ dzn_data['NUM_IGNORABLE_NEIGHBOR_LINKS'] = len(ignorable_links)
+ dzn_data['IGNORABLE_NEIGHBOR_LINKS'] = get_list(ignorable_links)
+ return dzn_data
+
+
+def get_mzn_model(anr_flag):
+ if anr_flag:
+ mzn_model = os.path.join(BASE_DIR, 'min_confusion_inl.mzn')
+ else:
+ mzn_model = os.path.join(BASE_DIR, 'no_conflicts_no_confusion.mzn')
+ return mzn_model
+
+
+def is_anr(request_json):
+ return 'pci-anr' in request_json["requestInfo"]["optimizers"]
+
+
+def get_list(edge_list):
+ array_list = []
+ for s in edge_list:
+ array_list.append([s[0], s[1]])
+ return sorted(array_list)
+
+
+def solve(mzn_model, dzn_data):
+ return pymzn.minizinc(mzn=mzn_model, data=dzn_data)
+
+
+def get_neighbor_list(network_cell_info):
+ neighbor_list = set()
+ for cell in network_cell_info['cell_list']:
+ add_to_neighbor_list(network_cell_info, cell, neighbor_list)
+ return neighbor_list
+
+
+def add_to_neighbor_list(network_cell_info, cell, neighbor_list):
+ for nbr in cell.get('nbr_list', []):
+ host_id = cell['id']
+ nbr_id = get_id(network_cell_info, nbr['targetCellId'])
+ if nbr_id and host_id != nbr_id:
+ neighbor_list.add((host_id, nbr_id))
+
+
+def get_second_level_neighbor(network_cell_info):
+ second_neighbor_list = set()
+ for cell in network_cell_info['cell_list']:
+ comb_list = build_second_level_list(network_cell_info, cell)
+ for comb in comb_list:
+ if comb[0] and comb[1]:
+ second_neighbor_list.add((comb[0], comb[1]))
+ return sorted(second_neighbor_list)
+
+
+def build_second_level_list(network_cell_info, cell):
+ second_nbr_list = []
+ for nbr in cell.get('nbr_list', []):
+ second_nbr_list.append(get_id(network_cell_info, nbr['targetCellId']))
+ return [list(elem) for elem in list(itertools.combinations(second_nbr_list, 2))]
+
+
+def get_ignorable_links(network_cell_info, request_json):
+ ignorable_list = set()
+ anr_input_list = request_json["cellInfo"].get('anrInputList', [])
+ if anr_input_list:
+ for anr_info in anr_input_list:
+ cell_id = get_id(network_cell_info, anr_info['cellId'])
+ anr_removable = anr_info.get('removeableNeighbors', [])
+ for anr in anr_removable:
+ ignorable_list.add((cell_id, get_id(network_cell_info, anr)))
+ return ignorable_list
diff --git a/apps/pci/optimizers/solver/pci_utils.py b/apps/pci/optimizers/solver/pci_utils.py
new file mode 100644
index 0000000..04829cf
--- /dev/null
+++ b/apps/pci/optimizers/solver/pci_utils.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.
+#
+# -------------------------------------------------------------------------
+#
+
+
+def get_id(network_cell_info, cell_id):
+ for i in network_cell_info['cell_list']:
+ if i['cell_id'] == cell_id:
+ return i['id']
+ return None
+
+
+def get_cell_id(network_cell_info, id):
+ for i in network_cell_info['cell_list']:
+ if i['id'] == id:
+ return i['cell_id']
+ return None
+
+
+def get_pci_value(network_cell_info, id):
+ cell_id = get_cell_id(network_cell_info, id)
+ for i in network_cell_info['cell_list']:
+ for j in i['nbr_list']:
+ if cell_id == j['targetCellId']:
+ return j['pciValue']
+ return None
diff --git a/apps/placement/__init__.py b/apps/placement/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/placement/__init__.py
diff --git a/apps/placement/models/__init__.py b/apps/placement/models/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/placement/models/__init__.py
diff --git a/apps/placement/models/api/__init__.py b/apps/placement/models/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/placement/models/api/__init__.py
diff --git a/apps/placement/models/api/placementRequest.py b/apps/placement/models/api/placementRequest.py
new file mode 100644
index 0000000..a0941cf
--- /dev/null
+++ b/apps/placement/models/api/placementRequest.py
@@ -0,0 +1,105 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2015-2017 AT&T Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType, StringType, URLType, IntType, BooleanType
+from schematics.types.compound import ModelType, ListType, DictType
+
+
+class RequestInfo(OSDFModel):
+ """Info for northbound request from client such as SO"""
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ callbackUrl = URLType(required=True)
+ callbackHeader = DictType(BaseType)
+ sourceId = StringType(required=True)
+ requestType = StringType(required=True)
+ numSolutions = IntType()
+ optimizers = ListType(StringType(required=True))
+ timeout = IntType()
+
+
+class Candidates(OSDFModel):
+ """Preferred candidate for a resource (sent as part of a request from client)"""
+ identifierType = StringType(required=True)
+ identifiers = ListType(StringType(required=True))
+ cloudOwner = StringType()
+
+
+class ModelMetaData(OSDFModel):
+ """Model information for a specific resource"""
+ modelInvariantId = StringType(required=True)
+ modelVersionId = StringType(required=True)
+ modelName = StringType()
+ modelType = StringType()
+ modelVersion = StringType()
+ modelCustomizationName = StringType()
+
+
+class LicenseModel(OSDFModel):
+ entitlementPoolUUID = ListType(StringType(required=True))
+ licenseKeyGroupUUID = ListType(StringType(required=True))
+
+
+class LicenseDemands(OSDFModel):
+ resourceModuleName = StringType(required=True)
+ serviceResourceId = StringType(required=True)
+ resourceModelInfo = ModelType(ModelMetaData, required=True)
+ existingLicenses = ModelType(LicenseModel)
+
+
+class LicenseInfo(OSDFModel):
+ licenseDemands = ListType(ModelType(LicenseDemands))
+
+
+class PlacementDemand(OSDFModel):
+ resourceModuleName = StringType(required=True)
+ serviceResourceId = StringType(required=True)
+ tenantId = StringType()
+ resourceModelInfo = ModelType(ModelMetaData, required=True)
+ existingCandidates = ListType(ModelType(Candidates))
+ excludedCandidates = ListType(ModelType(Candidates))
+ requiredCandidates = ListType(ModelType(Candidates))
+
+
+class ServiceInfo(OSDFModel):
+ serviceInstanceId = StringType(required=True)
+ modelInfo = ModelType(ModelMetaData, required=True)
+ serviceName = StringType(required=True)
+
+
+class SubscriberInfo(OSDFModel):
+ """Details on the customer that subscribes to the VNFs"""
+ globalSubscriberId = StringType(required=True)
+ subscriberName = StringType()
+ subscriberCommonSiteId = StringType()
+
+
+class PlacementInfo(OSDFModel):
+ """Information specific to placement optimization"""
+ requestParameters = DictType(BaseType)
+ placementDemands = ListType(ModelType(PlacementDemand), min_size=1)
+ subscriberInfo = ModelType(SubscriberInfo)
+
+
+class PlacementAPI(OSDFModel):
+ """Request for placement optimization (specific to optimization and additional metadata"""
+ requestInfo = ModelType(RequestInfo, required=True)
+ placementInfo = ModelType(PlacementInfo, required=True)
+ licenseInfo = ModelType(LicenseInfo)
+ serviceInfo = ModelType(ServiceInfo, required=True)
diff --git a/apps/placement/models/api/placementResponse.py b/apps/placement/models/api/placementResponse.py
new file mode 100644
index 0000000..13b8d7a
--- /dev/null
+++ b/apps/placement/models/api/placementResponse.py
@@ -0,0 +1,64 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2015-2017 AT&T Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType, StringType
+from schematics.types.compound import ModelType, ListType, DictType
+
+
+# TODO: update osdf.models
+
+class LicenseSolution(OSDFModel):
+ serviceResourceId = StringType(required=True)
+ resourceModuleName = StringType(required=True)
+ entitlementPoolUUID = ListType(StringType(required=True))
+ licenseKeyGroupUUID = ListType(StringType(required=True))
+ entitlementPoolInvariantUUID = ListType(StringType(required=True))
+ licenseKeyGroupInvariantUUID = ListType(StringType(required=True))
+
+
+class Candidates(OSDFModel):
+ """Preferred candidate for a resource (sent as part of a request from client)"""
+ identifierType = StringType(required=True)
+ identifiers = ListType(StringType(required=True))
+ cloudOwner = StringType()
+
+
+class AssignmentInfo(OSDFModel):
+ key = StringType(required=True)
+ value = BaseType(required=True)
+
+
+class PlacementSolution(OSDFModel):
+ serviceResourceId = StringType(required=True)
+ resourceModuleName = StringType(required=True)
+ solution = ModelType(Candidates, required=True)
+ assignmentInfo = ListType(ModelType(AssignmentInfo))
+
+
+class Solution(OSDFModel):
+ placementSolutions = ListType(ListType(ModelType(PlacementSolution), min_size=1))
+ licenseSolutions = ListType(ModelType(LicenseSolution), min_size=1)
+
+
+class PlacementResponse(OSDFModel):
+ transactionId = StringType(required=True)
+ requestId = StringType(required=True)
+ requestStatus = StringType(required=True)
+ statusMessage = StringType()
+ solutions = ModelType(Solution, required=True)
diff --git a/apps/placement/optimizers/__init__.py b/apps/placement/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/placement/optimizers/__init__.py
diff --git a/apps/placement/optimizers/conductor/__init__.py b/apps/placement/optimizers/conductor/__init__.py
new file mode 100644
index 0000000..4b25e5b
--- /dev/null
+++ b/apps/placement/optimizers/conductor/__init__.py
@@ -0,0 +1,17 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2017-2018 AT&T Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
diff --git a/apps/placement/optimizers/conductor/api_builder.py b/apps/placement/optimizers/conductor/api_builder.py
new file mode 100644
index 0000000..7b61580
--- /dev/null
+++ b/apps/placement/optimizers/conductor/api_builder.py
@@ -0,0 +1,99 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+
+import json
+
+from jinja2 import Template
+
+import apps.placement.optimizers.conductor.translation as tr
+from osdf.adapters.policy.utils import group_policies_gen
+from osdf.utils.programming_utils import list_flatten
+
+
+def _build_parameters(group_policies, request_json):
+ """
+ Function prepares parameters section for has request
+ :param group_policies: filtered policies
+ :param request_json: parameter data received from a client
+ :return:
+ """
+ initial_params = tr.get_opt_query_data(request_json, group_policies['request_param_query'])
+ params = dict()
+ params.update({"REQUIRED_MEM": initial_params.pop("requiredMemory", "")})
+ params.update({"REQUIRED_DISK": initial_params.pop("requiredDisk", "")})
+ params.update({"customer_lat": initial_params.pop("customerLatitude", 0.0)})
+ params.update({"customer_long": initial_params.pop("customerLongitude", 0.0)})
+ params.update({"service_name": request_json['serviceInfo']['serviceName']})
+ params.update({"service_id": request_json['serviceInfo']['serviceInstanceId']})
+
+ for key, val in initial_params.items():
+ if val and val != "":
+ params.update({key: val})
+
+ return params
+
+
+def conductor_api_builder(request_json, flat_policies: list, local_config,
+ template="apps/placement/templates/conductor_interface.json"):
+ """Build an OSDF southbound API call for HAS-Conductor/Placement optimization
+ :param request_json: parameter data received from a client
+ :param flat_policies: policy data received from the policy platform (flat policies)
+ :param template: template to generate southbound API call to conductor
+ :param local_config: local configuration file with pointers for the service specific information
+ :param prov_status: provStatus retrieved from Subscriber policy
+ :return: json to be sent to Conductor/placement optimization
+ """
+ templ = Template(open(template).read())
+ gp = group_policies_gen(flat_policies, local_config)
+ demand_vnf_name_list = []
+
+ for placementDemand in request_json['placementInfo']['placementDemands']:
+ demand_vnf_name_list.append(placementDemand['resourceModuleName'].lower())
+ demand_list = tr.gen_demands(request_json, gp['vnfPolicy'])
+ attribute_policy_list = tr.gen_attribute_policy(demand_vnf_name_list, gp['attribute'])
+ distance_to_location_policy_list = tr.gen_distance_to_location_policy(
+ demand_vnf_name_list, gp['distance_to_location'])
+ inventory_policy_list = tr.gen_inventory_group_policy(demand_vnf_name_list, gp['inventory_group'])
+ resource_instance_policy_list = tr.gen_resource_instance_policy(
+ demand_vnf_name_list, gp['instance_fit'])
+ resource_region_policy_list = tr.gen_resource_region_policy(demand_vnf_name_list, gp['region_fit'])
+ zone_policy_list = tr.gen_zone_policy(demand_vnf_name_list, gp['zone'])
+ optimization_policy_list = tr.gen_optimization_policy(demand_vnf_name_list, gp['placement_optimization'])
+ reservation_policy_list = tr.gen_reservation_policy(demand_vnf_name_list, gp['instance_reservation'])
+ capacity_policy_list = tr.gen_capacity_policy(demand_vnf_name_list, gp['vim_fit'])
+ hpa_policy_list = tr.gen_hpa_policy(demand_vnf_name_list, gp['hpa'])
+ req_params_dict = _build_parameters(gp, request_json)
+ conductor_policies = [attribute_policy_list, distance_to_location_policy_list, inventory_policy_list,
+ resource_instance_policy_list, resource_region_policy_list, zone_policy_list,
+ reservation_policy_list, capacity_policy_list, hpa_policy_list]
+ filtered_policies = [x for x in conductor_policies if len(x) > 0]
+ policy_groups = list_flatten(filtered_policies)
+ req_info = request_json['requestInfo']
+ request_type = req_info.get('requestType', None)
+ rendered_req = templ.render(
+ requestType=request_type,
+ demand_list=demand_list,
+ policy_groups=policy_groups,
+ optimization_policies=optimization_policy_list,
+ name=req_info['requestId'],
+ timeout=req_info['timeout'],
+ limit=req_info['numSolutions'],
+ request_params=req_params_dict,
+ json=json)
+ json_payload = json.dumps(json.loads(rendered_req)) # need this because template's JSON is ugly!
+ return json_payload
diff --git a/apps/placement/optimizers/conductor/conductor.py b/apps/placement/optimizers/conductor/conductor.py
new file mode 100755
index 0000000..05d1641
--- /dev/null
+++ b/apps/placement/optimizers/conductor/conductor.py
@@ -0,0 +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 apps.placement.optimizers.conductor.api_builder import conductor_api_builder
+from osdf.logging.osdf_logging import debug_log
+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", "directives": "oof_directives"}
+ for reco in conductor_response['plans'][0]['recommendations']:
+ for resource in reco.keys():
+ c = reco[resource]['candidate']
+ solution = {
+ 'resourceModuleName': resource,
+ 'serviceResourceId': reco[resource].get('service_resource_id', ""),
+ 'solution': {"identifierType": name_map.get(c['inventory_type'], c['inventory_type']),
+ 'identifiers': [c['candidate_id']],
+ 'cloudOwner': c.get('cloud_owner', "")},
+ 'assignmentInfo': []
+ }
+ for key, value in c.items():
+ if key in ["location_id", "location_type", "is_rehome", "host_id"]:
+ try:
+ solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
+ except KeyError:
+ debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))
+
+ for key, value in reco[resource]['attributes'].items():
+ try:
+ solution['assignmentInfo'].append({"key": name_map.get(key, key), "value": value})
+ except KeyError:
+ debug_log.debug("The key[{}] is not mapped and will not be returned in assignment info".format(key))
+ composite_solutions.append(solution)
+
+ request_status = "completed" if conductor_response['plans'][0]['status'] == "done" \
+ else conductor_response['plans'][0]['status']
+ 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/apps/placement/optimizers/conductor/remote_opt_processor.py b/apps/placement/optimizers/conductor/remote_opt_processor.py
new file mode 100644
index 0000000..a08f3a4
--- /dev/null
+++ b/apps/placement/optimizers/conductor/remote_opt_processor.py
@@ -0,0 +1,81 @@
+# -------------------------------------------------------------------------
+# 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 requests import RequestException
+
+import traceback
+from osdf.operation.error_handling import build_json_error_body
+from osdf.logging.osdf_logging import metrics_log, MH, error_log
+from apps.placement.optimizers.conductor import conductor
+from apps.license.optimizers.simple_license_allocation import license_optim
+from osdf.utils.interfaces import get_rest_client
+from osdf.utils.mdc_utils import mdc_from_json
+
+
+def process_placement_opt(request_json, policies, osdf_config):
+ """Perform the work for placement optimization (e.g. call SDC artifact and make conductor request)
+ NOTE: there is scope to make the requests to policy asynchronous to speed up overall performance
+ :param request_json: json content from original request
+ :param policies: flattened policies corresponding to this request
+ :param osdf_config: configuration specific to OSDF app
+ :param prov_status: provStatus retrieved from Subscriber policy
+ :return: None, but make a POST to callback URL
+ """
+
+ try:
+ mdc_from_json(request_json)
+ rc = get_rest_client(request_json, service="so")
+ req_id = request_json["requestInfo"]["requestId"]
+ transaction_id = request_json['requestInfo']['transactionId']
+
+ metrics_log.info(MH.inside_worker_thread(req_id))
+ license_info = None
+ if request_json.get('licenseInfo', {}).get('licenseDemands'):
+ license_info = license_optim(request_json)
+
+ # Conductor only handles placement, only call Conductor if placementDemands exist
+ if request_json.get('placementInfo', {}).get('placementDemands'):
+ metrics_log.info(MH.requesting("placement/conductor", req_id))
+ placement_response = conductor.request(request_json, osdf_config, policies)
+ if license_info: # Attach license solution if it exists
+ placement_response['solutionInfo']['licenseInfo'] = license_info
+ else: # License selection only scenario
+ placement_response = {
+ "transactionId": transaction_id,
+ "requestId": req_id,
+ "requestStatus": "completed",
+ "statusMessage": "License selection completed successfully",
+ "solutionInfo": {"licenseInfo": license_info}
+ }
+ except Exception as err:
+ error_log.error("Error for {} {}".format(req_id, traceback.format_exc()))
+
+ try:
+ body = build_json_error_body(err)
+ metrics_log.info(MH.sending_response(req_id, "ERROR"))
+ rc.request(json=body, noresponse=True)
+ except RequestException:
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
+ return
+
+ try:
+ metrics_log.info(MH.calling_back_with_body(req_id, rc.url,placement_response))
+ rc.request(json=placement_response, noresponse=True)
+ except RequestException : # can't do much here but log it and move on
+ error_log.error("Error sending asynchronous notification for {} {}".format(req_id, traceback.format_exc()))
+
diff --git a/apps/placement/optimizers/conductor/translation.py b/apps/placement/optimizers/conductor/translation.py
new file mode 100644
index 0000000..d361755
--- /dev/null
+++ b/apps/placement/optimizers/conductor/translation.py
@@ -0,0 +1,283 @@
+# -------------------------------------------------------------------------
+# 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.
+#
+# -------------------------------------------------------------------------
+#
+import copy
+import json
+import yaml
+import re
+
+from osdf.utils.data_conversion import text_to_symbol
+from osdf.utils.programming_utils import dot_notation
+
+policy_config_mapping = yaml.safe_load(open('config/has_config.yaml')).get('policy_config_mapping')
+
+
+def get_opt_query_data(req_json, policies):
+ """
+ Fetch service and order specific details from the requestParameters field of a request.
+ :param req_json: a request file
+ :param policies: A set of policies
+ :return: A dictionary with service and order-specific attributes.
+ """
+ req_param_dict = {}
+ if 'requestParameters' in req_json["placementInfo"]:
+ req_params = req_json["placementInfo"]["requestParameters"]
+ for policy in policies:
+ for queryProp in policy['content']['queryProperties']:
+ attr_val = queryProp['value'] if 'value' in queryProp and queryProp['value'] != "" \
+ else dot_notation(req_params, queryProp['attribute_location'])
+ if attr_val is not None:
+ req_param_dict.update({queryProp['attribute']: attr_val})
+ return req_param_dict
+
+
+def gen_optimization_policy(vnf_list, optimization_policy):
+ """Generate optimization policy details to pass to Conductor
+ :param vnf_list: List of vnf's to used in placement request
+ :param optimization_policy: optimization objective policy information provided in the incoming request
+ :return: List of optimization objective policies in a format required by Conductor
+ """
+ optimization_policy_list = []
+ for policy in optimization_policy:
+ content = policy['content']
+ parameter_list = []
+ parameters = ["cloud_version", "hpa_score"]
+
+ for attr in content['objectiveParameter']['parameterAttributes']:
+ parameter = attr['parameter'] if attr['parameter'] in parameters else attr['parameter']+"_between"
+ vnfs = get_matching_vnfs(attr['resources'], vnf_list)
+ for vnf in vnfs:
+ value = [vnf] if attr['parameter'] in parameters else [attr['customerLocationInfo'], vnf]
+ parameter_list.append({
+ attr['operator']: [attr['weight'], {parameter: value}]
+ })
+
+ optimization_policy_list.append({
+ content['objective']: {content['objectiveParameter']['operator']: parameter_list }
+ })
+ return optimization_policy_list
+
+
+def get_matching_vnfs(resources, vnf_list, match_type="intersection"):
+ """Get a list of matching VNFs from the list of resources
+ :param resources:
+ :param vnf_list: List of vnfs to used in placement request
+ :param match_type: "intersection" or "all" or "any" (any => send all_vnfs if there is any intersection)
+ :return: List of matching VNFs
+ """
+ resources_lcase = [x.lower() for x in resources]
+ if match_type == "all": # don't bother with any comparisons
+ return resources if set(resources_lcase) <= set(vnf_list) else None
+ common_vnfs = set(vnf_list) & set(resources_lcase)
+ common_resources = [x for x in resources if x.lower() in common_vnfs]
+ if match_type == "intersection": # specifically requested intersection
+ return list(common_resources)
+ return resources if common_vnfs else None # "any" match => all resources to be returned
+
+
+def gen_policy_instance(vnf_list, resource_policy, match_type="intersection", rtype=None):
+ """Generate a list of policies
+ :param vnf_list: List of vnf's to used in placement request
+ :param resource_policy: policy for this specific resource
+ :param match_type: How to match the vnf_names with the vnf_list (intersection or "any")
+ intersection => return intersection; "any" implies return all vnf_names if intersection is not null
+ :param rtype: resource type (e.g. resourceRegionProperty or resourceInstanceProperty)
+ None => no controller information added to the policy specification to Conductor
+ :return: resource policy list in a format required by Conductor
+ """
+ resource_policy_list = []
+ related_policies = []
+ for policy in resource_policy:
+ pc = policy['content']
+ demands = get_matching_vnfs(pc['resources'], vnf_list, match_type=match_type)
+ resource = {pc['identity']: {'type': pc['policyType'], 'demands': demands}}
+
+ if rtype:
+ resource[pc['identity']]['properties'] = {'controller': pc[rtype]['controller'],
+ 'request': json.loads(pc[rtype]['request'])}
+ if demands and len(demands) != 0:
+ resource_policy_list.append(resource)
+ related_policies.append(policy)
+ return resource_policy_list, related_policies
+
+
+def gen_resource_instance_policy(vnf_list, resource_instance_policy):
+ """Get policies governing resource instances in order to populate the Conductor API call"""
+ cur_policies, _ = gen_policy_instance(vnf_list, resource_instance_policy, rtype='resourceInstanceProperty')
+ return cur_policies
+
+
+def gen_resource_region_policy(vnf_list, resource_region_policy):
+ """Get policies governing resource region in order to populate the Conductor API call"""
+ cur_policies, _ = gen_policy_instance(vnf_list, resource_region_policy, rtype='resourceRegionProperty')
+ return cur_policies
+
+
+def gen_inventory_group_policy(vnf_list, inventory_group_policy):
+ """Get policies governing inventory group in order to populate the Conductor API call"""
+ cur_policies, _ = gen_policy_instance(vnf_list, inventory_group_policy, rtype=None)
+ return cur_policies
+
+
+def gen_reservation_policy(vnf_list, reservation_policy):
+ """Get policies governing resource instances in order to populate the Conductor API call"""
+ cur_policies, _ = gen_policy_instance(vnf_list, reservation_policy, rtype='instanceReservationProperty')
+ return cur_policies
+
+
+def gen_distance_to_location_policy(vnf_list, distance_to_location_policy):
+ """Get policies governing distance-to-location for VNFs in order to populate the Conductor API call"""
+ cur_policies, related_policies = gen_policy_instance(vnf_list, distance_to_location_policy, rtype=None)
+ for p_new, p_main in zip(cur_policies, related_policies): # add additional fields to each policy
+ properties = p_main['content']['distanceProperties']
+ pcp_d = properties['distance']
+ p_new[p_main['content']['identity']]['properties'] = {
+ 'distance': pcp_d['operator'] + " " + pcp_d['value'].lower() + " " + pcp_d['unit'].lower(),
+ 'location': properties['locationInfo']
+ }
+ return cur_policies
+
+
+def gen_attribute_policy(vnf_list, attribute_policy):
+ """Get policies governing attributes of VNFs in order to populate the Conductor API call"""
+ cur_policies, related_policies = gen_policy_instance(vnf_list, attribute_policy, rtype=None)
+ for p_new, p_main in zip(cur_policies, related_policies): # add additional fields to each policy
+ properties = p_main['content']['cloudAttributeProperty']
+ attribute_mapping = policy_config_mapping['attributes'] # wanted attributes and mapping
+ p_new[p_main['content']['identity']]['properties'] = {
+ 'evaluate': dict((k, properties.get(attribute_mapping.get(k))) for k in attribute_mapping.keys())
+ }
+ return cur_policies # cur_policies gets updated in place...
+
+
+def gen_zone_policy(vnf_list, zone_policy):
+ """Get zone policies in order to populate the Conductor API call"""
+ cur_policies, related_policies = gen_policy_instance(vnf_list, zone_policy, match_type="all", rtype=None)
+ for p_new, p_main in zip(cur_policies, related_policies): # add additional fields to each policy
+ pmz = p_main['content']['affinityProperty']
+ p_new[p_main['content']['identity']]['properties'] = {'category': pmz['category'], 'qualifier': pmz['qualifier']}
+ return cur_policies
+
+
+def gen_capacity_policy(vnf_list, capacity_policy):
+ """Get zone policies in order to populate the Conductor API call"""
+ cur_policies, related_policies = gen_policy_instance(vnf_list, capacity_policy, rtype=None)
+ for p_new, p_main in zip(cur_policies, related_policies): # add additional fields to each policy
+ pmz = p_main['content']['capacityProperty']
+ p_new[p_main['content']['identity']]['properties'] = \
+ {"controller": pmz['controller'], 'request': json.loads(pmz['request'])}
+ return cur_policies
+
+
+def gen_hpa_policy(vnf_list, hpa_policy):
+ """Get zone policies in order to populate the Conductor API call"""
+ cur_policies, related_policies = gen_policy_instance(vnf_list, hpa_policy, rtype=None)
+ for p_new, p_main in zip(cur_policies, related_policies): # add additional fields to each policy
+ p_new[p_main['content']['identity']]['properties'] = {'evaluate': p_main['content']['flavorFeatures']}
+ return cur_policies
+
+
+def get_augmented_policy_attributes(policy_property, demand):
+ """Get policy attributes and augment them using policy_config_mapping and demand information"""
+ attributes = copy.copy(policy_property['attributes'])
+ remapping = policy_config_mapping['remapping']
+ extra = dict((x, demand['resourceModelInfo'][remapping[x]]) for x in attributes if x in remapping)
+ attributes.update(extra)
+ return attributes
+
+
+def get_candidates_demands(demand):
+ """Get demands related to candidates; e.g. excluded/required"""
+ res = {}
+ for k, v in policy_config_mapping['candidates'].items():
+ if k not in demand:
+ continue
+ res[v] = [{'inventory_type': x['identifierType'], 'candidate_id': x['identifiers']} for x in demand[k]]
+ return res
+
+
+def get_policy_properties(demand, policies):
+ """Get policy_properties for cases where there is a match with the demand"""
+ for policy in policies:
+ policy_demands = set([x.lower() for x in policy['content'].get('resources', [])])
+ if demand['resourceModuleName'].lower() not in policy_demands:
+ continue # no match for this policy
+ for policy_property in policy['content']['vnfProperties']:
+ yield policy_property
+
+
+def get_demand_properties(demand, policies):
+ """Get list demand properties objects (named tuples) from policy"""
+ demand_properties = []
+ for policy_property in get_policy_properties(demand, policies):
+ prop = dict(inventory_provider=policy_property['inventoryProvider'],
+ inventory_type=policy_property['inventoryType'],
+ service_type=demand['serviceResourceId'],
+ service_resource_id=demand['serviceResourceId'])
+
+ prop.update({'unique': policy_property['unique']} if 'unique' in policy_property and
+ policy_property['unique'] else {})
+ prop['attributes'] = dict()
+ prop['attributes'].update({'global-customer-id': policy_property['customerId']}
+ if policy_property['customerId'] else {})
+ prop['attributes'].update({'model-invariant-id': demand['resourceModelInfo']['modelInvariantId']}
+ if demand['resourceModelInfo']['modelInvariantId'] else {})
+ prop['attributes'].update({'model-version-id': demand['resourceModelInfo']['modelVersionId']}
+ if demand['resourceModelInfo']['modelVersionId'] else {})
+ prop['attributes'].update({'equipment-role': policy_property['equipmentRole']}
+ if policy_property['equipmentRole'] else {})
+
+ if policy_property.get('attributes'):
+ for attr_key, attr_val in policy_property['attributes'].items():
+ update_converted_attribute(attr_key, attr_val, prop)
+
+ prop.update(get_candidates_demands(demand))
+ demand_properties.append(prop)
+ return demand_properties
+
+
+def update_converted_attribute(attr_key, attr_val, properties):
+ """
+ Updates dictonary of attributes with one specified in the arguments.
+ Automatically translates key namr from camelCase to hyphens
+ :param attr_key: key of the attribute
+ :param attr_val: value of the attribute
+ :param properties: dictionary with attributes to update
+ :return:
+ """
+ if attr_val:
+ remapping = policy_config_mapping['attributes']
+ if remapping.get(attr_key):
+ key_value = remapping.get(attr_key)
+ else:
+ key_value = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', attr_key)
+ key_value = re.sub('([a-z0-9])([A-Z])', r'\1-\2', key_value).lower()
+ properties['attributes'].update({key_value: attr_val})
+
+
+def gen_demands(req_json, vnf_policies):
+ """Generate list of demands based on request and VNF policies
+ :param req_json: Request object from the client (e.g. MSO)
+ :param vnf_policies: Policies associated with demand resources (e.g. from grouped_policies['vnfPolicy'])
+ :return: list of demand parameters to populate the Conductor API call
+ """
+ demand_dictionary = {}
+ for demand in req_json['placementInfo']['placementDemands']:
+ prop = get_demand_properties(demand, vnf_policies)
+ if len(prop) > 0:
+ demand_dictionary.update({demand['resourceModuleName']: prop})
+ return demand_dictionary
diff --git a/apps/placement/templates/conductor_interface.json b/apps/placement/templates/conductor_interface.json
new file mode 100755
index 0000000..030d6a0
--- /dev/null
+++ b/apps/placement/templates/conductor_interface.json
@@ -0,0 +1,39 @@
+{
+ "name": "{{ name }}",
+ "files": {},
+ "timeout": {{ timeout }},
+ "num_solution": "{{ limit }}",
+ "template": {
+ "homing_template_version": "2017-10-10",
+ "parameters": {
+ {% set comma=joiner(",") %}
+ {% for key, value in request_params.items() %} {{ comma() }}
+ "{{key}}": {{ json.dumps(value) }}
+ {% endfor %}
+ },
+ "locations": {
+ "customer_loc": {
+ "latitude": { "get_param": "customer_lat" },
+ "longitude": { "get_param": "customer_long" }
+ }
+ },
+ "demands": {{ json.dumps(demand_list) }},
+ {% set comma_main = joiner(",") %}
+ "constraints": {
+ {% set comma=joiner(",") %}
+ {% for elem in policy_groups %} {{ comma() }}
+ {% for key, value in elem.items() %}
+ "{{key}}": {{ json.dumps(value) }}
+ {% endfor %}
+ {% endfor %}
+ },
+ "optimization": {
+ {% set comma=joiner(",") %}
+ {% for elem in optimization_policies %} {{ comma() }}
+ {% for key, value in elem.items() %}
+ "{{key}}": {{ json.dumps(value) }}
+ {% endfor %}
+ {% endfor %}
+ }
+ }
+}
diff --git a/apps/placement/templates/plc_opt_request.jsont b/apps/placement/templates/plc_opt_request.jsont
new file mode 100755
index 0000000..a218b8a
--- /dev/null
+++ b/apps/placement/templates/plc_opt_request.jsont
@@ -0,0 +1,142 @@
+{
+ "name": "{{ name }}",
+ "files": "{{ files }}",
+ "timeout": "{{ timeout }}",
+ "num_solution": "{{ limit }}",
+ "template": {
+ "CUST_ID": "{{ cust_id }}",
+ "E2EVPNKEY": "{{ e2evpnkey }}",
+ "UCPEHOST": "{{ ucpehost }}",
+ "WAN_PORT1_UP": "{{ wan_port1_up }}",
+ "WAN_PORT1_DOWN": "{{ wan_port1_down }}",
+ "EFFECTIVE_BANDWIDTH": "{{ effective_bandwidth }}",
+ "SERVICE_INST": "{{ service_inst }}",
+ "locations": {
+ "customer_loc": {
+ "host_name": "{{ ucpehost }}"
+ }
+ },
+ "demands": [
+ {% set comma=joiner(",") %}
+ {% for demand in demand_list %} {{ comma() }}
+ {
+ "{{ demand.vnf_name }}": [
+ {% set comma2=joiner(",") %}
+ {% for property in demand.property %}
+ "inventory_provider": {{ property.inventory_provider }},
+ "inventory_type": {{ property.inventory_type }},
+ "service_type": {{ property.service_type }},
+ "customer_id": {{ property.customer_id }},
+ "candidate_id": {{ property.candidate_id }}
+ {% endfor %}
+ ]
+ }
+ {% endfor %}
+ ],
+ "constraints": {
+ {% set comma_main=joiner(",") %}
+
+ {% if attribute_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for attribute in attribute_policy_list %} {{ comma() }}
+ attribute['identity'] : {
+ "type": {{ attribute['type'] }},
+ "demands": {{ attribute['demands'] }},
+ "properties": {
+ "evaluate": {
+ "hypervisor": {{ attribute['property']['hypervisor'] }},
+ "aic_version": {{ attribute['property']['aicVersion'] }},
+ "aic_type": {{ attribute['property']['aicType'] }},
+ "dataplane": {{ attribute['property']['datatype'] }},
+ "network_roles": {{ attribute['property']['networkRoles'] }},
+ "complex": {{ attribute['property']['complex'] }}
+ }
+ }
+ }
+ {% endfor %}
+
+ {% if distance_to_location_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for distance_location in distance_to_location_policy_list %} {{ comma() }}
+ distance_location['identity'] : {
+ "type": {{ distance_location['type'] }},
+ "demands": {{ distance_location['demands'] }},
+ "properties": {
+ "distance": {{ distance_location['property']['distance'] }},
+ "location": {{ distance_location['property']['location'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if inventory_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for inventory in inventory_policy_list %} {{ comma() }}
+ inventory['identity'] : {
+ "type": {{ inventory['type'] }},
+ "demands": {{ inventory['demands'] }}
+ }
+ {% endfor %}
+
+ {% if resource_instance_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for resource_instance in resource_instance_policy_list %} {{ comma() }}
+ resource_instance['identity'] : {
+ "type": {{ resource_instance['type'] }},
+ "demands": {{ resource_instance['demands'] }},
+ "properties": {
+ "controller": {{ resource_instance['property']['controller'] }},
+ "request": {{ resource_instance['property']['request'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if resource_region_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for resource_region in resource_region_policy_list %} {{ comma() }}
+ resource_region['identity'] : {
+ "type": {{ resource_region['type'] }},
+ "demands": {{ resource_region['demands'] }},
+ "properties": {
+ "controller": {{ resource_region['property']['controller'] }},
+ "request": {{ resource_region['property']['request'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if zone_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for zone in zone_policy_list %} {{ comma() }}
+ zone['identity'] : {
+ "type": {{ zone['type'] }},
+ "demands": {{ zone['demands'] }},
+ "properties": {
+ "qualifier": {{ resource_region['property']['qualifier'] }},
+ "category": {{ resource_region['property']['category'] }}
+ }
+ }
+ {% endfor %}
+
+ {% if optmization_policy_list %} {{ comma_main() }} {% endif %}
+ {% set comma=joiner(",") %}
+ {% for optimization in optimization_policy_list %} {{ comma() }}
+ "optimization" : {
+ {{ optimization['objective'] }}: {
+ "sum": [
+ {% set comma2=joiner(",") %}
+ {% for parameter in optimization['parameter'] %} {{ comma() }}
+ {
+ "product": [
+ {{ parameter['weight'] }},
+ {
+ "distance_between": [{{ parameter['customerLocation'] }},{{ parameter['demand'] }}]
+ }
+ ]
+ }
+ {% endfor %}
+ ]
+ }
+ }
+ {% endfor %}
+ }
+ }
+}
diff --git a/apps/placement/templates/plc_opt_response.jsont b/apps/placement/templates/plc_opt_response.jsont
new file mode 100755
index 0000000..e5709e7
--- /dev/null
+++ b/apps/placement/templates/plc_opt_response.jsont
@@ -0,0 +1,10 @@
+{
+ "requestId": "{{requestId}}",
+ "transactionId": "{{transacationId}}",
+ "requestStatus": "{{requestStatus}}",
+ "statusMessage": "{{statusMessage}}"
+ "solutions": {
+ "placementSolutions": {{ json.dumps(composite_solutions) }},
+ "licenseSolutions":{{ json.dumps(license_solutions) }}
+ }
+}
diff --git a/apps/placement/templates/policy_request.jsont b/apps/placement/templates/policy_request.jsont
new file mode 100755
index 0000000..3a9e201
--- /dev/null
+++ b/apps/placement/templates/policy_request.jsont
@@ -0,0 +1,3 @@
+{
+ "policyName": "{{policy_name}}" {# we currently only support query by policy name only -- policyName #}
+}
diff --git a/apps/route/__init__.py b/apps/route/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/route/__init__.py
diff --git a/apps/route/optimizers/__init__.py b/apps/route/optimizers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/route/optimizers/__init__.py
diff --git a/apps/route/optimizers/simple_route_opt.py b/apps/route/optimizers/simple_route_opt.py
new file mode 100644
index 0000000..b00180d
--- /dev/null
+++ b/apps/route/optimizers/simple_route_opt.py
@@ -0,0 +1,153 @@
+# -------------------------------------------------------------------------
+# Copyright (c) 2018 Huawei Intellectual Property
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+import requests
+from requests.auth import HTTPBasicAuth
+
+from osdf.utils.mdc_utils import mdc_from_json
+
+
+class RouteOpt:
+
+ """
+ This values will need to deleted..
+ only added for the debug purpose
+ """
+ # DNS server and standard port of AAI..
+ # TODO: read the port from the configuration and add to DNS
+ aai_host = "https://aai.api.simpledemo.onap.org:8443"
+ aai_headers = {
+ "X-TransactionId": "9999",
+ "X-FromAppId": "OOF",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "Real-Time": "true"
+ }
+
+ def isCrossONAPLink(self, logical_link):
+ """
+ This method checks if cross link is cross onap
+ :param logical_link:
+ :return:
+ """
+ for relationship in logical_link["relationship-list"]["relationship"]:
+ if relationship["related-to"] == "ext-aai-network":
+ return True
+ return False
+
+ def getRoute(self, request):
+ """
+ This method checks
+ :param logical_link:
+ :return:
+ """
+ mdc_from_json(request)
+
+ src_access_node_id = request["srcPort"]["src-access-node-id"]
+ dst_access_node_id = request["dstPort"]["dst-access-node-id"]
+
+
+ ingress_p_interface = None
+ egress_p_interface = None
+
+ # for the case of request_json for same domain, return the same node with destination update
+ if src_access_node_id == dst_access_node_id:
+ data = '{'\
+ '"vpns":['\
+ '{'\
+ '"access-topology-id": "' + request["srcPort"]["src-access-topology-id"] + '",'\
+ '"access-client-id": "' + request["srcPort"]["src-access-client-id"] + '",'\
+ '"access-provider-id": "' + request["srcPort"]["src-access-provider-id"]+ '",'\
+ '"access-node-id": "' + request["srcPort"]["src-access-node-id"]+ '",'\
+ '"src-access-ltp-id": "' + request["srcPort"]["src-access-ltp-id"]+ '",'\
+ '"dst-access-ltp-id": "' + request["dstPort"]["dst-access-ltp-id"] +'"'\
+ '}'\
+ ']'\
+ '}'
+ return data
+ else:
+ logical_links = self.get_logical_links()
+
+ # take the logical link where both the p-interface in same onap
+ if logical_links != None:
+ for logical_link in logical_links.get("logical-link"):
+ if not self.isCrossONAPLink(logical_link):
+ # link is in local ONAP
+ for relationship in logical_link["relationship-list"]["relationship"]:
+ if relationship["related-to"] == "p-interface":
+ if src_access_node_id in relationship["related-link"]:
+ i_interface = relationship["related-link"].split("/")[-1]
+ ingress_p_interface = i_interface.split("-")[-1]
+ if dst_access_node_id in relationship["related-link"]:
+ e_interface = relationship["related-link"].split("/")[-1]
+ egress_p_interface = e_interface.split("-")[-1]
+ data = '{'\
+ '"vpns":['\
+ '{'\
+ '"access-topology-id": "' + request["srcPort"]["src-access-topology-id"] + '",'\
+ '"access-client-id": "' + request["srcPort"]["src-access-client-id"] + '",'\
+ '"access-provider-id": "' + request["srcPort"]["src-access-provider-id"]+ '",'\
+ '"access-node-id": "' + request["srcPort"]["src-access-node-id"]+ '",'\
+ '"src-access-ltp-id": "' + request["srcPort"]["src-access-ltp-id"]+ '",'\
+ '"dst-access-ltp-id": "' + ingress_p_interface +'"'\
+ '},'\
+ '{' \
+ '"access-topology-id": "' + request["dstPort"]["dst-access-topology-id"] + '",' \
+ '"access-topology-id": "' + request["dstPort"]["dst-access-topology-id"]+ '",' \
+ '"access-provider-id": "' + request["dstPort"]["dst-access-provider-id"]+ '",' \
+ '"access-node-id": "' + request["dstPort"]["dst-access-node-id"]+ '",' \
+ '"src-access-ltp-id": "' + egress_p_interface + '",' \
+ '"dst-access-ltp-id": "' + request["dstPort"]["dst-access-ltp-id"] + '"' \
+ '}'\
+ ']'\
+ '}'
+ return data
+
+
+ def get_pinterface(self, url):
+ """
+ This method returns details for p interface
+ :return: details of p interface
+ """
+ aai_req_url = self.aai_host + url
+ 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_logical_links(self):
+ """
+ This method returns list of all cross ONAP links
+ from /aai/v14/network/logical-links?operation-status="Up"
+ :return: logical-links[]
+ """
+ logical_link_url = "/aai/v13/network/logical-links?operational-status=up"
+ aai_req_url = self.aai_host + logical_link_url
+
+ response = requests.get(aai_req_url,
+ headers=self.aai_headers,
+ auth=HTTPBasicAuth("AAI", "AAI"),
+ verify=False)
+
+ logical_links = None
+ if response.status_code == 200:
+ return response.json() \ No newline at end of file
diff --git a/apps/templates/cms_opt_request.jsont b/apps/templates/cms_opt_request.jsont
new file mode 100755
index 0000000..006562b
--- /dev/null
+++ b/apps/templates/cms_opt_request.jsont
@@ -0,0 +1,35 @@
+{
+ "transaction_id": "{{ transaction_id }}",
+ "request_id": "{{ request_id }}",
+ "start_date" : "{{ start_time }}",
+ "end_date" : "{{ end_time }}",
+ "change_elements" : {{ json.dumps(change_elements) }},
+ "constraints" : [
+ {
+ "type" : "general_concurrency_limit",
+ "parameters": [{{ concurrency_limit }}]
+ },
+
+ {
+ "type" : "allowed_forbidden_periods",
+ "parameters" : {{ json.dumps(allowed_periods) }}
+ }
+
+ {% if spatial_conflicts is defined and spatial_conflicts|length > 0 %}
+ ,
+ {
+ "type" : "spatial_conflict",
+ "parameters": {{ json.dumps(spatial_conflicts) }}
+ }
+ {% endif %}
+
+
+ {% if critical_periods is defined and spatial_conflicts|length > 0 %}
+ ,
+ {
+ "type" : "critical_periods",
+ "parameters": {{ json.dumps(critical_periods) }}
+ }
+ {% endif %}
+ ]
+}
diff --git a/apps/templates/cms_opt_request.jsont_1707_v1 b/apps/templates/cms_opt_request.jsont_1707_v1
new file mode 100755
index 0000000..75ecbe5
--- /dev/null
+++ b/apps/templates/cms_opt_request.jsont_1707_v1
@@ -0,0 +1,67 @@
+{
+ "transaction_id": "{{ transaction_id }}",
+ "request_id": "{{ request_id }}",
+ "start_date" : "{{ start_time }}",
+ "end_date" : "{{ end_time }}",
+
+ "change_elements" : [
+ {% set comma = joiner(",") -%}
+ {% for element in all_upgrades -%} {{ comma() }}
+ {
+ "id" : "{{ element.id }}",
+ "failback_duration": {{ element.failback_duration }},
+ {% if element.group_id -%}
+ "group_id": "{{ element.group_id }}",
+ {% endif %}
+ {% if element.scheduled_on -%}
+ "scheduled_on": "{{ element.scheduled_on }}",
+ {% endif %}
+ "duration": {{ element.duration }}
+ }
+ {% endfor -%}
+ ],
+
+ "constraints" : [
+ {
+ "type" : "general_concurrency_limit",
+ "parameters": [{{ concurrency_limit }}]
+ },
+
+ {
+ "type" : "allowed_forbidden_periods",
+ "parameters" : [
+ {% set comma = joiner(",") -%}
+ {% for idx in all_pending -%} {{ comma() }}
+ { "id" : "{{ idx.id }}",
+ "allowed_periods": [ {{ allowed_periods }}]
+ }
+ {% endfor -%}
+ ]
+ },
+ {
+ "type" : "spatial_conflict",
+ "parameters": [
+ {% set comma = joiner(",") -%}
+ {% for pserver, vce_list in vce_pserver_mapping.items() -%} {{ comma() }}
+ {
+ "spatial_entity": "{{ pserver }}",
+ "affected_entities": {{ vce_list }}
+ }
+ {% endfor -%}
+ ]
+ },
+
+ {
+ "type" : "critical_periods",
+ "parameters": [
+ {% set comma = joiner(",") -%}
+ {% for element, conflict_period in conflict_interval.items() -%} {{ comma() }}
+ {
+ "id" : "{{ element }}",
+ "periods": [{{ conflict_period }}]
+ }
+ {% endfor -%}
+ ]
+ }
+ ]
+}
diff --git a/apps/templates/cms_opt_request_1702.jsont b/apps/templates/cms_opt_request_1702.jsont
new file mode 100755
index 0000000..bcafa45
--- /dev/null
+++ b/apps/templates/cms_opt_request_1702.jsont
@@ -0,0 +1,63 @@
+{
+ "request_id": "{{ request_id }}",
+ "startdate" : "{{ start_time }}",
+ "enddate" : "{{ end_time }}",
+
+ "change_elements" : [
+{% set comma = joiner(",") -%}
+{% for element in all_upgrades -%} {{ comma() }}
+ { "id" : "{{ element.id }}",
+ {% if element.scheduled -%} "scheduled_on": "{{ element.scheduled }}", {% endif -%}
+ "duration": {{ element.duration }}, {# duration in seconds #}
+ "failback_duration": {{ element.failback_duration }}, {# duration in seconds #}
+ "group_id": {{ element.group_id }}, {# duration in seconds #}
+ }{% endfor -%}
+ ],
+
+ "constraints" : [
+ {
+ "type" : "general_concurrency_limit",
+ "parameters" : [ {{ general_concurrency_limit }} ]
+ },
+
+ {
+ "type" : "allowed_forbidden_periods",
+ "parameters" : [
+{% set comma = joiner(",") -%}
+{% for idx in all_pending -%} {{ comma() }}
+ { "id" : "{{ idx.id }}",
+ "allowed_periods": [ {% set comma2 = joiner(",") -%}
+ {% for period in allowed_periods -%} {{ comma2() }} [{{ json.dumps(period[0]) }}, {{ json.dumps(period[1]) }}]
+ {% endfor -%} ] }{% endfor -%}
+ ]
+ }
+
+{% if p_v_conflict is defined and p_v_conflict|length > 0 %}
+ ,
+ {
+ "type" : "critical_periods",
+ "description" : "Simultaneous upgrades",
+ "parameters" : [
+{% set comma2 = joiner(",") -%}
+{% for element in p_v_conflict -%} {{ comma2() }}
+ {
+ "id" : "{{ element[0] }}",
+ "periods" : [{{ json.dumps(element[0]) }}, {{ json.dumps(element[1]) }}]
+ }
+{% endfor -%}
+{% endif %}
+
+{% for pserver, vce_group in grouped_vces.items() -%} {{ comma() }}
+ ,
+ {
+ "id" : "{{ pserver }}",
+ "name" : "VCE's on pserver {{ pserver }}",
+ "description": "Only some VCEs on a pserver can be upgraded at a time",
+ "max_num_upgrades" : {{ max_num_upgrades(vce_group) }},
+ "upgrades" : {{ json.dumps(vce_group) }}
+ }
+{% endfor -%}
+ ]
+ }
+ ]
+}
diff --git a/apps/templates/cms_opt_response.jsont b/apps/templates/cms_opt_response.jsont
new file mode 100644
index 0000000..a8817df
--- /dev/null
+++ b/apps/templates/cms_opt_response.jsont
@@ -0,0 +1,8 @@
+{
+ "transactionId": "{{transaction_id}}",
+ "scheduleId":"{{schedule_id}}",
+ "requestState": "{{request_state}}",
+ "status": "{{status}}",
+ "description": "{{description}}",
+ "schedule": {{schedule}}
+} \ No newline at end of file
diff --git a/apps/templates/license_opt_request.jsont b/apps/templates/license_opt_request.jsont
new file mode 100644
index 0000000..7baa759
--- /dev/null
+++ b/apps/templates/license_opt_request.jsont
@@ -0,0 +1,6 @@
+{
+ "transactionId": "{{transaction_id}}",
+ "requestId": "{{request_id}}",
+ "partNumber": "{{part_number}}",
+ "licenseModel" : "{{artifact}}"
+} \ No newline at end of file
diff --git a/apps/templates/test_cms_nb_req_from_client.jsont b/apps/templates/test_cms_nb_req_from_client.jsont
new file mode 100755
index 0000000..a60c8ff
--- /dev/null
+++ b/apps/templates/test_cms_nb_req_from_client.jsont
@@ -0,0 +1,19 @@
+{
+ "schedulingInfo": {
+ "change_management_id": "{{ change_management_id }}",
+ "start_time": "{{ start_time }}",
+ "end_time": "{{ end_time }}",
+ "policy_id": {{ json.dumps(policy_id) }}, {# a list of policy Ids #}
+ "service_type": "{{ service_type }}",
+ "workflow_type": "{{ workflow_type }}",
+ "upgrades": {{ json.dumps(upgrades) }} {# a list of node Ids #}
+ },
+ "requestInfo": {
+ "requestId": "{{ requestId }}",
+ "sourceId": "{{ sourceId }}",
+ "optimizer": "{{ optimizer }}",
+ "numSolutions": "{{ numSolutions }}",
+ "callbackUrl" : "{{ callbackUrl }}"
+ }
+}
+
diff --git a/apps/templates/test_plc_nb_req_from_client.jsont b/apps/templates/test_plc_nb_req_from_client.jsont
new file mode 100755
index 0000000..998ffb3
--- /dev/null
+++ b/apps/templates/test_plc_nb_req_from_client.jsont
@@ -0,0 +1,52 @@
+{
+ "requestInfo": {
+ "requestId": "{{requestId}}",
+ "sourceId": "{{sourceId}}",
+ "optimizer": "{{optimizer}}",
+ "numSolutions": {{numSolutions}},
+ "timeout": {{timeout}},
+ "callbackUrl" : "{{callbackUrl}}"
+ },
+ "placementInfo": {
+ "modelInfo": {
+ "modelType": "{{modelType}}",
+ "modelInvariant": "{{modelInvariantId}}",
+ "modelVersionId": "{{modelVersionId}}",
+ "modelName": "{{modelName}}",
+ "modelVersion": "{{modelVersion}}",
+ "modelCustomizationId": "{{modelCustomizationId}}"
+ },
+ "subscriberInfo": {
+ "globalSubscriberId": "{{globalSubscriberId}}",
+ "subscriberName": "{{subscriberName}}",
+ "subscriberCommonSiteId": "{{subscriberCommonSiteId}}",
+ "ucpeHostName": "{{ucpeHostName}}"
+ },
+ "policyId": {{json.dumps(policyId)}},
+ "vnfInfo": {
+ "vnfType": "{{vnfType}}",
+ "vnfPartNumber": "{{vnfPartNumber}}",
+ "nominalThroughput": "{{nominalThroughput}}",
+ "vnfSoftwareVersion": "{{vnfSoftwareVersion}}",
+ "vnfManagementOption": "{{vnfManagementOption}}"
+ },
+ "vpnInfo": {
+ "vpnId": "{{vpnId}}",
+ "pvcId": "{{pvcId}}"
+ },
+ "serviceInfo": {
+ "dhvServiceInfo": {
+ "serviceInstanceId": "{{serviceInstanceId}}",
+ "serviceType": "{{serviceType}}",
+ "e2evpnkey": "{{e2evpnkey}}",
+ "dhvSiteEffectiveTransportBandwidth": {{dhvSiteEffectiveTransportBandwidth}},
+ "dhvIPSecTransportBandwidthUp": {{dhvIPSecTransportBandwidthUp}},
+ "dhvIPSecTransportBandwidthDown": {{dhvIPSecTransportBandwidthDown}},
+ "dhvIPSec2TransportBandwidthUp": {{dhvIPSec2TransportBandwidthUp}},
+ "dhvIPSec2TransportBandwidthDown": {{dhvIPSec2TransportBandwidthDown}},
+ "dhvVendorName": "{{dhvVendorName}}"
+ }
+ },
+ "demandInfo": {{json.dumps(demandInfo)}}
+ }
+}