From fcb37e97e37137d3111924e993e75fdb83c2a0a0 Mon Sep 17 00:00:00 2001 From: krishnaa96 Date: Mon, 23 Mar 2020 13:11:07 +0530 Subject: Add functionality to support NSI selection Issue-ID: OPTFRA-677 Signed-off-by: krishnaa96 Change-Id: Ibc51e15fce4692a445df400053060d3a6977b4ce --- .../optimizers/conductor/remote_opt_processor.py | 4 +- apps/slice_selection/__init__.py | 0 apps/slice_selection/models/api/__init__.py | 17 +++ .../models/api/nsi_selection_request.py | 54 +++++++++ .../models/api/nsi_selection_response.py | 73 ++++++++++++ apps/slice_selection/optimizers/__init__.py | 17 +++ .../optimizers/conductor/__init__.py | 0 .../optimizers/conductor/remote_opt_processor.py | 104 +++++++++++++++++ .../optimizers/conductor/response_processor.py | 123 +++++++++++++++++++++ config/common_config.yaml | 18 +++ osdf/adapters/conductor/api_builder.py | 21 +++- osdf/adapters/conductor/conductor.py | 30 +++-- .../conductor/templates/conductor_interface.json | 2 + osdf/adapters/conductor/translation.py | 58 +++++----- osdfapp.py | 12 ++ .../slice_selection/conductor_error_response.json | 18 +++ .../new_solution_conductor_response.json | 87 +++++++++++++++ .../slice_selection/new_solution_nsi_response.json | 53 +++++++++ test/apps/slice_selection/nsi_error_response.json | 6 + test/apps/slice_selection/nsi_request.json | 30 +++++ .../shared_solution_conductor_response.json | 87 +++++++++++++++ .../shared_solution_nsi_response.json | 16 +++ test/apps/slice_selection/slice_policies.txt | 4 + .../slice_selection/test_remote_opt_processor.py | 102 +++++++++++++++++ test/conductor/test_conductor_calls.py | 4 +- .../subscriber_policy_URLLC_1.json | 34 ++++++ .../thresholdPolicy_URLLC_Core_1_latency.json | 32 ++++++ .../thresholdPolicy_URLLC_Core_1_reliability.json | 32 ++++++ .../policy-local-files/vnfPolicy_URLLC_Core_1.json | 37 +++++++ test/test_ConductorApiBuilder.py | 4 +- 30 files changed, 1028 insertions(+), 51 deletions(-) create mode 100644 apps/slice_selection/__init__.py create mode 100644 apps/slice_selection/models/api/__init__.py create mode 100644 apps/slice_selection/models/api/nsi_selection_request.py create mode 100644 apps/slice_selection/models/api/nsi_selection_response.py create mode 100644 apps/slice_selection/optimizers/__init__.py create mode 100644 apps/slice_selection/optimizers/conductor/__init__.py create mode 100644 apps/slice_selection/optimizers/conductor/remote_opt_processor.py create mode 100644 apps/slice_selection/optimizers/conductor/response_processor.py create mode 100644 test/apps/slice_selection/conductor_error_response.json create mode 100644 test/apps/slice_selection/new_solution_conductor_response.json create mode 100644 test/apps/slice_selection/new_solution_nsi_response.json create mode 100644 test/apps/slice_selection/nsi_error_response.json create mode 100644 test/apps/slice_selection/nsi_request.json create mode 100644 test/apps/slice_selection/shared_solution_conductor_response.json create mode 100644 test/apps/slice_selection/shared_solution_nsi_response.json create mode 100644 test/apps/slice_selection/slice_policies.txt create mode 100644 test/apps/slice_selection/test_remote_opt_processor.py create mode 100644 test/policy-local-files/subscriber_policy_URLLC_1.json create mode 100644 test/policy-local-files/thresholdPolicy_URLLC_Core_1_latency.json create mode 100644 test/policy-local-files/thresholdPolicy_URLLC_Core_1_reliability.json create mode 100644 test/policy-local-files/vnfPolicy_URLLC_Core_1.json diff --git a/apps/placement/optimizers/conductor/remote_opt_processor.py b/apps/placement/optimizers/conductor/remote_opt_processor.py index 0b5cb16..3d7a287 100644 --- a/apps/placement/optimizers/conductor/remote_opt_processor.py +++ b/apps/placement/optimizers/conductor/remote_opt_processor.py @@ -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 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 index 0000000..b45f74d --- /dev/null +++ b/apps/slice_selection/models/api/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# diff --git a/apps/slice_selection/models/api/nsi_selection_request.py b/apps/slice_selection/models/api/nsi_selection_request.py new file mode 100644 index 0000000..b7f3fbd --- /dev/null +++ b/apps/slice_selection/models/api/nsi_selection_request.py @@ -0,0 +1,54 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# + +from osdf.models.api.common import OSDFModel +from schematics.types import BaseType, StringType, 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 index 0000000..9547200 --- /dev/null +++ b/apps/slice_selection/models/api/nsi_selection_response.py @@ -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 index 0000000..b45f74d --- /dev/null +++ b/apps/slice_selection/optimizers/__init__.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# diff --git a/apps/slice_selection/optimizers/conductor/__init__.py b/apps/slice_selection/optimizers/conductor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/slice_selection/optimizers/conductor/remote_opt_processor.py b/apps/slice_selection/optimizers/conductor/remote_opt_processor.py new file mode 100644 index 0000000..a44fc4e --- /dev/null +++ b/apps/slice_selection/optimizers/conductor/remote_opt_processor.py @@ -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 index 0000000..5b7be01 --- /dev/null +++ b/apps/slice_selection/optimizers/conductor/response_processor.py @@ -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} diff --git a/config/common_config.yaml b/config/common_config.yaml index d9ecc18..04a5594 100644 --- a/config/common_config.yaml +++ b/config/common_config.yaml @@ -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: diff --git a/osdf/adapters/conductor/api_builder.py b/osdf/adapters/conductor/api_builder.py index 17057d8..c99c5eb 100644 --- a/osdf/adapters/conductor/api_builder.py +++ b/osdf/adapters/conductor/api_builder.py @@ -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 diff --git a/osdf/adapters/conductor/conductor.py b/osdf/adapters/conductor/conductor.py index 00069a4..155d4d5 100644 --- a/osdf/adapters/conductor/conductor.py +++ b/osdf/adapters/conductor/conductor.py @@ -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"]: diff --git a/osdf/adapters/conductor/templates/conductor_interface.json b/osdf/adapters/conductor/templates/conductor_interface.json index 030d6a0..d4a9a0e 100755 --- a/osdf/adapters/conductor/templates/conductor_interface.json +++ b/osdf/adapters/conductor/templates/conductor_interface.json @@ -11,12 +11,14 @@ "{{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": { diff --git a/osdf/adapters/conductor/translation.py b/osdf/adapters/conductor/translation.py index 12dfc88..002af88 100644 --- a/osdf/adapters/conductor/translation.py +++ b/osdf/adapters/conductor/translation.py @@ -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: diff --git a/osdfapp.py b/osdfapp.py index c0a554e..fdc2c1d 100755 --- a/osdfapp.py +++ b/osdfapp.py @@ -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 index 0000000..95a9750 --- /dev/null +++ b/test/apps/slice_selection/conductor_error_response.json @@ -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 index 0000000..39fef7b --- /dev/null +++ b/test/apps/slice_selection/new_solution_conductor_response.json @@ -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 index 0000000..de248c6 --- /dev/null +++ b/test/apps/slice_selection/new_solution_nsi_response.json @@ -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 index 0000000..c09bda8 --- /dev/null +++ b/test/apps/slice_selection/nsi_error_response.json @@ -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 index 0000000..69d6e80 --- /dev/null +++ b/test/apps/slice_selection/nsi_request.json @@ -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 index 0000000..5bccd44 --- /dev/null +++ b/test/apps/slice_selection/shared_solution_conductor_response.json @@ -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 index 0000000..b0c0e2a --- /dev/null +++ b/test/apps/slice_selection/shared_solution_nsi_response.json @@ -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 index 0000000..e4ccb65 --- /dev/null +++ b/test/apps/slice_selection/slice_policies.txt @@ -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 index 0000000..d9b4f24 --- /dev/null +++ b/test/apps/slice_selection/test_remote_opt_processor.py @@ -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() + diff --git a/test/conductor/test_conductor_calls.py b/test/conductor/test_conductor_calls.py index 0042ecb..d342fa5 100644 --- a/test/conductor/test_conductor_calls.py +++ b/test/conductor/test_conductor_calls.py @@ -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 index 0000000..ffa4d79 --- /dev/null +++ b/test/policy-local-files/subscriber_policy_URLLC_1.json @@ -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 index 0000000..35106f6 --- /dev/null +++ b/test/policy-local-files/thresholdPolicy_URLLC_Core_1_latency.json @@ -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 index 0000000..56089f0 --- /dev/null +++ b/test/policy-local-files/thresholdPolicy_URLLC_Core_1_reliability.json @@ -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 index 0000000..6582c17 --- /dev/null +++ b/test/policy-local-files/vnfPolicy_URLLC_Core_1.json @@ -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" + } + } + ] + } + } +} diff --git a/test/test_ConductorApiBuilder.py b/test/test_ConductorApiBuilder.py index 44c14d8..b3dd97c 100644 --- a/test/test_ConductorApiBuilder.py +++ b/test/test_ConductorApiBuilder.py @@ -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) -- cgit 1.2.3-korg