Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cloudbridge/cloud/base/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ def debug_mode(self):


class BaseCloudProvider(CloudProvider):
def __init__(self, config):
def __init__(self, config, middleware_manager=None):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, better than sending in middleware as a list.

self._config = BaseConfiguration(config)
self._config_parser = ConfigParser()
self._config_parser.read(CloudBridgeConfigLocations)
self._middleware = SimpleMiddlewareManager()
self._middleware = middleware_manager or SimpleMiddlewareManager()
self.add_required_middleware()

@property
Expand Down
60 changes: 59 additions & 1 deletion cloudbridge/cloud/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import pkgutil
from collections import defaultdict

from pyeventsystem.interfaces import Middleware
from pyeventsystem.middleware import AutoDiscoveredMiddleware
from pyeventsystem.middleware import SimpleMiddlewareManager

from cloudbridge.cloud import providers
from cloudbridge.cloud.interfaces import CloudProvider
from cloudbridge.cloud.interfaces import TestMockHelperMixin
Expand All @@ -12,6 +16,54 @@
log = logging.getLogger(__name__)


# Todo: Move to pyeventsystem if we're keeping this logic
class ParentMiddlewareManager(SimpleMiddlewareManager):

def __init__(self, event_manager=None):
super(ParentMiddlewareManager, self).__init__(event_manager)
self.middleware_constructors = []

def add_constructor(self, middleware_class, *args):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could possibly be renamed to add_middleware_class, accepting the class in question, and both the constructor args and kwargs.

self.middleware_constructors.append((middleware_class, args))

def remove_constructor(self, middleware_class, *args):
self.middleware_constructors.remove((middleware_class, args))

def generate_simple_manager(self):
new_manager = SimpleMiddlewareManager()
for middleware in self.middleware_list:
new_manager.add(middleware)
for constructor, args in self.middleware_constructors:
m = constructor(*args)
new_manager.add(m)
for handler in self.get_subscribed_handlers():
new_handler = handler.__class__(handler.event_pattern,
handler.priority,
handler.callback)
new_manager.events.subscribe(new_handler)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this means that events added to the factory later won't be subscribed to previously created providers? This is ok, but I think we need to clarify how exactly factory event subscriptions are meant to work and what the intended use cases are, so we know for sure that this is the correct behaviour.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was the original question that I had about this: whether we should make the provider object independent after it was created, or always treat it as a "child" of the factory. If the latter, it's definitely doable, but I would then suggest making the factory accessible through the provider as well.

return new_manager

# Removing install step from add. Since this manager is meant to create
# other managers rather than run. This will also simplify separating
# handlers added through middleware from those subscribed directly
def add(self, middleware):
Copy link
Copy Markdown
Contributor

@nuwang nuwang Feb 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should provide dual semantics here - one for shared middleware objects, and another for independent middleware, just to keep it simple. I'm currently leaning in favour of the factory namespacing child event dispatchers, and forwarding them as necessary, so that we still maintain event isolation within a given provider. I think we probably just need to avoid a scenario where providers are unwittingly subscribing to events outside of the provider.

if isinstance(middleware, Middleware):
m = middleware
else:
m = AutoDiscoveredMiddleware(middleware)
self.middleware_list.append(m)
return m

def get_subscribed_handlers(self):
handlers = []
# Todo: Expose this better in pyeventsystem library
event_dict = self.events._SimpleEventDispatcher__events
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we can't just access self.events?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.events is the SimpleEventDispatcher, which currently doesn't have a property exposing the events dict. We definitely should add the property, but I was avoiding changing the other library for the time being, before we discuss this further.

for key in event_dict.keys():
for handler in event_dict[key]:
handlers.append(handler)
return handlers


class ProviderList(object):
AWS = 'aws'
AZURE = 'azure'
Expand All @@ -27,9 +79,14 @@ class CloudProviderFactory(object):
"""

def __init__(self):
self._middleware = ParentMiddlewareManager()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a different name because ParentMiddlewareManager doesn't seem to explain what it's doing. Also, there is the issue of namespacing. And finally, I guess there's the issue of being able to intercept behaviour of a particular cloud (say openstack), and change behaviour for all classes of that cloud. One way would be to prefix the cloudname to the event. Another way would be is to check the instancetype of the sender: if isinstance(OpenStackProvider, event_args['sender'].provider) or some such thing. I think namespacing would be good as long as isolation is maintained.

self.provider_list = defaultdict(dict)
log.debug("Providers List: %s", self.provider_list)

@property
def middleware(self):
return self._middleware

def register_provider_class(self, cls):
"""
Registers a provider class with the factory. The class must
Expand Down Expand Up @@ -136,7 +193,8 @@ def create_provider(self, name, config):
'A provider with name {0} could not be'
' found'.format(name))
log.debug("Created '%s' provider", name)
return provider_class(config)
return provider_class(config,
self.middleware.generate_simple_manager())

def get_provider_class(self, name):
"""
Expand Down
4 changes: 2 additions & 2 deletions cloudbridge/cloud/providers/aws/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class AWSCloudProvider(BaseCloudProvider):
PROVIDER_ID = 'aws'
AWS_INSTANCE_DATA_DEFAULT_URL = "http://cloudve.org/cb-aws-vmtypes.json"

def __init__(self, config):
super(AWSCloudProvider, self).__init__(config)
def __init__(self, config, middleware_manager=None):
super(AWSCloudProvider, self).__init__(config, middleware_manager)

# Initialize cloud connection fields
# These are passed as-is to Boto
Expand Down
4 changes: 2 additions & 2 deletions cloudbridge/cloud/providers/azure/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
class AzureCloudProvider(BaseCloudProvider):
PROVIDER_ID = 'azure'

def __init__(self, config):
super(AzureCloudProvider, self).__init__(config)
def __init__(self, config, middleware_manager=None):
super(AzureCloudProvider, self).__init__(config, middleware_manager)

# mandatory config values
self.subscription_id = self._get_config_value(
Expand Down
4 changes: 2 additions & 2 deletions cloudbridge/cloud/providers/gcp/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ class GCPCloudProvider(BaseCloudProvider):

PROVIDER_ID = 'gcp'

def __init__(self, config):
super(GCPCloudProvider, self).__init__(config)
def __init__(self, config, middleware_manager=None):
super(GCPCloudProvider, self).__init__(config, middleware_manager)

# Disable warnings about file_cache not being available when using
# oauth2client >= 4.0.0.
Expand Down
4 changes: 2 additions & 2 deletions cloudbridge/cloud/providers/mock/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
"""
PROVIDER_ID = 'mock'

def __init__(self, config):
def __init__(self, config, middleware_manager=None):
self.setUpMock()
super(MockAWSCloudProvider, self).__init__(config)
super(MockAWSCloudProvider, self).__init__(config, middleware_manager)

def setUpMock(self):
"""
Expand Down
5 changes: 3 additions & 2 deletions cloudbridge/cloud/providers/openstack/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ class OpenStackCloudProvider(BaseCloudProvider):

PROVIDER_ID = 'openstack'

def __init__(self, config):
super(OpenStackCloudProvider, self).__init__(config)
def __init__(self, config, middleware_manager=None):
super(OpenStackCloudProvider, self).__init__(config,
middleware_manager)

# Initialize cloud connection fields
self.username = self._get_config_value(
Expand Down
97 changes: 97 additions & 0 deletions test/test_cloud_factory.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import unittest

from pyeventsystem.middleware import intercept

from cloudbridge.cloud import factory
from cloudbridge.cloud import interfaces
from cloudbridge.cloud.base import helpers as cb_helpers
from cloudbridge.cloud.factory import CloudProviderFactory
from cloudbridge.cloud.interfaces import TestMockHelperMixin
from cloudbridge.cloud.interfaces.provider import CloudProvider
Expand Down Expand Up @@ -85,3 +88,97 @@ class DummyClass(CloudProvider):
factory.register_provider_class(DummyClass)
self.assertTrue(DummyClass not in
factory.get_all_provider_classes())

def test_middleware_inherited(self):
start_count = 10

class SomeDummyClass(object):
count = start_count

@intercept(event_pattern="*", priority=2499)
def return_incremented(self, event_args, *args, **kwargs):
self.count += 1
return self.count

factory = CloudProviderFactory()
some_obj = SomeDummyClass()
factory.middleware.add(some_obj)
provider_name = cb_helpers.get_env("CB_TEST_PROVIDER", "aws")
first_prov = factory.create_provider(provider_name, {})
# Any dispatched event should be intercepted and increment the count
first_prov.storage.volumes.get("anything")
self.assertEqual(first_prov.networking.networks.get("anything"),
start_count + 2)
second_prov = factory.create_provider(provider_name, {})
# This count should be independent of the previous one
self.assertEqual(second_prov.networking.networks.get("anything"),
start_count + 3)

def test_middleware_inherited_constructor(self):
start_count = 10
increment = 2

class SomeDummyClass(object):
count = start_count

@intercept(event_pattern="*", priority=2499)
def return_incremented(self, event_args, *args, **kwargs):
self.count += 1
return self.count

factory = CloudProviderFactory()
factory.middleware.add_constructor(SomeDummyClass)
provider_name = cb_helpers.get_env("CB_TEST_PROVIDER", "aws")
first_prov = factory.create_provider(provider_name, {})
# Any dispatched event should be intercepted and increment the count
first_prov.storage.volumes.get("anything")
self.assertEqual(first_prov.networking.networks.get("anything"),
start_count + 2)
second_prov = factory.create_provider(provider_name, {})
# This count should be independent of the previous one
self.assertEqual(second_prov.networking.networks.get("anything"),
start_count + 1)

class SomeDummyClassWithArgs(object):
def __init__(self, start, increment):
self.count = start
self.increment = increment

@intercept(event_pattern="*", priority=2499)
def return_incremented(self, event_args, *args, **kwargs):
self.count += self.increment
return self.count

factory = CloudProviderFactory()
factory.middleware.add_constructor(SomeDummyClassWithArgs,
start_count, increment)
provider_name = cb_helpers.get_env("CB_TEST_PROVIDER", "aws")
first_prov = factory.create_provider(provider_name, {})
# Any dispatched event should be intercepted and increment the count
first_prov.storage.volumes.get("anything")
self.assertEqual(first_prov.networking.networks.get("anything"),
start_count + 2*increment)
second_prov = factory.create_provider(provider_name, {})
# This count should be independent of the previous one
self.assertEqual(second_prov.networking.networks.get("anything"),
start_count + increment)

def test_middleware_inherited_events(self):

class SomeDummyClass(object):

@intercept(event_pattern="*", priority=2499)
def return_goodbye(self, event_args, *args, **kwargs):
return "goodbye"

def return_hello(event_args, *args, **kwargs):
return "hello"

factory = CloudProviderFactory()
factory.middleware.add(SomeDummyClass())
factory.middleware.events.intercept("*", 2490, return_hello)
provider_name = cb_helpers.get_env("CB_TEST_PROVIDER", "aws")
prov = factory.create_provider(provider_name, {})
# Any dispatched event should be intercepted and return "hello" instead
self.assertEqual(prov.networking.networks.get("anything"),
"hello")