Multicloud vim controller plugin 81/37681/7
authorDileep Ranganathan <dileep.ranganathan@intel.com>
Tue, 20 Mar 2018 14:47:27 +0000 (07:47 -0700)
committerDileep Ranganathan <dileep.ranganathan@intel.com>
Wed, 28 Mar 2018 10:35:55 +0000 (03:35 -0700)
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 <dileep.ranganathan@intel.com>
20 files changed:
conductor/conductor/conf/vim_controller.py [new file with mode: 0644]
conductor/conductor/controller/translator.py
conductor/conductor/data/plugins/vim_controller/__init__.py [new file with mode: 0644]
conductor/conductor/data/plugins/vim_controller/base.py [new file with mode: 0644]
conductor/conductor/data/plugins/vim_controller/extensions.py [new file with mode: 0644]
conductor/conductor/data/plugins/vim_controller/multicloud.py [new file with mode: 0644]
conductor/conductor/data/service.py
conductor/conductor/opts.py
conductor/conductor/solver/optimizer/constraints/vim_fit.py [new file with mode: 0644]
conductor/conductor/solver/request/parser.py
conductor/conductor/solver/utils/constraint_engine_interface.py
conductor/conductor/tests/unit/controller/test_translator.py
conductor/conductor/tests/unit/data/candidate_list.json
conductor/conductor/tests/unit/data/hpa_constraints.json
conductor/conductor/tests/unit/data/plugins/inventory_provider/test_multicloud.py [new file with mode: 0644]
conductor/conductor/tests/unit/data/test_service.py
conductor/conductor/tests/unit/solver/candidate_list.json
conductor/conductor/tests/unit/solver/hpa_constraints.json
conductor/conductor/tests/unit/solver/test_vim_fit.py [new file with mode: 0644]
conductor/setup.cfg

diff --git a/conductor/conductor/conf/vim_controller.py b/conductor/conductor/conf/vim_controller.py
new file mode 100644 (file)
index 0000000..91eb079
--- /dev/null
@@ -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')
index dbff2d2..724b068 100644 (file)
@@ -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 (file)
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 (file)
index 0000000..6f35924
--- /dev/null
@@ -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 (file)
index 0000000..25887cf
--- /dev/null
@@ -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 (file)
index 0000000..cdc6cde
--- /dev/null
@@ -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
index 9617217..4948656 100644 (file)
@@ -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'))
index b32a39b..e2ace38 100644 (file)
@@ -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 (file)
index 0000000..6e6e052
--- /dev/null
@@ -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
index d7f3cec..0def215 100755 (executable)
@@ -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
index 256e4bb..a662425 100644 (file)
@@ -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
index 26b1182..0e3bf8e 100644 (file)
@@ -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',
index e29782c..789ab64 100644 (file)
@@ -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
index 3954cd5..9139204 100644 (file)
             "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 (file)
index 0000000..aacaab4
--- /dev/null
@@ -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()
index 385b45d..3f2adde 100644 (file)
@@ -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)
index e29782c..789ab64 100644 (file)
@@ -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
index 3954cd5..9139204 100644 (file)
             "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 (file)
index 0000000..9bbea2b
--- /dev/null
@@ -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()
index 8e3fc56..c6fd3c4 100644 (file)
@@ -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