Add functionality to support NSI selection 35/104135/1
authorkrishnaa96 <krishna.moorthy6@wipro.com>
Mon, 23 Mar 2020 07:41:07 +0000 (13:11 +0530)
committerkrishnaa96 <krishna.moorthy6@wipro.com>
Mon, 23 Mar 2020 07:46:31 +0000 (13:16 +0530)
Issue-ID: OPTFRA-677
Signed-off-by: krishnaa96 <krishna.moorthy6@wipro.com>
Change-Id: Ibc51e15fce4692a445df400053060d3a6977b4ce

30 files changed:
apps/placement/optimizers/conductor/remote_opt_processor.py
apps/slice_selection/__init__.py [new file with mode: 0644]
apps/slice_selection/models/api/__init__.py [new file with mode: 0644]
apps/slice_selection/models/api/nsi_selection_request.py [new file with mode: 0644]
apps/slice_selection/models/api/nsi_selection_response.py [new file with mode: 0644]
apps/slice_selection/optimizers/__init__.py [new file with mode: 0644]
apps/slice_selection/optimizers/conductor/__init__.py [new file with mode: 0644]
apps/slice_selection/optimizers/conductor/remote_opt_processor.py [new file with mode: 0644]
apps/slice_selection/optimizers/conductor/response_processor.py [new file with mode: 0644]
config/common_config.yaml
osdf/adapters/conductor/api_builder.py
osdf/adapters/conductor/conductor.py
osdf/adapters/conductor/templates/conductor_interface.json
osdf/adapters/conductor/translation.py
osdfapp.py
test/apps/slice_selection/conductor_error_response.json [new file with mode: 0644]
test/apps/slice_selection/new_solution_conductor_response.json [new file with mode: 0644]
test/apps/slice_selection/new_solution_nsi_response.json [new file with mode: 0644]
test/apps/slice_selection/nsi_error_response.json [new file with mode: 0644]
test/apps/slice_selection/nsi_request.json [new file with mode: 0644]
test/apps/slice_selection/shared_solution_conductor_response.json [new file with mode: 0644]
test/apps/slice_selection/shared_solution_nsi_response.json [new file with mode: 0644]
test/apps/slice_selection/slice_policies.txt [new file with mode: 0644]
test/apps/slice_selection/test_remote_opt_processor.py [new file with mode: 0644]
test/conductor/test_conductor_calls.py
test/policy-local-files/subscriber_policy_URLLC_1.json [new file with mode: 0644]
test/policy-local-files/thresholdPolicy_URLLC_Core_1_latency.json [new file with mode: 0644]
test/policy-local-files/thresholdPolicy_URLLC_Core_1_reliability.json [new file with mode: 0644]
test/policy-local-files/vnfPolicy_URLLC_Core_1.json [new file with mode: 0644]
test/test_ConductorApiBuilder.py

index 0b5cb16..3d7a287 100644 (file)
@@ -134,8 +134,8 @@ def process_placement_opt(request_json, policies, osdf_config):
             demands = request_json['placementInfo']['placementDemands']
             request_parameters = request_json['placementInfo']['requestParameters']
             service_info = request_json['serviceInfo']
-            resp = conductor.request(req_info, demands, request_parameters, service_info,
-                                               osdf_config, policies)
+            resp = conductor.request(req_info, demands, request_parameters, service_info, True,
+                                     osdf_config, policies)
             if resp["plans"][0].get("recommendations"):
                 placement_response = conductor_response_processor(resp, req_id, transaction_id)
             else:  # "solved" but no solutions found
diff --git a/apps/slice_selection/__init__.py b/apps/slice_selection/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/slice_selection/models/api/__init__.py b/apps/slice_selection/models/api/__init__.py
new file mode 100644 (file)
index 0000000..b45f74d
--- /dev/null
@@ -0,0 +1,17 @@
+# -------------------------------------------------------------------------
+#   Copyright (C) 2020 Wipro Limited.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
diff --git a/apps/slice_selection/models/api/nsi_selection_request.py b/apps/slice_selection/models/api/nsi_selection_request.py
new file mode 100644 (file)
index 0000000..b7f3fbd
--- /dev/null
@@ -0,0 +1,54 @@
+# -------------------------------------------------------------------------
+#   Copyright (C) 2020 Wipro Limited.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType, StringType, 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)
+    timeout = IntType()
+
+
+class NSTInfo(OSDFModel):
+    """Preferred candidate for a resource (sent as part of a request from client)"""
+    modelInvariantId = StringType(required=True)
+    modelVersionId = StringType(required=True)
+    modelName = StringType()
+    modelType = StringType()
+    modelVersion = StringType()
+    modelCustomizationName = StringType()
+
+
+class ServiceInfo(OSDFModel):
+    serviceInstanceId = StringType(required=True)
+    serviceName = StringType(required=True)
+
+
+class NSISelectionAPI(OSDFModel):
+    """Request for nsi selection (specific to optimization and additional metadata"""
+    requestInfo = ModelType(RequestInfo, required=True)
+    NSTInfoList = ListType(ModelType(NSTInfo), required=True)
+    serviceInfo = ModelType(ServiceInfo, required=True)
+    serviceProfile = DictType(BaseType, required=True)
diff --git a/apps/slice_selection/models/api/nsi_selection_response.py b/apps/slice_selection/models/api/nsi_selection_response.py
new file mode 100644 (file)
index 0000000..9547200
--- /dev/null
@@ -0,0 +1,73 @@
+# -------------------------------------------------------------------------
+#   Copyright (C) 2020 Wipro Limited.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+from osdf.models.api.common import OSDFModel
+from schematics.types import BaseType, StringType
+from schematics.types.compound import ModelType, ListType, DictType
+
+
+# TODO: update osdf.models
+class SharedNSISolution(OSDFModel):
+    invariantUUID = StringType(required=True)
+    UUID = StringType(required=True)
+    NSIName = StringType(required=True)
+    NSIId = StringType(required=True)
+    matchLevel = StringType(required=True)
+
+
+class NSSTInfo(OSDFModel):
+    invariantUUID = StringType(required=True)
+    UUID = StringType(required=True)
+    NSSTName = StringType(required=True)
+
+
+class NSSIInfo(OSDFModel):
+    NSSIName = StringType(required=True)
+    NSSIId = StringType(required=True)
+    matchLevel = StringType(required=True)
+
+
+class NSSISolution(OSDFModel):
+    sliceProfile = DictType(BaseType)
+    NSSTInfo = ModelType(NSSTInfo, required=True)
+    NSSISolution = ModelType(NSSIInfo, required=True)
+
+
+class NSTInfo(OSDFModel):
+    invariantUUID = StringType(required=True)
+    UUID = StringType(required=True)
+    NSTName = StringType(required=True)
+
+
+class NewNSISolution(OSDFModel):
+    matchLevel = StringType(required=True)
+    NSTInfo = ModelType(NSTInfo, required=True)
+    NSSISolutions = ListType(ModelType(NSSISolution))
+
+
+class Solution(OSDFModel):
+    sharedNSISolutions = ListType(ModelType(SharedNSISolution))
+    newNSISolutions = ListType(ModelType(NewNSISolution))
+
+
+class NSISelectionResponse(OSDFModel):
+    transactionId = StringType(required=True)
+    requestId = StringType(required=True)
+    requestStatus = StringType(required=True)
+    statusMessage = StringType()
+    solutions = ModelType(Solution, required=True)
diff --git a/apps/slice_selection/optimizers/__init__.py b/apps/slice_selection/optimizers/__init__.py
new file mode 100644 (file)
index 0000000..b45f74d
--- /dev/null
@@ -0,0 +1,17 @@
+# -------------------------------------------------------------------------
+#   Copyright (C) 2020 Wipro Limited.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
diff --git a/apps/slice_selection/optimizers/conductor/__init__.py b/apps/slice_selection/optimizers/conductor/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/apps/slice_selection/optimizers/conductor/remote_opt_processor.py b/apps/slice_selection/optimizers/conductor/remote_opt_processor.py
new file mode 100644 (file)
index 0000000..a44fc4e
--- /dev/null
@@ -0,0 +1,104 @@
+# -------------------------------------------------------------------------
+#   Copyright (C) 2020 Wipro Limited.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+"""
+Module for processing slice selection request
+"""
+
+import json
+import traceback
+from requests import RequestException
+
+from apps.slice_selection.optimizers.conductor.response_processor \
+    import conductor_response_processor, conductor_error_response_processor
+from osdf.adapters.conductor import conductor
+from osdf.adapters.policy.interface import get_policies
+from osdf.adapters.policy.utils import group_policies_gen
+from osdf.logging.osdf_logging import error_log, debug_log
+from osdf.utils.mdc_utils import mdc_from_json
+
+
+def process_nsi_selection_opt(request_json, osdf_config):
+    """Process the nsi selection request from API layer
+        :param request_json: api request
+        :param policies: flattened policies corresponding to this request
+        :param osdf_config: configuration specific to OSDF app
+        :return: response as a dictionary
+        """
+    req_info = request_json['requestInfo']
+    try:
+        mdc_from_json(request_json)
+
+        overall_recommendations = dict()
+        nst_info_map = dict()
+        for nst_info in request_json["NSTInfoList"]:
+            nst_name = nst_info["modelName"]
+            nst_info_map["nst_name"] = {"NSTName": nst_name,
+                                        "UUID": nst_info["modelVersionId"],
+                                        "invariantUUID": nst_info["modelInvariantId"]}
+
+            policy_request_json = request_json.copy()
+            policy_request_json['serviceInfo']['serviceName'] = nst_name
+
+            policies = get_policies(policy_request_json, "slice_selection")
+
+            demands = get_slice_demands(nst_name, policies, osdf_config.core)
+
+            request_parameters = {}
+            service_info = {}
+            req_info['numSolutions'] = 'all'
+            resp = conductor.request(req_info, demands, request_parameters, service_info, False,
+                                     osdf_config, policies)
+            debug_log.debug("Response from conductor {}".format(str(resp)))
+            overall_recommendations[nst_name] = resp["plans"][0].get("recommendations")
+
+        return conductor_response_processor(overall_recommendations, nst_info_map, req_info)
+
+    except Exception as ex:
+        error_log.error("Error for {} {}".format(req_info.get('requestId'),
+                                                 traceback.format_exc()))
+        if isinstance(ex, RequestException):
+            try:
+                error_message = json.loads(ex.response)['plans'][0]['message']
+            except Exception:
+                error_message = "Problem connecting to conductor"
+        else:
+            error_message = str(ex)
+        return conductor_error_response_processor(req_info, error_message)
+
+
+def get_slice_demands(model_name, policies, config):
+    """
+    :param model_name: model name of the slice
+    :param policies: flattened polcies corresponding to the request
+    :param config: configuration specific to OSDF app
+    :return: list of demands for the request
+    """
+    group_policies = group_policies_gen(policies, config)
+    subscriber_policy_list = group_policies["onap.policies.optimization.SubscriberPolicy"]
+    slice_demands = list()
+    for subscriber_policy in subscriber_policy_list:
+        policy_properties = subscriber_policy[list(subscriber_policy.keys())[0]]['properties']
+        if model_name in policy_properties["services"]:
+            subnet_attributes = policy_properties["properties"]["subscriberRole"][0]
+            for subnet in policy_properties["properties"]["subscriberName"]:
+                slice_demand = dict()
+                slice_demand["resourceModuleName"] = subnet
+                slice_demand['resourceModelInfo'] = subnet_attributes[subnet]
+                slice_demands.append(slice_demand)
+    return slice_demands
diff --git a/apps/slice_selection/optimizers/conductor/response_processor.py b/apps/slice_selection/optimizers/conductor/response_processor.py
new file mode 100644 (file)
index 0000000..5b7be01
--- /dev/null
@@ -0,0 +1,123 @@
+# -------------------------------------------------------------------------
+#   Copyright (C) 2020 Wipro Limited.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+"""
+Module for processing response from conductor for slice selection
+"""
+
+from osdf.logging.osdf_logging import debug_log
+
+
+SLICE_PROFILE_FIELDS = ["latency", "max_number_of_ues", "coverage_area_ta_list",
+                        "ue_mobility_level", "resource_sharing_level", "exp_data_rate_ul",
+                        "exp_data_rate_dl", "area_traffic_cap_ul", "area_traffic_cap_dl",
+                        "activity_factor", "e2e_latency", "jitter", "survival_time",
+                        "exp_data_rate", "payload_size", "traffic_density", "conn_density",
+                        "reliability", "service_area_dimension", "cs_availability"]
+
+
+def conductor_response_processor(overall_recommendations, nst_info_map, request_info):
+    """Process conductor response to form the response for the API request
+        :param overall_recommendations: recommendations from conductor
+        :param nst_info_map: NST info from the request
+        :param request_info: request info
+        :return: response json as a dictionary
+    """
+    shared_nsi_solutions = list()
+    new_nsi_solutions = list()
+
+    for nst_name, recommendations in overall_recommendations.items():
+        for recommendation in recommendations:
+            nsi_set = set(values['candidate']['nsi_name'] for key, values in recommendation.items())
+            if len(nsi_set) == 1:
+                nsi = nsi_set.pop()
+                debug_log.debug("The NSSIs in the solution belongs to the same NSI {}".format(nsi))
+                shared_nsi_solution = dict()
+                shared_nsi_solution["NSIName"] = nsi
+                shared_nsi_solutions.append(shared_nsi_solution)
+            else:
+                nssi_solutions = get_nssi_solutions(recommendation)
+                new_nsi_solution = dict()
+                new_nsi_solution['matchLevel'] = ""
+                new_nsi_solution['NSTInfo'] = nst_info_map.get(nst_name)
+                new_nsi_solution['NSSISolutions'] = nssi_solutions
+                new_nsi_solutions.append(new_nsi_solution)
+
+    solutions = dict()
+    solutions['sharedNSISolutions'] = shared_nsi_solutions
+    solutions['newNSISolutions'] = new_nsi_solutions
+    return get_nsi_selection_response(request_info, solutions)
+
+
+def conductor_error_response_processor(request_info, error_message):
+    """Form response message from the error message
+        :param request_info: request info
+        :param error_message: error message while processing the request
+        :return: response json as dictionary
+    """
+    return {'requestId': request_info['requestId'],
+            'transactionId': request_info['transactionId'],
+            'requestStatus': 'error',
+            'statusMessage': error_message}
+
+
+def get_nssi_solutions(recommendation):
+    """Get nssi solutions from recommendation
+        :param recommendation: recommendation from conductor
+        :return: new nssi solutions list
+    """
+    nssi_solutions = list()
+
+    for nsst_name, nsst_rec in recommendation.items():
+        candidate = nsst_rec['candidate']
+        nssi_info, slice_profile = get_solution_from_candidate(candidate)
+        nsst_info = {"NSSTName": nsst_name}
+        nssi_solution = {"sliceProfile": slice_profile,
+                         "NSSTInfo": nsst_info,
+                         "NSSISolution": nssi_info}
+        nssi_solutions.append(nssi_solution)
+    return nssi_solutions
+
+
+def get_solution_from_candidate(candidate):
+    """Get nssi info from candidate
+        :param candidate: Candidate from the recommendation
+        :return: nssi_info and slice profile derived from candidate
+    """
+    slice_profile = dict()
+    nssi_info = {"NSSIName": candidate['instance_name'],
+                 "NSSIId": candidate['candidate_id']}
+
+    for field in SLICE_PROFILE_FIELDS:
+        if candidate[field]:
+            slice_profile[field] = candidate[field]
+
+    return nssi_info, slice_profile
+
+
+def get_nsi_selection_response(request_info, solutions):
+    """Get NSI selection response from final solution
+        :param request_info: request info
+        :param solutions: final solutions
+        :return: NSI selection response to send back as dictionary
+    """
+    return {'requestId': request_info['requestId'],
+            'transactionId': request_info['transactionId'],
+            'requestStatus': 'completed',
+            'statusMessage': '',
+            'solutions': solutions}
index d9ecc18..04a5594 100644 (file)
@@ -11,6 +11,7 @@ osdf_temp:  # special configuration required for "workarounds" or testing
     local_policies:
         global_disabled: True
         local_placement_policies_enabled: True
+        local_slice_selection_policies_enabled: True
         placement_policy_dir_vcpe: "./test/policy-local-files/"
         placement_policy_files_vcpe: # workaroud for policy platform glitches (or "work-arounds" for other components)
             - Affinity_vCPE_1.json
@@ -39,6 +40,14 @@ osdf_temp:  # special configuration required for "workarounds" or testing
             - vnfPolicy_vPGN_TD.json
             - affinity_vFW_TD.json
             - QueryPolicy_vFW_TD.json
+
+        slice_selection_policy_dir_urllc_1: "./test/policy-local-files/"
+        slice_selection_policy_files_urllc_1:
+            - vnfPolicy_URLLC_Core_1.json
+            - thresholdPolicy_URLLC_Core_1_reliability.json
+            - thresholdPolicy_URLLC_Core_1_latency.json
+            - subscriber_policy_URLLC_1.json
+
 service_info:
     vCPE:
         vcpeHostName: requestParameters.vcpeHostName
@@ -68,6 +77,15 @@ policy_info:
         service_name:
             - properties.services
 
+    slice_selection:
+        policy_fetch: by_scope
+        policy_scope:
+            -
+                scope:
+                  - OSDF_FRANKFURT
+                service:
+                    - get_param: service_name
+
     placement:
         policy_fetch: by_scope
         policy_scope:
index 17057d8..c99c5eb 100644 (file)
@@ -33,7 +33,8 @@ def _build_parameters(group_policies, service_info, request_parameters):
         :param request_parameters: request parameters
         :return:
         """
-    initial_params = tr.get_opt_query_data(request_parameters, group_policies['onap.policies.optimization.QueryPolicy'])
+    initial_params = tr.get_opt_query_data(request_parameters,
+                                           group_policies['onap.policies.optimization.QueryPolicy'])
     params = dict()
     params.update({"REQUIRED_MEM": initial_params.pop("requiredMemory", "")})
     params.update({"REQUIRED_DISK": initial_params.pop("requiredDisk", "")})
@@ -49,16 +50,19 @@ def _build_parameters(group_policies, service_info, request_parameters):
     return params
 
 
-def conductor_api_builder(req_info, demands, request_parameters, service_info, flat_policies: list, local_config,
+def conductor_api_builder(req_info, demands, request_parameters, service_info,
+                          location_enabled, flat_policies: list, local_config,
                           template="osdf/adapters/conductor/templates/conductor_interface.json"):
     """Build an OSDF southbound API call for HAS-Conductor/Placement optimization
         :param req_info: parameter data received from a client
         :param demands: list of demands
         :param request_parameters: request parameters
         :param service_info: service info object
+        :param location_enabled: boolean to check location to be sent in the request
         :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 local_config: local configuration file with pointers for
+               the service specific information
         :return: json to be sent to Conductor/placement optimization
         """
 
@@ -88,10 +92,14 @@ def conductor_api_builder(req_info, demands, request_parameters, service_info, f
         demand_name_list, gp['onap.policies.optimization.Vim_fit'])
     hpa_policy_list = tr.gen_hpa_policy(
         demand_name_list, gp['onap.policies.optimization.HpaPolicy'])
+    threshold_policy_list = tr.gen_threshold_policy(demand_name_list,
+                                                    gp['onap.policies.optimization.'
+                                                       'ThresholdPolicy'])
     req_params_dict = _build_parameters(gp, service_info, request_parameters)
-    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]
+    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, threshold_policy_list]
     filtered_policies = [x for x in conductor_policies if len(x) > 0]
     policy_groups = list_flatten(filtered_policies)
     request_type = req_info.get('requestType', None)
@@ -104,6 +112,7 @@ def conductor_api_builder(req_info, demands, request_parameters, service_info, f
         timeout=req_info['timeout'],
         limit=req_info['numSolutions'],
         request_params=req_params_dict,
+        location_enabled=location_enabled,
         json=json)
     json_payload = json.dumps(json.loads(rendered_req))  # need this because template's JSON is ugly!
     return json_payload
index 00069a4..155d4d5 100644 (file)
@@ -28,7 +28,8 @@ from osdf.utils.interfaces import RestClient
 from osdf.operation.exceptions import BusinessException
 
 
-def request(req_info, demands, request_parameters, service_info, osdf_config, flat_policies):
+def request(req_info, demands, request_parameters, service_info, location_enabled,
+            osdf_config, flat_policies):
     config = osdf_config.deployment
     local_config = osdf_config.core
     uid, passwd = config['conductorUsername'], config['conductorPassword']
@@ -43,13 +44,16 @@ def request(req_info, demands, request_parameters, service_info, osdf_config, fl
         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))
+            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_info, demands, request_parameters, service_info, flat_policies,
+    rc = RestClient(userid=uid, passwd=passwd, method="GET", log_func=debug_log.debug,
+                    headers=headers)
+    conductor_req_json_str = conductor_api_builder(req_info, demands, request_parameters,
+                                                   service_info, location_enabled, flat_policies,
                                                    local_config)
     conductor_req_json = json.loads(conductor_req_json_str)
 
@@ -77,14 +81,16 @@ def request(req_info, demands, request_parameters, service_info, osdf_config, fl
                                     "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']))
+        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)))
+            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):
@@ -92,17 +98,21 @@ def initial_request_to_conductor(rc, conductor_url, conductor_req_json):
     :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
+    :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)
+    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
+    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"]:
index 030d6a0..d4a9a0e 100755 (executable)
         "{{key}}": {{ json.dumps(value) }}
       {% endfor %}
     },
+    {% if location_enabled %}
     "locations": {
         "customer_loc": {
             "latitude": { "get_param": "customer_lat" },
             "longitude": { "get_param": "customer_long" }
         }
     },
+    {% endif %}
     "demands": {{ json.dumps(demand_list) }},
     {% set comma_main = joiner(",") %}
     "constraints": {
index 12dfc88..002af88 100644 (file)
@@ -26,6 +26,19 @@ from osdf.utils.programming_utils import dot_notation
 
 policy_config_mapping = yaml.safe_load(open('config/has_config.yaml')).get('policy_config_mapping')
 
+CONSTRAINT_TYPE_MAP = {"onap.policies.optimization.AttributePolicy": "attribute",
+                       "onap.policies.optimization.DistancePolicy": "distance_to_location",
+                       "onap.policies.optimization.InventoryGroupPolicy": "inventory_group",
+                       "onap.policies.optimization.ResourceInstancePolicy": "instance_fit",
+                       "onap.policies.optimization.ResourceRegionPolicy": "region_fit",
+                       "onap.policies.optimization.AffinityPolicy": "zone",
+                       "onap.policies.optimization.InstanceReservationPolicy":
+                           "instance_reservation",
+                       "onap.policies.optimization.Vim_fit": "vim_fit",
+                       "onap.policies.optimization.HpaPolicy": "hpa",
+                       "onap.policies.optimization.ThresholdPolicy": "threshold"
+                       }
+
 
 def get_opt_query_data(request_parameters, policies):
     """
@@ -44,6 +57,7 @@ def get_opt_query_data(request_parameters, policies):
                     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
@@ -105,7 +119,7 @@ def gen_policy_instance(vnf_list, resource_policy, match_type="intersection", rt
     for policy in resource_policy:
         pc = policy[list(policy.keys())[0]]
         default, demands = get_matching_vnfs(pc['properties']['resources'], vnf_list, match_type=match_type)
-        resource = {pc['properties']['identity']: {'type': map_constraint_type(pc['type']), 'demands': demands}}
+        resource = {pc['properties']['identity']: {'type': CONSTRAINT_TYPE_MAP.get(pc['type']), 'demands': demands}}
 
         if rtype:
             resource[pc['properties']['identity']]['properties'] = {'controller': pc[rtype]['controller'],
@@ -115,13 +129,13 @@ def gen_policy_instance(vnf_list, resource_policy, match_type="intersection", rt
             if default:
                 for d in demands:
                     resource_repeated = True \
-                        if {pc['properties']['identity']: {'type': map_constraint_type(pc['type']), 'demands': d}} \
+                        if {pc['properties']['identity']: {'type': CONSTRAINT_TYPE_MAP.get(pc['type']), 'demands': d}} \
                            in resource_policy_list else False
                     if resource_repeated:
                         continue
                     else:
                         resource_policy_list.append(
-                            {pc['properties']['identity']: {'type': map_constraint_type(pc['type']), 'demands': d }})
+                            {pc['properties']['identity']: {'type': CONSTRAINT_TYPE_MAP.get(pc['type']), 'demands': d }})
                         policy[list(policy.keys())[0]]['properties']['resources'] = d
                         related_policies.append(policy)
             # Need to override the default policies, here delete the outdated policy stored in the db
@@ -216,6 +230,14 @@ def gen_hpa_policy(vnf_list, hpa_policy):
     return cur_policies
 
 
+def gen_threshold_policy(vnf_list, threshold_policy):
+    cur_policies, related_policies = gen_policy_instance(vnf_list, threshold_policy, rtype=None)
+    for p_new, p_main in zip(cur_policies, related_policies):
+        pmz = p_main[list(p_main.keys())[0]]['properties']['thresholdProperty']
+        p_new[p_main[list(p_main.keys())[0]]['properties']['identity']]['properties'] = pmz
+    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'])
@@ -260,13 +282,13 @@ def get_demand_properties(demand, policies):
                                                              policy_property['unique'] else {})
         prop['filtering_attributes'] = dict()
         prop['filtering_attributes'].update({'global-customer-id': policy_property['customerId']}
-                                            if policy_property['customerId'] else {})
+                                            if 'customerId' in policy_property and policy_property['customerId'] else {})
         prop['filtering_attributes'].update({'model-invariant-id': demand['resourceModelInfo']['modelInvariantId']}
                                             if demand['resourceModelInfo']['modelInvariantId'] else {})
         prop['filtering_attributes'].update({'model-version-id': demand['resourceModelInfo']['modelVersionId']}
                                             if demand['resourceModelInfo']['modelVersionId'] else {})
         prop['filtering_attributes'].update({'equipment-role': policy_property['equipmentRole']}
-                                            if policy_property['equipmentRole'] else {})
+                                            if 'equipmentRole' in policy_property and policy_property['equipmentRole'] else {})
 
         if policy_property.get('attributes'):
             for attr_key, attr_val in policy_property['attributes'].items():
@@ -304,7 +326,8 @@ def update_converted_attribute(attr_key, attr_val, properties, attribute_type):
 def gen_demands(demands, vnf_policies):
     """Generate list of demands based on request and VNF policies
     :param demands: A List of demands
-    :param vnf_policies: Policies associated with demand resources (e.g. from grouped_policies['vnfPolicy'])
+    :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 = {}
@@ -315,29 +338,6 @@ def gen_demands(demands, vnf_policies):
     return demand_dictionary
 
 
-def map_constraint_type(policy_type):
-    if "onap.policies.optimization.AttributePolicy" == policy_type:
-        return "attribute"
-    if "onap.policies.optimization.DistancePolicy" == policy_type:
-        return "distance_to_location"
-    if "onap.policies.optimization.InventoryGroupPolicy" == policy_type:
-        return "inventory_group"
-    if "onap.policies.optimization.ResourceInstancePolicy" == policy_type:
-        return "instance_fit"
-    if "onap.policies.optimization.ResourceRegionPolicy" == policy_type:
-        return "region_fit"
-    if "onap.policies.optimization.AffinityPolicy" == policy_type:
-        return "zone"
-    if "onap.policies.optimization.InstanceReservationPolicy" == policy_type:
-        return "instance_reservation"
-    if "onap.policies.optimization.Vim_fit" == policy_type:
-        return "vim_fit"
-    if "onap.policies.optimization.HpaPolicy" == policy_type:
-        return "hpa"
-    
-    return policy_type
-
-
 def gen_cloud_region(property):
     prop = {"cloud_region_attributes": dict()}
     if 'cloudRegion' in property:
index c0a554e..fdc2c1d 100755 (executable)
@@ -34,6 +34,8 @@ from apps.pci.optimizers.pci_opt_processor import process_pci_optimation
 from apps.placement.models.api.placementRequest import PlacementAPI
 from apps.placement.optimizers.conductor.remote_opt_processor import process_placement_opt
 from apps.route.optimizers.simple_route_opt import RouteOpt
+from apps.slice_selection.models.api.nsi_selection_request import NSISelectionAPI
+from apps.slice_selection.optimizers.conductor.remote_opt_processor import process_nsi_selection_opt
 from osdf.adapters.policy.interface import get_policies
 from osdf.adapters.policy.interface import upload_policy_models
 from osdf.config.base import osdf_config
@@ -137,5 +139,15 @@ def do_pci_optimization():
                       request_status="accepted", status_message="")
 
 
+@app.route("/api/oof/selection/nsi/v1", methods=["POST"])
+def do_nsi_selection():
+    request_json = request.get_json()
+    req_id = request_json['requestInfo']['requestId']
+    g.request_id = req_id
+    audit_log.info(MH.received_request(request.url, request.remote_addr, json.dumps(request_json)))
+    NSISelectionAPI(request_json).validate()
+    return process_nsi_selection_opt(request_json, osdf_config)
+
+
 if __name__ == "__main__":
     run_app()
diff --git a/test/apps/slice_selection/conductor_error_response.json b/test/apps/slice_selection/conductor_error_response.json
new file mode 100644 (file)
index 0000000..95a9750
--- /dev/null
@@ -0,0 +1,18 @@
+{
+  "plans": [
+    {
+      "status": "error",
+      "message": "Some error message",
+      "name": "Plan Name 1",
+      "links": [
+        [
+          {
+            "href": "http://conductor:8091/v1/plans/plan_id",
+            "rel": "self"
+          }
+        ]
+      ],
+      "id": "plan_id"
+    }
+  ]
+}
diff --git a/test/apps/slice_selection/new_solution_conductor_response.json b/test/apps/slice_selection/new_solution_conductor_response.json
new file mode 100644 (file)
index 0000000..39fef7b
--- /dev/null
@@ -0,0 +1,87 @@
+{
+   "plans":[
+      {
+         "status":"done",
+         "id":"plan_id",
+         "name":"Plan Name 1",
+         "links":[
+            [
+               {
+                  "href":"http://conductor:8091/v1/plans/plan_id",
+                  "rel":"self"
+               }
+            ]
+         ],
+         "recommendations":[
+            {
+               "URLLC_Core_1":{
+                  "inventory_provider":"aai",
+                  "candidate":{
+                     "exp_data_rate":0,
+                     "conn_density":0,
+                     "coverage_area_ta_list":"[{\"province\":\"??\",\"city\":\"???\",\"county\":\"???\",\"street\":\"?????\"}]",
+                     "activity_factor":0,
+                     "cs_availability":null,
+                     "candidate_id":"1a636c4d-5e76-427e-bfd6-241a947224b0",
+                     "area_traffic_cap_dl":null,
+                     "latency":20,
+                     "service_area_dimension":null,
+                     "domain":"cn",
+                     "e2e_latency":0,
+                     "area_traffic_cap_ul":null,
+                     "inventory_provider":"aai",
+                     "exp_data_rate_ul":100,
+                     "max_number_of_ues":0,
+                     "ue_mobility_level":"stationary",
+                     "candidate_type":"nssi",
+                     "traffic_density":0,
+                     "payload_size":0,
+                     "exp_data_rate_dl":100,
+                     "jitter":0,
+                     "survival_time":0,
+                     "resource_sharing_level":"0",
+                     "inventory_type":"nssi",
+                     "reliability":null,
+                     "cost":1.0,
+                     "nsi_name":"nsi_test_0211",
+                     "instance_name":"nssi_test_0211"
+                  }
+               },
+              "URLLC_Ran_1":{
+                "inventory_provider":"aai",
+                  "candidate":{
+                     "exp_data_rate":0,
+                     "conn_density":0,
+                     "coverage_area_ta_list":"[{\"province\":\"??\",\"city\":\"???\",\"county\":\"???\",\"street\":\"?????\"}]",
+                     "activity_factor":0,
+                     "cs_availability":null,
+                     "candidate_id":"490c68b0-639c-11ea-bc55-0242ac130003",
+                     "area_traffic_cap_dl":null,
+                     "latency":15,
+                     "service_area_dimension":null,
+                     "domain":"cn",
+                     "e2e_latency":0,
+                     "area_traffic_cap_ul":null,
+                     "inventory_provider":"aai",
+                     "exp_data_rate_ul":100,
+                     "max_number_of_ues":0,
+                     "ue_mobility_level":"stationary",
+                     "candidate_type":"nssi",
+                     "traffic_density":0,
+                     "payload_size":0,
+                     "exp_data_rate_dl":100,
+                     "jitter":0,
+                     "survival_time":0,
+                     "resource_sharing_level":"0",
+                     "inventory_type":"nssi",
+                     "reliability":null,
+                     "cost":1.0,
+                     "nsi_name":"nsi_test_0212",
+                     "instance_name":"nssi_test_ran_0211"
+                  }
+              }
+            }
+         ]
+      }
+   ]
+}
\ No newline at end of file
diff --git a/test/apps/slice_selection/new_solution_nsi_response.json b/test/apps/slice_selection/new_solution_nsi_response.json
new file mode 100644 (file)
index 0000000..de248c6
--- /dev/null
@@ -0,0 +1,53 @@
+{
+   "requestId":"d290f1ee-6c54-4b01-90e6-d701748f0851",
+   "transactionId":"d290f1ee-6c54-4b01-90e6-d701748f0851",
+   "requestStatus":"completed",
+   "statusMessage":"",
+   "solutions":{
+      "sharedNSISolutions":[
+
+      ],
+      "newNSISolutions":[
+         {
+            "matchLevel":"",
+            "NSTInfo":null,
+            "NSSISolutions":[
+               {
+                  "sliceProfile":{
+                     "latency":20,
+                     "coverage_area_ta_list":"[{\"province\":\"??\",\"city\":\"???\",\"county\":\"???\",\"street\":\"?????\"}]",
+                     "ue_mobility_level":"stationary",
+                     "resource_sharing_level":"0",
+                     "exp_data_rate_ul":100,
+                     "exp_data_rate_dl":100
+                  },
+                  "NSSTInfo":{
+                     "NSSTName":"URLLC_Core_1"
+                  },
+                  "NSSISolution":{
+                     "NSSIName":"nssi_test_0211",
+                     "NSSIId":"1a636c4d-5e76-427e-bfd6-241a947224b0"
+                  }
+               },
+               {
+                  "sliceProfile":{
+                     "latency":15,
+                     "coverage_area_ta_list":"[{\"province\":\"??\",\"city\":\"???\",\"county\":\"???\",\"street\":\"?????\"}]",
+                     "ue_mobility_level":"stationary",
+                     "resource_sharing_level":"0",
+                     "exp_data_rate_ul":100,
+                     "exp_data_rate_dl":100
+                  },
+                  "NSSTInfo":{
+                     "NSSTName":"URLLC_Ran_1"
+                  },
+                  "NSSISolution":{
+                     "NSSIName":"nssi_test_ran_0211",
+                     "NSSIId":"490c68b0-639c-11ea-bc55-0242ac130003"
+                  }
+               }
+            ]
+         }
+      ]
+   }
+}
\ No newline at end of file
diff --git a/test/apps/slice_selection/nsi_error_response.json b/test/apps/slice_selection/nsi_error_response.json
new file mode 100644 (file)
index 0000000..c09bda8
--- /dev/null
@@ -0,0 +1,6 @@
+{
+   "requestId":"d290f1ee-6c54-4b01-90e6-d701748f0851",
+   "transactionId":"d290f1ee-6c54-4b01-90e6-d701748f0851",
+   "requestStatus":"error",
+   "statusMessage":"Some error message"
+}
\ No newline at end of file
diff --git a/test/apps/slice_selection/nsi_request.json b/test/apps/slice_selection/nsi_request.json
new file mode 100644 (file)
index 0000000..69d6e80
--- /dev/null
@@ -0,0 +1,30 @@
+{
+  "serviceProfile": {
+    "latency": 2,
+    "security": "High",
+    "reliability": 99.9999,
+    "trafficDensity": 1,
+    "connDensity": 100000,
+    "expDataRate": 50,
+    "jitter": 1,
+    "survivalTime": 0
+  },
+  "serviceInfo":{
+      "serviceInstanceId": "209fb01e-60ca-4325-b074-c5ad4e0499f8",
+      "serviceName": ""
+   },
+  "requestInfo": {
+    "transactionId": "d290f1ee-6c54-4b01-90e6-d701748f0851",
+    "requestId": "d290f1ee-6c54-4b01-90e6-d701748f0851",
+    "callbackUrl": "http://0.0.0.0:9000/osdfCallback/",
+    "sourceId": "SO",
+    "timeout": 5
+  },
+  "NSTInfoList": [
+    {
+      "modelInvariantId": "fda3c1e8-7653-4acd-80ef-f5755c1d3859",
+      "modelVersionId": "a6906768-1cae-4e78-acd1-d753ac61f3e8",
+      "modelName": "URLLC_1"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/apps/slice_selection/shared_solution_conductor_response.json b/test/apps/slice_selection/shared_solution_conductor_response.json
new file mode 100644 (file)
index 0000000..5bccd44
--- /dev/null
@@ -0,0 +1,87 @@
+{
+   "plans":[
+      {
+         "status":"done",
+         "id":"plan_id",
+         "name":"Plan Name 1",
+         "links":[
+            [
+               {
+                  "href":"http://conductor:8091/v1/plans/plan_id",
+                  "rel":"self"
+               }
+            ]
+         ],
+         "recommendations":[
+            {
+               "URLLC_Core_1":{
+                  "inventory_provider":"aai",
+                  "candidate":{
+                     "exp_data_rate":0,
+                     "conn_density":0,
+                     "coverage_area_ta_list":"[{\"province\":\"??\",\"city\":\"???\",\"county\":\"???\",\"street\":\"?????\"}]",
+                     "activity_factor":0,
+                     "cs_availability":null,
+                     "candidate_id":"1a636c4d-5e76-427e-bfd6-241a947224b0",
+                     "area_traffic_cap_dl":null,
+                     "latency":20,
+                     "service_area_dimension":null,
+                     "domain":"cn",
+                     "e2e_latency":0,
+                     "area_traffic_cap_ul":null,
+                     "inventory_provider":"aai",
+                     "exp_data_rate_ul":100,
+                     "max_number_of_ues":0,
+                     "ue_mobility_level":"stationary",
+                     "candidate_type":"nssi",
+                     "traffic_density":0,
+                     "payload_size":0,
+                     "exp_data_rate_dl":100,
+                     "jitter":0,
+                     "survival_time":0,
+                     "resource_sharing_level":"0",
+                     "inventory_type":"nssi",
+                     "reliability":null,
+                     "cost":1.0,
+                     "nsi_name":"nsi_test_0212",
+                     "instance_name":"nssi_test_0211"
+                  }
+               },
+              "URLLC_Ran_1":{
+                "inventory_provider":"aai",
+                  "candidate":{
+                     "exp_data_rate":0,
+                     "conn_density":0,
+                     "coverage_area_ta_list":"[{\"province\":\"??\",\"city\":\"???\",\"county\":\"???\",\"street\":\"?????\"}]",
+                     "activity_factor":0,
+                     "cs_availability":null,
+                     "candidate_id":"490c68b0-639c-11ea-bc55-0242ac130003",
+                     "area_traffic_cap_dl":null,
+                     "latency":15,
+                     "service_area_dimension":null,
+                     "domain":"cn",
+                     "e2e_latency":0,
+                     "area_traffic_cap_ul":null,
+                     "inventory_provider":"aai",
+                     "exp_data_rate_ul":100,
+                     "max_number_of_ues":0,
+                     "ue_mobility_level":"stationary",
+                     "candidate_type":"nssi",
+                     "traffic_density":0,
+                     "payload_size":0,
+                     "exp_data_rate_dl":100,
+                     "jitter":0,
+                     "survival_time":0,
+                     "resource_sharing_level":"0",
+                     "inventory_type":"nssi",
+                     "reliability":null,
+                     "cost":1.0,
+                     "nsi_name":"nsi_test_0212",
+                     "instance_name":"nssi_test_ran_0211"
+                  }
+              }
+            }
+         ]
+      }
+   ]
+}
\ No newline at end of file
diff --git a/test/apps/slice_selection/shared_solution_nsi_response.json b/test/apps/slice_selection/shared_solution_nsi_response.json
new file mode 100644 (file)
index 0000000..b0c0e2a
--- /dev/null
@@ -0,0 +1,16 @@
+{
+   "requestId":"d290f1ee-6c54-4b01-90e6-d701748f0851",
+   "transactionId":"d290f1ee-6c54-4b01-90e6-d701748f0851",
+   "requestStatus":"completed",
+   "statusMessage":"",
+   "solutions":{
+      "sharedNSISolutions":[
+         {
+            "NSIName":"nsi_test_0212"
+         }
+      ],
+      "newNSISolutions":[
+
+      ]
+   }
+}
\ No newline at end of file
diff --git a/test/apps/slice_selection/slice_policies.txt b/test/apps/slice_selection/slice_policies.txt
new file mode 100644 (file)
index 0000000..e4ccb65
--- /dev/null
@@ -0,0 +1,4 @@
+subscriber_policy_URLLC_1.json
+thresholdPolicy_URLLC_Core_1_latency.json
+thresholdPolicy_URLLC_Core_1_reliability.json
+vnfPolicy_URLLC_Core_1.json
\ No newline at end of file
diff --git a/test/apps/slice_selection/test_remote_opt_processor.py b/test/apps/slice_selection/test_remote_opt_processor.py
new file mode 100644 (file)
index 0000000..d9b4f24
--- /dev/null
@@ -0,0 +1,102 @@
+# -------------------------------------------------------------------------
+#   Copyright (C) 2020 Wipro Limited.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#
+# -------------------------------------------------------------------------
+#
+
+import json
+import unittest
+from requests import RequestException
+
+from apps.slice_selection.optimizers.conductor.remote_opt_processor import process_nsi_selection_opt
+from osdf.adapters.local_data import local_policies
+from osdf.utils.interfaces import json_from_file, yaml_from_file
+from osdf.utils.programming_utils import DotDict
+import osdf.config.loader as config_loader
+from mock import patch, MagicMock
+import json
+from osdf.logging.osdf_logging import error_log, debug_log
+from osdf.adapters.policy.interface import get_policies
+
+
+class TestRemoteOptProcessor(unittest.TestCase):
+    def setUp(self):
+        self.config_spec = {
+            "deployment": "config/osdf_config.yaml",
+            "core": "config/common_config.yaml"
+        }
+        self.osdf_config = DotDict(config_loader.all_configs(**self.config_spec))
+
+    def tearDown(self):
+        patch.stopall()
+
+    def test_process_nsi_selection_opt(self):
+        main_dir = ""
+        request_file = main_dir + 'test/apps/slice_selection/nsi_request.json'
+        new_solution_response_file = main_dir + 'test/apps/slice_selection/new_solution_nsi_response.json'
+        shared_solution_response_file = main_dir + 'test/apps/slice_selection/shared_solution_nsi_response.json'
+        error_response_file = main_dir + 'test/apps/slice_selection/nsi_error_response.json'
+
+        request_json = json_from_file(request_file)
+        new_solution_response_json = json_from_file(new_solution_response_file)
+        shared_solution_response_json = json_from_file(shared_solution_response_file)
+        error_response_json = json_from_file(error_response_file)
+
+        policies_path = main_dir + 'test/policy-local-files'
+        slice_policies_file = main_dir + 'test/apps/slice_selection/slice_policies.txt'
+
+        valid_policies_files = local_policies.get_policy_names_from_file(slice_policies_file)
+        policies = [json_from_file(policies_path + '/' + name) for name in valid_policies_files]
+        self.patcher_get_policies = patch('osdf.adapters.policy.interface.remote_api',
+                                          return_value=policies)
+        self.Mock_get_policies = self.patcher_get_policies.start()
+
+        new_solution_conductor_response_file = 'test/apps/slice_selection/new_solution_conductor_response.json'
+        new_solution_conductor_response = json_from_file(new_solution_conductor_response_file)
+        self.patcher_req = patch('osdf.adapters.conductor.conductor.request',
+                                 return_value=new_solution_conductor_response)
+        self.Mock_req = self.patcher_req.start()
+        self.assertEquals(new_solution_response_json, process_nsi_selection_opt(request_json, self.osdf_config))
+        self.patcher_req.stop()
+
+        shared_solution_conductor_response_file = 'test/apps/slice_selection/shared_solution_conductor_response.json'
+        shared_solution_conductor_response = json_from_file(shared_solution_conductor_response_file)
+        self.patcher_req = patch('osdf.adapters.conductor.conductor.request',
+                                 return_value=shared_solution_conductor_response)
+        self.Mock_req = self.patcher_req.start()
+        self.assertEquals(shared_solution_response_json,
+                          process_nsi_selection_opt(request_json, self.osdf_config))
+        self.patcher_req.stop()
+
+        conductor_error_response_file = 'test/apps/slice_selection/conductor_error_response.json'
+        conductor_error_response = json_from_file(conductor_error_response_file)
+
+        self.patcher_req = patch('osdf.adapters.conductor.conductor.request',
+                                 side_effect=RequestException(response=json.dumps(conductor_error_response)))
+        self.Mock_req = self.patcher_req.start()
+        self.assertEquals(error_response_json, process_nsi_selection_opt(request_json, self.osdf_config))
+        self.patcher_req.stop()
+
+        self.patcher_req = patch('osdf.adapters.conductor.conductor.request',
+                                 side_effect=Exception("test_exception"))
+        self.Mock_req = self.patcher_req.start()
+        self.assertEquals('test_exception',
+                          process_nsi_selection_opt(request_json, self.osdf_config).get('statusMessage'))
+        self.patcher_req.stop()
+
+
+if __name__ == "__main__":
+    unittest.main()
+
index 0042ecb..d342fa5 100644 (file)
@@ -46,7 +46,7 @@ class TestConductorCalls(unittest.TestCase):
         demands = req_json['placementInfo']['placementDemands']
         request_parameters = req_json['placementInfo']['requestParameters']
         service_info = req_json['serviceInfo']
-        conductor.request(req_info, demands, request_parameters, service_info, self.osdf_config, policies)
+        conductor.request(req_info, demands, request_parameters, service_info, True, self.osdf_config, policies)
 
     def test_request_vfmod(self):
         req_json = json_from_file("./test/placement-tests/request_vfmod.json")
@@ -55,7 +55,7 @@ class TestConductorCalls(unittest.TestCase):
         demands = req_json['placementInfo']['placementDemands']
         request_parameters = req_json['placementInfo']['requestParameters']
         service_info = req_json['serviceInfo']
-        conductor.request(req_info, demands, request_parameters, service_info, self.osdf_config, policies)
+        conductor.request(req_info, demands, request_parameters, service_info, True, self.osdf_config, policies)
 
 
 if __name__ == "__main__":
diff --git a/test/policy-local-files/subscriber_policy_URLLC_1.json b/test/policy-local-files/subscriber_policy_URLLC_1.json
new file mode 100644 (file)
index 0000000..ffa4d79
--- /dev/null
@@ -0,0 +1,34 @@
+{
+  "OSDF_FRANKFURT.SubscriberPolicy_URLLC_1": {
+    "type": "onap.policies.optimization.SubscriberPolicy",
+    "version": "1.0.0",
+    "type_version": "1.0.0",
+    "metadata": {
+      "policy-id": "OSDF_FRANKFURT.SubscriberPolicy_URLLC_1",
+      "policy-version": 1
+    },
+    "properties": {
+      "scope": [
+        "OSDF_FRANKFURT",
+        "URLLC_1"
+      ],
+      "services": [
+        "URLLC_1"
+      ],
+      "identity": "subscriber_URLLC_1",
+      "properties": {
+        "subscriberName": [
+          "URLLC_Core_1"
+        ],
+        "subscriberRole": [
+          {
+            "URLLC_Core_1": {
+              "modelInvariantId": "21d57d4b-52ad-4d3c-a798-248b5bb9124a",
+              "modelVersionId": "bfba363e-e39c-4bd9-a9d5-1371c28f4d22"
+            }
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/test/policy-local-files/thresholdPolicy_URLLC_Core_1_latency.json b/test/policy-local-files/thresholdPolicy_URLLC_Core_1_latency.json
new file mode 100644 (file)
index 0000000..35106f6
--- /dev/null
@@ -0,0 +1,32 @@
+{
+  "OSDF_FRANKFURT.Threshold_URLLC_Core_1": {
+    "type": "onap.policies.optimization.ThresholdPolicy",
+    "version": "1.0.0",
+    "type_version": "1.0.0",
+    "metadata": {
+      "policy-id": "OSDF_FRANKFURT.Threshold_URLLC_Core_1_latency",
+      "policy-version": 1
+    },
+    "properties": {
+      "scope": [
+        "OSDF_FRANKFURT",
+        "URLLC_1",
+        "URLLC_Core_1"
+      ],
+      "resources": [
+        "URLLC_Core_1"
+      ],
+      "services": [
+        "URLLC_1"
+      ],
+      "identity": "Threshold_URLLC_Core_1_latency",
+      "applicableResources": "any",
+      "thresholdProperty": {
+          "attribute": "latency",
+          "operator": "lte",
+          "threshold": 5,
+          "unit": "ms"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/policy-local-files/thresholdPolicy_URLLC_Core_1_reliability.json b/test/policy-local-files/thresholdPolicy_URLLC_Core_1_reliability.json
new file mode 100644 (file)
index 0000000..56089f0
--- /dev/null
@@ -0,0 +1,32 @@
+{
+  "OSDF_FRANKFURT.Threshold_URLLC_Core_1": {
+    "type": "onap.policies.optimization.ThresholdPolicy",
+    "version": "1.0.0",
+    "type_version": "1.0.0",
+    "metadata": {
+      "policy-id": "OSDF_FRANKFURT.Threshold_URLLC_Core_1_reliability",
+      "policy-version": 1
+    },
+    "properties": {
+      "scope": [
+        "OSDF_FRANKFURT",
+        "URLLC_1",
+        "URLLC_Core_1"
+      ],
+      "resources": [
+        "URLLC_Core_1"
+      ],
+      "services": [
+        "URLLC_1"
+      ],
+      "identity": "Threshold_URLLC_Core_1_reliability",
+      "applicableResources": "any",
+      "thresholdProperty": {
+            "attribute":"reliability",
+            "operator":"gte",
+            "threshold":99.999,
+            "unit":""
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/test/policy-local-files/vnfPolicy_URLLC_Core_1.json b/test/policy-local-files/vnfPolicy_URLLC_Core_1.json
new file mode 100644 (file)
index 0000000..6582c17
--- /dev/null
@@ -0,0 +1,37 @@
+{
+  "OSDF_FRANKFURT.vnfPolicy_URLLC_Core_1": {
+    "type": "onap.policies.optimization.VnfPolicy",
+    "version": "1.0.0",
+    "type_version": "1.0.0",
+    "metadata": {
+      "policy-id": "OSDF_FRANKFURT.vnfPolicy_URLLC_Core_1",
+      "policy-version": 1
+    },
+    "properties": {
+      "scope": [
+        "OSDF_FRANKFURT",
+        "URLLC_1",
+        "URLLC_Core_1"
+      ],
+      "resources": [
+        "URLLC_Core_1"
+      ],
+      "services": [
+        "URLLC_1"
+      ],
+      "identity": "vnf_URLLC_Core_1",
+      "applicableResources": "any",
+      "vnfProperties": [
+        {
+          "inventoryProvider": "aai",
+          "inventoryType": "nssi",
+          "region": "RegionOne",
+          "attributes": {
+            "orchestrationStatus": "active",
+            "service-role": "nssi"
+          }
+        }
+      ]
+    }
+  }
+}
index 44c14d8..b3dd97c 100644 (file)
@@ -53,7 +53,7 @@ class TestConductorApiBuilder(unittest.TestCase):
         demands = request_json['placementInfo']['placementDemands']
         request_parameters = request_json['placementInfo']['requestParameters']
         service_info = request_json['serviceInfo']
-        templ_string = conductor_api_builder(req_info, demands, request_parameters, service_info, policies,
+        templ_string = conductor_api_builder(req_info, demands, request_parameters, service_info, True, policies,
                                              local_config, self.conductor_api_template)
         templ_json = json.loads(templ_string)
         self.assertEqual(templ_json["name"], "yyy-yyy-yyyy")
@@ -66,7 +66,7 @@ class TestConductorApiBuilder(unittest.TestCase):
         demands = request_json['placementInfo']['placementDemands']
         request_parameters = request_json['placementInfo']['requestParameters']
         service_info = request_json['serviceInfo']
-        templ_string = conductor_api_builder(req_info, demands, request_parameters, service_info, policies,
+        templ_string = conductor_api_builder(req_info, demands, request_parameters, service_info, True, policies,
                                              local_config, self.conductor_api_template)
         templ_json = json.loads(templ_string)
         self.assertEqual(templ_json, self.request_placement_vfmod_json)