diff options
33 files changed, 1758 insertions, 786 deletions
diff --git a/conductor.conf b/conductor.conf index a2a4765..b4e74e4 100755 --- a/conductor.conf +++ b/conductor.conf @@ -120,6 +120,28 @@ log_config_append = /usr/local/bin/log.conf #fatal_deprecations = false +[aaf_sms] + +# +# From conductor +# + +# Base URL for SMS, up to and not including the version, and without a trailing +# slash. (string value) +#aaf_sms_url = https://aaf-sms.onap:10443 + +# Timeout for SMS API Call (integer value) +#aaf_sms_timeout = 30 + +# Path to the cacert that will be used to verify If this is None, verify will +# be False and the server certis not verified by the client. (string value) +#aaf_ca_certs = AAF_RootCA.cer + +# Domain UUID - A unique UUID generated when the domainfor HAS is created by +# administrator during deployment (string value) +#secret_domain = has + + [aai] # diff --git a/conductor/conductor/common/config_loader.py b/conductor/conductor/common/config_loader.py new file mode 100644 index 0000000..60e05f1 --- /dev/null +++ b/conductor/conductor/common/config_loader.py @@ -0,0 +1,38 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 Intel Corporation Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# + +import json + +import yaml + + +def load_config_file(config_file, child_name="dockerConfiguration"): + """ + Load YAML/JSON configuration from a file + :param config_file: path to config file (.yaml or .json). + :param child_name: if present, return only that child node + :return: config (all or specific child node) + """ + with open(config_file, 'r') as fid: + res = {} + if config_file.endswith(".yaml"): + res = yaml.load(fid) + elif config_file.endswith(".json") or config_file.endswith("json"): + res = json.load(fid) + return res.get(child_name, res) if child_name else res diff --git a/conductor/conductor/common/music/api.py b/conductor/conductor/common/music/api.py index b3bb5fc..2517b8c 100644 --- a/conductor/conductor/common/music/api.py +++ b/conductor/conductor/common/music/api.py @@ -18,16 +18,15 @@ # """Music Data Store API""" - +import base64 import copy import logging import time -from oslo_config import cfg -from oslo_log import log - from conductor.common import rest from conductor.i18n import _LE, _LI # pylint: disable=W0212 +from oslo_config import cfg +from oslo_log import log LOG = log.getLogger(__name__) @@ -135,6 +134,11 @@ class MusicAPI(object): self.rest.session.headers['content-type'] = 'application/json' self.rest.session.headers['X-patchVersion'] = MUSIC_version[2] self.rest.session.headers['ns'] = CONF.music_api.aafns + # auth_str = 'Basic {}'.format(base64.encodestring( + # '{}:{}'.format(CONF.music_api.aafuser, + # CONF.music_api.aafpass)).strip()) + # self.rest.session.headers['Authorization'] = auth_str + self.rest.session.headers['userId'] = CONF.music_api.aafuser self.rest.session.headers['password'] = CONF.music_api.aafpass diff --git a/conductor/conductor/common/sms.py b/conductor/conductor/common/sms.py new file mode 100644 index 0000000..43b9522 --- /dev/null +++ b/conductor/conductor/common/sms.py @@ -0,0 +1,120 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 Intel Corporation Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# + +'''Secret Management Service Integration''' +from conductor.common import config_loader +from onapsmsclient import Client + +from oslo_config import cfg +from oslo_log import log + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + +AAF_SMS_OPTS = [ + cfg.StrOpt('aaf_sms_url', + default='https://aaf-sms.onap:10443', + help='Base URL for SMS, up to and not including ' + 'the version, and without a trailing slash.'), + cfg.IntOpt('aaf_sms_timeout', + default=30, + help='Timeout for SMS API Call'), + cfg.StrOpt('aaf_ca_certs', + default='AAF_RootCA.cer', + help='Path to the cacert that will be used to verify ' + 'If this is None, verify will be False and the server cert' + 'is not verified by the client.'), + cfg.StrOpt('secret_domain', + default='has', + help='Domain UUID - A unique UUID generated when the domain' + 'for HAS is created by administrator during deployment') +] + +CONF.register_opts(AAF_SMS_OPTS, group='aaf_sms') +config_spec = { + "preload_secrets": "../preload_secrets.yaml" +} + +secret_cache = {} + + +def preload_secrets(): + """ This is intended to load the secrets required for testing Application + Actual deployment will have a preload script. Make sure the config is + in sync""" + preload_config = config_loader.load_config_file( + config_spec.get("preload_secrets")) + domain = preload_config.get("domain") + config = CONF.aaf_sms + sms_url = config.aaf_sms_url + timeout = config.aaf_sms_timeout + cacert = config.aaf_ca_certs + sms_client = Client(url=sms_url, timeout=timeout, cacert=cacert) + domain = sms_client.createDomain(domain) + config.secret_domain = domain # uuid + secrets = preload_config.get("secrets") + for secret in secrets: + sms_client.storeSecret(domain, secret.get('name'), + secret.get('values')) + LOG.debug("Preload secrets complete") + + +def retrieve_secrets(): + """Get all secrets under the domain name""" + secret_dict = dict() + config = CONF.aaf_sms + sms_url = config.aaf_sms_url + timeout = config.aaf_sms_timeout + cacert = config.aaf_ca_certs + domain = config.secret_domain + sms_client = Client(url=sms_url, timeout=timeout, cacert=cacert) + secrets = sms_client.getSecretNames(domain) + for secret in secrets: + values = sms_client.getSecret(domain, secret) + secret_dict[secret] = values + LOG.debug("Secret Dictionary Retrieval Success") + return secret_dict + + +def delete_secrets(): + """ This is intended to delete the secrets for a clean initialization for + testing Application. Actual deployment will have a preload script. + Make sure the config is in sync""" + config = CONF.aaf_sms + sms_url = config.aaf_sms_url + timeout = config.aaf_sms_timeout + cacert = config.aaf_ca_certs + domain = config.secret_domain + sms_client = Client(url=sms_url, timeout=timeout, cacert=cacert) + ret_val = sms_client.deleteDomain(domain) + LOG.debug("Clean up complete") + return ret_val + + +if __name__ == "__main__": + # Initialize Secrets from SMS + preload_secrets() + + # Retrieve Secrets from SMS and load to secret cache + # Use the secret_cache instead of config files + secret_cache = retrieve_secrets() + + # Clean up Delete secrets and domain + delete_secrets() diff --git a/conductor/conductor/controller/translator.py b/conductor/conductor/controller/translator.py index 7cd90f4..4f0ed20 100644 --- a/conductor/conductor/controller/translator.py +++ b/conductor/conductor/controller/translator.py @@ -105,7 +105,7 @@ CONSTRAINTS = { }, } HPA_FEATURES = ['architecture', 'hpa-feature', 'hpa-feature-attributes', - 'hpa-version', 'mandatory'] + 'hpa-version', 'mandatory', 'directives'] HPA_OPTIONAL = ['score'] HPA_ATTRIBUTES = ['hpa-attribute-key', 'hpa-attribute-value', 'operator'] HPA_ATTRIBUTES_OPTIONAL = ['unit'] @@ -553,14 +553,18 @@ class Translator(object): def validate_hpa_constraints(self, req_prop, value): for para in value.get(req_prop): # Make sure there is at least one - # set of flavorLabel and flavorProperties - if not para.get('flavorLabel') \ + # set of id, type, directives and flavorProperties + if not para.get('id') \ + or not para.get('type') \ + or not para.get('directives') \ or not para.get('flavorProperties') \ - or para.get('flavorLabel') == '' \ + or para.get('id') == '' \ + or para.get('type') == '' \ + or not isinstance(para.get('directives'), list) \ or para.get('flavorProperties') == '': raise TranslatorException( "HPA requirements need at least " - "one set of flavorLabel and flavorProperties" + "one set of id, type, directives and flavorProperties" ) for feature in para.get('flavorProperties'): if type(feature) is not dict: @@ -574,7 +578,7 @@ class Translator(object): hpa_optional = set(feature.keys()).difference(HPA_FEATURES) if hpa_optional and not hpa_optional.issubset(HPA_OPTIONAL): raise TranslatorException( - "Lack of compulsory elements inside HPA feature") + "Got unrecognized elements inside HPA feature") if feature.get('mandatory') == 'False' and not feature.get( 'score'): raise TranslatorException( @@ -591,7 +595,7 @@ class Translator(object): if bool(hpa_attr_mandatory): raise TranslatorException( "Lack of compulsory elements inside HPA " - "feature atrributes") + "feature attributes") # process optional hpa attribute parameter hpa_attr_optional = set(attr.keys()).difference( HPA_ATTRIBUTES) @@ -794,6 +798,13 @@ class Translator(object): elif product_op.keys() == ['aic_version']: function = 'aic_version' args = product_op.get('aic_version') + elif product_op.keys() == ['hpa_score']: + function = 'hpa_score' + args = product_op.get('hpa_score') + if not self.is_hpa_policy_exists(args): + raise TranslatorException( + "HPA Score Optimization must include a " + "HPA Policy constraint ") elif product_op.keys() == ['sum']: nested = True nested_operands = product_op.get('sum') @@ -844,6 +855,18 @@ class Translator(object): ) return parsed + def is_hpa_policy_exists(self, demand_list): + # Check if a HPA constraint exist for the demands in the demand list. + constraints_copy = copy.deepcopy(self._constraints) + for demand in demand_list: + for name, constraint in constraints_copy.items(): + constraint_type = constraint.get('type') + if constraint_type == 'hpa': + hpa_demands = constraint.get('demands') + if demand in hpa_demands: + return True + return False + def parse_reservations(self, reservations): demands = self._demands if type(reservations) is not dict: @@ -880,7 +903,6 @@ class Translator(object): "request_type": request_type, "locations": self.parse_locations(self._locations), "demands": self.parse_demands(self._demands), - "objective": self.parse_optimization(self._optmization), "constraints": self.parse_constraints(self._constraints), "objective": self.parse_optimization(self._optmization), "reservations": self.parse_reservations(self._reservations), diff --git a/conductor/conductor/data/plugins/inventory_provider/aai.py b/conductor/conductor/data/plugins/inventory_provider/aai.py index d23a483..8634b3f 100644 --- a/conductor/conductor/data/plugins/inventory_provider/aai.py +++ b/conductor/conductor/data/plugins/inventory_provider/aai.py @@ -1340,6 +1340,6 @@ class AAI(base.InventoryProviderBase): def match_hpa(self, candidate, features): """Match HPA features requirement with the candidate flavors """ hpa_provider = hpa_utils.HpaMatchProvider(candidate, features) - flavor_map = hpa_provider.match_flavor() - return flavor_map + directives = hpa_provider.match_flavor() + return directives diff --git a/conductor/conductor/data/plugins/inventory_provider/hpa_utils.py b/conductor/conductor/data/plugins/inventory_provider/hpa_utils.py index 24f901b..2414e61 100644 --- a/conductor/conductor/data/plugins/inventory_provider/hpa_utils.py +++ b/conductor/conductor/data/plugins/inventory_provider/hpa_utils.py @@ -69,7 +69,7 @@ class HpaMatchProvider(object): if hpa_list not in req_filter_list: req_filter_list.append(hpa_list) max_score = -1 - flavor_map = None + directives = None for flavor in self.flavors_list: flavor_filter_list = [] try: @@ -84,15 +84,18 @@ class HpaMatchProvider(object): flavor_filter_list.append(hpa_list) # if flavor has the matching capability compare attributes if self._is_cap_supported(flavor_filter_list, req_filter_list): - match_found, score = self._compare_feature_attributes(flavor_cap_list) + match_found, score, req_directives = self._compare_feature_attributes(flavor_cap_list) if match_found: LOG.info(_LI("Matching Flavor found '{}' for request - {}"). format(flavor['flavor-name'], self.req_cap_list)) if score > max_score: max_score = score flavor_map = {"flavor-id": flavor['flavor-id'], - "flavor-name": flavor['flavor-name']} - return flavor_map + "flavor-name": flavor['flavor-name'], + "score": max_score} + directives = {"flavor_map": flavor_map, + "directives": req_directives} + return directives def _is_cap_supported(self, flavor, cap): @@ -211,7 +214,7 @@ class HpaMatchProvider(object): for capability in CapabilityDataParser.get_item(flavor_cap_list, 'hpa-capability'): flavor_feature, feature_attributes = capability.get_fields() - # One feature will match this condition as we have pre-filtered + # Multiple features will match this condition as we have pre-filtered if feature == flavor_feature: return feature_attributes @@ -220,24 +223,31 @@ class HpaMatchProvider(object): # and compare each attribute def _compare_feature_attributes(self, flavor_cap_list): score = 0 + directives = [] for capability in CapabilityDataParser.get_item(self.req_cap_list, None): hpa_feature, req_cfa_list = capability.get_fields() + feature_directive = capability.get_directives() + if feature_directive: + feature_directive[:] = [d for d in feature_directive + if d.get("type") != ""] + for item in feature_directive: + directives.append(item) flavor_cfa_list = self._get_flavor_cfa_list(hpa_feature, flavor_cap_list) if flavor_cfa_list is not None: for req_feature_attr in req_cfa_list: req_attr_key = req_feature_attr['hpa-attribute-key'] - # filter to get the attribute being compared + # filter to get the attribute being compared flavor_feature_attr = \ filter(lambda ele: ele['hpa-attribute-key'] == \ - req_attr_key, flavor_cfa_list) - if not flavor_feature_attr: - return False, 0 - if not self._compare_attribute(flavor_feature_attr[0], - req_feature_attr): - return False, 0 + req_attr_key, flavor_cfa_list) + if not flavor_feature_attr and capability.item['mandatory'] == 'True': + return False, 0, None + if not self._compare_attribute(flavor_feature_attr[0], req_feature_attr) \ + and capability.item['mandatory'] == 'True': + return False, 0, None if flavor_cfa_list is not None and capability.item['mandatory'] == 'False': score = score + int(capability.item['score']) - return True, score + return True, score, directives class CapabilityDataParser(object): @@ -268,3 +278,6 @@ class CapabilityDataParser(object): def get_feature(self): return self.item.get('hpa-feature') + + def get_directives(self): + return self.item.get('directives') diff --git a/conductor/conductor/data/service.py b/conductor/conductor/data/service.py index e9d597b..d6ea20f 100644 --- a/conductor/conductor/data/service.py +++ b/conductor/conductor/data/service.py @@ -52,13 +52,13 @@ DATA_OPTS = [ 'mode. When set to False, data will flush any abandoned ' 'messages at startup.'), cfg.FloatOpt('existing_placement_cost', - default=-8000.0, - help='Default value is -8000, which is the diameter of the earth. ' - 'The distance cannot larger than this value'), + default=-8000.0, + help='Default value is -8000, which is the diameter of the earth. ' + 'The distance cannot larger than this value'), cfg.FloatOpt('cloud_candidate_cost', - default=2.0), + default=2.0), cfg.FloatOpt('service_candidate_cost', - default=1.0), + default=1.0), ] CONF.register_opts(DATA_OPTS, group='data') @@ -223,7 +223,7 @@ class DataEndpoint(object): discard_set.add(candidate.get("candidate_id")) return discard_set - #(TODO:Larry) merge this function with the "get_candidate_discard_set" + # (TODO:Larry) merge this function with the "get_candidate_discard_set" def get_candidate_discard_set_by_cloud_region(self, value, candidate_list, value_attrib): discard_set = set() @@ -239,10 +239,8 @@ class DataEndpoint(object): (candidate.get(value_attrib) not in service_requests): discard_set.add(candidate.get("candidate_id")) - return discard_set - def get_inventory_group_candidates(self, ctx, arg): candidate_list = arg["candidate_list"] resolved_candidate = arg["resolved_candidate"] @@ -442,18 +440,22 @@ class DataEndpoint(object): ''' error = False candidate_list = arg["candidate_list"] - label_name = arg["label_name"] + id = arg["id"] + type = arg["type"] + directives = arg["directives"] + attr = directives[0].get("attributes") + label_name = attr[0].get("attribute_name") flavorProperties = arg["flavorProperties"] discard_set = set() - for candidate in candidate_list: + for i in range(len(candidate_list)): # perform this check only for cloud candidates - if candidate["inventory_type"] != "cloud": + if candidate_list[i]["inventory_type"] != "cloud": continue # Check if flavor mapping for current label_name already # exists. This is an invalid condition. - if candidate.get("flavor_map") and candidate["flavor_map"].get( - label_name): + if candidate_list[i].get("directives") and attr[0].get( + "attribute_value") != "": LOG.error(_LE("Flavor mapping for label name {} already" "exists").format(label_name)) continue @@ -461,31 +463,44 @@ class DataEndpoint(object): # RPC call to inventory provider for matching hpa capabilities results = self.ip_ext_manager.map_method( 'match_hpa', - candidate=candidate, + candidate=candidate_list[i], features=flavorProperties ) - if results and len(results) > 0: - flavor_info = results[0] + flavor_name = None + if results and len(results) > 0 and results[0] is not None: + LOG.debug("Find results {} and results length {}".format(results, len(results))) + flavor_info = results[0].get("flavor_map") + req_directives = results[0].get("directives") + LOG.debug("Get directives {}".format(req_directives)) + else: flavor_info = None LOG.info( _LW("No flavor mapping returned by " "inventory provider: {} for candidate: {}").format( self.ip_ext_manager.names()[0], - candidate.get("candidate_id"))) + candidate_list[i].get("candidate_id"))) if not flavor_info: - discard_set.add(candidate.get("candidate_id")) + discard_set.add(candidate_list[i].get("candidate_id")) else: if not flavor_info.get("flavor-name"): - discard_set.add(candidate.get("candidate_id")) + discard_set.add(candidate_list[i].get("candidate_id")) else: - # Create flavor_map if not exist already - if not candidate.get("flavor_map"): - candidate["flavor_map"] = {} + if not candidate_list[i].get("flavor_map"): + candidate_list[i]["flavor_map"] = {} # Create flavor mapping for label_name to flavor flavor_name = flavor_info.get("flavor-name") - candidate["flavor_map"][label_name] = flavor_name + candidate_list[i]["flavor_map"][label_name] = flavor_name + # Create directives if not exist already + if not candidate_list[i].get("all_directives"): + candidate_list[i]["all_directives"] = {} + candidate_list[i]["all_directives"]["directives"] = [] + # Create flavor mapping and merge directives + self.merge_directives(candidate_list, i, id, type, directives, req_directives) + if not candidate_list[i].get("hpa_score"): + candidate_list[i]["hpa_score"] = 0 + candidate_list[i]["hpa_score"] += flavor_info.get("score") # return candidates not in discard set candidate_list[:] = [c for c in candidate_list @@ -496,6 +511,34 @@ class DataEndpoint(object): self.ip_ext_manager.names()[0])) return {'response': candidate_list, 'error': error} + def merge_directives(self, candidate_list, index, id, type, directives, feature_directives): + ''' + Merge the flavor_directives with other diectives listed under hpa capabilities in the policy + :param candidate_list: all candidates + :param index: index number + :param id: vfc name + :param type: vfc type + :param directives: directives for each vfc + :param feature_directives: directives for hpa-features + :return: + ''' + directive= {"id": id, + "type": type, + "directives": ""} + for ele in directives: + if "flavor_directives" in ele.get("type"): + flag = True + break + else: + flag = False + if not flag: + LOG.error("No flavor directives found in {}".format(id)) + for item in feature_directives: + if item and item not in directives: + directives.append(item) + directive["directives"] = directives + candidate_list[index]["all_directives"]["directives"].append(directive) + def get_candidates_with_vim_capacity(self, ctx, arg): ''' RPC for getting candidates with vim capacity diff --git a/conductor/conductor/opts.py b/conductor/conductor/opts.py index e2ace38..52624cf 100644 --- a/conductor/conductor/opts.py +++ b/conductor/conductor/opts.py @@ -22,6 +22,7 @@ import itertools import conductor.api.app import conductor.common.music.api import conductor.common.music.messaging.component +import conductor.common.sms import conductor.conf.inventory_provider import conductor.conf.service_controller import conductor.conf.vim_controller @@ -68,4 +69,5 @@ def list_opts(): ('music_api', conductor.common.music.api.MUSIC_API_OPTS), ('solver', conductor.solver.service.SOLVER_OPTS), ('reservation', conductor.reservation.service.reservation_OPTS), + ('aaf_sms', conductor.common.sms.AAF_SMS_OPTS), ] diff --git a/conductor/conductor/solver/optimizer/constraints/hpa.py b/conductor/conductor/solver/optimizer/constraints/hpa.py index 98d95d9..106953b 100644 --- a/conductor/conductor/solver/optimizer/constraints/hpa.py +++ b/conductor/conductor/solver/optimizer/constraints/hpa.py @@ -54,15 +54,19 @@ class HPA(constraint.Constraint): self.constraint_type, demand_name)) vm_label_list = self.properties.get('evaluate') for vm_demand in vm_label_list: - label_name = vm_demand['flavorLabel'] + id = vm_demand['id'] + type = vm_demand['type'] + directives = vm_demand['directives'] flavorProperties = vm_demand['flavorProperties'] - response = (cei.get_candidates_with_hpa(label_name, + response = (cei.get_candidates_with_hpa(id, + type, + directives, _candidate_list, flavorProperties)) _candidate_list = response if not response: LOG.error(_LE("No matching candidates for HPA exists").format( - label_name)) + id)) break # No need to continue. diff --git a/conductor/conductor/solver/request/functions/hpa_score.py b/conductor/conductor/solver/request/functions/hpa_score.py new file mode 100755 index 0000000..b09e4a7 --- /dev/null +++ b/conductor/conductor/solver/request/functions/hpa_score.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 Intel Corporation Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# + +'''Objective function for hpa_score + Hardware Platform Awareness (HPA)''' + + +class HPAScore(object): + + def __init__(self, _type): + self.func_type = _type + self.score = 0 diff --git a/conductor/conductor/solver/request/objective.py b/conductor/conductor/solver/request/objective.py index 0559056..d957581 100755 --- a/conductor/conductor/solver/request/objective.py +++ b/conductor/conductor/solver/request/objective.py @@ -109,6 +109,20 @@ class Operand(object): for demand_name, candidate_info in _decision_path.decisions.items(): value += float(candidate_info['cost']) + elif self.function.func_type == "hpa_score": + # Currently only minimize objective goal is supported + # Higher the HPA score the better. + # Invert HPA Score if goal is minimize + invert = -1 + + # + # if self.function.goal == "max": + # invert = 1 + + for demand_name, candidate_info in _decision_path.decisions.items(): + hpa_score = invert * float(candidate_info.get('hpa_score', 0)) + value += hpa_score + if self.operation == "product": value *= self.weight diff --git a/conductor/conductor/solver/request/parser.py b/conductor/conductor/solver/request/parser.py index 0def215..da031cc 100755 --- a/conductor/conductor/solver/request/parser.py +++ b/conductor/conductor/solver/request/parser.py @@ -41,6 +41,7 @@ from conductor.solver.request import objective from conductor.solver.request.functions import aic_version from conductor.solver.request.functions import cost from conductor.solver.request.functions import distance_between +from conductor.solver.request.functions import hpa_score from oslo_log import log # from conductor.solver.request.functions import distance_between @@ -263,6 +264,9 @@ class Parser(object): func = cost.Cost("cost") func.loc = operand_data["function_param"] operand.function = func + elif operand_data["function"] == "hpa_score": + func = hpa_score.HPAScore("hpa_score") + operand.function = func self.objective.operand_list.append(operand) diff --git a/conductor/conductor/solver/service.py b/conductor/conductor/solver/service.py index e539acd..56ec683 100644 --- a/conductor/conductor/solver/service.py +++ b/conductor/conductor/solver/service.py @@ -347,10 +347,10 @@ class SolverService(cotyledon.Service): rec["candidate"]["host_id"] = resource.get("host_id") if rec["candidate"]["inventory_type"] == "cloud": - if resource.get("flavor_map"): - rec["attributes"]["flavors"] = resource.get( - "flavor_map") - + if resource.get("all_directives") and resource.get("flavor_map"): + rec["attributes"]["directives"] = \ + self.set_flavor_in_flavor_directives( + resource.get("flavor_map"), resource.get("all_directives")) # TODO(snarayanan): Add total value to recommendations? # msg = "--- total value of decision = {}" # LOG.debug(msg.format(_best_path.total_value)) @@ -387,3 +387,18 @@ class SolverService(cotyledon.Service): """Reload""" LOG.debug("%s" % self.__class__.__name__) self._restart() + + def set_flavor_in_flavor_directives(self, flavor_map, directives): + ''' + Insert the flavor name inside the flavor_map into flavor_directives + :param flavor_map: flavor map get + :param directives: All the directives get from request + ''' + flavor_label = flavor_map.keys() + for ele in directives.get("directives"): + for item in ele.get("directives"): + if "flavor_directives" in item.get("type"): + for attr in item.get("attributes"): + attr["attribute_value"] = flavor_map.get(attr["attribute_name"]) \ + if attr.get("attribute_name") in flavor_label else "" + return directives diff --git a/conductor/conductor/solver/utils/constraint_engine_interface.py b/conductor/conductor/solver/utils/constraint_engine_interface.py index 43331aa..bbb782d 100644 --- a/conductor/conductor/solver/utils/constraint_engine_interface.py +++ b/conductor/conductor/solver/utils/constraint_engine_interface.py @@ -117,7 +117,7 @@ class ConstraintEngineInterface(object): # response is a list of (candidate, cost) tuples return response - def get_candidates_with_hpa(self, label_name, candidate_list, + def get_candidates_with_hpa(self, id, type, directives, candidate_list, flavorProperties): ''' Returns the candidate_list with an addition of flavor_mapping for @@ -130,7 +130,9 @@ class ConstraintEngineInterface(object): ctxt = {} args = {"candidate_list": candidate_list, "flavorProperties": flavorProperties, - "label_name": label_name} + "id": id, + "type": type, + "directives": directives} response = self.client.call(ctxt=ctxt, method="get_candidates_with_hpa", args=args) diff --git a/conductor/conductor/tests/unit/controller/hpa_constraints.json b/conductor/conductor/tests/unit/controller/hpa_constraints.json new file mode 100644 index 0000000..a2543bc --- /dev/null +++ b/conductor/conductor/tests/unit/controller/hpa_constraints.json @@ -0,0 +1,221 @@ +{ + "HAS_Template": { + "constraints": { + "hpa_constraint_vG": { + "demands": [ + "vG" + ], + "name": "hpa_constraint_vG", + "type": "hpa", + "properties": { + "evaluate": [ + { + "flavorProperties": [ + { + "architecture": "generic", + "hpa-feature": "basicCapabilities", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numVirtualCpu", + "hpa-attribute-value": "4", + "operator": "=" + }, + { + "hpa-attribute-key": "virtualMemSize", + "hpa-attribute-value": "4", + "operator": "=", + "unit": "GB" + } + ], + "hpa-version": "v1", + "directives": [] + }, + { + "architecture": "generic", + "hpa-feature": "numa", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numaNodes", + "hpa-attribute-value": "2", + "operator": "=" + }, + { + "hpa-attribute-key": "numaCpu-0", + "hpa-attribute-value": "2", + "operator": "=" + }, + { + "hpa-attribute-key": "numaCpu-1", + "hpa-attribute-value": "4", + "operator": "=" + }, + { + "hpa-attribute-key": "numaMem-0", + "hpa-attribute-value": "2", + "operator": "=", + "unit": "GB" + }, + { + "hpa-attribute-key": "numaMem-1", + "hpa-attribute-value": "4", + "operator": "=", + "unit": "GB" + } + ], + "hpa-version": "v1", + "directives": [] + }, + { + "architecture": "generic", + "hpa-feature": "cpuPinning", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "logicalCpuThreadPinningPolicy", + "hpa-attribute-value": "prefer", + "operator": "=" + }, + { + "hpa-attribute-key": "logicalCpuPinningPolicy", + "hpa-attribute-value": "dedicated", + "operator": "=" + } + ], + "hpa-version": "v1", + "directives": [] + } + ], + "id": "vg_1", + "type": "vnfc", + "directives": [ + { + "type": "flavor_directives", + "attributes": [ + { + "attribute_name": "flavor_label_1", + "attribute_value": "" + } + ] + } + ] + }, + { + "flavorProperties": [ + { + "architecture": "generic", + "hpa-feature": "basicCapabilities", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numVirtualCpu", + "hpa-attribute-value": "8", + "operator": "=" + }, + { + "hpa-attribute-key": "virtualMemSize", + "hpa-attribute-value": "16", + "operator": "=", + "unit": "GB" + } + ], + "hpa-version": "v1", + "directives": [] + }, + { + "architecture": "generic", + "hpa-feature": "numa", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numaNodes", + "hpa-attribute-value": "2", + "operator": "=" + }, + { + "hpa-attribute-key": "numaCpu-0", + "hpa-attribute-value": "2", + "operator": "=" + }, + { + "hpa-attribute-key": "numaCpu-1", + "hpa-attribute-value": "4", + "operator": "=" + }, + { + "hpa-attribute-key": "numaMem-0", + "hpa-attribute-value": "2", + "operator": "=", + "unit": "GB" + }, + { + "hpa-attribute-key": "numaMem-1", + "hpa-attribute-value": "4", + "operator": "=", + "unit": "GB" + } + ], + "hpa-version": "v1", + "directives": [] + }, + { + "architecture": "generic", + "hpa-feature": "memoryPageSize", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "memoryPageSize", + "hpa-attribute-value": "2", + "operator": "=", + "unit": "GB" + } + ], + "hpa-version": "v1", + "directives": [] + } + ], + "id": "vg_2", + "type": "vnfc", + "directives": [ + { + "type": "flavor_directives", + "attributes": [ + { + "attribute_name": "flavor_label_2", + "attribute_value": "" + } + ] + } + ] + } + ] + } + }, + "constraint_vgmux_customer": { + "type": "distance_to_location", + "demands": [ + "vGMuxInfra" + ], + "properties": { + "distance": "< 100 km", + "location": "customer_loc" + } + }, + "check_cloud_capacity": { + "type": "vim_fit", + "demands": [ + "vG" + ], + "properties": { + "controller": "multicloud", + "request": { + "vCPU": 10, + "Memory": { + "quantity": "10", + "unit": "GB" + }, + "Storage": { + "quantity": "100", + "unit": "GB" + } + } + } + } + } + } +}
\ No newline at end of file diff --git a/conductor/conductor/tests/unit/controller/test_translator.py b/conductor/conductor/tests/unit/controller/test_translator.py index ba0e3ec..c61e57e 100644 --- a/conductor/conductor/tests/unit/controller/test_translator.py +++ b/conductor/conductor/tests/unit/controller/test_translator.py @@ -232,12 +232,25 @@ class TestNoExceptionTranslator(unittest.TestCase): ], "properties": { "evaluate": [ - {'flavorLabel': 'xx', + {'id': 'vg_0', + 'type': 'vnfc', + 'directives': [ + { + "type": "flavor_directives", + "attributes": [ + { + "attribute_name": "label_0", + "attribute_value": "" + } + ] + } + ], 'flavorProperties': [{ 'hpa-feature': 'BasicCapabilities', 'hpa-version': 'v1', 'architecture': 'generic', 'mandatory': 'False', + 'directives': [], 'score': '5', 'hpa-feature-attributes': [ { @@ -264,6 +277,7 @@ class TestNoExceptionTranslator(unittest.TestCase): {'architecture': 'generic', 'mandatory': 'False', 'score': '5', + 'directives': [], 'hpa-feature': 'BasicCapabilities', 'hpa-feature-attributes': [ { @@ -279,7 +293,20 @@ class TestNoExceptionTranslator(unittest.TestCase): } ], 'hpa-version': 'v1'}], - 'flavorLabel': 'xx'}]}, + 'id': 'vg_0', + 'type': 'vnfc', + 'directives': [ + { + 'type': 'flavor_directives', + 'attributes': [ + { + 'attribute_name': 'label_0', + 'attribute_value': '' + } + ] + } + ] + }]}, 'type': 'hpa' } } @@ -295,12 +322,25 @@ class TestNoExceptionTranslator(unittest.TestCase): ], "properties": { "evaluate": [ - {'flavorLabel': 'xx', + {'id': 'vg_0', + 'type': 'vnfc', + 'directives': [ + { + 'type': 'flavor_directives', + 'attributes': [ + { + 'attribute_name': 'label_0', + 'attribute_value': '' + } + ] + } + ], 'flavorProperties': [{ 'hpa-feature': 'BasicCapabilities', 'hpa-version': 'v1', 'architecture': 'generic', 'mandatory': 'True', + 'directives': [], 'hpa-feature-attributes': [ { 'hpa-attribute-key': 'numVirtualCpu', @@ -325,6 +365,7 @@ class TestNoExceptionTranslator(unittest.TestCase): 'flavorProperties': [ {'architecture': 'generic', 'mandatory': 'True', + 'directives': [], 'hpa-feature': 'BasicCapabilities', 'hpa-feature-attributes': [ { @@ -340,7 +381,19 @@ class TestNoExceptionTranslator(unittest.TestCase): } ], 'hpa-version': 'v1'}], - 'flavorLabel': 'xx'}]}, + 'id': 'vg_0', + 'type': 'vnfc', + 'directives': [ + { + 'type': 'flavor_directives', + 'attributes': [ + { + 'attribute_name': 'label_0', + 'attribute_value': '' + } + ] + } + ]}]}, 'type': 'hpa' } } @@ -356,7 +409,7 @@ class TestNoExceptionTranslator(unittest.TestCase): "vG" ], "properties": { - "evaluate": [{'flavor': 'xx', + "evaluate": [{'id': 'xx', 'flavorProperties': []}] } } @@ -369,7 +422,8 @@ class TestNoExceptionTranslator(unittest.TestCase): ], "properties": { "evaluate": [ - {'flavorLabel': 'xx', + {'id': 'xx', + 'type': 'xx', 'flavorProperties': [ { 'hpa-feature': '', @@ -393,7 +447,9 @@ class TestNoExceptionTranslator(unittest.TestCase): "properties": { "evaluate": [ { - "flavorLabel": "xx", + "id": "xx", + "type": 'xx', + "directives": [], "flavorProperties": [ { "hpa-feature": "BasicCapabilities", @@ -422,7 +478,9 @@ class TestNoExceptionTranslator(unittest.TestCase): "vG" ], "properties": { - "evaluate": [{'flavorLabel': 'xx', + "evaluate": [{'id': 'xx', + "type": 'xx', + "directives": [], 'flavorProperties': [{ 'hpa-feature': '', 'architecture': '', @@ -573,6 +631,62 @@ class TestNoExceptionTranslator(unittest.TestCase): opt), expected_parse) @patch('conductor.controller.translator.Translator.create_components') + def test_parse_optimization_multi_objective(self, mock_create): + hpa_json_file = './conductor/tests/unit/controller/hpa_constraints.json' + hpa_json = yaml.safe_load(open(hpa_json_file).read()) + expected_parse = {'goal': 'min', + 'operands': [{'function': 'distance_between', + 'function_param': ['customer_loc', + 'vGMuxInfra'], + 'operation': 'product', + 'weight': 2.0}, + {'function': 'distance_between', + 'function_param': ['customer_loc', + 'vG'], + 'operation': 'product', + 'weight': 4.0}, + {'function': 'hpa_score', + 'function_param': ['vG'], + 'operation': 'product', + 'weight': 8.0}, + ], + 'operation': 'sum' + } + + opt = {'minimize': { + 'sum': [{'product': [2.0, {'distance_between': ['customer_loc', 'vGMuxInfra']}]}, + {'product': [4.0, {'distance_between': ['customer_loc', 'vG']}]}, + {'product': [8.0, {'hpa_score': ['vG']}]} + ]}} + self.Translator._demands = {'vG': '', + 'vGMuxInfra': '', + 'customer_loc': ''} + self.Translator._locations = {'vG': '', + 'vGMuxInfra': '', + 'customer_loc': ''} + self.Translator._constraints = hpa_json["HAS_Template"]["constraints"] + self.maxDiff = None + self.assertEquals( + self.Translator.parse_optimization( + opt), expected_parse) + + # No HPA Policy test + non_hpa_dict = dict(hpa_json["HAS_Template"]["constraints"]) + non_hpa_dict.pop("hpa_constraint_vG") + self.Translator._constraints = non_hpa_dict + self.maxDiff = None + self.assertRaises(TranslatorException, + self.Translator.parse_optimization, opt) + + # HPA Policy Exists but not for the demand in objective function + hpa_wrong_demand_dict = dict(hpa_json["HAS_Template"]["constraints"]) + hpa_wrong_demand_dict["hpa_constraint_vG"]["demands"] = ["vGMuxInfra"] + self.Translator._constraints = hpa_wrong_demand_dict + self.maxDiff = None + self.assertRaises(TranslatorException, + self.Translator.parse_optimization, opt) + + @patch('conductor.controller.translator.Translator.create_components') def test_parse_reservation(self, mock_create): expected_resv = {'counter': 0, 'demands': { 'instance_vG': {'demands': {'vG': 'null'}, diff --git a/conductor/conductor/tests/unit/data/hpa_constraints.json b/conductor/conductor/tests/unit/data/hpa_constraints.json index 829eaf3..0f42cc9 100644 --- a/conductor/conductor/tests/unit/data/hpa_constraints.json +++ b/conductor/conductor/tests/unit/data/hpa_constraints.json @@ -26,7 +26,8 @@ "unit": "GB" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] }, { "architecture": "generic", @@ -60,7 +61,8 @@ "unit": "GB" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] }, { "architecture": "generic", @@ -77,10 +79,23 @@ "operator": "=" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] } ], - "flavorLabel": "flavor_label_1" + "id": "vg_1", + "type": "vnfc", + "directives": [ + { + "type": "flavor_directives", + "attributes": [ + { + "attribute_name": "flavor_label_1", + "attribute_value": "" + } + ] + } + ] }, { "flavorProperties": [ @@ -100,7 +115,8 @@ "unit": "GB" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] }, { "architecture": "generic", @@ -134,7 +150,8 @@ "unit": "GB" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] }, { "architecture": "generic", @@ -147,10 +164,23 @@ "unit": "GB" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] } ], - "flavorLabel": "flavor_label_2" + "id": "vg_2", + "type": "vnfc", + "directives": [ + { + "type": "flavor_directives", + "attributes": [ + { + "attribute_name": "flavor_label_2", + "attribute_value": "" + } + ] + } + ] } ] } diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/hpa_flavors.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/hpa_flavors.json index db5ea54..19cf8b9 100644 --- a/conductor/conductor/tests/unit/data/plugins/inventory_provider/hpa_flavors.json +++ b/conductor/conductor/tests/unit/data/plugins/inventory_provider/hpa_flavors.json @@ -198,6 +198,105 @@ "resource-version": "1520943846264" }, { + "flavor-id": "f5aa2b2e-3206-41b6-80d5-cf6t2b098c43", + "flavor-name": "flavor-ovsdpdk-cpu-pinning-sriov-NIC-Network-set", + "flavor-vcpus": 32, + "flavor-ram": 131072, + "flavor-disk": 2097152, + "flavor-ephemeral": 128, + "flavor-swap": "0", + "flavor-is-public": false, + "flavor-selflink": "pXtX", + "flavor-disabled": false, + "hpa-capabilities": { + "hpa-capability": [ + { + "hpa-capability-id": "8d36a8fe-bfee-446a-bbcb-881ee66c8f78", + "hpa-feature": "ovsDpdk", + "hpa-version": "v1", + "architecture": "generic", + "resource-version": "1520943846328", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "dataProcessingAccelerationLibrary", + "hpa-attribute-value": "{\"value\":\"v18.02\"}", + "resource-version": "1520943846346" + } + ] + }, + { + "hpa-capability-id": "c140c945-1532-4908-86c9-d7f71416f1dd", + "hpa-feature": "cpuPinning", + "hpa-version": "v1", + "architecture": "generic", + "resource-version": "1520943846297", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "logicalCpuPinningPolicy", + "hpa-attribute-value": "{\"value\":\"dedicated\"}", + "resource-version": "1520943846312" + }, + { + "hpa-attribute-key": "logicalCpuThreadPinningPolicy", + "hpa-attribute-value": "{\"value\":\"prefer\"}", + "resource-version": "1520943846301" + } + ] + }, + { + "hpa-capability-id": "4565615b-1077-4bb5-a340-c5be48db2aaa", + "hpa-feature": "basicCapabilities", + "hpa-version": "v1", + "architecture": "generic", + "resource-version": "1520943846269", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "virtualMemSize", + "hpa-attribute-value": "{\"value\":\"16\", \"unit\":\"GB\" }", + "resource-version": "1520943846282" + }, + { + "hpa-attribute-key": "numVirtualCpu", + "hpa-attribute-value": "{\"value\":\"8\"}", + "resource-version": "1520943846272" + } + ] + }, + { + "hpa-capability-id": "8fa22e64-41b4-471f-96ad-6c470868eo4c", + "hpa-feature": "sriovNICNetwork", + "hpa-version": "v1", + "architecture": "generic", + "resource-version": "1520943845443", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "pciCount", + "hpa-attribute-value": "{\"value\":\"1\"}", + "resource-version": "1520943845870" + }, + { + "hpa-attribute-key": "pciVendorId", + "hpa-attribute-value": "{\"value\": \"8086\"}", + "resource-version": "1520943845764" + }, + { + "hpa-attribute-key": "pciDeviceId", + "hpa-attribute-value": "{\"value\": \"1234\"}", + "resource-version": "1520943847729" + }, + { + "hpa-attribute-key": "physicalNetwork", + "hpa-attribute-value": "{\"value\": \"physnet1\"}", + "resource-version": "1520943871129" + } + ] + } + + ] + }, + "resource-version": "1520943439264" + }, + { "flavor-id": "f5aa2b2e-3206-41b6-80d5-cf041b098c43", "flavor-name": "flavor-cpu-pinning-ovsdpdk-instruction-set", "flavor-vcpus": 32, diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/hpa_req_features.json b/conductor/conductor/tests/unit/data/plugins/inventory_provider/hpa_req_features.json index 4a5d37a..bfda6a7 100644 --- a/conductor/conductor/tests/unit/data/plugins/inventory_provider/hpa_req_features.json +++ b/conductor/conductor/tests/unit/data/plugins/inventory_provider/hpa_req_features.json @@ -1,200 +1,327 @@ [ - [ - { - "hpa-feature":"basicCapabilities", - "hpa-version":"v1", - "architecture":"generic", - "mandatory":"True", - "hpa-feature-attributes":[ + [ + { + "hpa-feature": "basicCapabilities", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "True", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numVirtualCpu", + "hpa-attribute-value": "8", + "operator": "=" + }, + { + "hpa-attribute-key": "virtualMemSize", + "hpa-attribute-value": "16384", + "operator": "=", + "unit": "MB" + } + ] + }, + { + "hpa-feature": "cpuPinning", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "True", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "logicalCpuThreadPinningPolicy", + "hpa-attribute-value": "prefer", + "operator": "=" + }, + { + "hpa-attribute-key": "logicalCpuPinningPolicy", + "hpa-attribute-value": "dedicated", + "operator": "=" + } + ] + }, + { + "hpa-feature": "cpuTopology", + "hpa-version": "v1", + "mandatory": "True", + "architecture": "generic", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numCpuSockets", + "hpa-attribute-value": "2", + "operator": ">=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuSockets", + "hpa-attribute-value": "4", + "operator": "<=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuCores", + "hpa-attribute-value": "2", + "operator": ">=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuCores", + "hpa-attribute-value": "4", + "operator": "<=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuThreads", + "hpa-attribute-value": "4", + "operator": ">=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuThreads", + "hpa-attribute-value": "8", + "operator": "<=", + "unit": "" + } + ] + } + ], + [ + { + "hpa-feature": "basicCapabilities", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "True", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numVirtualCpu", + "hpa-attribute-value": "8", + "operator": "=" + }, + { + "hpa-attribute-key": "virtualMemSize", + "hpa-attribute-value": "16384", + "operator": "=", + "unit": "MB" + } + ] + }, + { + "hpa-feature": "ovsDpdk", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "False", + "score": "5", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "dataProcessingAccelerationLibrary", + "hpa-attribute-value": "v18.02", + "operator": "=" + } + ] + }, + { + "hpa-feature": "cpuPinning", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "False", + "score": "1", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "logicalCpuThreadPinningPolicy", + "hpa-attribute-value": "prefer", + "operator": "=" + }, + { + "hpa-attribute-key": "logicalCpuPinningPolicy", + "hpa-attribute-value": "dedicated", + "operator": "=" + } + ] + }, + { + "hpa-feature": "instructionSetExtensions", + "hpa-version": "v1", + "architecture": "Intel64", + "mandatory": "False", + "score": "5", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "instructionSetExtensions", + "hpa-attribute-value": [ + "A11", + "B22" + ], + "operator": "ALL" + } + ] + }, + { + "hpa-feature": "cpuTopology", + "hpa-version": "v1", + "mandatory": "True", + "architecture": "generic", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numCpuSockets", + "hpa-attribute-value": "2", + "operator": ">=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuSockets", + "hpa-attribute-value": "4", + "operator": "<=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuCores", + "hpa-attribute-value": "2", + "operator": ">=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuCores", + "hpa-attribute-value": "4", + "operator": "<=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuThreads", + "hpa-attribute-value": "4", + "operator": ">=", + "unit": "" + }, + { + "hpa-attribute-key": "numCpuThreads", + "hpa-attribute-value": "8", + "operator": "<=", + "unit": "" + } + ] + } + ], + [ + { + "hpa-feature": "basicCapabilities", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "True", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numVirtualCpu", + "hpa-attribute-value": "8", + "operator": "=" + }, + { + "hpa-attribute-key": "virtualMemSize", + "hpa-attribute-value": "16", + "operator": "=", + "unit": "GB" + } + ] + }, + { + "hpa-feature": "ovsDpdk", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "False", + "score": "5", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "dataProcessingAccelerationLibrary", + "hpa-attribute-value": "v18.02", + "operator": "=" + } + ] + }, + { + "hpa-feature": "cpuPinning", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "False", + "score": "1", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "logicalCpuThreadPinningPolicy", + "hpa-attribute-value": "prefer", + "operator": "=" + }, + { + "hpa-attribute-key": "logicalCpuPinningPolicy", + "hpa-attribute-value": "dedicated", + "operator": "=" + } + ] + }, + { + "hpa-feature": "instructionSetExtensions", + "hpa-version": "v1", + "architecture": "Intel64", + "mandatory": "False", + "score": "5", + "directives": [], + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "instructionSetExtensions", + "hpa-attribute-value": [ + "A11", + "B22" + ], + "operator": "ALL" + } + ] + }, + { + "hpa-feature": "sriovNICNetwork", + "hpa-version": "v1", + "mandatory": "False", + "architecture": "generic", + "directives": [ + { + "type": "sriovNICNetwork_directives", + "attributes": [ { - "hpa-attribute-key":"numVirtualCpu", - "hpa-attribute-value":"8", - "operator":"=" - }, - { - "hpa-attribute-key":"virtualMemSize", - "hpa-attribute-value":"16384", - "operator":"=", - "unit":"MB" - } - ] - }, - { - "hpa-feature":"cpuPinning", - "hpa-version":"v1", - "architecture":"generic", - "mandatory":"True", - "hpa-feature-attributes":[ - { - "hpa-attribute-key":"logicalCpuThreadPinningPolicy", - "hpa-attribute-value":"prefer", - "operator":"=" - }, - { - "hpa-attribute-key":"logicalCpuPinningPolicy", - "hpa-attribute-value":"dedicated", - "operator":"=" - } - ] - }, - { - "hpa-feature":"cpuTopology", - "hpa-version":"v1", - "mandatory":"True", - "architecture":"generic", - "hpa-feature-attributes":[ - { - "hpa-attribute-key":"numCpuSockets", - "hpa-attribute-value":"2", - "operator":">=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuSockets", - "hpa-attribute-value":"4", - "operator":"<=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuCores", - "hpa-attribute-value":"2", - "operator":">=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuCores", - "hpa-attribute-value":"4", - "operator":"<=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuThreads", - "hpa-attribute-value":"4", - "operator":">=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuThreads", - "hpa-attribute-value":"8", - "operator":"<=", - "unit":"" - } - ] - } - ], - [ - { - "hpa-feature":"basicCapabilities", - "hpa-version":"v1", - "architecture":"generic", - "mandatory":"True", - "hpa-feature-attributes":[ - { - "hpa-attribute-key":"numVirtualCpu", - "hpa-attribute-value":"8", - "operator":"=" - }, - { - "hpa-attribute-key":"virtualMemSize", - "hpa-attribute-value":"16384", - "operator":"=", - "unit":"MB" - } - ] - }, - { - "hpa-feature":"ovsDpdk", - "hpa-version":"v1", - "architecture":"generic", - "mandatory":"False", - "score":"5", - "hpa-feature-attributes":[ - { - "hpa-attribute-key":"dataProcessingAccelerationLibrary", - "hpa-attribute-value":"v18.02", - "operator":"=" - } - ] - }, - { - "hpa-feature":"cpuPinning", - "hpa-version":"v1", - "architecture":"generic", - "mandatory":"False", - "score":"1", - "hpa-feature-attributes":[ - { - "hpa-attribute-key":"logicalCpuThreadPinningPolicy", - "hpa-attribute-value":"prefer", - "operator":"=" - }, - { - "hpa-attribute-key":"logicalCpuPinningPolicy", - "hpa-attribute-value":"dedicated", - "operator":"=" - } - ] - }, - { - "hpa-feature":"instructionSetExtensions", - "hpa-version":"v1", - "architecture":"Intel64", - "mandatory":"False", - "score":"5", - "hpa-feature-attributes":[ - { - "hpa-attribute-key":"instructionSetExtensions", - "hpa-attribute-value":[ - "A11", - "B22" - ], - "operator":"ALL" - } - ] - }, - { - "hpa-feature":"cpuTopology", - "hpa-version":"v1", - "mandatory":"True", - "architecture":"generic", - "hpa-feature-attributes":[ - { - "hpa-attribute-key":"numCpuSockets", - "hpa-attribute-value":"2", - "operator":">=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuSockets", - "hpa-attribute-value":"4", - "operator":"<=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuCores", - "hpa-attribute-value":"2", - "operator":">=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuCores", - "hpa-attribute-value":"4", - "operator":"<=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuThreads", - "hpa-attribute-value":"4", - "operator":">=", - "unit":"" - }, - { - "hpa-attribute-key":"numCpuThreads", - "hpa-attribute-value":"8", - "operator":"<=", - "unit":"" + "attribute_name": "A", + "attribute_value": "a" } - ] - } - ] + ] + } + ], + "score": "7", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "pciCount", + "hpa-attribute-value": "1", + "operator": "=", + "unit": "" + }, + { + "hpa-attribute-key": "pciVendorId", + "hpa-attribute-value": "8086", + "operator": "=", + "unit": "" + }, + { + "hpa-attribute-key": "pciDeviceId", + "hpa-attribute-value": "1234", + "operator": "=", + "unit": "" + }, + { + "hpa-attribute-key": "physicalNetwork", + "hpa-attribute-value": "physnet1", + "operator": "=", + "unit": "" + } + ] + } + ] ]
\ No newline at end of file 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 e12a114..435e0a9 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 @@ -28,7 +28,6 @@ from oslo_config import cfg class TestAAI(unittest.TestCase): def setUp(self): - CONF = cfg.CONF CONF.register_opts(aai.AAI_OPTS, group='aai') self.conf = CONF @@ -38,20 +37,16 @@ class TestAAI(unittest.TestCase): mock.patch.stopall() def test_get_version_from_string(self): - self.assertEqual("2.5", self.aai_ep._get_version_from_string("AAI2.5")) self.assertEqual("3.0", self.aai_ep._get_version_from_string("AAI3.0")) def test_aai_versioned_path(self): - self.assertEqual('/{}/cloud-infrastructure/cloud-regions/?depth=0'.format(self.conf.aai.server_url_version), self.aai_ep._aai_versioned_path("/cloud-infrastructure/cloud-regions/?depth=0")) self.assertEqual('/{}/query?format=id'.format(self.conf.aai.server_url_version), self.aai_ep._aai_versioned_path("/query?format=id")) - def test_resolve_clli_location(self): - req_json_file = './conductor/tests/unit/data/plugins/inventory_provider/_request_clli_location.json' req_json = json.loads(open(req_json_file).read()) @@ -62,11 +57,10 @@ class TestAAI(unittest.TestCase): self.mock_get_request = mock.patch.object(AAI, '_request', return_value=response) self.mock_get_request.start() - self.assertEqual({'country': u'USA', 'latitude': u'40.39596', 'longitude': u'-74.135342'} , - self.aai_ep.resolve_clli_location("clli_code")) + self.assertEqual({'country': u'USA', 'latitude': u'40.39596', 'longitude': u'-74.135342'}, + self.aai_ep.resolve_clli_location("clli_code")) def test_get_inventory_group_pair(self): - req_json_file = './conductor/tests/unit/data/plugins/inventory_provider/_request_inventory_group_pair.json' req_json = json.loads(open(req_json_file).read()) @@ -77,11 +71,10 @@ class TestAAI(unittest.TestCase): self.mock_get_request = mock.patch.object(AAI, '_request', return_value=response) self.mock_get_request.start() - self.assertEqual([[u'instance-1', u'instance-2']] , - self.aai_ep.get_inventory_group_pairs("service_description")) + self.assertEqual([[u'instance-1', u'instance-2']], + self.aai_ep.get_inventory_group_pairs("service_description")) def test_resolve_host_location(self): - req_json_file = './conductor/tests/unit/data/plugins/inventory_provider/_request_host_name.json' req_json = json.loads(open(req_json_file).read()) @@ -99,11 +92,10 @@ class TestAAI(unittest.TestCase): self.mock_get_complex = mock.patch.object(AAI, '_get_complex', return_value=complex_json) self.mock_get_complex.start() - self.assertEqual({'country': u'USA', 'latitude': u'28.543251', 'longitude': u'-81.377112'} , + self.assertEqual({'country': u'USA', 'latitude': u'28.543251', 'longitude': u'-81.377112'}, self.aai_ep.resolve_host_location("host_name")) def test_resolve_demands(self): - self.assertEqual({}, self.aai_ep.resolve_demands(dict())) demands_list_file = './conductor/tests/unit/data/plugins/inventory_provider/demand_list.json' @@ -126,7 +118,8 @@ class TestAAI(unittest.TestCase): req_response.ok = True req_response.json.return_value = demand_service_response - self.mock_first_level_service_call = mock.patch.object(AAI, 'first_level_service_call', return_value=generic_vnf_list) + self.mock_first_level_service_call = mock.patch.object(AAI, 'first_level_service_call', + return_value=generic_vnf_list) self.mock_first_level_service_call.start() self.mock_get_regions = mock.patch.object(AAI, '_get_regions', return_value=regions_response) @@ -167,7 +160,6 @@ class TestAAI(unittest.TestCase): self.aai_ep.resolve_demands(demands_list)) def test_get_complex(self): - complex_json_file = './conductor/tests/unit/data/plugins/inventory_provider/_request_get_complex.json' complex_json = json.loads(open(complex_json_file).read()) @@ -179,12 +171,12 @@ class TestAAI(unittest.TestCase): self.mock_get_request = mock.patch.object(AAI, '_request', return_value=response) self.mock_get_request.start() - self.assertEqual({u'city': u'Middletown', u'latitude': u'28.543251', u'longitude': u'-81.377112', u'country': u'USA', u'region': u'SE'} , - self.aai_ep._get_complex("/v10/complex/complex_id", "complex_id")) - + self.assertEqual( + {u'city': u'Middletown', u'latitude': u'28.543251', u'longitude': u'-81.377112', u'country': u'USA', + u'region': u'SE'}, + self.aai_ep._get_complex("/v10/complex/complex_id", "complex_id")) def test_check_network_roles(self): - network_role_json_file = './conductor/tests/unit/data/plugins/inventory_provider/_request_network_role.json' network_role_json = json.loads(open(network_role_json_file).read()) @@ -195,12 +187,10 @@ class TestAAI(unittest.TestCase): self.mock_get_request = mock.patch.object(AAI, '_request', return_value=response) self.mock_get_request.start() - self.assertEqual(set(['test-cloud-value']) , - self.aai_ep.check_network_roles("network_role_id")) - + self.assertEqual(set(['test-cloud-value']), + self.aai_ep.check_network_roles("network_role_id")) def test_check_candidate_role(self): - candidate_role_json_file = './conductor/tests/unit/data/plugins/inventory_provider/_request_candidate_role.json' candidate_role_json = json.loads(open(candidate_role_json_file).read()) @@ -223,7 +213,8 @@ class TestAAI(unittest.TestCase): inventory_attributes['attr-1'] = 'attr-1-value1' self.assertEqual(True, - self.aai_ep.match_inventory_attributes(template_attributes, inventory_attributes, "candidate-id")) + self.aai_ep.match_inventory_attributes(template_attributes, inventory_attributes, + "candidate-id")) template_attributes['attr-1'] = { 'not': ['attr-1-value2'] @@ -268,7 +259,6 @@ class TestAAI(unittest.TestCase): self.aai_ep._refresh_cache()) def test_get_aai_rel_link(self): - relatonship_response_file = './conductor/tests/unit/data/plugins/inventory_provider/relationship_list.json' relatonship_response = json.loads(open(relatonship_response_file).read()) related_to = "service-instance" @@ -301,7 +291,7 @@ class TestAAI(unittest.TestCase): def test_match_hpa(self): flavor_json_file = \ - './conductor/tests/unit/data/plugins/inventory_provider/hpa_flavors.json' + './conductor/tests/unit/data/plugins/inventory_provider/hpa_flavors.json' flavor_json = json.loads(open(flavor_json_file).read()) feature_json_file = \ './conductor/tests/unit/data/plugins/inventory_provider/hpa_req_features.json' @@ -310,15 +300,34 @@ class TestAAI(unittest.TestCase): candidate_json = json.loads(open(candidate_json_file).read()) candidate_json['candidate_list'][1]['flavors'] = flavor_json - flavor_map = {"flavor-id": "f5aa2b2e-3206-41b6-80d5-cf041b098c43", - "flavor-name": "flavor-cpu-pinning-ovsdpdk-instruction-set"} + flavor_map = { + "directives": [], + "flavor_map": {"flavor-id": "f5aa2b2e-3206-41b6-80d5-cf041b098c43", + "flavor-name": "flavor-cpu-pinning-ovsdpdk-instruction-set", + "score": 0}} self.assertEqual(flavor_map, self.aai_ep.match_hpa(candidate_json['candidate_list'][1], feature_json[0])) - flavor_map = {"flavor-id": "f5aa2b2e-3206-41b6-80d5-cf041b098c43", - "flavor-name": "flavor-cpu-ovsdpdk-instruction-set" } + flavor_map = {"flavor_map": {"flavor-id": "f5aa2b2e-3206-41b6-80d5-cf041b098c43", + "flavor-name": "flavor-cpu-ovsdpdk-instruction-set", + "score": 10}, + "directives": []} self.assertEqual(flavor_map, - self.aai_ep.match_hpa(candidate_json['candidate_list'][1], - feature_json[1])) - + self.aai_ep.match_hpa(candidate_json['candidate_list'][1], + feature_json[1])) + flavor_map = {"flavor_map": {"flavor-id": "f5aa2b2e-3206-41b6-80d5-cf6t2b098c43", + "flavor-name": "flavor-ovsdpdk-cpu-pinning-sriov-NIC-Network-set", + "score": 13}, + "directives": [{ + "type": "sriovNICNetwork_directives", + "attributes": [ + { + "attribute_name": "A", + "attribute_value": "a" + } + ] + }]} + self.assertEqual(flavor_map, + self.aai_ep.match_hpa(candidate_json['candidate_list'][1], + feature_json[2])) diff --git a/conductor/conductor/tests/unit/data/test_service.py b/conductor/conductor/tests/unit/data/test_service.py index 01c2ab3..7953f3f 100644 --- a/conductor/conductor/tests/unit/data/test_service.py +++ b/conductor/conductor/tests/unit/data/test_service.py @@ -238,23 +238,38 @@ class TestDataEndpoint(unittest.TestCase): hpa_json["conductor_solver"]["constraints"][0].items()[0] hpa_constraint = constraint_info['properties'] flavorProperties = hpa_constraint['evaluate'][0]['flavorProperties'] - label_name = hpa_constraint['evaluate'][0]['flavorLabel'] + id = hpa_constraint['evaluate'][0]['id'] + type = hpa_constraint['evaluate'][0]['type'] + directives = hpa_constraint['evaluate'][0]['directives'] + attr = directives[0].get("attributes") + label_name = attr[0].get("attribute_name") ext_mock1.return_value = ['aai'] flavor_info = {"flavor-id": "vim-flavor-id1", "flavor-name": "vim-flavor-name1"} + directive = [ + { + "id": id, + "type": type, + "directives": directives + } + ] hpa_mock.return_value = [flavor_info] self.maxDiff = None - args = generate_args(candidate_list, flavorProperties, label_name) + args = generate_args(candidate_list, flavorProperties, id, type, directives) hpa_candidate_list = copy.deepcopy(candidate_list) hpa_candidate_list[1]['flavor_map'] = {} hpa_candidate_list[1]['flavor_map'][label_name] = "vim-flavor-name1" - expected_response = {'response': hpa_candidate_list, 'error': False} + hpa_candidate_list[1]['all_directives'] = {} + hpa_candidate_list[1]['all_directives']['directives'] = directive + hpa_candidate_list1 = [] + hpa_candidate_list1.append(hpa_candidate_list[0]) + expected_response = {'response': hpa_candidate_list1, 'error': False} self.assertEqual(expected_response, self.data_ep.get_candidates_with_hpa(None, args)) hpa_candidate_list2 = list() hpa_candidate_list2.append(copy.deepcopy(candidate_list[0])) - args = generate_args(candidate_list, flavorProperties, label_name) + args = generate_args(candidate_list, flavorProperties, id, type, directives) hpa_mock.return_value = [] expected_response = {'response': hpa_candidate_list2, 'error': False} self.assertEqual(expected_response, @@ -320,11 +335,13 @@ class TestDataEndpoint(unittest.TestCase): args)) -def generate_args(candidate_list, flavorProperties, label_name): +def generate_args(candidate_list, flavorProperties, vf_id, model_type, directives): arg_candidate_list = copy.deepcopy(candidate_list) args = {"candidate_list": arg_candidate_list, "flavorProperties": flavorProperties, - "label_name": label_name} + "id": vf_id, + "type": model_type, + "directives": directives} return args def ip_ext_sideeffect(*args, **kwargs): diff --git a/conductor/conductor/tests/unit/solver/hpa_constraints.json b/conductor/conductor/tests/unit/solver/hpa_constraints.json index 829eaf3..5f9c4c5 100644 --- a/conductor/conductor/tests/unit/solver/hpa_constraints.json +++ b/conductor/conductor/tests/unit/solver/hpa_constraints.json @@ -13,6 +13,7 @@ { "architecture": "generic", "hpa-feature": "basicCapabilities", + "directives": [], "hpa-feature-attributes": [ { "hpa-attribute-key": "numVirtualCpu", @@ -31,6 +32,7 @@ { "architecture": "generic", "hpa-feature": "numa", + "directives": [], "hpa-feature-attributes": [ { "hpa-attribute-key": "numaNodes", @@ -65,6 +67,7 @@ { "architecture": "generic", "hpa-feature": "cpuPinning", + "directives": [], "hpa-feature-attributes": [ { "hpa-attribute-key": "logicalCpuThreadPinningPolicy", @@ -80,7 +83,19 @@ "hpa-version": "v1" } ], - "flavorLabel": "flavor_label_1" + "id": "vg_1", + "type": "vnfc", + "directives": [ + { + "type": "flavor_directives", + "attributes": [ + { + "attribute_name": "flavor_label_1", + "attribute_value": "" + } + ] + } + ] }, { "flavorProperties": [ @@ -100,7 +115,8 @@ "unit": "GB" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] }, { "architecture": "generic", @@ -134,7 +150,8 @@ "unit": "GB" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] }, { "architecture": "generic", @@ -147,10 +164,23 @@ "unit": "GB" } ], - "hpa-version": "v1" + "hpa-version": "v1", + "directives": [] } ], - "flavorLabel": "flavor_label_2" + "id": "vg_2", + "type": "vnfc", + "directives": [ + { + "type": "flavor_directives", + "attributes": [ + { + "attribute_name": "flavor_label_2", + "attribute_value": "" + } + ] + } + ] } ] } diff --git a/conductor/conductor/tests/unit/test_sms.py b/conductor/conductor/tests/unit/test_sms.py new file mode 100644 index 0000000..b04111e --- /dev/null +++ b/conductor/conductor/tests/unit/test_sms.py @@ -0,0 +1,89 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2018 Intel Corporation Intellectual Property +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------- +# +import unittest +from uuid import uuid4 + +import requests_mock + +import conductor.common.sms as SMS +from oslo_config import cfg + + +class TestSMS(unittest.TestCase): + + def setUp(self): + self.config = cfg.CONF.aaf_sms + self.base_domain_url = '{}/v1/sms/domain' + self.domain_url = '{}/v1/sms/domain/{}' + self.secret_url = self.domain_url + '/secret' + + @requests_mock.mock() + def test_sms(self, mock_sms): + ''' NOTE: preload_secret generate the uuid for the domain + Create Domain API is called during the deployment using a + preload script. So the application oly knows the domain_uuid. + All sub-sequent SMS API calls needs the uuid. + For test purposes we need to do preload ourselves''' + sms_url = self.config.aaf_sms_url + + # JSON Data responses + secretnames = {'secretnames': ['s1', 's2', 's3', 's4']} + secretvalues = {'values': {'Password': '', 'UserName': ''}} + expecect_secret_dict = dict() + for secret in secretnames['secretnames']: + expecect_secret_dict[secret] = secretvalues['values'] + + # Part 1 : Preload Secrets ONLY FOR TEST + # Mock requests for preload_secret + cd_url = self.base_domain_url.format(sms_url) + domain_uuid1 = str(uuid4()) + s_url = self.secret_url.format(sms_url, domain_uuid1) + mock_sms.post(cd_url, status_code=200, json={'uuid': domain_uuid1}) + mock_sms.post(s_url, status_code=200) + # Initialize Secrets from SMS + SMS.preload_secrets() + + # Part 2: Retrieve Secret Test + # Mock requests for retrieve_secrets + # IMPORTANT: Read the config again as the preload_secrets has + # updated the config with uuid + domain_uuid2 = self.config.secret_domain + self.assertEqual(domain_uuid1, domain_uuid2) + + d_url = self.domain_url.format(sms_url, domain_uuid2) + s_url = self.secret_url.format(sms_url, domain_uuid2) + + # Retrieve Secrets from SMS and load to secret cache + # Use the secret_cache instead of config files + mock_sms.get(s_url, status_code=200, json=secretnames) + for secret in secretnames['secretnames']: + mock_sms.get('{}/{}'.format(s_url, secret), + status_code=200, json=secretvalues) + secret_cache = SMS.retrieve_secrets() + self.assertDictEqual(expecect_secret_dict, secret_cache, + 'Failed to retrieve secrets') + + # Part 3: Clean up Delete secrets and domain + # Mock requests for delete_secrets + mock_sms.delete(d_url, status_code=200) + self.assertTrue(SMS.delete_secrets()) + + +if __name__ == "__main__": + unittest.main() diff --git a/conductor/pom.xml b/conductor/pom.xml index 40501dc..fecf927 100644 --- a/conductor/pom.xml +++ b/conductor/pom.xml @@ -21,13 +21,13 @@ <parent> <groupId>org.onap.optf.has</groupId> - <version>1.2.0-SNAPSHOT</version> + <version>1.2.1-SNAPSHOT</version> <artifactId>optf-has</artifactId> </parent> <groupId>org.onap.optf.has</groupId> <artifactId>optf-has-conductor</artifactId> - <version>1.2.0-SNAPSHOT</version> + <version>1.2.1-SNAPSHOT</version> <name>optf-has-conductor</name> <description>Homing Allocation Service/Conductor</description> diff --git a/conductor/requirements.txt b/conductor/requirements.txt index 9359e26..d09c960 100644 --- a/conductor/requirements.txt +++ b/conductor/requirements.txt @@ -23,3 +23,4 @@ requests[security]!=2.9.0,>=2.8.1 # Apache-2.0 six>=1.9.0 # MIT, also required by futurist stevedore>=1.9.0 # Apache-2.0, also required by oslo.config WebOb>=1.2.3 # MIT +onapsmsclient>=0.0.3
\ No newline at end of file diff --git a/conductor/test-requirements.txt b/conductor/test-requirements.txt index c0e68d0..7466c9d 100644 --- a/conductor/test-requirements.txt +++ b/conductor/test-requirements.txt @@ -18,3 +18,4 @@ os-testr>=1.0.0 # Apache-2.0 tempest>=11.0.0 # Apache-2.0 pifpaf>=0.0.11 junitxml>=0.7 +requests-mock>=1.5.2 diff --git a/docs/sections/offeredapis.rst b/docs/sections/offeredapis.rst index 9ffb6fb..497f9a3 100644 --- a/docs/sections/offeredapis.rst +++ b/docs/sections/offeredapis.rst @@ -1,478 +1,18 @@ .. This work is licensed under a Creative Commons Attribution 4.0 International License. +.. http://creativecommons.org/licenses/by/4.0 Offered APIs ============================================= -.. This work is licensed under a Creative Commons Attribution 4.0 International License. -.. Copyright (C) 2017-2018 AT&T Intellectual Property. All rights reserved. - -Homing API v1 ------------------- - -*Updated: 28 Feb 2018* - This document describes the Homing API, provided by the Homing and Allocation service (Conductor). -It is a work in progress and subject to frequent revision. - -General API Information -------------------------- - -Authenticated calls that target a known URI but that use an HTTP method -the implementation does not support return a 405 Method Not Allowed -status. In addition, the HTTP OPTIONS method is supported for each known -URI. In both cases, the Allow response header indicates the supported -HTTP methods. See the API Errors section for more information about the -error response structure. - -API versions ------------------- - -List all Homing API versions ----------------------------- - -**GET** ``/``\ F - -**Normal response codes:** 200 - -.. code:: json - - { - "versions": [ - { - "status": "EXPERIMENTAL", - "id": "v1", - "updated": "2016-11-01T00:00:00Z", - "media-types": [ - { - "base": "application/json", - "type": "application/vnd.onap.homing-v1+json" - } - ], - "links": [ - { - "href": "http://has.ip/v1", - "rel": "self" - }, - { - "href": "http://has.url/", - "type": "text/html", - "rel": "describedby" - } - ] - } - ] - } - -This operation does not accept a request body. - -Plans ------ - -Create a plan -------------- - -**POST** ``/v1/plans`` - -- **Normal response codes:** 201 -- **Error response codes:** badRequest (400), unauthorized (401), - internalServerError (500) - -Request an inventory plan for one or more related service demands. - -The request includes or references a declarative **template**, -consisting of: - -- **Parameters** that can be referenced like macros -- **Demands** for service made against inventory -- **Locations** that are common to the overall plan -- **Constraints** made against demands, resulting in a set of inventory - candidates -- **Optimizations** to further narrow down the remaining candidates -The response contains an inventory **plan**, consisting of one or more -sets of recommended pairings of demands with an inventory candidate's -attributes and region. -Request Parameters -~~~~~~~~~~~~~~~~~~ - -+--------------------+-------+------------+-------------------+ -| Parameter | Style | Type | Description | -+====================+=======+============+===================+ -| ``name`` | plain | xsd:string | A name for the | -| (Optional) | | | new plan. If a | -| | | | name is not | -| | | | provided, it | -| | | | will be | -| | | | auto-generated | -| | | | based on the | -| | | | homing | -| | | | template. This | -| | | | name must be | -| | | | unique within | -| | | | a given | -| | | | Conductor | -| | | | environment. | -| | | | When deleting | -| | | | a plan, its | -| | | | name will not | -| | | | become | -| | | | available for | -| | | | reuse until | -| | | | the deletion | -| | | | completes | -| | | | successfully. | -| | | | Must only | -| | | | contain | -| | | | letters, | -| | | | numbers, | -| | | | hypens, full | -| | | | stops, | -| | | | underscores, | -| | | | and tildes | -| | | | (RFC 3986, | -| | | | Section 2.3). | -| | | | This parameter | -| | | | is immutable. | -+--------------------+-------+------------+-------------------+ -| ``id`` | plain | csapi:UUID | The UUID of | -| (Optional) | | | the plan. UUID | -| | | | is assigned by | -| | | | Conductor if | -| | | | no id is | -| | | | provided in | -| | | | the request. | -+--------------------+-------+------------+-------------------+ -| ``transaction_id`` | plain | csapi:UUID | The | -| | | | transaction id | -| | | | assigned by | -| | | | MSO. The logs | -| | | | should have | -| | | | this | -| | | | transaction id | -| | | | for tracking | -| | | | purposes. | -+--------------------+-------+------------+-------------------+ -| ``files`` | plain | xsd:dict | Supplies the | -| (Optional) | | | contents of | -| | | | files | -| | | | referenced in | -| | | | the template. | -| | | | Conductor | -| | | | templates can | -| | | | explicitly | -| | | | reference | -| | | | files by using | -| | | | the | -| | | | ``get_file`` | -| | | | intrinsic | -| | | | function. The | -| | | | value is a | -| | | | JSON object, | -| | | | where each key | -| | | | is a relative | -| | | | or absolute | -| | | | URI which | -| | | | serves as the | -| | | | name of a | -| | | | file, and the | -| | | | associated | -| | | | value provides | -| | | | the contents | -| | | | of the file. | -| | | | Additionally, | -| | | | some template | -| | | | authors encode | -| | | | their user | -| | | | data in a | -| | | | local file. | -| | | | The Homing | -| | | | client (e.g., | -| | | | a CLI) can | -| | | | examine the | -| | | | template for | -| | | | the | -| | | | ``get_file`` | -| | | | intrinsic | -| | | | function | -| | | | (e.g., | -| | | | ``{get_file: f | -| | | | ile.yaml}``) | -| | | | and add an | -| | | | entry to the | -| | | | ``files`` map | -| | | | with the path | -| | | | to the file as | -| | | | the name and | -| | | | the file | -| | | | contents as | -| | | | the value. Do | -| | | | not use this | -| | | | parameter to | -| | | | provide the | -| | | | content of the | -| | | | template | -| | | | located at the | -| | | | ``template_url`` | -| | | | address. | -| | | | Instead, use | -| | | | the | -| | | | ``template`` | -| | | | parameter to | -| | | | supply the | -| | | | template | -| | | | content as | -| | | | part of the | -| | | | request. | -+--------------------+-------+------------+-------------------+ -| ``template_url`` | plain | xsd:string | A URI to | -| (Optional) | | | the location | -| | | | containing the | -| | | | template on | -| | | | which to | -| | | | perform the | -| | | | operation. See | -| | | | the | -| | | | description of | -| | | | the | -| | | | ``template`` | -| | | | parameter for | -| | | | information | -| | | | about the | -| | | | expected | -| | | | template | -| | | | content | -| | | | located at the | -| | | | URI. This | -| | | | parameter is | -| | | | only required | -| | | | when you omit | -| | | | the | -| | | | ``template`` | -| | | | parameter. If | -| | | | you specify | -| | | | both | -| | | | parameters, | -| | | | this parameter | -| | | | is ignored. | -+--------------------+-------+------------+-------------------+ -| ``template`` | plain | xsd:string | The template | -| | | or xsd:dict| on which to | -| | | | perform the | -| | | | operation. See | -| | | | the Homing | -| | | | Template | -| | | | Guide | -| | | | for complete | -| | | | information on | -| | | | the format. | -| | | | This parameter | -| | | | is either | -| | | | provided as a | -| | | | ``string`` or | -| | | | ``dict`` in | -| | | | the JSON | -| | | | request body. | -| | | | For ``string`` | -| | | | content it may | -| | | | be a JSON- or | -| | | | YAML-formatted | -| | | | Conductor | -| | | | template. For | -| | | | ``dict`` | -| | | | content it | -| | | | must be a | -| | | | direct JSON | -| | | | representation | -| | | | of the | -| | | | Conductor | -| | | | template. This | -| | | | parameter is | -| | | | required only | -| | | | when you omit | -| | | | the | -| | | | ``template_url`` | -| | | | parameter. If | -| | | | you specify | -| | | | both | -| | | | parameters, | -| | | | this value | -| | | | overrides the | -| | | | ``template_url`` | -| | | | parameter | -| | | | value. | -+--------------------+-------+------------+-------------------+ -| ``timeout`` | plain | xsd:number | The timeout | -| (Optional) | | | for plan | -| | | | creation in | -| | | | minutes. | -| | | | Default is 1. | -+--------------------+-------+------------+-------------------+ -| ``limit`` | plain | xsd:number | The maximum | -| (Optional) | | | number of | -| | | | recommendations | -| | | | to return. | -| | | | Default is 1. | -+--------------------+-------+------------+-------------------+ - -**NOTE**: ``files``, ``template_url``, and ``timeout`` are not yet -supported. +To view API documentation in the interactive swagger UI download the following and +paste into the swagger tool here: https://editor.swagger.io -Response Parameters -~~~~~~~~~~~~~~~~~~~ +:download:`oof-has-api.json <./swaggerdoc/oof-has-api.json>` -+--------------------+----------+------------+--------------------+ -| Parameter | Style | Type | Description | -+====================+==========+============+====================+ -| ``plan`` | plain | xsd:dict | The ``plan`` | -| | | | object. | -+--------------------+----------+------------+--------------------+ -| ``id`` | plain | csapi:UUID | The UUID of | -| | | | the plan. | -+--------------------+----------+------------+--------------------+ -| ``transaction_id`` | plain | csapi:UUID | The | -| | | | transaction id | -| | | | assigned by | -| | | | the MSO. | -+--------------------+----------+------------+--------------------+ -| ``name`` | plain | xsd:string | The plan name. | -| | | | | -+--------------------+----------+------------+--------------------+ -| ``status`` | plain | xsd:string | The plan | -| | | | status. One of | -| | | | ``template``, | -| | | | ``translated``, | -| | | | ``solving``, | -| | | | ``solved``, or | -| | | | ``error``. See | -| | | | **Plan | -| | | | Status** table | -| | | | for | -| | | | descriptions | -| | | | of each value. | -+--------------------+----------+------------+--------------------+ -| ``message`` | plain | xsd:string | Additional | -| | | | context, if | -| | | | any, around | -| | | | the message | -| | | | status. If the | -| | | | status is | -| | | | ``error``, | -| | | | this may | -| | | | include a | -| | | | reason and | -| | | | suggested | -| | | | remediation, | -| | | | if available. | -+--------------------+----------+------------+--------------------+ -| ``links`` | plain | xsd:list | A list of URLs | -| | | | for the plan. | -| | | | Each URL is a | -| | | | JSON object | -| | | | with an | -| | | | ``href`` key | -| | | | indicating the | -| | | | URL and a | -| | | | ``rel`` key | -| | | | indicating its | -| | | | relationship | -| | | | to the plan in | -| | | | question. | -| | | | There may be | -| | | | multiple links | -| | | | returned. The | -| | | | ``self`` | -| | | | relationship | -| | | | identifies the | -| | | | URL of the | -| | | | plan itself. | -+--------------------+----------+------------+--------------------+ -| ``recommendations``| plain | xsd:list | A list of one | -| | | | or more | -| | | | recommendationS. | -| | | | A | -| | | | recommendation | -| | | | pairs each | -| | | | requested | -| | | | demand with an | -| | | | inventory | -| | | | provider, a | -| | | | single | -| | | | candidate, and | -| | | | an opaque | -| | | | dictionary of | -| | | | attributes. | -| | | | Refer to the | -| | | | Demand | -| | | | candidate | -| | | | schema in the | -| | | | Homing | -| | | | Template | -| | | | Guide | -| | | | for further | -| | | | details. (Note | -| | | | that, when | -| | | | ``inventory_type`` | -| | | | is ``cloud`` | -| | | | the | -| | | | candidate's | -| | | | ``candidate_id`` | -| | | | field is | -| | | | redundant and | -| | | | thus omitted.) | -+--------------------+----------+------------+--------------------+ - -Plan Status -~~~~~~~~~~~ - -+----------------+-----------------+ -| Status | Description | -+================+=================+ -| ``template`` | Plan request | -| | and homing | -| | template have | -| | been received. | -| | Awaiting | -| | translation. | -+----------------+-----------------+ -| ``translated`` | Homing | -| | template has | -| | been | -| | translated, | -| | and candidates | -| | have been | -| | obtained from | -| | inventory | -| | providers. | -| | Awaiting | -| | solving. | -+----------------+-----------------+ -| ``solving`` | Search for a | -| | solution is in | -| | progress. This | -| | may | -| | incorporate | -| | requests to | -| | service | -| | controllers | -| | for additional | -| | information. | -+----------------+-----------------+ -| ``solved`` | Search is | -| | complete. A | -| | solution with | -| | one or more | -| | recommendations | -| | was found. | -+----------------+-----------------+ -| ``not found`` | Search is | -| | complete. No | -| | recommendations | -| | were found. | -+----------------+-----------------+ -| ``error`` | An error was | -| | encountered. | -+----------------+-----------------+ +.. swaggerv2doc:: ./swaggerdoc/oof-has-api.json State Diagram ^^^^^^^^^^^^^ diff --git a/docs/sections/swaggerdoc/oof-has-api.json b/docs/sections/swaggerdoc/oof-has-api.json new file mode 100644 index 0000000..dcb2aeb --- /dev/null +++ b/docs/sections/swaggerdoc/oof-has-api.json @@ -0,0 +1,341 @@ +{ + "swagger" : "2.0", + "info" : { + "description" : "This is the ONAP OOF HAS (Homing and Allocation Service) API", + "version" : "1.0.0", + "title" : "HAS API", + "contact" : { + "email" : "frank.sandoval@oamtechnologies.com" + }, + "license" : { + "name" : "Apache 2.0", + "url" : "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "securityDefinitions" : { + "basicAuth" : { + "type" : "basic", + "description" : "HTTP Basic Auth" + } + }, + "security" : [ { + "basicAuth" : [ ] + } ], + "paths" : { + "/" : { + "get" : { + "summary" : "retrieve versions", + "operationId" : "retrieveVersions", + "description" : "retrieve supported versions of the API", + "security" : [ ], + "produces" : [ "application/json" ], + "responses" : { + "200" : { + "description" : "list of supported versions", + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/Versions" + } + } + }, + "400" : { + "description" : "bad request", + "schema" : { + "$ref" : "#/definitions/Error" + } + }, + "401" : { + "description" : "unauthorized request" + } + } + } + }, + "/v1/plans" : { + "post" : { + "summary" : "create a plan", + "operationId" : "createPlan", + "description" : "creates a plan from one or more service demands", + "consumes" : [ "application/json" ], + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "body", + "name" : "demand", + "description" : "service demand", + "schema" : { + "$ref" : "#/definitions/Demand" + } + } ], + "responses" : { + "201" : { + "description" : "plan created", + "schema" : { + "items" : { + "$ref" : "#/definitions/Plan" + } + } + }, + "400" : { + "description" : "bad request", + "schema" : { + "$ref" : "#/definitions/Error" + } + }, + "401" : { + "description" : "unauthorized request" + } + } + } + }, + "/v1/plans/{plan_id}" : { + "get" : { + "summary" : "retreive a plan", + "operationId" : "getPlan", + "description" : "retrieve a plan", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "path", + "name" : "plan_id", + "description" : "UUID of plan identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "200" : { + "description" : "retrieve a plan", + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/Plan" + } + } + }, + "400" : { + "description" : "bad request", + "schema" : { + "$ref" : "#/definitions/Error" + } + }, + "401" : { + "description" : "unauthorized request" + }, + "500" : { + "description" : "Internal Server Error" + } + } + }, + "delete" : { + "summary" : "delete a plan", + "operationId" : "deletePlan", + "description" : "delete a plan", + "produces" : [ "application/json" ], + "parameters" : [ { + "in" : "path", + "name" : "plan_id", + "description" : "UUID of plan identifier", + "required" : true, + "type" : "string", + "format" : "uuid" + } ], + "responses" : { + "204" : { + "description" : "deleted a plan" + }, + "400" : { + "description" : "bad request", + "schema" : { + "$ref" : "#/definitions/Error" + } + }, + "401" : { + "description" : "unauthorized request" + } + } + } + } + }, + "definitions" : { + "Demand" : { + "type" : "object", + "required" : [ "transaction_id", "template" ], + "properties" : { + "name" : { + "type" : "string" + }, + "id" : { + "type" : "string", + "format" : "uuid" + }, + "transaction_id" : { + "type" : "string", + "format" : "uuid", + "example" : "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "template" : { + "type" : "string", + "externalDocs" : { + "description" : "See here for template format", + "url" : "http://onap.readthedocs.io/en/latest/submodules/optf/has.git/docs/sections/homingspecification.html" + } + } + } + }, + "Plan" : { + "type" : "object", + "required" : [ "plan", "id", "transaction_id", "name", "status", "message", "links", "recommendations" ], + "properties" : { + "plan" : { + "type" : "string", + "example" : "JSON string describing plan", + "externalDocs" : { + "description" : "See here for plan format", + "url" : "http://onap.readthedocs.io/en/latest/submodules/optf/has.git/docs/sections/offeredapis.html" + } + }, + "id" : { + "type" : "string", + "format" : "uuid", + "example" : "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "transaction_id" : { + "type" : "string", + "format" : "uuid", + "example" : "d290f1ee-6c54-4b01-90e6-d701748f0851" + }, + "name" : { + "type" : "string", + "example" : "name of plan" + }, + "status" : { + "type" : "string", + "enum" : [ "template", "translated", "solving", "solved", "not found", "error" ] + }, + "message" : { + "type" : "string", + "example" : "Additional context, if any, around the message status" + }, + "links" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/Link" + }, + "externalDocs" : { + "description" : "See here for links description", + "url" : "http://onap.readthedocs.io/en/latest/submodules/optf/has.git/docs/sections/offeredapis.html" + } + }, + "recommendations" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/Recommendation" + }, + "externalDocs" : { + "description" : "Refer to the Demand candidate schema in the Homing Template Guide for further details", + "url" : "http://onap.readthedocs.io/en/latest/submodules/optf/has.git/docs/sections/homingspecification.html" + } + } + } + }, + "Link" : { + "required" : [ "href", "rel" ], + "properties" : { + "href" : { + "type" : "string", + "example" : "http://localhost:8091/v1" + }, + "rel" : { + "type" : "string", + "example" : "self" + } + } + }, + "Recommendation" : { + "required" : [ "recommendation" ], + "properties" : { + "recommendation" : { + "type" : "string", + "description" : "JSON string, see description of Plan.recommendations", + "example" : "JSON string describing recommendation" + } + } + }, + "MediaTypes" : { + "required" : [ "base", "type" ], + "properties" : { + "base" : { + "type" : "string", + "example" : "application/json" + }, + "rel" : { + "type" : "string", + "example" : "application/vnd.onap.has-v1+json" + } + } + }, + "Versions" : { + "required" : [ "name", "links", "media_types", "status", "updated" ], + "properties" : { + "id" : { + "type" : "string", + "example" : "v1" + }, + "links" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/Link" + } + }, + "media-types" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/MediaTypes" + } + }, + "status" : { + "type" : "string", + "example" : "EXPERIMENTAL" + }, + "updated" : { + "type" : "string", + "example" : "2016-11-01T00:00:00Z" + } + } + }, + "Error" : { + "properties" : { + "title" : { + "type" : "string", + "example" : "Bad Request" + }, + "explanation" : { + "type" : "string", + "example" : "pl an did not pass validation against callable" + }, + "code" : { + "type" : "integer", + "example" : 400 + }, + "error" : { + "properties" : { + "message" : { + "type" : "string", + "example" : "message" + }, + "traceback" : { + "type" : "string", + "example" : "traceback" + }, + "errortype" : { + "type" : "string", + "example" : "type" + } + } + } + } + } + }, + "schemes" : [ "https" ], + "host" : "virtserver.swaggerhub.com", + "basePath" : "/onap_oof/has/1.0.0" +}
\ No newline at end of file @@ -21,14 +21,14 @@ <parent> <groupId>org.onap.oparent</groupId> <artifactId>oparent-python</artifactId> - <version>1.1.1</version> + <version>1.2.0</version> </parent> <groupId>org.onap.optf.has</groupId> <artifactId>optf-has</artifactId> <name>optf-has</name> - <version>1.2.0-SNAPSHOT</version> + <version>1.2.1-SNAPSHOT</version> <description>Homing Allocation Service</description> <modules> diff --git a/preload_secrets.yaml b/preload_secrets.yaml new file mode 100755 index 0000000..65a814a --- /dev/null +++ b/preload_secrets.yaml @@ -0,0 +1,21 @@ +# DO NOT USE THIS IN PRODUCTION +# USED ONLY FOR TESTING +--- +domain: has +secrets: +- name: aai + values: + UserName: OOF + Password: OOF +- name: conductor_api + values: + UserName: admin1 + Password: plan.15 +- name: sdnc + values: + UserName: admin + Password: sdnc.15 +- name: music_api + values: + UserName: conductor + Password: c0nduct0r diff --git a/run-dockers.sh b/run-dockers.sh index 5efb080..e61ea3c 100755 --- a/run-dockers.sh +++ b/run-dockers.sh @@ -4,7 +4,7 @@ BUILD_ARGS="--no-cache" ORG="onap" -VERSION="1.1.1" +VERSION="1.2.1" PROJECT="optf-has" DOCKER_REPOSITORY="nexus3.onap.org:10003" IMAGE_NAME="${DOCKER_REPOSITORY}/${ORG}/${PROJECT}" diff --git a/version.properties b/version.properties index 2217466..eb859e6 100644 --- a/version.properties +++ b/version.properties @@ -19,7 +19,7 @@ major=1 minor=2 -patch=0 +patch=1 base_version=${major}.${minor}.${patch} |