Ver Fonte

Added auto-discovery of cloud providers to the CloudProvider factory.

Nuwan Goonasekera há 10 anos atrás
pai
commit
d170ab3799

+ 21 - 0
cloudbridge/cloud/base/helpers.py

@@ -0,0 +1,21 @@
+class TestMockHelperMixin(object):
+    """
+    A helper class that providers mock drivers can use to be notified when a
+    test setup/teardown occurs. This is useful when activating libraries
+    like HTTPretty which take over socket communications.
+    """
+
+    def setUpMock(self):
+        """
+        Called before a test is started.
+        """
+        raise NotImplementedError(
+            'TestMockHelperMixin.setUpMock not implemented')
+
+    def tearDownMock(self):
+        """
+        Called before test teardown.
+        """
+        raise NotImplementedError(
+            'TestMockHelperMixin.tearDownMock not implemented by this'
+            ' provider')

+ 92 - 61
cloudbridge/cloud/factory.py

@@ -1,10 +1,20 @@
+from collections import defaultdict
 import importlib
+import inspect
+import logging
+import pkgutil
+
+from cloudbridge.cloud import providers
+from cloudbridge.cloud.base.helpers import TestMockHelperMixin
+from cloudbridge.cloud.interfaces import CloudProvider
+
+
+log = logging.getLogger(__name__)
 
 
 class ProviderList(object):
     AWS = 'aws'
     OPENSTACK = 'openstack'
-    AZURE = 'azure'
 
 
 class CloudProviderFactory(object):
@@ -13,50 +23,80 @@ class CloudProviderFactory(object):
     Get info and handle on the available cloud provider implementations.
     """
 
-    def list_providers(self):
-        """
-        Get a list of available providers.
+    def __init__(self):
+        self.provider_list = defaultdict(dict)
 
-        (This function could eventually be implemented as a registry file
-        containing all available implementations, or alternatively, using
-        automatic discovery.)
-
-        :rtype: list
-        :return: A list of available providers and their interface versions.
+    def register_provider_class(self, cls):
         """
-        return [
-            {"name": ProviderList.OPENSTACK,
-             "class": "cloudbridge.cloud.providers.openstack.OpenStackCloud"
-                      "Provider"
-             },
-            {"name": ProviderList.AWS,
-             "class": "cloudbridge.cloud.providers.aws.AWSCloudProvider",
-             "mock_class": "cloudbridge.cloud.providers.aws.MockAWSCloud"
-                           "Provider"
-             }]
-
-    def find_provider_impl(self, name, get_mock=False):
+        Registers a provider class with the factory. The class must
+        inherit from cloudbridge.cloud.interfaces.CloudProvider
+        and also have a class attribute named SHORT_NAME.
+
+        The SHORT_NAME is a user friendly short name for the cloud provider,
+        such as 'aws'. The SHORT_NAME must also be included in the
+        cloudbridge.factory.ProviderList.
+
+        :type  cls: class
+        :param cls: A class implementing the CloudProvider interface.
+                    Mock providers must also implement
+                    :py:class:`cloudbridge.cloud.base.helpers.
+                    TestMockHelperMixin`.
         """
-        Finds a provider implementation class given its name.
+        if isinstance(cls, type) and issubclass(cls, CloudProvider):
+            if hasattr(cls, "SHORT_NAME"):
+                short_name = getattr(cls, "SHORT_NAME")
+                if issubclass(cls, TestMockHelperMixin):
+                    if self.provider_list.get(short_name, {}).get(
+                            'mock_class'):
+                        log.warn("Mock provider with shortname: %s is already "
+                                 "registered. Overriding with class: %s",
+                                 short_name, cls)
+                    self.provider_list[short_name]['mock_class'] = cls
+                else:
+                    if self.provider_list.get(short_name, {}).get('class'):
+                        log.warn("Provider with shortname: %s is already "
+                                 "registered. Overriding with class: %s",
+                                 short_name, cls)
+                    self.provider_list[short_name]['class'] = cls
+            else:
+                log.warn("Provider class: %s implements CloudProvider but"
+                         " does not define SHORT_NAME. Ignoring...", cls)
+        else:
+            log.debug("Class: %s does not implement the CloudProvider"
+                      "  interface. Ignoring...", cls)
 
-        :type name: str
-        :param name: A name of the provider whose implementation to look for.
+    def discover_providers(self):
+        """
+        Discover all available providers within the
+        cloudbridge.cloud.providers package.
+        """
+        for _, modname, _ in pkgutil.iter_modules(providers.__path__):
+            module = importlib.import_module(
+                "{0}.{1}".format(providers.__name__,
+                                 modname))
+            classes = inspect.getmembers(module, inspect.isclass)
+            for _, cls in classes:
+                self.register_provider_class(cls)
 
-        :type get_mock: ``bool``
-        :param get_mock: If True, returns a mock version of the provider
-        if available, or the real version if not.
+    def list_providers(self):
+        """
+        Get a list of available providers.
 
-        :rtype: ``None`` or str
-        :return: If found, return a module (including class name) of the
-                 provider or ``None`` if the provider was not found.
+        It uses a simple automatic discovery system by iterating through all
+        submodules in cloudbridge.cloud.providers.
+
+        :rtype: dict
+        :return: A dict of available providers and their implementations in the
+                 following format::
+                 {'aws': {'class': aws.provider.AWSCloudProvider,
+                          'mock_class': aws.provider.MockAWSCloudProvider},
+                  'openstack': {'class': openstack.provider.OpenStackCloudProvi
+                                         der}
+                 }
         """
-        for provider in self.list_providers():
-            if provider['name'] == name:
-                if get_mock and provider.get("mock_class"):
-                    return provider["mock_class"]
-                else:
-                    return provider["class"]
-        return None
+        if not self.provider_list:
+            self.discover_providers()
+        return self.provider_list
 
     def create_provider(self, name, config):
         """
@@ -76,20 +116,13 @@ class CloudProviderFactory(object):
         :return:  a concrete provider instance
         :rtype: ``object`` of :class:`.CloudProvider`
         """
-        impl = self.find_provider_impl(name)
-        if impl is None:
+        provider_class = self.get_provider_class(name)
+        if provider_class is None:
             raise NotImplementedError(
                 'A provider with name {0} could not be'
                 ' found'.format(name))
-        provider_class = self._get_provider_class(impl)
         return provider_class(config)
 
-    def _get_provider_class(self, impl):
-        module_name, class_name = impl.rsplit(".", 1)
-        provider_class = getattr(importlib.import_module(module_name),
-                                 class_name)
-        return provider_class
-
     def get_provider_class(self, name, get_mock=False):
         """
         Return a class for the requested provider.
@@ -102,12 +135,14 @@ class CloudProviderFactory(object):
         :return: A class corresponding to the requested provider or ``None``
                  if the provider was not found.
         """
-        provider_class = self.find_provider_impl(
-            name,
-            get_mock=get_mock)
-        if provider_class:
-            return self._get_provider_class(provider_class)
-        return None
+        impl = self.list_providers().get(name)
+        if impl:
+            if get_mock and impl.get("mock_class"):
+                return impl["mock_class"]
+            else:
+                return impl["class"]
+        else:
+            return None
 
     def get_all_provider_classes(self, get_mock=False):
         """
@@ -122,13 +157,9 @@ class CloudProviderFactory(object):
         if none found.
         """
         all_providers = []
-        for provider in self.list_providers():
-            if get_mock and provider.get("mock_class"):
-                all_providers.append(
-                    self._get_provider_class(
-                        provider["mock_class"]))
+        for impl in self.list_providers().values():
+            if get_mock and impl.get("mock_class"):
+                all_providers.append(impl["mock_class"])
             else:
-                all_providers.append(
-                    self._get_provider_class(
-                        provider["class"]))
+                all_providers.append(impl["class"])
         return all_providers

+ 3 - 1
cloudbridge/cloud/providers/aws/provider.py

@@ -11,7 +11,7 @@ from moto.ec2 import mock_ec2
 from moto.s3 import mock_s3
 
 from cloudbridge.cloud.base import BaseCloudProvider
-from test.helpers import TestMockHelperMixin
+from cloudbridge.cloud.base.helpers import TestMockHelperMixin
 
 from .services import AWSBlockStoreService
 from .services import AWSComputeService
@@ -21,6 +21,8 @@ from .services import AWSSecurityService
 
 class AWSCloudProvider(BaseCloudProvider):
 
+    SHORT_NAME = 'aws'
+
     def __init__(self, config):
         super(AWSCloudProvider, self).__init__(config)
         self.cloud_type = 'aws'

+ 2 - 0
cloudbridge/cloud/providers/openstack/provider.py

@@ -24,6 +24,8 @@ from .services import OpenStackSecurityService
 
 class OpenStackCloudProvider(BaseCloudProvider):
 
+    SHORT_NAME = 'openstack'
+
     def __init__(self, config):
         super(OpenStackCloudProvider, self).__init__(config)
 

+ 2 - 23
test/helpers.py

@@ -2,8 +2,10 @@ from contextlib import contextmanager
 import os
 import sys
 import unittest
+
 from six import reraise
 
+from cloudbridge.cloud.base.helpers import TestMockHelperMixin
 from cloudbridge.cloud.factory import CloudProviderFactory
 
 
@@ -91,29 +93,6 @@ def get_test_instance(provider, name, keypair=None, security_groups=None):
     return instance
 
 
-class TestMockHelperMixin(object):
-    """
-    A helper class that providers mock drivers can use to be notified when a
-    test setup/teardown occurs. This is useful when activating libraries
-    like HTTPretty which take over socket communications.
-    """
-
-    def setUpMock(self):
-        """
-        Called before a test is started.
-        """
-        raise NotImplementedError(
-            'TestMockHelperMixin.setUpMock not implemented')
-
-    def tearDownMock(self):
-        """
-        Called before test teardown.
-        """
-        raise NotImplementedError(
-            'TestMockHelperMixin.tearDownMock not implemented by this'
-            ' provider')
-
-
 class ProviderTestBase(object):
 
     """

+ 43 - 18
test/test_cloud_factory.py

@@ -2,7 +2,9 @@ import unittest
 
 from cloudbridge.cloud import factory
 from cloudbridge.cloud import interfaces
+from cloudbridge.cloud.base.helpers import TestMockHelperMixin
 from cloudbridge.cloud.factory import CloudProviderFactory
+from cloudbridge.cloud.interfaces.provider import CloudProvider
 from cloudbridge.cloud.providers.aws import AWSCloudProvider
 import test.helpers as helpers
 
@@ -27,23 +29,6 @@ class CloudFactoryTestCase(unittest.TestCase):
         with self.assertRaises(NotImplementedError):
             CloudProviderFactory().create_provider("ec23", {})
 
-    def test_find_provider_impl_valid(self):
-        """
-        Searching for a provider with a known name should return a
-        valid implementation
-        """
-        self.assertEqual(CloudProviderFactory().find_provider_impl(
-            factory.ProviderList.AWS),
-            "cloudbridge.cloud.providers.aws.AWSCloudProvider")
-
-    def test_find_provider_impl_invalid(self):
-        """
-        Searching for a provider with an invalid name should return
-        None
-        """
-        self.assertIsNone(
-            CloudProviderFactory().find_provider_impl("openstack1"))
-
     def test_find_provider_mock_valid(self):
         """
         Searching for a provider with a known mock driver should return
@@ -61,7 +46,7 @@ class CloudFactoryTestCase(unittest.TestCase):
             self.assertTrue(
                 not issubclass(
                     cls,
-                    helpers.TestMockHelperMixin),
+                    TestMockHelperMixin),
                 "Did not expect mock but %s implements mock provider" %
                 cls)
 
@@ -79,3 +64,43 @@ class CloudFactoryTestCase(unittest.TestCase):
         return None
         """
         self.assertIsNone(CloudProviderFactory().get_provider_class("aws1"))
+
+    def test_register_test_class_invalid(self):
+        """
+        Attempting to register an invalid test class should be ignored
+        """
+        class DummyClass(object):
+            SHORT_NAME = 'aws'
+
+        factory = CloudProviderFactory()
+        factory.register_provider_class(DummyClass)
+        self.assertTrue(DummyClass not in
+                        factory.get_all_provider_classes(get_mock=False))
+
+    def test_register_test_class_double(self):
+        """
+        Attempting to register the same class twice should register second
+        instance
+        """
+        class DummyClass(CloudProvider):
+            SHORT_NAME = 'aws'
+
+        factory = CloudProviderFactory()
+        factory.register_provider_class(DummyClass)
+        self.assertTrue(DummyClass in
+                        factory.get_all_provider_classes(get_mock=False))
+        self.assertTrue(AWSCloudProvider not in
+                        factory.get_all_provider_classes(get_mock=False))
+
+    def test_register_test_class_without_shortname(self):
+        """
+        Attempting to register a class without a SHORT_NAME attribute
+        should be ignored.
+        """
+        class DummyClass(CloudProvider):
+            pass
+
+        factory = CloudProviderFactory()
+        factory.register_provider_class(DummyClass)
+        self.assertTrue(DummyClass not in
+                        factory.get_all_provider_classes(get_mock=False))