فهرست منبع

Merge pull request #33 from gvlproject/nuwan_gce_changes

Merge latest master into GCE + other fixes
Nuwan Goonasekera 9 سال پیش
والد
کامیت
80e2e7bb15
48فایلهای تغییر یافته به همراه1458 افزوده شده و 915 حذف شده
  1. 1 0
      .gitignore
  2. 8 5
      .travis.yml
  3. 43 1
      CHANGELOG.rst
  4. 1 2
      README.rst
  5. 3 2
      cloudbridge/__init__.py
  6. 11 3
      cloudbridge/cloud/base/provider.py
  7. 10 5
      cloudbridge/cloud/base/resources.py
  8. 1 0
      cloudbridge/cloud/base/services.py
  9. 13 12
      cloudbridge/cloud/factory.py
  10. 94 14
      cloudbridge/cloud/interfaces/resources.py
  11. 55 19
      cloudbridge/cloud/interfaces/services.py
  12. 13 11
      cloudbridge/cloud/providers/aws/provider.py
  13. 82 28
      cloudbridge/cloud/providers/aws/resources.py
  14. 79 203
      cloudbridge/cloud/providers/aws/services.py
  15. 15 5
      cloudbridge/cloud/providers/gce/helpers.py
  16. 4 2
      cloudbridge/cloud/providers/gce/provider.py
  17. 12 9
      cloudbridge/cloud/providers/gce/services.py
  18. 1 0
      cloudbridge/cloud/providers/openstack/helpers.py
  19. 69 9
      cloudbridge/cloud/providers/openstack/provider.py
  20. 152 57
      cloudbridge/cloud/providers/openstack/resources.py
  21. 43 45
      cloudbridge/cloud/providers/openstack/services.py
  22. 19 0
      docs/api_docs/cloud/exceptions.rst
  23. 0 15
      docs/api_docs/cloud/resources.rst
  24. 1 0
      docs/api_docs/ref.rst
  25. 5 1
      docs/getting_started.rst
  26. 2 1
      docs/topics/contributor_guide.rst
  27. 5 4
      docs/topics/launch.rst
  28. 4 4
      docs/topics/networking.rst
  29. 21 0
      docs/topics/release_process.rst
  30. 48 14
      docs/topics/setup.rst
  31. 23 10
      docs/topics/testing.rst
  32. 18 11
      setup.py
  33. 4 59
      test/__init__.py
  34. BIN
      test/fixtures/logo.jpg
  35. 67 111
      test/helpers.py
  36. 31 15
      test/test_block_store_service.py
  37. 2 1
      test/test_cloud_factory.py
  38. 3 6
      test/test_cloud_helpers.py
  39. 189 96
      test/test_compute_service.py
  40. 38 27
      test/test_image_service.py
  41. 8 8
      test/test_instance_types_service.py
  42. 6 7
      test/test_interface.py
  43. 64 31
      test/test_network_service.py
  44. 4 6
      test/test_object_life_cycle.py
  45. 90 10
      test/test_object_store_service.py
  46. 8 7
      test/test_region_service.py
  47. 65 33
      test/test_security_service.py
  48. 23 6
      tox.ini

+ 1 - 0
.gitignore

@@ -57,3 +57,4 @@ docs/_build/
 target/
 target/
 
 
 *.DS_Store
 *.DS_Store
+/venv/

+ 8 - 5
.travis.yml

@@ -1,12 +1,15 @@
 language: python
 language: python
-python: 3.5
+python: 3.6
 os:
 os:
   - linux
   - linux
 #  - osx
 #  - osx
 env:
 env:
-  - TOX_ENV=py27
-  - TOX_ENV=py35
-  - TOX_ENV=pypy
+  - TOX_ENV=py27-aws
+  - TOX_ENV=py27-openstack
+  - TOX_ENV=py36-aws
+  - TOX_ENV=py36-openstack
+  - TOX_ENV=pypy-aws
+  - TOX_ENV=pypy-openstack
 matrix:
 matrix:
   fast_finish: true
   fast_finish: true
   allow_failures:
   allow_failures:
@@ -19,4 +22,4 @@ script:
   - tox -e $TOX_ENV
   - tox -e $TOX_ENV
 after_success:
 after_success:
   - coveralls
   - coveralls
-  - codecov 
+  - codecov

+ 43 - 1
CHANGELOG.rst

@@ -1,4 +1,46 @@
-0.1.1 - Aug 10, 2016.
+0.3.2 - June 10, 2017. (sha f07f3cbd758a0872b847b5537d9073c90f87c24d)
+-------
+
+* Patch release to support files>5GB with OpenStack (thanks @MartinPaulo)
+* Misc bug fixes
+
+0.3.1 - April 18, 2017. (sha f36a462e886d8444cb2818f6573677ecf0565315)
+-------
+
+* Patch for binary file handling in openstack
+
+0.3.0 - April 11, 2017. (sha 13539ccda9e4809082796574d18b1b9bb3f2c624)
+-------
+
+* Reworked test framework to rely on tox's test generation features. This
+  allows for individual test cases to be run on a per provider basis.
+* Added more OpenStack swift config options (OS_AUTH_TOKEN and OS_STORAGE_URL)
+* Added supports for accessing EC2 containers with restricted permissions.
+* Removed exists() method from object store interface. Use get()==None check
+  instead.
+* New method (img.min_disk) for geting size of machine image.
+* Test improvements (flake8 during build, more tests)
+* Misc bug fixes and improvements
+* Changed library to beta state
+* General documentation updates (testing, release process)
+
+0.2.0 - March 23, 2017. (sha a442d96b829ea2c721728520b01981fa61774625)
+-------
+
+* Reworked the instance launch method to require subnet vs. network. This
+  removed the option of adding network interface to a launch config object.
+* Added object store methods: upload from file path, list objects with a
+  prefix, check if an object exists, (AWS only) get an accessible URL for an
+  object (thanks @VJalili)
+* Modified `get_ec2_credentials()` method to `get_or_create_ec2_credentials()`
+* Added an option to read provider config values from a file
+  (`~/.cloudbridge` or `/etc/cloudbridge`)
+* Replaced py35 with py36 for running tests
+* Added logging configuration for the library
+* General documentation updates
+
+
+0.1.1 - Aug 10, 2016. (sha 0122fb1173c88ae64e40140ffd35ff3797e9e4ad)
 -------
 -------
 
 
 * For AWS, always launch instances into private networking (i.e., VPC)
 * For AWS, always launch instances into private networking (i.e., VPC)

+ 1 - 2
README.rst

@@ -1,7 +1,6 @@
 CloudBridge aims to provide a simple layer of abstraction over
 CloudBridge aims to provide a simple layer of abstraction over
 different cloud providers, reducing or eliminating the need to write
 different cloud providers, reducing or eliminating the need to write
-conditional code for each cloud. It is currently under development and is in
-an Alpha state.
+conditional code for each cloud.
 
 
 .. image:: https://landscape.io/github/gvlproject/cloudbridge/master/landscape.svg?style=flat
 .. image:: https://landscape.io/github/gvlproject/cloudbridge/master/landscape.svg?style=flat
    :target: https://landscape.io/github/gvlproject/cloudbridge/master
    :target: https://landscape.io/github/gvlproject/cloudbridge/master

+ 3 - 2
cloudbridge/__init__.py

@@ -2,7 +2,7 @@
 import logging
 import logging
 
 
 # Current version of the library
 # Current version of the library
-__version__ = '0.1.1'
+__version__ = '0.3.2'
 
 
 
 
 def get_version():
 def get_version():
@@ -32,6 +32,7 @@ class NullHandler(logging.Handler):
         """Don't emit a log."""
         """Don't emit a log."""
         pass
         pass
 
 
+
 TRACE = 5  # Lower than debug which is 10
 TRACE = 5  # Lower than debug which is 10
 
 
 
 
@@ -46,13 +47,13 @@ class CBLogger(logging.Logger):
         """Add ``trace`` log level."""
         """Add ``trace`` log level."""
         self.log(TRACE, msg, *args, **kwargs)
         self.log(TRACE, msg, *args, **kwargs)
 
 
+
 # By default, do not force any logging by the library. If you want to see the
 # By default, do not force any logging by the library. If you want to see the
 # log messages in your scripts, add the following to the top of your script:
 # log messages in your scripts, add the following to the top of your script:
 #   import cloudbridge
 #   import cloudbridge
 #   cloudbridge.set_stream_logger(__name__)
 #   cloudbridge.set_stream_logger(__name__)
 #   OR
 #   OR
 #   cloudbridge.set_file_logger(__name__, '/tmp/cb.log')
 #   cloudbridge.set_file_logger(__name__, '/tmp/cb.log')
-
 default_format_string = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
 default_format_string = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
 logging.setLoggerClass(CBLogger)
 logging.setLoggerClass(CBLogger)
 logging.addLevelName(TRACE, "TRACE")
 logging.addLevelName(TRACE, "TRACE")

+ 11 - 3
cloudbridge/cloud/base/provider.py

@@ -1,14 +1,16 @@
 """Base implementation of a provider interface."""
 """Base implementation of a provider interface."""
+import functools
 import os
 import os
+from os.path import expanduser
 try:
 try:
     from configparser import SafeConfigParser
     from configparser import SafeConfigParser
 except ImportError:  # Python 2
 except ImportError:  # Python 2
     from ConfigParser import SafeConfigParser
     from ConfigParser import SafeConfigParser
-from os.path import expanduser
 
 
 from cloudbridge.cloud.interfaces import CloudProvider
 from cloudbridge.cloud.interfaces import CloudProvider
-from cloudbridge.cloud.interfaces.resources import Configuration
 from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
+from cloudbridge.cloud.interfaces.resources import Configuration
+
 
 
 DEFAULT_RESULT_LIMIT = 50
 DEFAULT_RESULT_LIMIT = 50
 DEFAULT_WAIT_TIMEOUT = 600
 DEFAULT_WAIT_TIMEOUT = 600
@@ -95,6 +97,10 @@ class BaseCloudProvider(CloudProvider):
             raise ProviderConnectionException(
             raise ProviderConnectionException(
                 "Authentication with cloud provider failed: %s" % (e,))
                 "Authentication with cloud provider failed: %s" % (e,))
 
 
+    def _deepgetattr(self, obj, attr):
+        """Recurses through an attribute chain to get the ultimate value."""
+        return functools.reduce(getattr, attr.split('.'), obj)
+
     def has_service(self, service_type):
     def has_service(self, service_type):
         """
         """
         Checks whether this provider supports a given service.
         Checks whether this provider supports a given service.
@@ -106,10 +112,12 @@ class BaseCloudProvider(CloudProvider):
         :return: ``True`` if the service type is supported.
         :return: ``True`` if the service type is supported.
         """
         """
         try:
         try:
-            if getattr(self, service_type):
+            if self._deepgetattr(self, service_type):
                 return True
                 return True
         except AttributeError:
         except AttributeError:
             pass  # Undefined service type
             pass  # Undefined service type
+        except NotImplementedError:
+            pass  # service not implemented
         return False
         return False
 
 
     def _get_config_value(self, key, default_value):
     def _get_config_value(self, key, default_value):

+ 10 - 5
cloudbridge/cloud/base/resources.py

@@ -9,14 +9,14 @@ import os
 import shutil
 import shutil
 import time
 import time
 
 
-import six
-
 from cloudbridge.cloud.interfaces.exceptions \
 from cloudbridge.cloud.interfaces.exceptions \
     import InvalidConfigurationException
     import InvalidConfigurationException
+from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
 from cloudbridge.cloud.interfaces.resources import Bucket
 from cloudbridge.cloud.interfaces.resources import Bucket
 from cloudbridge.cloud.interfaces.resources import BucketObject
 from cloudbridge.cloud.interfaces.resources import BucketObject
 from cloudbridge.cloud.interfaces.resources import CloudResource
 from cloudbridge.cloud.interfaces.resources import CloudResource
+from cloudbridge.cloud.interfaces.resources import FloatingIP
 from cloudbridge.cloud.interfaces.resources import Instance
 from cloudbridge.cloud.interfaces.resources import Instance
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import InstanceType
 from cloudbridge.cloud.interfaces.resources import InstanceType
@@ -30,18 +30,17 @@ from cloudbridge.cloud.interfaces.resources import ObjectLifeCycleMixin
 from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
 from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
 from cloudbridge.cloud.interfaces.resources import PlacementZone
 from cloudbridge.cloud.interfaces.resources import PlacementZone
 from cloudbridge.cloud.interfaces.resources import Region
 from cloudbridge.cloud.interfaces.resources import Region
-from cloudbridge.cloud.interfaces.resources import Router
 from cloudbridge.cloud.interfaces.resources import ResultList
 from cloudbridge.cloud.interfaces.resources import ResultList
+from cloudbridge.cloud.interfaces.resources import Router
 from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SecurityGroupRule
 from cloudbridge.cloud.interfaces.resources import SecurityGroupRule
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import Subnet
 from cloudbridge.cloud.interfaces.resources import Subnet
-from cloudbridge.cloud.interfaces.resources import FloatingIP
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import VolumeState
-from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 
 
+import six
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 
 
@@ -667,6 +666,9 @@ class BaseNetwork(BaseCloudResource, Network, BaseObjectLifeCycleMixin):
 
 
 class BaseSubnet(Subnet, BaseCloudResource):
 class BaseSubnet(Subnet, BaseCloudResource):
 
 
+    CB_DEFAULT_SUBNET_NAME = os.environ.get('CB_DEFAULT_SUBNET_NAME',
+                                            'CloudBridgeSubnet')
+
     def __init__(self, provider):
     def __init__(self, provider):
         super(BaseSubnet, self).__init__(provider)
         super(BaseSubnet, self).__init__(provider)
 
 
@@ -699,6 +701,9 @@ class BaseFloatingIP(FloatingIP, BaseCloudResource):
 
 
 class BaseRouter(Router, BaseCloudResource):
 class BaseRouter(Router, BaseCloudResource):
 
 
+    CB_DEFAULT_ROUTER_NAME = os.environ.get('CB_DEFAULT_ROUTER_NAME',
+                                            'CloudBridgeRouter')
+
     def __init__(self, provider):
     def __init__(self, provider):
         super(BaseRouter, self).__init__(provider)
         super(BaseRouter, self).__init__(provider)
 
 

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

@@ -16,6 +16,7 @@ from cloudbridge.cloud.interfaces.services import SecurityService
 from cloudbridge.cloud.interfaces.services import SnapshotService
 from cloudbridge.cloud.interfaces.services import SnapshotService
 from cloudbridge.cloud.interfaces.services import SubnetService
 from cloudbridge.cloud.interfaces.services import SubnetService
 from cloudbridge.cloud.interfaces.services import VolumeService
 from cloudbridge.cloud.interfaces.services import VolumeService
+
 from .resources import BasePageableObjectMixin
 from .resources import BasePageableObjectMixin
 
 
 
 

+ 13 - 12
cloudbridge/cloud/factory.py

@@ -1,11 +1,12 @@
-from cloudbridge.cloud import providers
-from cloudbridge.cloud.interfaces import CloudProvider
-from cloudbridge.cloud.interfaces import TestMockHelperMixin
-from collections import defaultdict
 import importlib
 import importlib
 import inspect
 import inspect
 import logging
 import logging
 import pkgutil
 import pkgutil
+from collections import defaultdict
+
+from cloudbridge.cloud import providers
+from cloudbridge.cloud.interfaces import CloudProvider
+from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
 
 
 
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
@@ -48,19 +49,19 @@ class CloudProviderFactory(object):
                 if issubclass(cls, TestMockHelperMixin):
                 if issubclass(cls, TestMockHelperMixin):
                     if self.provider_list.get(provider_id, {}).get(
                     if self.provider_list.get(provider_id, {}).get(
                             'mock_class'):
                             'mock_class'):
-                        log.warn("Mock provider with id: %s is already "
-                                 "registered. Overriding with class: %s",
-                                 provider_id, cls)
+                        log.warning("Mock provider with id: %s is already "
+                                    "registered. Overriding with class: %s",
+                                    provider_id, cls)
                     self.provider_list[provider_id]['mock_class'] = cls
                     self.provider_list[provider_id]['mock_class'] = cls
                 else:
                 else:
                     if self.provider_list.get(provider_id, {}).get('class'):
                     if self.provider_list.get(provider_id, {}).get('class'):
-                        log.warn("Provider with id: %s is already "
-                                 "registered. Overriding with class: %s",
-                                 provider_id, cls)
+                        log.warning("Provider with id: %s is already "
+                                    "registered. Overriding with class: %s",
+                                    provider_id, cls)
                     self.provider_list[provider_id]['class'] = cls
                     self.provider_list[provider_id]['class'] = cls
             else:
             else:
-                log.warn("Provider class: %s implements CloudProvider but"
-                         " does not define PROVIDER_ID. Ignoring...", cls)
+                log.warning("Provider class: %s implements CloudProvider but"
+                            " does not define PROVIDER_ID. Ignoring...", cls)
         else:
         else:
             log.debug("Class: %s does not implement the CloudProvider"
             log.debug("Class: %s does not implement the CloudProvider"
                       "  interface. Ignoring...", cls)
                       "  interface. Ignoring...", cls)

+ 94 - 14
cloudbridge/cloud/interfaces/resources.py

@@ -487,10 +487,6 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
     def terminate(self):
     def terminate(self):
         """
         """
         Permanently terminate this instance.
         Permanently terminate this instance.
-
-        :rtype: ``bool``
-        :return: ``True`` if the termination of the instance was successfully
-                 initiated; ``False`` otherwise.
         """
         """
         pass
         pass
 
 
@@ -584,6 +580,26 @@ class Instance(ObjectLifeCycleMixin, CloudResource):
         """
         """
         pass
         pass
 
 
+    @abstractmethod
+    def add_security_group(self, sg):
+        """
+        Add a security group to this instance
+
+        :type sg: ``SecurityGroup``
+        :param sg: The SecurityGroup to associate with the instance.
+        """
+        pass
+
+    @abstractmethod
+    def remove_security_group(self, sg):
+        """
+        Remove a security group from this instance
+
+        :type sg: ``SecurityGroup``
+        :param sg: The SecurityGroup to associate with the instance.
+        """
+        pass
+
 
 
 class MachineImageState(object):
 class MachineImageState(object):
 
 
@@ -746,6 +762,17 @@ class MachineImage(ObjectLifeCycleMixin, CloudResource):
         """
         """
         pass
         pass
 
 
+    @abstractproperty
+    def min_disk(self):
+        """
+        Returns the minimum size of the disk that's required to
+        boot this image (in GB)
+
+        :rtype: ``int``
+        :return: The minimum disk size needed by this image
+        """
+        pass
+
     @abstractmethod
     @abstractmethod
     def delete(self):
     def delete(self):
         """
         """
@@ -856,7 +883,7 @@ class Network(CloudResource):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create_subnet(self, cidr_block, name=None):
+    def create_subnet(self, cidr_block, name=None, zone=None):
         """
         """
         Create a new network subnet and associate it with this Network.
         Create a new network subnet and associate it with this Network.
 
 
@@ -868,6 +895,11 @@ class Network(CloudResource):
         :param name: An optional subnet name. The name will be set if the
         :param name: An optional subnet name. The name will be set if the
                      provider supports it.
                      provider supports it.
 
 
+        :type zone: ``str``
+        :param zone: Placement zone where to create the subnet. Some providers
+                     may not support subnet zones, in which case the value is
+                     ignored.
+
         :rtype: ``object`` of :class:`.Subnet`
         :rtype: ``object`` of :class:`.Subnet`
         :return:  A Subnet object
         :return:  A Subnet object
         """
         """
@@ -921,6 +953,18 @@ class Subnet(CloudResource):
         """
         """
         pass
         pass
 
 
+    @abstractproperty
+    def zone(self):
+        """
+        Placement zone of the subnet.
+
+        If the provider does not support subnet placement, return ``None``.
+
+        :rtype: :class:`.PlacementZone` object
+        :return: Placement zone of the subnet, or ``None`` if not defined.
+        """
+        pass
+
     @abstractmethod
     @abstractmethod
     def delete(self):
     def delete(self):
         """
         """
@@ -1826,8 +1870,8 @@ class SecurityGroup(CloudResource):
         Create a security group rule. If the rule already exists, simply
         Create a security group rule. If the rule already exists, simply
         returns it.
         returns it.
 
 
-        You need to pass in either ``src_group`` OR ``ip_protocol``,
-        ``from_port``, ``to_port``, and ``cidr_ip``. In other words, either
+        You need to pass in either ``src_group`` OR ``ip_protocol`` AND
+        ``from_port``, ``to_port``, ``cidr_ip``. In other words, either
         you are authorizing another group or you are authorizing some
         you are authorizing another group or you are authorizing some
         ip-based rule.
         ip-based rule.
 
 
@@ -1856,7 +1900,7 @@ class SecurityGroup(CloudResource):
         """
         """
         Get a security group rule with the specified parameters.
         Get a security group rule with the specified parameters.
 
 
-        You need to pass in either ``src_group`` OR ``ip_protocol``,
+        You need to pass in either ``src_group`` OR ``ip_protocol`` AND
         ``from_port``, ``to_port``, and ``cidr_ip``. Note that when retrieving
         ``from_port``, ``to_port``, and ``cidr_ip``. Note that when retrieving
         a group rule, this method will return only one rule although possibly
         a group rule, this method will return only one rule although possibly
         several rules exist for the group rule. In that case, use the
         several rules exist for the group rule. In that case, use the
@@ -2037,6 +2081,16 @@ class BucketObject(CloudResource):
         """
         """
         pass
         pass
 
 
+    @abstractmethod
+    def upload_from_file(self, path):
+        """
+        Store the contents of the file pointed by the "path" variable.
+
+        :type path: ``str``
+        :param path: Absolute path to the file to be uploaded to S3.
+        """
+        pass
+
     @abstractmethod
     @abstractmethod
     def delete(self):
     def delete(self):
         """
         """
@@ -2047,6 +2101,23 @@ class BucketObject(CloudResource):
         """
         """
         pass
         pass
 
 
+    @abstractmethod
+    def generate_url(self, expires_in=0):
+        """
+        Generate a URL to this object.
+
+        If the object is public, `expires_in` argument is not necessary, but if
+        the object is private, the lifetime of URL is set using `expires_in`
+        argument.
+
+        :type expires_in: ``int``
+        :param expires_in: Time to live of the generated URL in seconds.
+
+        :rtype: ``str``
+        :return: A URL to access the object.
+        """
+        pass
+
 
 
 class Bucket(PageableObjectMixin, CloudResource):
 class Bucket(PageableObjectMixin, CloudResource):
 
 
@@ -2073,12 +2144,12 @@ class Bucket(PageableObjectMixin, CloudResource):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def get(self, key):
+    def get(self, name):
         """
         """
         Retrieve a given object from this bucket.
         Retrieve a given object from this bucket.
 
 
-        :type key: ``str``
-        :param key: the identifier of the object to retrieve
+        :type name: ``str``
+        :param name: The identifier of the object to retrieve
 
 
         :rtype: :class:``.BucketObject``
         :rtype: :class:``.BucketObject``
         :return: The BucketObject or ``None`` if it cannot be found.
         :return: The BucketObject or ``None`` if it cannot be found.
@@ -2086,9 +2157,18 @@ class Bucket(PageableObjectMixin, CloudResource):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit=None, marker=None, prefix=None):
         """
         """
-        List all objects within this bucket.
+        List objects in this bucket.
+
+        :type limit: ``int``
+        :param limit: Maximum number of elements to return.
+
+        :type marker: ``int``
+        :param marker: Fetch results after this offset.
+
+        :type prefix: ``str``
+        :param prefix: Prefix criteria by which to filter listed objects.
 
 
         :rtype: :class:``.BucketObject``
         :rtype: :class:``.BucketObject``
         :return: List of all available BucketObjects within this bucket.
         :return: List of all available BucketObjects within this bucket.
@@ -2112,7 +2192,7 @@ class Bucket(PageableObjectMixin, CloudResource):
     @abstractmethod
     @abstractmethod
     def create_object(self, name):
     def create_object(self, name):
         """
         """
-        Creates a new object within this bucket.
+        Create a new object within this bucket.
 
 
         :rtype: :class:``.BucketObject``
         :rtype: :class:``.BucketObject``
         :return: The newly created bucket object
         :return: The newly created bucket object

+ 55 - 19
cloudbridge/cloud/interfaces/services.py

@@ -187,7 +187,12 @@ class InstanceService(PageableObjectMixin, CloudService):
                 print("Instance Data: {0}", instance)
                 print("Instance Data: {0}", instance)
 
 
         :type  limit: ``int``
         :type  limit: ``int``
-        :param limit: The maximum number of objects to return
+        :param limit: The maximum number of objects to return. Note that the
+                      maximum is not guaranteed to be honoured, and a lower
+                      maximum may be enforced depending on the provider. In
+                      such a case, the returned ResultList's is_truncated
+                      property can be used to determine whether more records
+                      are available.
 
 
         :type  marker: ``str``
         :type  marker: ``str``
         :param marker: The marker is an opaque identifier used to assist
         :param marker: The marker is an opaque identifier used to assist
@@ -200,7 +205,7 @@ class InstanceService(PageableObjectMixin, CloudService):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create(self, name, image, instance_type, network=None, zone=None,
+    def create(self, name, image, instance_type, subnet, zone=None,
                key_pair=None, security_groups=None, user_data=None,
                key_pair=None, security_groups=None, user_data=None,
                launch_config=None,
                launch_config=None,
                **kwargs):
                **kwargs):
@@ -218,33 +223,40 @@ class InstanceService(PageableObjectMixin, CloudService):
         :param instance_type: The InstanceType or name, specifying the size of
         :param instance_type: The InstanceType or name, specifying the size of
                               the instance to boot into
                               the instance to boot into
 
 
-        :type  network:  ``Network`` or ``str``
-        :param network:  The Network or an ID with which the instance should
-                         be associated. If no network was specified, this
-                         method will attempt to find a 'default' one and launch
-                         the instance using that network. A 'default' network
-                         is one tagged as such by the native API. If such tag
-                         or functionality does not exist, an attempt to create
-                         a new network (by default called 'CloudBridgeNet')
-                         will be made. If that falls through, an attempt will
-                         be made to launch the instance without specifying the
-                         network parameter (this is under the assumption the
-                         private networking functionality is not available on
-                         the provider).
+        :type  subnet:  ``Subnet`` or ``str``
+        :param subnet: The subnet object or a subnet string ID with which the
+                       instance should be associated. The subnet is a mandatory
+                       parameter, and must be provided when launching an
+                       instance.
+
+                       Note: Older clouds (with classic networking), may not
+                       have proper subnet support and are not guaranteed to
+                       work. Some providers (e.g. OpenStack) support a null
+                       value but the behaviour is implementation specific.
 
 
         :type  zone: ``Zone`` or ``str``
         :type  zone: ``Zone`` or ``str``
         :param zone: The Zone or its name, where the instance should be placed.
         :param zone: The Zone or its name, where the instance should be placed.
+                     This parameter is provided for legacy compatibility (with
+                     classic networks).
+
+                     The subnet's placement zone will take precedence over this
+                     parameter, but in its absence, this value will be used.
 
 
         :type  key_pair: ``KeyPair`` or ``str``
         :type  key_pair: ``KeyPair`` or ``str``
         :param key_pair: The KeyPair object or its name, to set for the
         :param key_pair: The KeyPair object or its name, to set for the
                          instance.
                          instance.
 
 
         :type  security_groups: A ``list`` of ``SecurityGroup`` objects or a
         :type  security_groups: A ``list`` of ``SecurityGroup`` objects or a
-                                list of ``str`` names
+                                list of ``str`` object IDs
         :param security_groups: A list of ``SecurityGroup`` objects or a list
         :param security_groups: A list of ``SecurityGroup`` objects or a list
-                                of ``SecurityGroup`` names, which should be
+                                of ``SecurityGroup`` IDs, which should be
                                 assigned to this instance.
                                 assigned to this instance.
 
 
+                                The security groups must be associated with the
+                                same network as the supplied subnet. Use
+                                ``network.security_groups`` to retrieve a list
+                                of security groups belonging to a network.
+
         :type  user_data: ``str``
         :type  user_data: ``str``
         :param user_data: An extra userdata object which is compatible with
         :param user_data: An extra userdata object which is compatible with
                           the provider.
                           the provider.
@@ -655,7 +667,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create(self, network_id, cidr_block, name=None):
+    def create(self, network_id, cidr_block, name=None, zone=None):
         """
         """
         Create a new subnet within the supplied network.
         Create a new subnet within the supplied network.
 
 
@@ -670,11 +682,35 @@ class SubnetService(PageableObjectMixin, CloudService):
         :param name: An optional subnet name. The name will be set if the
         :param name: An optional subnet name. The name will be set if the
                      provider supports it.
                      provider supports it.
 
 
+        :type zone: ``str``
+        :param zone: An optional placement zone for the subnet. Some providers
+                     may not support this, in which case the value is ignored.
+
         :rtype: ``object`` of :class:`.Subnet`
         :rtype: ``object`` of :class:`.Subnet`
         :return:  A Subnet object
         :return:  A Subnet object
         """
         """
         pass
         pass
 
 
+    @abstractmethod
+    def get_or_create_default(self, zone=None):
+        """
+        Return a default subnet for the account or create one if not found.
+
+        A default network is one marked as such by the provider or matches the
+        default name used by this library (e.g., CloudBridgeNet).
+
+        If this method creates a new subnet, it will create one in each zone
+        available from the provider.
+
+        :type zone: ``str``
+        :param zone: Placement zone where to look for the subnet. If not
+                     supplied, a subnet from random zone will be selected.
+
+        :rtype: ``object`` of :class:`.Subnet`
+        :return: A Subnet object
+        """
+        pass
+
     @abstractmethod
     @abstractmethod
     def delete(self, subnet):
     def delete(self, subnet):
         """
         """
@@ -970,7 +1006,7 @@ class SecurityGroupService(PageableObjectMixin, CloudService):
     @abstractmethod
     @abstractmethod
     def find(self, name, limit=None, marker=None):
     def find(self, name, limit=None, marker=None):
         """
         """
-        Get all security groups associated with your account.
+        Get security groups associated with your account filtered by name.
 
 
         :type name: str
         :type name: str
         :param name: The name of the security group to retrieve.
         :param name: The name of the security group to retrieve.

+ 13 - 11
cloudbridge/cloud/providers/aws/provider.py

@@ -1,6 +1,4 @@
-"""
-Provider implementation based on boto library for AWS-compatible clouds.
-"""
+"""Provider implementation based on boto library for AWS-compatible clouds."""
 
 
 import os
 import os
 
 
@@ -9,8 +7,8 @@ from boto.ec2.regioninfo import RegionInfo
 try:
 try:
     # These are installed only for the case of a dev instance
     # These are installed only for the case of a dev instance
     from httpretty import HTTPretty
     from httpretty import HTTPretty
-    from moto.ec2 import mock_ec2
-    from moto.s3 import mock_s3
+    from moto import mock_ec2
+    from moto import mock_s3
 except ImportError:
 except ImportError:
     # TODO: Once library logging is configured, change this
     # TODO: Once library logging is configured, change this
     print("[aws provider] moto library not available!")
     print("[aws provider] moto library not available!")
@@ -28,6 +26,8 @@ from .services import AWSSecurityService
 class AWSCloudProvider(BaseCloudProvider):
 class AWSCloudProvider(BaseCloudProvider):
 
 
     PROVIDER_ID = 'aws'
     PROVIDER_ID = 'aws'
+    AWS_INSTANCE_DATA_DEFAULT_URL = "https://d168wakzal7fp0.cloudfront.net/" \
+                                    "aws_instance_data.json"
 
 
     def __init__(self, config):
     def __init__(self, config):
         super(AWSCloudProvider, self).__init__(config)
         super(AWSCloudProvider, self).__init__(config)
@@ -38,6 +38,7 @@ class AWSCloudProvider(BaseCloudProvider):
             'aws_access_key', os.environ.get('AWS_ACCESS_KEY', None))
             'aws_access_key', os.environ.get('AWS_ACCESS_KEY', None))
         self.s_key = self._get_config_value(
         self.s_key = self._get_config_value(
             'aws_secret_key', os.environ.get('AWS_SECRET_KEY', None))
             'aws_secret_key', os.environ.get('AWS_SECRET_KEY', None))
+        self.session_token = self._get_config_value('aws_session_token', None)
         # EC2 connection fields
         # EC2 connection fields
         self.ec2_is_secure = self._get_config_value('ec2_is_secure', True)
         self.ec2_is_secure = self._get_config_value('ec2_is_secure', True)
         self.region_name = self._get_config_value(
         self.region_name = self._get_config_value(
@@ -133,6 +134,7 @@ class AWSCloudProvider(BaseCloudProvider):
         vpc_conn = boto.connect_vpc(
         vpc_conn = boto.connect_vpc(
             aws_access_key_id=self.a_key,
             aws_access_key_id=self.a_key,
             aws_secret_access_key=self.s_key,
             aws_secret_access_key=self.s_key,
+            security_token=self.session_token,
             is_secure=self.ec2_is_secure,
             is_secure=self.ec2_is_secure,
             region=r,
             region=r,
             port=self.ec2_port,
             port=self.ec2_port,
@@ -147,6 +149,7 @@ class AWSCloudProvider(BaseCloudProvider):
         """
         """
         s3_conn = boto.connect_s3(aws_access_key_id=self.a_key,
         s3_conn = boto.connect_s3(aws_access_key_id=self.a_key,
                                   aws_secret_access_key=self.s_key,
                                   aws_secret_access_key=self.s_key,
+                                  security_token=self.session_token,
                                   is_secure=self.s3_is_secure,
                                   is_secure=self.s3_is_secure,
                                   port=self.s3_port,
                                   port=self.s3_port,
                                   host=self.s3_host,
                                   host=self.s3_host,
@@ -170,9 +173,9 @@ class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
         self.s3mock = mock_s3()
         self.s3mock = mock_s3()
         self.s3mock.start()
         self.s3mock.start()
         HTTPretty.register_uri(
         HTTPretty.register_uri(
-            method="GET",
-            uri="https://d168wakzal7fp0.cloudfront.net/aws_instance_data.json",
-            body="""
+            HTTPretty.GET,
+            self.AWS_INSTANCE_DATA_DEFAULT_URL,
+            body=u"""
 [
 [
   {
   {
     "family": "General Purpose",
     "family": "General Purpose",
@@ -196,13 +199,12 @@ class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
     "storage": null,
     "storage": null,
     "max_bandwidth": 0,
     "max_bandwidth": 0,
     "instance_type": "t2.nano",
     "instance_type": "t2.nano",
-    "ECU": "variable,
+    "ECU": "variable",
     "memory": 0.5,
     "memory": 0.5,
     "ebs_max_bandwidth": 0
     "ebs_max_bandwidth": 0
   }
   }
 ]
 ]
-"""
-        )
+""")
 
 
     def tearDownMock(self):
     def tearDownMock(self):
         """
         """

+ 82 - 28
cloudbridge/cloud/providers/aws/resources.py

@@ -1,9 +1,19 @@
 """
 """
 DataTypes used by this provider
 DataTypes used by this provider
 """
 """
+import hashlib
+import inspect
+import json
+
+from datetime import datetime
+
+from boto.exception import EC2ResponseError
+from boto.s3.key import Key
+
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseBucketObject
 from cloudbridge.cloud.base.resources import BaseBucketObject
+from cloudbridge.cloud.base.resources import BaseFloatingIP
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInstanceType
 from cloudbridge.cloud.base.resources import BaseInstanceType
 from cloudbridge.cloud.base.resources import BaseKeyPair
 from cloudbridge.cloud.base.resources import BaseKeyPair
@@ -17,23 +27,16 @@ from cloudbridge.cloud.base.resources import BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSubnet
 from cloudbridge.cloud.base.resources import BaseSubnet
-from cloudbridge.cloud.base.resources import BaseFloatingIP
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
-from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import RouterState
 from cloudbridge.cloud.interfaces.resources import RouterState
+from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import VolumeState
-from datetime import datetime
-import hashlib
-import inspect
-import json
 
 
-from boto.exception import EC2ResponseError
-from boto.s3.key import Key
 from retrying import retry
 from retrying import retry
 
 
 
 
@@ -83,6 +86,18 @@ class AWSMachineImage(BaseMachineImage):
         """
         """
         return self._ec2_image.description
         return self._ec2_image.description
 
 
+    @property
+    def min_disk(self):
+        """
+        Returns the minimum size of the disk that's required to
+        boot this image (in GB)
+
+        :rtype: ``int``
+        :return: The minimum disk size needed by this image
+        """
+        bdm = self._ec2_image.block_device_mapping
+        return bdm[self._ec2_image.root_device_name].size
+
     def delete(self):
     def delete(self):
         """
         """
         Delete this image
         Delete this image
@@ -342,7 +357,7 @@ class AWSInstance(BaseInstance):
         """
         """
         if self._ec2_instance.vpc_id:
         if self._ec2_instance.vpc_id:
             aid = self._provider._vpc_conn.get_all_addresses([ip_address])[0]
             aid = self._provider._vpc_conn.get_all_addresses([ip_address])[0]
-            return self._provider._ec2_conn.associate_address(
+            return self._provider.ec2_conn.associate_address(
                 self._ec2_instance.id, allocation_id=aid.allocation_id)
                 self._ec2_instance.id, allocation_id=aid.allocation_id)
         else:
         else:
             return self._ec2_instance.use_ip(ip_address)
             return self._ec2_instance.use_ip(ip_address)
@@ -351,8 +366,28 @@ class AWSInstance(BaseInstance):
         """
         """
         Remove a elastic IP address from this instance.
         Remove a elastic IP address from this instance.
         """
         """
-        raise NotImplementedError(
-            'remove_floating_ip not implemented by this provider.')
+        ip_addr = self._provider._vpc_conn.get_all_addresses([ip_address])[0]
+        if self._ec2_instance.vpc_id:
+            return self._provider.ec2_conn.disassociate_address(
+                association_id=ip_addr.association_id)
+        else:
+            return self._provider.ec2_conn.disassociate_address(
+                public_ip=ip_addr.public_ip)
+
+    def add_security_group(self, sg):
+        """
+        Add a security group to this instance
+        """
+        self._ec2_instance.modify_attribute(
+            'groupSet', [g.id for g in self._ec2_instance.groups] + [sg.id])
+
+    def remove_security_group(self, sg):
+        """
+        Remove a security group from this instance
+        """
+        self._ec2_instance.modify_attribute(
+            'groupSet', [g.id for g in self._ec2_instance.groups
+                         if g.id != sg.id])
 
 
     @property
     @property
     def state(self):
     def state(self):
@@ -646,7 +681,7 @@ class AWSSecurityGroup(BaseSecurityGroup):
         :return: Rule object if successful or ``None``.
         :return: Rule object if successful or ``None``.
         """
         """
         try:
         try:
-            if not isinstance(src_group, SecurityGroup):
+            if src_group and not isinstance(src_group, SecurityGroup):
                 src_group = self._provider.security.security_groups.get(
                 src_group = self._provider.security.security_groups.get(
                     src_group)
                     src_group)
 
 
@@ -672,17 +707,17 @@ class AWSSecurityGroup(BaseSecurityGroup):
                  cidr_ip=None, src_group=None):
                  cidr_ip=None, src_group=None):
         for rule in self._security_group.rules:
         for rule in self._security_group.rules:
             if (rule.ip_protocol == ip_protocol and
             if (rule.ip_protocol == ip_protocol and
-               rule.from_port == from_port and
-               rule.to_port == to_port and
-               rule.grants[0].cidr_ip == cidr_ip) or \
-               (rule.grants[0].group_id == src_group.id if src_group and
-               hasattr(rule.grants[0], 'group_id') else False):
+                rule.from_port == from_port and
+                rule.to_port == to_port and
+                rule.grants[0].cidr_ip == cidr_ip) or \
+                    (rule.grants[0].group_id == src_group.id if src_group and
+                        hasattr(rule.grants[0], 'group_id') else False):
                 return AWSSecurityGroupRule(self._provider, rule, self)
                 return AWSSecurityGroupRule(self._provider, rule, self)
         return None
         return None
 
 
     def to_json(self):
     def to_json(self):
-        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
-        js = {k: v for(k, v) in attr if not k.startswith('_')}
+        attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
+        js = {k: v for (k, v) in attr if not k.startswith('_')}
         json_rules = [r.to_json() for r in self.rules]
         json_rules = [r.to_json() for r in self.rules]
         js['rules'] = [json.loads(r) for r in json_rules]
         js['rules'] = [json.loads(r) for r in json_rules]
         if js.get('network_id'):
         if js.get('network_id'):
@@ -738,8 +773,8 @@ class AWSSecurityGroupRule(BaseSecurityGroupRule):
         return None
         return None
 
 
     def to_json(self):
     def to_json(self):
-        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
-        js = {k: v for(k, v) in attr if not k.startswith('_')}
+        attr = inspect.getmembers(self, lambda a: not (inspect.isroutine(a)))
+        js = {k: v for (k, v) in attr if not k.startswith('_')}
         js['group'] = self.group.id if self.group else ''
         js['group'] = self.group.id if self.group else ''
         js['parent'] = self.parent.id if self.parent else ''
         js['parent'] = self.parent.id if self.parent else ''
         return json.dumps(js, sort_keys=True)
         return json.dumps(js, sort_keys=True)
@@ -807,6 +842,12 @@ class AWSBucketObject(BaseBucketObject):
         """
         """
         self._key.set_contents_from_string(data)
         self._key.set_contents_from_string(data)
 
 
+    def upload_from_file(self, path):
+        """
+        Store the contents of the file pointed by the "path" variable.
+        """
+        self._key.set_contents_from_filename(path)
+
     def delete(self):
     def delete(self):
         """
         """
         Delete this object.
         Delete this object.
@@ -816,6 +857,12 @@ class AWSBucketObject(BaseBucketObject):
         """
         """
         self._key.delete()
         self._key.delete()
 
 
+    def generate_url(self, expires_in=0):
+        """
+        Generate a URL to this object.
+        """
+        return self._key.generate_url(expires_in=expires_in)
+
 
 
 class AWSBucket(BaseBucket):
 class AWSBucket(BaseBucket):
 
 
@@ -834,16 +881,16 @@ class AWSBucket(BaseBucket):
         """
         """
         return self._bucket.name
         return self._bucket.name
 
 
-    def get(self, key):
+    def get(self, name):
         """
         """
         Retrieve a given object from this bucket.
         Retrieve a given object from this bucket.
         """
         """
-        key = Key(self._bucket, key)
-        if key.exists():
+        key = Key(self._bucket, name)
+        if key and key.exists():
             return AWSBucketObject(self._provider, key)
             return AWSBucketObject(self._provider, key)
         return None
         return None
 
 
-    def list(self, limit=None, marker=None):
+    def list(self, limit=None, marker=None, prefix=None):
         """
         """
         List all objects within this bucket.
         List all objects within this bucket.
 
 
@@ -851,7 +898,8 @@ class AWSBucket(BaseBucket):
         :return: List of all available BucketObjects within this bucket.
         :return: List of all available BucketObjects within this bucket.
         """
         """
         objects = [AWSBucketObject(self._provider, obj)
         objects = [AWSBucketObject(self._provider, obj)
-                   for obj in self._bucket.list()]
+                   for obj in self._bucket.list(prefix=prefix)]
+
         return ClientPagedResultList(self._provider, objects,
         return ClientPagedResultList(self._provider, objects,
                                      limit=limit, marker=marker)
                                      limit=limit, marker=marker)
 
 
@@ -959,8 +1007,9 @@ class AWSNetwork(BaseNetwork):
         subnets = self._provider.vpc_conn.get_all_subnets(filters=flter)
         subnets = self._provider.vpc_conn.get_all_subnets(filters=flter)
         return [AWSSubnet(self._provider, subnet) for subnet in subnets]
         return [AWSSubnet(self._provider, subnet) for subnet in subnets]
 
 
-    def create_subnet(self, cidr_block, name=None):
-        subnet = self._provider.vpc_conn.create_subnet(self.id, cidr_block)
+    def create_subnet(self, cidr_block, name=None, zone=None):
+        subnet = self._provider.vpc_conn.create_subnet(self.id, cidr_block,
+                                                       availability_zone=zone)
         cb_subnet = AWSSubnet(self._provider, subnet)
         cb_subnet = AWSSubnet(self._provider, subnet)
         if name:
         if name:
             cb_subnet.name = name
             cb_subnet.name = name
@@ -1009,6 +1058,11 @@ class AWSSubnet(BaseSubnet):
     def network_id(self):
     def network_id(self):
         return self._subnet.vpc_id
         return self._subnet.vpc_id
 
 
+    @property
+    def zone(self):
+        return AWSPlacementZone(self._provider, self._subnet.availability_zone,
+                                self._provider.region_name)
+
     def delete(self):
     def delete(self):
         return self._provider.vpc_conn.delete_subnet(subnet_id=self.id)
         return self._provider.vpc_conn.delete_subnet(subnet_id=self.id)
 
 

+ 79 - 203
cloudbridge/cloud/providers/aws/services.py

@@ -1,10 +1,10 @@
 """Services implemented by the AWS provider."""
 """Services implemented by the AWS provider."""
-import time
 import string
 import string
+import time
 
 
 from boto.ec2.blockdevicemapping import BlockDeviceMapping
 from boto.ec2.blockdevicemapping import BlockDeviceMapping
 from boto.ec2.blockdevicemapping import BlockDeviceType
 from boto.ec2.blockdevicemapping import BlockDeviceType
-from boto.exception import EC2ResponseError
+from boto.exception import EC2ResponseError, S3ResponseError
 
 
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
@@ -22,12 +22,11 @@ from cloudbridge.cloud.base.services import BaseSecurityService
 from cloudbridge.cloud.base.services import BaseSnapshotService
 from cloudbridge.cloud.base.services import BaseSnapshotService
 from cloudbridge.cloud.base.services import BaseSubnetService
 from cloudbridge.cloud.base.services import BaseSubnetService
 from cloudbridge.cloud.base.services import BaseVolumeService
 from cloudbridge.cloud.base.services import BaseVolumeService
-from cloudbridge.cloud.interfaces.resources import InstanceType
 from cloudbridge.cloud.interfaces.exceptions \
 from cloudbridge.cloud.interfaces.exceptions \
     import InvalidConfigurationException
     import InvalidConfigurationException
+from cloudbridge.cloud.interfaces.resources import InstanceType
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 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 PlacementZone
 from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Snapshot
@@ -50,8 +49,8 @@ from .resources import AWSSnapshot
 from .resources import AWSSubnet
 from .resources import AWSSubnet
 from .resources import AWSVolume
 from .resources import AWSVolume
 
 
-import cloudbridge as cb
 # Uncomment to enable logging by default for this module
 # Uncomment to enable logging by default for this module
+# import cloudbridge as cb
 # cb.set_stream_logger(__name__)
 # cb.set_stream_logger(__name__)
 
 
 
 
@@ -363,11 +362,26 @@ class AWSObjectStoreService(BaseObjectStoreService):
         Returns a bucket given its ID. Returns ``None`` if the bucket
         Returns a bucket given its ID. Returns ``None`` if the bucket
         does not exist.
         does not exist.
         """
         """
-        bucket = self.provider.s3_conn.lookup(bucket_id)
-        if bucket:
+        try:
+            # Make a call to make sure the bucket exists. While this would
+            # normally return a Bucket instance, there's an edge case where a
+            # 403 response can occur when the bucket exists but the
+            # user simply does not have permissions to access it. See below.
+            bucket = self.provider.s3_conn.get_bucket(bucket_id)
             return AWSBucket(self.provider, bucket)
             return AWSBucket(self.provider, bucket)
-        else:
-            return None
+        except S3ResponseError as e:
+            # If 403, it means the bucket exists, but the user does not have
+            # permissions to access the bucket. However, limited operations
+            # may be permitted (with a session token for example), so return a
+            # Bucket instance to allow further operations.
+            # http://stackoverflow.com/questions/32331456/using-boto-upload-file-to-s3-
+            # sub-folder-when-i-have-no-permissions-on-listing-fo
+            if e.status == 403:
+                bucket = self.provider.s3_conn.get_bucket(bucket_id,
+                                                          validate=False)
+                return AWSBucket(self.provider, bucket)
+        # For all other responses, it's assumed that the bucket does not exist.
+        return None
 
 
     def find(self, name, limit=None, marker=None):
     def find(self, name, limit=None, marker=None):
         """
         """
@@ -467,13 +481,14 @@ class AWSInstanceService(BaseInstanceService):
     def __init__(self, provider):
     def __init__(self, provider):
         super(AWSInstanceService, self).__init__(provider)
         super(AWSInstanceService, self).__init__(provider)
 
 
-    def create(self, name, image, instance_type, network=None, zone=None,
+    def create(self, name, image, instance_type, subnet, zone=None,
                key_pair=None, security_groups=None, user_data=None,
                key_pair=None, security_groups=None, user_data=None,
                launch_config=None, **kwargs):
                launch_config=None, **kwargs):
         image_id = image.id if isinstance(image, MachineImage) else image
         image_id = image.id if isinstance(image, MachineImage) else image
         instance_size = instance_type.id if \
         instance_size = instance_type.id if \
             isinstance(instance_type, InstanceType) else instance_type
             isinstance(instance_type, InstanceType) else instance_type
-        network_id = network.id if isinstance(network, Network) else network
+        subnet = (self.provider.network.subnets.get(subnet)
+                  if isinstance(subnet, str) else subnet)
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         key_pair_name = key_pair.name if isinstance(
         key_pair_name = key_pair.name if isinstance(
             key_pair,
             key_pair,
@@ -484,7 +499,7 @@ class AWSInstanceService(BaseInstanceService):
             bdm = None
             bdm = None
 
 
         subnet_id, zone_id, security_group_ids = \
         subnet_id, zone_id, security_group_ids = \
-            self._resolve_launch_options(zone_id, network_id, security_groups)
+            self._resolve_launch_options(subnet, zone_id, security_groups)
 
 
         reservation = self.provider.ec2_conn.run_instances(
         reservation = self.provider.ec2_conn.run_instances(
             image_id=image_id, instance_type=instance_size,
             image_id=image_id, instance_type=instance_size,
@@ -498,202 +513,37 @@ class AWSInstanceService(BaseInstanceService):
             instance.name = name
             instance.name = name
         return instance
         return instance
 
 
-    def _resolve_launch_options(self, zone_id=None, vpc_id=None,
+    def _resolve_launch_options(self, subnet=None, zone_id=None,
                                 security_groups=None):
                                 security_groups=None):
         """
         """
         Work out interdependent launch options.
         Work out interdependent launch options.
 
 
-        Some launch options are required and interdependent so work through
-        those constraints. There are 8 subsets of options so the logic works
-        through each of those combinations to figure out the proper launch
-        options.
+        Some launch options are required and interdependent so make sure
+        they conform to the interface contract.
+
+        :type subnet: ``Subnet``
+        :param subnet: Subnet object within which to launch.
 
 
         :type zone_id: ``str``
         :type zone_id: ``str``
         :param zone_id: ID of the zone where the launch should happen.
         :param zone_id: ID of the zone where the launch should happen.
 
 
-        :type vpc_id: ``str``
-        :param vpc_id: ID of the network within which to launch.
-
-        :type security_groups: ``list`` of ``str``
-        :param zone_id: List of security group names.
+        :type security_groups: ``list`` of ``id``
+        :param zone_id: List of security group IDs.
 
 
         :rtype: triplet of ``str``
         :rtype: triplet of ``str``
         :return: Subnet ID, zone ID and security group IDs for launch.
         :return: Subnet ID, zone ID and security group IDs for launch.
 
 
-        :raise ValueError: In case a conflicting combination is found or the
-                           method cannot infer the defaults, raise.
-        """
-        def _get_default_vpc(vpcs, exc="No default network found."):
-            """
-            Inspect supplied VPCs to figure out a default one or create one.
-
-            Default VPC either has ``is_default`` property set or matches the
-            default network name used by this library. If a default network
-            is not found, an attempt to create one is made.
-
-            :type vpcs: ``list``
-            :param vpcs: A list of boto VPC objects.
-
-            :type exc: ``str``
-            :type exc: A string value to use if/when raising ValueError.
-
-            :rtype: ``str``
-            :return: Default VPC ID.
-            """
-            default_vpc = None
-            for vpc in vpcs:
-                if vpc.is_default:
-                    default_vpc = vpc.id
-                    break
-            if not default_vpc:
-                for vpc in vpcs:
-                    if vpc.tags.get('Name', '') == \
-                       AWSNetwork.CB_DEFAULT_NETWORK_NAME:
-                        default_vpc = vpc.id
-                        break
-            if not default_vpc:
-                net = self.provider.network.create(
-                    name=AWSNetwork.CB_DEFAULT_NETWORK_NAME)
-                default_vpc = net.id
-            return default_vpc
-
-        def _get_potential_subnets(filters, exc):
-            """
-            Query existing subnets through supplied filters.
-
-            :type filters: ``dict``
-            :param filters: A dictionary supplying desired filters.
-
-            :type exc: ``str``
-            :type exc: A string value to use if/when raising ValueError.
-
-            :rtype: tuple of ``str``
-            :return: A tuple with a random subnet that matches supplied
-                     filters and an availability zone where the given subnet
-                     is defined.
-
-            :raise ValueError: If no subnet can be found, raise a ValueError.
-            """
-            potential_subnets = self.provider.vpc_conn.get_all_subnets(
-                filters=filters)
-            if potential_subnets and len(potential_subnets) > 0:
-                sn_id = potential_subnets[0].id
-                zone_id = potential_subnets[0].availability_zone
-            else:
-                raise ValueError(exc)
-            return sn_id, zone_id
-
-        def _get_security_groups(security_groups, vpc_id=None, obj=False):
-            """
-            Resolve exact security groups to use.
-
-            :type security_groups: A ``list`` of ``SecurityGroup`` objects or
-                                   a list of ``str`` names.
-            :param security_groups: A list of ``SecurityGroup`` objects or a
-                                    list of ``SecurityGroup`` names, which
-                                    should be resolved.
-
-            :type vpc_id: ``str``
-            :param vpc_id: ID of the network within which to launch.
-
-            :type obj: ``bool``
-            :param obj: If True, return provider-native security group objects.
-                        Otherwise, return the IDs.
-
-            :rtype: list
-            :return: provider-native security group objects or the IDs (see
-                    ``obj`` param).
-            """
-            if isinstance(security_groups, list) and \
-               isinstance(security_groups[0], SecurityGroup):
-                return [sg._security_group if obj else sg.id
-                        for sg in security_groups]
-            else:
-                flters = {'group_name': security_groups}
-                if vpc_id:
-                    flters['vpc_id'] = vpc_id
-                sgs = self.provider.ec2_conn.get_all_security_groups(
-                    filters=flters)
-                return list(set([sg if obj else sg.id for sg in sgs]))
-
-        if zone_id and vpc_id and security_groups:
-            exc = "No subnets found in zone {0} for network {1}.".format(
-                zone_id, vpc_id)
-            flters = {'availabilityZone': zone_id, 'state': 'available',
-                      'vpcId': vpc_id}
-            sn_id, _ = _get_potential_subnets(flters, exc)
-            sg_ids = _get_security_groups(security_groups, vpc_id)
-        elif vpc_id and security_groups:
-            sg_ids = _get_security_groups(security_groups, vpc_id)
-            exc = "No subnets found in network {0}.".format(vpc_id)
-            flters = {'state': 'available', 'vpcId': vpc_id}
-            sn_id, zone_id = _get_potential_subnets(flters, exc)
-        elif vpc_id and zone_id:
-            flters = {'availabilityZone': zone_id, 'state': 'available',
-                      'vpcId': vpc_id}
-            exc = "No subnets found in zone {0} for network {1}.".format(
-                zone_id, vpc_id)
-            sn_id, _ = _get_potential_subnets(flters, exc)
-            sg_ids = None
-        elif zone_id and security_groups:
-            sgs = _get_security_groups(security_groups, obj=True)
-            # Get VPCs the supplied SGs belong to
-            vpc_ids = list(set([sg.vpc_id for sg in sgs if sg.vpc_id]))
-            vpcs = []
-            if vpc_ids:
-                vpcs = self.provider.vpc_conn.get_all_vpcs(vpc_ids=vpc_ids)
-            exc = ("No default network found for zone {0} and security groups "
-                   "{1}".format(zone_id, security_groups))
-            default_vpc = _get_default_vpc(vpcs, exc)
-            # Filter only the SGs within the default VPC
-            sg_ids = _get_security_groups(security_groups, default_vpc)
-            flters = {'availabilityZone': zone_id, 'state': 'available',
-                      'vpc_id': default_vpc}
-            exc = "No subnets found in zone {0} for default network {1}." \
-                .format(zone_id, default_vpc)
-            sn_id, _ = _get_potential_subnets(flters, exc)
-        elif vpc_id:
-            flters = {'state': 'available', 'vpcId': vpc_id}
-            exc = "No subnets found for network {0}.".format(vpc_id)
-            sn_id, zone_id = _get_potential_subnets(flters, exc)
-            sg_ids = None
-        elif zone_id:
-            vpcs = self.provider.vpc_conn.get_all_vpcs()
-            exc = "No default network exists for security zone {0}.".format(
-                zone_id)
-            default_vpc = _get_default_vpc(vpcs, exc)
-            flters = {'availabilityZone': zone_id, 'state': 'available',
-                      'vpcId': default_vpc}
-            exc = "No subnets found in zone {0} for default network {1}." \
-                .format(zone_id, default_vpc)
-            sn_id, _ = _get_potential_subnets(flters, exc)
-            sg_ids = None
-        elif security_groups:
-            sgs = _get_security_groups(security_groups, obj=True)
-            # Get VPCs the supplied SGs belong to
-            vpc_ids = list(set([sg.vpc_id for sg in sgs if sg.vpc_id]))
-            vpcs = []
-            if vpc_ids:
-                vpcs = self.provider.vpc_conn.get_all_vpcs(vpc_ids=vpc_ids)
-            exc = "No default network exists for security groups {0}.".format(
-                security_groups)
-            default_vpc = _get_default_vpc(vpcs, exc)
-            # Filter only the SGs within the default VPC
-            sg_ids = _get_security_groups(security_groups, default_vpc)
-            flters = {'state': 'available', 'vpcId': default_vpc}
-            exc = "No subnets found in network {0}.".format(default_vpc)
-            sn_id, zone_id = _get_potential_subnets(flters, exc)
+        :raise ValueError: In case a conflicting combination is found.
+        """
+        if subnet:
+            # subnet's zone takes precedence
+            zone_id = subnet.zone.id
+        if isinstance(security_groups, list) and isinstance(
+                security_groups[0], SecurityGroup):
+            security_group_ids = [sg.id for sg in security_groups]
         else:
         else:
-            # Nothing was defined, use all defaults
-            vpcs = self.provider.vpc_conn.get_all_vpcs()
-            default_vpc = _get_default_vpc(vpcs)
-            flters = {'state': 'available', 'vpcId': default_vpc}
-            exc = "No subnets found for default network {1}.".format(
-                default_vpc)
-            sn_id, zone_id = _get_potential_subnets(flters, exc)
-            sg_ids = None
-
-        return sn_id, zone_id, sg_ids
+            security_group_ids = security_groups
+        return subnet.id, zone_id, security_group_ids
 
 
     def _process_block_device_mappings(self, launch_config, zone=None):
     def _process_block_device_mappings(self, launch_config, zone=None):
         """
         """
@@ -794,9 +644,6 @@ class AWSInstanceService(BaseInstanceService):
                                      reservations.next_token,
                                      reservations.next_token,
                                      False, data=instances)
                                      False, data=instances)
 
 
-AWS_INSTANCE_DATA_DEFAULT_URL = "https://d168wakzal7fp0.cloudfront.net/" \
-                                "aws_instance_data.json"
-
 
 
 class AWSInstanceTypesService(BaseInstanceTypesService):
 class AWSInstanceTypesService(BaseInstanceTypesService):
 
 
@@ -809,8 +656,8 @@ class AWSInstanceTypesService(BaseInstanceTypesService):
         Fetch info about the available instances.
         Fetch info about the available instances.
 
 
         To update this information, update the file pointed to by the
         To update this information, update the file pointed to by the
-        ``AWS_INSTANCE_DATA_DEFAULT_URL`` above. The content for this file
-        should be obtained from this repo
+        ``provider.AWS_INSTANCE_DATA_DEFAULT_URL`` above. The content for this
+        file should be obtained from this repo:
         https://github.com/powdahound/ec2instances.info (in particular, this
         https://github.com/powdahound/ec2instances.info (in particular, this
         file: https://raw.githubusercontent.com/powdahound/ec2instances.info/
         file: https://raw.githubusercontent.com/powdahound/ec2instances.info/
         master/www/instances.json).
         master/www/instances.json).
@@ -818,7 +665,8 @@ class AWSInstanceTypesService(BaseInstanceTypesService):
         TODO: Needs a caching function with timeout
         TODO: Needs a caching function with timeout
         """
         """
         r = requests.get(self.provider.config.get(
         r = requests.get(self.provider.config.get(
-            "aws_instance_info_url", AWS_INSTANCE_DATA_DEFAULT_URL))
+            "aws_instance_info_url",
+            self.provider.AWS_INSTANCE_DATA_DEFAULT_URL))
         return r.json()
         return r.json()
 
 
     def list(self, limit=None, marker=None):
     def list(self, limit=None, marker=None):
@@ -929,15 +777,43 @@ class AWSSubnetService(BaseSubnetService):
         subnets = self.provider.vpc_conn.get_all_subnets(filters=fltr)
         subnets = self.provider.vpc_conn.get_all_subnets(filters=fltr)
         return [AWSSubnet(self.provider, subnet) for subnet in subnets]
         return [AWSSubnet(self.provider, subnet) for subnet in subnets]
 
 
-    def create(self, network, cidr_block, name=None):
+    def create(self, network, cidr_block, name=None, zone=None):
         network_id = network.id if isinstance(network, AWSNetwork) else network
         network_id = network.id if isinstance(network, AWSNetwork) else network
-        subnet = self.provider.vpc_conn.create_subnet(network_id, cidr_block)
+        subnet = self.provider.vpc_conn.create_subnet(network_id, cidr_block,
+                                                      availability_zone=zone)
         cb_subnet = AWSSubnet(self.provider, subnet)
         cb_subnet = AWSSubnet(self.provider, subnet)
         if name:
         if name:
             time.sleep(2)  # The subnet does not always get created in time
             time.sleep(2)  # The subnet does not always get created in time
             cb_subnet.name = name
             cb_subnet.name = name
         return cb_subnet
         return cb_subnet
 
 
+    def get_or_create_default(self, zone=None):
+        filtr = {'availabilityZone': zone} if zone else None
+        sns = self.provider.vpc_conn.get_all_subnets(filters=filtr)
+        for sn in sns:
+            if sn.defaultForAz:
+                return AWSSubnet(self.provider, sn)
+        # No provider-default Subnet exists, look for a library-default one
+        for sn in sns:
+            if sn.tags.get('Name') == AWSSubnet.CB_DEFAULT_SUBNET_NAME:
+                return AWSSubnet(self.provider, sn)
+        # No provider-default Subnet exists, try to create it (net + subnets)
+        default_net = self.provider.network.create(
+            name=AWSNetwork.CB_DEFAULT_NETWORK_NAME)
+        # Create a subnet in each of the region's zones
+        region = self.provider.compute.regions.get(
+            self.provider.vpc_conn.region.name)
+        default_sn = None
+        for i, z in enumerate(region.zones):
+            sn = self.create(default_net, '10.0.{0}.0/24'.format(i),
+                             AWSSubnet.CB_DEFAULT_SUBNET_NAME, z.name)
+            if zone and zone == z.name:
+                default_sn = sn
+        # No specific zone was supplied; return the last created subnet
+        if not default_sn:
+            default_sn = sn
+        return default_sn
+
     def delete(self, subnet):
     def delete(self, subnet):
         subnet_id = subnet.id if isinstance(subnet, AWSSubnet) else subnet
         subnet_id = subnet.id if isinstance(subnet, AWSSubnet) else subnet
         return self.provider.vpc_conn.delete_subnet(subnet_id=subnet_id)
         return self.provider.vpc_conn.delete_subnet(subnet_id=subnet_id)

+ 15 - 5
cloudbridge/cloud/providers/gce/helpers.py

@@ -1,9 +1,19 @@
-import os
-from Crypto.PublicKey import RSA
+# based on http://stackoverflow.com/a/39126754
+from cryptography.hazmat.primitives import serialization as crypt_serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.backends import default_backend
 
 
 
 
 def generate_key_pair():
 def generate_key_pair():
-    kp = RSA.generate(2048, os.urandom)
-    public_key = kp.publickey().exportKey("OpenSSH").split(" ")[1]
-    private_key = kp.exportKey("PEM")
+    key_pair = rsa.generate_private_key(
+        backend=default_backend(),
+        public_exponent=65537,
+        key_size=2048)
+    private_key = key_pair.private_bytes(
+        crypt_serialization.Encoding.PEM,
+        crypt_serialization.PrivateFormat.PKCS8,
+        crypt_serialization.NoEncryption())
+    public_key = key_pair.public_key().public_bytes(
+        crypt_serialization.Encoding.OpenSSH,
+        crypt_serialization.PublicFormat.OpenSSH)
     return private_key, public_key
     return private_key, public_key

+ 4 - 2
cloudbridge/cloud/providers/gce/provider.py

@@ -35,7 +35,7 @@ class GCPResourceUrl(object):
 
 
 
 
 class GCPResources(object):
 class GCPResources(object):
-    
+
     def __init__(self, connection):
     def __init__(self, connection):
         self._connection = connection
         self._connection = connection
 
 
@@ -112,7 +112,8 @@ class GCPResources(object):
         GCP servers.
         GCP servers.
         """
         """
         url = url.strip()
         url = url.strip()
-        if url.startswith(self._root_url): url = url[len(self._root_url):]
+        if url.startswith(self._root_url):
+            url = url[len(self._root_url):]
         if url.startswith(self._service_path):
         if url.startswith(self._service_path):
             url = url[len(self._service_path):]
             url = url[len(self._service_path):]
 
 
@@ -213,6 +214,7 @@ class GCECloudProvider(BaseCloudProvider):
         """
         """
         http = httplib2.Http()
         http = httplib2.Http()
         http = self._credentials.authorize(http)
         http = self._credentials.authorize(http)
+
         def _postproc(*kwargs):
         def _postproc(*kwargs):
             if len(kwargs) >= 2:
             if len(kwargs) >= 2:
                 # The first argument is request, and the second is response.
                 # The first argument is request, and the second is response.

+ 12 - 9
cloudbridge/cloud/providers/gce/services.py

@@ -119,7 +119,7 @@ class GCEKeyPairService(BaseKeyPairService):
             # elems should be "ssh-rsa <public_key> <email>"
             # elems should be "ssh-rsa <public_key> <email>"
             elems = key.split(" ")
             elems = key.split(" ")
             if elems and elems[0]:  # ignore blank lines
             if elems and elems[0]:  # ignore blank lines
-                yield GCEKeyPairService.GCEKeyInfo(elems[0], elems[1],
+                yield GCEKeyPairService.GCEKeyInfo(elems[0], elems[1].encode('ascii'),
                                                    elems[2])
                                                    elems[2])
 
 
     def gce_metadata_save_op(self, callback):
     def gce_metadata_save_op(self, callback):
@@ -432,7 +432,7 @@ class GCEInstanceService(BaseInstanceService):
     def __init__(self, provider):
     def __init__(self, provider):
         super(GCEInstanceService, self).__init__(provider)
         super(GCEInstanceService, self).__init__(provider)
 
 
-    def create(self, name, image, instance_type, network=None, zone=None,
+    def create(self, name, image, instance_type, subnet, zone=None,
                key_pair=None, security_groups=None, user_data=None,
                key_pair=None, security_groups=None, user_data=None,
                launch_config=None, **kwargs):
                launch_config=None, **kwargs):
         """
         """
@@ -441,9 +441,10 @@ class GCEInstanceService(BaseInstanceService):
         if not zone:
         if not zone:
             zone = self.provider.default_zone
             zone = self.provider.default_zone
         if not launch_config:
         if not launch_config:
-            if network:
+            if subnet:
+                network = self.provider.network.get(subnet.network_id)
                 network_url = (network.resource_url
                 network_url = (network.resource_url
-                               if isinstance(network, Network) else network)
+                               if isinstance(network, GCENetwork) else network)
             else:
             else:
                 network_url = 'global/networks/default'
                 network_url = 'global/networks/default'
             config = {
             config = {
@@ -454,12 +455,14 @@ class GCEInstanceService(BaseInstanceService):
                            'initializeParams': {
                            'initializeParams': {
                                'sourceImage': image.resource_url,
                                'sourceImage': image.resource_url,
                            }
                            }
-                       }],
+                           }],
                 'networkInterfaces': [
                 'networkInterfaces': [
-                    {'network': network_url,
-                     'accessConfigs': [{'type': 'ONE_TO_ONE_NAT',
-                                        'name': 'External NAT'}]
-                 }],
+                    {
+                        # TODO: Should replace network below with subnetwork
+                        'network': network_url,
+                        'accessConfigs': [{'type': 'ONE_TO_ONE_NAT',
+                                           'name': 'External NAT'}]
+                    }],
             }
             }
             if security_groups and isinstance(security_groups, list):
             if security_groups and isinstance(security_groups, list):
                 sg_names = []
                 sg_names = []

+ 1 - 0
cloudbridge/cloud/providers/openstack/helpers.py

@@ -2,6 +2,7 @@
 Helper functions
 Helper functions
 """
 """
 import itertools
 import itertools
+
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 
 
 
 

+ 69 - 9
cloudbridge/cloud/providers/openstack/provider.py

@@ -1,16 +1,23 @@
 """Provider implementation based on OpenStack Python clients for OpenStack."""
 """Provider implementation based on OpenStack Python clients for OpenStack."""
 
 
+import inspect
+
 import os
 import os
 
 
 from cinderclient import client as cinder_client
 from cinderclient import client as cinder_client
+
+from cloudbridge.cloud.base import BaseCloudProvider
+
 from keystoneauth1 import session
 from keystoneauth1 import session
+
 from keystoneclient import client as keystone_client
 from keystoneclient import client as keystone_client
+
 from neutronclient.v2_0 import client as neutron_client
 from neutronclient.v2_0 import client as neutron_client
+
 from novaclient import client as nova_client
 from novaclient import client as nova_client
 from novaclient import shell as nova_shell
 from novaclient import shell as nova_shell
-from swiftclient import client as swift_client
 
 
-from cloudbridge.cloud.base import BaseCloudProvider
+from swiftclient import client as swift_client
 
 
 from .services import OpenStackBlockStoreService
 from .services import OpenStackBlockStoreService
 from .services import OpenStackComputeService
 from .services import OpenStackComputeService
@@ -45,9 +52,6 @@ class OpenStackCloudProvider(BaseCloudProvider):
             os.environ.get('OS_PROJECT_DOMAIN_NAME', None))
             os.environ.get('OS_PROJECT_DOMAIN_NAME', None))
         self.user_domain_name = self._get_config_value(
         self.user_domain_name = self._get_config_value(
             'os_user_domain_name', os.environ.get('OS_USER_DOMAIN_NAME', None))
             'os_user_domain_name', os.environ.get('OS_USER_DOMAIN_NAME', None))
-        self.identity_api_version = self._get_config_value(
-            'os_identity_api_version',
-            os.environ.get('OS_IDENTITY_API_VERSION', None))
 
 
         # Service connections, lazily initialized
         # Service connections, lazily initialized
         self._nova = None
         self._nova = None
@@ -234,10 +238,66 @@ class OpenStackCloudProvider(BaseCloudProvider):
 #         return glance_client.Client(version=api_version,
 #         return glance_client.Client(version=api_version,
 #                                     session=self.keystone.session)
 #                                     session=self.keystone.session)
 
 
-    def _connect_swift(self):
-        """Get an OpenStack Swift (object store) client object cloud."""
-        return swift_client.Connection(authurl=self.auth_url,
-                                       session=self._keystone_session)
+    @staticmethod
+    def _clean_options(options, method_to_match):
+        """
+        Returns a **copy** of the source options with all keys that are not in
+        the ``method_to_match`` parameter list removed.
+
+        .. note:: If ``options`` has the ``os_options`` key it will have
+            both the key and its value removed. This is because any entries
+            in this dictionary value will override our settings. This
+            situation is only going to happen when the `_connect_swift`
+            method is called by the SwiftService to manufacture new
+            connections.
+
+        .. seealso::
+            https://docs.openstack.org/developer/python-swiftclient/swiftclient.html#module-swiftclient.client
+
+        :param options: The source options.
+        :type options: ``dict``
+        :param method_to_match: The method whose signature is to be matched
+        :type method_to_match: A callable
+        :return: A copy of the source options with all keys that are not in the
+            ``method_to_match`` parameter list removed. If options is ``None``
+            then this will be an empty dictionary
+        :rtype: ``dict``
+        """
+        result = {}
+        if options:
+            try:
+                method_signature = inspect.signature(method_to_match)
+                parameters = set(method_signature.parameters.keys())
+            except AttributeError:
+                parameters = set(inspect.getargspec(method_to_match).args)
+            result = {key: val for key, val in options.items() if
+                      key in parameters}
+            # Don't allow the options to override our authentication
+            result.pop('os_options', None)
+        return result
+
+    def _connect_swift(self, options=None):
+        """
+        Get an OpenStack Swift (object store) client connection.
+
+        :param options: A dictionary of options from which values will be
+            passed to the connection.
+        :return: A Swift client connection using the auth credentials held by
+            the OpenStackCloudProvider instance
+        """
+        clean_options = self._clean_options(options,
+                                            swift_client.Connection.__init__)
+        storage_url = self._get_config_value(
+            'os_storage_url', os.environ.get('OS_STORAGE_URL', None))
+        auth_token = self._get_config_value(
+            'os_auth_token', os.environ.get('OS_AUTH_TOKEN', None))
+        if storage_url and auth_token:
+            clean_options['preauthurl'] = storage_url
+            clean_options['preauthtoken'] = auth_token
+        else:
+            clean_options['authurl'] = self.auth_url
+            clean_options['session'] = self._keystone_session
+        return swift_client.Connection(**clean_options)
 
 
     def _connect_neutron(self):
     def _connect_neutron(self):
         """Get an OpenStack Neutron (networking) client object cloud."""
         """Get an OpenStack Neutron (networking) client object cloud."""

+ 152 - 57
cloudbridge/cloud/providers/openstack/resources.py

@@ -1,9 +1,16 @@
 """
 """
 DataTypes used by this provider
 DataTypes used by this provider
 """
 """
+import inspect
+import ipaddress
+import json
+
+import os
+
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseBucket
 from cloudbridge.cloud.base.resources import BaseBucketObject
 from cloudbridge.cloud.base.resources import BaseBucketObject
+from cloudbridge.cloud.base.resources import BaseFloatingIP
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInstanceType
 from cloudbridge.cloud.base.resources import BaseInstanceType
 from cloudbridge.cloud.base.resources import BaseKeyPair
 from cloudbridge.cloud.base.resources import BaseKeyPair
@@ -16,24 +23,26 @@ from cloudbridge.cloud.base.resources import BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSubnet
 from cloudbridge.cloud.base.resources import BaseSubnet
-from cloudbridge.cloud.base.resources import BaseFloatingIP
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import RouterState
 from cloudbridge.cloud.interfaces.resources import RouterState
+from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import VolumeState
-from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
-import inspect
-import json
-
-import ipaddress
 
 
 from keystoneclient.v3.regions import Region
 from keystoneclient.v3.regions import Region
+
 import novaclient.exceptions as novaex
 import novaclient.exceptions as novaex
-import swiftclient.exceptions as swiftex
+
+import swiftclient
+
+from swiftclient.service import SwiftService, SwiftUploadObject
+
+ONE_GIG = 1048576000  # in bytes
+FIVE_GIG = ONE_GIG * 5  # in bytes
 
 
 
 
 class OpenStackMachineImage(BaseMachineImage):
 class OpenStackMachineImage(BaseMachineImage):
@@ -77,6 +86,17 @@ class OpenStackMachineImage(BaseMachineImage):
         """
         """
         return None
         return None
 
 
+    @property
+    def min_disk(self):
+        """
+        Returns the minimum size of the disk that's required to
+        boot this image (in GB)
+
+        :rtype: ``int``
+        :return: The minimum disk size needed by this image
+        """
+        return self._os_image.minDisk
+
     def delete(self):
     def delete(self):
         """
         """
         Delete this image
         Delete this image
@@ -360,6 +380,18 @@ class OpenStackInstance(BaseInstance):
         """
         """
         self._os_instance.remove_floating_ip(ip_address)
         self._os_instance.remove_floating_ip(ip_address)
 
 
+    def add_security_group(self, sg):
+        """
+        Add a security group to this instance
+        """
+        self._os_instance.add_security_group(sg.id)
+
+    def remove_security_group(self, sg):
+        """
+        Remove a security group from this instance
+        """
+        self._os_instance.remove_security_group(sg.id)
+
     @property
     @property
     def state(self):
     def state(self):
         return OpenStackInstance.INSTANCE_STATE_MAP.get(
         return OpenStackInstance.INSTANCE_STATE_MAP.get(
@@ -399,8 +431,8 @@ class OpenStackRegion(BaseRegion):
 
 
     @property
     @property
     def zones(self):
     def zones(self):
-        # detailed must be set to ``False`` because the (default) ``True``
-        # value requires Admin privileges
+        # ``detailed`` param must be set to ``False`` because the (default)
+        # ``True`` value requires Admin privileges
         if self.name == self._provider.region_name:  # optimisation
         if self.name == self._provider.region_name:  # optimisation
             zones = self._provider.nova.availability_zones.list(detailed=False)
             zones = self._provider.nova.availability_zones.list(detailed=False)
         else:
         else:
@@ -412,8 +444,7 @@ class OpenStackRegion(BaseRegion):
                 # return an empty list
                 # return an empty list
                 zones = []
                 zones = []
 
 
-        return [OpenStackPlacementZone(self._provider, z.zoneName,
-                                       self._os_region)
+        return [OpenStackPlacementZone(self._provider, z.zoneName, self.name)
                 for z in zones]
                 for z in zones]
 
 
 
 
@@ -670,6 +701,7 @@ class OpenStackNetwork(BaseNetwork):
 
 
     @property
     @property
     def state(self):
     def state(self):
+        self.refresh()
         return OpenStackNetwork._NETWORK_STATE_MAP.get(
         return OpenStackNetwork._NETWORK_STATE_MAP.get(
             self._network.get('status', None),
             self._network.get('status', None),
             NetworkState.UNKNOWN)
             NetworkState.UNKNOWN)
@@ -691,7 +723,8 @@ class OpenStackNetwork(BaseNetwork):
                    .get('subnets', []))
                    .get('subnets', []))
         return [OpenStackSubnet(self._provider, subnet) for subnet in subnets]
         return [OpenStackSubnet(self._provider, subnet) for subnet in subnets]
 
 
-    def create_subnet(self, cidr_block, name=''):
+    def create_subnet(self, cidr_block, name='', zone=None):
+        """OpenStack has no support for subnet zones so the value is ignored"""
         subnet_info = {'name': name, 'network_id': self.id,
         subnet_info = {'name': name, 'network_id': self.id,
                        'cidr': cidr_block, 'ip_version': 4}
                        'cidr': cidr_block, 'ip_version': 4}
         subnet = (self._provider.neutron.create_subnet({'subnet': subnet_info})
         subnet = (self._provider.neutron.create_subnet({'subnet': subnet_info})
@@ -699,11 +732,9 @@ class OpenStackNetwork(BaseNetwork):
         return OpenStackSubnet(self._provider, subnet)
         return OpenStackSubnet(self._provider, subnet)
 
 
     def refresh(self):
     def refresh(self):
-        """
-        Refreshes the state of this network by re-querying the cloud provider
-        for its latest state.
-        """
-        return self.state
+        """Refresh the state of this network by re-querying the provider."""
+        net = self._provider.neutron.list_networks(id=self.id).get('networks')
+        self._network = net[0] if net else {}
 
 
 
 
 class OpenStackSubnet(BaseSubnet):
 class OpenStackSubnet(BaseSubnet):
@@ -728,6 +759,15 @@ class OpenStackSubnet(BaseSubnet):
     def network_id(self):
     def network_id(self):
         return self._subnet.get('network_id', None)
         return self._subnet.get('network_id', None)
 
 
+    @property
+    def zone(self):
+        """
+        OpenStack does not have a notion of placement zone for subnets.
+
+        Default to None.
+        """
+        return None
+
     def delete(self):
     def delete(self):
         if self.id in str(self._provider.neutron.list_subnets()):
         if self.id in str(self._provider.neutron.list_subnets()):
             self._provider.neutron.delete_subnet(self.id)
             self._provider.neutron.delete_subnet(self.id)
@@ -755,7 +795,7 @@ class OpenStackFloatingIP(BaseFloatingIP):
         return self._ip.get('fixed_ip_address', None)
         return self._ip.get('fixed_ip_address', None)
 
 
     def in_use(self):
     def in_use(self):
-        return True if self._ip.get('status', None) == 'ACTIVE' else False
+        return bool(self._ip.get('port_id', None))
 
 
     def delete(self):
     def delete(self):
         self._provider.neutron.delete_floatingip(self.id)
         self._provider.neutron.delete_floatingip(self.id)
@@ -875,8 +915,8 @@ class OpenStackSecurityGroup(BaseSecurityGroup):
         """
         """
         Create a security group rule.
         Create a security group rule.
 
 
-        You need to pass in either ``src_group`` OR ``ip_protocol``,
-        ``from_port``, ``to_port``, and ``cidr_ip``.  In other words, either
+        You need to pass in either ``src_group`` OR ``ip_protocol`` AND
+        ``from_port``, ``to_port``, ``cidr_ip``.  In other words, either
         you are authorizing another group or you are authorizing some
         you are authorizing another group or you are authorizing some
         ip-based rule.
         ip-based rule.
 
 
@@ -902,20 +942,19 @@ class OpenStackSecurityGroup(BaseSecurityGroup):
             if not isinstance(src_group, SecurityGroup):
             if not isinstance(src_group, SecurityGroup):
                 src_group = self._provider.security.security_groups.get(
                 src_group = self._provider.security.security_groups.get(
                     src_group)
                     src_group)
-            for protocol in ['udp', 'tcp']:
-                existing_rule = self.get_rule(ip_protocol=ip_protocol,
-                                              from_port=1,
-                                              to_port=65535,
-                                              src_group=src_group)
-                if existing_rule:
-                    return existing_rule
-
-                rule = self._provider.nova.security_group_rules.create(
-                    parent_group_id=self._security_group.id,
-                    ip_protocol=protocol,
-                    from_port=1,
-                    to_port=65535,
-                    group_id=src_group.id)
+            existing_rule = self.get_rule(ip_protocol=ip_protocol,
+                                          from_port=from_port,
+                                          to_port=to_port,
+                                          src_group=src_group)
+            if existing_rule:
+                return existing_rule
+
+            rule = self._provider.nova.security_group_rules.create(
+                parent_group_id=self._security_group.id,
+                ip_protocol=ip_protocol,
+                from_port=from_port,
+                to_port=to_port,
+                group_id=src_group.id)
             if rule:
             if rule:
                 # We can only return one Rule so default to TCP (ie, last in
                 # We can only return one Rule so default to TCP (ie, last in
                 # the for loop above).
                 # the for loop above).
@@ -942,16 +981,16 @@ class OpenStackSecurityGroup(BaseSecurityGroup):
 
 
     def get_rule(self, ip_protocol=None, from_port=None, to_port=None,
     def get_rule(self, ip_protocol=None, from_port=None, to_port=None,
                  cidr_ip=None, src_group=None):
                  cidr_ip=None, src_group=None):
-        # Update SG object; otherwise, recently added rules do now show
+        # Update SG object; otherwise, recently added rules do not show
         self._security_group = self._provider.nova.security_groups.get(
         self._security_group = self._provider.nova.security_groups.get(
             self._security_group)
             self._security_group)
         for rule in self._security_group.rules:
         for rule in self._security_group.rules:
             if (rule['ip_protocol'] == ip_protocol and
             if (rule['ip_protocol'] == ip_protocol and
                 rule['from_port'] == from_port and
                 rule['from_port'] == from_port and
                 rule['to_port'] == to_port and
                 rule['to_port'] == to_port and
-                rule['ip_range'].get('cidr') == cidr_ip) or \
-               (rule['group'].get('name') == src_group.name if src_group
-                    else False):
+                (rule['ip_range'].get('cidr') == cidr_ip or
+                 (rule['group'].get('name') == src_group.name if src_group
+                  else False))):
                 return OpenStackSecurityGroupRule(self._provider, rule, self)
                 return OpenStackSecurityGroupRule(self._provider, rule, self)
         return None
         return None
 
 
@@ -1023,9 +1062,7 @@ class OpenStackBucketObject(BaseBucketObject):
 
 
     @property
     @property
     def name(self):
     def name(self):
-        """
-        Get this object's name.
-        """
+        """Get this object's name."""
         return self._obj.get("name")
         return self._obj.get("name")
 
 
     @property
     @property
@@ -1037,10 +1074,7 @@ class OpenStackBucketObject(BaseBucketObject):
         return self._obj.get("last_modified")
         return self._obj.get("last_modified")
 
 
     def iter_content(self):
     def iter_content(self):
-        """
-        Returns this object's content as an
-        iterable.
-        """
+        """Returns this object's content as an iterable."""
         _, content = self._provider.swift.get_object(
         _, content = self._provider.swift.get_object(
             self.cbcontainer.name, self.name, resp_chunk_size=65536)
             self.cbcontainer.name, self.name, resp_chunk_size=65536)
         return content
         return content
@@ -1049,24 +1083,81 @@ class OpenStackBucketObject(BaseBucketObject):
         """
         """
         Set the contents of this object to the data read from the source
         Set the contents of this object to the data read from the source
         string.
         string.
+
+        .. warning:: Will fail if the data is larger than 5 Gig.
         """
         """
         self._provider.swift.put_object(self.cbcontainer.name, self.name,
         self._provider.swift.put_object(self.cbcontainer.name, self.name,
                                         data)
                                         data)
 
 
+    def upload_from_file(self, path):
+        """
+        Stores the contents of the file pointed by the ``path`` variable.
+        If the file is bigger than 5 Gig, it will be broken into segments.
+
+        :type path: ``str``
+        :param path: Absolute path to the file to be uploaded to Swift.
+        :rtype: ``bool``
+        :return: ``True`` if successful, ``False`` if not.
+
+        .. note::
+            * The size of the segments chosen (or any of the other upload
+              options) is not under user control.
+            * If called this method will remap the
+              ``swiftclient.service.get_conn`` factory method to
+              ``self._provider._connect_swift``
+
+        .. seealso:: https://github.com/gvlproject/cloudbridge/issues/35#issuecomment-297629661 # noqa
+        """
+        upload_options = {}
+        if 'segment_size' not in upload_options:
+            if os.path.getsize(path) >= FIVE_GIG:
+                upload_options['segment_size'] = FIVE_GIG
+
+        # remap the swift service's connection factory method
+        swiftclient.service.get_conn = self._provider._connect_swift
+
+        result = True
+        with SwiftService() as swift:
+            upload_object = SwiftUploadObject(path, object_name=self.name)
+            for up_res in swift.upload(self.cbcontainer.name,
+                                       [upload_object, ],
+                                       options=upload_options):
+                result = result and up_res['success']
+        return result
+
     def delete(self):
     def delete(self):
         """
         """
         Delete this object.
         Delete this object.
 
 
-        :rtype: bool
+        :rtype: ``bool``
         :return: True if successful
         :return: True if successful
+
+        .. note:: If called this method will remap the
+              ``swiftclient.service.get_conn`` factory method to
+              ``self._provider._connect_swift``
         """
         """
-        try:
-            self._provider.swift.delete_object(self.cbcontainer.name,
-                                               self.name)
-        except swiftex.ClientException as err:
-            if err.http_status == 404:
-                return True
-        return False
+
+        # remap the swift service's connection factory method
+        swiftclient.service.get_conn = self._provider._connect_swift
+
+        result = True
+        with SwiftService() as swift:
+            for del_res in swift.delete(self.cbcontainer.name, [self.name, ]):
+                result = result and del_res['success']
+        return result
+
+    def generate_url(self, expires_in=0):
+        """
+        Generates a URL to this object.
+
+        If the object is public, `expires_in` argument is not necessary, but if
+        the object is private, the life time of URL is set using `expires_in`
+        argument.
+
+        See here for implementation details:
+        http://stackoverflow.com/a/37057172
+        """
+        raise NotImplementedError("This functionality is not implemented yet.")
 
 
 
 
 class OpenStackBucket(BaseBucket):
 class OpenStackBucket(BaseBucket):
@@ -1086,19 +1177,23 @@ class OpenStackBucket(BaseBucket):
         """
         """
         return self._bucket.get("name")
         return self._bucket.get("name")
 
 
-    def get(self, key):
+    def get(self, name):
         """
         """
         Retrieve a given object from this bucket.
         Retrieve a given object from this bucket.
+
+        FIXME: If multiple objects match the name as their name prefix,
+        all will be returned by the provider but this method will only
+        return the first element.
         """
         """
         _, object_list = self._provider.swift.get_container(
         _, object_list = self._provider.swift.get_container(
-            self.name, prefix=key)
+            self.name, prefix=name)
         if object_list:
         if object_list:
             return OpenStackBucketObject(self._provider, self,
             return OpenStackBucketObject(self._provider, self,
                                          object_list[0])
                                          object_list[0])
         else:
         else:
             return None
             return None
 
 
-    def list(self, limit=None, marker=None):
+    def list(self, limit=None, marker=None, prefix=None):
         """
         """
         List all objects within this bucket.
         List all objects within this bucket.
 
 
@@ -1107,7 +1202,7 @@ class OpenStackBucket(BaseBucket):
         """
         """
         _, object_list = self._provider.swift.get_container(
         _, object_list = self._provider.swift.get_container(
             self.name, limit=oshelpers.os_result_limit(self._provider, limit),
             self.name, limit=oshelpers.os_result_limit(self._provider, limit),
-            marker=marker)
+            marker=marker, prefix=prefix)
         cb_objects = [OpenStackBucketObject(
         cb_objects = [OpenStackBucketObject(
             self._provider, self, obj) for obj in object_list]
             self._provider, self, obj) for obj in object_list]
 
 

+ 43 - 45
cloudbridge/cloud/providers/openstack/services.py

@@ -26,13 +26,15 @@ from cloudbridge.cloud.base.services import BaseVolumeService
 from cloudbridge.cloud.interfaces.resources import InstanceType
 from cloudbridge.cloud.interfaces.resources import InstanceType
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import KeyPair
 from cloudbridge.cloud.interfaces.resources import MachineImage
 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 PlacementZone
 from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.interfaces.resources import Snapshot
 from cloudbridge.cloud.interfaces.resources import Snapshot
+from cloudbridge.cloud.interfaces.resources import Subnet
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 from cloudbridge.cloud.providers.openstack import helpers as oshelpers
 
 
+from neutronclient.common.exceptions import NeutronClientException
+
 from novaclient.exceptions import NotFound as NovaNotFound
 from novaclient.exceptions import NotFound as NovaNotFound
 
 
 from .resources import OpenStackBucket
 from .resources import OpenStackBucket
@@ -81,10 +83,10 @@ class OpenStackSecurityService(BaseSecurityService):
         """
         """
         return self._security_groups
         return self._security_groups
 
 
-    def get_ec2_credentials(self):
+    def get_or_create_ec2_credentials(self):
         """
         """
         A provider specific method than returns the ec2 credentials for the
         A provider specific method than returns the ec2 credentials for the
-        current user.
+        current user, or creates a new pair if one doesn't exist.
         """
         """
         keystone = self.provider.keystone
         keystone = self.provider.keystone
         if hasattr(keystone, 'ec2'):
         if hasattr(keystone, 'ec2'):
@@ -92,6 +94,10 @@ class OpenStackSecurityService(BaseSecurityService):
                           if cred.tenant_id == keystone.tenant_id]
                           if cred.tenant_id == keystone.tenant_id]
             if user_creds:
             if user_creds:
                 return user_creds[0]
                 return user_creds[0]
+            else:
+                return keystone.ec2.create(keystone.user_id,
+                                           keystone.tenant_id)
+
         return None
         return None
 
 
     def get_ec2_endpoints(self):
     def get_ec2_endpoints(self):
@@ -558,7 +564,7 @@ class OpenStackInstanceService(BaseInstanceService):
     def __init__(self, provider):
     def __init__(self, provider):
         super(OpenStackInstanceService, self).__init__(provider)
         super(OpenStackInstanceService, self).__init__(provider)
 
 
-    def create(self, name, image, instance_type, network=None, zone=None,
+    def create(self, name, image, instance_type, subnet, zone=None,
                key_pair=None, security_groups=None, user_data=None,
                key_pair=None, security_groups=None, user_data=None,
                launch_config=None,
                launch_config=None,
                **kwargs):
                **kwargs):
@@ -568,7 +574,10 @@ class OpenStackInstanceService(BaseInstanceService):
             isinstance(instance_type, InstanceType) else \
             isinstance(instance_type, InstanceType) else \
             self.provider.compute.instance_types.find(
             self.provider.compute.instance_types.find(
                 name=instance_type)[0].id
                 name=instance_type)[0].id
-        network_id = network.id if isinstance(network, Network) else network
+        network_id = subnet.network_id if isinstance(subnet, Subnet) else None
+        if not network_id and subnet:
+            network_id = (self.provider.network.subnets.get(subnet).network_id
+                          if isinstance(subnet, str) else None)
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         zone_id = zone.id if isinstance(zone, PlacementZone) else zone
         key_pair_name = key_pair.name if \
         key_pair_name = key_pair.name if \
             isinstance(key_pair, KeyPair) else key_pair
             isinstance(key_pair, KeyPair) else key_pair
@@ -583,8 +592,8 @@ class OpenStackInstanceService(BaseInstanceService):
         bdm = None
         bdm = None
         if launch_config:
         if launch_config:
             bdm = self._to_block_device_mapping(launch_config)
             bdm = self._to_block_device_mapping(launch_config)
-        net = self._get_network(network_id)
 
 
+        log.debug("Launching in network %s" % network_id)
         os_instance = self.provider.nova.servers.create(
         os_instance = self.provider.nova.servers.create(
             name,
             name,
             None if self._has_root_device(launch_config) else image_id,
             None if self._has_root_device(launch_config) else image_id,
@@ -596,7 +605,7 @@ class OpenStackInstanceService(BaseInstanceService):
             security_groups=security_groups_list,
             security_groups=security_groups_list,
             userdata=user_data,
             userdata=user_data,
             block_device_mapping_v2=bdm,
             block_device_mapping_v2=bdm,
-            nics=net)
+            nics=[{'net-id': network_id}] if network_id else None)
         return OpenStackInstance(self.provider, os_instance)
         return OpenStackInstance(self.provider, os_instance)
 
 
     def _to_block_device_mapping(self, launch_config):
     def _to_block_device_mapping(self, launch_config):
@@ -649,43 +658,6 @@ class OpenStackInstanceService(BaseInstanceService):
                 return True
                 return True
         return False
         return False
 
 
-    def _get_network(self, network_id=None):
-        """
-        Format the network ID for the API call, figuring out a default network.
-
-        If a network_id is not supplied, figure out which is the default
-        network and use it. A default network is either marked as such by the
-        provider or matches the default network name defined within this
-        library (by default CloudBridgeNet). If a default network cannot be
-        found, attempt to create a new one.
-        """
-        if network_id:
-            return [{'net-id': network_id}]
-        for net in self.provider.network.list():
-            if net.name == OpenStackNetwork.CB_DEFAULT_NETWORK_NAME:
-                return [{'net-id': net.id}]
-        try:
-            # Try to create a complete, Internet-connected new network
-            net = self.provider.network.create(
-                name=OpenStackNetwork.CB_DEFAULT_NETWORK_NAME)
-            sn = net.create_subnet('10.0.0.0/16', '{0}Subnet'.format(
-                OpenStackNetwork.CB_DEFAULT_NETWORK_NAME))
-            router = self.provider.network.create_router('{0}Router'.format(
-                OpenStackNetwork.CB_DEFAULT_NETWORK_NAME))
-            for n in self.provider.network.list():
-                if n.external:
-                    external_net = n
-                    break
-            router.attach_network(external_net.id)
-            router.add_route(sn.id)
-            return [{'net-id': net.id}]
-        except Exception as exc:
-            # At this point we assume the provider does support user-defined
-            # networks so return None
-            log.warn("Exception occurred trying to create a default "
-                     "CloudBridge network: {0}".format(exc))
-            return None
-
     def create_launch_config(self):
     def create_launch_config(self):
         return BaseLaunchConfig(self.provider)
         return BaseLaunchConfig(self.provider)
 
 
@@ -796,7 +768,8 @@ class OpenStackSubnetService(BaseSubnetService):
         subnets = self.provider.neutron.list_subnets().get('subnets', [])
         subnets = self.provider.neutron.list_subnets().get('subnets', [])
         return [OpenStackSubnet(self.provider, subnet) for subnet in subnets]
         return [OpenStackSubnet(self.provider, subnet) for subnet in subnets]
 
 
-    def create(self, network, cidr_block, name=''):
+    def create(self, network, cidr_block, name='', zone=None):
+        """zone param is ignored."""
         network_id = (network.id if isinstance(network, OpenStackNetwork)
         network_id = (network.id if isinstance(network, OpenStackNetwork)
                       else network)
                       else network)
         subnet_info = {'name': name, 'network_id': network_id,
         subnet_info = {'name': name, 'network_id': network_id,
@@ -805,6 +778,31 @@ class OpenStackSubnetService(BaseSubnetService):
                   .get('subnet'))
                   .get('subnet'))
         return OpenStackSubnet(self.provider, subnet)
         return OpenStackSubnet(self.provider, subnet)
 
 
+    def get_or_create_default(self, zone=None):
+        """
+        Subnet zone is not supported by OpenStack and is thus ignored.
+        """
+        try:
+            for sn in self.list():
+                if sn.name == OpenStackSubnet.CB_DEFAULT_SUBNET_NAME:
+                    return sn
+            # No default; create one
+            net = self.provider.network.create(
+                OpenStackNetwork.CB_DEFAULT_NETWORK_NAME)
+            sn = net.create_subnet(cidr_block='10.0.0.0/24',
+                                   name=OpenStackSubnet.CB_DEFAULT_SUBNET_NAME)
+            router = self.provider.network.create_router(
+                OpenStackRouter.CB_DEFAULT_ROUTER_NAME)
+            for n in self.provider.network.list():
+                if n.external:
+                    external_net = n
+                    break
+            router.attach_network(external_net.id)
+            router.add_route(sn.id)
+            return sn
+        except NeutronClientException:
+            return None
+
     def delete(self, subnet):
     def delete(self, subnet):
         subnet_id = (subnet.id if isinstance(subnet, OpenStackSubnet)
         subnet_id = (subnet.id if isinstance(subnet, OpenStackSubnet)
                      else subnet)
                      else subnet)

+ 19 - 0
docs/api_docs/cloud/exceptions.rst

@@ -0,0 +1,19 @@
+Exceptions
+==========
+
+.. contents:: :local:
+
+CloudBridgeBaseException
+------------------------
+.. autoclass:: cloudbridge.cloud.interfaces.exceptions.CloudBridgeBaseException
+    :members:
+
+WaitStateException
+------------------
+.. autoclass:: cloudbridge.cloud.interfaces.exceptions.WaitStateException
+    :members:
+
+InvalidConfigurationException
+-----------------------------
+.. autoclass:: cloudbridge.cloud.interfaces.exceptions.InvalidConfigurationException
+    :members:

+ 0 - 15
docs/api_docs/cloud/resources.rst

@@ -8,21 +8,6 @@ CloudServiceType
 .. autoclass:: cloudbridge.cloud.interfaces.resources.CloudServiceType
 .. autoclass:: cloudbridge.cloud.interfaces.resources.CloudServiceType
     :members:
     :members:
 
 
-CloudBridgeBaseException
-------------------------
-.. autoclass:: cloudbridge.cloud.interfaces.resources.CloudBridgeBaseException
-    :members:
-
-WaitStateException
-------------------
-.. autoclass:: cloudbridge.cloud.interfaces.resources.WaitStateException
-    :members:
-
-InvalidConfigurationException
------------------------------
-.. autoclass:: cloudbridge.cloud.interfaces.resources.InvalidConfigurationException
-    :members:
-
 ObjectLifeCycleMixin
 ObjectLifeCycleMixin
 --------------------
 --------------------
 .. autoclass:: cloudbridge.cloud.interfaces.resources.ObjectLifeCycleMixin
 .. autoclass:: cloudbridge.cloud.interfaces.resources.ObjectLifeCycleMixin

+ 1 - 0
docs/api_docs/ref.rst

@@ -10,3 +10,4 @@ This section includes the API documentation for the reference interface.
    cloud/providers.rst
    cloud/providers.rst
    cloud/services.rst
    cloud/services.rst
    cloud/resources.rst
    cloud/resources.rst
+   cloud/exceptions.rst

+ 5 - 1
docs/getting_started.rst

@@ -94,9 +94,13 @@ on disk as a read-only file.
 Create a security group
 Create a security group
 -----------------------
 -----------------------
 Next, we need to create a security group and add a rule to allow ssh access.
 Next, we need to create a security group and add a rule to allow ssh access.
+A security group needs to be associated with a private network, so we'll also
+need to fetch it.
 
 
 .. code-block:: python
 .. code-block:: python
 
 
+    provider.network.list()  # Find a desired network ID
+    net = provider.network.get('desired network ID')
     sg = provider.security.security_groups.create(
     sg = provider.security.security_groups.create(
         'cloudbridge_intro', 'A security group used by CloudBridge', net.id)
         'cloudbridge_intro', 'A security group used by CloudBridge', net.id)
     sg.add_rule('tcp', 22, 22, '0.0.0.0/0')
     sg.add_rule('tcp', 22, 22, '0.0.0.0/0')
@@ -115,7 +119,7 @@ also add the network interface as a launch argument.
                        key=lambda x: x.vcpus*x.ram)[0]
                        key=lambda x: x.vcpus*x.ram)[0]
     inst = provider.compute.instances.create(
     inst = provider.compute.instances.create(
         name='CloudBridge-intro', image=img, instance_type=inst_type,
         name='CloudBridge-intro', image=img, instance_type=inst_type,
-        key_pair=kp, security_groups=[sg])
+        network=net, key_pair=kp, security_groups=[sg])
     # Wait until ready
     # Wait until ready
     inst.wait_till_ready()  # This is a blocking call
     inst.wait_till_ready()  # This is a blocking call
     # Show instance state
     # Show instance state

+ 2 - 1
docs/topics/contributor_guide.rst

@@ -9,5 +9,6 @@ CloudBridge Provider.
 
 
     Design Goals <design_goals.rst>
     Design Goals <design_goals.rst>
     Testing <testing.rst>
     Testing <testing.rst>
-    Provider development walkthrough <provider_development.rst>
+    Provider Development Walkthrough <provider_development.rst>
+    Release Process <release_process.rst>
 
 

+ 5 - 4
docs/topics/launch.rst

@@ -14,7 +14,7 @@ and 4 GB RAM.
 
 
 .. code-block:: python
 .. code-block:: python
 
 
-    img = provider.compute.images.get('ami-5ac2cd4d')  # Ubuntu 14.04 on AWS
+    img = provider.compute.images.get('ami-f4cc1de2')  # Ubuntu 16.04 on AWS
     inst_type = sorted([t for t in provider.compute.instance_types.list()
     inst_type = sorted([t for t in provider.compute.instance_types.list()
                         if t.vcpus >= 2 and t.ram >= 4],
                         if t.vcpus >= 2 and t.ram >= 4],
                        key=lambda x: x.vcpus*x.ram)[0]
                        key=lambda x: x.vcpus*x.ram)[0]
@@ -45,16 +45,17 @@ Private networking
 ~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~
 Private networking gives you control over the networking setup for your
 Private networking gives you control over the networking setup for your
 instance(s) and is considered the preferred method for launching instances. To
 instance(s) and is considered the preferred method for launching instances. To
-launch an instance with an explicit private network, just supply it as an
-additional argument to the ``create`` method:
+launch an instance with an explicit private network, supply a subnet within
+a network as an additional argument to the ``create`` method:
 
 
 .. code-block:: python
 .. code-block:: python
 
 
     provider.network.list()  # Find a desired network ID
     provider.network.list()  # Find a desired network ID
     net = provider.network.get('desired network ID')
     net = provider.network.get('desired network ID')
+    sn = net.subnets()[0]  # Get a handle on the desired subnet to launch with
     inst = provider.compute.instances.create(
     inst = provider.compute.instances.create(
         name='CloudBridge-VPC', image=img, instance_type=inst_type,
         name='CloudBridge-VPC', image=img, instance_type=inst_type,
-        network=net, key_pair=kp, security_groups=[sg])
+        subnet=sn, key_pair=kp, security_groups=[sg])
 
 
 For more information on how to create and setup a private network, take a look
 For more information on how to create and setup a private network, take a look
 at `Networking <./networking.html>`_.
 at `Networking <./networking.html>`_.

+ 4 - 4
docs/topics/networking.rst

@@ -15,9 +15,9 @@ Create a new private network
 ----------------------------
 ----------------------------
 Creating a private network is a simple, one-line command but appropriately
 Creating a private network is a simple, one-line command but appropriately
 connecting it so it has Internet access is a multi-step process:
 connecting it so it has Internet access is a multi-step process:
-(1) create a network; (2) create a subnet within the network; (3) create a
+(1) create a network; (2) create a subnet within this network; (3) create a
 router; (4) attach the router to an external network; and (5) add a route to
 router; (4) attach the router to an external network; and (5) add a route to
-the router that links with with a subnet. For some providers, any network can
+the router that links with a subnet. For some providers, any network can
 be external (ie, connected to the Internet) while for others it's a specific,
 be external (ie, connected to the Internet) while for others it's a specific,
 pre-defined one that exists in the an account by default. In order to properly
 pre-defined one that exists in the an account by default. In order to properly
 connect the router, we need to ensure we're using an external network.
 connect the router, we need to ensure we're using an external network.
@@ -35,9 +35,9 @@ the block and allow up to 16 IP addresses into the subnet (``/28``).
     if not net.external:
     if not net.external:
         for n in self.provider.network.list():
         for n in self.provider.network.list():
             if n.external:
             if n.external:
-                external_net = n
+                net = n
                 break
                 break
-    router.attach_network(external_net.id)
+    router.attach_network(net.id)
     router.add_route(sn.id)
     router.add_route(sn.id)
 
 
 Retrieve an existing private network
 Retrieve an existing private network

+ 21 - 0
docs/topics/release_process.rst

@@ -0,0 +1,21 @@
+Release Process
+~~~~~~~~~~~~~~~
+
+1. Increment version number in cloudbridge/__init__.py as per semver rules.
+
+2. Freeze all library dependencies in setup.py. The version numbers can be a range
+   with the upper limit being the latest known working version, and the lowest being
+   the last known working version. 
+
+3. Run all tox tests.
+
+4. Add release notes to CHANGELOG.rst. Also add last commit hash to changelog.
+
+5. Release to PyPi
+
+.. code-block:: bash
+
+   python setup.py sdist upload
+   python setup.py bdist_wheel upload
+
+6. Tag release and make github release.

+ 48 - 14
docs/topics/setup.rst

@@ -2,10 +2,11 @@ Setup
 -----
 -----
 To initialize a connection to a cloud and get a provider object, you will
 To initialize a connection to a cloud and get a provider object, you will
 need to provide the cloud's access credentials to CloudBridge. These may
 need to provide the cloud's access credentials to CloudBridge. These may
-be provided in one of two ways:
+be provided in one of following ways:
 
 
 1. Environment variables
 1. Environment variables
 2. A dictionary
 2. A dictionary
+3. Configuration file
 
 
 Providing access credentials through environment variables
 Providing access credentials through environment variables
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -28,8 +29,8 @@ Mandatory variables  Optional Variables
 OS_AUTH_URL			 NOVA_SERVICE_NAME
 OS_AUTH_URL			 NOVA_SERVICE_NAME
 OS_USERNAME			 OS_COMPUTE_API_VERSION
 OS_USERNAME			 OS_COMPUTE_API_VERSION
 OS_PASSWORD			 OS_VOLUME_API_VERSION
 OS_PASSWORD			 OS_VOLUME_API_VERSION
-OS_PROJECT_NAME
-OS_REGION_NAME
+OS_PROJECT_NAME      OS_STORAGE_URL
+OS_REGION_NAME       OS_AUTH_TOKEN
 ===================  ==================
 ===================  ==================
 
 
 
 
@@ -74,6 +75,8 @@ default_result_limit  Number of results that a ``.list()`` method should return.
 ====================  ==================
 ====================  ==================
 Variable		      Description
 Variable		      Description
 ====================  ==================
 ====================  ==================
+aws_session_token     Session key for your AWS account (if using temporary
+                      credentials).
 ec2_is_secure         True to use an SSL connection. Default is ``True``.
 ec2_is_secure         True to use an SSL connection. Default is ``True``.
 ec2_region_name       Default region name. Defaults to ``us-east-1``.
 ec2_region_name       Default region name. Defaults to ``us-east-1``.
 ec2_region_endpoint   Endpoint to use. Default is ``ec2.us-east-1.amazonaws.com``.
 ec2_region_endpoint   Endpoint to use. Default is ``ec2.us-east-1.amazonaws.com``.
@@ -92,20 +95,51 @@ s3_validate_certs     Whether to use SSL certificate verification. Default is
 ====================  ==================
 ====================  ==================
 
 
 
 
+Providing access credentials in a file
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+CloudBridge can also read credentials from a file on your local file system.
+The file should be placed in one of two locations: ``/etc/cloudbridge.ini`` or
+``~/.cloudbridge``. Each set of credentials should be delineated with the
+provider ID (e.g., ``openstack``, ``aws``) with the necessary credentials
+being supplied in YAML format. Note that only one set of credentials per
+cloud provider type can be supplied (i.e., via this method, it is not possible
+to provide credentials for two different OpenStack clouds).
+
+.. code-block:: bash
+
+    [openstack]
+    os_username: username
+    os_password: password
+    os_auth_url: auth url
+    os_user_domain_name: user domain name
+    os_project_domain_name: project domain name
+    os_project_name: project name
+
+    [aws]
+    aws_access_key: access key
+    aws_secret_key: secret key
+
+
 Other configuration variables
 Other configuration variables
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 In addition to the provider specific configuration variables above, there are
 In addition to the provider specific configuration variables above, there are
 some general configuration environment variables that apply to CloudBridge as
 some general configuration environment variables that apply to CloudBridge as
 a whole
 a whole
 
 
-=====================  ==================
-Variable		       Description
-=====================  ==================
-CB_DEBUG               Setting ``CB_DEBUG=True`` will cause detailed debug
-                       output to be printed for each provider (including HTTP
-                       traces).
-CB_USE_MOCK_PROVIDERS  Setting this to ``True`` will cause the CloudBridge test
-                       suite to use mock drivers when available.
-CB_TEST_PROVIDER       Set this value to a valid :class:`.ProviderList` value
-                       such as ``aws``, to limit tests to that provider only.
-=====================  ==================
+======================  ==================
+Variable		            Description
+======================  ==================
+CB_DEBUG                Setting ``CB_DEBUG=True`` will cause detailed debug
+                        output to be printed for each provider (including HTTP
+                        traces).
+CB_USE_MOCK_PROVIDERS   Setting this to ``True`` will cause the CloudBridge test
+                        suite to use mock drivers when available.
+CB_TEST_PROVIDER        Set this value to a valid :class:`.ProviderList` value
+                        such as ``aws``, to limit tests to that provider only.
+CB_DEFAULT_SUBNET_NAME  Name to be used for a subnet that will be considered
+                        the 'default' by the library. This default will be used
+                        only in cases there is no subnet marked as the default by the provider.
+CB_DEFAULT_NETWORK_NAME Name to be used for a network that will be considered
+                        the 'default' by the library. This default will be used
+                        only in cases there is no network marked as the default by the provider.
+======================= ==================

+ 23 - 10
docs/topics/testing.rst

@@ -38,18 +38,31 @@ This will run all the tests for all the environments defined in file
 ``tox.ini``.
 ``tox.ini``.
 
 
 
 
-Specific environment
-~~~~~~~~~~~~~~~~~~~~
+Specific environment and infrastructure
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 If you’d like to run the tests on a specific environment only, say Python 2.7,
 If you’d like to run the tests on a specific environment only, say Python 2.7,
-use a command like this: ``tox -e py27``. Alternativley, to use your default
-python, you can also run the test command directly ``python setup.py test``.
+against a specific infrastructure, say aws, use a command like this:
+``tox -e py27-aws``. The available provider names are listed in the
+`ProviderList`_ class (e.g., ``aws`` or ``openstack``).  
 
 
-Select infrastructure
-~~~~~~~~~~~~~~~~~~~~~
-You can also run the tests on a specific cloud only. To do so, export an
-environment variable ``CB_TEST_PROVIDER`` and specify the desired provider
-name. The available provider names are listed in the `ProviderList`_ class
-(e.g., ``aws`` or ``openstack``). Then, run the ``tox`` command.
+Specific test cases
+~~~~~~~~~~~~~~~~~~~~
+You can run a specific test case, as follows:
+``tox -- -s test.test_cloud_factory.CloudFactoryTestCase``
+
+It can also be restricted to a particular environment as follows:
+``tox -e "py27-aws" -- -s test.test_cloud_factory.CloudFactoryTestCase``
+
+Using unittest directly
+~~~~~~~~~~~~~~~~~~~~~~~
+You can also run the tests against your active virtual environment directly
+with ``python setup.py test``. You will need to set the ``CB_TEST_PROVIDER``
+and ``CB_USE_MOCK_PROVIDERS`` environment variables prior to running the tests,
+or they will default to ``CB_TEST_PROVIDER=aws`` and
+``CB_USE_MOCK_PROVIDERS=True``.
+
+You can also run a specific test case, as follows:
+``python setup.py test -s test.test_cloud_factory.CloudFactoryTestCase``
 
 
 Using a mock provider
 Using a mock provider
 ~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~

+ 18 - 11
setup.py

@@ -1,7 +1,9 @@
+"""Library install script for setuptools."""
 import ast
 import ast
 import os
 import os
 import re
 import re
-from setuptools import setup, find_packages
+
+from setuptools import find_packages, setup
 
 
 # Cannot use "from cloudbridge import get_version" because that would try to
 # Cannot use "from cloudbridge import get_version" because that would try to
 # import the six package which may not be installed yet.
 # import the six package which may not be installed yet.
@@ -14,16 +16,20 @@ with open(os.path.join('cloudbridge', '__init__.py')) as f:
             break
             break
 
 
 base_reqs = ['bunch>=1.0.1', 'six>=1.10.0', 'retrying>=1.3.3']
 base_reqs = ['bunch>=1.0.1', 'six>=1.10.0', 'retrying>=1.3.3']
-openstack_reqs = ['python-novaclient>=7.0.0',
-                  'python-glanceclient>=2.5.0',
-                  'python-cinderclient>=1.9.0',
-                  'python-swiftclient>=3.2.0',
-                  'python-neutronclient>=6.0.0',
-                  'python-keystoneclient>=3.8.0']
-aws_reqs = ['boto>=2.38.0']
-gce_reqs = ['google-api-python-client>=1.4.2', "pycrypto"]
+openstack_reqs = ['requests<2.13.0',
+                  'Babel>=2.3.4,<2.4.0',
+                  'python-novaclient==7.0.0',
+                  'python-glanceclient>=2.5.0,<=2.6.0',
+                  'python-cinderclient>=1.9.0,<=2.0.1',
+                  'python-swiftclient>=3.2.0,<=3.3.0',
+                  'python-neutronclient>=6.0.0,<=6.1.0',
+                  'python-keystoneclient>=3.8.0,<=3.10.0']
+aws_reqs = ['boto>=2.38.0,<=2.46.1']
+gce_reqs = ['google-api-python-client>=1.4.2', "cryptography>=1.4"]
 full_reqs = base_reqs + aws_reqs + openstack_reqs + gce_reqs
 full_reqs = base_reqs + aws_reqs + openstack_reqs + gce_reqs
-dev_reqs = (['tox>=2.1.1', 'moto>=0.4.18', 'sphinx>=1.3.1'] + full_reqs)
+# httpretty is required with/for moto 1.0.0 or AWS tests fail
+dev_reqs = (['tox>=2.1.1', 'moto<1.0.0', 'sphinx>=1.3.1', 'flake8>=3.3.0',
+             'flake8-import-order>=0.12', 'httpretty==0.8.10'] + full_reqs)
 
 
 setup(name='cloudbridge',
 setup(name='cloudbridge',
       version=version,
       version=version,
@@ -42,7 +48,7 @@ setup(name='cloudbridge',
       packages=find_packages(),
       packages=find_packages(),
       license='MIT',
       license='MIT',
       classifiers=[
       classifiers=[
-          'Development Status :: 3 - Alpha',
+          'Development Status :: 4 - Beta',
           'Environment :: Console',
           'Environment :: Console',
           'Intended Audience :: Developers',
           'Intended Audience :: Developers',
           'Intended Audience :: System Administrators',
           'Intended Audience :: System Administrators',
@@ -54,6 +60,7 @@ setup(name='cloudbridge',
           'Programming Language :: Python :: 3',
           'Programming Language :: Python :: 3',
           'Programming Language :: Python :: 3.4',
           'Programming Language :: Python :: 3.4',
           'Programming Language :: Python :: 3.5',
           'Programming Language :: Python :: 3.5',
+          'Programming Language :: Python :: 3.6',
           'Programming Language :: Python :: Implementation :: CPython',
           'Programming Language :: Python :: Implementation :: CPython',
           'Programming Language :: Python :: Implementation :: PyPy'],
           'Programming Language :: Python :: Implementation :: PyPy'],
       test_suite="test"
       test_suite="test"

+ 4 - 59
test/__init__.py

@@ -1,64 +1,9 @@
 """
 """
-Tests the functionality of each provider implementation against registered
-test cases. These tests require that provider credentials are set as
-environment variables, as required for each provider (see `tox.ini` for a list
-of env variables).
-
-Since the tests exercise the ``cloudbridge`` interfaces, and there are multiple
-implementations of these interfaces, for m interfaces and n implementation,
-exercising all interfaces means that m*n test case classes are needed.
-Otherwise, the standard test runners such as unittest and nose2 do not
-correctly pick up the tests.
-
-To avoid an explosion of repetitive test cases, the
-``ProviderTestCaseGenerator`` class will automatically generate a new Python
-class for each combination of test and provider. The ``load_tests`` protocol
-(https://docs.python.org/2/library/unittest.html#load-tests-protocol)
-is used to aid test discovery.
-
 Use ``python setup.py test`` to run these unit tests (alternatively, use
 Use ``python setup.py test`` to run these unit tests (alternatively, use
 ``python -m unittest test``).
 ``python -m unittest test``).
 
 
-All test cases need to be registered below, and available providers will be
-discovered through the ``ProviderFactory``. Test Cases must not inherit from
-``unittest.TestCase``, to avoid confusing unittest and nose2's automatic
-discovery. (The test generator will automatically add ``unittest.TestCase``
-as a base class to each combination).
+You must set the CB_TEST_PROVIDER environment variable before running the
+tests. Otherwise, the test suite will default to running against the mock
+aws provider. Alternatively, use tox, which will run tests for all available
+provider combinations.
 """
 """
-import cloudbridge
-from test.helpers import ProviderTestCaseGenerator
-from test.test_block_store_service import CloudBlockStoreServiceTestCase
-from test.test_cloud_helpers import CloudHelpersTestCase
-from test.test_compute_service import CloudComputeServiceTestCase
-from test.test_image_service import CloudImageServiceTestCase
-from test.test_instance_types_service import CloudInstanceTypesServiceTestCase
-from test.test_interface import CloudInterfaceTestCase
-from test.test_network_service import CloudNetworkServiceTestCase
-from test.test_object_life_cycle import CloudObjectLifeCycleTestCase
-from test.test_object_store_service import CloudObjectStoreServiceTestCase
-from test.test_region_service import CloudRegionServiceTestCase
-from test.test_security_service import CloudSecurityServiceTestCase
-
-
-PROVIDER_TESTS = [
-    CloudHelpersTestCase,
-    CloudInterfaceTestCase,
-    CloudObjectLifeCycleTestCase,
-    CloudSecurityServiceTestCase,
-    CloudNetworkServiceTestCase,
-    CloudInstanceTypesServiceTestCase,
-    CloudBlockStoreServiceTestCase,
-    CloudObjectStoreServiceTestCase,
-    CloudComputeServiceTestCase,
-    CloudRegionServiceTestCase,
-    CloudImageServiceTestCase
-]
-
-
-def load_tests(loader=None, tests=None, pattern=None):
-    """
-    This function is required to aid the load_tests protocol
-    (https://docs.python.org/2/library/unittest.html#load-tests-protocol)
-    """
-    cloudbridge.init_logging()
-    return ProviderTestCaseGenerator(PROVIDER_TESTS).generate_tests()

BIN
test/fixtures/logo.jpg


+ 67 - 111
test/helpers.py

@@ -1,14 +1,16 @@
-from contextlib import contextmanager
+import functools
 import os
 import os
 import sys
 import sys
 import unittest
 import unittest
 
 
-from six import reraise
+from contextlib import contextmanager
 
 
 from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
 
+from six import reraise
+
 
 
 def parse_bool(val):
 def parse_bool(val):
     if val:
     if val:
@@ -50,6 +52,28 @@ def cleanup_action(cleanup_func):
         print("Error during cleanup: {0}".format(e))
         print("Error during cleanup: {0}".format(e))
 
 
 
 
+def skipIfNoService(services):
+    """
+    A decorator for skipping tests if the provider
+    does not implement a given service.
+    """
+    def wrap(func):
+        """
+        The actual wrapper
+        """
+        @functools.wraps(func)
+        def wrapper(self, *args, **kwargs):
+            provider = getattr(self, 'provider')
+            if provider:
+                for service in services:
+                    if not provider.has_service(service):
+                        self.skipTest("Skipping test because '%s' service is"
+                                      " not implemented" % (service,))
+            func(self, *args, **kwargs)
+        return wrapper
+    return wrap
+
+
 TEST_DATA_CONFIG = {
 TEST_DATA_CONFIG = {
     "AWSCloudProvider": {
     "AWSCloudProvider": {
         "image": os.environ.get('CB_IMAGE_AWS', 'ami-5ac2cd4d'),
         "image": os.environ.get('CB_IMAGE_AWS', 'ami-5ac2cd4d'),
@@ -58,7 +82,7 @@ TEST_DATA_CONFIG = {
     },
     },
     "OpenStackCloudProvider": {
     "OpenStackCloudProvider": {
         "image": os.environ.get('CB_IMAGE_OS',
         "image": os.environ.get('CB_IMAGE_OS',
-                                'a471339a-bd0e-41e2-9406-4f308267ed0f'),
+                                '842b949c-ea76-48df-998d-8a41f2626243'),
         "instance_type": os.environ.get('CB_INSTANCE_TYPE_OS', 'm1.tiny'),
         "instance_type": os.environ.get('CB_INSTANCE_TYPE_OS', 'm1.tiny'),
         "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
         "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
     },
     },
@@ -85,7 +109,8 @@ def create_test_network(provider, name):
     """
     """
     net = provider.network.create(name=name)
     net = provider.network.create(name=name)
     cidr_block = (net.cidr_block).split('/')[0] or '10.0.0.1'
     cidr_block = (net.cidr_block).split('/')[0] or '10.0.0.1'
-    sn = net.create_subnet(cidr_block='{0}/28'.format(cidr_block, name=name))
+    sn = net.create_subnet(cidr_block='{0}/28'.format(cidr_block), name=name,
+                           zone=get_provider_test_data(provider, 'placement'))
     return net, sn
     return net, sn
 
 
 
 
@@ -99,26 +124,26 @@ def delete_test_network(network):
 
 
 
 
 def create_test_instance(
 def create_test_instance(
-        provider, instance_name, network, zone=None, launch_config=None,
+        provider, instance_name, subnet, launch_config=None,
         key_pair=None, security_groups=None):
         key_pair=None, security_groups=None):
     return provider.compute.instances.create(
     return provider.compute.instances.create(
         instance_name,
         instance_name,
         get_provider_test_data(provider, 'image'),
         get_provider_test_data(provider, 'image'),
         get_provider_test_data(provider, 'instance_type'),
         get_provider_test_data(provider, 'instance_type'),
-        network=network,
-        zone=zone,
+        subnet=subnet,
+        zone=get_provider_test_data(provider, 'placement'),
         key_pair=key_pair,
         key_pair=key_pair,
         security_groups=security_groups,
         security_groups=security_groups,
         launch_config=launch_config)
         launch_config=launch_config)
 
 
 
 
 def get_test_instance(provider, name, key_pair=None, security_groups=None,
 def get_test_instance(provider, name, key_pair=None, security_groups=None,
-                      network=None):
+                      subnet=None):
     launch_config = None
     launch_config = None
     instance = create_test_instance(
     instance = create_test_instance(
         provider,
         provider,
         name,
         name,
-        network=network,
+        subnet=subnet,
         key_pair=key_pair,
         key_pair=key_pair,
         security_groups=security_groups,
         security_groups=security_groups,
         launch_config=launch_config)
         launch_config=launch_config)
@@ -126,29 +151,31 @@ def get_test_instance(provider, name, key_pair=None, security_groups=None,
     return instance
     return instance
 
 
 
 
+def get_test_fixtures_folder():
+    return os.path.join(os.path.dirname(__file__), 'fixtures/')
+
+
+def delete_test_instance(instance):
+    if instance:
+        instance.terminate()
+        instance.wait_for([InstanceState.TERMINATED, InstanceState.UNKNOWN],
+                          terminal_states=[InstanceState.ERROR])
+
+
 def cleanup_test_resources(instance=None, network=None, security_group=None,
 def cleanup_test_resources(instance=None, network=None, security_group=None,
                            key_pair=None):
                            key_pair=None):
-    with cleanup_action(lambda: delete_test_network(network)):
-        with cleanup_action(lambda: key_pair.delete()):
-            with cleanup_action(lambda: security_group.delete()):
-                instance.terminate()
-                instance.wait_for(
-                    [InstanceState.TERMINATED, InstanceState.UNKNOWN],
-                    terminal_states=[InstanceState.ERROR])
+    """Clean up any combination of supplied resources."""
+    with cleanup_action(lambda: delete_test_network(network)
+                        if network else None):
+        with cleanup_action(lambda: key_pair.delete() if key_pair else None):
+            with cleanup_action(lambda: security_group.delete()
+                                if security_group else None):
+                delete_test_instance(instance)
 
 
 
 
-class ProviderTestBase(object):
+class ProviderTestBase(unittest.TestCase):
 
 
-    """
-    A dummy base class for Test Cases. Does not inherit from unittest.TestCase
-    to avoid confusing test discovery by unittest and nose2. unittest.TestCase
-    is injected as a base class by the generator, so calling the unittest
-    constructor works correctly.
-    """
-
-    def __init__(self, methodName, provider):
-        unittest.TestCase.__init__(self, methodName=methodName)
-        self.provider = provider
+    _provider = None
 
 
     def setUp(self):
     def setUp(self):
         if isinstance(self.provider, TestMockHelperMixin):
         if isinstance(self.provider, TestMockHelperMixin):
@@ -157,17 +184,7 @@ class ProviderTestBase(object):
     def tearDown(self):
     def tearDown(self):
         if isinstance(self.provider, TestMockHelperMixin):
         if isinstance(self.provider, TestMockHelperMixin):
             self.provider.tearDownMock()
             self.provider.tearDownMock()
-
-
-class ProviderTestCaseGenerator():
-
-    """
-    Generates test cases for all provider - testcase combinations.
-    Detailed docs at test/__init__.py
-    """
-
-    def __init__(self, test_classes):
-        self.all_test_classes = test_classes
+        self._provider = None
 
 
     def get_provider_wait_interval(self, provider_class):
     def get_provider_wait_interval(self, provider_class):
         if issubclass(provider_class, TestMockHelperMixin):
         if issubclass(provider_class, TestMockHelperMixin):
@@ -175,80 +192,19 @@ class ProviderTestCaseGenerator():
         else:
         else:
             return 1
             return 1
 
 
-    def create_provider_instance(self, provider_class):
-        """
-        Instantiate a default provider instance. All required connection
-        settings are expected to be set as environment variables.
-        """
+    def create_provider_instance(self):
+        provider_name = os.environ.get("CB_TEST_PROVIDER", "aws")
+        use_mock_drivers = parse_bool(
+            os.environ.get("CB_USE_MOCK_PROVIDERS", "True"))
+        factory = CloudProviderFactory()
+        provider_class = factory.get_provider_class(provider_name,
+                                                    get_mock=use_mock_drivers)
         config = {'default_wait_interval':
         config = {'default_wait_interval':
                   self.get_provider_wait_interval(provider_class)}
                   self.get_provider_wait_interval(provider_class)}
         return provider_class(config)
         return provider_class(config)
 
 
-    def generate_new_test_class(self, name, testcase_class):
-        """
-        Generates a new type which inherits from the given testcase_class and
-        unittest.TestCase
-        """
-        class_name = "{0}{1}".format(name, testcase_class.__name__)
-        return type(class_name, (testcase_class, unittest.TestCase), {})
-
-    def generate_test_suite_for_provider_testcase(
-            self, provider_class, testcase_class):
-        """
-        Generate and return a suite of tests for a specific provider class and
-        testcase combination
-        """
-        testloader = unittest.TestLoader()
-        testnames = testloader.getTestCaseNames(testcase_class)
-        suite = unittest.TestSuite()
-        for name in testnames:
-            generated_cls = self.generate_new_test_class(
-                provider_class.__name__,
-                testcase_class)
-            suite.addTest(
-                generated_cls(
-                    name,
-                    self.create_provider_instance(provider_class)))
-        return suite
-
-    def generate_test_suite_for_provider(self, provider_class):
-        """
-        Generate and return a suite of all available tests for a given provider
-        class
-        """
-        suite = unittest.TestSuite()
-        suites = [
-            self.generate_test_suite_for_provider_testcase(
-                provider_class, test_class)
-            for test_class in self.all_test_classes]
-        for s in suites:
-            suite.addTest(s)
-        return suite
-
-    def generate_tests(self):
-        """
-        Generate and return a suite of tests for all provider and test class
-        combinations
-        """
-        factory = CloudProviderFactory()
-        use_mock_drivers = parse_bool(
-            os.environ.get("CB_USE_MOCK_PROVIDERS", True))
-        provider_name = os.environ.get("CB_TEST_PROVIDER", None)
-        if provider_name:
-            provider_classes = [
-                factory.get_provider_class(
-                    provider_name,
-                    get_mock=use_mock_drivers)]
-            if not provider_classes[0]:
-                raise ValueError(
-                    "Could not find specified test provider %s" %
-                    provider_name)
-        else:
-            provider_classes = factory.get_all_provider_classes(
-                get_mock=use_mock_drivers)
-        suite = unittest.TestSuite()
-        suites = [
-            self.generate_test_suite_for_provider(p) for p in provider_classes]
-        for s in suites:
-            suite.addTest(s)
-        return suite
+    @property
+    def provider(self):
+        if not self._provider:
+            self._provider = self.create_provider_instance()
+        return self._provider

+ 31 - 15
test/test_block_store_service.py

@@ -1,20 +1,19 @@
+import time
 import uuid
 import uuid
 
 
-import six
+from test import helpers
+from test.helpers import ProviderTestBase
 
 
 from cloudbridge.cloud.interfaces import SnapshotState
 from cloudbridge.cloud.interfaces import SnapshotState
 from cloudbridge.cloud.interfaces import VolumeState
 from cloudbridge.cloud.interfaces import VolumeState
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
-from test.helpers import ProviderTestBase
-import test.helpers as helpers
 
 
+import six
 
 
-class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudBlockStoreServiceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
+class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
 
+    @helpers.skipIfNoService(['block_store.volumes'])
     def test_crud_volume(self):
     def test_crud_volume(self):
         """
         """
         Create a new volume, check whether the expected values are set,
         Create a new volume, check whether the expected values are set,
@@ -91,6 +90,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
             "Volume %s should have been deleted but still exists." %
             "Volume %s should have been deleted but still exists." %
             name)
             name)
 
 
+    @helpers.skipIfNoService(['block_store.volumes'])
     def test_attach_detach_volume(self):
     def test_attach_detach_volume(self):
         """
         """
         Create a new volume, and attempt to attach it to an instance
         Create a new volume, and attempt to attach it to an instance
@@ -98,11 +98,16 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
         instance_name = "CBVolOps-{0}-{1}".format(
         instance_name = "CBVolOps-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
-        net, _ = helpers.create_test_network(self.provider, instance_name)
-        test_instance = helpers.get_test_instance(self.provider, instance_name,
-                                                  network=net)
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        net = None
+        test_instance = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance, net)):
                 test_instance, net)):
+            net, subnet = helpers.create_test_network(
+                self.provider, instance_name)
+            test_instance = helpers.get_test_instance(
+                self.provider, instance_name, subnet=subnet)
             name = "CBUnitTestAttachVol-{0}".format(uuid.uuid4())
             name = "CBUnitTestAttachVol-{0}".format(uuid.uuid4())
             test_vol = self.provider.block_store.volumes.create(
             test_vol = self.provider.block_store.volumes.create(
                 name, 1, test_instance.zone_id)
                 name, 1, test_instance.zone_id)
@@ -117,6 +122,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     [VolumeState.AVAILABLE],
                     [VolumeState.AVAILABLE],
                     terminal_states=[VolumeState.ERROR, VolumeState.DELETED])
                     terminal_states=[VolumeState.ERROR, VolumeState.DELETED])
 
 
+    @helpers.skipIfNoService(['block_store.volumes'])
     def test_volume_properties(self):
     def test_volume_properties(self):
         """
         """
         Test volume properties
         Test volume properties
@@ -125,11 +131,17 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
         vol_desc = 'newvoldesc1'
         vol_desc = 'newvoldesc1'
-        net, _ = helpers.create_test_network(self.provider, instance_name)
-        test_instance = helpers.get_test_instance(self.provider, instance_name,
-                                                  network=net)
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        test_instance = None
+        net = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance, net)):
                 test_instance, net)):
+            net, subnet = helpers.create_test_network(
+                self.provider, instance_name)
+            test_instance = helpers.get_test_instance(
+                self.provider, instance_name, subnet=subnet)
+
             name = "CBUnitTestVolProps-{0}".format(uuid.uuid4())
             name = "CBUnitTestVolProps-{0}".format(uuid.uuid4())
             test_vol = self.provider.block_store.volumes.create(
             test_vol = self.provider.block_store.volumes.create(
                 name, 1, test_instance.zone_id, description=vol_desc)
                 name, 1, test_instance.zone_id, description=vol_desc)
@@ -163,8 +175,9 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                                  "/dev/sda2")
                                  "/dev/sda2")
                 test_vol.detach()
                 test_vol.detach()
                 test_vol.name = 'newvolname1'
                 test_vol.name = 'newvolname1'
-                # Force a refresh before checking attachment status
-                test_vol.refresh()
+                test_vol.wait_for(
+                    [VolumeState.AVAILABLE],
+                    terminal_states=[VolumeState.ERROR, VolumeState.DELETED])
                 self.assertEqual(test_vol.name, 'newvolname1')
                 self.assertEqual(test_vol.name, 'newvolname1')
                 self.assertEqual(test_vol.description, vol_desc)
                 self.assertEqual(test_vol.description, vol_desc)
                 self.assertIsNone(test_vol.attachments)
                 self.assertIsNone(test_vol.attachments)
@@ -172,6 +185,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     [VolumeState.AVAILABLE],
                     [VolumeState.AVAILABLE],
                     terminal_states=[VolumeState.ERROR, VolumeState.DELETED])
                     terminal_states=[VolumeState.ERROR, VolumeState.DELETED])
 
 
+    @helpers.skipIfNoService(['block_store.snapshots'])
     def test_crud_snapshot(self):
     def test_crud_snapshot(self):
         """
         """
         Create a new volume, create a snapshot of the volume, and check
         Create a new volume, create a snapshot of the volume, and check
@@ -280,6 +294,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
 
             # Test creation of a snap via SnapshotService
             # Test creation of a snap via SnapshotService
             snap_too_name = "CBSnapToo-{0}".format(name)
             snap_too_name = "CBSnapToo-{0}".format(name)
+            time.sleep(15)  # Or get SnapshotCreationPerVolumeRateExceeded
             test_snap_too = self.provider.block_store.snapshots.create(
             test_snap_too = self.provider.block_store.snapshots.create(
                 name=snap_too_name, volume=test_vol, description=snap_too_name)
                 name=snap_too_name, volume=test_vol, description=snap_too_name)
             with helpers.cleanup_action(lambda: cleanup_snap(test_snap_too)):
             with helpers.cleanup_action(lambda: cleanup_snap(test_snap_too)):
@@ -289,6 +304,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     "repr(obj) should contain the object id so that the object"
                     "repr(obj) should contain the object id so that the object"
                     " can be reconstructed, but does not.")
                     " can be reconstructed, but does not.")
 
 
+    @helpers.skipIfNoService(['block_store.snapshots'])
     def test_snapshot_properties(self):
     def test_snapshot_properties(self):
         """
         """
         Test snapshot properties
         Test snapshot properties

+ 2 - 1
test/test_cloud_factory.py

@@ -1,5 +1,7 @@
 import unittest
 import unittest
 
 
+from test import helpers
+
 from cloudbridge.cloud import factory
 from cloudbridge.cloud import factory
 from cloudbridge.cloud import interfaces
 from cloudbridge.cloud import interfaces
 from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.factory import CloudProviderFactory
@@ -7,7 +9,6 @@ from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from cloudbridge.cloud.interfaces.provider import CloudProvider
 from cloudbridge.cloud.interfaces.provider import CloudProvider
 from cloudbridge.cloud.providers.aws import AWSCloudProvider
 from cloudbridge.cloud.providers.aws import AWSCloudProvider
 from cloudbridge.cloud.providers.aws.provider import MockAWSCloudProvider
 from cloudbridge.cloud.providers.aws.provider import MockAWSCloudProvider
-import test.helpers as helpers
 
 
 
 
 class CloudFactoryTestCase(unittest.TestCase):
 class CloudFactoryTestCase(unittest.TestCase):

+ 3 - 6
test/test_cloud_helpers.py

@@ -1,8 +1,9 @@
 import itertools
 import itertools
 
 
+from test.helpers import ProviderTestBase
+
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
 from cloudbridge.cloud.base.resources import ServerPagedResultList
-from test.helpers import ProviderTestBase
 
 
 
 
 class DummyResult(object):
 class DummyResult(object):
@@ -17,10 +18,6 @@ class DummyResult(object):
 
 
 class CloudHelpersTestCase(ProviderTestBase):
 class CloudHelpersTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudHelpersTestCase, self).__init__(
-            methodName=methodName, provider=provider)
-
     def setUp(self):
     def setUp(self):
         super(CloudHelpersTestCase, self).setUp()
         super(CloudHelpersTestCase, self).setUp()
         self.objects = [DummyResult(1, "One"),
         self.objects = [DummyResult(1, "One"),
@@ -75,4 +72,4 @@ class CloudHelpersTestCase(ProviderTestBase):
         self.assertTrue(results.supports_server_paging, "Server paged result"
         self.assertTrue(results.supports_server_paging, "Server paged result"
                         " lists should return True for server paging.")
                         " lists should return True for server paging.")
         with self.assertRaises(NotImplementedError):
         with self.assertRaises(NotImplementedError):
-            _ = results.data
+            results.data

+ 189 - 96
test/test_compute_service.py

@@ -1,31 +1,36 @@
+import ipaddress
 import uuid
 import uuid
 
 
-import ipaddress
-import six
-from cloudbridge.cloud.interfaces \
-    import InvalidConfigurationException
+from test import helpers
+from test.helpers import ProviderTestBase
+
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import InstanceState
-from cloudbridge.cloud.interfaces.resources import InstanceType
+from cloudbridge.cloud.interfaces import InvalidConfigurationException
+from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
-from test.helpers import ProviderTestBase
-import test.helpers as helpers
+from cloudbridge.cloud.interfaces.resources import InstanceType
+# from cloudbridge.cloud.interfaces.resources import SnapshotState
 
 
+import six
 
 
-class CloudComputeServiceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudComputeServiceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
+class CloudComputeServiceTestCase(ProviderTestBase):
 
 
+    @helpers.skipIfNoService(['compute.instances', 'network'])
     def test_crud_instance(self):
     def test_crud_instance(self):
         name = "CBInstCrud-{0}-{1}".format(
         name = "CBInstCrud-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
-        net, _ = helpers.create_test_network(self.provider, name)
-        inst = helpers.get_test_instance(self.provider, name, network=net)
-
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        inst = None
+        net = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 inst, net)):
                 inst, net)):
+            net, subnet = helpers.create_test_network(self.provider, name)
+            inst = helpers.get_test_instance(self.provider, name,
+                                             subnet=subnet)
+
             all_instances = self.provider.compute.instances.list()
             all_instances = self.provider.compute.instances.list()
 
 
             list_instances = [i for i in all_instances if i.name == name]
             list_instances = [i for i in all_instances if i.name == name]
@@ -88,21 +93,31 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             return False
             return False
         return True
         return True
 
 
+    @helpers.skipIfNoService(['compute.instances', 'network',
+                              'security.security_groups',
+                              'security.key_pairs'])
     def test_instance_properties(self):
     def test_instance_properties(self):
         name = "CBInstProps-{0}-{1}".format(
         name = "CBInstProps-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
-        net, _ = helpers.create_test_network(self.provider, name)
-        kp = self.provider.security.key_pairs.create(name=name)
-        sg = self.provider.security.security_groups.create(
-            name=name, description=name, network_id=net.id)
-        test_instance = helpers.get_test_instance(self.provider,
-                                                  name, key_pair=kp,
-                                                  security_groups=[sg],
-                                                  network=net)
 
 
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        test_instance = None
+        net = None
+        sg = None
+        kp = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance, net, sg, kp)):
                 test_instance, net, sg, kp)):
+            net, subnet = helpers.create_test_network(self.provider, name)
+            kp = self.provider.security.key_pairs.create(name=name)
+            sg = self.provider.security.security_groups.create(
+                name=name, description=name, network_id=net.id)
+            test_instance = helpers.get_test_instance(self.provider,
+                                                      name, key_pair=kp,
+                                                      security_groups=[sg],
+                                                      subnet=subnet)
+
             self.assertTrue(
             self.assertTrue(
                 test_instance.id in repr(test_instance),
                 test_instance.id in repr(test_instance),
                 "repr(obj) should contain the object id so that the object"
                 "repr(obj) should contain the object id so that the object"
@@ -168,6 +183,8 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 "Instance type {0} does not match expected type {1}".format(
                 "Instance type {0} does not match expected type {1}".format(
                     itype.name, expected_type))
                     itype.name, expected_type))
 
 
+    @helpers.skipIfNoService(['compute.instances', 'compute.images',
+                              'compute.instance_types'])
     def test_block_device_mapping_launch_config(self):
     def test_block_device_mapping_launch_config(self):
         lc = self.provider.compute.instances.create_launch_config()
         lc = self.provider.compute.instances.create_launch_config()
 
 
@@ -193,12 +210,12 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         # Override root volume size
         # Override root volume size
         image_id = helpers.get_provider_test_data(self.provider, "image")
         image_id = helpers.get_provider_test_data(self.provider, "image")
         img = self.provider.compute.images.get(image_id)
         img = self.provider.compute.images.get(image_id)
+        # The size should be greater then the ami size
+        # and therefore, img.min_disk is used.
         lc.add_volume_device(
         lc.add_volume_device(
             is_root=True,
             is_root=True,
             source=img,
             source=img,
-            # TODO: This should be greater than the ami size or tests will fail
-            # on actual infrastructure. Needs an image.size method
-            size=2,
+            size=img.min_disk if img and img.min_disk else 2,
             delete_on_terminate=True)
             delete_on_terminate=True)
 
 
         # Attempting to add more than one root volume should raise an
         # Attempting to add more than one root volume should raise an
@@ -227,87 +244,163 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             "Expected %d total block devices bit found %d" %
             "Expected %d total block devices bit found %d" %
             (2 + inst_type.num_ephemeral_disks, len(lc.block_devices)))
             (2 + inst_type.num_ephemeral_disks, len(lc.block_devices)))
 
 
+    @helpers.skipIfNoService(['compute.instances', 'compute.images',
+                              'compute.instance_types', 'block_store.volumes'])
     def test_block_device_mapping_attachments(self):
     def test_block_device_mapping_attachments(self):
         name = "CBInstBlkAttch-{0}-{1}".format(
         name = "CBInstBlkAttch-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
 
 
-#         test_vol = self.provider.block_store.volumes.create(
-#             name,
-#             1,
-#             helpers.get_provider_test_data(self.provider, "placement"))
-#         with helpers.cleanup_action(lambda: test_vol.delete()):
-#             test_vol.wait_till_ready()
-#             test_snap = test_vol.create_snapshot(name=name,
-#                                                  description=name)
+        # Comment out BDM tests because OpenStack is not stable enough yet
+        if True:
+            if True:
+
+                # test_vol = self.provider.block_store.volumes.create(
+                #    name,
+                #    1,
+                #    helpers.get_provider_test_data(self.provider,
+                #                                   "placement"))
+                # with helpers.cleanup_action(lambda: test_vol.delete()):
+                #    test_vol.wait_till_ready()
+                #    test_snap = test_vol.create_snapshot(name=name,
+                #                                         description=name)
+                #
+                #    def cleanup_snap(snap):
+                #        snap.delete()
+                #        snap.wait_for(
+                #            [SnapshotState.UNKNOWN],
+                #            terminal_states=[SnapshotState.ERROR])
+                #
+                #    with helpers.cleanup_action(lambda:
+                #                                cleanup_snap(test_snap)):
+                #         test_snap.wait_till_ready()
+
+                lc = self.provider.compute.instances.create_launch_config()
+
+#                 # Add a new blank volume
+#                 lc.add_volume_device(size=1, delete_on_terminate=True)
 #
 #
-#             def cleanup_snap(snap):
-#                 snap.delete()
-#                 snap.wait_for(
-#                     [SnapshotState.UNKNOWN],
-#                     terminal_states=[SnapshotState.ERROR])
-#
-#             with helpers.cleanup_action(lambda: cleanup_snap(test_snap)):
-#                 test_snap.wait_till_ready()
-
-        lc = self.provider.compute.instances.create_launch_config()
-
-        # Add a new blank volume
-#         lc.add_volume_device(size=1, delete_on_terminate=True)
-
-        # Attach an existing volume
+#                 # Attach an existing volume
 #                 lc.add_volume_device(size=1, source=test_vol,
 #                 lc.add_volume_device(size=1, source=test_vol,
 #                                      delete_on_terminate=True)
 #                                      delete_on_terminate=True)
-
-        # Add a new volume based on a snapshot
+#
+#                 # Add a new volume based on a snapshot
 #                 lc.add_volume_device(size=1, source=test_snap,
 #                 lc.add_volume_device(size=1, source=test_snap,
 #                                      delete_on_terminate=True)
 #                                      delete_on_terminate=True)
 
 
-        # Override root volume size
-        image_id = helpers.get_provider_test_data(
-            self.provider,
-            "image")
-        img = self.provider.compute.images.get(image_id)
-        lc.add_volume_device(
-            is_root=True,
-            source=img,
-            # TODO: This should be greater than the ami size or tests
-            # will fail on actual infrastructure. Needs an image.size
-            # method
-            size=2,
-            delete_on_terminate=True)
-
-        # Add all available ephemeral devices
-        instance_type_name = helpers.get_provider_test_data(
-            self.provider,
-            "instance_type")
-        inst_type = self.provider.compute.instance_types.find(
-            name=instance_type_name)[0]
-        for _ in range(inst_type.num_ephemeral_disks):
-            lc.add_ephemeral_device()
+                # Override root volume size
+                image_id = helpers.get_provider_test_data(
+                    self.provider,
+                    "image")
+                img = self.provider.compute.images.get(image_id)
+                # The size should be greater then the ami size
+                # and therefore, img.min_disk is used.
+                lc.add_volume_device(
+                    is_root=True,
+                    source=img,
+                    size=img.min_disk if img and img.min_disk else 2,
+                    delete_on_terminate=True)
+
+                # Add all available ephemeral devices
+                instance_type_name = helpers.get_provider_test_data(
+                    self.provider,
+                    "instance_type")
+                inst_type = self.provider.compute.instance_types.find(
+                    name=instance_type_name)[0]
+                for _ in range(inst_type.num_ephemeral_disks):
+                    lc.add_ephemeral_device()
+
+                net, subnet = helpers.create_test_network(self.provider, name)
+
+                with helpers.cleanup_action(lambda:
+                                            helpers.delete_test_network(net)):
+
+                    inst = helpers.create_test_instance(
+                        self.provider,
+                        name,
+                        subnet=subnet,
+                        launch_config=lc)
+
+                    with helpers.cleanup_action(lambda:
+                                                helpers.delete_test_instance(
+                                                    inst)):
+                        try:
+                            inst.wait_till_ready()
+                        except WaitStateException as e:
+                            self.fail("The block device mapped launch did not "
+                                      " complete successfully: %s" % e)
+                        # TODO: Check instance attachments and make sure they
+                        # correspond to requested mappings
+
+    @helpers.skipIfNoService(['compute.instances', 'network',
+                              'security.security_groups'])
+    def test_instance_methods(self):
+        name = "CBInstProps-{0}-{1}".format(
+            self.provider.name,
+            uuid.uuid4())
 
 
-        net, _ = helpers.create_test_network(self.provider, name)
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        test_inst = None
+        net = None
+        sg = None
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                test_inst, net, sg)):
+            net, subnet = helpers.create_test_network(self.provider, name)
+            test_inst = helpers.get_test_instance(self.provider, name,
+                                                  subnet=subnet)
+            sg = self.provider.security.security_groups.create(
+                name=name, description=name, network_id=net.id)
+
+            # Check adding a security group to a running instance
+            test_inst.add_security_group(sg)
+            test_inst.refresh()
+            self.assertTrue(
+                sg in test_inst.security_groups, "Expected security group '%s'"
+                " to be among instance security_groups: [%s]" %
+                (sg, test_inst.security_groups))
 
 
-        inst = helpers.create_test_instance(
-            self.provider,
-            name,
-            network=net,
-            # We don't have a way to match the test net placement and this zone
-            # zone=helpers.get_provider_test_data(self.provider, 'placement'),
-            launch_config=lc)
-
-        def cleanup(instance, net):
-            instance.terminate()
-            instance.wait_for(
-                [InstanceState.TERMINATED, InstanceState.UNKNOWN],
-                terminal_states=[InstanceState.ERROR])
-            helpers.delete_test_network(net)
-
-        with helpers.cleanup_action(lambda: cleanup(inst, net)):
-            try:
-                inst.wait_till_ready()
-            except WaitStateException as e:
-                self.fail("The block device mapped launch did not "
-                          " complete successfully: %s" % e)
-            # TODO: Check instance attachments and make sure they
-            # correspond to requested mappings
+            # Check removing a security group from a running instance
+            test_inst.remove_security_group(sg)
+            test_inst.refresh()
+            self.assertTrue(
+                sg not in test_inst.security_groups, "Expected security group"
+                " '%s' to be removed from instance security_groups: [%s]" %
+                (sg, test_inst.security_groups))
+
+            # check floating ips
+            router = self.provider.network.create_router(name=name)
+
+            with helpers.cleanup_action(lambda: router.delete()):
+
+                # TODO: Cloud specific code, needs fixing
+                if self.provider.PROVIDER_ID == 'openstack':
+                    for n in self.provider.network.list():
+                        if n.external:
+                            external_net = n
+                            break
+                else:
+                    external_net = net
+                router.attach_network(external_net.id)
+                router.add_route(subnet.id)
+
+                def cleanup_router():
+                    router.remove_route(subnet.id)
+                    router.detach_network()
+
+                with helpers.cleanup_action(lambda: cleanup_router()):
+                    # check whether adding an elastic ip works
+                    fip = self.provider.network.create_floating_ip()
+                    with helpers.cleanup_action(lambda: fip.delete()):
+                        test_inst.add_floating_ip(fip.public_ip)
+                        test_inst.refresh()
+                        self.assertIn(fip.public_ip, test_inst.public_ips)
+
+                        if isinstance(self.provider, TestMockHelperMixin):
+                            # TODO: Moto bug does not refresh removed public ip
+                            return
+
+                        # check whether removing an elastic ip works
+                        test_inst.remove_floating_ip(fip.public_ip)
+                        test_inst.refresh()
+                        self.assertNotIn(fip.public_ip, test_inst.public_ips)

+ 38 - 27
test/test_image_service.py

@@ -1,18 +1,18 @@
 import uuid
 import uuid
 
 
-import six
+from test import helpers
+from test.helpers import ProviderTestBase
 
 
 from cloudbridge.cloud.interfaces import MachineImageState
 from cloudbridge.cloud.interfaces import MachineImageState
-from test.helpers import ProviderTestBase
-import test.helpers as helpers
+from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
 
+import six
 
 
-class CloudImageServiceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudImageServiceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
+class CloudImageServiceTestCase(ProviderTestBase):
 
 
+    @helpers.skipIfNoService(['compute.images', 'network',
+                              'compute.instances'])
     def test_create_and_list_image(self):
     def test_create_and_list_image(self):
         """
         """
         Create a new image and check whether that image can be listed.
         Create a new image and check whether that image can be listed.
@@ -22,11 +22,18 @@ class CloudImageServiceTestCase(ProviderTestBase):
         instance_name = "CBImageTest-{0}-{1}".format(
         instance_name = "CBImageTest-{0}-{1}".format(
             self.provider.name,
             self.provider.name,
             uuid.uuid4())
             uuid.uuid4())
-        net, _ = helpers.create_test_network(self.provider, instance_name)
-        test_instance = helpers.get_test_instance(self.provider, instance_name,
-                                                  network=net)
+
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        test_instance = None
+        net = None
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance, net)):
                 test_instance, net)):
+            net, subnet = helpers.create_test_network(
+                self.provider, instance_name)
+            test_instance = helpers.get_test_instance(
+                self.provider, instance_name, subnet=subnet)
+
             name = "CBUnitTestListImg-{0}".format(uuid.uuid4())
             name = "CBUnitTestListImg-{0}".format(uuid.uuid4())
             test_image = test_instance.create_image(name)
             test_image = test_instance.create_image(name)
 
 
@@ -48,26 +55,27 @@ class CloudImageServiceTestCase(ProviderTestBase):
                         test_image.description, six.string_types),
                         test_image.description, six.string_types),
                     "Image description must be None or a string")
                     "Image description must be None or a string")
 
 
-                images = self.provider.compute.images.list()
-                list_images = [image for image in images
-                               if image.name == name]
-                self.assertTrue(
-                    len(list_images) == 1,
-                    "List images does not return the expected image %s" %
-                    name)
+                # This check won't work when >50 images are available
+                # images = self.provider.compute.images.list()
+                # list_images = [image for image in images
+                #                if image.name == name]
+                # self.assertTrue(
+                #     len(list_images) == 1,
+                #     "List images does not return the expected image %s" %
+                #     name)
 
 
                 # check iteration
                 # check iteration
                 iter_images = [image for image in self.provider.compute.images
                 iter_images = [image for image in self.provider.compute.images
                                if image.name == name]
                                if image.name == name]
                 self.assertTrue(
                 self.assertTrue(
-                    len(iter_images) == 1,
-                    "Iter images does not return the expected image %s" %
-                    name)
+                    name in [ii.name for ii in iter_images],
+                    "Iter images (%s) does not contain the expected image %s" %
+                    (iter_images, name))
 
 
                 # find image
                 # find image
                 found_images = self.provider.compute.images.find(name=name)
                 found_images = self.provider.compute.images.find(name=name)
                 self.assertTrue(
                 self.assertTrue(
-                    len(found_images) == 1,
+                    name in [fi.name for fi in found_images],
                     "Find images error: expected image %s but found: %s" %
                     "Find images error: expected image %s but found: %s" %
                     (name, found_images))
                     (name, found_images))
 
 
@@ -82,19 +90,22 @@ class CloudImageServiceTestCase(ProviderTestBase):
                 get_img = self.provider.compute.images.get(
                 get_img = self.provider.compute.images.get(
                     test_image.id)
                     test_image.id)
                 self.assertTrue(
                 self.assertTrue(
-                    found_images[0] == iter_images[0] == get_img == test_image,
+                    found_images[0] == get_img == test_image,
                     "Objects returned by list: {0} and get: {1} are not as "
                     "Objects returned by list: {0} and get: {1} are not as "
                     " expected: {2}" .format(found_images[0].id,
                     " expected: {2}" .format(found_images[0].id,
                                              get_img.id,
                                              get_img.id,
                                              test_image.id))
                                              test_image.id))
                 self.assertTrue(
                 self.assertTrue(
-                    list_images[0].name == found_images[0].name ==
-                    get_img.name == test_image.name,
-                    "Names returned by list: {0}, find: {1} and get: {2} are"
-                    " not as expected: {3}" .format(list_images[0].name,
-                                                    found_images[0].name,
+                    found_images[0].name == get_img.name == test_image.name,
+                    "Names returned by find: {0} and get: {1} are"
+                    " not as expected: {2}" .format(found_images[0].name,
                                                     get_img.name,
                                                     get_img.name,
                                                     test_image.name))
                                                     test_image.name))
+                # TODO: Fix moto so that the BDM is populated correctly
+                if not isinstance(self.provider, TestMockHelperMixin):
+                    # check image size
+                    self.assertGreater(get_img.min_disk, 0, "Minimum disk size"
+                                       " required by image is invalid")
             # TODO: Images take a long time to deregister on EC2. Needs
             # TODO: Images take a long time to deregister on EC2. Needs
             # investigation
             # investigation
             images = self.provider.compute.images.list()
             images = self.provider.compute.images.list()

+ 8 - 8
test/test_instance_types_service.py

@@ -1,21 +1,19 @@
 from test import helpers
 from test import helpers
 
 
-import six
+from test.helpers import ProviderTestBase
 
 
 from cloudbridge.cloud.interfaces.resources import InstanceType
 from cloudbridge.cloud.interfaces.resources import InstanceType
-from test.helpers import ProviderTestBase
 
 
+import six
 
 
-class CloudInstanceTypesServiceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudInstanceTypesServiceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
+class CloudInstanceTypesServiceTestCase(ProviderTestBase):
 
 
+    @helpers.skipIfNoService(['compute.instance_types'])
     def test_instance_types(self):
     def test_instance_types(self):
         instance_types = self.provider.compute.instance_types.list()
         instance_types = self.provider.compute.instance_types.list()
-        # check iteration
-        iter_instance_types = list(self.provider.compute.instance_types)
+        # Check iteration, keeping the first 50 entries (the .list() default)
+        iter_instance_types = list(self.provider.compute.instance_types)[:50]
         self.assertListEqual(iter_instance_types, instance_types)
         self.assertListEqual(iter_instance_types, instance_types)
 
 
         for inst_type in instance_types:
         for inst_type in instance_types:
@@ -69,6 +67,7 @@ class CloudInstanceTypesServiceTestCase(ProviderTestBase):
                     inst_type.extra_data, dict),
                     inst_type.extra_data, dict),
                 "InstanceType extra_data must be None or a dict")
                 "InstanceType extra_data must be None or a dict")
 
 
+    @helpers.skipIfNoService(['compute.instance_types'])
     def test_instance_types_find(self):
     def test_instance_types_find(self):
         """
         """
         Searching for an instance by name should return an
         Searching for an instance by name should return an
@@ -91,6 +90,7 @@ class CloudInstanceTypesServiceTestCase(ProviderTestBase):
             self.provider.compute.instance_types.find(
             self.provider.compute.instance_types.find(
                 non_existent_param="random_value")
                 non_existent_param="random_value")
 
 
+    @helpers.skipIfNoService(['compute.instance_types'])
     def test_instance_types_get(self):
     def test_instance_types_get(self):
         """
         """
         Searching for an instance by id should return an
         Searching for an instance by id should return an

+ 6 - 7
test/test_interface.py

@@ -1,18 +1,17 @@
 import unittest
 import unittest
+
+from test.helpers import ProviderTestBase
+
 import cloudbridge
 import cloudbridge
+
 from cloudbridge.cloud import interfaces
 from cloudbridge.cloud import interfaces
-from test.helpers import ProviderTestBase
-from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
-from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.factory import CloudProviderFactory
+from cloudbridge.cloud.interfaces import TestMockHelperMixin
+from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 
 
 
 
 class CloudInterfaceTestCase(ProviderTestBase):
 class CloudInterfaceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudInterfaceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
-
     def test_name_property(self):
     def test_name_property(self):
         """
         """
         Name should always return a value and should not raise an exception
         Name should always return a value and should not raise an exception

+ 64 - 31
test/test_network_service.py

@@ -1,17 +1,16 @@
 import test.helpers as helpers
 import test.helpers as helpers
 import uuid
 import uuid
 from test.helpers import ProviderTestBase
 from test.helpers import ProviderTestBase
+
 from cloudbridge.cloud.interfaces.resources import RouterState
 from cloudbridge.cloud.interfaces.resources import RouterState
 
 
 
 
 class CloudNetworkServiceTestCase(ProviderTestBase):
 class CloudNetworkServiceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudNetworkServiceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
-
+    @helpers.skipIfNoService(['network'])
     def test_crud_network_service(self):
     def test_crud_network_service(self):
         name = 'cbtestnetworkservice-{0}'.format(uuid.uuid4())
         name = 'cbtestnetworkservice-{0}'.format(uuid.uuid4())
+        subnet_name = 'cbtestsubnetservice-{0}'.format(uuid.uuid4())
         net = self.provider.network.create(name=name)
         net = self.provider.network.create(name=name)
         with helpers.cleanup_action(
         with helpers.cleanup_action(
             lambda:
             lambda:
@@ -32,21 +31,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 "Get network did not return the expected network {0}."
                 "Get network did not return the expected network {0}."
                 .format(name))
                 .format(name))
 
 
-        netl = self.provider.network.list()
-        found_net = [n for n in netl if n.name == name]
-        self.assertEqual(
-            len(found_net), 0,
-            "Network {0} should have been deleted but still exists."
-            .format(name))
-
-    def test_crud_subnet_service(self):
-        name = 'cbtestnetworkservice-{0}'.format(uuid.uuid4())
-        subnet_name = 'cbtestsubnetservice-{0}'.format(uuid.uuid4())
-        net = self.provider.network.create(name=name)
-        with helpers.cleanup_action(
-            lambda:
-                self.provider.network.delete(network_id=net.id)
-        ):
+            # check subnet
             subnet = self.provider.network.subnets.create(
             subnet = self.provider.network.subnets.create(
                 network=net, cidr_block="10.0.0.1/24", name=subnet_name)
                 network=net, cidr_block="10.0.0.1/24", name=subnet_name)
             with helpers.cleanup_action(
             with helpers.cleanup_action(
@@ -73,6 +58,38 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 "Subnet {0} should have been deleted but still exists."
                 "Subnet {0} should have been deleted but still exists."
                 .format(subnet_name))
                 .format(subnet_name))
 
 
+            # Check floating IP address
+            ip = self.provider.network.create_floating_ip()
+            ip_id = ip.id
+            with helpers.cleanup_action(lambda: ip.delete()):
+                ipl = self.provider.network.floating_ips()
+                self.assertTrue(
+                    ip in ipl,
+                    "Floating IP address {0} should exist in the list {1}"
+                    .format(ip.id, ipl))
+                # 2016-08: address filtering not implemented in moto
+                # empty_ipl = self.provider.network.floating_ips('dummy-net')
+                # self.assertFalse(
+                #     empty_ipl,
+                #     "Bogus network should not have any floating IPs: {0}"
+                #     .format(empty_ipl))
+                self.assertIn(
+                    ip.public_ip, repr(ip),
+                    "repr(obj) should contain the address public IP value.")
+                self.assertFalse(
+                    ip.private_ip,
+                    "Floating IP should not have a private IP value ({0})."
+                    .format(ip.private_ip))
+                self.assertFalse(
+                    ip.in_use(),
+                    "Newly created floating IP address should not be in use.")
+            ipl = self.provider.network.floating_ips()
+            found_ip = [a for a in ipl if a.id == ip_id]
+            self.assertTrue(
+                len(found_ip) == 0,
+                "Floating IP {0} should have been deleted but still exists."
+                .format(ip_id))
+
         netl = self.provider.network.list()
         netl = self.provider.network.list()
         found_net = [n for n in netl if n.name == name]
         found_net = [n for n in netl if n.name == name]
         self.assertEqual(
         self.assertEqual(
@@ -112,6 +129,9 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             "Floating IP {0} should have been deleted but still exists."
             "Floating IP {0} should have been deleted but still exists."
             .format(ip_id))
             .format(ip_id))
 
 
+    @helpers.skipIfNoService(['network'])
+    @helpers.skipIfNoService(['network'])
+    @helpers.skipIfNoService(['network'])
     def test_crud_network(self):
     def test_crud_network(self):
         name = 'cbtestnetwork-{0}'.format(uuid.uuid4())
         name = 'cbtestnetwork-{0}'.format(uuid.uuid4())
         subnet_name = 'cbtestsubnet-{0}'.format(uuid.uuid4())
         subnet_name = 'cbtestsubnet-{0}'.format(uuid.uuid4())
@@ -121,8 +141,8 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         ):
         ):
             net.wait_till_ready()
             net.wait_till_ready()
             self.assertEqual(
             self.assertEqual(
-                net.refresh(), 'available',
-                "Network in state %s , yet should be 'available'" % net.state)
+                net.state, 'available',
+                "Network in state '%s', yet should be 'available'" % net.state)
 
 
             self.assertIn(
             self.assertIn(
                 net.id, repr(net),
                 net.id, repr(net),
@@ -135,7 +155,10 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 % net.cidr_block)
                 % net.cidr_block)
 
 
             cidr = '10.0.1.0/24'
             cidr = '10.0.1.0/24'
-            sn = net.create_subnet(cidr_block=cidr, name=subnet_name)
+            sn = net.create_subnet(
+                cidr_block=cidr, name=subnet_name,
+                zone=helpers.get_provider_test_data(self.provider,
+                                                    'placement'))
             with helpers.cleanup_action(lambda: sn.delete()):
             with helpers.cleanup_action(lambda: sn.delete()):
                 self.assertTrue(
                 self.assertTrue(
                     sn.id in [s.id for s in net.subnets()],
                     sn.id in [s.id for s in net.subnets()],
@@ -152,21 +175,30 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                     "Subnet's CIDR %s should match the specified one %s." % (
                     "Subnet's CIDR %s should match the specified one %s." % (
                         sn.cidr_block, cidr))
                         sn.cidr_block, cidr))
 
 
+    @helpers.skipIfNoService(['network.routers'])
     def test_crud_router(self):
     def test_crud_router(self):
 
 
         def _cleanup(net, subnet, router):
         def _cleanup(net, subnet, router):
-            router.remove_route(subnet.id)
-            router.detach_network()
-            router.delete()
-            subnet.delete()
-            net.delete()
+            with helpers.cleanup_action(lambda: net.delete()):
+                with helpers.cleanup_action(lambda: subnet.delete()):
+                    with helpers.cleanup_action(lambda: router.delete()):
+                        router.remove_route(subnet.id)
+                        router.detach_network()
 
 
         name = 'cbtestrouter-{0}'.format(uuid.uuid4())
         name = 'cbtestrouter-{0}'.format(uuid.uuid4())
-        router = self.provider.network.create_router(name=name)
-        net = self.provider.network.create(name=name)
-        cidr = '10.0.1.0/24'
-        sn = net.create_subnet(cidr_block=cidr, name=name)
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        net = None
+        sn = None
+        router = None
         with helpers.cleanup_action(lambda: _cleanup(net, sn, router)):
         with helpers.cleanup_action(lambda: _cleanup(net, sn, router)):
+            router = self.provider.network.create_router(name=name)
+            net = self.provider.network.create(name=name)
+            cidr = '10.0.1.0/24'
+            sn = net.create_subnet(cidr_block=cidr, name=name,
+                                   zone=helpers.get_provider_test_data(
+                                       self.provider, 'placement'))
+
             # Check basic router properties
             # Check basic router properties
             self.assertIn(
             self.assertIn(
                 router, self.provider.network.routers(),
                 router, self.provider.network.routers(),
@@ -188,6 +220,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 "Router {0} should not be assoc. with a network {1}".format(
                 "Router {0} should not be assoc. with a network {1}".format(
                     router.id, router.network_id))
                     router.id, router.network_id))
 
 
+            # TODO: Cloud specific code, needs fixing
             # Check router connectivity
             # Check router connectivity
             # On OpenStack only one network is external and on AWS every
             # On OpenStack only one network is external and on AWS every
             # network is external, yet we need to use the one we've created?!
             # network is external, yet we need to use the one we've created?!

+ 4 - 6
test/test_object_life_cycle.py

@@ -1,17 +1,15 @@
 import uuid
 import uuid
 
 
+from test import helpers
+from test.helpers import ProviderTestBase
+
 from cloudbridge.cloud.interfaces import VolumeState
 from cloudbridge.cloud.interfaces import VolumeState
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
-from test.helpers import ProviderTestBase
-import test.helpers as helpers
 
 
 
 
 class CloudObjectLifeCycleTestCase(ProviderTestBase):
 class CloudObjectLifeCycleTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudObjectLifeCycleTestCase, self).__init__(
-            methodName=methodName, provider=provider)
-
+    @helpers.skipIfNoService(['block_store.volumes'])
     def test_object_life_cycle(self):
     def test_object_life_cycle(self):
         """
         """
         Test object life cycle methods by using a volume.
         Test object life cycle methods by using a volume.

+ 90 - 10
test/test_object_store_service.py

@@ -1,18 +1,22 @@
+import filecmp
+import os
+import tempfile
+import uuid
+
 from datetime import datetime
 from datetime import datetime
 from io import BytesIO
 from io import BytesIO
-import uuid
+from test import helpers
+from test.helpers import ProviderTestBase
+from unittest import skip
 
 
 from cloudbridge.cloud.interfaces.resources import BucketObject
 from cloudbridge.cloud.interfaces.resources import BucketObject
-from test.helpers import ProviderTestBase
-import test.helpers as helpers
 
 
+import requests
 
 
-class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudObjectStoreServiceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
+class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
 
+    @helpers.skipIfNoService(['object_store'])
     def test_crud_bucket(self):
     def test_crud_bucket(self):
         """
         """
         Create a new bucket, check whether the expected values are set,
         Create a new bucket, check whether the expected values are set,
@@ -66,6 +70,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
             "Bucket %s should have been deleted but still exists." %
             "Bucket %s should have been deleted but still exists." %
             name)
             name)
 
 
+    @helpers.skipIfNoService(['object_store'])
     def test_crud_bucket_objects(self):
     def test_crud_bucket_objects(self):
         """
         """
         Create a new bucket, upload some contents into the bucket, and
         Create a new bucket, upload some contents into the bucket, and
@@ -80,7 +85,8 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
         self.assertEqual([], objects)
         self.assertEqual([], objects)
 
 
         with helpers.cleanup_action(lambda: test_bucket.delete()):
         with helpers.cleanup_action(lambda: test_bucket.delete()):
-            obj_name = "hello_world.txt"
+            obj_name_prefix = "hello"
+            obj_name = obj_name_prefix + "_world.txt"
             obj = test_bucket.create_object(obj_name)
             obj = test_bucket.create_object(obj_name)
 
 
             self.assertTrue(
             self.assertTrue(
@@ -101,7 +107,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                     "Object size property needs to be a int, not {0}".format(
                     "Object size property needs to be a int, not {0}".format(
                         type(objs[0].size)))
                         type(objs[0].size)))
                 self.assertTrue(
                 self.assertTrue(
-                    datetime.strptime(objs[0].last_modified,
+                    datetime.strptime(objs[0].last_modified[:23],
                                       "%Y-%m-%dT%H:%M:%S.%f"),
                                       "%Y-%m-%dT%H:%M:%S.%f"),
                     "Object's last_modified field format {0} not matching."
                     "Object's last_modified field format {0} not matching."
                     .format(objs[0].last_modified))
                     .format(objs[0].last_modified))
@@ -130,6 +136,13 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                     isinstance(obj_too, BucketObject),
                     isinstance(obj_too, BucketObject),
                     "Did not get object {0} of expected type.".format(obj_too))
                     "Did not get object {0} of expected type.".format(obj_too))
 
 
+                prefix_filtered_list = test_bucket.list(prefix=obj_name_prefix)
+                self.assertTrue(
+                    len(objs) == len(prefix_filtered_list) == 1,
+                    'The number of objects returned by list function, '
+                    'with and without a prefix, are expected to be equal, '
+                    'but its detected otherwise.')
+
             objs = test_bucket.list()
             objs = test_bucket.list()
             found_objs = [o for o in objs if o.name == obj_name]
             found_objs = [o for o in objs if o.name == obj_name]
             self.assertTrue(
             self.assertTrue(
@@ -137,8 +150,8 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                 "Object %s should have been deleted but still exists." %
                 "Object %s should have been deleted but still exists." %
                 obj_name)
                 obj_name)
 
 
+    @helpers.skipIfNoService(['object_store'])
     def test_upload_download_bucket_content(self):
     def test_upload_download_bucket_content(self):
-
         name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
         name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
         test_bucket = self.provider.object_store.create(name)
         test_bucket = self.provider.object_store.create(name)
 
 
@@ -159,3 +172,70 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                 for data in obj.iter_content():
                 for data in obj.iter_content():
                     target_stream2.write(data)
                     target_stream2.write(data)
                 self.assertEqual(target_stream2.getvalue(), content)
                 self.assertEqual(target_stream2.getvalue(), content)
+
+    @skip("Skip until OpenStack implementation is provided")
+    @helpers.skipIfNoService(['object_store'])
+    def test_generate_url(self):
+        name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
+        test_bucket = self.provider.object_store.create(name)
+
+        with helpers.cleanup_action(lambda: test_bucket.delete()):
+            obj_name = "hello_upload_download.txt"
+            obj = test_bucket.create_object(obj_name)
+
+            with helpers.cleanup_action(lambda: obj.delete()):
+                content = b"Hello World. Generate a url."
+                obj.upload(content)
+                target_stream = BytesIO()
+                obj.save_content(target_stream)
+
+                url = obj.generate_url(100)
+                self.assertEqual(requests.get(url).content, content)
+
+    @helpers.skipIfNoService(['object_store'])
+    def test_upload_download_bucket_content_from_file(self):
+        name = "cbtestbucketobjs-{0}".format(uuid.uuid4())
+        test_bucket = self.provider.object_store.create(name)
+
+        with helpers.cleanup_action(lambda: test_bucket.delete()):
+            obj_name = "hello_upload_download.txt"
+            obj = test_bucket.create_object(obj_name)
+
+            with helpers.cleanup_action(lambda: obj.delete()):
+                test_file = os.path.join(
+                    helpers.get_test_fixtures_folder(), 'logo.jpg')
+                obj.upload_from_file(test_file)
+                target_stream = BytesIO()
+                obj.save_content(target_stream)
+                with open(test_file, 'rb') as f:
+                    self.assertEqual(target_stream.getvalue(), f.read())
+
+    @skip("Skip unless you want to test swift objects bigger than 5 Gig")
+    @helpers.skipIfNoService(['object_store'])
+    def test_upload_download_bucket_content_with_large_file(self):
+        """
+        Creates a 6 Gig file in the temp directory, then uploads it to
+        Swift. Once uploaded, then downloads to a new file in the temp
+        directory and compares the two files to see if they match.
+        """
+        temp_dir = tempfile.gettempdir()
+        file_name = '6GigTest.tmp'
+        six_gig_file = os.path.join(temp_dir, file_name)
+        with open(six_gig_file, "wb") as out:
+            out.truncate(6 * 1024 * 1024 * 1024)  # 6 Gig...
+        with helpers.cleanup_action(lambda: os.remove(six_gig_file)):
+            download_file = "{0}/cbtestfile-{1}".format(temp_dir, file_name)
+            bucket_name = "cbtestbucketlargeobjs-{0}".format(uuid.uuid4())
+            test_bucket = self.provider.object_store.create(bucket_name)
+            with helpers.cleanup_action(lambda: test_bucket.delete()):
+                test_obj = test_bucket.create_object(file_name)
+                with helpers.cleanup_action(lambda: test_obj.delete()):
+                    file_uploaded = test_obj.upload_from_file(six_gig_file)
+                    self.assertTrue(file_uploaded, "Could not upload object?")
+                    with helpers.cleanup_action(
+                            lambda: os.remove(download_file)):
+                        with open(download_file, 'wb') as f:
+                            test_obj.save_content(f)
+                            self.assertTrue(
+                                filecmp.cmp(six_gig_file, download_file),
+                                "Uploaded file != downloaded")

+ 8 - 7
test/test_region_service.py

@@ -1,16 +1,14 @@
-import six
+from test import helpers
+from test.helpers import ProviderTestBase
 
 
 from cloudbridge.cloud.interfaces import Region
 from cloudbridge.cloud.interfaces import Region
-from test.helpers import ProviderTestBase
-import test.helpers as helpers
 
 
+import six
 
 
-class CloudRegionServiceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudRegionServiceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
+class CloudRegionServiceTestCase(ProviderTestBase):
 
 
+    @helpers.skipIfNoService(['compute.regions'])
     def test_get_and_list_regions(self):
     def test_get_and_list_regions(self):
         """
         """
         Test whether the region listing methods work,
         Test whether the region listing methods work,
@@ -47,6 +45,7 @@ class CloudRegionServiceTestCase(ProviderTestBase):
             "Region name {0} not in JSON representation {1}".format(
             "Region name {0} not in JSON representation {1}".format(
                 region.name, region.to_json()))
                 region.name, region.to_json()))
 
 
+    @helpers.skipIfNoService(['compute.regions'])
     def test_regions_unique(self):
     def test_regions_unique(self):
         """
         """
         Regions should not return duplicate items
         Regions should not return duplicate items
@@ -55,6 +54,7 @@ class CloudRegionServiceTestCase(ProviderTestBase):
         unique_regions = set([region.id for region in regions])
         unique_regions = set([region.id for region in regions])
         self.assertTrue(len(regions) == len(list(unique_regions)))
         self.assertTrue(len(regions) == len(list(unique_regions)))
 
 
+    @helpers.skipIfNoService(['compute.regions'])
     def test_current_region(self):
     def test_current_region(self):
         """
         """
         RegionService.current should return a valid region
         RegionService.current should return a valid region
@@ -63,6 +63,7 @@ class CloudRegionServiceTestCase(ProviderTestBase):
         self.assertIsInstance(current_region, Region)
         self.assertIsInstance(current_region, Region)
         self.assertTrue(current_region in self.provider.compute.regions.list())
         self.assertTrue(current_region in self.provider.compute.regions.list())
 
 
+    @helpers.skipIfNoService(['compute.regions'])
     def test_zones(self):
     def test_zones(self):
         """
         """
         Test whether regions return the correct zone information
         Test whether regions return the correct zone information

+ 65 - 33
test/test_security_service.py

@@ -1,20 +1,19 @@
 """Test cloudbridge.security modules."""
 """Test cloudbridge.security modules."""
 import json
 import json
-from test.helpers import ProviderTestBase
-import time
+import unittest
 import uuid
 import uuid
 
 
-import test.helpers as helpers
+from test import helpers
+from test.helpers import ProviderTestBase
 
 
+from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
 
-class CloudSecurityServiceTestCase(ProviderTestBase):
 
 
-    def __init__(self, methodName, provider):
-        super(CloudSecurityServiceTestCase, self).__init__(
-            methodName=methodName, provider=provider)
+class CloudSecurityServiceTestCase(ProviderTestBase):
 
 
+    @helpers.skipIfNoService(['security.key_pairs'])
     def test_crud_key_pair_service(self):
     def test_crud_key_pair_service(self):
-        name = 'cbtestkeypairA-{0}'.format(uuid.uuid4()).lower()
+        name = 'cbtestkeypairA-{0}'.format(uuid.uuid4())
         kp = self.provider.security.key_pairs.create(name=name)
         kp = self.provider.security.key_pairs.create(name=name)
         with helpers.cleanup_action(
         with helpers.cleanup_action(
             lambda:
             lambda:
@@ -64,8 +63,9 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             no_kp,
             no_kp,
             "Found a key pair {0} that should not exist?".format(no_kp))
             "Found a key pair {0} that should not exist?".format(no_kp))
 
 
+    @helpers.skipIfNoService(['security.key_pairs'])
     def test_key_pair(self):
     def test_key_pair(self):
-        name = 'cbtestkeypairB-{0}'.format(uuid.uuid4()).lower()
+        name = 'cbtestkeypairB-{0}'.format(uuid.uuid4())
         kp = self.provider.security.key_pairs.create(name=name)
         kp = self.provider.security.key_pairs.create(name=name)
         with helpers.cleanup_action(lambda: kp.delete()):
         with helpers.cleanup_action(lambda: kp.delete()):
             kpl = self.provider.security.key_pairs.list()
             kpl = self.provider.security.key_pairs.list()
@@ -100,14 +100,19 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
                 lambda: self.provider.network.delete(network_id=net.id)):
                 lambda: self.provider.network.delete(network_id=net.id)):
             self.provider.security.security_groups.delete(group_id=sg.id)
             self.provider.security.security_groups.delete(group_id=sg.id)
 
 
+    @helpers.skipIfNoService(['security.security_groups'])
     def test_crud_security_group_service(self):
     def test_crud_security_group_service(self):
-        name = 'cbtestsecuritygroupA-{0}'.format(uuid.uuid4()).lower()
-        net = self.provider.network.create(name=name)
-        sg = self.provider.security.security_groups.create(
-            name=name, description=name, network_id=net.id)
-        #Empty security groups don't exist in GCE. Let's add a dummy rule.
-        sg.add_rule(ip_protocol='tcp', cidr_ip='0.0.0.0/0')
-        with helpers.cleanup_action(lambda: self.cleanup_sg(sg, net)):
+        name = 'CBTestSecurityGroupA-{0}'.format(uuid.uuid4())
+
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        net = None
+        sg = None
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                network=net, security_group=sg)):
+            net, _ = helpers.create_test_network(self.provider, name)
+            sg = self.provider.security.security_groups.create(
+                name=name, description=name, network_id=net.id)
             self.assertEqual(name, sg.description)
             self.assertEqual(name, sg.description)
 
 
             # test list method
             # test list method
@@ -155,13 +160,20 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             len(no_sg) == 0,
             len(no_sg) == 0,
             "Found a bogus security group?!?".format(no_sg))
             "Found a bogus security group?!?".format(no_sg))
 
 
+    @helpers.skipIfNoService(['security.security_groups'])
     def test_security_group(self):
     def test_security_group(self):
         """Test for proper creation of a security group."""
         """Test for proper creation of a security group."""
-        name = 'cbtestsecuritygroupB-{0}'.format(uuid.uuid4()).lower()
-        net = self.provider.network.create(name=name)
-        sg = self.provider.security.security_groups.create(
-            name=name, description=name, network_id=net.id)
-        with helpers.cleanup_action(lambda: self.cleanup_sg(sg, net)):
+        name = 'CBTestSecurityGroupB-{0}'.format(uuid.uuid4())
+
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        net = None
+        sg = None
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                network=net, security_group=sg)):
+            net, _ = helpers.create_test_network(self.provider, name)
+            sg = self.provider.security.security_groups.create(
+                name=name, description=name, network_id=net.id)
             rule = sg.add_rule(ip_protocol='tcp', from_port=1111, to_port=1111,
             rule = sg.add_rule(ip_protocol='tcp', from_port=1111, to_port=1111,
                                cidr_ip='0.0.0.0/0')
                                cidr_ip='0.0.0.0/0')
             found_rule = sg.get_rule(ip_protocol='tcp', from_port=1111,
             found_rule = sg.get_rule(ip_protocol='tcp', from_port=1111,
@@ -195,7 +207,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 #                 sort_keys=True)
 #                 sort_keys=True)
 #             self.assertTrue(
 #             self.assertTrue(
 #                 sg.to_json() == json_repr,
 #                 sg.to_json() == json_repr,
-#                 "JSON sec group representation {0} does not match expected {1}"
+#                 "JSON SG representation {0} does not match expected {1}"
 #                 .format(sg.to_json(), json_repr))
 #                 .format(sg.to_json(), json_repr))
 
 
         sgl = self.provider.security.security_groups.list()
         sgl = self.provider.security.security_groups.list()
@@ -205,13 +217,26 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             "Security group {0} should have been deleted but still exists."
             "Security group {0} should have been deleted but still exists."
             .format(name))
             .format(name))
 
 
+    @helpers.skipIfNoService(['security.security_groups'])
     def test_security_group_rule_add_twice(self):
     def test_security_group_rule_add_twice(self):
         """Test whether adding the same rule twice succeeds."""
         """Test whether adding the same rule twice succeeds."""
-        name = 'cbtestsecuritygroupB-{0}'.format(uuid.uuid4()).lower()
-        net = self.provider.network.create(name=name)
-        sg = self.provider.security.security_groups.create(
-            name=name, description=name, network_id=net.id)
-        with helpers.cleanup_action(lambda: self.cleanup_sg(sg, net)):
+        if isinstance(self.provider, TestMockHelperMixin):
+            raise unittest.SkipTest(
+                "Mock provider returns InvalidParameterValue: "
+                "Value security_group is invalid for parameter.")
+
+        name = 'CBTestSecurityGroupC-{0}'.format(uuid.uuid4())
+
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        net = None
+        sg = None
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                network=net, security_group=sg)):
+
+            net, _ = helpers.create_test_network(self.provider, name)
+            sg = self.provider.security.security_groups.create(
+                name=name, description=name, network_id=net.id)
             rule = sg.add_rule(ip_protocol='tcp', from_port=1111, to_port=1111,
             rule = sg.add_rule(ip_protocol='tcp', from_port=1111, to_port=1111,
                                cidr_ip='0.0.0.0/0')
                                cidr_ip='0.0.0.0/0')
             # attempting to add the same rule twice should succeed
             # attempting to add the same rule twice should succeed
@@ -222,18 +247,25 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
                 "Expected rule {0} not found in security group: {1}".format(
                 "Expected rule {0} not found in security group: {1}".format(
                     same_rule, sg.rules))
                     same_rule, sg.rules))
 
 
+    @helpers.skipIfNoService(['security.security_groups'])
     def test_security_group_group_rule(self):
     def test_security_group_group_rule(self):
         """Test for proper creation of a security group rule."""
         """Test for proper creation of a security group rule."""
-        name = 'cbtestsecuritygroupC-{0}'.format(uuid.uuid4()).lower()
-        net = self.provider.network.create(name=name)
-        sg = self.provider.security.security_groups.create(
-            name=name, description=name, network_id=net.id)
-        with helpers.cleanup_action(lambda: self.cleanup_sg(sg, net)):
+        name = 'CBTestSecurityGroupD-{0}'.format(uuid.uuid4())
+
+        # Declare these variables and late binding will allow
+        # the cleanup method access to the most current values
+        net = None
+        sg = None
+        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+                network=net, security_group=sg)):
+            net, _ = helpers.create_test_network(self.provider, name)
+            sg = self.provider.security.security_groups.create(
+                name=name, description=name, network_id=net.id)
             self.assertTrue(
             self.assertTrue(
                 len(sg.rules) == 0,
                 len(sg.rules) == 0,
                 "Expected no security group group rule. Got {0}."
                 "Expected no security group group rule. Got {0}."
                 .format(sg.rules))
                 .format(sg.rules))
-            rule = sg.add_rule(src_group=sg, ip_protocol='tcp', from_port=0,
+            rule = sg.add_rule(src_group=sg, ip_protocol='tcp', from_port=1,
                                to_port=65535)
                                to_port=65535)
             self.assertTrue(
             self.assertTrue(
                 rule.group.name == name,
                 rule.group.name == name,

+ 23 - 6
tox.ini

@@ -1,14 +1,31 @@
 # Tox (http://tox.testrun.org/) is a tool for running tests
 # Tox (http://tox.testrun.org/) is a tool for running tests
 # in multiple virtualenvs. This configuration file will run the
 # in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it, "pip install tox"
-# and then run "tox" from this directory.
+# test suite on all supported python versions and providers.
+# To use it, "pip install tox" and then run "tox" from this directory.
+# You will have to set all required environment variables (below) before
+# running the tests.
+#
+# Alternatively, to run mock tests only, run tox as follows:
+# CB_USE_MOCK_PROVIDERS=True tox -e py27-aws
+#
+# Simply running tox -e py27-aws also works, because the default is to use
+# mock providers.
 
 
 [tox]
 [tox]
-envlist = py27, py35, pypy
+envlist = {py27,py36,pypy}-{aws,openstack,gce}
 
 
 [testenv]
 [testenv]
-commands = {envpython} -m coverage run --branch --source=cloudbridge --omit=cloudbridge/cloud/interfaces/* setup.py test
-passenv = AWS_ACCESS_KEY AWS_SECRET_KEY GCE_CLIENT_EMAIL GCE_PROJECT_NAME GCE_DEFAULT_ZONE GCE_SERVICE_CREDS_FILE OS_AUTH_URL OS_PASSWORD OS_TENANT_NAME OS_USERNAME OS_REGION_NAME NOVA_SERVICE_NAME CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS CB_IMAGE_OS CB_INSTANCE_TYPE_OS CB_PLACEMENT_OS CB_TEST_PROVIDER CB_USE_MOCK_PROVIDERS
+commands = flake8 cloudbridge test setup.py 
+    {envpython} -m coverage run --branch --source=cloudbridge --omit=cloudbridge/cloud/interfaces/* setup.py test {posargs}
+setenv =
+    aws: CB_TEST_PROVIDER=aws
+    openstack: CB_TEST_PROVIDER=openstack
+    gce: CB_TEST_PROVIDER=gce
+passenv =
+    CB_USE_MOCK_PROVIDERS
+    aws: CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS AWS_ACCESS_KEY AWS_SECRET_KEY
+    openstack:  CB_IMAGE_OS CB_INSTANCE_TYPE_OS CB_PLACEMENT_OS OS_AUTH_URL OS_PASSWORD OS_PROJECT_NAME OS_TENANT_NAME OS_USERNAME OS_REGION_NAME OS_USER_DOMAIN_NAME OS_PROJECT_DOMAIN_NAME NOVA_SERVICE_NAME
+    gce: CB_IMAGE_GCE CB_INSTANCE_TYPE_GCE CB_PLACEMENT_GCE GCE_CLIENT_EMAIL GCE_PROJECT_NAME GCE_DEFAULT_ZONE GCE_SERVICE_CREDS_FILE
 deps =
 deps =
     -rrequirements.txt
     -rrequirements.txt
-    coverage
+    coverage