From da8b12c1737efbdafc0d31db35d5c22e8eae0da0 Mon Sep 17 00:00:00 2001 From: krishnaa96 Date: Tue, 3 Mar 2020 15:01:51 +0530 Subject: Add functionalities to support NSSI selection Add threshold constraint Modify AAI plugin to get NSSIs from AAI Issue-ID: OPTFRA-677 Signed-off-by: krishnaa96 Change-Id: Ic9bc97aca3835eba99d0a3f1580c281c3856b1cf --- conductor/conductor/common/sms.py | 12 ++- conductor/conductor/controller/translator.py | 9 ++- .../data/plugins/inventory_provider/aai.py | 86 ++++++++++++++++++++++ conductor/conductor/data/service.py | 3 + .../solver/optimizer/constraints/threshold.py | 62 ++++++++++++++++ conductor/conductor/solver/optimizer/search.py | 6 +- conductor/conductor/solver/request/parser.py | 14 +++- conductor/conductor/solver/service.py | 6 +- .../tests/functional/simulators/aaisim/aaisim.py | 19 +++++ .../aaisim/responses/get_nssi_response.json | 64 ++++++++++++++++ .../plugins/inventory_provider/nssi_candidate.json | 33 +++++++++ .../plugins/inventory_provider/nssi_response.json | 63 ++++++++++++++++ .../data/plugins/inventory_provider/test_aai.py | 32 ++++++++ .../unit/solver/optimizer/constraints/__init__.py | 25 +++++++ .../solver/optimizer/constraints/test_threshold.py | 63 ++++++++++++++++ 15 files changed, 488 insertions(+), 9 deletions(-) create mode 100644 conductor/conductor/solver/optimizer/constraints/threshold.py create mode 100644 conductor/conductor/tests/functional/simulators/aaisim/responses/get_nssi_response.json create mode 100644 conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_candidate.json create mode 100644 conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_response.json create mode 100644 conductor/conductor/tests/unit/solver/optimizer/constraints/__init__.py create mode 100644 conductor/conductor/tests/unit/solver/optimizer/constraints/test_threshold.py diff --git a/conductor/conductor/common/sms.py b/conductor/conductor/common/sms.py index 6e21392..ed71b8a 100644 --- a/conductor/conductor/common/sms.py +++ b/conductor/conductor/common/sms.py @@ -27,8 +27,7 @@ import conductor.data.plugins.inventory_provider.aai import conductor.api.controllers.v1.plans import conductor.common.music.api import conductor.data.plugins.service_controller.sdnc - - +from conductor.common.utils import cipherUtils LOG = log.getLogger(__name__) @@ -105,7 +104,7 @@ def load_secrets(): config.set_override('username', secret_dict['aai']['username'], 'aai') config.set_override('password', secret_dict['aai']['password'], 'aai') config.set_override('username', secret_dict['conductor_api']['username'], 'conductor_api') - config.set_override('password', secret_dict['conductor_api']['password'], 'conductor_api') + config.set_override('password', decrypt_pass(secret_dict['conductor_api']['password']), 'conductor_api') config.set_override('aafuser', secret_dict['music_api']['aafuser'], 'music_api') config.set_override('aafpass', secret_dict['music_api']['aafpass'], 'music_api') config.set_override('aafns', secret_dict['music_api']['aafns'], 'music_api') @@ -116,6 +115,13 @@ def load_secrets(): config.set_override('aaf_conductor_user', secret_dict['aaf_api']['aaf_conductor_user'], 'aaf_api') +def decrypt_pass(passwd): + if passwd == '' or passwd == 'NA': + return passwd + else: + return cipherUtils.AESCipher.get_instance().decrypt(passwd) + + def delete_secrets(): """ This is intended to delete the secrets for a clean initialization for testing Application. Actual deployment will have a preload script. diff --git a/conductor/conductor/controller/translator.py b/conductor/conductor/controller/translator.py index 8184639..9fa5b6b 100644 --- a/conductor/conductor/controller/translator.py +++ b/conductor/conductor/controller/translator.py @@ -1,6 +1,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2015-2017 AT&T Intellectual Property +# 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. @@ -43,7 +44,7 @@ CONF = cfg.CONF VERSIONS = ["2016-11-01", "2017-10-10", "2018-02-01"] LOCATION_KEYS = ['latitude', 'longitude', 'host_name', 'clli_code'] INVENTORY_PROVIDERS = ['aai'] -INVENTORY_TYPES = ['cloud', 'service', 'transport', 'vfmodule'] +INVENTORY_TYPES = ['cloud', 'service', 'transport', 'vfmodule', 'nssi'] DEFAULT_INVENTORY_PROVIDER = INVENTORY_PROVIDERS[0] CANDIDATE_KEYS = ['candidate_id', 'cost', 'inventory_type', 'location_id', 'location_type'] @@ -67,6 +68,11 @@ CONSTRAINTS = { 'split': True, 'required': ['evaluate'], }, + 'threshold': { + 'split': True, + 'required': ['attribute', 'threshold', 'operator'], + 'optional': ['unit'] + }, 'distance_between_demands': { 'required': ['distance'], 'thresholds': { @@ -761,7 +767,6 @@ class Translator(object): raise TranslatorException( "Optimization goal 'minimize' must " "contain a single function of 'sum'.") - operands = optimization_copy['minimize']['sum'] if type(operands) is not list: # or len(operands) != 2: diff --git a/conductor/conductor/data/plugins/inventory_provider/aai.py b/conductor/conductor/data/plugins/inventory_provider/aai.py index cf764e5..658f838 100644 --- a/conductor/conductor/data/plugins/inventory_provider/aai.py +++ b/conductor/conductor/data/plugins/inventory_provider/aai.py @@ -1,6 +1,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2015-2017 AT&T Intellectual Property +# 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. @@ -1877,6 +1878,12 @@ class AAI(base.InventoryProviderBase): # add candidate to demand candidates resolved_demands[name].append(candidate) + elif inventory_type == 'nssi': + if filtering_attributes and model_invariant_id: + resolved_demands[name] = self.get_nssi_candidates(filtering_attributes, + model_invariant_id, model_version_id, + service_role, candidate_uniqueness) + else: LOG.error("Unknown inventory_type " " {}".format(inventory_type)) @@ -1947,4 +1954,83 @@ class AAI(base.InventoryProviderBase): directives = None return directives + def get_nssi_candidates(self, filtering_attributes, model_invariant_id, model_version_id, service_role, + candidate_uniqueness): + raw_path = ('nodes/service-instances' + + '?model-invariant-id={}'.format(model_invariant_id) + + ('&model-version-id={}'.format(model_version_id) if model_version_id else '') + + ('&service-role={}'.format(service_role) if service_role else '') + + '&depth=2') + + path = self._aai_versioned_path(raw_path) + aai_response = self._request('get', path, data=None) + + if aai_response is None or aai_response.status_code != 200: + return None + return self.filter_nssi_candidates(aai_response.json(), filtering_attributes, candidate_uniqueness) + + def filter_nssi_candidates(self, response_body, filtering_attributes, candidate_uniqueness): + + candidates = list() + if filtering_attributes and response_body is not None: + nssi_instances = response_body.get("service-instance", []) + + for nssi_instance in nssi_instances: + + inventory_attributes = dict() + inventory_attributes["orchestration-status"] = nssi_instance.get('orchestration-status') + inventory_attributes["service-role"] = nssi_instance.get('service-role') + + if self.match_inventory_attributes(filtering_attributes, inventory_attributes, + nssi_instance.get('service-instance-id')): + + properties = list() + relationships = nssi_instance['relationship-list']['relationship'] + for relationship in relationships: + if relationship['related-to'] == 'service-instance': + properties = relationship['related-to-property'] + + nsi_name = None + if properties: + for prop in properties: + if prop['property-key'] == 'service-instance.service-instance-name': + nsi_name = prop['property-value'] + + slice_profiles = nssi_instance.get('slice-profiles').get('slice-profile') + slice_profile = min(slice_profiles, key=lambda x: x['latency']) + + candidate = dict() + candidate['candidate_id'] = nssi_instance.get('service-instance-id') + candidate['instance_name'] = nssi_instance.get('service-instance-name') + candidate['cost'] = self.conf.data.nssi_candidate_cost + candidate['candidate_type'] = 'nssi' + candidate['inventory_type'] = 'nssi' + candidate['inventory_provider'] = 'aai' + candidate['domain'] = nssi_instance.get('environment-context') + candidate['latency'] = slice_profile.get('latency') + candidate['max_number_of_ues'] = slice_profile.get('max-number-of-UEs') + candidate['coverage_area_ta_list'] = slice_profile.get('coverage-area-TA-list') + candidate['ue_mobility_level'] = slice_profile.get('ue-mobility-level') + candidate['resource_sharing_level'] = slice_profile.get('resource-sharing-level') + candidate['exp_data_rate_ul'] = slice_profile.get('exp-data-rate-UL') + candidate['exp_data_rate_dl'] = slice_profile.get('exp-data-rate-DL') + candidate['area_traffic_cap_ul'] = slice_profile.get('area-traffic-cap-UL') + candidate['area_traffic_cap_dl'] = slice_profile.get('area-traffic-cap-DL') + candidate['activity_factor'] = slice_profile.get('activity-factor') + candidate['e2e_latency'] = slice_profile.get('e2e-latency') + candidate['jitter'] = slice_profile.get('jitter') + candidate['survival_time'] = slice_profile.get('survival-time') + candidate['exp_data_rate'] = slice_profile.get('exp-data-rate') + candidate['payload_size'] = slice_profile.get('payload-size') + candidate['traffic_density'] = slice_profile.get('traffic-density') + candidate['conn_density'] = slice_profile.get('conn-density') + candidate['reliability'] = slice_profile.get('reliability') + candidate['service_area_dimension'] = slice_profile.get('service-area-dimension') + candidate['cs_availability'] = slice_profile.get('cs-availability') + candidate['uniqueness'] = candidate_uniqueness + if nsi_name: + candidate['nsi_name'] = nsi_name + candidates.append(candidate) + + return candidates diff --git a/conductor/conductor/data/service.py b/conductor/conductor/data/service.py index ab1a331..07f66c3 100644 --- a/conductor/conductor/data/service.py +++ b/conductor/conductor/data/service.py @@ -1,6 +1,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2015-2017 AT&T Intellectual Property +# 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. @@ -60,6 +61,8 @@ DATA_OPTS = [ default=2.0), cfg.FloatOpt('service_candidate_cost', default=1.0), + cfg.FloatOpt('nssi_candidate_cost', + default=1.0), ] CONF.register_opts(DATA_OPTS, group='data') diff --git a/conductor/conductor/solver/optimizer/constraints/threshold.py b/conductor/conductor/solver/optimizer/constraints/threshold.py new file mode 100644 index 0000000..a94c608 --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/threshold.py @@ -0,0 +1,62 @@ +# +# ------------------------------------------------------------------------- +# 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 conductor.i18n import _LI +from conductor.solver.optimizer.constraints import constraint +from oslo_log import log + +LOG = log.getLogger(__name__) + + +class Threshold(constraint.Constraint): + + OPERATIONS = {'gte': lambda x, y: x >= y, + 'lte': lambda x, y: x <= y, + 'gt': lambda x, y: x > y, + 'lt': lambda x, y: x < y, + 'eq': lambda x, y: x == y + } + + def __init__(self, _name, _type, _demand_list, _priority=0, + _properties=None): + constraint.Constraint.__init__( + self, _name, _type, _demand_list, _priority) + self.attribute = _properties.get('attribute') + self.operation = self.OPERATIONS.get(_properties.get('operator')) + self.threshold = _properties.get('threshold') + + def solve(self, _decision_path, _candidate_list, _request): + + filtered_candidates = list() + demand_name = _decision_path.current_demand.name + + LOG.info(_LI("Solving constraint type '{}' for demand - [{}]").format( + self.constraint_type, demand_name)) + + for candidate in _candidate_list: + attribute_value = candidate.get(self.attribute) + if self.operation(attribute_value, self.threshold): + filtered_candidates.append(candidate) + + return filtered_candidates + + + + + diff --git a/conductor/conductor/solver/optimizer/search.py b/conductor/conductor/solver/optimizer/search.py index 9c4fe46..53d3918 100755 --- a/conductor/conductor/solver/optimizer/search.py +++ b/conductor/conductor/solver/optimizer/search.py @@ -1,6 +1,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2015-2017 AT&T Intellectual Property +# 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. @@ -104,8 +105,9 @@ class Search(object): msg = "--- demand = {}, chosen resource = {} at {}" for demand_name in _best_path.decisions: resource = _best_path.decisions[demand_name] - LOG.debug(msg.format(demand_name, resource["candidate_id"], - resource["location_id"])) + if 'location_id' in resource: + LOG.debug(msg.format(demand_name, resource["candidate_id"], + resource["location_id"])) msg = "--- total value of decision = {}" LOG.debug(msg.format(_best_path.total_value)) diff --git a/conductor/conductor/solver/request/parser.py b/conductor/conductor/solver/request/parser.py index 0ce3290..13fd5e6 100755 --- a/conductor/conductor/solver/request/parser.py +++ b/conductor/conductor/solver/request/parser.py @@ -2,6 +2,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2015-2017 AT&T Intellectual Property +# 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. @@ -37,6 +38,7 @@ from conductor.solver.optimizer.constraints \ import service as service_constraint from conductor.solver.optimizer.constraints import vim_fit from conductor.solver.optimizer.constraints import zone +from conductor.solver.optimizer.constraints import threshold from conductor.solver.request import demand from conductor.solver.request import objective from conductor.solver.request.functions import aic_version @@ -207,6 +209,14 @@ class Parser(object): _properties=c_property) self.constraints[my_attribute_constraint.name] = \ my_attribute_constraint + elif constraint_type == "threshold": + c_property = constraint_info.get("properties") + my_threshold_constraint = \ + threshold.Threshold(constraint_id, + constraint_type, + constraint_demands, + _properties=c_property) + self.constraints[my_threshold_constraint.name] = my_threshold_constraint elif constraint_type == "hpa": LOG.debug("Creating constraint - {}".format(constraint_type)) c_property = constraint_info.get("properties") @@ -523,8 +533,10 @@ class Parser(object): constraint.rank = 7 elif constraint.constraint_type == "region_fit": constraint.rank = 8 - else: + elif constraint.constraint_type == "threshold": constraint.rank = 9 + else: + constraint.rank = 10 def attr_sort(self, attrs=['rank']): # this helper for sorting the rank diff --git a/conductor/conductor/solver/service.py b/conductor/conductor/solver/service.py index 2ed0c4a..7643a70 100644 --- a/conductor/conductor/solver/service.py +++ b/conductor/conductor/solver/service.py @@ -1,6 +1,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2015-2017 AT&T Intellectual Property +# 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. @@ -454,7 +455,7 @@ class SolverService(cotyledon.Service): p.message = message # Metrics to Prometheus - m_svc_name = p.template['parameters'].get('service_name', 'N/A') + m_svc_name = p.template.get('parameters', {}).get('service_name', 'N/A') PC.VNF_FAILURE.labels('ONAP', m_svc_name).inc() while 'FAILURE' in _is_success: @@ -496,6 +497,9 @@ class SolverService(cotyledon.Service): 'aic_version': resource.get("cloud_region_version")}, } + if rec["candidate"]["inventory_type"] == "nssi": + rec["candidate"] = resource + if resource.get('vim-id'): rec["candidate"]['vim-id'] = resource.get('vim-id') diff --git a/conductor/conductor/tests/functional/simulators/aaisim/aaisim.py b/conductor/conductor/tests/functional/simulators/aaisim/aaisim.py index 7712fff..bb13482 100755 --- a/conductor/conductor/tests/functional/simulators/aaisim/aaisim.py +++ b/conductor/conductor/tests/functional/simulators/aaisim/aaisim.py @@ -1,6 +1,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2018 AT&T Intellectual Property +# 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. @@ -27,6 +28,7 @@ urls = ( '/aai/v14/cloud-infrastructure/complexes/complex/DLLSTX233','get_complex_DLLSTX233', '/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/HPA-cloud/cloud-region-1/flavors/', 'get_flavors_region_1', '/aai/v14/cloud-infrastructure/cloud-regions/cloud-region/HPA-cloud/cloud-region-2/flavors/', 'get_flavors_region_2', + '/aai/v14/nodes/service-instances', 'get_nssi', ) @@ -136,6 +138,23 @@ class get_flavors_region_2: return json.dumps(json_data) +class get_nssi: + def GET(self): + print("------------------------------------------------------") + replyfile = "get_nssi_response.json" + # replyToAaiGet (web, replydir, replyfile) + fullreply = replydir + replyfile + trid = web.ctx.env.get('X_TRANSACTIONID', '111111') + # print ("X-TransactionId : {}".format(trid)) + print("this is the context : {}".format(web.ctx.fullpath)) + with open(fullreply) as json_file: + json_data = json.load(json_file) + print(json_data) + + web.header('Content-Type', 'application/json') + web.header('X-TransactionId', trid) + return json.dumps(json_data) + if __name__ == "__main__": app = web.application(urls, globals()) diff --git a/conductor/conductor/tests/functional/simulators/aaisim/responses/get_nssi_response.json b/conductor/conductor/tests/functional/simulators/aaisim/responses/get_nssi_response.json new file mode 100644 index 0000000..b7ef43b --- /dev/null +++ b/conductor/conductor/tests/functional/simulators/aaisim/responses/get_nssi_response.json @@ -0,0 +1,64 @@ +{"service-instance": [{ + "service-instance-id": "1a636c4d-5e76-427e-bfd6-241a947224b0", + "service-instance-name": "nssi_test_0211", + "service-type": "embb", + "service-role": "nssi", + "environment-context": "cn", + "model-invariant-id": "21d57d4b-52ad-4d3c-a798-248b5bb9124a", + "model-version-id": "bfba363e-e39c-4bd9-a9d5-1371c28f4d22", + "resource-version": "1581418601616", + "orchestration-status": "active", + "relationship-list": { + "relationship": [ + { + "related-to": "service-instance", + "relationship-label": "org.onap.relationships.inventory.ComposedOf", + "related-link": "/aai/v16/business/customers/customer/5GCustomer/service-subscriptions/service-subscription/5G/service-instances/service-instance/4115d3c8-dd59-45d6-b09d-e756dee9b518", + "relationship-data": [ + { + "relationship-key": "customer.global-customer-id", + "relationship-value": "5GCustomer" + }, + { + "relationship-key": "service-subscription.service-type", + "relationship-value": "5G" + }, + { + "relationship-key": "service-instance.service-instance-id", + "relationship-value": "4115d3c8-dd59-45d6-b09d-e756dee9b518" + } + ], + "related-to-property": [ + { + "property-key": "service-instance.service-instance-name", + "property-value": "nsi_test_0211" + } + ] + } + ] + }, + "slice-profiles": { + "slice-profile": [ + { + "profile-id": "cdad9f49-4201-4e3a-aac1-b0f27902c299", + "latency": 20, + "max-number-of-UEs": 0, + "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, + "activity-factor": 0, + "e2e-latency": 0, + "jitter": 0, + "survival-time": 0, + "exp-data-rate": 0, + "payload-size": 0, + "traffic-density": 0, + "conn-density": 0, + "reliability": 99.999, + "resource-version": "1581418602494" + } + ] + } +}]} diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_candidate.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_candidate.json new file mode 100644 index 0000000..a26f322 --- /dev/null +++ b/conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_candidate.json @@ -0,0 +1,33 @@ +[ + { + "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", + "uniqueness": "true" + } +] \ No newline at end of file diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_response.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_response.json new file mode 100644 index 0000000..e22ce39 --- /dev/null +++ b/conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_response.json @@ -0,0 +1,63 @@ +{"service-instance": [{ + "service-instance-id": "1a636c4d-5e76-427e-bfd6-241a947224b0", + "service-instance-name": "nssi_test_0211", + "service-type": "embb", + "service-role": "nssi", + "environment-context": "cn", + "model-invariant-id": "21d57d4b-52ad-4d3c-a798-248b5bb9124a", + "model-version-id": "bfba363e-e39c-4bd9-a9d5-1371c28f4d22", + "resource-version": "1581418601616", + "orchestration-status": "active", + "relationship-list": { + "relationship": [ + { + "related-to": "service-instance", + "relationship-label": "org.onap.relationships.inventory.ComposedOf", + "related-link": "/aai/v16/business/customers/customer/5GCustomer/service-subscriptions/service-subscription/5G/service-instances/service-instance/4115d3c8-dd59-45d6-b09d-e756dee9b518", + "relationship-data": [ + { + "relationship-key": "customer.global-customer-id", + "relationship-value": "5GCustomer" + }, + { + "relationship-key": "service-subscription.service-type", + "relationship-value": "5G" + }, + { + "relationship-key": "service-instance.service-instance-id", + "relationship-value": "4115d3c8-dd59-45d6-b09d-e756dee9b518" + } + ], + "related-to-property": [ + { + "property-key": "service-instance.service-instance-name", + "property-value": "nsi_test_0211" + } + ] + } + ] + }, + "slice-profiles": { + "slice-profile": [ + { + "profile-id": "cdad9f49-4201-4e3a-aac1-b0f27902c299", + "latency": 20, + "max-number-of-UEs": 0, + "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, + "activity-factor": 0, + "e2e-latency": 0, + "jitter": 0, + "survival-time": 0, + "exp-data-rate": 0, + "payload-size": 0, + "traffic-density": 0, + "conn-density": 0, + "resource-version": "1581418602494" + } + ] + } +}]} diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/test_aai.py b/conductor/conductor/tests/unit/data/plugins/inventory_provider/test_aai.py index 5b9984a..cf18087 100644 --- a/conductor/conductor/tests/unit/data/plugins/inventory_provider/test_aai.py +++ b/conductor/conductor/tests/unit/data/plugins/inventory_provider/test_aai.py @@ -1,6 +1,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2015-2017 AT&T Intellectual Property +# 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. @@ -731,4 +732,35 @@ tenant/3c6c471ada7747fe8ff7f28e100b61e8/vservers/vserver/00bddefc-126e-4e4f-a18d self.assertEqual(None, self.aai_ep.match_hpa(candidate_json['candidate_list'][1], feature_json[5])) + def test_get_nssi_candidates(self): + nssi_response_file = './conductor/tests/unit/data/plugins/inventory_provider/nssi_response.json' + nssi_response = json.loads(open(nssi_response_file).read()) + nssi_candidates_file = './conductor/tests/unit/data/plugins/inventory_provider/nssi_candidate.json' + nssi_candidates = json.loads(open(nssi_candidates_file).read()) + + service_role = 'nssi' + model_invariant_id = '21d57d4b-52ad-4d3c-a798-248b5bb9124a' + model_version_id = 'bfba363e-e39c-4bd9-a9d5-1371c28f4d22' + orchestration_status = 'active' + filtering_attributes = dict() + filtering_attributes['orchestration-status'] = orchestration_status + filtering_attributes['service-role'] = service_role + filtering_attributes['model-invariant-id'] = model_invariant_id + filtering_attributes['model-version-id'] = model_version_id + + self.assertEqual(nssi_candidates, self.aai_ep.filter_nssi_candidates(nssi_response, filtering_attributes, "true")) + + nssi_response['service-instance'][0]['orchestration-status'] = 'deactivated' + + self.assertEqual([], self.aai_ep.filter_nssi_candidates(nssi_response, filtering_attributes, "true")) + + nssi_response['service-instance'][0]['service-role'] = 'service' + + self.assertEqual([], self.aai_ep.filter_nssi_candidates(nssi_response, filtering_attributes, "true")) + + self.assertEqual([], self.aai_ep.filter_nssi_candidates(None, filtering_attributes, "true")) + + self.assertEqual([], self.aai_ep.filter_nssi_candidates(None, None, "true")) + + self.assertEqual([], self.aai_ep.filter_nssi_candidates(nssi_response, None, "true")) diff --git a/conductor/conductor/tests/unit/solver/optimizer/constraints/__init__.py b/conductor/conductor/tests/unit/solver/optimizer/constraints/__init__.py new file mode 100644 index 0000000..3c6c3eb --- /dev/null +++ b/conductor/conductor/tests/unit/solver/optimizer/constraints/__init__.py @@ -0,0 +1,25 @@ +# +# ------------------------------------------------------------------------- +# 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. +# +# ------------------------------------------------------------------------- +# + + +class NotImplementedError(NotImplementedError): + # FIXME(jd) This is used by WSME to return a correct HTTP code. We should + # not expose it here but wrap our methods in the API to convert it to a + # proper HTTP error. + code = 501 diff --git a/conductor/conductor/tests/unit/solver/optimizer/constraints/test_threshold.py b/conductor/conductor/tests/unit/solver/optimizer/constraints/test_threshold.py new file mode 100644 index 0000000..34b8193 --- /dev/null +++ b/conductor/conductor/tests/unit/solver/optimizer/constraints/test_threshold.py @@ -0,0 +1,63 @@ +# +# ------------------------------------------------------------------------- +# 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 conductor.solver.optimizer.constraints.threshold import Threshold +from conductor.solver.optimizer.decision_path import DecisionPath +from conductor.solver.request.demand import Demand + + +class TestThreshold(unittest.TestCase): + + def test_solve(self): + + candidates_file = './conductor/tests/unit/data/plugins/inventory_provider/nssi_candidate.json' + candidates = json.loads(open(candidates_file).read()) + + properties = {'attribute': 'latency', 'threshold': 30, 'operator': 'lte'} + + threshold_obj = Threshold('urllc_threshold', 'threshold', ['URLLC'], _priority=0, _properties=properties) + + decision_path = DecisionPath() + decision_path.current_demand = Demand('URLLC') + + self.assertEqual(candidates, threshold_obj.solve(decision_path, candidates, None)) + + properties = {'attribute': 'latency', 'threshold': 10, 'operator': 'lte'} + + threshold_obj = Threshold('urllc_threshold', 'threshold', ['URLLC'], _priority=0, _properties=properties) + + self.assertEqual([], threshold_obj.solve(decision_path, candidates, None)) + + properties = {'attribute': 'exp_data_rate_ul', 'threshold': 70, 'operator': 'gte'} + + threshold_obj = Threshold('urllc_threshold', 'threshold', ['URLLC'], _priority=0, _properties=properties) + + self.assertEqual(candidates, threshold_obj.solve(decision_path, candidates, None)) + + properties = {'attribute': 'exp_data_rate_ul', 'threshold': 120, 'operator': 'gte'} + + threshold_obj = Threshold('urllc_threshold', 'threshold', ['URLLC'], _priority=0, _properties=properties) + + self.assertEqual([], threshold_obj.solve(decision_path, candidates, None)) + + +if __name__ == "__main__": + unittest.main() -- cgit 1.2.3-korg