Просмотр исходного кода

Merge master into middleware-refactor

almahmoud 7 лет назад
Родитель
Сommit
538ee46a9d

+ 1 - 1
.travis.yml

@@ -57,7 +57,7 @@ before_install:
            }
            ;;
         *)
-           echo "Build triggered through API or CRON job. Running regardless of changes"
+           echo "Build triggered through API or CRON job. Running regardless of changes..."
            ;;
       esac
 install:

+ 26 - 10
cloudbridge/cloud/base/events.py

@@ -14,9 +14,9 @@ class InterceptingEventHandler(EventHandler):
 
     def __init__(self, event_pattern, priority, callback):
         self.__dispatcher = None
-        self.event_pattern = event_pattern
-        self.priority = priority
-        self.callback = callback
+        self.__event_pattern = event_pattern
+        self.__priority = priority
+        self.__callback = callback
 
     def __lt__(self, other):
         # This is required for the bisect module to insert
@@ -33,14 +33,21 @@ class InterceptingEventHandler(EventHandler):
         else:
             return None
 
+    @property
     def event_pattern(self):
-        pass
+        return self.__event_pattern
 
+    @property
     def priority(self):
-        pass
+        return self.__priority
 
+    @property
     def callback(self):
-        pass
+        return self.__callback
+
+    @callback.setter
+    def callback(self, value):
+        self.__callback = value
 
     @property
     def dispatcher(self):
@@ -151,20 +158,29 @@ class SimpleEventDispatcher(EventDispatcher):
             raise HandlerException(message)
         return cache_list
 
+    def _invalidate_cache(self, event_pattern=None):
+        if not event_pattern:
+            # invalidate entire cache
+            self.__handler_cache = {}
+        else:
+            # Smarter invalidation by only deleting events that
+            # are affected by the pattern
+            for key in list(self.__handler_cache.keys()):
+                if re.search(fnmatch.translate(event_pattern), key):
+                    del self.__handler_cache[key]
+
     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 = {}
+        self._invalidate_cache(event_pattern=event_handler.event_pattern)
 
     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 = {}
+        self._invalidate_cache(event_pattern=event_handler.event_pattern)
 
     def observe(self, event_pattern, priority, callback):
         handler = ObservingEventHandler(event_pattern, priority, callback)

+ 5 - 3
cloudbridge/cloud/base/helpers.py

@@ -16,6 +16,8 @@ import six
 
 import cloudbridge
 
+from ..interfaces.exceptions import InvalidParamException
+
 
 def generate_key_pair():
     """
@@ -69,7 +71,7 @@ def generic_find(filter_names, kwargs, objs):
 
     # All kwargs should have been popped at this time.
     if len(kwargs) > 0:
-        raise TypeError(
+        raise InvalidParamException(
             "Unrecognised parameters for search: %s. Supported attributes: %s"
             % (kwargs, filter_names))
 
@@ -153,8 +155,8 @@ def rename_kwargs(func_name, kwargs, aliases):
     for alias, new in aliases.items():
         if alias in kwargs:
             if new in kwargs:
-                raise TypeError('{} received both {} and {}'.format(
-                    func_name, alias, new))
+                raise InvalidParamException(
+                    '{} received both {} and {}'.format(func_name, alias, new))
             # Manually invoke the deprecated decorator with an empty lambda
             # to signal deprecation
             deprecated(deprecated_in='1.1',

+ 9 - 0
cloudbridge/cloud/base/provider.py

@@ -11,6 +11,7 @@ except ImportError:  # Python 2
 import six
 
 from ..base.events import SimpleEventDispatcher
+from ..base.middleware import ExceptionWrappingMiddleware
 from ..base.middleware import SimpleMiddlewareManager
 from ..interfaces import CloudProvider
 from ..interfaces.exceptions import ProviderConnectionException
@@ -88,6 +89,7 @@ class BaseCloudProvider(CloudProvider):
         self._config_parser.read(CloudBridgeConfigLocations)
         self._events = SimpleEventDispatcher()
         self._middleware = SimpleMiddlewareManager(self._events)
+        self.add_required_middleware()
 
     @property
     def config(self):
@@ -105,6 +107,13 @@ class BaseCloudProvider(CloudProvider):
     def middleware(self):
         return self._middleware
 
+    def add_required_middleware(self):
+        """
+        Adds common middleware that is essential for cloudbridge to function.
+        Any other extra middleware can be added through the provider factory.
+        """
+        self.middleware.add(ExceptionWrappingMiddleware())
+
     def authenticate(self):
         """
         A basic implementation which simply runs a low impact command to

+ 8 - 2
cloudbridge/cloud/base/resources.py

@@ -466,7 +466,10 @@ class BaseVolume(BaseCloudResource, BaseObjectLifeCycleMixin, Volume):
             interval=interval)
 
     def delete(self):
-        self._provider.storage.volumes.delete(self.id)
+        """
+        Delete this volume.
+        """
+        return self._provider.storage.volumes.delete(self)
 
 
 class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
@@ -491,7 +494,10 @@ class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
             interval=interval)
 
     def delete(self):
-        self._provider.storage.snapshots.delete(self.id)
+        """
+        Delete this snapshot.
+        """
+        return self._provider.storage.snapshots.delete(self)
 
 
 class BaseKeyPair(BaseCloudResource, KeyPair):

+ 14 - 7
cloudbridge/cloud/base/services.py

@@ -16,6 +16,7 @@ from cloudbridge.cloud.base.resources import BaseVMFirewall
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.interfaces.exceptions import \
     InvalidConfigurationException
+from cloudbridge.cloud.interfaces.exceptions import InvalidParamException
 from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.services import BucketObjectService
 from cloudbridge.cloud.interfaces.services import BucketService
@@ -152,6 +153,12 @@ class BaseKeyPairService(
                              name,  public_key_material=public_key_material)
 
     def delete(self, key_pair_id):
+        return self.dispatch(self, "provider.security.key_pairs.delete",
+                             key_pair_id)
+
+    @implement(event_pattern="provider.security.key_pairs.delete",
+               priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def _delete(self, key_pair_id):
         """
         Delete an existing key pair.
 
@@ -234,9 +241,9 @@ class BaseVMFirewallService(
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])
@@ -391,7 +398,7 @@ class BaseBucketService(
 
     # Generic find will be used for providers where we have not implemented
     # provider-specific querying for find method
-    @implement(event_pattern="*.storage.buckets.find",
+    @implement(event_pattern="provider.storage.buckets.find",
                priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
     def _find(self, **kwargs):
         obj_list = self
@@ -400,9 +407,9 @@ class BaseBucketService(
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])

+ 12 - 1
cloudbridge/cloud/interfaces/exceptions.py

@@ -94,8 +94,19 @@ class DuplicateResourceException(CloudBridgeBaseException):
     pass
 
 
+class InvalidParamException(InvalidNameException):
+    """
+    Marker interface for an invalid or unexpected parameter, for example,
+    to a service.find() method.
+    """
+
+    def __init__(self, msg):
+        super(InvalidParamException, self).__init__(msg)
+
+
 class HandlerException(CloudBridgeBaseException):
     """
     Marker interface for event handler exceptions.
     """
-    pass
+    def __init__(self, msg):
+        super(HandlerException, self).__init__(msg)

+ 41 - 25
cloudbridge/cloud/interfaces/resources.py

@@ -33,7 +33,7 @@ class CloudResource(object):
 
     This interface has a  _provider property that can be used to access the
     provider associated with the resource, which is only intended for use by
-    subclasses. Every cloudbridge resource also has an id, a name and a
+    subclasses. Every CloudBridge resource also has an id, a name and a
     label property. The id property is a unique identifier for the resource.
     The name is a more user-friendly version of an id, suitable for
     display to an end-user. However, it cannot be used in place of id. See
@@ -60,11 +60,11 @@ class CloudResource(object):
         Get the resource identifier.
 
         The id property is used to uniquely identify the resource, and is an
-        opaque value which should not be interpreted by cloudbridge clients,
+        opaque value which should not be interpreted by CloudBridge clients,
         and is a value meaningful to the underlying cloud provider.
 
-        :rtype: ``str`` :return: ID for this resource as returned by the cloud
-        middleware.
+        :rtype: ``str``
+        :return: ID for this resource as returned by the cloud middleware.
         """
         pass
 
@@ -74,30 +74,32 @@ class CloudResource(object):
         Get the name id for the resource.
 
         The name property is typically a user-friendly id value for the
-        resource. The name is different from the id property in the
-        following ways:
-        1. The name property is often a more user-friendly value to
-           display to the user than the id property.
-        2. The name may sometimes be the same as the id, but should never
-           be used in place of the id.
-        3. The id is what will uniquely identify a resource, and will be used
-           internally by cloudbridge for all get operations etc.
-        4. All resources have a name.
-        5. The name is read-only.
-        6. However, the name may not necessarily be unique, which is the
-           reason why it should not be used for uniquely identifying a
-           resource.
+        resource. The name is different from the id property in the following
+        ways:
+
+         1. The name property is often a more user-friendly value to
+            display to the user than the id property.
+         2. The name may sometimes be the same as the id, but should never
+            be used in place of the id.
+         3. The id is what will uniquely identify a resource, and will be used
+            internally by CloudBridge for all get operations etc.
+         4. All resources have a name.
+         5. The name is read-only.
+         6. However, the name may not necessarily be unique, which is the
+            reason why it should not be used for uniquely identifying a
+            resource.
+
         Example:
-        The AWS machine image name maps to a cloudbridge name. It is not
-        editable and is a user friendly name such as 'Ubuntu 14.04' and
+        The AWS machine image name maps to a CloudBridge name. It is not
+        editable and is a user friendly name such as 'Ubuntu 18.04' and
         corresponds to the ami-name. It is distinct from the ami-id, which
-        maps to cloudbridge's id property. The ami-name cannot be edited, and
+        maps to CloudBridge's id property. The ami-name cannot be edited, and
         is set at creation time. It is not necessarily unique.
-        In Azure, the machine image's name corresponds to cloudbridge's name
+        In Azure, the machine image's name corresponds to CloudBridge's name
         property. In Azure, it also happens to be the same as the id property.
 
         The name property and the label property share the same character
-        restrictions. see :py:attr:`~LabeledCloudResource.label`
+        restrictions. See :py:attr:`~LabeledCloudResource.label`.
         """
         pass
 
@@ -121,6 +123,7 @@ class LabeledCloudResource(CloudResource):
         in the underlying cloud provider, or be simulated through tags/labels.
 
         The label property adheres to the following restrictions:
+
         * Must be at least 3 characters in length.
         * Cannot be longer than 63 characters.
         * May only contain ASCII characters comprising of lowercase letters,
@@ -129,18 +132,19 @@ class LabeledCloudResource(CloudResource):
           (i.e. cannot begin or end with a dash)
 
         Some resources may not support labels, in which case, a
-        NotImplementedError will be thrown.
+        ``NotImplementedError`` will be thrown.
 
         :rtype: ``str``
         :return: Label for this resource as returned by the cloud middleware.
-        :throws NotImplementedError if this resource does not support labels
+        :raise: ``NotImplementedError`` if this resource does not support
+                labels.
         """
         pass
 
 
 class Configuration(dict):
     """
-    Represents a cloudbridge configuration object
+    Represents a CloudBridge configuration object
     """
 
     @abstractproperty
@@ -507,6 +511,9 @@ class Instance(ObjectLifeCycleMixin, LabeledCloudResource):
     def label(self, value):
         """
         Set the instance label.
+
+        :type value: ``str``
+        :param value: The value to set the label to.
         """
         pass
 
@@ -895,6 +902,9 @@ class Network(ObjectLifeCycleMixin, LabeledCloudResource):
     def label(self, value):
         """
         Set the resource label.
+
+        :type value: ``str``
+        :param value: The value to set the label to.
         """
         pass
 
@@ -1013,6 +1023,9 @@ class Subnet(ObjectLifeCycleMixin, LabeledCloudResource):
     def label(self, value):
         """
         Set the resource label.
+
+        :type value: ``str``
+        :param value: The value to set the label to.
         """
         pass
 
@@ -1232,6 +1245,9 @@ class Router(LabeledCloudResource):
     def label(self, value):
         """
         Set the resource label.
+
+        :type value: ``str``
+        :param value: The value to set the label to.
         """
         pass
 

+ 20 - 2
cloudbridge/cloud/interfaces/services.py

@@ -362,6 +362,15 @@ class VolumeService(PageableObjectMixin, CloudService):
         """
         pass
 
+    def delete(self, volume):
+        """
+        Delete an existing volume.
+
+        :type volume: ``str`` or :class:`Volume`
+        :param volume: The object or ID of the volume to be deleted.
+        """
+        pass
+
 
 class SnapshotService(PageableObjectMixin, CloudService):
     """
@@ -370,7 +379,7 @@ class SnapshotService(PageableObjectMixin, CloudService):
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, volume_id):
+    def get(self, snapshot_id):
         """
         Returns a snapshot given its id.
 
@@ -422,6 +431,15 @@ class SnapshotService(PageableObjectMixin, CloudService):
         """
         pass
 
+    def delete(self, snapshot):
+        """
+        Delete an existing snapshot.
+
+        :type snapshot: ``str`` or :class:`Snapshot`
+        :param snapshot: The object or ID of the snapshot to be deleted.
+        """
+        pass
+
 
 class StorageService(CloudService):
 
@@ -701,7 +719,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         :param subnet_id: The ID of the subnet to retrieve.
 
         :rtype: ``object`` of :class:`.Subnet`
-        return: a Subnet object
+        :return: a Subnet object
         """
         pass
 

+ 31 - 17
cloudbridge/cloud/providers/aws/services.py

@@ -32,6 +32,7 @@ from cloudbridge.cloud.base.services import BaseVolumeService
 from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
 from cloudbridge.cloud.interfaces.exceptions import \
     InvalidConfigurationException
+from cloudbridge.cloud.interfaces.exceptions import InvalidParamException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 from cloudbridge.cloud.interfaces.resources import Network
@@ -106,8 +107,9 @@ class AWSKeyPairService(BaseKeyPairService):
 
         # 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'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'name'))
 
         log.debug("Searching for Key Pair %s", name)
         return self.svc.find(filter_name='key-name', filter_value=name)
@@ -176,8 +178,9 @@ class AWSVMFirewallService(BaseVMFirewallService):
         log.debug("Searching for Firewall Service %s", label)
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
         return self.svc.find(filter_name='tag:Name',
                              filter_value=label)
 
@@ -236,8 +239,9 @@ class AWSVolumeService(BaseVolumeService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         log.debug("Searching for AWS Volume Service %s", label)
         return self.svc.find(filter_name='tag:Name', filter_value=label)
@@ -267,7 +271,10 @@ class AWSVolumeService(BaseVolumeService):
     @implement(event_pattern="provider.storage.volumes.delete",
                priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
     def _delete(self, volume_id):
-        aws_vol = self.svc.get_raw(volume_id)
+        if isinstance(volume_id, AWSVolume):
+            aws_vol = volume_id._volume
+        else:
+            aws_vol = self.svc.get_raw(volume_id)
         aws_vol.delete()
 
 
@@ -323,6 +330,8 @@ class AWSSnapshotService(BaseSnapshotService):
     @implement(event_pattern="provider.storage.snapshots.delete",
                priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
     def _delete(self, snapshot_id):
+        if isinstance(snapshot_id, AWSSnapshot):
+            aws_snap = snapshot_id._snapshot
         aws_snap = self.svc.get_raw(snapshot_id)
         aws_snap.delete()
 
@@ -499,8 +508,9 @@ class AWSImageService(BaseImageService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         extra_args = {}
         if owner:
@@ -676,8 +686,9 @@ class AWSInstanceService(BaseInstanceService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         return self.svc.find(filter_name='tag:Name', filter_value=label)
 
@@ -807,8 +818,9 @@ class AWSNetworkService(BaseNetworkService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         log.debug("Searching for AWS Network Service %s", label)
         return self.svc.find(filter_name='tag:Name', filter_value=label)
@@ -882,8 +894,9 @@ class AWSSubnetService(BaseSubnetService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         log.debug("Searching for AWS Subnet Service %s", label)
         return self.svc.find(filter_name='tag:Name', filter_value=label)
@@ -1034,8 +1047,9 @@ class AWSRouterService(BaseRouterService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         log.debug("Searching for AWS Router Service %s", label)
         return self.svc.find(filter_name='tag:Name', filter_value=label)

+ 25 - 20
cloudbridge/cloud/providers/azure/services.py

@@ -29,6 +29,7 @@ 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 InvalidParamException
 from cloudbridge.cloud.interfaces.exceptions import InvalidValueException
 from cloudbridge.cloud.interfaces.resources import MachineImage
 from cloudbridge.cloud.interfaces.resources import Network
@@ -188,9 +189,9 @@ class AzureKeyPairService(BaseKeyPairService):
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])
@@ -278,9 +279,9 @@ class AzureVolumeService(BaseVolumeService):
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])
@@ -342,7 +343,9 @@ class AzureVolumeService(BaseVolumeService):
     @implement(event_pattern="provider.storage.volumes.delete",
                priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
     def _delete(self, volume_id):
-        self.provider.azure_client.delete_disk(volume_id)
+        vol_id = (volume_id.id if isinstance(volume_id, AzureVolume)
+                  else volume_id)
+        self.provider.azure_client.delete_disk(vol_id)
 
 
 class AzureSnapshotService(BaseSnapshotService):
@@ -369,9 +372,9 @@ class AzureSnapshotService(BaseSnapshotService):
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])
@@ -413,7 +416,9 @@ class AzureSnapshotService(BaseSnapshotService):
     @implement(event_pattern="provider.storage.snapshots.delete",
                priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
     def _delete(self, snapshot_id):
-        self.provider.azure_client.delete_snapshot(snapshot_id)
+        snap_id = (snapshot_id.id if isinstance(snapshot_id, AzureSnapshot)
+                   else snapshot_id)
+        self.provider.azure_client.delete_snapshot(snap_id)
 
 
 class AzureBucketService(BaseBucketService):
@@ -554,9 +559,9 @@ class AzureImageService(BaseImageService):
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])
@@ -885,9 +890,9 @@ class AzureInstanceService(BaseInstanceService):
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])
@@ -1161,9 +1166,9 @@ class AzureRouterService(BaseRouterService):
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])

+ 83 - 38
cloudbridge/cloud/providers/gce/services.py

@@ -30,6 +30,7 @@ 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 InvalidParamException
 from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VMFirewall
 from cloudbridge.cloud.providers.gce import helpers
@@ -113,9 +114,9 @@ class GCEKeyPairService(BaseKeyPairService):
 
         # 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)))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
 
         return ClientPagedResultList(self.provider,
                                      matches if matches else [])
@@ -247,7 +248,8 @@ class GCEVMTypeService(BaseVMTypeService):
             is_match = True
             for key, value in kwargs.items():
                 if key not in inst_type:
-                    raise TypeError("The attribute key is not valid.")
+                    raise InvalidParamException(
+                        "Unrecognised parameters for search: %s." % key)
                 if inst_type.get(key) != value:
                     is_match = False
                     break
@@ -334,13 +336,20 @@ class GCEImageService(BaseImageService):
                 return public_image
         return None
 
-    def find(self, label, limit=None, marker=None):
+    def find(self, limit=None, marker=None, **kwargs):
         """
         Searches for an image by a given list of attributes
         """
-        filters = {'label': label}
+        label = kwargs.pop('label', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
+
         # Retrieve all available images by setting limit to sys.maxsize
-        images = [image for image in self if image.label == filters['label']]
+        images = [image for image in self if image.label == label]
         return ClientPagedResultList(self.provider, images,
                                      limit=limit, marker=marker)
 
@@ -532,16 +541,23 @@ class GCEInstanceService(BaseInstanceService):
 
     @implement(event_pattern="provider.compute.instances.find",
                priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
-    def _find(self, label, limit=None, marker=None):
+    def _find(self, limit=None, marker=None, **kwargs):
         """
         Searches for instances by instance label.
         :return: a list of Instance objects
         """
+        label = kwargs.pop('label', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
+
         instances = [instance for instance in self.list()
                      if instance.label == label]
-        if limit and len(instances) > limit:
-            instances = instances[:limit]
-        return instances
+        return ClientPagedResultList(self.provider, instances,
+                                     limit=limit, marker=marker)
 
     @implement(event_pattern="provider.compute.instances.list",
                priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
@@ -655,7 +671,8 @@ class GCENetworkService(BaseNetworkService):
         obj_list = self
         filters = ['name', 'label']
         matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=limit, marker=marker)
 
     @implement(event_pattern="provider.networking.networks.list",
                priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
@@ -748,11 +765,12 @@ class GCERouterService(BaseRouterService):
 
     @implement(event_pattern="provider.networking.routers.find",
                priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
-    def _find(self, **kwargs):
+    def find(self, limit=None, marker=None, **kwargs):
         obj_list = self
         filters = ['name', 'label']
         matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=limit, marker=marker)
 
     @implement(event_pattern="provider.networking.routers.list",
                priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
@@ -1039,7 +1057,18 @@ class GCEVolumeService(BaseVolumeService):
 
     @implement(event_pattern="provider.storage.volumes.find",
                priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
-    def _find(self, label, limit=None, marker=None):
+    def _find(self, limit=None, marker=None, **kwargs):
+        """
+        Searches for a volume by a given list of attributes.
+        """
+        label = kwargs.pop('label', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
+
         filtr = 'labels.cblabel eq ' + label
         max_result = limit if limit is not None and limit < 500 else 500
         response = (self.provider
@@ -1063,6 +1092,13 @@ class GCEVolumeService(BaseVolumeService):
     @implement(event_pattern="provider.storage.volumes.list",
                priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
     def _list(self, limit=None, marker=None):
+        """
+        List all volumes.
+
+        limit: The maximum number of volumes to return. The returned
+               ResultList's is_truncated property can be used to determine
+               whether more records are available.
+        """
         # For GCE API, Acceptable values are 0 to 500, inclusive.
         # (Default: 500).
         max_result = limit if limit is not None and limit < 500 else 500
@@ -1116,13 +1152,17 @@ class GCEVolumeService(BaseVolumeService):
     @implement(event_pattern="provider.storage.volumes.delete",
                priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
     def _delete(self, volume_id):
-        vol = self.provider.get_resource('disks', volume_id)
-        zone_name = self.provider.parse_url(vol.get('zone')).parameters['zone']
+        if isinstance(volume_id, GCEVolume):
+            gce_vol = volume_id._volume
+        else:
+            gce_vol = self.provider.get_resource('disks', volume_id)
+        zone_name = (self.provider.parse_url(gce_vol.get('zone'))
+                         .parameters['zone'])
         (self._provider.gce_compute
                        .disks()
                        .delete(project=self.provider.project_name,
                                zone=zone_name,
-                               disk=vol.get('name'))
+                               disk=gce_vol.get('name'))
                        .execute())
 
 
@@ -1139,7 +1179,15 @@ class GCESnapshotService(BaseSnapshotService):
 
     @implement(event_pattern="provider.storage.snapshots.find",
                priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
-    def _find(self, label, limit=None, marker=None):
+    def _find(self, limit=None, marker=None, **kwargs):
+        label = kwargs.pop('label', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
+
         filtr = 'labels.cblabel eq ' + label
         max_result = limit if limit is not None and limit < 500 else 500
         response = (self.provider
@@ -1207,7 +1255,10 @@ class GCESnapshotService(BaseSnapshotService):
     @implement(event_pattern="provider.storage.snapshots.delete",
                priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
     def _delete(self, snapshot_id):
-        gce_snap = self.provider.get_resource('snapshots', snapshot_id)
+        if isinstance(snapshot_id, GCESnapshot):
+            gce_snap = snapshot_id._snapshot
+        else:
+            gce_snap = self.provider.get_resource('snapshots', snapshot_id)
         (self.provider
              .gce_compute
              .snapshots()
@@ -1234,10 +1285,15 @@ class GCSBucketService(BaseBucketService):
 
     @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.
-        """
+    def _find(self, limit=None, marker=None, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'name'))
+
         buckets = [bucket for bucket in self if name in bucket.name]
         return ClientPagedResultList(self.provider, buckets, limit=limit,
                                      marker=marker)
@@ -1350,22 +1406,11 @@ class GCSBucketObjectService(BaseBucketObjectService):
                                      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)
-
+    def find(self, bucket, limit=None, marker=None, **kwargs):
         filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        matches = cb_helpers.generic_find(filters, kwargs, bucket.objects)
         return ClientPagedResultList(self._provider, list(matches),
-                                     limit=None, marker=None)
+                                     limit=limit, marker=marker)
 
     def _create_object_with_media_body(self, bucket, name, media_body):
         response = (self.provider

+ 28 - 15
cloudbridge/cloud/providers/openstack/services.py

@@ -38,6 +38,7 @@ 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 InvalidParamException
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 from cloudbridge.cloud.interfaces.resources import Network
@@ -169,8 +170,9 @@ class OpenStackKeyPairService(BaseKeyPairService):
 
         # 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'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'name'))
 
         keypairs = self.provider.nova.keypairs.findall(name=name)
         results = [OpenStackKeyPair(self.provider, kp)
@@ -314,8 +316,9 @@ class OpenStackVolumeService(BaseVolumeService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         log.debug("Searching for an OpenStack Volume with the label %s", label)
         search_opts = {'name': label}
@@ -354,7 +357,10 @@ class OpenStackVolumeService(BaseVolumeService):
     @implement(event_pattern="provider.storage.volumes.delete",
                priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
     def _delete(self, volume_id):
-        os_vol = self.provider.cinder.volumes.get(volume_id)
+        if isinstance(volume_id, OpenStackVolume):
+            os_vol = volume_id._volume
+        else:
+            os_vol = self.provider.cinder.volumes.get(volume_id)
         os_vol.delete()
 
 
@@ -381,8 +387,9 @@ class OpenStackSnapshotService(BaseSnapshotService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         search_opts = {'name': label,  # TODO: Cinder is ignoring name
                        'limit': oshelpers.os_result_limit(self.provider),
@@ -421,7 +428,10 @@ class OpenStackSnapshotService(BaseSnapshotService):
     @implement(event_pattern="provider.storage.snapshots.delete",
                priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
     def _delete(self, snapshot_id):
-        os_snap = self.provider.cinder.volume_snapshots.get(snapshot_id)
+        if isinstance(snapshot_id, OpenStackSnapshot):
+            os_snap = snapshot_id._snapshot
+        else:
+            os_snap = self.provider.cinder.volume_snapshots.get(snapshot_id)
         os_snap.delete()
 
 
@@ -454,8 +464,9 @@ class OpenStackBucketService(BaseBucketService):
 
         # 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'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'name'))
         _, container_list = self.provider.swift.get_account()
         cb_buckets = [OpenStackBucket(self.provider, c)
                       for c in container_list
@@ -757,8 +768,9 @@ class OpenStackInstanceService(BaseInstanceService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         search_opts = {'name': label}
         cb_insts = [
@@ -918,8 +930,9 @@ class OpenStackNetworkService(BaseNetworkService):
 
         # All kwargs should have been popped at this time.
         if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
 
         log.debug("Searching for OpenStack Network with label: %s", label)
         networks = [OpenStackNetwork(self.provider, network)
@@ -1018,7 +1031,7 @@ class OpenStackSubnetService(BaseSubnetService):
             sn = self.provider.networking.subnets.create(
                 label=OpenStackSubnet.CB_DEFAULT_SUBNET_LABEL,
                 cidr_block=OpenStackSubnet.CB_DEFAULT_SUBNET_IPV4RANGE,
-                network=net)
+                network=net, zone=zone)
             router = self.provider.networking.routers.get_or_create_default(
                 net)
             router.attach_subnet(sn)

+ 2 - 2
docs/api_docs/cloud/services.rst

@@ -55,7 +55,7 @@ SubnetService
 
 FloatingIPService
 -----------------
-.. autoclass:: cloudbridge.cloud.interfaces.services.FloatingIPService
+.. autoclass:: cloudbridge.cloud.interfaces.resources.FloatingIPContainer
     :members:
 
 RouterService
@@ -65,7 +65,7 @@ RouterService
 
 GatewayService
 -----------------
-.. autoclass:: cloudbridge.cloud.interfaces.services.GatewayService
+.. autoclass:: cloudbridge.cloud.interfaces.resources.GatewayContainer
     :members:
 
 BucketService

+ 4 - 2
docs/conf.py

@@ -121,7 +121,9 @@ html_theme = 'sphinx_rtd_theme'
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+html_theme_options = {
+    'style_external_links': True
+}
 
 # Add any paths that contain custom themes here, relative to this directory.
 #html_theme_path = []
@@ -146,7 +148,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+# html_static_path = ['_static']
 
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied

Разница между файлами не показана из-за своего большого размера
+ 183 - 16
docs/extras/_images/object_relationships_detailed.svg


+ 1 - 1
docs/getting_started.rst

@@ -214,7 +214,7 @@ listed in order to help map each resource with the service that handles it.
 Note that labeled resources allow to find by label, while unlabeled
 resources find by name or their special properties (eg: public_ip for
 floating IPs). For more detailed information on the types of resources and
-their provider mappings, see :doc:`topics/resource_types_and_mappings`.
+their provider mappings, see :doc:`topics/resource_types_and_mapping`.
 
 .. code-block:: python
 

+ 4 - 1
docs/topics/aws_mapping.rst

@@ -1,3 +1,6 @@
+Detailed AWS Type and Resource Mappings
+=======================================
+
 AWS Dashboard
 -------------
 AWS has a particular dashboard as resources are found within different
@@ -88,7 +91,7 @@ web portal.
    resources in AWS. By default, this label will appear in the first
    column.
 
-.. figure:: captures/az-ami-dash.png
+.. figure:: captures/aws-ami-dash.png
    :alt: name, ID, and label properties for AWS EC2 AMIs
 
    When an AWS resource allows for an unchangeable name, the CloudBridge

+ 7 - 3
docs/topics/azure_mapping.rst

@@ -1,3 +1,6 @@
+Detailed Azure Type and Resource Mappings
+=========================================
+
 Azure - Labeled Resources
 -------------------------
 +---------------------------------------+------------------------+-------+------------------------+------------------------------------+
@@ -106,8 +109,9 @@ It is also important to note that some of these resources are paid and
 required a plan to use, while others are free but likewise require accepting
 certain terms before being used. These plans and terms are passed and
 accepted silently by CloudBridge in order to keep the code cloud-independent.
-We therefore encourage using the `marketplace website<https://azuremarketplace.microsoft.com/en-us>`_
-to view image and plan details before using them in CloudBridge.
+We therefore encourage using the
+`marketplace website <https://azuremarketplace.microsoft.com/en-us>`_
+to view the images and plan details before using them in CloudBridge.
 
 Additionally, Subnets are a particular resource in Azure because they are
 not simply found in the Resource Group like most resources, but are rather
@@ -214,4 +218,4 @@ Finally, Firewall Rules in Azure differ from traditional unlabeled
 resources by the fact that they do not take a `name` parameter at creation.
 These rules can be found within each Firewall (i.e. Security Group) in the
 Azure web portal, and will have an automatically generated `name` of the form
-'cb-rule-[int]'.
+'cb-rule-[int]'.

+ 1 - 1
docs/topics/object_storage.rst

@@ -1,5 +1,5 @@
 Working with object storage
-==========================
+===========================
 Object storage provides a simple way to store and retrieve large amounts of
 unstructured data over HTTP. Object Storage is also referred to as Blob (Binary
 Large OBject) Storage by Azure, and Simple Storage Service (S3) by Amazon.

+ 22 - 19
docs/topics/os_mapping.rst

@@ -1,5 +1,8 @@
+Detailed OpenStack Type and Resource Mappings
+=============================================
+
 OpenStack - Labeled Resources
------------------------
+-----------------------------
 +------------------------+------------------------+-----------+----------------+----------+
 | Labeled Resource       | OS Resource Type       | CB ID     | CB Name        | CB Label |
 +========================+========================+===========+================+==========+
@@ -22,32 +25,32 @@ OpenStack - Labeled Resources
 
 The resources listed above are labeled, they thus have both the `name` and
 `label` properties in CloudBridge. These resources require a mandatory `label`
-parameter at creation. For all labeled resources, the `label` property in OpenStack
-maps to the Name attribute. However, unlike in Azure or AWS, no resource has
-an unchangeable name by which to identify it in our OpenStack implementation.
-The `name` property will therefore map to the ID, preserving its role as an unchangeable 
-identifier even though not easily readable in this context. Finally, labeled resources
-support a `label` parameter for the `find` method in their corresponding services.
-The below screenshots will help map these properties to OpenStack objects in the
-web portal.
-Additionally, although OpenStack Security Groups are not associated with a
-specific network, such an association is done in CloudBridge, due to its
-necessity in AWS. As such, the VMFirewall creation method requires a
-`network` parameter and the association is accomplished in OpenStack through
-the description, by appending the following string to the user-provided description
-(if any) at creation: "[CB-AUTO-associated-network-id: associated_net_id]"
+parameter at creation. For all labeled resources, the `label` property in
+OpenStack maps to the Name attribute. However, unlike in Azure or AWS, no
+resource has an unchangeable name by which to identify it in our OpenStack
+implementation. The `name` property will therefore map to the ID, preserving
+its role as an unchangeable identifier even though not easily readable in this
+context. Finally, labeled resources support a `label` parameter for the `find`
+method in their corresponding services. The below screenshots will help map
+these properties to OpenStack objects in the web portal. Additionally, although
+OpenStack Security Groups are not associated with a specific network, such an
+association is done in CloudBridge, due to its necessity in AWS. As such, the
+VMFirewall creation method requires a `network` parameter and the association
+is accomplished in OpenStack through the description, by appending the
+following string to the user-provided description (if any) at creation:
+"[CB-AUTO-associated-network-id: associated_net_id]"
 
 .. figure:: captures/os-instance-dash.png
    :alt: name, ID, and label properties for OS Instances
 
    The CloudBridge `name` and `ID` properties map to the unchangeable
    resource ID in OpenStack as resources do not allow for an unchangeable
-   name. The `label` property maps to the 'Name' for all resources in 
+   name. The `label` property maps to the 'Name' for all resources in
    OpenStack. By default, this label will appear in the first column.
 
 
 OpenStack - Unlabeled Resources
----------------------------
+-------------------------------
 +-----------------------+------------------------+-------+---------+----------+
 | Unlabeled Resource    | OS Resource Type       | CB ID | CB Name | CB Label |
 +=======================+========================+=======+=========+==========+
@@ -76,7 +79,7 @@ services.
 
 
 OpenStack - Special Unlabeled Resources
------------------------------------
+---------------------------------------
 +--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
 | Unlabeled Resource       | OS Resource Type       | CB ID | CB Name                                                                | CB Label |
 +==========================+========================+=======+========================================================================+==========+
@@ -105,4 +108,4 @@ Finally, Firewall Rules in OpenStack differ from traditional unlabeled resources
 by the fact that they do not take a `name` parameter at creation, and the
 `name` property is automatically generated from the rule's properties, as
 shown above. These rules can be found within each Firewall (i.e. Security
-Group) in the web portal, and will not have any name in the OpenStack dashboard.
+Group) in the web portal, and will not have any name in the OpenStack dashboard.

+ 1 - 0
docs/topics/overview.rst

@@ -14,4 +14,5 @@ Introductions to all the key parts of CloudBridge you'll need to know:
     Paging and iteration <paging_and_iteration.rst>
     Using block storage <block_storage.rst>
     Using object storage <object_storage.rst>
+    Resource types and mapping <resource_types_and_mapping.rst>
     Troubleshooting <troubleshooting.rst>

+ 6 - 6
docs/topics/provider_development.rst

@@ -56,8 +56,8 @@ You should see the tests fail with the following message:
 
 .. code-block:: bash
 
-    TypeError: Can't instantiate abstract class GCECloudProvider with abstract
-    methods storage, compute, security, network
+    "TypeError: Can't instantiate abstract class GCECloudProvider with abstract
+    methods storage, compute, security, network."
 
 6. Therefore, our next step is to implement these methods. We can start off by
 implementing these methods in ``provider.py`` and raising a
@@ -125,8 +125,8 @@ tests to fail:
 
 .. code-block:: bash
 
-    TypeError: Can't instantiate abstract class GCESecurityService with abstract
-    methods key_pairs, security_groups
+    "TypeError: Can't instantiate abstract class GCESecurityService with abstract
+    methods key_pairs, security_groups."
 
 The Abstract Base Classes are doing their job and flagging all methods that
 need to be implemented.
@@ -180,8 +180,8 @@ Once again, running the tests will complain of missing methods:
 
 .. code-block:: bash
 
-    TypeError: Can't instantiate abstract class GCEKeyPairService with abstract
-    methods create, find, get, list
+    "TypeError: Can't instantiate abstract class GCEKeyPairService with abstract
+    methods create, find, get, list."
 
 11. Keep implementing the methods till the security service works, and the
 tests pass.

+ 29 - 18
docs/topics/resource_types_and_mapping.rst

@@ -10,30 +10,33 @@ providers' resources and features. Notably, in order to create a robust and
 conceptually consistent cross-cloud library, resources were separated into
 `labeled` and `unlabeled resources,` and were given three main properties:
 `ID`, `name`, and `label`.
+
 The `ID` corresponds to a unique identifier that can be reliably used to
 reference a resource. Users can safely use an ID knowing that it will always
 point to the same resource. All resources have an `ID` property, thus making
-it the recommended oproperty for reliably identifying a resource.
+it the recommended property for reliably identifying a resource.
+
 The `label` property, conversely, is a modifiable value that does not need
-to be unique. Unlike the name property, it is not used to identify a
+to be unique. Unlike the `name` property, it is not used to identify a
 particular resource, but rather label a resource for easier distinction.
-Only labeled resources have the label property, and these resources require
+Only labeled resources have the `label` property, and these resources require
 a `label` parameter be set at creation time.
-The `name` property corresponds to an unchangeable and unique designation for
-a particular resource. This property is meant to be, in some ways, a more
+
+The `name` property corresponds to an unchangeable and unique designation for a
+particular resource. This property is meant to be, in some ways, a more
 human-readable identifier. Thus, when no conceptually comparable property
-exists for a given resource in a particular provider, the ID is returned
-instead, as is the case for all OpenStack and some AWS resources. Given the 
-discrepancy between providers, using the `name` property is not advisable 
-for cross-cloud usage of the library. Labeled resources will use the label
-given at creation as a prefix to the set name, when this property is separable
-from the ID as is the case in Azure and some AWS resources. Finally, unlabeled
-resources will always support a `name`, and some unlabeled resources will require
-a name parameter at creation. Below is a list of all resources classified by
-whether they support a `label` property.
+exists for a given resource in a particular provider, the `ID` is returned
+instead, as is the case for all OpenStack and some AWS resources. Given the
+discrepancy between providers, using the `name` property is not advisable for
+cross-cloud usage of the library. Labeled resources will use the label given at
+creation as a prefix to the set `name`, when this property is separable from
+the `ID` as is the case in Azure and some AWS resources. Finally, unlabeled
+resources will always support a `name`, and some unlabeled resources will
+require a `name` parameter at creation. Below is a list of all resources
+classified by whether they support a `label` property.
 
 +-------------------+---------------------+
-| Labeled Resources | Unlabeled Resources | 
+| Labeled Resources | Unlabeled Resources |
 +===================+=====================+
 | Instance          | Key Pair            |
 +-------------------+---------------------+
@@ -60,6 +63,14 @@ properties to provider objects, as well as some useful dashboard navigation.
 These sections will thus present summary tables delineating the different types of
 CloudBridge resources, as well as present some design decisions made to
 preserve consistency across providers:
--`Detailed Azure Mappings <azure_mapping.html>`_
--`Detailed AWS Mappings <aws_mapping.html>`_
--`Detailed OpenStack Mappings <os_mapping.html>`_
+
+.. toctree::
+   :maxdepth: 1
+
+   Detailed AWS Mappings <aws_mapping.rst>
+   Detailed Azure Mappings <azure_mapping.rst>
+   Detailed OpenStack Mappings <os_mapping.rst>
+
+.. - `Detailed Azure Mappings <azure_mapping.html>`_
+.. - `Detailed AWS Mappings <aws_mapping.html>`_
+.. - `Detailed OpenStack Mappings <os_mapping.html>`_

+ 2 - 1
test/helpers/standard_interface_tests.py

@@ -11,6 +11,7 @@ import tenacity
 
 from cloudbridge.cloud.interfaces.exceptions \
     import InvalidNameException
+from cloudbridge.cloud.interfaces.exceptions import InvalidParamException
 from cloudbridge.cloud.interfaces.resources import LabeledCloudResource
 from cloudbridge.cloud.interfaces.resources import ObjectLifeCycleMixin
 from cloudbridge.cloud.interfaces.resources import ResultList
@@ -100,7 +101,7 @@ def check_find_non_existent(test, service, obj):
         find_objs = service.find(label="random_imagined_obj_name", **args)
     else:
         find_objs = service.find(name="random_imagined_obj_name")
-    with test.assertRaises(TypeError):
+    with test.assertRaises(InvalidParamException):
         service.find(notaparameter="random_imagined_obj_name")
     test.assertTrue(
         len(find_objs) == 0,

+ 3 - 0
test/test_event_system.py

@@ -29,6 +29,9 @@ class EventSystemTestCase(unittest.TestCase):
         dispatcher = SimpleEventDispatcher()
         handler = dispatcher.observe(event_pattern=EVENT_NAME, priority=1000,
                                      callback=my_callback)
+        assert handler.event_pattern == EVENT_NAME
+        assert handler.priority == 1000
+        assert handler.callback == my_callback
         self.assertIsInstance(handler, EventHandler)
         result = dispatcher.dispatch(self, EVENT_NAME, 'first_pos_arg',
                                      a_keyword_arg='another_thing')

Некоторые файлы не были показаны из-за большого количества измененных файлов