diff options
21 files changed, 1091 insertions, 42 deletions
diff --git a/conductor/conductor/conf/vim_controller.py b/conductor/conductor/conf/vim_controller.py new file mode 100644 index 0000000..91eb079 --- /dev/null +++ b/conductor/conductor/conf/vim_controller.py @@ -0,0 +1,33 @@ +# +# ------------------------------------------------------------------------- +# 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. +# +# ------------------------------------------------------------------------- +# + + +from oslo_config import cfg + +from conductor.i18n import _ + +VIM_CONTROLLER_EXT_MANAGER_OPTS = [ + cfg.ListOpt('extensions', + default=['multicloud'], + help=_('Extensions list to use')), +] + + +def register_extension_manager_opts(cfg=cfg.CONF): + cfg.register_opts(VIM_CONTROLLER_EXT_MANAGER_OPTS, 'vim_controller') diff --git a/conductor/conductor/controller/translator.py b/conductor/conductor/controller/translator.py index dbff2d2..2913c69 100644 --- a/conductor/conductor/controller/translator.py +++ b/conductor/conductor/controller/translator.py @@ -22,17 +22,16 @@ import datetime import json import os import uuid -import yaml -from oslo_config import cfg -from oslo_log import log import six - +import yaml from conductor import __file__ as conductor_root -from conductor.common.music import messaging as music_messaging -from conductor.common import threshold from conductor import messaging from conductor import service +from conductor.common import threshold +from conductor.common.music import messaging as music_messaging +from oslo_config import cfg +from oslo_log import log LOG = log.getLogger(__name__) @@ -95,7 +94,21 @@ CONSTRAINTS = { 'category': ['disaster', 'region', 'complex', 'country', 'time', 'maintenance']}, }, + 'vim_fit': { + 'split': True, + 'required': ['controller'], + 'optional': ['request'], + }, + 'hpa': { + 'split': True, + 'required': ['evaluate'], + }, } +HPA_FEATURES = ['architecture', 'hpa-feature', 'hpa-feature-attributes', + 'hpa-version', 'mandatory'] +HPA_OPTIONAL = ['score'] +HPA_ATTRIBUTES = ['hpa-attribute-key', 'hpa-attribute-value', 'operator'] +HPA_ATTRIBUTES_OPTIONAL = ['unit'] class TranslatorException(Exception): @@ -508,7 +521,7 @@ class Translator(object): resolved_demands = \ response and response.get('resolved_demands') - required_candidates = resolved_demands\ + required_candidates = resolved_demands \ .get('required_candidates') if not resolved_demands: raise TranslatorException( @@ -537,6 +550,57 @@ class Translator(object): return parsed + 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') \ + or not para.get('flavorProperties') \ + or para.get('flavorLabel') == '' \ + or para.get('flavorProperties') == '': + raise TranslatorException( + "HPA requirements need at least " + "one set of flavorLabel and flavorProperties" + ) + for feature in para.get('flavorProperties'): + if type(feature) is not dict: + raise TranslatorException("HPA feature must be a dict") + # process mandatory parameter + hpa_mandatory = set(HPA_FEATURES).difference(feature.keys()) + if bool(hpa_mandatory): + raise TranslatorException( + "Lack of compulsory elements inside HPA feature") + # process optional parameter + 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") + if feature.get('mandatory') == 'False' and not feature.get( + 'score'): + raise TranslatorException( + "Score needs to be present if mandatory is False") + + for attr in feature.get('hpa-feature-attributes'): + if type(attr) is not dict: + raise TranslatorException( + "HPA feature attributes must be a dict") + + # process mandatory hpa attribute parameter + hpa_attr_mandatory = set(HPA_ATTRIBUTES).difference( + attr.keys()) + if bool(hpa_attr_mandatory): + raise TranslatorException( + "Lack of compulsory elements inside HPA " + "feature atrributes") + # process optional hpa attribute parameter + hpa_attr_optional = set(attr.keys()).difference( + HPA_ATTRIBUTES) + if hpa_attr_optional and not hpa_attr_optional.issubset( + HPA_ATTRIBUTES_OPTIONAL): + raise TranslatorException( + "Invalid attributes '{}' found inside HPA " + "feature attributes".format(hpa_attr_optional)) + def parse_constraints(self, constraints): """Validate/prepare constraints for use by the solver.""" if not isinstance(constraints, dict): @@ -585,6 +649,9 @@ class Translator(object): "No value specified for property '{}' in " "constraint named '{}'".format( req_prop, name)) + # For HPA constraints + if constraint_type == 'hpa': + self.validate_hpa_constraints(req_prop, value) # Make sure there are no unknown properties optional = constraint_def.get('optional', []) diff --git a/conductor/conductor/data/plugins/vim_controller/__init__.py b/conductor/conductor/data/plugins/vim_controller/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/conductor/conductor/data/plugins/vim_controller/__init__.py diff --git a/conductor/conductor/data/plugins/vim_controller/base.py b/conductor/conductor/data/plugins/vim_controller/base.py new file mode 100644 index 0000000..6f35924 --- /dev/null +++ b/conductor/conductor/data/plugins/vim_controller/base.py @@ -0,0 +1,37 @@ +# +# ------------------------------------------------------------------------- +# 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 abc + +from oslo_log import log +import six + +from conductor.data.plugins import base + +LOG = log.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class VimControllerBase(base.DataPlugin): + """Base class for Vim Controller plugins""" + + @abc.abstractmethod + def name(self): + """Return human-readable name.""" + pass diff --git a/conductor/conductor/data/plugins/vim_controller/extensions.py b/conductor/conductor/data/plugins/vim_controller/extensions.py new file mode 100644 index 0000000..25887cf --- /dev/null +++ b/conductor/conductor/data/plugins/vim_controller/extensions.py @@ -0,0 +1,45 @@ +# +# ------------------------------------------------------------------------- +# 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. +# +# ------------------------------------------------------------------------- +# + +from oslo_log import log +import stevedore + +from conductor.conf import vim_controller +from conductor.i18n import _LI + +LOG = log.getLogger(__name__) + +vim_controller.register_extension_manager_opts() + + +class Manager(stevedore.named.NamedExtensionManager): + """Manage Vim Controller extensions.""" + + def __init__(self, conf, namespace): + super(Manager, self).__init__( + namespace, conf.vim_controller.extensions, + invoke_on_load=True, name_order=True) + LOG.info(_LI("Loaded Vim controller extensions: %s"), self.names()) + + def initialize(self): + """Initialize enabled Vim controller extensions.""" + for extension in self.extensions: + LOG.info(_LI("Initializing Vim controller extension '%s'"), + extension.name) + extension.obj.initialize() diff --git a/conductor/conductor/data/plugins/vim_controller/multicloud.py b/conductor/conductor/data/plugins/vim_controller/multicloud.py new file mode 100644 index 0000000..cdc6cde --- /dev/null +++ b/conductor/conductor/data/plugins/vim_controller/multicloud.py @@ -0,0 +1,144 @@ +# +# ------------------------------------------------------------------------- +# 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. +# +# ------------------------------------------------------------------------- +# + +'''Multicloud Vim controller plugin''' + +import time +import uuid + +from conductor.common import rest +from conductor.data.plugins.vim_controller import base +from conductor.i18n import _LE, _LI +from oslo_config import cfg +from oslo_log import log + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + +MULTICLOUD_OPTS = [ + cfg.StrOpt('server_url', + default='http://msb.onap.org/api/multicloud', + help='Base URL for Multicloud without a trailing slash.'), + cfg.StrOpt('multicloud_rest_timeout', + default=30, + help='Timeout for Multicloud Rest Call'), + cfg.StrOpt('multicloud_retries', + default=3, + help='Number of retry for Multicloud Rest Call'), + cfg.StrOpt('server_url_version', + default='v0', + help='The version of Multicloud API.'), +] + +CONF.register_opts(MULTICLOUD_OPTS, group='multicloud') + + +class MULTICLOUD(base.VimControllerBase): + """Multicloud Vim controller""" + + def __init__(self): + """Initializer""" + self.conf = CONF + self.base = self.conf.multicloud.server_url.rstrip('/') + self.version = self.conf.multicloud.server_url_version.rstrip('/') + self.timeout = self.conf.multicloud.multicloud_rest_timeout + self.retries = self.conf.multicloud.multicloud_retries + + def initialize(self): + LOG.info(_LI("**** Initializing Multicloud Vim controller *****")) + self._init_rest_request() + + def name(self): + """Return human-readable name.""" + return "MultiCloud" + + def _request(self, method='get', path='/', data=None, + context=None, value=None): + """Performs HTTP request.""" + headers = { + 'X-FromAppId': 'CONDUCTOR', + 'X-TransactionId': str(uuid.uuid4()), + } + kwargs = { + "method": method, + "path": path, + "headers": headers, + "data": data, + } + + start_time = time.time() + response = self.rest.request(**kwargs) + elapsed = time.time() - start_time + LOG.debug("Total time for Multicloud request " + "({0:}: {1:}): {2:.3f} sec".format(context, value, elapsed)) + + if response is None: + LOG.error(_LE("No response from Multicloud ({}: {})"). + format(context, value)) + elif response.status_code != 200: + LOG.error(_LE("Multicloud request ({}: {}) returned HTTP " + "status {} {}, link: {}{}"). + format(context, value, + response.status_code, response.reason, + self.base, path)) + return response + + def _init_rest_request(self): + + kwargs = { + "server_url": self.base, + "retries": self.retries, + "log_debug": self.conf.debug, + "read_timeout": self.timeout, + } + self.rest = rest.REST(**kwargs) + + def check_vim_capacity(self, vim_request): + LOG.debug("Invoking check_vim_capacity api") + path = '/{}/{}'.format(self.version, 'check_vim_capacity') + + data = {} + data['vCPU'] = vim_request['vCPU'] + data['Memory'] = vim_request['Memory']['quantity'] + data['Storage'] = vim_request['Storage']['quantity'] + data['VIMs'] = vim_request['VIMs'] + response = self._request('post', path=path, data=data, + context="vim capacity", value="all") + LOG.debug("Response check_vim_capacity api - {}".format(response)) + if response is None or response.status_code != 200: + return None + + body = response.json() + + if body: + vims = body.get("VIMs") + if vims: + return vims + else: + LOG.error(_LE( + "Unable to get VIMs with cpu-{}, memory-{}, disk-{}") + .format(data['vCPU'], + data['Memory'], + data['Storage'])) + return None + else: + LOG.error(_LE("Unable to get VIMs from Multicloud with " + "requirement {}").format(data)) + return None diff --git a/conductor/conductor/data/service.py b/conductor/conductor/data/service.py index 9617217..5912963 100644 --- a/conductor/conductor/data/service.py +++ b/conductor/conductor/data/service.py @@ -27,6 +27,7 @@ from conductor.common.music import messaging as music_messaging from conductor.common.utils import conductor_logging_util as log_util from conductor.data.plugins.inventory_provider import extensions as ip_ext from conductor.data.plugins.service_controller import extensions as sc_ext +from conductor.data.plugins.vim_controller import extensions as vc_ext from conductor.i18n import _LE, _LI, _LW from oslo_config import cfg from oslo_log import log @@ -77,6 +78,9 @@ class DataServiceLauncher(object): self.ip_ext_manager = ( ip_ext.Manager(conf, 'conductor.inventory_provider.plugin')) self.ip_ext_manager.initialize() + self.vc_ext_manager = ( + vc_ext.Manager(conf, 'conductor.vim_controller.plugin')) + self.vc_ext_manager.initialize() self.sc_ext_manager = ( sc_ext.Manager(conf, 'conductor.service_controller.plugin')) self.sc_ext_manager.initialize() @@ -87,6 +91,7 @@ class DataServiceLauncher(object): topic = "data" target = music_messaging.Target(topic=topic) endpoints = [DataEndpoint(self.ip_ext_manager, + self.vc_ext_manager, self.sc_ext_manager), ] flush = not self.conf.data.concurrent kwargs = {'transport': transport, @@ -101,9 +106,10 @@ class DataServiceLauncher(object): class DataEndpoint(object): - def __init__(self, ip_ext_manager, sc_ext_manager): + def __init__(self, ip_ext_manager, vc_ext_manager, sc_ext_manager): self.ip_ext_manager = ip_ext_manager + self.vc_ext_manager = vc_ext_manager self.sc_ext_manager = sc_ext_manager self.plugin_cache = {} @@ -437,7 +443,7 @@ class DataEndpoint(object): error = False candidate_list = arg["candidate_list"] label_name = arg["label_name"] - features = arg["features"] + flavorProperties = arg["flavorProperties"] discard_set = set() for candidate in candidate_list: # perform this check only for cloud candidates @@ -457,7 +463,7 @@ class DataEndpoint(object): results = self.ip_ext_manager.map_method( 'match_hpa', candidate=candidate, - features=features + features=flavorProperties ) if results and len(results) > 0: @@ -491,6 +497,51 @@ class DataEndpoint(object): self.ip_ext_manager.names()[0])) return {'response': candidate_list, 'error': error} + def get_candidates_with_vim_capacity(self, ctx, arg): + ''' + RPC for getting candidates with vim capacity + :param ctx: context + :param arg: contains input passed from client side for RPC call + :return: response candidate_list with with required vim capacity + ''' + error = False + candidate_list = arg["candidate_list"] + vim_request = arg["request"] + vim_list = set() + discard_set = set() + for candidate in candidate_list: + if candidate["inventory_type"] == "cloud": + vim_list.add(candidate['vim-id']) + + vim_request['VIMs'] = list(vim_list) + vims_result = self.vc_ext_manager.map_method( + 'check_vim_capacity', + vim_request + ) + + if vims_result and len(vims_result) > 0: + vims_set = set(vims_result[0]) + for candidate in candidate_list: + # perform this check only for cloud candidates + if candidate["inventory_type"] == "cloud": + if candidate['vim-id'] not in vims_set: + discard_set.add(candidate.get("candidate_id")) + + # return candidates not in discard set + candidate_list[:] = [c for c in candidate_list + if c['candidate_id'] not in discard_set] + else: + error = True + LOG.warn(_LI( + "Multicloud did not respond properly to request: {}".format( + vim_request))) + + LOG.info(_LI( + "Candidates with with vim capacity: {}, vim controller: " + "{}").format(candidate_list, self.vc_ext_manager.names()[0])) + + return {'response': candidate_list, 'error': error} + def resolve_demands(self, ctx, arg): log_util.setLoggerFilter(LOG, ctx.get('keyspace'), ctx.get('plan_id')) diff --git a/conductor/conductor/opts.py b/conductor/conductor/opts.py index b32a39b..e2ace38 100644 --- a/conductor/conductor/opts.py +++ b/conductor/conductor/opts.py @@ -24,10 +24,12 @@ import conductor.common.music.api import conductor.common.music.messaging.component import conductor.conf.inventory_provider import conductor.conf.service_controller +import conductor.conf.vim_controller import conductor.controller.service import conductor.controller.translator_svc import conductor.data.plugins.inventory_provider.aai import conductor.data.plugins.service_controller.sdnc +import conductor.data.plugins.vim_controller.multicloud import conductor.reservation.service import conductor.service import conductor.solver.service @@ -53,6 +55,10 @@ def list_opts(): conductor.conf.inventory_provider. INV_PROVIDER_EXT_MANAGER_OPTS)), ('aai', conductor.data.plugins.inventory_provider.aai.AAI_OPTS), + ('vim_controller', itertools.chain( + conductor.conf.vim_controller.VIM_CONTROLLER_EXT_MANAGER_OPTS)), + ('multicloud', + conductor.data.plugins.vim_controller.multicloud.MULTICLOUD_OPTS), ('service_controller', itertools.chain( conductor.conf.service_controller. SVC_CONTROLLER_EXT_MANAGER_OPTS)), diff --git a/conductor/conductor/solver/optimizer/constraints/hpa.py b/conductor/conductor/solver/optimizer/constraints/hpa.py index a7e8d3c..9ef37df 100644 --- a/conductor/conductor/solver/optimizer/constraints/hpa.py +++ b/conductor/conductor/solver/optimizer/constraints/hpa.py @@ -54,11 +54,11 @@ 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['label'] - features = vm_demand['features'] + label_name = vm_demand['flavorLabel'] + flavorProperties = vm_demand['flavorProperties'] response = (cei.get_candidates_with_hpa(label_name, _candidate_list, - features)) + flavorProperties)) if response: _candidate_list = response else: diff --git a/conductor/conductor/solver/optimizer/constraints/vim_fit.py b/conductor/conductor/solver/optimizer/constraints/vim_fit.py new file mode 100644 index 0000000..6e6e052 --- /dev/null +++ b/conductor/conductor/solver/optimizer/constraints/vim_fit.py @@ -0,0 +1,60 @@ +#!/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. +# +# ------------------------------------------------------------------------- +# + +'''Solver class for constraint type vim_fit + Multicloud capacity check''' + +# python imports + +from conductor.i18n import _LI +# Conductor imports +from conductor.solver.optimizer.constraints import constraint +# Third-party library imports +from oslo_log import log + +LOG = log.getLogger(__name__) + + +class VimFit(constraint.Constraint): + def __init__(self, _name, _type, _demand_list, _priority=0, + _properties=None): + constraint.Constraint.__init__( + self, _name, _type, _demand_list, _priority) + self.properties = _properties + + def solve(self, _decision_path, _candidate_list, _request): + ''' + Solver for Multicloud vim_fit constraint type. + :param _decision_path: decision tree + :param _candidate_list: List of candidates + :param _request: solver request + :return: candidate_list with selected vim_list + ''' + # call conductor engine with request parameters + cei = _request.cei + demand_name = _decision_path.current_demand.name + vim_request = self.properties.get('request') + LOG.info(_LI("Solving constraint type '{}' for demand - [{}]").format( + self.constraint_type, demand_name)) + response = ( + cei.get_candidates_with_vim_capacity(_candidate_list, vim_request)) + if response: + _candidate_list = response + return _candidate_list diff --git a/conductor/conductor/solver/request/parser.py b/conductor/conductor/solver/request/parser.py index d7f3cec..0def215 100755 --- a/conductor/conductor/solver/request/parser.py +++ b/conductor/conductor/solver/request/parser.py @@ -34,6 +34,7 @@ from conductor.solver.optimizer.constraints \ import inventory_group 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.request import demand from conductor.solver.request import objective @@ -215,6 +216,14 @@ class Parser(object): constraint_demands, _properties=c_property) self.constraints[my_hpa_constraint.name] = my_hpa_constraint + elif constraint_type == "vim_fit": + LOG.debug("Creating constraint - {}".format(constraint_type)) + c_property = constraint_info.get("properties") + my_vim_constraint = vim_fit.VimFit(constraint_id, + constraint_type, + constraint_demands, + _properties=c_property) + self.constraints[my_vim_constraint.name] = my_vim_constraint else: LOG.error("unknown constraint type {}".format(constraint_type)) return @@ -326,12 +335,14 @@ class Parser(object): constraint.rank = 4 elif constraint.constraint_type == "inventory_group": constraint.rank = 5 - elif constraint.constraint_type == "instance_fit": + elif constraint.constraint_type == "vim_fit": constraint.rank = 6 - elif constraint.constraint_type == "region_fit": + elif constraint.constraint_type == "instance_fit": constraint.rank = 7 - else: + elif constraint.constraint_type == "region_fit": constraint.rank = 8 + else: + constraint.rank = 9 def attr_sort(self, attrs=['rank']): # this helper for sorting the rank diff --git a/conductor/conductor/solver/utils/constraint_engine_interface.py b/conductor/conductor/solver/utils/constraint_engine_interface.py index 256e4bb..43331aa 100644 --- a/conductor/conductor/solver/utils/constraint_engine_interface.py +++ b/conductor/conductor/solver/utils/constraint_engine_interface.py @@ -117,21 +117,39 @@ class ConstraintEngineInterface(object): # response is a list of (candidate, cost) tuples return response - def get_candidates_with_hpa(self, label_name, candidate_list, features): + def get_candidates_with_hpa(self, label_name, candidate_list, + flavorProperties): ''' Returns the candidate_list with an addition of flavor_mapping for matching cloud candidates with hpa constraints. :param label_name: vm_label_name passed from the SO/Policy :param candidate_list: list of candidates to process - :param features: hpa features for this vm_label_name + :param flavorProperties: hpa features for this vm_label_name :return: candidate_list with hpa features and flavor mapping ''' ctxt = {} args = {"candidate_list": candidate_list, - "features": features, + "flavorProperties": flavorProperties, "label_name": label_name} response = self.client.call(ctxt=ctxt, method="get_candidates_with_hpa", args=args) LOG.debug("get_candidates_with_hpa response: {}".format(response)) return response + + def get_candidates_with_vim_capacity(self, candidate_list, vim_request): + ''' + Returns the candidate_list with required vim capacity. + :param candidate_list: list of candidates to process + :param requests: vim requests with required cpu, memory and disk + :return: candidate_list with required vim capacity. + ''' + ctxt = {} + args = {"candidate_list": candidate_list, + "request": vim_request} + response = self.client.call(ctxt=ctxt, + method="get_candidates_with_vim_capacity", + args=args) + LOG.debug( + "get_candidates_with_vim_capacity response: {}".format(response)) + return response diff --git a/conductor/conductor/tests/unit/controller/test_translator.py b/conductor/conductor/tests/unit/controller/test_translator.py index 26b1182..ba0e3ec 100644 --- a/conductor/conductor/tests/unit/controller/test_translator.py +++ b/conductor/conductor/tests/unit/controller/test_translator.py @@ -19,15 +19,15 @@ """Test classes for translator""" import os -import yaml -import uuid import unittest +import uuid +import yaml +from conductor import __file__ as conductor_root from conductor.controller.translator import Translator from conductor.controller.translator import TranslatorException -from conductor import __file__ as conductor_root -from oslo_config import cfg from mock import patch +from oslo_config import cfg def get_template(): @@ -223,6 +223,318 @@ class TestNoExceptionTranslator(unittest.TestCase): 'type': 'distance_to_location'}} self.assertEquals(self.Translator.parse_constraints(constraints), rtn) + def test_parse_hpa_constraints(self): + hpa_constraint = { + "hpa_constraint": { + "type": "hpa", + "demands": [ + "vG" + ], + "properties": { + "evaluate": [ + {'flavorLabel': 'xx', + 'flavorProperties': [{ + 'hpa-feature': 'BasicCapabilities', + 'hpa-version': 'v1', + 'architecture': 'generic', + 'mandatory': 'False', + 'score': '5', + 'hpa-feature-attributes': [ + { + 'hpa-attribute-key': 'numVirtualCpu', + 'hpa-attribute-value': '4', + 'operator': '=' + }, + { + 'hpa-attribute-key': 'virtualMemSize', + 'hpa-attribute-value': '4', + 'operator': '=', + 'unit': 'GB' + } + ] + }], } + ] + }}} + rtn = { + 'hpa_constraint_vG': { + 'demands': 'vG', + 'name': 'hpa_constraint', + 'properties': {'evaluate': [{ + 'flavorProperties': [ + {'architecture': 'generic', + 'mandatory': 'False', + 'score': '5', + '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'}], + 'flavorLabel': 'xx'}]}, + 'type': 'hpa' + } + } + + self.assertEquals(self.Translator.parse_constraints(hpa_constraint), + rtn) + + hpa_constraint_2 = { + "hpa_constraint": { + "type": "hpa", + "demands": [ + "vG" + ], + "properties": { + "evaluate": [ + {'flavorLabel': 'xx', + 'flavorProperties': [{ + 'hpa-feature': 'BasicCapabilities', + 'hpa-version': 'v1', + 'architecture': 'generic', + 'mandatory': 'True', + 'hpa-feature-attributes': [ + { + 'hpa-attribute-key': 'numVirtualCpu', + 'hpa-attribute-value': '4', + 'operator': '=' + }, + { + 'hpa-attribute-key': 'virtualMemSize', + 'hpa-attribute-value': '4', + 'operator': '=', + 'unit': 'GB' + } + ] + }], } + ] + }}} + rtn_2 = { + 'hpa_constraint_vG': { + 'demands': 'vG', + 'name': 'hpa_constraint', + 'properties': {'evaluate': [{ + 'flavorProperties': [ + {'architecture': 'generic', + 'mandatory': 'True', + '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'}], + 'flavorLabel': 'xx'}]}, + 'type': 'hpa' + } + } + + self.assertEquals(self.Translator.parse_constraints(hpa_constraint_2), + rtn_2) + + def test_parse_hpa_constraints_format_validation(self): + hpa_constraint_1 = { + "hpa_constraint": { + "type": "hpa", + "demands": [ + "vG" + ], + "properties": { + "evaluate": [{'flavor': 'xx', + 'flavorProperties': []}] + } + } + } + hpa_constraint_2 = { + "hpa_constraint": { + "type": "hpa", + "demands": [ + "vG" + ], + "properties": { + "evaluate": [ + {'flavorLabel': 'xx', + 'flavorProperties': [ + { + 'hpa-feature': '', + 'hpa-version': '', + 'architecture': '', + 'mandatory': '', + 'hpa-feature-attributes': [''], + } + ]} + ] + } + } + } + + hpa_constraint_3 = { + "hpa_constraint": { + "type": "hpa", + "demands": [ + "vG" + ], + "properties": { + "evaluate": [ + { + "flavorLabel": "xx", + "flavorProperties": [ + { + "hpa-feature": "BasicCapabilities", + "hpa-version": "v1", + "architecture": "generic", + "mandatory": "False", + "score": "5", + "hpa-feature-attributes": [ + { + "hpa-attribute-key": "numVirtualCpu", + "hpa-attribute-value": "4" + } + ] + } + ] + } + ] + } + } + } + + hpa_constraint_4 = { + "hpa_constraint": { + "type": "hpa", + "demands": [ + "vG" + ], + "properties": { + "evaluate": [{'flavorLabel': 'xx', + 'flavorProperties': [{ + 'hpa-feature': '', + 'architecture': '', + 'mandatory': '', + 'hpa-feature-attributes': [''], + }]}] + } + } + } + + hpa_constraint_5 = { + "hpa_constraint": { + "type": "hpa", + "demands": [ + "vG" + ], + "properties": { + "evaluate": [ + {'flavorLabel': 'xx', + 'flavorProperties': [ + { + 'hpa-feature': 'BasicCapabilities', + 'hpa-version': 'v1', + 'architecture': 'generic', + 'mandatory': 'False', + 'hpa-feature-attributes': [ + { + 'hpa-attribute-key': 'numVirtualCpu', + 'hpa-attribute-value': '4', + + }, + ] + } + ], } + ] + } + } + } + + self.assertRaises(TranslatorException, + self.Translator.parse_constraints, hpa_constraint_1) + self.assertRaises(TranslatorException, + self.Translator.parse_constraints, hpa_constraint_2) + self.assertRaises(TranslatorException, + self.Translator.parse_constraints, hpa_constraint_3) + self.assertRaises(TranslatorException, + self.Translator.parse_constraints, hpa_constraint_4) + self.assertRaises(TranslatorException, + self.Translator.parse_constraints, hpa_constraint_5) + + def test_parse_vim_fit_constraint(self): + vim_fit_constraint = { + "check_cloud_capacity": { + "type": "vim_fit", + "demands": [ + "vG" + ], + "properties": { + "controller": "multicloud", + "request": { + "vCPU": 10, + "Memory": { + "quantity": "10", + "unit": "GB" + }, + "Storage": { + "quantity": "100", + "unit": "GB" + } + } + } + } + } + expected_response = { + "check_cloud_capacity_vG" : { + "type": "vim_fit", + "demands": "vG", + "name": "check_cloud_capacity", + "properties": { + "controller": "multicloud", + "request": { + "vCPU": 10, + "Memory": { + "quantity": "10", + "unit": "GB" + }, + "Storage": { + "quantity": "100", + "unit": "GB" + } + } + } + } + } + vim_fit_constraint2 = { + "check_cloud_capacity": { + "type": "vim_fit", + "demands": [ + "vG" + ], + "properties": { + "vim-controller": "multicloud" + } + } + } + self.maxDiff = None + self.assertEquals(expected_response, self.Translator.parse_constraints( + vim_fit_constraint)) + self.assertRaises(TranslatorException, + self.Translator.parse_constraints, + vim_fit_constraint2) + @patch('conductor.controller.translator.Translator.create_components') def test_parse_optimization(self, mock_create): expected_parse = {'goal': 'min', diff --git a/conductor/conductor/tests/unit/data/candidate_list.json b/conductor/conductor/tests/unit/data/candidate_list.json index e29782c..789ab64 100644 --- a/conductor/conductor/tests/unit/data/candidate_list.json +++ b/conductor/conductor/tests/unit/data/candidate_list.json @@ -18,7 +18,8 @@ "complex_name": "dalls_one", "cloud_owner": "att-aic", "cloud_region_version": "1.1", - "physical_location_id": "DLLSTX55" + "physical_location_id": "DLLSTX55", + "vim-id": "att-aic_DLLSTX55" }, { "candidate_id": "NYCNY55", @@ -37,7 +38,8 @@ "complex_name": "ny_one", "cloud_owner": "att-aic", "cloud_region_version": "1.1", - "physical_location_id": "NYCNY55" + "physical_location_id": "NYCNY55", + "vim-id": "att-aic_DLLSTX55" } ] }
\ No newline at end of file diff --git a/conductor/conductor/tests/unit/data/hpa_constraints.json b/conductor/conductor/tests/unit/data/hpa_constraints.json index 3954cd5..829eaf3 100644 --- a/conductor/conductor/tests/unit/data/hpa_constraints.json +++ b/conductor/conductor/tests/unit/data/hpa_constraints.json @@ -9,7 +9,7 @@ "properties": { "evaluate": [ { - "features": [ + "flavorProperties": [ { "architecture": "generic", "hpa-feature": "basicCapabilities", @@ -80,10 +80,10 @@ "hpa-version": "v1" } ], - "label": "flavor_label_1" + "flavorLabel": "flavor_label_1" }, { - "features": [ + "flavorProperties": [ { "architecture": "generic", "hpa-feature": "basicCapabilities", @@ -150,7 +150,7 @@ "hpa-version": "v1" } ], - "label": "flavor_label_2" + "flavorLabel": "flavor_label_2" } ] } @@ -165,6 +165,26 @@ "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" + } + } + } + } } ] } diff --git a/conductor/conductor/tests/unit/data/plugins/inventory_provider/test_multicloud.py b/conductor/conductor/tests/unit/data/plugins/inventory_provider/test_multicloud.py new file mode 100644 index 0000000..aacaab4 --- /dev/null +++ b/conductor/conductor/tests/unit/data/plugins/inventory_provider/test_multicloud.py @@ -0,0 +1,96 @@ +# +# ------------------------------------------------------------------------- +# 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 + +import conductor.data.plugins.vim_controller.multicloud as mc +import mock +from oslo_config import cfg + + +class TestMultiCloud(unittest.TestCase): + + def setUp(self): + cli_opts = [ + cfg.BoolOpt('debug', + short='d', + default=False, + help='Print debugging output.'), + ] + cfg.CONF.register_cli_opts(cli_opts) + self.mc_ep = mc.MULTICLOUD() + self.mc_ep.conf.set_override('debug', False) + + def tearDown(self): + mock.patch.stopall() + + def test_initialize(self): + self.mc_ep.initialize() + self.assertEqual('http://msb.onap.org/api/multicloud', + self.mc_ep.rest.server_url) + self.assertEqual((float(3.05), float(30)), self.mc_ep.rest.timeout) + self.assertEqual(None, super(mc.MULTICLOUD, self.mc_ep).name()) + self.assertEqual("MultiCloud", self.mc_ep.name()) + + @mock.patch.object(mc.LOG, 'error') + @mock.patch.object(mc.LOG, 'debug') + @mock.patch.object(mc.LOG, 'info') + @mock.patch('conductor.common.rest.REST.request') + def test_check_vim_capacity(self, rest_mock, i_mock, d_mock, e_mock): + self.mc_ep.initialize() + response = mock.MagicMock() + response.status_code = 400 + response.text = {"VIMs": ["att-aic_NYCNY33"]} + vim_request = { + "vCPU": 10, + "Memory": { + "quantity": "10", + "unit": "GB" + }, + "Storage": { + "quantity": "100", + "unit": "GB" + }, + "VIMs": ["att-aic_NYCNY33"] + } + + rest_mock.return_value = None + self.assertEqual(None, self.mc_ep.check_vim_capacity(vim_request)) + rest_mock.return_value = response + self.assertEqual(None, self.mc_ep.check_vim_capacity(vim_request)) + response.status_code = 200 + response.json.return_value = response.text + rest_mock.return_value = response + self.assertEqual(['att-aic_NYCNY33'], + self.mc_ep.check_vim_capacity(vim_request)) + response.json.return_value = None + rest_mock.return_value = response + self.assertEqual(None, self.mc_ep.check_vim_capacity(vim_request)) + response.text = {"VIMs": []} + response.json.return_value = response.text + rest_mock.return_value = response + self.assertEqual(None, self.mc_ep.check_vim_capacity(vim_request)) + response.text = {"VIMs": None} + response.json.return_value = response.text + rest_mock.return_value = response + self.assertEqual(None, self.mc_ep.check_vim_capacity(vim_request)) + + +if __name__ == "__main__": + unittest.main() diff --git a/conductor/conductor/tests/unit/data/test_service.py b/conductor/conductor/tests/unit/data/test_service.py index 385b45d..4b841de 100644 --- a/conductor/conductor/tests/unit/data/test_service.py +++ b/conductor/conductor/tests/unit/data/test_service.py @@ -28,6 +28,7 @@ import yaml from conductor.common.utils import conductor_logging_util as log_util from conductor.data.plugins.inventory_provider import extensions as ip_ext from conductor.data.plugins.service_controller import extensions as sc_ext +from conductor.data.plugins.vim_controller import extensions as vc_ext from conductor.data.service import DataEndpoint from oslo_config import cfg @@ -37,9 +38,13 @@ class TestDataEndpoint(unittest.TestCase): def setUp(self): ip_ext_manager = ( ip_ext.Manager(cfg.CONF, 'conductor.inventory_provider.plugin')) + vc_ext_manager = ( + vc_ext.Manager(cfg.CONF, 'conductor.vim_controller.plugin')) sc_ext_manager = ( sc_ext.Manager(cfg.CONF, 'conductor.service_controller.plugin')) - self.data_ep = DataEndpoint(ip_ext_manager, sc_ext_manager) + self.data_ep = DataEndpoint(ip_ext_manager, + vc_ext_manager, + sc_ext_manager) def tearDown(self): pass @@ -232,14 +237,14 @@ class TestDataEndpoint(unittest.TestCase): (constraint_id, constraint_info) = \ hpa_json["conductor_solver"]["constraints"][0].items()[0] hpa_constraint = constraint_info['properties'] - features = hpa_constraint['evaluate'][0]['features'] - label_name = hpa_constraint['evaluate'][0]['label'] + flavorProperties = hpa_constraint['evaluate'][0]['flavorProperties'] + label_name = hpa_constraint['evaluate'][0]['flavorLabel'] ext_mock1.return_value = ['aai'] flavor_info = {"flavor-id": "vim-flavor-id1", "flavor-name": "vim-flavor-name1"} hpa_mock.return_value = [flavor_info] self.maxDiff = None - args = generate_args(candidate_list, features, label_name) + args = generate_args(candidate_list, flavorProperties, label_name) hpa_candidate_list = copy.deepcopy(candidate_list) hpa_candidate_list[1]['flavor_map'] = {} hpa_candidate_list[1]['flavor_map'][label_name] = "vim-flavor-name1" @@ -249,7 +254,7 @@ class TestDataEndpoint(unittest.TestCase): hpa_candidate_list2 = list() hpa_candidate_list2.append(copy.deepcopy(candidate_list[0])) - args = generate_args(candidate_list, features, label_name) + args = generate_args(candidate_list, flavorProperties, label_name) hpa_mock.return_value = [] expected_response = {'response': hpa_candidate_list2, 'error': False} self.assertEqual(expected_response, @@ -274,11 +279,46 @@ class TestDataEndpoint(unittest.TestCase): self.assertEqual(expected_response, self.data_ep.get_candidates_with_hpa(None, args)) + @mock.patch.object(service.LOG, 'warn') + @mock.patch.object(service.LOG, 'info') + @mock.patch.object(stevedore.ExtensionManager, 'names') + @mock.patch.object(stevedore.ExtensionManager, 'map_method') + def test_get_candidates_with_vim_capacity(self, vim_mock, ext_mock1, + info_mock, warn_mock): + req_json_file = './conductor/tests/unit/data/candidate_list.json' + hpa_json_file = './conductor/tests/unit/data/hpa_constraints.json' + hpa_json = yaml.safe_load(open(hpa_json_file).read()) + req_json = yaml.safe_load(open(req_json_file).read()) + candidate_list = req_json['candidate_list'] + ext_mock1.return_value = ['MultiCloud'] + (constraint_id, constraint_info) = \ + hpa_json["conductor_solver"]["constraints"][2].items()[0] + vim_request = constraint_info['properties']['request'] + ctxt = {} + args = {"candidate_list": candidate_list, + "request": vim_request} + vim_mock.return_value = ['att-aic_DLLSTX55'] + self.assertEqual({'response': candidate_list, 'error': False}, + self.data_ep.get_candidates_with_vim_capacity(ctxt, + args)) + vim_mock.return_value = ['att-aic_NYCNY33'] + self.assertEqual({'response': [candidate_list[0]], 'error': False}, + self.data_ep.get_candidates_with_vim_capacity(ctxt, + args)) + vim_mock.return_value = [] + self.assertEqual({'response': candidate_list, 'error': True}, + self.data_ep.get_candidates_with_vim_capacity(ctxt, + args)) + vim_mock.return_value = None + self.assertEqual({'response': candidate_list, 'error': True}, + self.data_ep.get_candidates_with_vim_capacity(ctxt, + args)) + -def generate_args(candidate_list, features, label_name): +def generate_args(candidate_list, flavorProperties, label_name): arg_candidate_list = copy.deepcopy(candidate_list) args = {"candidate_list": arg_candidate_list, - "features": features, + "flavorProperties": flavorProperties, "label_name": label_name} return args diff --git a/conductor/conductor/tests/unit/solver/candidate_list.json b/conductor/conductor/tests/unit/solver/candidate_list.json index e29782c..789ab64 100644 --- a/conductor/conductor/tests/unit/solver/candidate_list.json +++ b/conductor/conductor/tests/unit/solver/candidate_list.json @@ -18,7 +18,8 @@ "complex_name": "dalls_one", "cloud_owner": "att-aic", "cloud_region_version": "1.1", - "physical_location_id": "DLLSTX55" + "physical_location_id": "DLLSTX55", + "vim-id": "att-aic_DLLSTX55" }, { "candidate_id": "NYCNY55", @@ -37,7 +38,8 @@ "complex_name": "ny_one", "cloud_owner": "att-aic", "cloud_region_version": "1.1", - "physical_location_id": "NYCNY55" + "physical_location_id": "NYCNY55", + "vim-id": "att-aic_DLLSTX55" } ] }
\ No newline at end of file diff --git a/conductor/conductor/tests/unit/solver/hpa_constraints.json b/conductor/conductor/tests/unit/solver/hpa_constraints.json index 3954cd5..829eaf3 100644 --- a/conductor/conductor/tests/unit/solver/hpa_constraints.json +++ b/conductor/conductor/tests/unit/solver/hpa_constraints.json @@ -9,7 +9,7 @@ "properties": { "evaluate": [ { - "features": [ + "flavorProperties": [ { "architecture": "generic", "hpa-feature": "basicCapabilities", @@ -80,10 +80,10 @@ "hpa-version": "v1" } ], - "label": "flavor_label_1" + "flavorLabel": "flavor_label_1" }, { - "features": [ + "flavorProperties": [ { "architecture": "generic", "hpa-feature": "basicCapabilities", @@ -150,7 +150,7 @@ "hpa-version": "v1" } ], - "label": "flavor_label_2" + "flavorLabel": "flavor_label_2" } ] } @@ -165,6 +165,26 @@ "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" + } + } + } + } } ] } diff --git a/conductor/conductor/tests/unit/solver/test_vim_fit.py b/conductor/conductor/tests/unit/solver/test_vim_fit.py new file mode 100644 index 0000000..9bbea2b --- /dev/null +++ b/conductor/conductor/tests/unit/solver/test_vim_fit.py @@ -0,0 +1,82 @@ +# +# ------------------------------------------------------------------------- +# 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 + +import mock +import yaml +from conductor.solver.optimizer.constraints import vim_fit +from conductor.solver.utils import constraint_engine_interface as cei + + +class TestVimFit(unittest.TestCase): + + def setUp(self): + req_json_file = './conductor/tests/unit/solver/candidate_list.json' + hpa_json_file = './conductor/tests/unit/solver/hpa_constraints.json' + hpa_json = yaml.safe_load(open(hpa_json_file).read()) + req_json = yaml.safe_load(open(req_json_file).read()) + + (constraint_id, constraint_info) = \ + hpa_json["conductor_solver"]["constraints"][2].items()[0] + c_property = constraint_info['properties'] + constraint_type = constraint_info['properties'] + constraint_demands = list() + parsed_demands = constraint_info['demands'] + if isinstance(parsed_demands, list): + for d in parsed_demands: + constraint_demands.append(d) + self.vim_fit = vim_fit.VimFit(constraint_id, + constraint_type, + constraint_demands, + _properties=c_property) + + self.candidate_list = req_json['candidate_list'] + + def tearDown(self): + pass + + @mock.patch.object(vim_fit.LOG, 'error') + @mock.patch.object(vim_fit.LOG, 'info') + @mock.patch.object(vim_fit.LOG, 'debug') + def test_solve(self, debug_mock, info_mock, error_mock): + + self.maxDiff = None + + mock_decision_path = mock.MagicMock() + mock_decision_path.current_demand.name = 'vG' + request_mock = mock.MagicMock() + client_mock = mock.MagicMock() + client_mock.call.return_value = None + request_mock.cei = cei.ConstraintEngineInterface(client_mock) + + self.assertEqual(self.candidate_list, + self.vim_fit.solve(mock_decision_path, + self.candidate_list, request_mock)) + client_mock.call.return_value = self.candidate_list[1] + request_mock.cei = cei.ConstraintEngineInterface(client_mock) + + self.assertEqual(self.candidate_list[1], + self.vim_fit.solve(mock_decision_path, + self.candidate_list, request_mock)) + + +if __name__ == "__main__": + unittest.main() diff --git a/conductor/setup.cfg b/conductor/setup.cfg index 8e3fc56..c6fd3c4 100644 --- a/conductor/setup.cfg +++ b/conductor/setup.cfg @@ -48,6 +48,9 @@ console_scripts = conductor.inventory_provider.plugin = aai = conductor.data.plugins.inventory_provider.aai:AAI +conductor.vim_controller.plugin = + multicloud = conductor.data.plugins.vim_controller.multicloud:MULTICLOUD + conductor.service_controller.plugin = sdnc = conductor.data.plugins.service_controller.sdnc:SDNC |