From 39f63ef31ba3f0dc0db3ee8d5ff502a715702e1a Mon Sep 17 00:00:00 2001 From: Dileep Ranganathan Date: Tue, 20 Mar 2018 07:47:27 -0700 Subject: Multicloud vim controller plugin Implemented Multicloud vim controller plugin Updated translator to parse vim_fit constraint Implemented vim_fit constraint type Implemented RPC for check_vim_capacity Reordered constraint rank Added unit tests Change-Id: I5f01cf8fbefbb4b53e4370c5c6b43f72897e62bd Issue-ID: OPTFRA-148 Signed-off-by: Dileep Ranganathan --- conductor/conductor/conf/vim_controller.py | 33 +++++ conductor/conductor/controller/translator.py | 16 ++- .../data/plugins/vim_controller/__init__.py | 0 .../conductor/data/plugins/vim_controller/base.py | 37 ++++++ .../data/plugins/vim_controller/extensions.py | 45 +++++++ .../data/plugins/vim_controller/multicloud.py | 144 +++++++++++++++++++++ conductor/conductor/data/service.py | 53 +++++++- conductor/conductor/opts.py | 6 + .../solver/optimizer/constraints/vim_fit.py | 60 +++++++++ conductor/conductor/solver/request/parser.py | 17 ++- .../solver/utils/constraint_engine_interface.py | 17 +++ .../tests/unit/controller/test_translator.py | 62 +++++++++ .../conductor/tests/unit/data/candidate_list.json | 6 +- .../conductor/tests/unit/data/hpa_constraints.json | 20 +++ .../plugins/inventory_provider/test_multicloud.py | 96 ++++++++++++++ .../conductor/tests/unit/data/test_service.py | 42 +++++- .../tests/unit/solver/candidate_list.json | 6 +- .../tests/unit/solver/hpa_constraints.json | 20 +++ .../conductor/tests/unit/solver/test_vim_fit.py | 82 ++++++++++++ conductor/setup.cfg | 3 + 20 files changed, 750 insertions(+), 15 deletions(-) create mode 100644 conductor/conductor/conf/vim_controller.py create mode 100644 conductor/conductor/data/plugins/vim_controller/__init__.py create mode 100644 conductor/conductor/data/plugins/vim_controller/base.py create mode 100644 conductor/conductor/data/plugins/vim_controller/extensions.py create mode 100644 conductor/conductor/data/plugins/vim_controller/multicloud.py create mode 100644 conductor/conductor/solver/optimizer/constraints/vim_fit.py create mode 100644 conductor/conductor/tests/unit/data/plugins/inventory_provider/test_multicloud.py create mode 100644 conductor/conductor/tests/unit/solver/test_vim_fit.py 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..724b068 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,6 +94,11 @@ CONSTRAINTS = { 'category': ['disaster', 'region', 'complex', 'country', 'time', 'maintenance']}, }, + 'vim_fit': { + 'split': True, + 'required': ['controller'], + 'optional': ['request'], + }, } 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 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..4948656 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 = {} @@ -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/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..a662425 100644 --- a/conductor/conductor/solver/utils/constraint_engine_interface.py +++ b/conductor/conductor/solver/utils/constraint_engine_interface.py @@ -135,3 +135,20 @@ class ConstraintEngineInterface(object): 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..0e3bf8e 100644 --- a/conductor/conductor/tests/unit/controller/test_translator.py +++ b/conductor/conductor/tests/unit/controller/test_translator.py @@ -223,6 +223,68 @@ class TestNoExceptionTranslator(unittest.TestCase): 'type': 'distance_to_location'}} self.assertEquals(self.Translator.parse_constraints(constraints), rtn) + 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..9139204 100644 --- a/conductor/conductor/tests/unit/data/hpa_constraints.json +++ b/conductor/conductor/tests/unit/data/hpa_constraints.json @@ -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..3f2adde 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 @@ -274,6 +279,41 @@ 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): arg_candidate_list = copy.deepcopy(candidate_list) 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..9139204 100644 --- a/conductor/conductor/tests/unit/solver/hpa_constraints.json +++ b/conductor/conductor/tests/unit/solver/hpa_constraints.json @@ -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 -- cgit 1.2.3-korg