summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShankaranarayanan Puzhavakath Narayanan <snarayanan@research.att.com>2020-03-16 12:39:17 +0000
committerGerrit Code Review <gerrit@onap.org>2020-03-16 12:39:17 +0000
commit9ec53106de839d09654e0a44fb7b1d5500cedd7a (patch)
treefeacd699af57945a51d272741988ce217d561206
parent7422ddaf96e94a11c010616d8f81b3fa953ca553 (diff)
parentda8b12c1737efbdafc0d31db35d5c22e8eae0da0 (diff)
Merge "Add functionalities to support NSSI selection"
-rw-r--r--conductor/conductor/common/sms.py12
-rw-r--r--conductor/conductor/controller/translator.py9
-rw-r--r--conductor/conductor/data/plugins/inventory_provider/aai.py86
-rw-r--r--conductor/conductor/data/service.py3
-rw-r--r--conductor/conductor/solver/optimizer/constraints/threshold.py62
-rwxr-xr-xconductor/conductor/solver/optimizer/search.py6
-rwxr-xr-xconductor/conductor/solver/request/parser.py14
-rw-r--r--conductor/conductor/solver/service.py6
-rwxr-xr-xconductor/conductor/tests/functional/simulators/aaisim/aaisim.py19
-rw-r--r--conductor/conductor/tests/functional/simulators/aaisim/responses/get_nssi_response.json64
-rw-r--r--conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_candidate.json33
-rw-r--r--conductor/conductor/tests/unit/data/plugins/inventory_provider/nssi_response.json63
-rw-r--r--conductor/conductor/tests/unit/data/plugins/inventory_provider/test_aai.py32
-rw-r--r--conductor/conductor/tests/unit/solver/optimizer/constraints/__init__.py25
-rw-r--r--conductor/conductor/tests/unit/solver/optimizer/constraints/test_threshold.py63
15 files changed, 488 insertions, 9 deletions
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()