Răsfoiți Sursa

Merge pull request #179 from CloudVE/middleware

Middleware
Nuwan Goonasekera 7 ani în urmă
părinte
comite
4bf6d39fad
33 a modificat fișierele cu 2557 adăugiri și 321 ștergeri
  1. 195 0
      cloudbridge/cloud/base/events.py
  2. 177 0
      cloudbridge/cloud/base/middleware.py
  3. 15 4
      cloudbridge/cloud/base/provider.py
  4. 22 0
      cloudbridge/cloud/base/resources.py
  5. 164 0
      cloudbridge/cloud/base/services.py
  6. 235 0
      cloudbridge/cloud/interfaces/events.py
  7. 7 0
      cloudbridge/cloud/interfaces/exceptions.py
  8. 62 0
      cloudbridge/cloud/interfaces/middleware.py
  9. 3 1
      cloudbridge/cloud/interfaces/provider.py
  10. 3 1
      cloudbridge/cloud/interfaces/resources.py
  11. 103 1
      cloudbridge/cloud/interfaces/services.py
  12. 0 37
      cloudbridge/cloud/providers/aws/resources.py
  13. 70 18
      cloudbridge/cloud/providers/aws/services.py
  14. 4 3
      cloudbridge/cloud/providers/azure/azure_client.py
  15. 4 3
      cloudbridge/cloud/providers/azure/provider.py
  16. 33 55
      cloudbridge/cloud/providers/azure/resources.py
  17. 115 47
      cloudbridge/cloud/providers/azure/services.py
  18. 10 80
      cloudbridge/cloud/providers/gce/resources.py
  19. 115 4
      cloudbridge/cloud/providers/gce/services.py
  20. 2 50
      cloudbridge/cloud/providers/openstack/resources.py
  21. 80 17
      cloudbridge/cloud/providers/openstack/services.py
  22. 219 0
      docs/topics/event_system.rst
  23. 34 0
      test/helpers/__init__.py
  24. 19 0
      test/test_block_store_service.py
  25. 11 0
      test/test_compute_service.py
  26. 468 0
      test/test_event_system.py
  27. 10 0
      test/test_image_service.py
  28. 286 0
      test/test_middleware_system.py
  29. 29 0
      test/test_network_service.py
  30. 19 0
      test/test_object_store_service.py
  31. 11 0
      test/test_region_service.py
  32. 21 0
      test/test_security_service.py
  33. 11 0
      test/test_vm_types_service.py

+ 195 - 0
cloudbridge/cloud/base/events.py

@@ -0,0 +1,195 @@
+import bisect
+import fnmatch
+import logging
+import re
+
+from ..interfaces.events import EventDispatcher
+from ..interfaces.events import EventHandler
+from ..interfaces.exceptions import HandlerException
+
+log = logging.getLogger(__name__)
+
+
+class InterceptingEventHandler(EventHandler):
+
+    def __init__(self, event_pattern, priority, callback):
+        self.__dispatcher = None
+        self.event_pattern = event_pattern
+        self.priority = priority
+        self.callback = callback
+
+    def __lt__(self, other):
+        # This is required for the bisect module to insert
+        # event handlers sorted by priority
+        return self.priority < other.priority
+
+    def _get_next_handler(self, event):
+        handler_list = self.dispatcher.get_handlers_for_event(event)
+        # find position of this handler
+        pos = bisect.bisect_left(handler_list, self)
+        assert handler_list[pos] == self
+        if pos < len(handler_list)-1:
+            return handler_list[pos+1]
+        else:
+            return None
+
+    def event_pattern(self):
+        pass
+
+    def priority(self):
+        pass
+
+    def callback(self):
+        pass
+
+    @property
+    def dispatcher(self):
+        return self.__dispatcher
+
+    @dispatcher.setter
+    # pylint:disable=arguments-differ
+    def dispatcher(self, value):
+        self.__dispatcher = value
+
+    def invoke(self, event_args, *args, **kwargs):
+        next_handler = self._get_next_handler(event_args.get('event'))
+        event_args['next_handler'] = next_handler
+        # callback is responsible for invoking the next_handler and
+        # controlling the result value
+        result = self.callback(event_args, *args, **kwargs)
+        # Remove handler specific callback info
+        event_args.pop('next_handler', None)
+        return result
+
+    def unsubscribe(self):
+        if self.dispatcher:
+            self.dispatcher.unsubscribe(self)
+
+
+class ObservingEventHandler(InterceptingEventHandler):
+
+    def __init__(self, event_pattern, priority, callback):
+        super(ObservingEventHandler, self).__init__(event_pattern, priority,
+                                                    callback)
+
+    def invoke(self, event_args, *args, **kwargs):
+        # Observers shouldn't pass a next_handler
+        event_args.pop('next_handler', None)
+        next_handler = self._get_next_handler(event_args.get('event'))
+        # Notify listener. Ignore result from observable handler
+        self.callback(event_args, *args, **kwargs)
+        # Kick off the remaining handler chain
+        if next_handler:
+            return next_handler.invoke(event_args, *args, **kwargs)
+        else:
+            return None
+
+
+class ImplementingEventHandler(InterceptingEventHandler):
+
+    def __init__(self, event_pattern, priority, callback):
+        super(ImplementingEventHandler, self).__init__(event_pattern, priority,
+                                                       callback)
+
+    def invoke(self, event_args, *args, **kwargs):
+        result = self.callback(*args, **kwargs)
+        next_handler = self._get_next_handler(event_args.get('event'))
+        if next_handler:
+            event_args['next_handler'] = next_handler
+            event_args['result'] = result
+            next_handler.invoke(event_args, *args, **kwargs)
+            event_args.pop('result', None)
+            event_args.pop('next_handler', None)
+        return result
+
+
+class SimpleEventDispatcher(EventDispatcher):
+
+    def __init__(self):
+        # The dict key is event_pattern.
+        # The dict value is a list of handlers for the event pattern, sorted
+        # by event priority
+        self.__events = {}
+        self.__handler_cache = {}
+
+    def get_handlers_for_event(self, event):
+        handlers = self.__handler_cache.get(event)
+        if handlers is None:
+            self.__handler_cache[event] = self._create_handler_cache(
+                event)
+            return self.__handler_cache.get(event)
+        else:
+            return handlers
+
+    def _create_handler_cache(self, event):
+        cache_list = []
+        # Find all patterns matching event
+        for key in self.__events.keys():
+            if re.search(fnmatch.translate(key), event):
+                cache_list.extend(self.__events[key])
+        cache_list.sort(key=lambda h: h.priority)
+
+        # Make sure all priorities are unique
+        priority_list = [h.priority for h in cache_list]
+        if len(set(priority_list)) != len(priority_list):
+            guilty_prio = None
+            for prio in priority_list:
+                if prio == guilty_prio:
+                    break
+                guilty_prio = prio
+
+            # guilty_prio should never be none since we checked for
+            # duplicates before iterating
+            guilty_names = [h.callback.__name__ for h in cache_list
+                            if h.priority == guilty_prio]
+
+            message = "Event '{}' has multiple subscribed handlers " \
+                      "at priority '{}', with function names [{}]. " \
+                      "Each priority must only have a single " \
+                      "corresponding handler." \
+                .format(event, guilty_prio, ", ".join(guilty_names))
+            raise HandlerException(message)
+        return cache_list
+
+    def subscribe(self, event_handler):
+        event_handler.dispatcher = self
+        handler_list = self.__events.get(event_handler.event_pattern, [])
+        handler_list.append(event_handler)
+        self.__events[event_handler.event_pattern] = handler_list
+        # invalidate cache
+        self.__handler_cache = {}
+
+    def unsubscribe(self, event_handler):
+        handler_list = self.__events.get(event_handler.event_pattern, [])
+        handler_list.remove(event_handler)
+        event_handler.dispatcher = None
+        # invalidate cache
+        self.__handler_cache = {}
+
+    def observe(self, event_pattern, priority, callback):
+        handler = ObservingEventHandler(event_pattern, priority, callback)
+        self.subscribe(handler)
+        return handler
+
+    def intercept(self, event_pattern, priority, callback):
+        handler = InterceptingEventHandler(event_pattern, priority, callback)
+        self.subscribe(handler)
+        return handler
+
+    def implement(self, event_pattern, priority, callback):
+        handler = ImplementingEventHandler(event_pattern, priority, callback)
+        self.subscribe(handler)
+        return handler
+
+    def dispatch(self, sender, event, *args, **kwargs):
+        handlers = self.get_handlers_for_event(event)
+
+        if handlers:
+            # only kick off first handler in chain
+            event_args = {'event': event, 'sender': sender}
+            return handlers[0].invoke(event_args, *args, **kwargs)
+        else:
+            message = "Event '{}' has no subscribed handlers.".\
+                format(event)
+            log.warning(message)
+            return None

+ 177 - 0
cloudbridge/cloud/base/middleware.py

@@ -0,0 +1,177 @@
+import inspect
+import logging
+import sys
+
+import six
+
+from ..base.events import ImplementingEventHandler
+from ..base.events import InterceptingEventHandler
+from ..base.events import ObservingEventHandler
+from ..interfaces.events import EventHandler
+from ..interfaces.exceptions import CloudBridgeBaseException
+from ..interfaces.middleware import Middleware
+from ..interfaces.middleware import MiddlewareManager
+
+log = logging.getLogger(__name__)
+
+
+def intercept(event_pattern, priority):
+    def deco(f):
+        # Mark function as having an event_handler so we can discover it
+        # The callback cannot be set to f as it is not bound yet and will be
+        # set during auto discovery
+        f.__event_handler = InterceptingEventHandler(
+            event_pattern, priority, None)
+        return f
+    return deco
+
+
+def observe(event_pattern, priority):
+    def deco(f):
+        # Mark function as having an event_handler so we can discover it
+        # The callback cannot be set to f as it is not bound yet and will be
+        # set during auto discovery
+        f.__event_handler = ObservingEventHandler(
+            event_pattern, priority, None)
+        return f
+    return deco
+
+
+def implement(event_pattern, priority):
+    def deco(f):
+        # Mark function as having an event_handler so we can discover it
+        # The callback cannot be set to f as it is not bound yet and will be
+        # set during auto discovery
+        f.__event_handler = ImplementingEventHandler(
+            event_pattern, priority, None)
+        return f
+    return deco
+
+
+class SimpleMiddlewareManager(MiddlewareManager):
+
+    def __init__(self, event_manager):
+        self.events = event_manager
+        self.middleware_list = []
+
+    def add(self, middleware):
+        if isinstance(middleware, Middleware):
+            m = middleware
+        else:
+            m = AutoDiscoveredMiddleware(middleware)
+        m.install(self.events)
+        self.middleware_list.append(m)
+        return m
+
+    def remove(self, middleware):
+        middleware.uninstall()
+        self.middleware_list.remove(middleware)
+
+
+class BaseMiddleware(Middleware):
+
+    def __init__(self):
+        self.event_handlers = []
+        self.events = None
+
+    def install(self, event_manager):
+        self.events = event_manager
+        discovered_handlers = self.discover_handlers(self)
+        self.add_handlers(discovered_handlers)
+
+    def add_handlers(self, handlers):
+        if not hasattr(self, "event_handlers"):
+            # In case the user forgot to call super class init
+            self.event_handlers = []
+        for handler in handlers:
+            self.events.subscribe(handler)
+        self.event_handlers.extend(handlers)
+
+    def uninstall(self):
+        for handler in self.event_handlers:
+            handler.unsubscribe()
+        self.event_handlers = []
+        self.events = None
+
+    @staticmethod
+    def discover_handlers(class_or_obj):
+
+        # https://bugs.python.org/issue30533
+        # simulating a getmembers_static to be easily replaced with the
+        # function if they add it to inspect module
+        def getmembers_static(obj, predicate=None):
+            results = []
+            for key in dir(obj):
+                if not inspect.isdatadescriptor(getattr(obj.__class__,
+                                                        key,
+                                                        None)):
+                    try:
+                        value = getattr(obj, key)
+                    except AttributeError:
+                        continue
+                    if not predicate or predicate(value):
+                        results.append((key, value))
+            return results
+
+        discovered_handlers = []
+        for _, func in getmembers_static(class_or_obj, inspect.ismethod):
+            handler = getattr(func, "__event_handler", None)
+            if handler and isinstance(handler, EventHandler):
+                # Set the properly bound method as the callback
+                handler.callback = func
+                discovered_handlers.append(handler)
+        return discovered_handlers
+
+
+class AutoDiscoveredMiddleware(BaseMiddleware):
+
+    def __init__(self, class_or_obj):
+        super(AutoDiscoveredMiddleware, self).__init__()
+        self.obj_to_discover = class_or_obj
+
+    def install(self, event_manager):
+        super(AutoDiscoveredMiddleware, self).install(event_manager)
+        discovered_handlers = self.discover_handlers(self.obj_to_discover)
+        self.add_handlers(discovered_handlers)
+
+
+class EventDebugLoggingMiddleware(BaseMiddleware):
+    """
+    Logs all event parameters. This middleware should not be enabled other
+    than for debugging, as it could log sensitive parameters such as
+    access keys.
+    """
+    @observe(event_pattern="*", priority=100)
+    def pre_log_event(self, event_args, *args, **kwargs):
+        log.debug("Event: {0}, args: {1} kwargs: {2}".format(
+            event_args.get("event"), args, kwargs))
+
+    @observe(event_pattern="*", priority=4900)
+    def post_log_event(self, event_args, *args, **kwargs):
+        log.debug("Event: {0}, result: {1}".format(
+            event_args.get("event"), event_args.get("result")))
+
+
+class ExceptionWrappingMiddleware(BaseMiddleware):
+    """
+    Wraps all unhandled exceptions in cloudbridge exceptions.
+    """
+    @intercept(event_pattern="*", priority=1050)
+    def wrap_exception(self, event_args, *args, **kwargs):
+        next_handler = event_args.pop("next_handler")
+        if not next_handler:
+            return
+        try:
+            return next_handler.invoke(event_args, *args, **kwargs)
+        except Exception as e:
+            if isinstance(e, CloudBridgeBaseException):
+                raise
+            else:
+                ex_type, ex_value, traceback = sys.exc_info()
+                cb_ex = CloudBridgeBaseException(
+                    "CloudBridgeBaseException: {0} from exception type: {1}"
+                    .format(ex_value, ex_type))
+                if sys.version_info >= (3, 0):
+                    six.raise_from(cb_ex, e)
+                else:
+                    six.reraise(CloudBridgeBaseException, cb_ex, traceback)

+ 15 - 4
cloudbridge/cloud/base/provider.py

@@ -10,9 +10,11 @@ except ImportError:  # Python 2
 
 import six
 
-from cloudbridge.cloud.interfaces import CloudProvider
-from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
-from cloudbridge.cloud.interfaces.resources import Configuration
+from ..base.events import SimpleEventDispatcher
+from ..base.middleware import SimpleMiddlewareManager
+from ..interfaces import CloudProvider
+from ..interfaces.exceptions import ProviderConnectionException
+from ..interfaces.resources import Configuration
 
 log = logging.getLogger(__name__)
 
@@ -80,11 +82,12 @@ class BaseConfiguration(Configuration):
 
 
 class BaseCloudProvider(CloudProvider):
-
     def __init__(self, config):
         self._config = BaseConfiguration(config)
         self._config_parser = ConfigParser()
         self._config_parser.read(CloudBridgeConfigLocations)
+        self._events = SimpleEventDispatcher()
+        self._middleware = SimpleMiddlewareManager(self._events)
 
     @property
     def config(self):
@@ -94,6 +97,14 @@ class BaseCloudProvider(CloudProvider):
     def name(self):
         return str(self.__class__.__name__)
 
+    @property
+    def events(self):
+        return self._events
+
+    @property
+    def middleware(self):
+        return self._middleware
+
     def authenticate(self):
         """
         A basic implementation which simply runs a low impact command to

+ 22 - 0
cloudbridge/cloud/base/resources.py

@@ -752,6 +752,14 @@ class BaseBucket(BaseCloudResource, Bucket):
                 # check from most to least likely mutables
                 self.name == other.name)
 
+    def delete(self):
+        """
+        Delete this bucket.
+        """
+        self._provider.storage.buckets.delete(self.id)
+
+    # TODO: Discuss creating `create_object` method, or change docs
+
 
 class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):
 
@@ -763,6 +771,20 @@ class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):
     def _provider(self):
         return self.__provider
 
+    def get(self, name):
+        return self._provider.storage.bucket_objects.get(self.bucket, name)
+
+    def list(self, limit=None, marker=None, prefix=None):
+        return self._provider.storage.bucket_objects.list(self.bucket, limit,
+                                                          marker, prefix)
+
+    def find(self, **kwargs):
+        return self._provider.storage.bucket_objects.find(self.bucket,
+                                                          **kwargs)
+
+    def create(self, name):
+        return self._provider.storage.bucket_objects.create(self.bucket, name)
+
 
 class BaseGatewayContainer(GatewayContainer, BasePageableObjectMixin):
 

+ 164 - 0
cloudbridge/cloud/base/services.py

@@ -4,11 +4,16 @@ Base implementation for services available through a provider
 import logging
 
 import cloudbridge.cloud.base.helpers as cb_helpers
+from cloudbridge.cloud.base.middleware import implement
+from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.base.resources import BaseRouter
 from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.interfaces.exceptions import \
+    InvalidConfigurationException
 from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.resources import Router
+from cloudbridge.cloud.interfaces.services import BucketObjectService
 from cloudbridge.cloud.interfaces.services import BucketService
 from cloudbridge.cloud.interfaces.services import CloudService
 from cloudbridge.cloud.interfaces.services import ComputeService
@@ -35,13 +40,53 @@ log = logging.getLogger(__name__)
 
 class BaseCloudService(CloudService):
 
+    STANDARD_EVENT_PRIORITY = 2500
+
     def __init__(self, provider):
+        self._service_event_pattern = "provider"
         self._provider = provider
+        # discover and register all middleware
+        provider.middleware.add(self)
 
     @property
     def provider(self):
         return self._provider
 
+    def dispatch(self, sender, event, *args, **kwargs):
+        return self._provider.events.dispatch(sender, event, *args, **kwargs)
+
+    def _generate_event_pattern(self, func_name):
+        return ".".join((self._service_event_pattern, func_name))
+
+    def observe_function(self, func_name, priority, callback):
+        event_pattern = self._generate_event_pattern(func_name)
+        self.provider.events.observe(event_pattern, priority, callback)
+
+    def intercept_function(self, func_name, priority, callback):
+        event_pattern = self._generate_event_pattern(func_name)
+        self.provider.events.intercept(event_pattern, priority, callback)
+
+    def implement_function(self, func_name, priority, callback):
+        event_pattern = self._generate_event_pattern(func_name)
+        self.provider.events.implement(event_pattern, priority, callback)
+
+    def dispatch_function(self, sender, func_name, *args, **kwargs):
+        """
+        Emits the event corresponding to the given function name for the
+        current service
+
+        :type sender: CloudService
+        :param sender: The CloudBridge Service object sending the emit signal
+        :type func_name: str
+        :param func_name: The name of the function to be emitted. e.g.: 'get'
+        :type args: CloudService
+
+        :return:  The return value resulting from the handler chain invocations
+        """
+        full_event_name = self._generate_event_pattern(func_name)
+        return self._provider.events.dispatch(sender, full_event_name,
+                                              *args, **kwargs)
+
 
 class BaseSecurityService(SecurityService, BaseCloudService):
 
@@ -54,6 +99,7 @@ class BaseKeyPairService(
 
     def __init__(self, provider):
         super(BaseKeyPairService, self).__init__(provider)
+        self._service_event_pattern += ".security.key_pairs"
 
     def delete(self, key_pair_id):
         """
@@ -79,6 +125,7 @@ class BaseVMFirewallService(
 
     def __init__(self, provider):
         super(BaseVMFirewallService, self).__init__(provider)
+        self._service_event_pattern += ".security.vm_firewalls"
 
     def find(self, **kwargs):
         obj_list = self
@@ -106,6 +153,7 @@ class BaseVolumeService(
 
     def __init__(self, provider):
         super(BaseVolumeService, self).__init__(provider)
+        self._service_event_pattern += ".storage.volumes"
 
 
 class BaseSnapshotService(
@@ -113,6 +161,7 @@ class BaseSnapshotService(
 
     def __init__(self, provider):
         super(BaseSnapshotService, self).__init__(provider)
+        self._service_event_pattern += ".storage.snapshots"
 
 
 class BaseBucketService(
@@ -120,6 +169,114 @@ class BaseBucketService(
 
     def __init__(self, provider):
         super(BaseBucketService, self).__init__(provider)
+        self._service_event_pattern += ".storage.buckets"
+
+    # Generic find will be used for providers where we have not implemented
+    # provider-specific querying for find method
+    @implement(event_pattern="*.storage.buckets.find",
+               priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def _find(self, **kwargs):
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs,
+                                                           ", ".join(filters)))
+
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
+
+    def get(self, bucket_id):
+        """
+        Returns a bucket given its ID. Returns ``None`` if the bucket
+        does not exist.
+
+        :type bucket_id: str
+        :param bucket_id: The id of the desired bucket.
+
+        :rtype: ``Bucket``
+        :return:  ``None`` is returned if the bucket does not exist, and
+                  the bucket's provider-specific CloudBridge object is
+                  returned if the bucket is found.
+        """
+        return self.dispatch(self, "provider.storage.buckets.get", bucket_id)
+
+    def find(self, **kwargs):
+        """
+        Returns a list of buckets filtered by the given keyword arguments.
+        Accepted search arguments are: 'name'
+        """
+        return self.dispatch(self, "provider.storage.buckets.find", **kwargs)
+
+    def list(self, limit=None, marker=None):
+        """
+        List all buckets.
+        """
+        return self.dispatch(self, "provider.storage.buckets.list",
+                             limit=limit, marker=marker)
+
+    def create(self, name, location=None):
+        """
+        Create a new bucket.
+
+        :type name: str
+        :param name: The name of the bucket to be created. Note that names
+                     must be unique, and are unchangeable.
+
+        :rtype: ``Bucket``
+        :return:  The created bucket's provider-specific CloudBridge object.
+        """
+        BaseBucket.assert_valid_resource_name(name)
+        return self.dispatch(self, "provider.storage.buckets.create",
+                             name, location=location)
+
+    def delete(self, bucket_id):
+        """
+        Delete an existing bucket.
+
+        :type bucket_id: str
+        :param bucket_id: The ID of the bucket to be deleted.
+        """
+        return self.dispatch(self, "provider.storage.buckets.delete",
+                             bucket_id)
+
+
+class BaseBucketObjectService(
+        BasePageableObjectMixin, BucketObjectService, BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseBucketObjectService, self).__init__(provider)
+        self._service_event_pattern += ".storage.bucket_objects"
+        self._bucket = None
+
+    # Default bucket needs to be set in order for the service to be iterable
+    def set_bucket(self, bucket):
+        bucket = bucket if isinstance(bucket, BaseBucket) \
+                 else self.provider.storage.buckets.get(bucket)
+        self._bucket = bucket
+
+    def __iter__(self):
+        if not self._bucket:
+            message = "You must set a bucket before iterating through its " \
+                      "objects. We do not allow iterating through all " \
+                      "buckets at this time. In order to set a bucket, use: " \
+                      "`provider.storage.bucket_objects.set_bucket(my_bucket)`"
+            raise InvalidConfigurationException(message)
+        result_list = self.list(bucket=self._bucket)
+        if result_list.supports_server_paging:
+            for result in result_list:
+                yield result
+            while result_list.is_truncated:
+                result_list = self.list(bucket=self._bucket,
+                                        marker=result_list.marker)
+                for result in result_list:
+                    yield result
+        else:
+            for result in result_list.data:
+                yield result
 
 
 class BaseComputeService(ComputeService, BaseCloudService):
@@ -133,6 +290,7 @@ class BaseImageService(
 
     def __init__(self, provider):
         super(BaseImageService, self).__init__(provider)
+        self._service_event_pattern += ".compute.images"
 
 
 class BaseInstanceService(
@@ -140,6 +298,7 @@ class BaseInstanceService(
 
     def __init__(self, provider):
         super(BaseInstanceService, self).__init__(provider)
+        self._service_event_pattern += ".compute.instances"
 
 
 class BaseVMTypeService(
@@ -147,6 +306,7 @@ class BaseVMTypeService(
 
     def __init__(self, provider):
         super(BaseVMTypeService, self).__init__(provider)
+        self._service_event_pattern += ".compute.vm_types"
 
     def get(self, vm_type_id):
         vm_type = (t for t in self if t.id == vm_type_id)
@@ -164,6 +324,7 @@ class BaseRegionService(
 
     def __init__(self, provider):
         super(BaseRegionService, self).__init__(provider)
+        self._service_event_pattern += ".compute.regions"
 
     def find(self, **kwargs):
         obj_list = self
@@ -183,6 +344,7 @@ class BaseNetworkService(
 
     def __init__(self, provider):
         super(BaseNetworkService, self).__init__(provider)
+        self._service_event_pattern += ".networking.networks"
 
     @property
     def subnets(self):
@@ -213,6 +375,7 @@ class BaseSubnetService(
 
     def __init__(self, provider):
         super(BaseSubnetService, self).__init__(provider)
+        self._service_event_pattern += ".networking.subnets"
 
     def find(self, **kwargs):
         obj_list = self
@@ -238,6 +401,7 @@ class BaseRouterService(
 
     def __init__(self, provider):
         super(BaseRouterService, self).__init__(provider)
+        self._service_event_pattern += ".networking.routers"
 
     def delete(self, router):
         if isinstance(router, Router):

+ 235 - 0
cloudbridge/cloud/interfaces/events.py

@@ -0,0 +1,235 @@
+from abc import ABCMeta
+from abc import abstractmethod
+from abc import abstractproperty
+
+
+class EventDispatcher(object):
+
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def observe(self, event_pattern, priority, callback):
+        """
+        Register a callback to be invoked when a given event occurs. `observe`
+        will allow you to listen to events as they occur, but not modify the
+        event chain or its parameters. If you need to modify an event, use
+        the `intercept` method. `observe` is a simplified case of `intercept`,
+        and receives a simpler list of parameters in its callback.
+
+        :type event_pattern: str
+        :param event_pattern: The name or pattern of the event to which you are
+            subscribing the callback function. The pattern may contain glob
+            wildcard parameters to be notified on any matching event name.
+
+        :type priority: int
+        :param priority: The priority that this handler should be given.
+            When the event is emitted, all handlers will be run in order of
+            priority.
+
+        :type callback: function
+        :param callback: The callback function that should be invoked. The
+            callback must have a signature of the form:
+            `def callback(event_args, *args, **kwargs)`
+            Where the first positional argument is always event_args, which
+            is a dict value containing information about the event.
+            `event_args` includes the following keys:
+            'event': The name of the event
+            'sender': The object which raised the event
+            The event_args dict can also be used to convey additional info to
+            downstream event handlers.
+            The rest of the arguments to the callback can be any combination
+            of positional or keyword arguments as desired.
+
+        :rtype: :class:`.EventHandler`
+        :return:  An object of class EventHandler. The EventHandler will
+            already be subscribed to the dispatcher, and need not be manually
+            subscribed. The returned event handler can be used to unsubscribe
+            from future events when required.
+        """
+        pass
+
+    @abstractmethod
+    def intercept(self, event_pattern, priority, callback):
+        """
+        Register a callback to be invoked when a given event occurs. Intercept
+        will allow you to both observe events and modify the event chain and
+        its parameters. If you only want to observe an event, use the `observe`
+        method. Intercept and `observe` only differ in what parameters the
+        callback receives, with intercept receiving additional parameters to
+        allow controlling the event chain.
+
+        :type event_pattern: str
+        :param event_pattern: The name or pattern of the event to which you are
+            subscribing the callback function. The pattern may contain glob
+            wildcard parameters to be notified on any matching event name.
+
+        :type priority: int
+        :param priority: The priority that this handler should be given.
+            When the event is emitted, all handlers will be run in order of
+            priority.
+
+        :type callback: function
+        :param callback: The callback function that should be invoked. The
+            callback must have a signature of the form:
+            `def callback(event_args, *args, **kwargs)`
+            Where the first positional argument is always event_args, which
+            is a dict value containing information about the event.
+            `event_args` includes the following keys:
+            'event': The name of the event
+            'sender': The object which raised the event
+            'event_handler': The next event handler in the chain
+                             (only for intercepting handlers)
+            The event_args dict can also be used to convey additional info to
+            downstream event handlers.
+            The rest of the arguments to the callback can be any combination
+            of positional or keyword arguments as desired.
+
+        :rtype: :class:`.EventHandler`
+        :return:  An object of class EventHandler. The EventHandler will
+            already be subscribed to the dispatcher, and need not be manually
+            subscribed. The returned event handler can be used to unsubscribe
+            from future events when required.
+        """
+        pass
+
+    @abstractmethod
+    def dispatch(self, sender, event, *args, **kwargs):
+        """
+        Raises an event, which will trigger all handlers that are registered
+        for this event. The return value of the emit function is the return
+        value of the highest priority handler (i.e. the first handler in the
+        event chain). The first event handlers is responsible for calling the
+        next handler in the event chain, and so on, propagating arguments
+        and return values as desired.
+
+        :type event: str
+        :param event: The name of the event which is being raised.
+
+        :type sender: object
+        :param sender: The object which is raising the event
+
+        All additional positional and keyword arguments are passed through
+        to the callback functions for the event as is. Refer to the c
+        """
+        pass
+
+    @abstractmethod
+    def subscribe(self, event_handler):
+        """
+        Register an event handler with this dispatcher. The observe and
+        intercept methods will construct an event handler and subscribe it for
+        you automatically, and therefore, there is usually no need to invoke
+        subscribe directly unless you have a special type of event handler.
+
+        :type event_handler: :class:`.EventHandler`
+        :param event_handler: An object of class EventHandler.
+        """
+        pass
+
+    @abstractmethod
+    def unsubscribe(self, event_handler):
+        """
+        Unregister an event handler from this dispatcher. The event handler
+        will no longer be notified on events.
+
+        :type event_handler: :class:`.EventHandler`
+        :param event_handler: An object of class EventHandler.
+        """
+        pass
+
+    @abstractmethod
+    def get_handlers_for_event(self, event):
+        """
+        Returns a list of all registered handlers for a given event, sorted
+        in order of priority.
+
+        :type event: str
+        :param event: The name of the event
+        """
+        pass
+
+
+class EventHandler(object):
+
+    __metaclass__ = ABCMeta
+
+    @abstractproperty
+    def event_pattern(self):
+        """
+        The event pattern that this handler is listening to. May include glob
+        patterns, in which case, any matching event name will trigger this
+        handler.
+        e.g.
+            provider.storage.*
+            provider.storage.volumes.list
+        """
+        pass
+
+    @abstractproperty
+    def priority(self):
+        """
+        The priority of this handler. When a matching event occurs, handlers
+        are invoked in order of priority.
+        The priorities ranges from 0-1000 and 2000-3000 and >4000 are reserved
+        for use by cloudbridge.
+        Users should listen on priorities between 1000-2000 for pre handlers
+        and 2000-3000 for post handlers.
+        e.g.
+            provider.storage.*
+            provider.storage.volumes.list
+        """
+        pass
+
+    @abstractproperty
+    def callback(self):
+        """
+        The callback that will be triggered when this event handler is invoked.
+        The callback signature must accept *args and **kwargs and pass them
+        through.
+        The callback must have a signature of the form:
+        `def callback(event_args, *args, **kwargs)`
+        where the first positional argument is always event_args, which
+        is a dict value containing information about the event.
+        `event_args` includes the following keys:
+        'event': The name of the event
+        'sender': The object which raised the event
+        'event_handler': The next event handler in the chain
+                         (only for intercepting handlers)
+        The event_args dict can also be used to convey additional info to
+        downstream event handlers.
+        The rest of the arguments to the callback can be any combination
+        of positional or keyword arguments as desired.
+        """
+        pass
+
+    @abstractmethod
+    def invoke(self, event_args, *args, **kwargs):
+        """
+        Executes this event handler's callback
+
+        :type event_args: dict
+        :param event_args: The first positional argument is always event_args,
+           which is a dict value containing information about the event.
+           `event_args` includes the following keys:
+           'event': The name of the event
+           'sender': The object which raised the event
+           'event_handler': The next event handler in the chain
+                            (only for intercepting handlers)
+           The event_args dict can also be used to convey additional info to
+           downstream event handlers.
+        """
+        pass
+
+    @abstractmethod
+    def unsubscribe(self):
+        """
+        Unsubscribes from currently subscribed events.
+        """
+        pass
+
+    @abstractproperty
+    def dispatcher(self):
+        """
+        Get or sets the dispatcher currently associated with this event handler
+        """
+        pass

+ 7 - 0
cloudbridge/cloud/interfaces/exceptions.py

@@ -92,3 +92,10 @@ class DuplicateResourceException(CloudBridgeBaseException):
     result in a DuplicateResourceException.
     """
     pass
+
+
+class HandlerException(CloudBridgeBaseException):
+    """
+    Marker interface for event handler exceptions.
+    """
+    pass

+ 62 - 0
cloudbridge/cloud/interfaces/middleware.py

@@ -0,0 +1,62 @@
+from abc import ABCMeta
+from abc import abstractmethod
+
+
+class Middleware(object):
+    """
+    Provides a mechanism for grouping related event handlers together, to
+    provide logically cohesive middleware. The middleware class allows event
+    handlers to subscribe to events through the install method, and unsubscribe
+    through the uninstall method. This allows event handlers to be added and
+    removed as a group. The event handler implementations will also typically
+    live inside the middleware class. For example, LoggingMiddleware may
+    register multiple event handlers to log data before and after calls.
+    ResourceTrackingMiddleware may track all objects that are created or
+    deleted.
+    """
+
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def install(self, provider):
+        """
+        Use this method to subscribe all event handlers that are part of this
+        middleware. The install method will be called when the middleware is
+        first added to a MiddleWareManager.
+
+        :type provider: :class:`.Provider`
+        :param provider: The provider that this middleware belongs to
+        """
+        pass
+
+    @abstractmethod
+    def uninstall(self, provider):
+        """
+        Use this method to unsubscribe all event handlers for this middleware.
+        """
+        pass
+
+
+class MiddlewareManager(object):
+    """
+    Provides a mechanism for tracking a list of installed middleware
+    """
+
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def add(self, middleware):
+        """
+        Use this method to add middleware to this middleware manager.
+
+        :type middleware: :class:`.Middleware`
+        :param middleware: The middleware implementation
+        """
+        pass
+
+    @abstractmethod
+    def remove(self, middleware):
+        """
+        Use this method to remove this middleware from the middleware manager.
+        """
+        pass

+ 3 - 1
cloudbridge/cloud/interfaces/provider.py

@@ -1,7 +1,9 @@
 """
 Specification for a provider interface
 """
-from abc import ABCMeta, abstractmethod, abstractproperty
+from abc import ABCMeta
+from abc import abstractmethod
+from abc import abstractproperty
 
 
 class CloudProvider(object):

+ 3 - 1
cloudbridge/cloud/interfaces/resources.py

@@ -1,7 +1,9 @@
 """
 Specifications for data objects exposed through a ``provider`` or ``service``.
 """
-from abc import ABCMeta, abstractmethod, abstractproperty
+from abc import ABCMeta
+from abc import abstractmethod
+from abc import abstractproperty
 from enum import Enum
 
 

+ 103 - 1
cloudbridge/cloud/interfaces/services.py

@@ -1,7 +1,9 @@
 """
 Specifications for services available through a provider
 """
-from abc import ABCMeta, abstractmethod, abstractproperty
+from abc import ABCMeta
+from abc import abstractmethod
+from abc import abstractproperty
 
 from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
 
@@ -945,6 +947,106 @@ class BucketService(PageableObjectMixin, CloudService):
         pass
 
 
+class BucketObjectService(PageableObjectMixin, CloudService):
+
+    """
+    The Bucket Object Service interface provides access to the underlying
+    object storage capabilities of this provider. This service is optional and
+    the :func:`CloudProvider.has_service()` method should be used to verify its
+    availability before using the service.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, bucket, object_id):
+        """
+        Returns a bucket object given its ID and the ID of bucket containing
+        it. Returns ``None`` if the bucket object or bucket does not exist.
+        On some providers, such as AWS and OpenStack, the bucket id is the
+        same as its name.
+
+        Example:
+
+        .. code-block:: python
+
+            bucket = provider.storage.buckets.get('my_bucket_id')
+            buck_obj = provider.storage.bucket_objects.get('my_object_id',
+                                                           bucket)
+            print(buck_obj.id, buck_obj.name)
+
+        :rtype: :class:`.BucketObject`
+        :return:  a BucketObject instance
+        """
+        pass
+
+    @abstractmethod
+    def find(self, bucket, **kwargs):
+        """
+        Searches for a bucket object in a bucket by a given list of attributes.
+
+        Supported attributes: name
+
+        Example:
+
+        .. code-block:: python
+
+            bucket = provider.storage.buckets.get('my_bucket_id')
+            objs = provider.storage.bucket_objects.find(bucket,
+                                                        name='my_obj_name')
+            for buck_obj in objs:
+                print(buck_obj.id, buck_obj.name)
+
+        :rtype: :class:`.BucketObject`
+        :return:  a BucketObject instance
+        """
+        pass
+
+    @abstractmethod
+    def list(self, bucket, limit=None, marker=None):
+        """
+        List all bucket objects within a bucket.
+
+        Example:
+
+        .. code-block:: python
+
+            bucket = provider.storage.buckets.get('my_bucket_id')
+            objs = provider.storage.bucket_objects.list(bucket)
+            for buck_obj in objs:
+                print(buck_obj.id, buck_obj.name)
+
+        :rtype: :class:`.BucketObject`
+        :return:  a BucketObject instance
+        """
+        pass
+
+    @abstractmethod
+    def create(self, bucket, object_name):
+        """
+        Create a new bucket object within a bucket.
+
+        Example:
+
+        .. code-block:: python
+
+            bucket = provider.storage.buckets.get('my_bucket_id')
+            buck_obj = provider.storage.bucket_objects.create('my_name',
+                                                              bucket)
+            print(buck_obj.name)
+
+
+        :type object_name: str
+        :param object_name: The name of this bucket.
+
+        :type bucket: str
+        :param bucket: A bucket object.
+
+        :return:  a BucketObject instance
+        :rtype: ``object`` of :class:`.BucketObject`
+        """
+        pass
+
+
 class SecurityService(CloudService):
 
     """

+ 0 - 37
cloudbridge/cloud/providers/aws/resources.py

@@ -7,7 +7,6 @@ import logging
 
 from botocore.exceptions import ClientError
 
-import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseBucketContainer
@@ -889,48 +888,12 @@ class AWSBucket(BaseBucket):
     def objects(self):
         return self._object_container
 
-    def delete(self, delete_contents=False):
-        self._bucket.delete()
-
 
 class AWSBucketContainer(BaseBucketContainer):
 
     def __init__(self, provider, bucket):
         super(AWSBucketContainer, self).__init__(provider, bucket)
 
-    def get(self, name):
-        try:
-            # pylint:disable=protected-access
-            obj = self.bucket._bucket.Object(name)
-            # load() throws an error if object does not exist
-            obj.load()
-            return AWSBucketObject(self._provider, obj)
-        except ClientError:
-            return None
-
-    def list(self, limit=None, marker=None, prefix=None):
-        if prefix:
-            # pylint:disable=protected-access
-            boto_objs = self.bucket._bucket.objects.filter(Prefix=prefix)
-        else:
-            # pylint:disable=protected-access
-            boto_objs = self.bucket._bucket.objects.all()
-        objects = [AWSBucketObject(self._provider, obj) for obj in boto_objs]
-        return ClientPagedResultList(self._provider, objects,
-                                     limit=limit, marker=marker)
-
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches),
-                                     limit=None, marker=None)
-
-    def create(self, name):
-        # pylint:disable=protected-access
-        obj = self.bucket._bucket.Object(name)
-        return AWSBucketObject(self._provider, obj)
-
 
 class AWSRegion(BaseRegion):
 

+ 70 - 18
cloudbridge/cloud/providers/aws/services.py

@@ -10,7 +10,9 @@ import cachetools
 import requests
 
 import cloudbridge.cloud.base.helpers as cb_helpers
+from cloudbridge.cloud.base.middleware import implement
 from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.services import BaseBucketObjectService
 from cloudbridge.cloud.base.services import BaseBucketService
 from cloudbridge.cloud.base.services import BaseComputeService
 from cloudbridge.cloud.base.services import BaseImageService
@@ -27,8 +29,9 @@ from cloudbridge.cloud.base.services import BaseSubnetService
 from cloudbridge.cloud.base.services import BaseVMFirewallService
 from cloudbridge.cloud.base.services import BaseVMTypeService
 from cloudbridge.cloud.base.services import BaseVolumeService
-from cloudbridge.cloud.interfaces.exceptions \
-    import DuplicateResourceException, InvalidConfigurationException
+from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
+from cloudbridge.cloud.interfaces.exceptions import \
+    InvalidConfigurationException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 from cloudbridge.cloud.interfaces.resources import Network
@@ -41,6 +44,7 @@ from cloudbridge.cloud.interfaces.resources import Volume
 from .helpers import BotoEC2Service
 from .helpers import BotoS3Service
 from .resources import AWSBucket
+from .resources import AWSBucketObject
 from .resources import AWSInstance
 from .resources import AWSKeyPair
 from .resources import AWSLaunchConfig
@@ -177,6 +181,7 @@ class AWSStorageService(BaseStorageService):
         self._volume_svc = AWSVolumeService(self.provider)
         self._snapshot_svc = AWSSnapshotService(self.provider)
         self._bucket_svc = AWSBucketService(self.provider)
+        self._bucket_obj_svc = AWSBucketObjectService(self.provider)
 
     @property
     def volumes(self):
@@ -190,6 +195,10 @@ class AWSStorageService(BaseStorageService):
     def buckets(self):
         return self._bucket_svc
 
+    @property
+    def bucket_objects(self):
+        return self._bucket_obj_svc
+
 
 class AWSVolumeService(BaseVolumeService):
 
@@ -300,12 +309,13 @@ class AWSBucketService(BaseBucketService):
                                  cb_resource=AWSBucket,
                                  boto_collection_name='buckets')
 
-    def get(self, bucket_id):
+    @implement(event_pattern="provider.storage.buckets.get",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _get(self, bucket_id):
         """
         Returns a bucket given its ID. Returns ``None`` if the bucket
         does not exist.
         """
-        log.debug("Getting AWS Bucket Service with the id: %s", bucket_id)
         try:
             # Make a call to make sure the bucket exists. There's an edge case
             # where a 403 response can occur when the bucket exists but the
@@ -329,21 +339,16 @@ class AWSBucketService(BaseBucketService):
         # For all other responses, it's assumed that the bucket does not exist.
         return None
 
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches),
-                                     limit=None, marker=None)
-
-    def list(self, limit=None, marker=None):
+    @implement(event_pattern="provider.storage.buckets.list",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _list(self, limit, marker):
         return self.svc.list(limit=limit, marker=marker)
 
-    def create(self, name, location=None):
-        log.debug("Creating AWS Bucket with the params "
-                  "[name: %s, location: %s]", name, location)
+    @implement(event_pattern="provider.storage.buckets.create",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _create(self, name, location):
         AWSBucket.assert_valid_resource_name(name)
-        loc_constraint = location or self.provider.region_name
+        location = location or self.provider.region_name
         # Due to an API issue in S3, specifying us-east-1 as a
         # LocationConstraint results in an InvalidLocationConstraint.
         # Therefore, it must be special-cased and omitted altogether.
@@ -351,7 +356,7 @@ class AWSBucketService(BaseBucketService):
         # In addition, us-east-1 also behaves differently when it comes
         # to raising duplicate resource exceptions, so perform a manual
         # check
-        if loc_constraint == 'us-east-1':
+        if location == 'us-east-1':
             try:
                 # check whether bucket already exists
                 self.provider.s3_conn.meta.client.head_bucket(Bucket=name)
@@ -365,7 +370,7 @@ class AWSBucketService(BaseBucketService):
             try:
                 return self.svc.create('create_bucket', Bucket=name,
                                        CreateBucketConfiguration={
-                                           'LocationConstraint': loc_constraint
+                                           'LocationConstraint': location
                                         })
             except ClientError as e:
                 if e.response['Error']['Code'] == "BucketAlreadyOwnedByYou":
@@ -374,6 +379,53 @@ class AWSBucketService(BaseBucketService):
                 else:
                     raise
 
+    @implement(event_pattern="provider.storage.buckets.delete",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _delete(self, bucket_id):
+        bucket = self._get(bucket_id)
+        if bucket:
+            bucket._bucket.delete()
+
+
+class AWSBucketObjectService(BaseBucketObjectService):
+
+    def __init__(self, provider):
+        super(AWSBucketObjectService, self).__init__(provider)
+
+    def get(self, bucket, object_id):
+        try:
+            # pylint:disable=protected-access
+            obj = bucket._bucket.Object(object_id)
+            # load() throws an error if object does not exist
+            obj.load()
+            return AWSBucketObject(self.provider, obj)
+        except ClientError:
+            return None
+
+    def list(self, bucket, limit=None, marker=None, prefix=None):
+        if prefix:
+            # pylint:disable=protected-access
+            boto_objs = bucket._bucket.objects.filter(Prefix=prefix)
+        else:
+            # pylint:disable=protected-access
+            boto_objs = bucket._bucket.objects.all()
+        objects = [AWSBucketObject(self.provider, obj) for obj in boto_objs]
+        return ClientPagedResultList(self.provider, objects,
+                                     limit=limit, marker=marker)
+
+    def find(self, bucket, **kwargs):
+        obj_list = [AWSBucketObject(self.provider, o)
+                    for o in bucket._bucket.objects.all()]
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self.provider, list(matches),
+                                     limit=None, marker=None)
+
+    def create(self, bucket, name):
+        # pylint:disable=protected-access
+        obj = bucket._bucket.Object(name)
+        return AWSBucketObject(self.provider, obj)
+
 
 class AWSComputeService(BaseComputeService):
 

+ 4 - 3
cloudbridge/cloud/providers/azure/azure_client.py

@@ -19,9 +19,10 @@ from msrestazure.azure_exceptions import CloudError
 
 import tenacity
 
-from cloudbridge.cloud.interfaces.exceptions import \
-    DuplicateResourceException, InvalidLabelException, \
-    ProviderConnectionException, WaitStateException
+from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
+from cloudbridge.cloud.interfaces.exceptions import InvalidLabelException
+from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
+from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 
 from . import helpers as azure_helpers
 

+ 4 - 3
cloudbridge/cloud/providers/azure/provider.py

@@ -12,9 +12,10 @@ from cloudbridge.cloud.base import BaseCloudProvider
 from cloudbridge.cloud.base.helpers import get_env
 from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 from cloudbridge.cloud.providers.azure.azure_client import AzureClient
-from cloudbridge.cloud.providers.azure.services \
-    import AzureComputeService, AzureNetworkingService, \
-    AzureSecurityService, AzureStorageService
+from cloudbridge.cloud.providers.azure.services import AzureComputeService
+from cloudbridge.cloud.providers.azure.services import AzureNetworkingService
+from cloudbridge.cloud.providers.azure.services import AzureSecurityService
+from cloudbridge.cloud.providers.azure.services import AzureStorageService
 
 log = logging.getLogger(__name__)
 

+ 33 - 55
cloudbridge/cloud/providers/azure/resources.py

@@ -13,18 +13,39 @@ from msrestazure.azure_exceptions import CloudError
 
 import pysftp
 
-import cloudbridge.cloud.base.helpers as cb_helpers
-from cloudbridge.cloud.base.resources import BaseAttachmentInfo, \
-    BaseBucket, BaseBucketContainer, BaseBucketObject, BaseFloatingIP, \
-    BaseFloatingIPContainer, BaseGatewayContainer, BaseInstance, \
-    BaseInternetGateway, BaseKeyPair, BaseLaunchConfig, \
-    BaseMachineImage, BaseNetwork, BasePlacementZone, BaseRegion, BaseRouter, \
-    BaseSnapshot, BaseSubnet, BaseVMFirewall, BaseVMFirewallRule, \
-    BaseVMFirewallRuleContainer, BaseVMType, BaseVolume, ClientPagedResultList
-from cloudbridge.cloud.interfaces import InstanceState, VolumeState
-from cloudbridge.cloud.interfaces.resources import Instance, \
-    MachineImageState, NetworkState, RouterState, \
-    SnapshotState, SubnetState, TrafficDirection
+from cloudbridge.cloud.base.resources import BaseAttachmentInfo
+from cloudbridge.cloud.base.resources import BaseBucket
+from cloudbridge.cloud.base.resources import BaseBucketContainer
+from cloudbridge.cloud.base.resources import BaseBucketObject
+from cloudbridge.cloud.base.resources import BaseFloatingIP
+from cloudbridge.cloud.base.resources import BaseFloatingIPContainer
+from cloudbridge.cloud.base.resources import BaseGatewayContainer
+from cloudbridge.cloud.base.resources import BaseInstance
+from cloudbridge.cloud.base.resources import BaseInternetGateway
+from cloudbridge.cloud.base.resources import BaseKeyPair
+from cloudbridge.cloud.base.resources import BaseLaunchConfig
+from cloudbridge.cloud.base.resources import BaseMachineImage
+from cloudbridge.cloud.base.resources import BaseNetwork
+from cloudbridge.cloud.base.resources import BasePlacementZone
+from cloudbridge.cloud.base.resources import BaseRegion
+from cloudbridge.cloud.base.resources import BaseRouter
+from cloudbridge.cloud.base.resources import BaseSnapshot
+from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.base.resources import BaseVMFirewall
+from cloudbridge.cloud.base.resources import BaseVMFirewallRule
+from cloudbridge.cloud.base.resources import BaseVMFirewallRuleContainer
+from cloudbridge.cloud.base.resources import BaseVMType
+from cloudbridge.cloud.base.resources import BaseVolume
+from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.interfaces import InstanceState
+from cloudbridge.cloud.interfaces import VolumeState
+from cloudbridge.cloud.interfaces.resources import Instance
+from cloudbridge.cloud.interfaces.resources import MachineImageState
+from cloudbridge.cloud.interfaces.resources import NetworkState
+from cloudbridge.cloud.interfaces.resources import RouterState
+from cloudbridge.cloud.interfaces.resources import SnapshotState
+from cloudbridge.cloud.interfaces.resources import SubnetState
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
 
 from . import helpers as azure_helpers
 
@@ -337,12 +358,6 @@ class AzureBucket(BaseBucket):
         """
         return self._bucket.name
 
-    def delete(self, delete_contents=True):
-        """
-        Delete this bucket.
-        """
-        self._provider.azure_client.delete_container(self.name)
-
     def exists(self, name):
         """
         Determine if an object with given name exists in this bucket.
@@ -359,43 +374,6 @@ class AzureBucketContainer(BaseBucketContainer):
     def __init__(self, provider, bucket):
         super(AzureBucketContainer, self).__init__(provider, bucket)
 
-    def get(self, key):
-        """
-        Retrieve a given object from this bucket.
-        """
-        try:
-            obj = self._provider.azure_client.get_blob(self.bucket.name,
-                                                       key)
-            return AzureBucketObject(self._provider, self.bucket, obj)
-        except AzureException as azureEx:
-            log.exception(azureEx)
-            return None
-
-    def list(self, limit=None, marker=None, prefix=None):
-        """
-        List all objects within this bucket.
-
-        :rtype: BucketObject
-        :return: List of all available BucketObjects within this bucket.
-        """
-        objects = [AzureBucketObject(self._provider, self.bucket, obj)
-                   for obj in
-                   self._provider.azure_client.list_blobs(
-                       self.bucket.name, prefix=prefix)]
-        return ClientPagedResultList(self._provider, objects,
-                                     limit=limit, marker=marker)
-
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
-
-    def create(self, name):
-        self._provider.azure_client.create_blob_from_text(
-            self.bucket.name, name, '')
-        return self.get(name)
-
 
 class AzureVolume(BaseVolume):
     VOLUME_STATE_MAP = {

+ 115 - 47
cloudbridge/cloud/providers/azure/services.py

@@ -8,25 +8,51 @@ from azure.mgmt.compute.models import DiskCreateOption
 from msrestazure.azure_exceptions import CloudError
 
 import cloudbridge.cloud.base.helpers as cb_helpers
-from cloudbridge.cloud.base.resources import ClientPagedResultList, \
-    ServerPagedResultList
-from cloudbridge.cloud.base.services import BaseBucketService, \
-    BaseComputeService, \
-    BaseImageService, BaseInstanceService, BaseKeyPairService, \
-    BaseNetworkService, BaseNetworkingService, BaseRegionService, \
-    BaseRouterService, BaseSecurityService, BaseSnapshotService, \
-    BaseStorageService, BaseSubnetService, BaseVMFirewallService, \
-    BaseVMTypeService, BaseVolumeService
-from cloudbridge.cloud.interfaces.exceptions import \
-    DuplicateResourceException, InvalidValueException
-from cloudbridge.cloud.interfaces.resources import MachineImage, \
-    Network, PlacementZone, Snapshot, Subnet, VMFirewall, VMType, Volume
-
-from .resources import AzureBucket, \
-    AzureInstance, AzureKeyPair, \
-    AzureLaunchConfig, AzureMachineImage, AzureNetwork, \
-    AzureRegion, AzureRouter, AzureSnapshot, AzureSubnet, \
-    AzureVMFirewall, AzureVMType, AzureVolume
+from cloudbridge.cloud.base.middleware import implement
+from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.resources import ServerPagedResultList
+from cloudbridge.cloud.base.services import BaseBucketObjectService
+from cloudbridge.cloud.base.services import BaseBucketService
+from cloudbridge.cloud.base.services import BaseComputeService
+from cloudbridge.cloud.base.services import BaseImageService
+from cloudbridge.cloud.base.services import BaseInstanceService
+from cloudbridge.cloud.base.services import BaseKeyPairService
+from cloudbridge.cloud.base.services import BaseNetworkService
+from cloudbridge.cloud.base.services import BaseNetworkingService
+from cloudbridge.cloud.base.services import BaseRegionService
+from cloudbridge.cloud.base.services import BaseRouterService
+from cloudbridge.cloud.base.services import BaseSecurityService
+from cloudbridge.cloud.base.services import BaseSnapshotService
+from cloudbridge.cloud.base.services import BaseStorageService
+from cloudbridge.cloud.base.services import BaseSubnetService
+from cloudbridge.cloud.base.services import BaseVMFirewallService
+from cloudbridge.cloud.base.services import BaseVMTypeService
+from cloudbridge.cloud.base.services import BaseVolumeService
+from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
+from cloudbridge.cloud.interfaces.exceptions import InvalidValueException
+from cloudbridge.cloud.interfaces.resources import MachineImage
+from cloudbridge.cloud.interfaces.resources import Network
+from cloudbridge.cloud.interfaces.resources import PlacementZone
+from cloudbridge.cloud.interfaces.resources import Snapshot
+from cloudbridge.cloud.interfaces.resources import Subnet
+from cloudbridge.cloud.interfaces.resources import VMFirewall
+from cloudbridge.cloud.interfaces.resources import VMType
+from cloudbridge.cloud.interfaces.resources import Volume
+
+from .resources import AzureBucket
+from .resources import AzureBucketObject
+from .resources import AzureInstance
+from .resources import AzureKeyPair
+from .resources import AzureLaunchConfig
+from .resources import AzureMachineImage
+from .resources import AzureNetwork
+from .resources import AzureRegion
+from .resources import AzureRouter
+from .resources import AzureSnapshot
+from .resources import AzureSubnet
+from .resources import AzureVMFirewall
+from .resources import AzureVMType
+from .resources import AzureVolume
 
 log = logging.getLogger(__name__)
 
@@ -191,6 +217,7 @@ class AzureStorageService(BaseStorageService):
         self._volume_svc = AzureVolumeService(self.provider)
         self._snapshot_svc = AzureSnapshotService(self.provider)
         self._bucket_svc = AzureBucketService(self.provider)
+        self._bucket_obj_svc = AzureBucketObjectService(self.provider)
 
     @property
     def volumes(self):
@@ -204,6 +231,10 @@ class AzureStorageService(BaseStorageService):
     def buckets(self):
         return self._bucket_svc
 
+    @property
+    def bucket_objects(self):
+        return self._bucket_obj_svc
+
 
 class AzureVolumeService(BaseVolumeService):
     def __init__(self, provider):
@@ -365,7 +396,9 @@ class AzureBucketService(BaseBucketService):
     def __init__(self, provider):
         super(AzureBucketService, self).__init__(provider)
 
-    def get(self, bucket_id):
+    @implement(event_pattern="provider.storage.buckets.get",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _get(self, bucket_id):
         """
         Returns a bucket given its ID. Returns ``None`` if the bucket
         does not exist.
@@ -377,41 +410,76 @@ class AzureBucketService(BaseBucketService):
             log.exception(error)
             return None
 
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+    @implement(event_pattern="provider.storage.buckets.list",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _list(self, limit, marker):
+        buckets = [AzureBucket(self.provider, bucket)
+                   for bucket
+                   in self.provider.azure_client.list_containers()[0]]
+        return ClientPagedResultList(self.provider, buckets,
+                                     limit=limit, marker=marker)
 
-        # All kwargs should have been popped at this time.
-        if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs,
-                                                           ", ".join(filters)))
+    @implement(event_pattern="provider.storage.buckets.create",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _create(self, name, location=None):
+        """
+        Create a new bucket.
+        """
+        AzureBucket.assert_valid_resource_name(name)
+        bucket = self.provider.azure_client.create_container(name)
+        return AzureBucket(self.provider, bucket)
 
-        return ClientPagedResultList(self.provider,
-                                     matches if matches else [])
+    @implement(event_pattern="provider.storage.buckets.delete",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _delete(self, bucket_id):
+        """
+        Delete this bucket.
+        """
+        self.provider.azure_client.delete_container(bucket_id)
 
-    def list(self, limit=None, marker=None):
+
+class AzureBucketObjectService(BaseBucketObjectService):
+    def __init__(self, provider):
+        super(AzureBucketObjectService, self).__init__(provider)
+
+    def get(self, bucket, object_id):
         """
-        List all containers.
+        Retrieve a given object from this bucket.
         """
-        buckets, resume_marker = self.provider.azure_client.list_containers(
-            marker=marker,
-            limit=limit or self.provider.config.default_result_limit)
-        results = [AzureBucket(self.provider, bucket)
-                   for bucket in buckets]
-        return ServerPagedResultList(is_truncated=resume_marker,
-                                     marker=resume_marker,
-                                     supports_total=False,
-                                     data=results)
+        try:
+            obj = self.provider.azure_client.get_blob(bucket.name,
+                                                      object_id)
+            return AzureBucketObject(self.provider, bucket, obj)
+        except AzureException as azureEx:
+            log.exception(azureEx)
+            return None
 
-    def create(self, name, location=None):
+    def list(self, bucket, limit=None, marker=None, prefix=None):
         """
-        Create a new bucket.
+        List all objects within this bucket.
+
+        :rtype: BucketObject
+        :return: List of all available BucketObjects within this bucket.
         """
-        AzureBucket.assert_valid_resource_name(name)
-        bucket = self.provider.azure_client.create_container(name)
-        return AzureBucket(self.provider, bucket)
+        objects = [AzureBucketObject(self.provider, bucket, obj)
+                   for obj in
+                   self.provider.azure_client.list_blobs(
+                       bucket.name, prefix=prefix)]
+        return ClientPagedResultList(self.provider, objects,
+                                     limit=limit, marker=marker)
+
+    def find(self, bucket, **kwargs):
+        obj_list = [AzureBucketObject(self.provider, bucket, obj)
+                    for obj in
+                    self.provider.azure_client.list_blobs(bucket.name)]
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self.provider, list(matches))
+
+    def create(self, bucket, name):
+        self.provider.azure_client.create_blob_from_text(
+            bucket.name, name, '')
+        return self.get(bucket, name)
 
 
 class AzureComputeService(BaseComputeService):

+ 10 - 80
cloudbridge/cloud/providers/gce/resources.py

@@ -16,7 +16,6 @@ from collections import namedtuple
 import googleapiclient
 
 import cloudbridge as cb
-import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseBucketContainer
@@ -2150,8 +2149,11 @@ class GCSObject(BaseBucketObject):
             data = data.encode()
         media_body = googleapiclient.http.MediaIoBaseUpload(
                 io.BytesIO(data), mimetype='plain/text')
-        response = self._bucket.create_object_with_media_body(self.name,
-                                                              media_body)
+        response = (self._provider
+                        .storage.bucket_objects
+                        ._create_object_with_media_body(self._bucket,
+                                                        self.name,
+                                                        media_body))
         if response:
             self._obj = response
 
@@ -2162,8 +2164,11 @@ class GCSObject(BaseBucketObject):
         with open(path, 'rb') as f:
             media_body = googleapiclient.http.MediaIoBaseUpload(
                     f, 'application/octet-stream')
-            response = self._bucket.create_object_with_media_body(self.name,
-                                                                  media_body)
+            response = (self._provider
+                        .storage.bucket_objects
+                        ._create_object_with_media_body(self._bucket,
+                                                        self.name,
+                                                        media_body))
             if response:
                 self._obj = response
 
@@ -2201,47 +2206,6 @@ class GCSBucketContainer(BaseBucketContainer):
     def __init__(self, provider, bucket):
         super(GCSBucketContainer, self).__init__(provider, bucket)
 
-    def get(self, name):
-        """
-        Retrieve a given object from this bucket.
-        """
-        obj = self._provider.get_resource('objects', name,
-                                          bucket=self.bucket.name)
-        return GCSObject(self._provider, self.bucket, obj) if obj else None
-
-    def list(self, limit=None, marker=None, prefix=None):
-        """
-        List all objects within this bucket.
-        """
-        max_result = limit if limit is not None and limit < 500 else 500
-        response = (self._provider
-                        .gcs_storage
-                        .objects()
-                        .list(bucket=self.bucket.name,
-                              prefix=prefix if prefix else '',
-                              maxResults=max_result,
-                              pageToken=marker)
-                        .execute())
-        objects = []
-        for obj in response.get('items', []):
-            objects.append(GCSObject(self._provider, self.bucket, obj))
-        if len(objects) > max_result:
-            cb.log.warning('Expected at most %d results; got %d',
-                           max_result, len(objects))
-        return ServerPagedResultList('nextPageToken' in response,
-                                     response.get('nextPageToken'),
-                                     False, data=objects)
-
-    def find(self, **kwargs):
-        obj_list = self.list()
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches),
-                                     limit=None, marker=None)
-
-    def create(self, name):
-        return self.bucket.create_object(name)
-
 
 class GCSBucket(BaseBucket):
 
@@ -2265,40 +2229,6 @@ class GCSBucket(BaseBucket):
     def objects(self):
         return self._object_container
 
-    def delete(self, delete_contents=False):
-        """
-        Delete this bucket.
-        """
-        (self._provider
-             .gcs_storage
-             .buckets()
-             .delete(bucket=self.name)
-             .execute())
-        # GCS has a rate limit of 1 operation per 2 seconds for bucket
-        # creation/deletion: https://cloud.google.com/storage/quotas.  Throttle
-        # here to avoid future failures.
-        time.sleep(2)
-
-    def create_object(self, name):
-        """
-        Create an empty plain text object.
-        """
-        response = self.create_object_with_media_body(
-            name,
-            googleapiclient.http.MediaIoBaseUpload(
-                    io.BytesIO(b''), mimetype='plain/text'))
-        return GCSObject(self._provider, self, response) if response else None
-
-    def create_object_with_media_body(self, name, media_body):
-        response = (self._provider
-                        .gcs_storage
-                        .objects()
-                        .insert(bucket=self.name,
-                                body={'name': name},
-                                media_body=media_body)
-                        .execute())
-        return response
-
 
 class GCELaunchConfig(BaseLaunchConfig):
 

+ 115 - 4
cloudbridge/cloud/providers/gce/services.py

@@ -1,3 +1,4 @@
+import io
 import ipaddress
 import json
 import logging
@@ -8,8 +9,10 @@ import googleapiclient
 
 import cloudbridge as cb
 from cloudbridge.cloud.base import helpers as cb_helpers
+from cloudbridge.cloud.base.middleware import implement
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
+from cloudbridge.cloud.base.services import BaseBucketObjectService
 from cloudbridge.cloud.base.services import BaseBucketService
 from cloudbridge.cloud.base.services import BaseComputeService
 from cloudbridge.cloud.base.services import BaseImageService
@@ -46,6 +49,7 @@ from .resources import GCEVMFirewall
 from .resources import GCEVMType
 from .resources import GCEVolume
 from .resources import GCSBucket
+from .resources import GCSObject
 
 log = logging.getLogger(__name__)
 
@@ -919,6 +923,7 @@ class GCPStorageService(BaseStorageService):
         self._volume_svc = GCEVolumeService(self.provider)
         self._snapshot_svc = GCESnapshotService(self.provider)
         self._bucket_svc = GCSBucketService(self.provider)
+        self._bucket_obj_svc = GCSBucketObjectService(self.provider)
 
     @property
     def volumes(self):
@@ -932,6 +937,10 @@ class GCPStorageService(BaseStorageService):
     def buckets(self):
         return self._bucket_svc
 
+    @property
+    def bucket_objects(self):
+        return self._bucket_obj_svc
+
 
 class GCEVolumeService(BaseVolumeService):
 
@@ -1130,7 +1139,9 @@ class GCSBucketService(BaseBucketService):
     def __init__(self, provider):
         super(GCSBucketService, self).__init__(provider)
 
-    def get(self, bucket_id):
+    @implement(event_pattern="provider.storage.buckets.get",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _get(self, bucket_id):
         """
         Returns a bucket given its ID. Returns ``None`` if the bucket
         does not exist or if the user does not have permission to access the
@@ -1139,7 +1150,9 @@ class GCSBucketService(BaseBucketService):
         bucket = self.provider.get_resource('buckets', bucket_id)
         return GCSBucket(self.provider, bucket) if bucket else None
 
-    def find(self, name, limit=None, marker=None):
+    @implement(event_pattern="provider.storage.buckets.find",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _find(self, name, limit=None, marker=None):
         """
         Searches in bucket names for a substring.
         """
@@ -1147,7 +1160,9 @@ class GCSBucketService(BaseBucketService):
         return ClientPagedResultList(self.provider, buckets, limit=limit,
                                      marker=marker)
 
-    def list(self, limit=None, marker=None):
+    @implement(event_pattern="provider.storage.buckets.list",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _list(self, limit=None, marker=None):
         """
         List all containers.
         """
@@ -1169,7 +1184,9 @@ class GCSBucketService(BaseBucketService):
                                      response.get('nextPageToken'),
                                      False, data=buckets)
 
-    def create(self, name, location=None):
+    @implement(event_pattern="provider.storage.buckets.create",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _create(self, name, location=None):
         GCSBucket.assert_valid_resource_name(name)
         body = {'name': name}
         if location:
@@ -1193,3 +1210,97 @@ class GCSBucketService(BaseBucketService):
                     'Bucket already exists with name {0}'.format(name))
             else:
                 raise
+
+    @implement(event_pattern="provider.storage.buckets.delete",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _delete(self, bucket_id):
+        """
+        Delete this bucket.
+        """
+        # GCE uses name rather than URL to identify resources
+        name = (self._provider._storage_resources
+                    .parse_url(bucket_id)
+                    .parameters.get("bucket"))
+        (self._provider
+             .gcs_storage
+             .buckets()
+             .delete(bucket=name)
+             .execute())
+        # GCS has a rate limit of 1 operation per 2 seconds for bucket
+        # creation/deletion: https://cloud.google.com/storage/quotas. Throttle
+        # here to avoid future failures.
+        time.sleep(2)
+
+
+class GCSBucketObjectService(BaseBucketObjectService):
+
+    def __init__(self, provider):
+        super(GCSBucketObjectService, self).__init__(provider)
+
+    def get(self, bucket, name):
+        """
+        Retrieve a given object from this bucket.
+        """
+        obj = self.provider.get_resource('objects', name,
+                                         bucket=bucket.name)
+        return GCSObject(self.provider, bucket, obj) if obj else None
+
+    def list(self, bucket, limit=None, marker=None, prefix=None):
+        """
+        List all objects within this bucket.
+        """
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcs_storage
+                        .objects()
+                        .list(bucket=bucket.name,
+                              prefix=prefix if prefix else '',
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        objects = []
+        for obj in response.get('items', []):
+            objects.append(GCSObject(self.provider, bucket, obj))
+        if len(objects) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(objects))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=objects)
+
+    def find(self, bucket, **kwargs):
+        master_list = []
+        obj_list = self.list(bucket=bucket, limit=500)
+        if obj_list.supports_server_paging:
+            master_list.extend(obj_list)
+            while obj_list.is_truncated:
+                obj_list = self.list(marker=obj_list.marker,
+                                     **kwargs)
+                master_list.extend(obj_list)
+        else:
+            master_list.extend(obj_list.data)
+
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=None, marker=None)
+
+    def _create_object_with_media_body(self, bucket, name, media_body):
+        response = (self.provider
+                    .gcs_storage
+                    .objects()
+                    .insert(bucket=bucket.name,
+                            body={'name': name},
+                            media_body=media_body)
+                    .execute())
+        return response
+
+    def create(self, bucket, name):
+        response = self._create_object_with_media_body(
+                            bucket,
+                            name,
+                            googleapiclient.http.MediaIoBaseUpload(
+                                io.BytesIO(b''), mimetype='plain/text'))
+        return GCSObject(self._provider,
+                         bucket,
+                         response) if response else None

+ 2 - 50
cloudbridge/cloud/providers/openstack/resources.py

@@ -23,7 +23,8 @@ from openstack.exceptions import NotFoundException
 from openstack.exceptions import ResourceNotFound
 
 import swiftclient
-from swiftclient.service import SwiftService, SwiftUploadObject
+from swiftclient.service import SwiftService
+from swiftclient.service import SwiftUploadObject
 from swiftclient.utils import generate_temp_url
 
 import cloudbridge.cloud.base.helpers as cb_helpers
@@ -60,7 +61,6 @@ from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SubnetState
 from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VolumeState
-from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 
 ONE_GIG = 1048576000  # in bytes
 FIVE_GIG = ONE_GIG * 5  # in bytes
@@ -1517,56 +1517,8 @@ class OpenStackBucket(BaseBucket):
     def objects(self):
         return self._object_container
 
-    def delete(self, delete_contents=False):
-        self._provider.swift.delete_container(self.name)
-
 
 class OpenStackBucketContainer(BaseBucketContainer):
 
     def __init__(self, provider, bucket):
         super(OpenStackBucketContainer, self).__init__(provider, bucket)
-
-    def get(self, name):
-        """
-        Retrieve a given object from this bucket.
-        """
-        # Swift always returns a reference for the container first,
-        # followed by a list containing references to objects.
-        _, object_list = self._provider.swift.get_container(
-            self.bucket.name, prefix=name)
-        # Loop through list of objects looking for an exact name vs. a prefix
-        for obj in object_list:
-            if obj.get('name') == name:
-                return OpenStackBucketObject(self._provider,
-                                             self.bucket,
-                                             obj)
-        return None
-
-    def list(self, limit=None, marker=None, prefix=None):
-        """
-        List all objects within this bucket.
-
-        :rtype: BucketObject
-        :return: List of all available BucketObjects within this bucket.
-        """
-        _, object_list = self._provider.swift.get_container(
-            self.bucket.name,
-            limit=oshelpers.os_result_limit(self._provider, limit),
-            marker=marker, prefix=prefix)
-        cb_objects = [OpenStackBucketObject(
-            self._provider, self.bucket, obj) for obj in object_list]
-
-        return oshelpers.to_server_paged_list(
-            self._provider,
-            cb_objects,
-            limit)
-
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
-
-    def create(self, object_name):
-        self._provider.swift.put_object(self.bucket.name, object_name, None)
-        return self.get(object_name)

+ 80 - 17
cloudbridge/cloud/providers/openstack/services.py

@@ -15,8 +15,10 @@ from openstack.exceptions import ResourceNotFound
 from swiftclient import ClientException as SwiftClientException
 
 import cloudbridge.cloud.base.helpers as cb_helpers
+from cloudbridge.cloud.base.middleware import implement
 from cloudbridge.cloud.base.resources import BaseLaunchConfig
 from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.services import BaseBucketObjectService
 from cloudbridge.cloud.base.services import BaseBucketService
 from cloudbridge.cloud.base.services import BaseComputeService
 from cloudbridge.cloud.base.services import BaseImageService
@@ -47,6 +49,7 @@ from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 
 from .resources import OpenStackBucket
+from .resources import OpenStackBucketObject
 from .resources import OpenStackInstance
 from .resources import OpenStackKeyPair
 from .resources import OpenStackMachineImage
@@ -251,6 +254,7 @@ class OpenStackStorageService(BaseStorageService):
         self._volume_svc = OpenStackVolumeService(self.provider)
         self._snapshot_svc = OpenStackSnapshotService(self.provider)
         self._bucket_svc = OpenStackBucketService(self.provider)
+        self._bucket_obj_svc = OpenStackBucketObjectService(self.provider)
 
     @property
     def volumes(self):
@@ -264,6 +268,10 @@ class OpenStackStorageService(BaseStorageService):
     def buckets(self):
         return self._bucket_svc
 
+    @property
+    def bucket_objects(self):
+        return self._bucket_obj_svc
+
 
 class OpenStackVolumeService(BaseVolumeService):
 
@@ -403,12 +411,13 @@ class OpenStackBucketService(BaseBucketService):
     def __init__(self, provider):
         super(OpenStackBucketService, self).__init__(provider)
 
-    def get(self, bucket_id):
+    @implement(event_pattern="provider.storage.buckets.get",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _get(self, bucket_id):
         """
         Returns a bucket given its ID. Returns ``None`` if the bucket
         does not exist.
         """
-        log.debug("Getting OpenStack bucket with the id: %s", bucket_id)
         _, container_list = self.provider.swift.get_account(
             prefix=bucket_id)
         if container_list:
@@ -419,27 +428,24 @@ class OpenStackBucketService(BaseBucketService):
             log.debug("Bucket %s was not found.", bucket_id)
             return None
 
-    def find(self, **kwargs):
+    @implement(event_pattern="provider.storage.buckets.find",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _find(self, **kwargs):
         name = kwargs.pop('name', None)
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
             raise TypeError("Unrecognised parameters for search: %s."
                             " Supported attributes: %s" % (kwargs, 'name'))
-
-        log.debug("Searching for the OpenStack Bucket with the name: %s", name)
-        _, container_list = self.provider.swift.get_account(
-            limit=oshelpers.os_result_limit(self.provider),
-            marker=None)
+        _, container_list = self.provider.swift.get_account()
         cb_buckets = [OpenStackBucket(self.provider, c)
                       for c in container_list
                       if name in c.get("name")]
         return oshelpers.to_server_paged_list(self.provider, cb_buckets)
 
-    def list(self, limit=None, marker=None):
-        """
-        List all containers.
-        """
+    @implement(event_pattern="provider.storage.buckets.list",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _list(self, limit, marker):
         _, container_list = self.provider.swift.get_account(
             limit=oshelpers.os_result_limit(self.provider, limit),
             marker=marker)
@@ -447,12 +453,11 @@ class OpenStackBucketService(BaseBucketService):
                       for c in container_list]
         return oshelpers.to_server_paged_list(self.provider, cb_buckets, limit)
 
-    def create(self, name, location=None):
-        """
-        Create a new bucket.
-        """
-        log.debug("Creating a new OpenStack Bucket with the name: %s", name)
+    @implement(event_pattern="provider.storage.buckets.create",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _create(self, name, location):
         OpenStackBucket.assert_valid_resource_name(name)
+        location = location or self.provider.region_name
         try:
             self.provider.swift.head_container(name)
             raise DuplicateResourceException(
@@ -461,6 +466,64 @@ class OpenStackBucketService(BaseBucketService):
             self.provider.swift.put_container(name)
             return self.get(name)
 
+    @implement(event_pattern="provider.storage.buckets.delete",
+               priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def _delete(self, bucket_id):
+        self.provider.swift.delete_container(bucket_id)
+
+
+class OpenStackBucketObjectService(BaseBucketObjectService):
+
+    def __init__(self, provider):
+        super(OpenStackBucketObjectService, self).__init__(provider)
+
+    def get(self, bucket, name):
+        """
+        Retrieve a given object from this bucket.
+        """
+        # Swift always returns a reference for the container first,
+        # followed by a list containing references to objects.
+        _, object_list = self.provider.swift.get_container(
+            bucket.name, prefix=name)
+        # Loop through list of objects looking for an exact name vs. a prefix
+        for obj in object_list:
+            if obj.get('name') == name:
+                return OpenStackBucketObject(self.provider,
+                                             bucket,
+                                             obj)
+        return None
+
+    def list(self, bucket, limit=None, marker=None, prefix=None):
+        """
+        List all objects within this bucket.
+
+        :rtype: BucketObject
+        :return: List of all available BucketObjects within this bucket.
+        """
+        _, object_list = self.provider.swift.get_container(
+            bucket.name,
+            limit=oshelpers.os_result_limit(self.provider, limit),
+            marker=marker, prefix=prefix)
+        cb_objects = [OpenStackBucketObject(
+            self.provider, bucket, obj) for obj in object_list]
+
+        return oshelpers.to_server_paged_list(
+            self.provider,
+            cb_objects,
+            limit)
+
+    def find(self, bucket, **kwargs):
+        _, obj_list = self.provider.swift.get_container(bucket.name)
+        cb_objs = [OpenStackBucketObject(self.provider, bucket, obj)
+                   for obj in obj_list]
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, cb_objs)
+        return ClientPagedResultList(self.provider, list(matches))
+
+    def create(self, bucket, object_name):
+        self.provider.swift.put_object(bucket.name, object_name, None)
+        return self.get(bucket, object_name)
+
 
 class OpenStackComputeService(BaseComputeService):
 

+ 219 - 0
docs/topics/event_system.rst

@@ -0,0 +1,219 @@
+Working with the CloudBridge Event System
+=========================================
+In order to provide more comprehensive logging and standardize CloudBridge
+functions, we have adopted a middleware layer to handle event calls. In short,
+each event has a corresponding list of dispatchers called in priority order.
+For the time being, only a listening subscription model is implemented, thus
+each event has a series of subscribed methods accepting the same parameters,
+that get run in priority order along with the main function call.
+This Event System allows both developers and users to easily add
+intermediary functions by event name, without having to modify the
+pre-existing code, thus improving the library's flexibility.
+
+Event Handler
+-------------
+Each function attached to an event has a corresponding handler. This handler
+has a type, a callback function, and a link to the next handler. When
+invoked, the handler will call its callback function and, when available,
+invoke the next handler in the linked list of handlers.
+
+Handler Types
+-------------
+Each Event Handler has a type, which determines how it's invoked. There are
+currently two supported types: `SUBSCRIPTION`, and `RESULT_SUBSCRIPTION`.
+Handlers of `SUBSCRIPTION` type are simple listeners, who intercept the main
+function arguments but do not modify them. They are independent of any
+previous or future handler, and have no return value. Their associated
+callback function expects the exact same parameters as the main function.
+Handlers of `RESULT_SUBSCRIPTION` type are similar to `SUBSCRIPTION` handlers,
+but have access to the last non-null return value from any previous handler.
+They are similarly listeners, intercepting arguments without modifying them
+and do not return any value. Their associated callback will however be
+called with an additional keyword parameter named `callback_result` holding
+the last non-null return value from any previous handler. The callback
+function thus needs to accept such a parameter.
+
+Event Dispatcher
+----------------
+A single event dispatcher is initialized with the provider, and will hold
+the entirety of the handlers for all events. This dispatcher handles new
+subscriptions and event calls. When an event is called, the dispatcher will
+link each handler to the next one in line, then invoke the first handler,
+thus triggering the chain of handlers.
+
+Priorities
+----------
+As previously mentioned, dispatchers will be invoked in order of priority.
+These priorities are assigned at subscription time, and must be unique.
+Below are the default priorities used across events:
+
++------------------------+----------+
+| Handler                | Priority |
++------------------------+----------+
+| Pre-Logger             | 2000     |
++------------------------+----------+
+| Main Function Call     | 2500     |
++------------------------+----------+
+| Post-Logger            | 3000     |
++------------------------+----------+
+
+The Pre- and Post- loggers represent universal loggers respectively keeping
+track of the event called and its parameters before the call, and the returned
+value after the call. The main function call represents the core function,
+which is not subscribed permanently, but rather called directly with the event.
+
+User Example
+------------
+From a user's perspective, the Event System is invisible unless the user
+wishes to extend the chain of handlers with their own code:
+
+.. code-block:: python
+
+    from cloudbridge.cloud.factory import CloudProviderFactory, ProviderList
+
+    provider = CloudProviderFactory().create_provider(ProviderList.FIRST, {})
+    id = 'thisIsAnID'
+    obj = provider.storage.buckets.get(id)
+
+However, if they wish to add their own logging interface, for example, they
+can do so without modifying CloudBridge code:
+
+
+.. code-block:: python
+
+    from cloudbridge.cloud.factory import CloudProviderFactory, ProviderList
+
+    provider = CloudProviderFactory().create_provider(ProviderList.AZURE, {})
+
+    ## I don't want to setup a logger, just want to print some messages for
+    ## debugging
+    def print_id(obj_id):
+        print("I am getting this id: " + obj_id)
+
+    provider.storage.buckets.subscribe("get", priority=1500, callback=print_id)
+
+    id1 = 'thisIsAnID'
+    id2 = 'thisIsAnID2'
+
+    ## The subscribed print function will get called every time the get
+    ## method is invoked
+    obj1 = provider.storage.buckets.get(id1)
+    ## I am getting this id: thisIsAnID
+    obj2 = provider.storage.buckets.get(id2)
+    ## I am getting this id: thisIsAnID2
+
+
+Developer Example
+-----------------
+Below is an example of the way in which the Event System works for a simple
+getter, from the CloudBridge developer perspective.
+
+.. code-block:: python
+
+    ## Provider Specific code
+    class MyFirstProviderService(BaseService):
+
+        def __init__(self, provider):
+            super(MyFirstProviderService, self).__init__(provider)
+
+        def _get(self, obj_id):
+            # do the getting
+            resource = ...
+            return MyFirstProviderResource(resource)
+
+    class MySecondProviderService(BaseService):
+
+        def __init__(self, provider):
+            super(MySecondProviderService, self).__init__(provider)
+
+        def _get(self, obj_id):
+            # do the getting
+            resource = ...
+            return MySecondProviderResource(resource)
+
+    ## Base code
+    class BaseService(ProviderService):
+        def __init__(self, provider):
+            super(Service, self).__init__(provider)
+            # Example: provider.storage.buckets for buckets
+            self._service_event_name = "provider.service.servicename"
+
+        def _init_get(self):
+
+            def _get_pre_log(obj_id):
+                log.debug("Getting {} object with the id: {}".format(
+                    self.provider.name, bucket_id))
+
+            def _get_post_log(callback_result, obj_id):
+                log.debug("Returned object: {}".format(callback_result))
+
+            self.subscribe("get", 2000, _get_pre_log)
+            self.subscribe("get", 3000, _get_post_log,
+                                 result_callback=True)
+
+            self.mark_initialized("get")
+
+        # Public get function
+        def get(self, obj_id):
+            """
+            Returns an object given its ID. Returns ``None`` if the object
+            does not exist.
+            """
+            if not self.check_initialized("get"):
+                self._init_get()
+            return self.call("get", priority=2500,
+                                   main_call=self._get,
+                                   obj_id=obj_id)
+
+Thus, adding a new provider only requires adding the Service class with a
+protected class accepting the same parameters, and the logging and public
+method signature will remain the same, as the code will not be re-written
+for each provider.
+Additionally, if a developer needs to add additional logging for a
+particular service, beyond the default logging for all services, they can do
+so in the event initialisation function, and it will be applied to all
+providers. For example:
+
+.. code-block:: python
+
+    ## Base code
+    class BaseService(ProviderService):
+        def __init__(self, provider):
+            super(Service, self).__init__(provider)
+            self._service_event_name = "provider.service"
+
+        def _init_get(self):
+
+            def _get_pre_log(obj_id):
+                log.debug("Getting {} object with the id: {}".format(
+                    self.provider.name, bucket_id))
+
+            def _get_post_log(callback_result, obj_id):
+                log.debug("Returned object: {}".format(callback_result))
+
+            def _special_none_log(callback_result, obj_id):
+                if not callback_result:
+                    log.debug("There is no object with id '{}'".format(obj_id))
+
+            self.subscribe("get", 2000, _get_pre_log)
+            self.subscribe("get", 3000, _get_post_log,
+                                 result_callback=True)
+            self.subscribe("get", 2750, _special_none_log,
+                                 result_callback=True)
+
+            self.mark_initialized("get")
+
+       # Public get function
+        def get(self, obj_id):
+            """
+            Returns an object given its ID. Returns ``None`` if the object
+            does not exist.
+            """
+            if not self.check_initialized("get"):
+                self._init_get()
+            return self.call("get", priority=2500,
+                                   main_call=self._get,
+                                   obj_id=obj_id)
+
+
+

+ 34 - 0
test/helpers/__init__.py

@@ -1,4 +1,5 @@
 import functools
+import operator
 import os
 import sys
 import traceback
@@ -81,6 +82,39 @@ def skipIfNoService(services):
     return wrap
 
 
+def skipIfPython(op, major, minor):
+    """
+    A decorator for skipping tests if the python
+    version doesn't match
+    """
+    def stringToOperator(op):
+        op_map = {
+            "=": operator.eq,
+            "==": operator.eq,
+            "<": operator.lt,
+            "<=": operator.le,
+            ">": operator.gt,
+            ">=": operator.ge,
+        }
+        return op_map.get(op)
+
+    def wrap(func):
+        """
+        The actual wrapper
+        """
+        @functools.wraps(func)
+        def wrapper(self, *args, **kwargs):
+            op_func = stringToOperator(op)
+            if op_func(sys.version_info, (major, minor)):
+                self.skipTest(
+                    "Skipping test because python version {0} is {1} expected"
+                    " version {2}".format(sys.version_info[:2],
+                                          op, (major, minor)))
+            func(self, *args, **kwargs)
+        return wrapper
+    return wrap
+
+
 TEST_DATA_CONFIG = {
     "AWSCloudProvider": {
         # Match the ami value with entry in custom_amis.json for use with moto

+ 19 - 0
test/test_block_store_service.py

@@ -19,6 +19,25 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['storage.volumes', 'storage.volumes'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(
+            self.provider.storage.volumes._service_event_pattern,
+            "provider.storage.volumes",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("volumes",
+                                     "provider.storage.volumes",
+                                     self.provider.storage.volumes.
+                                     _service_event_pattern))
+        self.assertEqual(
+            self.provider.storage.snapshots._service_event_pattern,
+            "provider.storage.snapshots",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("snapshots",
+                                     "provider.storage.snapshots",
+                                     self.provider.storage.snapshots.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['storage.volumes'])
     def test_crud_volume(self):
         def create_vol(label):

+ 11 - 0
test/test_compute_service.py

@@ -20,6 +20,17 @@ class CloudComputeServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['compute.instances'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(
+            self.provider.compute.instances._service_event_pattern,
+            "provider.compute.instances",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("instances",
+                                     "provider.compute.instances",
+                                     self.provider.compute.instances.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['compute.instances', 'networking.networks'])
     def test_crud_instance(self):
         label = "cb-instcrud-{0}".format(helpers.get_uuid())

+ 468 - 0
test/test_event_system.py

@@ -0,0 +1,468 @@
+import unittest
+
+from cloudbridge.cloud.base.events import SimpleEventDispatcher
+from cloudbridge.cloud.interfaces.events import EventHandler
+from cloudbridge.cloud.interfaces.exceptions import HandlerException
+
+
+class EventSystemTestCase(unittest.TestCase):
+
+    def test_emit_event_no_handlers(self):
+        dispatcher = SimpleEventDispatcher()
+        result = dispatcher.dispatch(self, "event.hello.world")
+        self.assertIsNone(result, "Result should be none as there are no"
+                          "registered handlers")
+
+    def test_emit_event_observing_handler(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback(event_args, *args, **kwargs):
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': EVENT_NAME})
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            callback_tracker[0] += 'obs'
+            return "hello"
+
+        dispatcher = SimpleEventDispatcher()
+        handler = dispatcher.observe(event_pattern=EVENT_NAME, priority=1000,
+                                     callback=my_callback)
+        self.assertIsInstance(handler, EventHandler)
+        result = dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
+                                     a_keyword_arg='another_thing')
+        self.assertEqual(
+            callback_tracker[0], "obs", "callback should have been invoked"
+            "once and contain value `obs` but tracker value is {0}".format(
+                callback_tracker[0]))
+        self.assertIsNone(result, "Result should be none as this is an"
+                          " observing handler")
+
+    def test_emit_event_intercepting_handler(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback(event_args, *args, **kwargs):
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': EVENT_NAME,
+                                  'next_handler': None})
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            callback_tracker[0] += "intcpt"
+            return "world"
+
+        dispatcher = SimpleEventDispatcher()
+        handler = dispatcher.intercept(event_pattern=EVENT_NAME, priority=1000,
+                                       callback=my_callback)
+        self.assertIsInstance(handler, EventHandler)
+        result = dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
+                                     a_keyword_arg='another_thing')
+        self.assertEqual(
+            callback_tracker[0], "intcpt", "callback should have been invoked"
+            "once and contain value `intcpt` but tracker value is {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, "world", "Result should be `world` as this"
+                         " is an intercepting handler")
+
+    def test_emit_event_implementing_handler(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback(*args, **kwargs):
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            callback_tracker[0] += "impl"
+            return "world"
+
+        dispatcher = SimpleEventDispatcher()
+        handler = dispatcher.implement(event_pattern=EVENT_NAME, priority=1000,
+                                       callback=my_callback)
+        self.assertIsInstance(handler, EventHandler)
+        result = dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
+                                     a_keyword_arg='another_thing')
+        self.assertEqual(
+            callback_tracker[0], "impl", "callback should have been invoked"
+            "once and contain value `intcpt` but tracker value is {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, "world", "Result should be `world` as this"
+                         " is an implementing handler")
+
+    def test_emit_event_observe_precedes_intercept(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback_obs(event_args, *args, **kwargs):
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': EVENT_NAME})
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            callback_tracker[0] += "obs_"
+            return "hello"
+
+        def my_callback_intcpt(event_args, *args, **kwargs):
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': EVENT_NAME,
+                                  'next_handler': None})
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            callback_tracker[0] += "intcpt_"
+            return "world"
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.observe(EVENT_NAME, 1000, my_callback_obs)
+        dispatcher.intercept(EVENT_NAME, 1001, my_callback_intcpt)
+        result = dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
+                                     a_keyword_arg='another_thing')
+        self.assertEqual(
+            callback_tracker[0], "obs_intcpt_", "callback was not invoked in "
+            "expected order. Should have been obs_intcpt_ but is {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, "world", "Result should be `world` as this"
+                         " is the return value of the intercepting handler")
+
+    def test_emit_event_observe_follows_intercept(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback_intcpt(event_args, *args, **kwargs):
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            self.assertEqual(event_args.get('sender'), self)
+            self.assertEqual(event_args.get('next_handler').priority, 1001)
+            self.assertEqual(event_args.get('next_handler').callback.__name__,
+                             "my_callback_obs")
+            callback_tracker[0] += "intcpt_"
+            # invoke next handler
+            next_handler = event_args.get('next_handler')
+            assert next_handler.priority == 1001
+            assert next_handler.event_pattern == EVENT_NAME
+            assert next_handler.callback == my_callback_obs
+            retval = next_handler.invoke(event_args, *args, **kwargs)
+            self.assertIsNone(retval, "Return values of observable handlers"
+                              " should not be propagated.")
+            return "world"
+
+        def my_callback_obs(event_args, *args, **kwargs):
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': EVENT_NAME})
+            callback_tracker[0] += "obs_"
+            return "hello"
+
+        dispatcher = SimpleEventDispatcher()
+        # register priorities out of order to test that too
+        dispatcher.observe(EVENT_NAME, 1001, my_callback_obs)
+        dispatcher.intercept(EVENT_NAME, 1000, my_callback_intcpt)
+        result = dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
+                                     a_keyword_arg='another_thing')
+        self.assertEqual(
+            callback_tracker[0], "intcpt_obs_", "callback was not invoked in "
+            "expected order. Should have been intcpt_obs_ but is {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, "world", "Result should be `world` as this"
+                         " is the return value of the intercepting handler")
+
+    def test_emit_event_intercept_follows_intercept(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback_intcpt1(event_args, *args, **kwargs):
+            self.assertEqual(event_args.get('sender'), self)
+            next_handler = event_args.get('next_handler')
+            self.assertEqual(next_handler.priority, 2020)
+            self.assertEqual(next_handler.callback.__name__,
+                             "my_callback_intcpt2")
+            callback_tracker[0] += "intcpt1_"
+            # invoke next handler but ignore return value
+            return "hello" + next_handler.invoke(event_args, *args, **kwargs)
+
+        def my_callback_intcpt2(event_args, *args, **kwargs):
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': EVENT_NAME,
+                                  'next_handler': None})
+            callback_tracker[0] += "intcpt2_"
+            return "world"
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.intercept(EVENT_NAME, 2000, my_callback_intcpt1)
+        dispatcher.intercept(EVENT_NAME, 2020, my_callback_intcpt2)
+        result = dispatcher.dispatch(self, EVENT_NAME)
+        self.assertEqual(
+            callback_tracker[0], "intcpt1_intcpt2_", "callback was not invoked"
+            " in expected order. Should have been intcpt1_intcpt2_ but is"
+            " {0}".format(callback_tracker[0]))
+        self.assertEqual(result, "helloworld", "Result should be `helloworld` "
+                         "as this is the expected return value from the chain")
+
+    def test_emit_event_implement_follows_intercept(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback_intcpt(event_args, *args, **kwargs):
+            self.assertEqual(event_args.get('sender'), self)
+            next_handler = event_args.get('next_handler')
+            self.assertEqual(next_handler.priority, 2020)
+            self.assertEqual(next_handler.callback.__name__,
+                             "my_callback_impl")
+            callback_tracker[0] += "intcpt_"
+            # invoke next handler but ignore return value
+            return "hello" + next_handler.invoke(event_args, *args, **kwargs)
+
+        def my_callback_impl(*args, **kwargs):
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            callback_tracker[0] += "impl_"
+            return "world"
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.intercept(EVENT_NAME, 2000, my_callback_intcpt)
+        dispatcher.implement(EVENT_NAME, 2020, my_callback_impl)
+        result = dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
+                                     a_keyword_arg='another_thing')
+        self.assertEqual(
+            callback_tracker[0], "intcpt_impl_", "callback was not invoked"
+            " in expected order. Should have been intcpt_impl_ but is"
+            " {0}".format(callback_tracker[0]))
+        self.assertEqual(result, "helloworld", "Result should be `helloworld` "
+                         "as this is the expected return value from the chain")
+
+    def test_emit_event_implement_precedes_intercept(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback_intcpt(event_args, *args, **kwargs):
+            # Impl result should be accessible to intercepts that follow
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': EVENT_NAME,
+                                  'result': 'world',
+                                  'next_handler': None})
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            callback_tracker[0] += "intcpt_"
+            return "hello"
+
+        def my_callback_impl(*args, **kwargs):
+            self.assertSequenceEqual(args, ['first_pos_arg'])
+            self.assertDictEqual(kwargs, {'a_keyword_arg': 'another_thing'})
+            callback_tracker[0] += "impl_"
+            return "world"
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.implement(EVENT_NAME, 2000, my_callback_impl)
+        dispatcher.intercept(EVENT_NAME, 2020, my_callback_intcpt)
+        result = dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
+                                     a_keyword_arg='another_thing')
+        self.assertEqual(
+            callback_tracker[0], "impl_intcpt_", "callback was not invoked"
+            " in expected order. Should have been intcpt_intcpt_ but is"
+            " {0}".format(callback_tracker[0]))
+        self.assertEqual(result, "world", "Result should be `world` "
+                         "as this is the expected return value from the chain")
+
+    def test_subscribe_event_duplicate_priority(self):
+
+        def my_callback(event_args, *args, **kwargs):
+            pass
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.intercept("event.hello.world", 1000, my_callback)
+        dispatcher.intercept("event.hello.world", 1000, my_callback)
+        with self.assertRaises(HandlerException):
+            dispatcher.dispatch(self, "event.hello.world")
+
+    def test_subscribe_event_duplicate_wildcard_priority(self):
+
+        def my_callback(event_args, *args, **kwargs):
+            pass
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.intercept("event.hello.world", 1000, my_callback)
+        dispatcher.intercept("event.hello.*", 1000, my_callback)
+        with self.assertRaises(HandlerException):
+            dispatcher.dispatch(self, "event.hello.world")
+
+    def test_subscribe_event_duplicate_wildcard_priority_allowed(self):
+        # duplicate priorities for different wildcard namespaces allowed
+        def my_callback(event_args, *args, **kwargs):
+            pass
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.intercept("event.hello.world", 1000, my_callback)
+        dispatcher.intercept("someevent.hello.*", 1000, my_callback)
+        # emit should work fine in this case with no exceptions
+        dispatcher.dispatch(self, "event.hello.world")
+
+    def test_subscribe_multiple_events(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback1(event_args, *args, **kwargs):
+            self.assertDictEqual(event_args, {'sender': self,
+                                              'event': EVENT_NAME})
+            callback_tracker[0] += "event1_"
+            return "hello"
+
+        def my_callback2(event_args, *args, **kwargs):
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': "event.hello.anotherworld"})
+            callback_tracker[0] += "event2_"
+            return "another"
+
+        def my_callback3(event_args, *args, **kwargs):
+            self.assertDictEqual(event_args,
+                                 {'sender': self,
+                                  'event': "event.hello.anotherworld",
+                                  'next_handler': None})
+            callback_tracker[0] += "event3_"
+            return "world"
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.observe(EVENT_NAME, 2000, my_callback1)
+        # register to a different event with the same priority
+        dispatcher.observe("event.hello.anotherworld", 2000, my_callback2)
+        dispatcher.intercept("event.hello.anotherworld", 2020, my_callback3)
+        result = dispatcher.dispatch(self, EVENT_NAME)
+        self.assertEqual(
+            callback_tracker[0], "event1_", "only `event.hello.world` handlers"
+            " should have been  triggered but received {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, None, "Result should be `helloworld` "
+                         "as this is the expected return value from the chain")
+
+        result = dispatcher.dispatch(self, "event.hello.anotherworld")
+        self.assertEqual(
+            callback_tracker[0], "event1_event2_event3_", "only handlers for"
+            "  event `event.hello.anotherworld` should have been  triggered"
+            " but received {0}".format(callback_tracker[0]))
+        self.assertEqual(result, "world", "Result should be `world` "
+                         "as this is the expected return value from the chain")
+
+    def test_subscribe_wildcard(self):
+        callback_tracker = ['']
+
+        def my_callback1(event_args, *args, **kwargs):
+            callback_tracker[0] += "event1_"
+            next_handler = event_args.get('next_handler')
+            return "hello" + next_handler.invoke(event_args, *args, **kwargs)
+
+        def my_callback2(event_args, *args, **kwargs):
+            callback_tracker[0] += "event2_"
+            next_handler = event_args.get('next_handler')
+            return "some" + next_handler.invoke(event_args, *args, **kwargs)
+
+        def my_callback3(event_args, *args, **kwargs):
+            callback_tracker[0] += "event3_"
+            next_handler = event_args.get('next_handler')
+            return "other" + next_handler.invoke(event_args, *args, **kwargs)
+
+        def my_callback4(*args, **kwargs):
+            callback_tracker[0] += "event4_"
+            return "world"
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.intercept("event.*", 2000, my_callback1)
+        # register to a different event with the same priority
+        dispatcher.intercept("event.hello.*", 2010, my_callback2)
+        dispatcher.intercept("event.hello.there", 2030, my_callback4)
+        dispatcher.intercept("event.*.there", 2020, my_callback3)
+        dispatcher.intercept("event.*.world", 2020, my_callback4)
+        dispatcher.intercept("someevent.hello.there", 2030, my_callback3)
+        # emit a series of events
+        result = dispatcher.dispatch(self, "event.hello.there")
+
+        self.assertEqual(
+            callback_tracker[0], "event1_event2_event3_event4_",
+            "Event handlers executed in unexpected order {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, "hellosomeotherworld")
+
+        result = dispatcher.dispatch(self, "event.test.hello.world")
+        self.assertEqual(
+            callback_tracker[0], "event1_event2_event3_event4_event1_event4_",
+            "Event handlers executed in unexpected order {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, "helloworld")
+
+    # make sure cache gets invalidated when subscribing after emit
+    def test_subscribe_after_emit(self):
+        callback_tracker = ['']
+
+        def my_callback1(event_args, *args, **kwargs):
+            callback_tracker[0] += "event1_"
+            next_handler = event_args.get('next_handler')
+            if next_handler:
+                return "hello" + next_handler.invoke(
+                    event_args, *args, **kwargs)
+            else:
+                return "hello"
+
+        def my_callback2(*args, **kwargs):
+            callback_tracker[0] += "event2_"
+            return "some"
+
+        dispatcher = SimpleEventDispatcher()
+        dispatcher.intercept("event.hello.world", 1000, my_callback1)
+        dispatcher.dispatch(self, "event.hello.world")
+        dispatcher.intercept("event.hello.*", 1001, my_callback2)
+        result = dispatcher.dispatch(self, "event.hello.world")
+
+        self.assertEqual(
+            callback_tracker[0], "event1_event1_event2_",
+            "Event handlers executed in unexpected order {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, "hellosome")
+
+    def test_unubscribe(self):
+        EVENT_NAME = "event.hello.world"
+        callback_tracker = ['']
+
+        def my_callback1(event_args, *args, **kwargs):
+            callback_tracker[0] += "event1_"
+            next_handler = event_args.get('next_handler')
+            if next_handler:
+                return "hello" + next_handler.invoke(
+                    event_args, *args, **kwargs)
+            else:
+                return "hello"
+
+        def my_callback2(*args, **kwargs):
+            callback_tracker[0] += "event2_"
+            return "some"
+
+        dispatcher = SimpleEventDispatcher()
+        hndlr1 = dispatcher.intercept(EVENT_NAME, 1000, my_callback1)
+        dispatcher.dispatch(self, EVENT_NAME)
+        hndlr2 = dispatcher.intercept("event.hello.*", 1001, my_callback2)
+        # Both handlers should be registered
+        self.assertListEqual(
+            [my_callback1, my_callback2],
+            [handler.callback for handler in
+             dispatcher.get_handlers_for_event(EVENT_NAME)])
+        hndlr1.unsubscribe()
+
+        # Only my_callback2 should be registered after unsubscribe
+        self.assertListEqual(
+            [my_callback2],
+            [handler.callback for handler in
+             dispatcher.get_handlers_for_event(EVENT_NAME)])
+
+        result = dispatcher.dispatch(self, EVENT_NAME)
+
+        self.assertEqual(
+            callback_tracker[0], "event1_event2_",
+            "Event handlers executed in unexpected order {0}".format(
+                callback_tracker[0]))
+        self.assertEqual(result, "some")
+
+        hndlr2.unsubscribe()
+        result = dispatcher.dispatch(self, "event.hello.world")
+        self.assertEqual(result, None)

+ 10 - 0
test/test_image_service.py

@@ -11,6 +11,16 @@ class CloudImageServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['compute.images'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(self.provider.compute.images._service_event_pattern,
+                         "provider.compute.images",
+                         "Event pattern for {} service should be '{}', "
+                         "but found '{}'.".format("images",
+                                                  "provider.compute.images",
+                                                  self.provider.compute.images.
+                                                  _service_event_pattern))
+
     @helpers.skipIfNoService(['compute.images', 'networking.networks',
                               'compute.instances'])
     def test_create_and_list_image(self):

+ 286 - 0
test/test_middleware_system.py

@@ -0,0 +1,286 @@
+import unittest
+
+from cloudbridge.cloud.base.events import SimpleEventDispatcher
+from cloudbridge.cloud.base.middleware import BaseMiddleware
+from cloudbridge.cloud.base.middleware import EventDebugLoggingMiddleware
+from cloudbridge.cloud.base.middleware import ExceptionWrappingMiddleware
+from cloudbridge.cloud.base.middleware import SimpleMiddlewareManager
+from cloudbridge.cloud.base.middleware import implement
+from cloudbridge.cloud.base.middleware import intercept
+from cloudbridge.cloud.base.middleware import observe
+from cloudbridge.cloud.interfaces.exceptions import CloudBridgeBaseException
+from cloudbridge.cloud.interfaces.exceptions import \
+    InvalidConfigurationException
+from cloudbridge.cloud.interfaces.middleware import Middleware
+
+from .helpers import skipIfPython
+
+
+class MiddlewareSystemTestCase(unittest.TestCase):
+
+    def test_basic_middleware(self):
+
+        class DummyMiddleWare(Middleware):
+
+            def __init__(self):
+                self.invocation_order = ""
+
+            def install(self, event_manager):
+                self.event_manager = event_manager
+                self.invocation_order += "install_"
+
+            def uninstall(self):
+                self.invocation_order += "uninstall"
+
+        dispatcher = SimpleEventDispatcher()
+        manager = SimpleMiddlewareManager(dispatcher)
+        middleware = DummyMiddleWare()
+        manager.add(middleware)
+
+        self.assertEqual(middleware.invocation_order, "install_",
+                         "install should be called when adding new middleware")
+
+        manager.remove(middleware)
+        self.assertEqual(middleware.invocation_order, "install_uninstall",
+                         "uninstall should be called when removing middleware")
+
+    def test_base_middleware(self):
+        EVENT_NAME = "some.event.occurred"
+
+        class DummyMiddleWare(BaseMiddleware):
+
+            def __init__(self):
+                self.invocation_order = ""
+
+            @intercept(event_pattern="some.event.*", priority=900)
+            def my_callback_intcpt(self, event_args, *args, **kwargs):
+                self.invocation_order += "intcpt_"
+                assert 'first_pos_arg' in args
+                assert kwargs.get('a_keyword_arg') == "something"
+                next_handler = event_args.get('next_handler')
+                return next_handler.invoke(event_args, *args, **kwargs)
+
+            @implement(event_pattern="some.event.*", priority=950)
+            def my_callback_impl(self, *args, **kwargs):
+                self.invocation_order += "impl_"
+                assert 'first_pos_arg' in args
+                assert kwargs.get('a_keyword_arg') == "something"
+                return "hello"
+
+            @observe(event_pattern="some.event.*", priority=1000)
+            def my_callback_obs(self, event_args, *args, **kwargs):
+                self.invocation_order += "obs"
+                assert 'first_pos_arg' in args
+                assert event_args['result'] == "hello"
+                assert kwargs.get('a_keyword_arg') == "something"
+
+        dispatcher = SimpleEventDispatcher()
+        manager = SimpleMiddlewareManager(dispatcher)
+        middleware = DummyMiddleWare()
+        manager.add(middleware)
+        dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
+                            a_keyword_arg='something')
+
+        self.assertEqual(middleware.invocation_order, "intcpt_impl_obs")
+        self.assertListEqual(
+            [middleware.my_callback_intcpt, middleware.my_callback_impl,
+             middleware.my_callback_obs],
+            [handler.callback for handler
+             in dispatcher.get_handlers_for_event(EVENT_NAME)])
+
+        manager.remove(middleware)
+
+        self.assertListEqual([], dispatcher.get_handlers_for_event(EVENT_NAME))
+
+    def test_multiple_middleware(self):
+        EVENT_NAME = "some.really.interesting.event.occurred"
+
+        class DummyMiddleWare1(BaseMiddleware):
+
+            @observe(event_pattern="some.really.*", priority=1000)
+            def my_obs1_3(self, *args, **kwargs):
+                pass
+
+            @implement(event_pattern="some.*", priority=970)
+            def my_impl1_2(self, *args, **kwargs):
+                return "hello"
+
+            @intercept(event_pattern="some.*", priority=900)
+            def my_intcpt1_1(self, event_args, *args, **kwargs):
+                next_handler = event_args.get('next_handler')
+                return next_handler.invoke(event_args, *args, **kwargs)
+
+        class DummyMiddleWare2(BaseMiddleware):
+
+            @observe(event_pattern="some.really.*", priority=1050)
+            def my_obs2_3(self, *args, **kwargs):
+                pass
+
+            @intercept(event_pattern="*", priority=950)
+            def my_intcpt2_2(self, event_args, *args, **kwargs):
+                next_handler = event_args.get('next_handler')
+                return next_handler.invoke(event_args, *args, **kwargs)
+
+            @implement(event_pattern="some.really.*", priority=920)
+            def my_impl2_1(self, *args, **kwargs):
+                pass
+
+        dispatcher = SimpleEventDispatcher()
+        manager = SimpleMiddlewareManager(dispatcher)
+        middleware1 = DummyMiddleWare1()
+        middleware2 = DummyMiddleWare2()
+        manager.add(middleware1)
+        manager.add(middleware2)
+        dispatcher.dispatch(self, EVENT_NAME)
+
+        # Callbacks in both middleware classes should be registered
+        self.assertListEqual(
+            [middleware1.my_intcpt1_1, middleware2.my_impl2_1,
+             middleware2.my_intcpt2_2, middleware1.my_impl1_2,
+             middleware1.my_obs1_3, middleware2.my_obs2_3],
+            [handler.callback for handler
+             in dispatcher.get_handlers_for_event(EVENT_NAME)])
+
+        manager.remove(middleware1)
+
+        # Only middleware2 callbacks should be registered
+        self.assertListEqual(
+            [middleware2.my_impl2_1, middleware2.my_intcpt2_2,
+             middleware2.my_obs2_3],
+            [handler.callback for handler in
+             dispatcher.get_handlers_for_event(EVENT_NAME)])
+
+        # add middleware back to check that internal state is properly handled
+        manager.add(middleware1)
+
+        # should one again equal original list
+        self.assertListEqual(
+            [middleware1.my_intcpt1_1, middleware2.my_impl2_1,
+             middleware2.my_intcpt2_2, middleware1.my_impl1_2,
+             middleware1.my_obs1_3, middleware2.my_obs2_3],
+            [handler.callback for handler
+             in dispatcher.get_handlers_for_event(EVENT_NAME)])
+
+    def test_automatic_middleware(self):
+        EVENT_NAME = "another.interesting.event.occurred"
+
+        class SomeDummyClass(object):
+
+            @observe(event_pattern="another.really.*", priority=1000)
+            def not_a_match(self, *args, **kwargs):
+                pass
+
+            @intercept(event_pattern="another.*", priority=900)
+            def my_callback_intcpt2(self, *args, **kwargs):
+                pass
+
+            def not_an_event_handler(self, *args, **kwargs):
+                pass
+
+            @observe(event_pattern="another.interesting.*", priority=1000)
+            def my_callback_obs1(self, *args, **kwargs):
+                pass
+
+            @implement(event_pattern="another.interesting.*", priority=1050)
+            def my_callback_impl(self, *args, **kwargs):
+                pass
+
+        dispatcher = SimpleEventDispatcher()
+        manager = SimpleMiddlewareManager(dispatcher)
+        some_obj = SomeDummyClass()
+        middleware = manager.add(some_obj)
+        dispatcher.dispatch(self, EVENT_NAME)
+
+        # Middleware should be discovered even if class containing interceptors
+        # doesn't inherit from Middleware
+        self.assertListEqual(
+            [some_obj.my_callback_intcpt2, some_obj.my_callback_obs1,
+             some_obj.my_callback_impl],
+            [handler.callback for handler
+             in dispatcher.get_handlers_for_event(EVENT_NAME)])
+
+        manager.remove(middleware)
+
+        # Callbacks should be correctly removed
+        self.assertListEqual(
+            [],
+            [handler.callback for handler in
+             dispatcher.get_handlers_for_event(EVENT_NAME)])
+
+
+class ExceptionWrappingMiddlewareTestCase(unittest.TestCase):
+
+    def test_unknown_exception_is_wrapped(self):
+        EVENT_NAME = "an.exceptional.event"
+
+        class SomeDummyClass(object):
+
+            @implement(event_pattern=EVENT_NAME, priority=2500)
+            def raise_a_non_cloudbridge_exception(self, *args, **kwargs):
+                raise Exception("Some unhandled exception")
+
+        dispatcher = SimpleEventDispatcher()
+        manager = SimpleMiddlewareManager(dispatcher)
+        middleware = ExceptionWrappingMiddleware()
+        manager.add(middleware)
+
+        # no exception should be raised when there's no next handler
+        dispatcher.dispatch(self, EVENT_NAME)
+
+        some_obj = SomeDummyClass()
+        manager.add(some_obj)
+
+        with self.assertRaises(CloudBridgeBaseException):
+            dispatcher.dispatch(self, EVENT_NAME)
+
+    def test_cloudbridge_exception_is_passed_through(self):
+        EVENT_NAME = "an.exceptional.event"
+
+        class SomeDummyClass(object):
+
+            @implement(event_pattern=EVENT_NAME, priority=2500)
+            def raise_a_cloudbridge_exception(self, *args, **kwargs):
+                raise InvalidConfigurationException()
+
+        dispatcher = SimpleEventDispatcher()
+        manager = SimpleMiddlewareManager(dispatcher)
+        some_obj = SomeDummyClass()
+        manager.add(some_obj)
+        middleware = ExceptionWrappingMiddleware()
+        manager.add(middleware)
+
+        with self.assertRaises(InvalidConfigurationException):
+            dispatcher.dispatch(self, EVENT_NAME)
+
+
+class EventDebugLoggingMiddlewareTestCase(unittest.TestCase):
+
+    # Only python 3 has assertLogs support
+    @skipIfPython("<", 3, 0)
+    def test_messages_logged(self):
+        EVENT_NAME = "an.exceptional.event"
+
+        class SomeDummyClass(object):
+
+            @implement(event_pattern=EVENT_NAME, priority=2500)
+            def return_some_value(self, *args, **kwargs):
+                return "hello world"
+
+        dispatcher = SimpleEventDispatcher()
+        manager = SimpleMiddlewareManager(dispatcher)
+        middleware = EventDebugLoggingMiddleware()
+        manager.add(middleware)
+        some_obj = SomeDummyClass()
+        manager.add(some_obj)
+
+        with self.assertLogs('cloudbridge.cloud.base.middleware',
+                             level='DEBUG') as cm:
+            dispatcher.dispatch(self, EVENT_NAME,
+                                "named_param", keyword_param="hello")
+        self.assertTrue(
+            "named_param" in cm.output[0]
+            and "keyword_param" in cm.output[0] and "hello" in cm.output[0],
+            "Log output {0} not as expected".format(cm.output[0]))
+        self.assertTrue(
+            "hello world" in cm.output[1],
+            "Log output {0} does not contain result".format(cm.output[1]))

+ 29 - 0
test/test_network_service.py

@@ -16,6 +16,35 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['networking.subnets',
+                              'networking.networks',
+                              'networking.routers'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(
+            self.provider.networking.networks._service_event_pattern,
+            "provider.networking.networks",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("networks",
+                                     "provider.networking.networks",
+                                     self.provider.networking.networks.
+                                     _service_event_pattern))
+        self.assertEqual(
+            self.provider.networking.subnets._service_event_pattern,
+            "provider.networking.subnets",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("subnets",
+                                     "provider.networking.subnets",
+                                     self.provider.networking.subnets.
+                                     _service_event_pattern))
+        self.assertEqual(
+            self.provider.networking.routers._service_event_pattern,
+            "provider.networking.routers",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("routers",
+                                     "provider.networking.routers",
+                                     self.provider.networking.routers.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['networking.networks'])
     def test_crud_network(self):
 

+ 19 - 0
test/test_object_store_service.py

@@ -21,6 +21,25 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['storage.bucket_objects', 'storage.buckets'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(
+            self.provider.storage.buckets._service_event_pattern,
+            "provider.storage.buckets",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("buckets",
+                                     "provider.storage.buckets",
+                                     self.provider.storage.buckets.
+                                     _service_event_pattern))
+        self.assertEqual(
+            self.provider.storage.bucket_objects._service_event_pattern,
+            "provider.storage.bucket_objects",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("bucket_objects",
+                                     "provider.storage.bucket_objects",
+                                     self.provider.storage.bucket_objects.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['storage.buckets'])
     def test_crud_bucket(self):
 

+ 11 - 0
test/test_region_service.py

@@ -11,6 +11,17 @@ class CloudRegionServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['compute.regions'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(
+            self.provider.compute.regions._service_event_pattern,
+            "provider.compute.regions",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("regions",
+                                     "provider.compute.regions",
+                                     self.provider.compute.regions.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['compute.regions'])
     def test_get_and_list_regions(self):
         regions = list(self.provider.compute.regions)

+ 21 - 0
test/test_security_service.py

@@ -15,6 +15,27 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['security.vm_firewalls'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(
+            self.provider.security.key_pairs.
+            _service_event_pattern,
+            "provider.security.key_pairs",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("key_pairs",
+                                     "provider.security.key_pairs",
+                                     self.provider.security.
+                                     key_pairs.
+                                     _service_event_pattern))
+        self.assertEqual(
+            self.provider.security.vm_firewalls._service_event_pattern,
+            "provider.security.vm_firewalls",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("vm_firewalls",
+                                     "provider.security.vm_firewalls",
+                                     self.provider.security.vm_firewalls.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['security.key_pairs'])
     def test_crud_key_pair_service(self):
 

+ 11 - 0
test/test_vm_types_service.py

@@ -9,6 +9,17 @@ class CloudVMTypeServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['compute.vm_types'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(self.provider.compute.vm_types._service_event_pattern,
+                         "provider.compute.vm_types",
+                         "Event pattern for {} service should be '{}', "
+                         "but found '{}'.".format("vm_types",
+                                                  "provider.compute.vm_types",
+                                                  self.provider.compute.
+                                                  vm_types.
+                                                  _service_event_pattern))
+
     @helpers.skipIfNoService(['compute.vm_types'])
     def test_vm_type_properties(self):