almahmoud 7 лет назад
Родитель
Сommit
aeccfd0fe9
100 измененных файлов с 9248 добавлено и 2866 удалено
  1. 1 0
      .gitignore
  2. 31 25
      .travis.yml
  3. 39 45
      README.rst
  4. 2 2
      cloudbridge/__init__.py
  5. 6 4
      cloudbridge/cloud/base/helpers.py
  6. 57 0
      cloudbridge/cloud/base/middleware.py
  7. 19 4
      cloudbridge/cloud/base/provider.py
  8. 72 92
      cloudbridge/cloud/base/resources.py
  9. 220 60
      cloudbridge/cloud/base/services.py
  10. 168 0
      cloudbridge/cloud/base/subservices.py
  11. 27 41
      cloudbridge/cloud/factory.py
  12. 10 0
      cloudbridge/cloud/interfaces/exceptions.py
  13. 22 5
      cloudbridge/cloud/interfaces/provider.py
  14. 56 375
      cloudbridge/cloud/interfaces/resources.py
  15. 462 19
      cloudbridge/cloud/interfaces/services.py
  16. 401 0
      cloudbridge/cloud/interfaces/subservices.py
  17. 0 1
      cloudbridge/cloud/providers/aws/__init__.py
  18. 19 2
      cloudbridge/cloud/providers/aws/helpers.py
  19. 0 64
      cloudbridge/cloud/providers/aws/provider.py
  20. 29 227
      cloudbridge/cloud/providers/aws/resources.py
  21. 466 195
      cloudbridge/cloud/providers/aws/services.py
  22. 39 0
      cloudbridge/cloud/providers/aws/subservices.py
  23. 4 3
      cloudbridge/cloud/providers/azure/azure_client.py
  24. 4 3
      cloudbridge/cloud/providers/azure/provider.py
  25. 48 271
      cloudbridge/cloud/providers/azure/resources.py
  26. 536 301
      cloudbridge/cloud/providers/azure/services.py
  27. 38 0
      cloudbridge/cloud/providers/azure/subservices.py
  28. 35 0
      cloudbridge/cloud/providers/gcp/README.rst
  29. 5 0
      cloudbridge/cloud/providers/gcp/__init__.py
  30. 188 0
      cloudbridge/cloud/providers/gcp/helpers.py
  31. 369 0
      cloudbridge/cloud/providers/gcp/provider.py
  32. 2028 0
      cloudbridge/cloud/providers/gcp/resources.py
  33. 1642 0
      cloudbridge/cloud/providers/gcp/services.py
  34. 39 0
      cloudbridge/cloud/providers/gcp/subservices.py
  35. 5 0
      cloudbridge/cloud/providers/mock/__init__.py
  36. 100 0
      cloudbridge/cloud/providers/mock/provider.py
  37. 96 296
      cloudbridge/cloud/providers/openstack/resources.py
  38. 426 227
      cloudbridge/cloud/providers/openstack/services.py
  39. 41 0
      cloudbridge/cloud/providers/openstack/subservices.py
  40. 0 5
      cloudbridge/test/__init__.py
  41. 97 72
      cloudbridge/test/helpers/__init__.py
  42. 5 3
      cloudbridge/test/helpers/standard_interface_tests.py
  43. 36 28
      cloudbridge/test/test_block_store_service.py
  44. 31 72
      cloudbridge/test/test_cloud_factory.py
  45. 6 3
      cloudbridge/test/test_cloud_helpers.py
  46. 58 35
      cloudbridge/test/test_compute_service.py
  47. 16 9
      cloudbridge/test/test_image_service.py
  48. 7 13
      cloudbridge/test/test_interface.py
  49. 110 52
      cloudbridge/test/test_network_service.py
  50. 17 17
      cloudbridge/test/test_object_life_cycle.py
  51. 42 28
      cloudbridge/test/test_object_store_service.py
  52. 12 13
      cloudbridge/test/test_region_service.py
  53. 41 25
      cloudbridge/test/test_security_service.py
  54. 14 5
      cloudbridge/test/test_vm_types_service.py
  55. BIN
      credentials.tar.gz.enc
  56. 2 2
      docs/api_docs/cloud/services.rst
  57. 7 5
      docs/conf.py
  58. 183 16
      docs/extras/_images/object_relationships_detailed.svg
  59. 50 40
      docs/getting_started.rst
  60. 168 0
      docs/topics/aws_mapping.rst
  61. 221 0
      docs/topics/azure_mapping.rst
  62. BIN
      docs/topics/captures/aws-ami-dash.png
  63. BIN
      docs/topics/captures/aws-bucket.png
  64. BIN
      docs/topics/captures/aws-instance-dash.png
  65. BIN
      docs/topics/captures/aws-services-dash.png
  66. BIN
      docs/topics/captures/az-app-1.png
  67. BIN
      docs/topics/captures/az-app-2.png
  68. BIN
      docs/topics/captures/az-app-3.png
  69. BIN
      docs/topics/captures/az-app-4.png
  70. BIN
      docs/topics/captures/az-app-5.png
  71. BIN
      docs/topics/captures/az-app-6.png
  72. BIN
      docs/topics/captures/az-app-7.png
  73. BIN
      docs/topics/captures/az-dir-1.png
  74. BIN
      docs/topics/captures/az-dir-2.png
  75. BIN
      docs/topics/captures/az-label-dash.png
  76. BIN
      docs/topics/captures/az-net-id.png
  77. BIN
      docs/topics/captures/az-net-label.png
  78. BIN
      docs/topics/captures/az-role-1.png
  79. BIN
      docs/topics/captures/az-role-2.png
  80. BIN
      docs/topics/captures/az-role-3.png
  81. BIN
      docs/topics/captures/az-storacc.png
  82. BIN
      docs/topics/captures/az-sub-1.png
  83. BIN
      docs/topics/captures/az-sub-2.png
  84. BIN
      docs/topics/captures/az-subnet-label.png
  85. BIN
      docs/topics/captures/az-subnet-name.png
  86. BIN
      docs/topics/captures/gce-sa-1.png
  87. BIN
      docs/topics/captures/gce-sa-2.png
  88. BIN
      docs/topics/captures/gce-sa-3.png
  89. BIN
      docs/topics/captures/gce-sa-4.png
  90. BIN
      docs/topics/captures/gce-sa-5.png
  91. BIN
      docs/topics/captures/os-instance-dash.png
  92. BIN
      docs/topics/captures/os-kp-dash.png
  93. 0 121
      docs/topics/dashboard.rst
  94. 3 3
      docs/topics/design_decisions.rst
  95. 219 0
      docs/topics/event_system.rst
  96. 7 7
      docs/topics/launch.rst
  97. 30 27
      docs/topics/networking.rst
  98. 3 3
      docs/topics/object_storage.rst
  99. 111 0
      docs/topics/os_mapping.rst
  100. 2 0
      docs/topics/overview.rst

+ 1 - 0
.gitignore

@@ -59,6 +59,7 @@ target/
 *.DS_Store
 /venv/
 
+credentials.tar.gz
 bootstrap.py
 ISB-*
 launch.json

+ 31 - 25
.travis.yml

@@ -2,38 +2,42 @@ dist: trusty
 language: python
 cache:
   directories:
-    - $HOME/.cache/pip
-    - $TRAVIS_BUILD_DIR/.tox
+  - "$HOME/.cache/pip"
+  - "$TRAVIS_BUILD_DIR/.tox"
 os:
-  - linux
-#  - osx
+- linux
 matrix:
   fast_finish: true
   allow_failures:
-    - os: osx
+  - os: osx
   include:
-    - python: 2.7
-      env: TOX_ENV=py27-aws
-    - python: 2.7
-      env: TOX_ENV=py27-azure
-    - python: 2.7
-      env: TOX_ENV=py27-openstack
-    - python: 3.6
-      env: TOX_ENV=py36-aws
-    - python: 3.6
-      env: TOX_ENV=py36-azure
-    - python: 3.6
-      env: TOX_ENV=py36-openstack
-    - python: pypy-5.3.1
-      env: TOX_ENV=pypy-aws
-    - python: pypy-5.3.1
-      env: TOX_ENV=pypy-azure
-    - python: pypy-5.3.1
-      env: TOX_ENV=pypy-openstack
+  - python: 2.7
+    env: TOX_ENV=py27-aws
+  - python: 2.7
+    env: TOX_ENV=py27-azure
+  - python: 2.7
+    env: TOX_ENV=py27-gcp
+  - python: 2.7
+    env: TOX_ENV=py27-mock
+  - python: 2.7
+    env: TOX_ENV=py27-openstack
+  - python: 3.6
+    env: TOX_ENV=py36-aws
+  - python: 3.6
+    env: TOX_ENV=py36-azure
+  - python: 3.6
+    env: TOX_ENV=py36-gcp
+  - python: 3.6
+    env: TOX_ENV=py36-mock
+  - python: 3.6
+    env: TOX_ENV=py36-openstack
 env:
   global:
     - PYTHONUNBUFFERED=True
+    - COVERALLS_PARALLEL=true
 before_install:
+    - openssl aes-256-cbc -K $encrypted_b3fcf6d0737c_key -iv $encrypted_b3fcf6d0737c_iv
+            -in credentials.tar.gz.enc -out credentials.tar.gz -d
     - |
       case "$TRAVIS_EVENT_TYPE" in
         push|pull_request)
@@ -57,7 +61,7 @@ before_install:
            }
            ;;
         *)
-           echo "Build triggered through API or CRON job. Running regardless of changes"
+           echo "Build triggered through API or CRON job. Running regardless of changes."
            ;;
       esac
 install:
@@ -67,7 +71,7 @@ install:
     - pip install coveralls
     - pip install codecov
 script:
-    - tox -r -e $TOX_ENV
+    - tox -e $TOX_ENV
 after_script:
     - |
       case "$TRAVIS_EVENT_TYPE" in
@@ -88,3 +92,5 @@ after_script:
            coveralls & codecov & wait
            ;;
       esac
+notifications:
+    webhooks: https://coveralls.io/webhook

+ 39 - 45
README.rst

@@ -1,7 +1,15 @@
-CloudBridge provides a simple layer of abstraction over different
+CloudBridge provides a consistent layer of abstraction over different
 Infrastructure-as-a-Service cloud providers, reducing or eliminating the need
 to write conditional code for each cloud.
 
+Documentation
+~~~~~~~~~~~~~
+Detailed documentation can be found at http://cloudbridge.cloudve.org.
+
+
+Build Status Tests
+~~~~~~~~~~~~~~~~~~
+
 .. image:: https://coveralls.io/repos/CloudVE/cloudbridge/badge.svg?branch=master&service=github
    :target: https://coveralls.io/github/CloudVE/cloudbridge?branch=master
    :alt: Code Coverage
@@ -14,53 +22,44 @@ to write conditional code for each cloud.
    :target: http://cloudbridge.readthedocs.org/en/latest/?badge=latest
    :alt: Documentation Status
 
-.. image:: https://badge.waffle.io/CloudVE/cloudbridge.png?label=in%20progress&title=In%20Progress 
-   :target: https://waffle.io/CloudVE/cloudbridge?utm_source=badge
-   :alt: 'Waffle.io - Issues in progress'
-
 .. |aws-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/1
               :target: https://travis-ci.org/CloudVE/cloudbridge
-.. |aws-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/4
-              :target: https://travis-ci.org/CloudVE/cloudbridge
-.. |aws-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/7
+.. |aws-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/6
               :target: https://travis-ci.org/CloudVE/cloudbridge
 
-.. |os-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/3
-             :target: https://travis-ci.org/CloudVE/cloudbridge
-.. |os-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/6
-             :target: https://travis-ci.org/CloudVE/cloudbridge
-.. |os-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/9
-             :target: https://travis-ci.org/CloudVE/cloudbridge
-
 .. |azure-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/2
-                :target: https://travis-ci.org/CloudVE/cloudbridge/branches
-.. |azure-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/5
-                :target: https://travis-ci.org/CloudVE/cloudbridge/branches
-.. |azure-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/8
-                :target: https://travis-ci.org/CloudVE/cloudbridge/branches
+                :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |azure-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/7
+                :target: https://travis-ci.org/CloudVE/cloudbridge
 
-.. |gce-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/gce/3
-              :target: https://travis-ci.org/CloudVE/cloudbridge/branches
-.. |gce-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/gce/6
-              :target: https://travis-ci.org/CloudVE/cloudbridge/branches
-.. |gce-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/gce/9
-              :target: https://travis-ci.org/CloudVE/cloudbridge/branches
+.. |gcp-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/3
+              :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |gcp-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/8
+              :target: https://travis-ci.org/CloudVE/cloudbridge
 
+.. |mock-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/4
+              :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |mock-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/9
+              :target: https://travis-ci.org/CloudVE/cloudbridge
 
-Build Status Tests
-~~~~~~~~~~~~~~~~~~
+.. |os-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/5
+             :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |os-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/10
+             :target: https://travis-ci.org/CloudVE/cloudbridge
 
-+--------------------------+--------------+--------------+--------------+
-| **Provider/Environment** | py27         | py36         | pypy         |
-+--------------------------+--------------+--------------+--------------+
-| **AWS**                  | |aws-py27|   | |aws-py36|   | |aws-pypy|   |
-+--------------------------+--------------+--------------+--------------+
-| **OpenStack**            | |os-py27|    | |os-py36|    | |os-pypy|    |
-+--------------------------+--------------+--------------+--------------+
-| **Azure**                | |azure-py27| | |azure-py36| | |azure-py36| |
-+--------------------------+--------------+--------------+--------------+
-| **GCE (alpha)**          | |gce-py27|   | |gce-py36|   | |gce-pypy|   |
-+--------------------------+--------------+--------------+--------------+
++---------------------------+----------------+----------------+
+| **Provider/Environment**  | **Python 2.7** | **Python 3.6** |
++---------------------------+----------------+----------------+
+| **Amazon Web Services**   | |aws-py27|     | |aws-py36|     |
++---------------------------+----------------+----------------+
+| **Google Cloud Platform** | |gcp-py27|     | |gcp-py36|     |
++---------------------------+----------------+----------------+
+| **Microsoft Azure**       | |azure-py27|   | |azure-py36|   |
++---------------------------+----------------+----------------+
+| **OpenStack**             | |os-py27|      | |os-py36|      |
++---------------------------+----------------+----------------+
+| **Mock Provider**         | |mock-py27|    | |mock-py36|    |
++---------------------------+----------------+----------------+
 
 Installation
 ~~~~~~~~~~~~
@@ -89,7 +88,7 @@ exploring the API:
   print(provider.security.key_pairs.list())
 
 The exact same command (as well as any other CloudBridge method) will run with
-any of the supported providers: ``ProviderList.[AWS | AZURE | OPENSTACK]``!
+any of the supported providers: ``ProviderList.[AWS | AZURE | GCP | OPENSTACK]``!
 
 
 Citation
@@ -101,11 +100,6 @@ presented at the Proceedings of the XSEDE16 Conference on Diversity, Big Data, a
 DOI: http://dx.doi.org/10.1145/2949550.2949648
 
 
-Documentation
-~~~~~~~~~~~~~
-Documentation can be found at https://cloudbridge.readthedocs.org.
-
-
 Quick Reference
 ~~~~~~~~~~~~~~~
 The following object graph shows how to access various provider services, and the resource

+ 2 - 2
cloudbridge/__init__.py

@@ -53,7 +53,7 @@ class CBLogger(logging.Logger):
 #   import cloudbridge
 #   cloudbridge.set_stream_logger(__name__)
 #   OR
-#   cloudbridge.set_file_logger(__name__, '/tmp/cb.log')
+#   cloudbridge.set_file_logger(__name__, '/tmp/log')
 default_format_string = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
 logging.setLoggerClass(CBLogger)
 logging.addLevelName(TRACE, "TRACE")
@@ -66,7 +66,7 @@ log.addHandler(NullHandler())
 #   import cloudbridge
 #   cloudbridge.set_stream_logger(__name__)
 #   OR
-#   cloudbridge.set_file_logger(__name__, '/tmp/cb.log')
+#   cloudbridge.set_file_logger(__name__, '/tmp/log')
 
 
 def set_stream_logger(name, level=TRACE, format_string=None):

+ 6 - 4
cloudbridge/cloud/base/helpers.py

@@ -16,6 +16,8 @@ import six
 
 import cloudbridge
 
+from ..interfaces.exceptions import InvalidParamException
+
 
 def generate_key_pair():
     """
@@ -69,7 +71,7 @@ def generic_find(filter_names, kwargs, objs):
 
     # All kwargs should have been popped at this time.
     if len(kwargs) > 0:
-        raise TypeError(
+        raise InvalidParamException(
             "Unrecognised parameters for search: %s. Supported attributes: %s"
             % (kwargs, filter_names))
 
@@ -136,7 +138,7 @@ def get_env(varname, default_value=None):
     return value
 
 
-# Alias deprication decorator, following:
+# Alias deprecation decorator, following:
 # https://stackoverflow.com/questions/49802412/
 # how-to-implement-deprecation-in-python-with-argument-alias
 def deprecated_alias(**aliases):
@@ -153,8 +155,8 @@ def rename_kwargs(func_name, kwargs, aliases):
     for alias, new in aliases.items():
         if alias in kwargs:
             if new in kwargs:
-                raise TypeError('{} received both {} and {}'.format(
-                    func_name, alias, new))
+                raise InvalidParamException(
+                    '{} received both {} and {}'.format(func_name, alias, new))
             # Manually invoke the deprecated decorator with an empty lambda
             # to signal deprecation
             deprecated(deprecated_in='1.1',

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

@@ -0,0 +1,57 @@
+import logging
+import sys
+
+from pyeventsystem.middleware import dispatch as pyevent_dispatch
+from pyeventsystem.middleware import intercept
+from pyeventsystem.middleware import observe
+
+import six
+
+from ..interfaces.exceptions import CloudBridgeBaseException
+
+log = logging.getLogger(__name__)
+
+
+dispatch = pyevent_dispatch
+
+
+class EventDebugLoggingMiddleware(object):
+    """
+    Logs all event parameters. This middleware should not be enabled other
+    than for debugging, as it could log sensitive parameters such as
+    access keys.
+    """
+    @observe(event_pattern="*", priority=100)
+    def pre_log_event(self, event_args, *args, **kwargs):
+        log.debug("Event: {0}, args: {1} kwargs: {2}".format(
+            event_args.get("event"), args, kwargs))
+
+    @observe(event_pattern="*", priority=4900)
+    def post_log_event(self, event_args, *args, **kwargs):
+        log.debug("Event: {0}, result: {1}".format(
+            event_args.get("event"), event_args.get("result")))
+
+
+class ExceptionWrappingMiddleware(object):
+    """
+    Wraps all unhandled exceptions in cloudbridge exceptions.
+    """
+    @intercept(event_pattern="*", priority=1050)
+    def wrap_exception(self, event_args, *args, **kwargs):
+        next_handler = event_args.pop("next_handler")
+        if not next_handler:
+            return
+        try:
+            return next_handler.invoke(event_args, *args, **kwargs)
+        except Exception as e:
+            if isinstance(e, CloudBridgeBaseException):
+                raise
+            else:
+                ex_type, ex_value, traceback = sys.exc_info()
+                cb_ex = CloudBridgeBaseException(
+                    "CloudBridgeBaseException: {0} from exception type: {1}"
+                    .format(ex_value, ex_type))
+                if sys.version_info >= (3, 0):
+                    six.raise_from(cb_ex, e)
+                else:
+                    six.reraise(CloudBridgeBaseException, cb_ex, traceback)

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

@@ -8,11 +8,14 @@ try:
 except ImportError:  # Python 2
     from ConfigParser import SafeConfigParser as ConfigParser
 
+from pyeventsystem.middleware import SimpleMiddlewareManager
+
 import six
 
-from cloudbridge.cloud.interfaces import CloudProvider
-from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
-from cloudbridge.cloud.interfaces.resources import Configuration
+from ..base.middleware import ExceptionWrappingMiddleware
+from ..interfaces import CloudProvider
+from ..interfaces.exceptions import ProviderConnectionException
+from ..interfaces.resources import Configuration
 
 log = logging.getLogger(__name__)
 
@@ -80,11 +83,12 @@ class BaseConfiguration(Configuration):
 
 
 class BaseCloudProvider(CloudProvider):
-
     def __init__(self, config):
         self._config = BaseConfiguration(config)
         self._config_parser = ConfigParser()
         self._config_parser.read(CloudBridgeConfigLocations)
+        self._middleware = SimpleMiddlewareManager()
+        self.add_required_middleware()
 
     @property
     def config(self):
@@ -94,6 +98,17 @@ class BaseCloudProvider(CloudProvider):
     def name(self):
         return str(self.__class__.__name__)
 
+    @property
+    def middleware(self):
+        return self._middleware
+
+    def add_required_middleware(self):
+        """
+        Adds common middleware that is essential for cloudbridge to function.
+        Any other extra middleware can be added through the provider factory.
+        """
+        self.middleware.add(ExceptionWrappingMiddleware())
+
     def authenticate(self):
         """
         A basic implementation which simply runs a low impact command to

+ 72 - 92
cloudbridge/cloud/base/resources.py

@@ -12,21 +12,17 @@ import uuid
 
 import six
 
-import cloudbridge.cloud.base.helpers as cb_helpers
-from cloudbridge.cloud.interfaces.exceptions \
-    import InvalidConfigurationException
+from cloudbridge.cloud.interfaces.exceptions import \
+    InvalidConfigurationException
 from cloudbridge.cloud.interfaces.exceptions import InvalidLabelException
 from cloudbridge.cloud.interfaces.exceptions import InvalidNameException
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
 from cloudbridge.cloud.interfaces.resources import Bucket
-from cloudbridge.cloud.interfaces.resources import BucketContainer
 from cloudbridge.cloud.interfaces.resources import BucketObject
 from cloudbridge.cloud.interfaces.resources import CloudResource
 from cloudbridge.cloud.interfaces.resources import FloatingIP
-from cloudbridge.cloud.interfaces.resources import FloatingIPContainer
 from cloudbridge.cloud.interfaces.resources import FloatingIpState
-from cloudbridge.cloud.interfaces.resources import GatewayContainer
 from cloudbridge.cloud.interfaces.resources import GatewayState
 from cloudbridge.cloud.interfaces.resources import Instance
 from cloudbridge.cloud.interfaces.resources import InstanceState
@@ -49,11 +45,12 @@ from cloudbridge.cloud.interfaces.resources import Subnet
 from cloudbridge.cloud.interfaces.resources import SubnetState
 from cloudbridge.cloud.interfaces.resources import VMFirewall
 from cloudbridge.cloud.interfaces.resources import VMFirewallRule
-from cloudbridge.cloud.interfaces.resources import VMFirewallRuleContainer
 from cloudbridge.cloud.interfaces.resources import VMType
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import VolumeState
 
+from . import helpers as cb_helpers
+
 log = logging.getLogger(__name__)
 
 
@@ -86,8 +83,8 @@ class BaseCloudResource(CloudResource):
             raise InvalidLabelException(
                 u"Invalid label: %s. Label must be at least 3 characters long"
                 " and at most 63 characters. It must consist of lowercase"
-                " letters, numbers, or dashes. The label must not start or"
-                " end with a dash." % name)
+                " letters, numbers, or dashes. The label must start with a "
+                "letter and not end with a dash." % name)
 
     @staticmethod
     def assert_valid_resource_name(name):
@@ -265,12 +262,16 @@ class BasePageableObjectMixin(PageableObjectMixin):
     """
 
     def __iter__(self):
-        result_list = self.list()
+        for result in self.iter():
+            yield result
+
+    def iter(self, **kwargs):
+        result_list = self.list(**kwargs)
         if result_list.supports_server_paging:
             for result in result_list:
                 yield result
             while result_list.is_truncated:
-                result_list = self.list(marker=result_list.marker)
+                result_list = self.list(marker=result_list.marker, **kwargs)
                 for result in result_list:
                     yield result
         else:
@@ -319,6 +320,9 @@ class BaseInstance(BaseCloudResource, BaseObjectLifeCycleMixin, Instance):
             timeout=timeout,
             interval=interval)
 
+    def delete(self):
+        self._provider.compute.instances.delete(self)
+
 
 class BaseLaunchConfig(LaunchConfig):
 
@@ -458,6 +462,12 @@ class BaseVolume(BaseCloudResource, BaseObjectLifeCycleMixin, Volume):
             timeout=timeout,
             interval=interval)
 
+    def delete(self):
+        """
+        Delete this volume.
+        """
+        return self._provider.storage.volumes.delete(self)
+
 
 class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
 
@@ -480,6 +490,12 @@ class BaseSnapshot(BaseCloudResource, BaseObjectLifeCycleMixin, Snapshot):
             timeout=timeout,
             interval=interval)
 
+    def delete(self):
+        """
+        Delete this snapshot.
+        """
+        return self._provider.storage.snapshots.delete(self)
+
 
 class BaseKeyPair(BaseCloudResource, KeyPair):
 
@@ -518,15 +534,7 @@ class BaseKeyPair(BaseCloudResource, KeyPair):
         self._private_material = value
 
     def delete(self):
-        """
-        Delete this KeyPair.
-
-        :rtype: bool
-        :return: True if successful, otherwise False.
-        """
-        # This implementation assumes the `delete` method exists across
-        #  multiple providers.
-        self._key_pair.delete()
+        self._provider.security.key_pairs.delete(self)
 
 
 class BaseVMFirewall(BaseCloudResource, VMFirewall):
@@ -575,38 +583,7 @@ class BaseVMFirewall(BaseCloudResource, VMFirewall):
         """
         Delete this VM firewall.
         """
-        return self._vm_firewall.delete()
-
-
-class BaseVMFirewallRuleContainer(BasePageableObjectMixin,
-                                  VMFirewallRuleContainer):
-
-    def __init__(self, provider, firewall):
-        self.__provider = provider
-        self.firewall = firewall
-
-    @property
-    def _provider(self):
-        return self.__provider
-
-    def get(self, rule_id):
-        matches = [rule for rule in self if rule.id == rule_id]
-        if matches:
-            return matches[0]
-        else:
-            return None
-
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name', 'direction', 'protocol', 'from_port', 'to_port',
-                   'cidr', 'src_dest_fw', 'src_dest_fw_id']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
-
-    def delete(self, rule_id):
-        rule = self.get(rule_id)
-        if rule:
-            rule.delete()
+        return self._provider.security.vm_firewalls.delete(self)
 
 
 class BaseVMFirewallRule(BaseCloudResource, VMFirewallRule):
@@ -664,6 +641,9 @@ class BaseVMFirewallRule(BaseCloudResource, VMFirewallRule):
         js['firewall'] = self.firewall.id
         return js
 
+    def delete(self):
+        self._provider.security._vm_firewall_rules.delete(self.firewall, self)
+
 
 class BasePlacementZone(BaseCloudResource, PlacementZone):
 
@@ -748,33 +728,38 @@ class BaseBucket(BaseCloudResource, Bucket):
                 # check from most to least likely mutables
                 self.name == other.name)
 
+    def delete(self):
+        """
+        Delete this bucket.
+        """
+        self._provider.storage.buckets.delete(self.id)
 
-class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):
-
-    def __init__(self, provider, bucket):
-        self.__provider = provider
-        self.bucket = bucket
-
-    @property
-    def _provider(self):
-        return self.__provider
-
-
-class BaseGatewayContainer(GatewayContainer, BasePageableObjectMixin):
-
-    def __init__(self, provider, network):
-        self._network = network
-        self._provider = provider
+    # TODO: Discuss creating `create_object` method, or change docs
 
 
 class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
     CB_DEFAULT_NETWORK_LABEL = os.environ.get('CB_DEFAULT_NETWORK_LABEL',
                                               'cloudbridge-net')
+    CB_DEFAULT_IPV4RANGE = os.environ.get('CB_DEFAULT_IPV4RANGE',
+                                          u'10.0.0.0/16')
 
     def __init__(self, provider):
         super(BaseNetwork, self).__init__(provider)
 
+    @staticmethod
+    def cidr_blocks_overlap(block1, block2):
+        common_length = min(int(block1.split('/')[1]),
+                            int(block2.split('/')[1]))
+
+        p1 = [format(int(b), '08b') for b in block1.split('/')[0].split('.')]
+        prefix1 = ''.join(p1)[:common_length]
+
+        p2 = [format(int(b), '08b') for b in block2.split('/')[0].split('.')]
+        prefix2 = ''.join(p2)[:common_length]
+
+        return prefix1 == prefix2
+
     def wait_till_ready(self, timeout=None, interval=None):
         self.wait_for(
             [NetworkState.AVAILABLE],
@@ -782,9 +767,8 @@ class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
             timeout=timeout,
             interval=interval)
 
-    def create_subnet(self, label, cidr_block, zone=None):
-        return self._provider.networking.subnets.create(
-            label=label, network=self, cidr_block=cidr_block, zone=zone)
+    def delete(self):
+        self._provider.networking.networks.delete(self)
 
     def __eq__(self, other):
         return (isinstance(other, Network) and
@@ -797,6 +781,8 @@ class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
 
     CB_DEFAULT_SUBNET_LABEL = os.environ.get('CB_DEFAULT_SUBNET_LABEL',
                                              'cloudbridge-subnet')
+    CB_DEFAULT_SUBNET_IPV4RANGE = os.environ.get('CB_DEFAULT_SUBNET_IPV4RANGE',
+                                                 '10.0.0.0/24')
 
     def __init__(self, provider):
         super(BaseSubnet, self).__init__(provider)
@@ -818,27 +804,8 @@ class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
             timeout=timeout,
             interval=interval)
 
-
-class BaseFloatingIPContainer(FloatingIPContainer, BasePageableObjectMixin):
-
-    def __init__(self, provider, gateway):
-        self.__provider = provider
-        self.gateway = gateway
-
-    @property
-    def _provider(self):
-        return self.__provider
-
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name', 'public_ip']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
-
-    def delete(self, fip_id):
-        floating_ip = self.get(fip_id)
-        if floating_ip:
-            floating_ip.delete()
+    def delete(self):
+        self._provider.networking.subnets.delete(self)
 
 
 class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
@@ -868,6 +835,12 @@ class BaseFloatingIP(BaseCloudResource, BaseObjectLifeCycleMixin, FloatingIP):
                 self._provider == other._provider and
                 self.id == other.id)
 
+    def delete(self):
+        # For OS where the gateway is necessary, we pass the gateway when
+        # deleting, for all others we pass None and it will be ignored
+        gw = getattr(self, '_gateway_id', None)
+        self._provider.networking._floating_ips.delete(gw, self.id)
+
 
 class BaseRouter(BaseCloudResource, Router):
 
@@ -883,6 +856,9 @@ class BaseRouter(BaseCloudResource, Router):
                 self._provider == other._provider and
                 self.id == other.id)
 
+    def delete(self):
+        self._provider.networking.routers.delete(self)
+
 
 class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
                           InternetGateway):
@@ -906,3 +882,7 @@ class BaseInternetGateway(BaseCloudResource, BaseObjectLifeCycleMixin,
             terminal_states=[GatewayState.ERROR, GatewayState.UNKNOWN],
             timeout=timeout,
             interval=interval)
+
+    def delete(self):
+        return self._provider.networking._gateways.delete(self.network_id,
+                                                          self)

+ 220 - 60
cloudbridge/cloud/base/services.py

@@ -3,11 +3,14 @@ Base implementation for services available through a provider
 """
 import logging
 
-import cloudbridge.cloud.base.helpers as cb_helpers
-from cloudbridge.cloud.interfaces.resources import Router
+from cloudbridge.cloud.interfaces.exceptions import InvalidParamException
+from cloudbridge.cloud.interfaces.resources import Network
+from cloudbridge.cloud.interfaces.services import BucketObjectService
 from cloudbridge.cloud.interfaces.services import BucketService
 from cloudbridge.cloud.interfaces.services import CloudService
 from cloudbridge.cloud.interfaces.services import ComputeService
+from cloudbridge.cloud.interfaces.services import FloatingIPService
+from cloudbridge.cloud.interfaces.services import GatewayService
 from cloudbridge.cloud.interfaces.services import ImageService
 from cloudbridge.cloud.interfaces.services import InstanceService
 from cloudbridge.cloud.interfaces.services import KeyPairService
@@ -19,11 +22,17 @@ from cloudbridge.cloud.interfaces.services import SecurityService
 from cloudbridge.cloud.interfaces.services import SnapshotService
 from cloudbridge.cloud.interfaces.services import StorageService
 from cloudbridge.cloud.interfaces.services import SubnetService
+from cloudbridge.cloud.interfaces.services import VMFirewallRuleService
 from cloudbridge.cloud.interfaces.services import VMFirewallService
 from cloudbridge.cloud.interfaces.services import VMTypeService
 from cloudbridge.cloud.interfaces.services import VolumeService
 
+from . import helpers as cb_helpers
+from .middleware import dispatch
+from .resources import BaseNetwork
 from .resources import BasePageableObjectMixin
+from .resources import BaseRouter
+from .resources import BaseSubnet
 from .resources import ClientPagedResultList
 
 log = logging.getLogger(__name__)
@@ -31,32 +40,90 @@ log = logging.getLogger(__name__)
 
 class BaseCloudService(CloudService):
 
+    STANDARD_EVENT_PRIORITY = 2500
+
     def __init__(self, provider):
+        self._service_event_pattern = "provider"
         self._provider = provider
+        # discover and register all middleware
+        provider.middleware.add(self)
 
     @property
     def provider(self):
         return self._provider
 
+    @property
+    def events(self):
+        return self._provider.middleware.events
+
 
-class BaseComputeService(ComputeService, BaseCloudService):
+class BaseSecurityService(SecurityService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseComputeService, self).__init__(provider)
+        super(BaseSecurityService, self).__init__(provider)
 
 
-class BaseVolumeService(
-        BasePageableObjectMixin, VolumeService, BaseCloudService):
+class BaseKeyPairService(
+        BasePageableObjectMixin, KeyPairService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseVolumeService, self).__init__(provider)
+        super(BaseKeyPairService, self).__init__(provider)
+        self._service_event_pattern += ".security.key_pairs"
 
 
-class BaseSnapshotService(
-        BasePageableObjectMixin, SnapshotService, BaseCloudService):
+class BaseVMFirewallService(
+        BasePageableObjectMixin, VMFirewallService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseSnapshotService, self).__init__(provider)
+        super(BaseVMFirewallService, self).__init__(provider)
+        self._service_event_pattern += ".security.vm_firewalls"
+
+    @dispatch(event="provider.security.vm_firewalls.find",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
+
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
+
+
+class BaseVMFirewallRuleService(BasePageableObjectMixin,
+                                VMFirewallRuleService,
+                                BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseVMFirewallRuleService, self).__init__(provider)
+        self._provider = provider
+
+    @property
+    def provider(self):
+        return self._provider
+
+    @dispatch(event="provider.security.vm_firewall_rules.get",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def get(self, firewall, rule_id):
+        matches = [rule for rule in firewall.rules if rule.id == rule_id]
+        if matches:
+            return matches[0]
+        else:
+            return None
+
+    @dispatch(event="provider.security.vm_firewall_rules.find",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def find(self, firewall, **kwargs):
+        obj_list = firewall.rules
+        filters = ['name', 'direction', 'protocol', 'from_port', 'to_port',
+                   'cidr', 'src_dest_fw', 'src_dest_fw_id']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
 
 
 class BaseStorageService(StorageService, BaseCloudService):
@@ -65,11 +132,20 @@ class BaseStorageService(StorageService, BaseCloudService):
         super(BaseStorageService, self).__init__(provider)
 
 
-class BaseImageService(
-        BasePageableObjectMixin, ImageService, BaseCloudService):
+class BaseVolumeService(
+        BasePageableObjectMixin, VolumeService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseImageService, self).__init__(provider)
+        super(BaseVolumeService, self).__init__(provider)
+        self._service_event_pattern += ".storage.volumes"
+
+
+class BaseSnapshotService(
+        BasePageableObjectMixin, SnapshotService, BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseSnapshotService, self).__init__(provider)
+        self._service_event_pattern += ".storage.snapshots"
 
 
 class BaseBucketService(
@@ -77,44 +153,55 @@ class BaseBucketService(
 
     def __init__(self, provider):
         super(BaseBucketService, self).__init__(provider)
+        self._service_event_pattern += ".storage.buckets"
 
+    # Generic find will be used for providers where we have not implemented
+    # provider-specific querying for find method
+    @dispatch(event="provider.storage.buckets.find",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
 
-class BaseSecurityService(SecurityService, BaseCloudService):
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
+
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
+
+
+class BaseBucketObjectService(BucketObjectService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseSecurityService, self).__init__(provider)
+        super(BaseBucketObjectService, self).__init__(provider)
+        self._service_event_pattern += ".storage._bucket_objects"
+        self._bucket = None
 
 
-class BaseKeyPairService(
-        BasePageableObjectMixin, KeyPairService, BaseCloudService):
+class BaseComputeService(ComputeService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseKeyPairService, self).__init__(provider)
+        super(BaseComputeService, self).__init__(provider)
 
-    def delete(self, key_pair_id):
-        """
-        Delete an existing key pair.
 
-        :type key_pair_id: str
-        :param key_pair_id: The id of the key pair to be deleted.
+class BaseImageService(
+        BasePageableObjectMixin, ImageService, BaseCloudService):
 
-        :rtype: ``bool``
-        :return:  ``True`` if the key does not exist. Note that this implies
-                  that the key may not have been deleted by this method but
-                  instead has not existed in the first place.
-        """
-        log.info("Deleting the existing key pair %s", key_pair_id)
-        kp = self.get(key_pair_id)
-        if kp:
-            kp.delete()
-        return True
+    def __init__(self, provider):
+        super(BaseImageService, self).__init__(provider)
+        self._service_event_pattern += ".compute.images"
 
 
-class BaseVMFirewallService(
-        BasePageableObjectMixin, VMFirewallService, BaseCloudService):
+class BaseInstanceService(
+        BasePageableObjectMixin, InstanceService, BaseCloudService):
 
     def __init__(self, provider):
-        super(BaseVMFirewallService, self).__init__(provider)
+        super(BaseInstanceService, self).__init__(provider)
+        self._service_event_pattern += ".compute.instances"
 
 
 class BaseVMTypeService(
@@ -122,11 +209,16 @@ class BaseVMTypeService(
 
     def __init__(self, provider):
         super(BaseVMTypeService, self).__init__(provider)
+        self._service_event_pattern += ".compute.vm_types"
 
+    @dispatch(event="provider.compute.vm_types.get",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
     def get(self, vm_type_id):
         vm_type = (t for t in self if t.id == vm_type_id)
         return next(vm_type, None)
 
+    @dispatch(event="provider.compute.vm_types.find",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
     def find(self, **kwargs):
         obj_list = self
         filters = ['name']
@@ -134,19 +226,15 @@ class BaseVMTypeService(
         return ClientPagedResultList(self._provider, list(matches))
 
 
-class BaseInstanceService(
-        BasePageableObjectMixin, InstanceService, BaseCloudService):
-
-    def __init__(self, provider):
-        super(BaseInstanceService, self).__init__(provider)
-
-
 class BaseRegionService(
         BasePageableObjectMixin, RegionService, BaseCloudService):
 
     def __init__(self, provider):
         super(BaseRegionService, self).__init__(provider)
+        self._service_event_pattern += ".compute.regions"
 
+    @dispatch(event="provider.compute.regions.find",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
     def find(self, **kwargs):
         obj_list = self
         filters = ['name']
@@ -165,17 +253,40 @@ class BaseNetworkService(
 
     def __init__(self, provider):
         super(BaseNetworkService, self).__init__(provider)
+        self._service_event_pattern += ".networking.networks"
 
     @property
     def subnets(self):
         return [subnet for subnet in self.provider.subnets
                 if subnet.network_id == self.id]
 
-    def delete(self, network_id):
-        network = self.get(network_id)
-        if network:
-            log.info("Deleting network %s", network_id)
-            network.delete()
+    def get_or_create_default(self):
+        networks = self.provider.networking.networks.find(
+            label=BaseNetwork.CB_DEFAULT_NETWORK_LABEL)
+
+        if networks:
+            return networks[0]
+        else:
+            log.info("Creating a CloudBridge-default network labeled %s",
+                     BaseNetwork.CB_DEFAULT_NETWORK_LABEL)
+            return self.provider.networking.networks.create(
+                BaseNetwork.CB_DEFAULT_NETWORK_LABEL, '10.0.0.0/16')
+
+    @dispatch(event="provider.networking.networks.find",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs,
+                                                           ", ".join(filters)))
+
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
 
 
 class BaseSubnetService(
@@ -183,27 +294,76 @@ class BaseSubnetService(
 
     def __init__(self, provider):
         super(BaseSubnetService, self).__init__(provider)
+        self._service_event_pattern += ".networking.subnets"
 
-    def find(self, **kwargs):
-        obj_list = self
+    @dispatch(event="provider.networking.subnets.find",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def find(self, network=None, **kwargs):
+        if not network:
+            obj_list = self
+        else:
+            obj_list = network.subnets
         filters = ['label']
         matches = cb_helpers.generic_find(filters, kwargs, obj_list)
         return ClientPagedResultList(self._provider, list(matches))
 
+    def get_or_create_default(self, zone):
+        # Look for a CB-default subnet
+        matches = self.find(label=BaseSubnet.CB_DEFAULT_SUBNET_LABEL)
+        if matches:
+            return matches[0]
+
+        # No provider-default Subnet exists, try to create it (net + subnets)
+        network = self.provider.networking.networks.get_or_create_default()
+        subnet = self.create(BaseSubnet.CB_DEFAULT_SUBNET_LABEL, network,
+                             BaseSubnet.CB_DEFAULT_SUBNET_IPV4RANGE, zone)
+        return subnet
+
 
 class BaseRouterService(
         BasePageableObjectMixin, RouterService, BaseCloudService):
 
     def __init__(self, provider):
         super(BaseRouterService, self).__init__(provider)
-
-    def delete(self, router):
-        if isinstance(router, Router):
-            log.info("Router %s successful deleted.", router)
-            router.delete()
+        self._service_event_pattern += ".networking.routers"
+
+    def get_or_create_default(self, network):
+        net_id = network.id if isinstance(network, Network) else network
+        routers = self.provider.networking.routers.find(
+            label=BaseRouter.CB_DEFAULT_ROUTER_LABEL)
+        for router in routers:
+            if router.network_id == net_id:
+                return router
         else:
-            log.info("Getting router %s", router)
-            router = self.get(router)
-            if router:
-                log.info("Router %s successful deleted.", router)
-                router.delete()
+            return self.provider.networking.routers.create(
+                network=net_id, label=BaseRouter.CB_DEFAULT_ROUTER_LABEL)
+
+
+class BaseGatewayService(GatewayService, BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseGatewayService, self).__init__(provider)
+        self._provider = provider
+
+    @property
+    def provider(self):
+        return self._provider
+
+
+class BaseFloatingIPService(FloatingIPService, BaseCloudService):
+
+    def __init__(self, provider):
+        super(BaseFloatingIPService, self).__init__(provider)
+        self._provider = provider
+
+    @property
+    def provider(self):
+        return self._provider
+
+    @dispatch(event="provider.networking.floating_ips.find",
+              priority=BaseCloudService.STANDARD_EVENT_PRIORITY)
+    def find(self, gateway, **kwargs):
+        obj_list = gateway.floating_ips
+        filters = ['name', 'public_ip']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))

+ 168 - 0
cloudbridge/cloud/base/subservices.py

@@ -0,0 +1,168 @@
+import logging
+
+from cloudbridge.cloud.interfaces.subservices import BucketObjectSubService
+from cloudbridge.cloud.interfaces.subservices import FloatingIPSubService
+from cloudbridge.cloud.interfaces.subservices import GatewaySubService
+from cloudbridge.cloud.interfaces.subservices import SubnetSubService
+from cloudbridge.cloud.interfaces.subservices import VMFirewallRuleSubService
+
+from .resources import BasePageableObjectMixin
+
+log = logging.getLogger(__name__)
+
+
+class BaseBucketObjectSubService(BasePageableObjectMixin,
+                                 BucketObjectSubService):
+
+    def __init__(self, provider, bucket):
+        self.__provider = provider
+        self.bucket = bucket
+
+    @property
+    def _provider(self):
+        return self.__provider
+
+    def get(self, name):
+        return self._provider.storage._bucket_objects.get(self.bucket, name)
+
+    def list(self, limit=None, marker=None, prefix=None):
+        return self._provider.storage._bucket_objects.list(self.bucket, limit,
+                                                           marker, prefix)
+
+    def find(self, **kwargs):
+        return self._provider.storage._bucket_objects.find(self.bucket,
+                                                           **kwargs)
+
+    def create(self, name):
+        return self._provider.storage._bucket_objects.create(self.bucket, name)
+
+
+class BaseGatewaySubService(GatewaySubService, BasePageableObjectMixin):
+
+    def __init__(self, provider, network):
+        self._network = network
+        self.__provider = provider
+
+    @property
+    def _provider(self):
+        return self.__provider
+
+    def get_or_create(self):
+        return (self._provider.networking
+                              ._gateways
+                              .get_or_create(self._network))
+
+    def delete(self, gateway):
+        return (self._provider.networking
+                              ._gateways
+                              .delete(self._network, gateway))
+
+    def list(self, limit=None, marker=None):
+        return (self._provider.networking
+                              ._gateways
+                              .list(self._network, limit, marker))
+
+
+class BaseVMFirewallRuleSubService(BasePageableObjectMixin,
+                                   VMFirewallRuleSubService):
+
+    def __init__(self, provider, firewall):
+        self.__provider = provider
+        self._firewall = firewall
+
+    @property
+    def _provider(self):
+        return self.__provider
+
+    def get(self, rule_id):
+        return self._provider.security._vm_firewall_rules.get(self._firewall,
+                                                              rule_id)
+
+    def list(self, limit=None, marker=None):
+        return self._provider.security._vm_firewall_rules.list(self._firewall,
+                                                               limit, marker)
+
+    def create(self, direction, protocol=None, from_port=None,
+               to_port=None, cidr=None, src_dest_fw=None):
+        return (self._provider
+                    .security
+                    ._vm_firewall_rules
+                    .create(self._firewall, direction, protocol, from_port,
+                            to_port, cidr, src_dest_fw))
+
+    def find(self, **kwargs):
+        return self._provider.security._vm_firewall_rules.find(self._firewall,
+                                                               **kwargs)
+
+    def delete(self, rule_id):
+        return (self._provider
+                    .security
+                    ._vm_firewall_rules
+                    .delete(self._firewall, rule_id))
+
+
+class BaseFloatingIPSubService(FloatingIPSubService, BasePageableObjectMixin):
+
+    def __init__(self, provider, gateway):
+        self.__provider = provider
+        self.gateway = gateway
+
+    @property
+    def _provider(self):
+        return self.__provider
+
+    def get(self, fip_id):
+        return self._provider.networking._floating_ips.get(self.gateway,
+                                                           fip_id)
+
+    def list(self, limit=None, marker=None):
+        return self._provider.networking._floating_ips.list(self.gateway,
+                                                            limit, marker)
+
+    def find(self, **kwargs):
+        return self._provider.networking._floating_ips.find(self.gateway,
+                                                            **kwargs)
+
+    def create(self):
+        return self._provider.networking._floating_ips.create(self.gateway)
+
+    def delete(self, fip):
+        return self._provider.networking._floating_ips.delete(self.gateway,
+                                                              fip)
+
+
+class BaseSubnetSubService(SubnetSubService, BasePageableObjectMixin):
+
+    def __init__(self, provider, network):
+        self.__provider = provider
+        self.network = network
+
+    @property
+    def _provider(self):
+        return self.__provider
+
+    def get(self, subnet_id):
+        sn = self._provider.networking.subnets.get(self.network, subnet_id)
+        if sn.network_id != self.network.id:
+            log.warning("The SubnetSubService nested in the network '{}' "
+                        "returned subnet '{}' which is attached to another "
+                        "network '{}'".format(str(self.network), str(sn),
+                                              str(sn.network)))
+        return sn
+
+    def list(self, limit=None, marker=None):
+        return self._provider.networking.subnets.list(network=self.network,
+                                                      limit=limit,
+                                                      marker=marker)
+
+    def find(self, **kwargs):
+        return self._provider.networking.subnets.find(network=self.network,
+                                                      **kwargs)
+
+    def create(self, label, cidr_block, zone):
+        return self._provider.networking.subnets.create(label,
+                                                        self.network,
+                                                        cidr_block, zone)
+
+    def delete(self, subnet):
+        return self._provider.networking.subnets.delete(subnet)

+ 27 - 41
cloudbridge/cloud/factory.py

@@ -14,8 +14,10 @@ log = logging.getLogger(__name__)
 
 class ProviderList(object):
     AWS = 'aws'
-    OPENSTACK = 'openstack'
     AZURE = 'azure'
+    GCP = 'gcp'
+    OPENSTACK = 'openstack'
+    MOCK = 'mock'
 
 
 class CloudProviderFactory(object):
@@ -47,19 +49,11 @@ class CloudProviderFactory(object):
         if isinstance(cls, type) and issubclass(cls, CloudProvider):
             if hasattr(cls, "PROVIDER_ID"):
                 provider_id = getattr(cls, "PROVIDER_ID")
-                if issubclass(cls, TestMockHelperMixin):
-                    if self.provider_list.get(provider_id, {}).get(
-                            'mock_class'):
-                        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
-                else:
-                    if self.provider_list.get(provider_id, {}).get('class'):
-                        log.warning("Provider with id: %s is already "
-                                    "registered. Overriding with class: %s",
-                                    provider_id, cls)
-                    self.provider_list[provider_id]['class'] = cls
+                if self.provider_list.get(provider_id, {}).get('class'):
+                    log.warning("Provider with id: %s is already "
+                                "registered. Overriding with class: %s",
+                                provider_id, cls)
+                self.provider_list[provider_id]['class'] = cls
             else:
                 log.warning("Provider class: %s implements CloudProvider but"
                             " does not define PROVIDER_ID. Ignoring...", cls)
@@ -75,7 +69,10 @@ class CloudProviderFactory(object):
         """
         for _, modname, _ in pkgutil.iter_modules(providers.__path__):
             log.debug("Importing provider: %s", modname)
-            self._import_provider(modname)
+            try:
+                self._import_provider(modname)
+            except Exception as e:
+                log.warn("Could not import provider: %s", e)
 
     def _import_provider(self, module_name):
         """
@@ -101,8 +98,7 @@ class CloudProviderFactory(object):
         :rtype: dict
         :return: A dict of available providers and their implementations in the
                  following format::
-                 {'aws': {'class': aws.provider.AWSCloudProvider,
-                          'mock_class': aws.provider.MockAWSCloudProvider},
+                 {'aws': {'class': aws.provider.AWSCloudProvider},
                   'openstack': {'class': openstack.provider.OpenStackCloudProvi
                                          der}
                  }
@@ -123,10 +119,10 @@ class CloudProviderFactory(object):
         :param name: Cloud provider name: one of ``aws``, ``openstack``,
         ``azure``.
 
-        :type config: an object with required fields
-        :param config: This can be a Bunch or any other object whose fields can
-                       be accessed using dot notation. See specific provider
-                       implementation for the required fields.
+        :type config: :class:`dict`
+        :param config: A dictionary or an iterable of key/value pairs (as
+                       tuples or other iterables of length two). See specific
+                       provider implementation for the required fields.
 
         :return:  a concrete provider instance
         :rtype: ``object`` of :class:`.CloudProvider`
@@ -142,14 +138,10 @@ class CloudProviderFactory(object):
         log.debug("Created '%s' provider", name)
         return provider_class(config)
 
-    def get_provider_class(self, name, get_mock=False):
+    def get_provider_class(self, name):
         """
         Return a class for the requested provider.
 
-        :type get_mock: ``bool``
-        :param get_mock: If True, returns a mock version of the provider
-        if available, or the real version if not.
-
         :rtype: provider class or ``None``
         :return: A class corresponding to the requested provider or ``None``
                  if the provider was not found.
@@ -157,24 +149,19 @@ class CloudProviderFactory(object):
         log.debug("Returning a class for the %s provider", name)
         impl = self.list_providers().get(name)
         if impl:
-            if get_mock and impl.get("mock_class"):
-                log.debug("param get_mock set to True, returning "
-                          "a mock version of the provider %s", name)
-                return impl["mock_class"]
-            else:
-                log.debug("Returning the real version of %s", name)
-                return impl["class"]
+            log.debug("Returning provider class for %s", name)
+            return impl["class"]
         else:
             log.debug("Provider with the name: %s not found", name)
             return None
 
-    def get_all_provider_classes(self, get_mock=False):
+    def get_all_provider_classes(self, ignore_mocks=False):
         """
         Returns a list of classes for all available provider implementations
 
-        :type get_mock: ``bool``
-        :param get_mock: If True, returns a mock version of the provider
-        if available, or the real version if not.
+        :type ignore_mocks: ``bool``
+        :param ignore_mocks: If True, does not return mock providers. Mock
+        providers are providers which implement the TestMockHelperMixin.
 
         :rtype: type ``class`` or ``None``
         :return: A list of all available provider classes or an empty list
@@ -182,10 +169,9 @@ class CloudProviderFactory(object):
         """
         all_providers = []
         for impl in self.list_providers().values():
-            if get_mock and impl.get("mock_class"):
-                log.debug("param get_mock set to True, appending "
-                          "a mock version of the provider %s", impl)
-                all_providers.append(impl["mock_class"])
+            if ignore_mocks:
+                if not issubclass(impl["class"], TestMockHelperMixin):
+                    all_providers.append(impl["class"])
             else:
                 all_providers.append(impl["class"])
         log.info("List of provider classes: %s", all_providers)

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

@@ -92,3 +92,13 @@ class DuplicateResourceException(CloudBridgeBaseException):
     result in a DuplicateResourceException.
     """
     pass
+
+
+class InvalidParamException(InvalidNameException):
+    """
+    Marker interface for an invalid or unexpected parameter, for example,
+    to a service.find() method.
+    """
+
+    def __init__(self, msg):
+        super(InvalidParamException, self).__init__(msg)

+ 22 - 5
cloudbridge/cloud/interfaces/provider.py

@@ -1,7 +1,9 @@
 """
 Specification for a provider interface
 """
-from abc import ABCMeta, abstractmethod, abstractproperty
+from abc import ABCMeta
+from abc import abstractmethod
+from abc import abstractproperty
 
 
 class CloudProvider(object):
@@ -18,10 +20,10 @@ class CloudProvider(object):
 
         :type config: :class:`dict`
         :param config: A dictionary object containing provider initialization
-                       values. Alternatively, this can be a Bunch or any other
-                       object whose fields can be accessed as members. See
-                       specific provider implementation for the required
-                       fields.
+                       values. Alternatively, this can be an iterable of
+                       key/value pairs (as tuples or other iterables of length
+                       two). See specific provider implementation for the
+                       required fields.
 
         :rtype: :class:`.CloudProvider`
         :return:  a concrete provider instance
@@ -53,6 +55,21 @@ class CloudProvider(object):
                   used to initialize the provider, as well as other global
                   configuration properties.
         """
+        pass
+
+    @abstractproperty
+    def middleware(self):
+        """
+        Returns the middleware manager associated with this provider. The
+        middleware manager can be used to add or remove middleware from
+        cloudbridge. Refer to pyeventsystem documentation for more information
+        on how the middleware manager works.
+
+        :rtype: :class:`.MiddlewareManager`
+        :return:  An object of class MiddlewareManager, which can be used to
+        add or remove middleware from cloudbridge.
+        """
+        pass
 
     @abstractmethod
     def authenticate(self):

+ 56 - 375
cloudbridge/cloud/interfaces/resources.py

@@ -1,7 +1,9 @@
 """
 Specifications for data objects exposed through a ``provider`` or ``service``.
 """
-from abc import ABCMeta, abstractmethod, abstractproperty
+from abc import ABCMeta
+from abc import abstractmethod
+from abc import abstractproperty
 from enum import Enum
 
 
@@ -31,7 +33,7 @@ class CloudResource(object):
 
     This interface has a  _provider property that can be used to access the
     provider associated with the resource, which is only intended for use by
-    subclasses. Every cloudbridge resource also has an id, a name and a
+    subclasses. Every CloudBridge resource also has an id, a name and a
     label property. The id property is a unique identifier for the resource.
     The name is a more user-friendly version of an id, suitable for
     display to an end-user. However, it cannot be used in place of id. See
@@ -58,11 +60,11 @@ class CloudResource(object):
         Get the resource identifier.
 
         The id property is used to uniquely identify the resource, and is an
-        opaque value which should not be interpreted by cloudbridge clients,
+        opaque value which should not be interpreted by CloudBridge clients,
         and is a value meaningful to the underlying cloud provider.
 
-        :rtype: ``str`` :return: ID for this resource as returned by the cloud
-        middleware.
+        :rtype: ``str``
+        :return: ID for this resource as returned by the cloud middleware.
         """
         pass
 
@@ -72,30 +74,32 @@ class CloudResource(object):
         Get the name id for the resource.
 
         The name property is typically a user-friendly id value for the
-        resource. The name is different from the id property in the
-        following ways:
-        1. The name property is often a more user-friendly value to
-           display to the user than the id property.
-        2. The name may sometimes be the same as the id, but should never
-           be used in place of the id.
-        3. The id is what will uniquely identify a resource, and will be used
-           internally by cloudbridge for all get operations etc.
-        4. All resources have a name.
-        5. The name is read-only.
-        6. However, the name may not necessarily be unique, which is the
-           reason why it should not be used for uniquely identifying a
-           resource.
+        resource. The name is different from the id property in the following
+        ways:
+
+         1. The name property is often a more user-friendly value to
+            display to the user than the id property.
+         2. The name may sometimes be the same as the id, but should never
+            be used in place of the id.
+         3. The id is what will uniquely identify a resource, and will be used
+            internally by CloudBridge for all get operations etc.
+         4. All resources have a name.
+         5. The name is read-only.
+         6. However, the name may not necessarily be unique, which is the
+            reason why it should not be used for uniquely identifying a
+            resource.
+
         Example:
-        The AWS machine image name maps to a cloudbridge name. It is not
-        editable and is a user friendly name such as 'Ubuntu 14.04' and
+        The AWS machine image name maps to a CloudBridge name. It is not
+        editable and is a user friendly name such as 'Ubuntu 18.04' and
         corresponds to the ami-name. It is distinct from the ami-id, which
-        maps to cloudbridge's id property. The ami-name cannot be edited, and
+        maps to CloudBridge's id property. The ami-name cannot be edited, and
         is set at creation time. It is not necessarily unique.
-        In Azure, the machine image's name corresponds to cloudbridge's name
+        In Azure, the machine image's name corresponds to CloudBridge's name
         property. In Azure, it also happens to be the same as the id property.
 
         The name property and the label property share the same character
-        restrictions. see :py:attr:`~LabeledCloudResource.label`
+        restrictions. See :py:attr:`~LabeledCloudResource.label`.
         """
         pass
 
@@ -119,6 +123,7 @@ class LabeledCloudResource(CloudResource):
         in the underlying cloud provider, or be simulated through tags/labels.
 
         The label property adheres to the following restrictions:
+
         * Must be at least 3 characters in length.
         * Cannot be longer than 63 characters.
         * May only contain ASCII characters comprising of lowercase letters,
@@ -127,18 +132,19 @@ class LabeledCloudResource(CloudResource):
           (i.e. cannot begin or end with a dash)
 
         Some resources may not support labels, in which case, a
-        NotImplementedError will be thrown.
+        ``NotImplementedError`` will be thrown.
 
         :rtype: ``str``
         :return: Label for this resource as returned by the cloud middleware.
-        :throws NotImplementedError if this resource does not support labels
+        :raise: ``NotImplementedError`` if this resource does not support
+                labels.
         """
         pass
 
 
 class Configuration(dict):
     """
-    Represents a cloudbridge configuration object
+    Represents a CloudBridge configuration object
     """
 
     @abstractproperty
@@ -460,7 +466,7 @@ class ResultList(list):
         """
         Indicate whether this ``ResultList`` supports server side paging.
 
-        If server side paging is not supported, the result will useclient side
+        If server side paging is not supported, the result will use client side
         paging and the data property provides direct access to all available
         data.
         """
@@ -505,6 +511,9 @@ class Instance(ObjectLifeCycleMixin, LabeledCloudResource):
     def label(self, value):
         """
         Set the instance label.
+
+        :type value: ``str``
+        :param value: The value to set the label to.
         """
         pass
 
@@ -893,6 +902,9 @@ class Network(ObjectLifeCycleMixin, LabeledCloudResource):
     def label(self, value):
         """
         Set the resource label.
+
+        :type value: ``str``
+        :param value: The value to set the label to.
         """
         pass
 
@@ -949,36 +961,13 @@ class Network(ObjectLifeCycleMixin, LabeledCloudResource):
         """
         pass
 
-    @abstractmethod
-    def create_subnet(self, label, cidr_block, zone=None):
-        """
-        Create a new network subnet and associate it with this Network.
-
-        :type label: ``str``
-        :param label: The subnet label. The subnet name will be derived from
-                      this label.
-
-        :type cidr_block: ``str``
-        :param cidr_block: CIDR block within this Network to assign to the
-                           subnet.
-
-        :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`
-        :return:  A Subnet object
-        """
-        pass
-
     @abstractproperty
     def gateways(self):
         """
         Provides access to the internet gateways attached to this network.
 
-        :rtype: :class:`.GatewayContainer`
-        :return: A GatewayContainer object
+        :rtype: :class:`.GatewaySubService`
+        :return: A GatewaySubService object
         """
         pass
 
@@ -1011,6 +1000,9 @@ class Subnet(ObjectLifeCycleMixin, LabeledCloudResource):
     def label(self, value):
         """
         Set the resource label.
+
+        :type value: ``str``
+        :param value: The value to set the label to.
         """
         pass
 
@@ -1067,76 +1059,6 @@ class Subnet(ObjectLifeCycleMixin, LabeledCloudResource):
         pass
 
 
-class FloatingIPContainer(PageableObjectMixin):
-    """
-    Base interface for a FloatingIP Service.
-    """
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def get(self, fip_id):
-        """
-        Returns a FloatingIP given its ID or ``None`` if not found.
-
-        :type fip_id: ``str``
-        :param fip_id: The ID of the FloatingIP to retrieve.
-
-        :rtype: ``object`` of :class:`.FloatingIP`
-        :return: a FloatingIP object
-        """
-        pass
-
-    @abstractmethod
-    def list(self, limit=None, marker=None):
-        """
-        List floating (i.e., static) IP addresses.
-
-        :rtype: ``list`` of :class:`.FloatingIP`
-        :return: list of FloatingIP objects
-        """
-        pass
-
-    @abstractmethod
-    def find(self, **kwargs):
-        """
-        Searches for a FloatingIP by a given list of attributes.
-
-        Supported attributes: label, public_ip
-
-        Example:
-
-        .. code-block:: python
-
-            fip = provider.networking.gateways.get('id').floating_ips.find(
-                        public_ip='public_ip')
-
-
-        :rtype: List of ``object`` of :class:`.FloatingIP`
-        :return: A list of FloatingIP objects matching the supplied attributes.
-        """
-        pass
-
-    @abstractmethod
-    def create(self):
-        """
-        Allocate a new floating (i.e., static) IP address.
-
-        :rtype: ``object`` of :class:`.FloatingIP`
-        :return:  A FloatingIP object
-        """
-        pass
-
-    @abstractmethod
-    def delete(self, fip_id):
-        """
-        Delete an existing FloatingIP.
-
-        :type fip_id: ``str``
-        :param fip_id: The ID of the FloatingIP to be deleted.
-        """
-        pass
-
-
 class FloatingIpState(object):
 
     """
@@ -1230,6 +1152,9 @@ class Router(LabeledCloudResource):
     def label(self, value):
         """
         Set the resource label.
+
+        :type value: ``str``
+        :param value: The value to set the label to.
         """
         pass
 
@@ -1339,49 +1264,6 @@ class GatewayState(object):
     ERROR = "error"
 
 
-class GatewayContainer(PageableObjectMixin):
-    """
-    Manage internet gateway resources.
-    """
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def get_or_create_inet_gateway(self):
-        """
-        Creates new or returns an existing internet gateway for a network.
-
-        The returned gateway object can subsequently be attached to a router to
-        provide internet routing to a network.
-
-        :type  name: ``str``
-        :param name: The gateway label.
-
-        :rtype: ``object``  of :class:`.InternetGateway` or ``None``
-        :return: an InternetGateway object of ``None`` if not found.
-        """
-        pass
-
-    @abstractmethod
-    def delete(self, gateway):
-        """
-        Delete a gateway.
-
-        :type gateway: :class:`.Gateway` object
-        :param gateway: Gateway object to delete.
-        """
-        pass
-
-    @abstractmethod
-    def list(self, limit=None, marker=None):
-        """
-        List all available internet gateways.
-
-        :rtype: ``list`` of :class:`.InternetGateway` or ``None``
-        :return: Current list of internet gateways.
-        """
-        pass
-
-
 class Gateway(CloudResource):
     """
     Represents a gateway resource.
@@ -1411,8 +1293,8 @@ class Gateway(CloudResource):
         """
         Provides access to floating IPs connected to this internet gateway.
 
-        :rtype: :class:`.FloatingIPContainer`
-        :return: A FloatingIPContainer object
+        :rtype: :class:`.FloatingIPSubService`
+        :return: A FloatingIPSubService object
         """
         pass
 
@@ -2004,146 +1886,13 @@ class VMFirewall(LabeledCloudResource):
     @abstractproperty
     def rules(self):
         """
-        Get a container for the rules belonging to this VM firewall.
+        Get access to the rules belonging to this VM firewall.
 
         This object can be used for further operations on rules, such as get,
         list, create, etc.
 
-        :rtype: An object of :class:`.VMFirewallRuleContainer`
-        :return: A VMFirewallRuleContainer for further operations
-        """
-        pass
-
-
-class VMFirewallRuleContainer(PageableObjectMixin):
-    """
-    Base interface for Firewall rules.
-    """
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def get(self, rule_id):
-        """
-        Return a firewall rule given its ID.
-
-        Returns ``None`` if the rule does not exist.
-
-        Example:
-
-        .. code-block:: python
-
-            fw = provider.security.vm_firewalls.get('my_fw_id')
-            rule = fw.rules.get('rule_id')
-            print(rule.id, rule.label)
-
-        :rtype: :class:`.FirewallRule`
-        :return:  a FirewallRule instance
-        """
-        pass
-
-    @abstractmethod
-    def list(self, limit=None, marker=None):
-        """
-        List all firewall rules associated with this firewall.
-
-        :rtype: ``list`` of :class:`.FirewallRule`
-        :return:  list of Firewall rule objects
-        """
-        pass
-
-    @abstractmethod
-    def create(self,  direction, protocol=None, from_port=None,
-               to_port=None, cidr=None, src_dest_fw=None):
-        """
-        Create a VM firewall rule.
-
-        If a matching rule already exists, return it.
-
-        Example:
-
-        .. code-block:: python
-            from cloudbridge.cloud.interfaces.resources import TrafficDirection
-
-            fw = provider.security.vm_firewalls.get('my_fw_id')
-            fw.rules.create(TrafficDirection.INBOUND, protocol='tcp',
-                            from_port=80, to_port=80, cidr='10.0.0.0/16')
-            fw.rules.create(TrafficDirection.INBOUND, src_dest_fw=fw)
-            fw.rules.create(TrafficDirection.OUTBOUND, src_dest_fw=fw)
-
-        You need to pass in either ``src_dest_fw`` OR ``protocol`` AND
-        ``from_port``, ``to_port``, ``cidr``. In other words, either
-        you are authorizing another group or you are authorizing some
-        IP-based rule.
-
-        :type direction: :class:``.TrafficDirection``
-        :param direction: Either ``TrafficDirection.INBOUND`` |
-                          ``TrafficDirection.OUTBOUND``
-
-        :type protocol: ``str``
-        :param protocol: Either ``tcp`` | ``udp`` | ``icmp``.
-
-        :type from_port: ``int``
-        :param from_port: The beginning port number you are enabling.
-
-        :type to_port: ``int``
-        :param to_port: The ending port number you are enabling.
-
-        :type cidr: ``str`` or list of ``str``
-        :param cidr: The CIDR block you are providing access to.
-
-        :type src_dest_fw: :class:`.VMFirewall`
-        :param src_dest_fw: The VM firewall object which is the
-                            source/destination of the traffic, depending on
-                            whether it's ingress/egress traffic.
-
-        :rtype: :class:`.VMFirewallRule`
-        :return: Rule object if successful or ``None``.
-        """
-        pass
-
-    @abstractmethod
-    def find(self, **kwargs):
-        """
-        Find a firewall rule filtered by the given parameters.
-
-        :type label: str
-        :param label: The label of the VM firewall to retrieve.
-
-        :type protocol: ``str``
-        :param protocol: Either ``tcp`` | ``udp`` | ``icmp``.
-
-        :type from_port: ``int``
-        :param from_port: The beginning port number you are enabling.
-
-        :type to_port: ``int``
-        :param to_port: The ending port number you are enabling.
-
-        :type cidr: ``str`` or list of ``str``
-        :param cidr: The CIDR block you are providing access to.
-
-        :type src_dest_fw: :class:`.VMFirewall`
-        :param src_dest_fw: The VM firewall object which is the
-                            source/destination of the traffic, depending on
-                            whether it's ingress/egress traffic.
-
-        :type src_dest_fw_id: :class:`.str`
-        :param src_dest_fw_id: The VM firewall id which is the
-                               source/destination of the traffic, depending on
-                               whether it's ingress/egress traffic.
-
-        :rtype: list of :class:`VMFirewallRule`
-        :return: A list of VMFirewall objects or an empty list if none
-                 found.
-        """
-        pass
-
-    @abstractmethod
-    def delete(self, rule_id):
-        """
-        Delete an existing VMFirewall rule.
-
-        :type rule_id: str
-        :param rule_id: The VM firewall rule to be deleted.
+        :rtype: An object of :class:`.VMFirewallRuleSubService`
+        :return: A VMFirewallRuleSubService for further operations
         """
         pass
 
@@ -2334,11 +2083,11 @@ class BucketObject(CloudResource):
     @abstractmethod
     def generate_url(self, expires_in):
         """
-        Generate a URL to this object.
+        Generate a signed 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.
+        A signed URL associated with an object gives time-limited read access
+        to that specific object. Anyone in possession of the URL has the access
+        granted by the URL.
 
         :type expires_in: ``int``
         :param expires_in: Time to live of the generated URL in seconds.
@@ -2415,71 +2164,3 @@ class Bucket(CloudResource):
         :return: ``True`` if successful.
         """
         pass
-
-
-class BucketContainer(PageableObjectMixin):
-    """
-    A container service for objects within a bucket.
-    """
-    __metaclass__ = ABCMeta
-
-    @abstractmethod
-    def get(self, name):
-        """
-        Retrieve a given object from this bucket.
-
-        :type name: ``str``
-        :param name: The identifier of the object to retrieve
-
-        :rtype: :class:``.BucketObject``
-        :return: The BucketObject or ``None`` if it cannot be found.
-        """
-        pass
-
-    @abstractmethod
-    # pylint:disable=arguments-differ
-    def list(self, limit=None, marker=None, prefix=None):
-        """
-        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: List of ``objects`` of :class:``.BucketObject``
-        :return: List of all available BucketObjects within this bucket.
-        """
-        pass
-
-    @abstractmethod
-    def find(self, **kwargs):
-        """
-        Search for an object by a given list of attributes.
-
-        Supported attributes: ``name``
-
-        :rtype: List of ``objects`` of :class:`.BucketObject`
-        :return: A list of BucketObjects matching the supplied attributes.
-
-        :type limit: ``int``
-        :param limit: Maximum number of elements to return.
-
-        :type marker: ``int``
-        :param marker: Fetch results after this offset.
-        """
-        pass
-
-    @abstractmethod
-    def create(self, name):
-        """
-        Create a new object within this bucket.
-
-        :rtype: :class:``.BucketObject``
-        :return: The newly created bucket object
-        """
-        pass

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

@@ -1,7 +1,9 @@
 """
 Specifications for services available through a provider
 """
-from abc import ABCMeta, abstractmethod, abstractproperty
+from abc import ABCMeta
+from abc import abstractmethod
+from abc import abstractproperty
 
 from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
 
@@ -360,6 +362,15 @@ class VolumeService(PageableObjectMixin, CloudService):
         """
         pass
 
+    def delete(self, volume):
+        """
+        Delete an existing volume.
+
+        :type volume: ``str`` or :class:`Volume`
+        :param volume: The object or ID of the volume to be deleted.
+        """
+        pass
+
 
 class SnapshotService(PageableObjectMixin, CloudService):
     """
@@ -368,7 +379,7 @@ class SnapshotService(PageableObjectMixin, CloudService):
     __metaclass__ = ABCMeta
 
     @abstractmethod
-    def get(self, volume_id):
+    def get(self, snapshot_id):
         """
         Returns a snapshot given its id.
 
@@ -420,6 +431,15 @@ class SnapshotService(PageableObjectMixin, CloudService):
         """
         pass
 
+    def delete(self, snapshot):
+        """
+        Delete an existing snapshot.
+
+        :type snapshot: ``str`` or :class:`Snapshot`
+        :param snapshot: The object or ID of the snapshot to be deleted.
+        """
+        pass
+
 
 class StorageService(CloudService):
 
@@ -584,6 +604,28 @@ class NetworkingService(CloudService):
         """
         pass
 
+    @abstractproperty
+    def _floating_ips(self):
+        """
+        Provides access to floating ips for this provider.
+        This service is not iterable.
+
+        :rtype: :class:`.FloatingIPService`
+        :return: a FloatingIPService object
+        """
+        pass
+
+    @abstractproperty
+    def _gateways(self):
+        """
+        Provides access to internet gateways for this provider.
+        This service is not iterable.
+
+        :rtype: :class:`.GatewayService`
+        :return: a GatewayService object
+        """
+        pass
+
 
 class NetworkService(PageableObjectMixin, CloudService):
 
@@ -643,7 +685,7 @@ class NetworkService(PageableObjectMixin, CloudService):
                            subnets you create fall within this initially
                            specified range. Note that the block size should be
                            between a /16 netmask (65,536 IP addresses) and /28
-                           netmask (16 IP addresses). e.g. 10.0.0.0/16
+                           netmask (16 IP addresses), e.g. 10.0.0.0/16.
 
         :rtype: ``object`` of :class:`.Network`
         :return:  A Network object
@@ -651,12 +693,12 @@ class NetworkService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def delete(self, network_id):
+    def delete(self, network):
         """
         Delete an existing Network.
 
-        :type network_id: ``str``
-        :param network_id: The ID of the network to be deleted.
+        :type network: ``str`` or :class:`.Network`
+        :param network: The object or id of the network to be deleted.
         """
         pass
 
@@ -699,7 +741,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         :param subnet_id: The ID of the subnet to retrieve.
 
         :rtype: ``object`` of :class:`.Subnet`
-        return: a Subnet object
+        :return: a Subnet object
         """
         pass
 
@@ -730,7 +772,7 @@ class SubnetService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, label, network_id, cidr_block, zone):
+    def create(self, label, network, cidr_block, zone):
         """
         Create a new subnet within the supplied network.
 
@@ -945,6 +987,110 @@ class BucketService(PageableObjectMixin, CloudService):
         pass
 
 
+class BucketObjectService(CloudService):
+
+    """
+    The Bucket Object Service interface provides access to the underlying
+    object storage capabilities of this provider. This service is optional and
+    the :func:`CloudProvider.has_service()` method should be used to verify its
+    availability before using the service.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, bucket, object_id):
+        """
+        Returns a bucket object given its ID and the ID of bucket containing
+        it. Returns ``None`` if the bucket object or bucket does not exist.
+        On some providers, such as AWS and OpenStack, the bucket id is the
+        same as its name.
+
+        Example:
+
+        .. code-block:: python
+
+            bucket = provider.storage.buckets.get('my_bucket_id')
+            # pylint:disable=protected-access
+            buck_obj = provider.storage._bucket_objects.get('my_object_id',
+                                                           bucket)
+            print(buck_obj.id, buck_obj.name)
+
+        :rtype: :class:`.BucketObject`
+        :return:  a BucketObject instance
+        """
+        pass
+
+    @abstractmethod
+    def find(self, bucket, **kwargs):
+        """
+        Searches for a bucket object in a bucket by a given list of attributes.
+
+        Supported attributes: name
+
+        Example:
+
+        .. code-block:: python
+
+            bucket = provider.storage.buckets.get('my_bucket_id')
+            # pylint:disable=protected-access
+            objs = provider.storage._bucket_objects.find(bucket,
+                                                        name='my_obj_name')
+            for buck_obj in objs:
+                print(buck_obj.id, buck_obj.name)
+
+        :rtype: :class:`.BucketObject`
+        :return:  a BucketObject instance
+        """
+        pass
+
+    @abstractmethod
+    def list(self, bucket, limit=None, marker=None):
+        """
+        List all bucket objects within a bucket.
+
+        Example:
+
+        .. code-block:: python
+
+            bucket = provider.storage.buckets.get('my_bucket_id')
+            # pylint:disable=protected-access
+            objs = provider.storage._bucket_objects.list(bucket)
+            for buck_obj in objs:
+                print(buck_obj.id, buck_obj.name)
+
+        :rtype: :class:`.BucketObject`
+        :return:  a BucketObject instance
+        """
+        pass
+
+    @abstractmethod
+    def create(self, bucket, object_name):
+        """
+        Create a new bucket object within a bucket.
+
+        Example:
+
+        .. code-block:: python
+
+            bucket = provider.storage.buckets.get('my_bucket_id')
+            # pylint:disable=protected-access
+            buck_obj = provider.storage._bucket_objects.create('my_name',
+                                                              bucket)
+            print(buck_obj.name)
+
+
+        :type object_name: str
+        :param object_name: The name of this bucket.
+
+        :type bucket: str
+        :param bucket: A bucket object.
+
+        :return:  a BucketObject instance
+        :rtype: ``object`` of :class:`.BucketObject`
+        """
+        pass
+
+
 class SecurityService(CloudService):
 
     """
@@ -997,6 +1143,17 @@ class SecurityService(CloudService):
         """
         pass
 
+    @abstractproperty
+    def _vm_firewall_rules(self):
+        """
+        Provides access to firewall (security group) rules for this provider.
+        This service is not iterable.
+
+        :rtype: :class:`.VMFirewallRuleService`
+        :return: a VMFirewallRuleService object
+        """
+        pass
+
 
 class KeyPairService(PageableObjectMixin, CloudService):
 
@@ -1068,12 +1225,12 @@ class KeyPairService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def delete(self, key_pair_id):
+    def delete(self, key_pair):
         """
-        Delete an existing VMFirewall.
+        Delete an existing keypair.
 
-        :type key_pair_id: str
-        :param key_pair_id: The id of the key pair to be deleted.
+        :type key_pair: ``str`` or :class:`.KeyPair`
+        :param key_pair: The object or id of the key pair to be deleted.
 
         :rtype: ``bool``
         :return:  ``True`` if the key does not exist, ``False`` otherwise. Note
@@ -1119,15 +1276,15 @@ class VMFirewallService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def create(self, label, network_id, description=None):
+    def create(self, label, network, description=None):
         """
         Create a new VMFirewall.
 
         :type label: str
         :param label: The label for the new VM firewall.
 
-        :type  network_id: ``str``
-        :param network_id: Network ID under which to create the VM firewall.
+        :type  network: ``str``
+        :param network: Network ID under which to create the VM firewall.
 
         :type description: str
         :param description: The description of the new VM firewall.
@@ -1154,12 +1311,165 @@ class VMFirewallService(PageableObjectMixin, CloudService):
         pass
 
     @abstractmethod
-    def delete(self, group_id):
+    def delete(self, vm_firewall):
         """
         Delete an existing VMFirewall.
 
-        :type group_id: str
-        :param group_id: The VM firewall ID to be deleted.
+        :type vm_firewall: ``str`` or :class:`.VMFirewall`
+        :param vm_firewall: The object or VM firewall ID to be deleted.
+        """
+        pass
+
+
+class VMFirewallRuleService(CloudService):
+    """
+    Base interface for Firewall rules.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, firewall, rule_id):
+        """
+        Return a firewall rule given its ID.
+
+        Returns ``None`` if the rule does not exist.
+
+        Example:
+
+        .. code-block:: python
+
+            fw = provider.security.vm_firewalls.get('my_fw_id')
+            rule = fw.rules.get('rule_id')
+            print(rule.id, rule.label)
+
+        :type firewall: ``VMFirewall``
+        :param firewall: The firewall to which the rule is attached
+
+        :type rule_id: str
+        :param rule_id: The ID of the desired firewall rule
+
+        :rtype: :class:`.FirewallRule`
+        :return:  a FirewallRule instance
+        """
+        pass
+
+    @abstractmethod
+    def list(self, firewall, limit=None, marker=None):
+        """
+        List all firewall rules associated with this firewall.
+
+        :type firewall: ``VMFirewall``
+        :param firewall: The firewall to which the rules are attached
+
+        :rtype: ``list`` of :class:`.FirewallRule`
+        :return:  list of Firewall rule objects
+        """
+        pass
+
+    @abstractmethod
+    def create(self, firewall,  direction, protocol=None, from_port=None,
+               to_port=None, cidr=None, src_dest_fw=None):
+        """
+        Create a VM firewall rule.
+
+        If a matching rule already exists, return it.
+
+        Example:
+
+        .. code-block:: python
+            from cloudbridge.cloud.interfaces.resources import TrafficDirection
+            from cloudbridge.cloud.interfaces.resources import BaseNetwork
+
+            fw = provider.security.vm_firewalls.get('my_fw_id')
+            fw.rules.create(TrafficDirection.INBOUND, protocol='tcp',
+                            from_port=80, to_port=80,
+                            cidr=BaseNetwork.CB_DEFAULT_IPV4RANGE)
+            fw.rules.create(TrafficDirection.INBOUND, src_dest_fw=fw)
+            fw.rules.create(TrafficDirection.OUTBOUND, src_dest_fw=fw)
+
+        You need to pass in either ``src_dest_fw`` OR ``protocol`` AND
+        ``from_port``, ``to_port``, ``cidr``. In other words, either
+        you are authorizing another group or you are authorizing some
+        IP-based rule.
+
+        :type firewall: ``VMFirewall``
+        :param firewall: The firewall to which the rule should be attached
+
+        :type direction: :class:``.TrafficDirection``
+        :param direction: Either ``TrafficDirection.INBOUND`` |
+                          ``TrafficDirection.OUTBOUND``
+
+        :type protocol: ``str``
+        :param protocol: Either ``tcp`` | ``udp`` | ``icmp``.
+
+        :type from_port: ``int``
+        :param from_port: The beginning port number you are enabling.
+
+        :type to_port: ``int``
+        :param to_port: The ending port number you are enabling.
+
+        :type cidr: ``str`` or list of ``str``
+        :param cidr: The CIDR block you are providing access to.
+
+        :type src_dest_fw: :class:`.VMFirewall`
+        :param src_dest_fw: The VM firewall object which is the
+                            source/destination of the traffic, depending on
+                            whether it's ingress/egress traffic.
+
+        :rtype: :class:`.VMFirewallRule`
+        :return: Rule object if successful or ``None``.
+        """
+        pass
+
+    @abstractmethod
+    def find(self, firewall, **kwargs):
+        """
+        Find a firewall rule filtered by the given parameters.
+
+        :type firewall: ``VMFirewall``
+        :param firewall: The firewall in which to look for rules
+
+        :type label: str
+        :param label: The label of the VM firewall to retrieve.
+
+        :type protocol: ``str``
+        :param protocol: Either ``tcp`` | ``udp`` | ``icmp``.
+
+        :type from_port: ``int``
+        :param from_port: The beginning port number you are enabling.
+
+        :type to_port: ``int``
+        :param to_port: The ending port number you are enabling.
+
+        :type cidr: ``str`` or list of ``str``
+        :param cidr: The CIDR block you are providing access to.
+
+        :type src_dest_fw: :class:`.VMFirewall`
+        :param src_dest_fw: The VM firewall object which is the
+                            source/destination of the traffic, depending on
+                            whether it's ingress/egress traffic.
+
+        :type src_dest_fw_id: :class:`.str`
+        :param src_dest_fw_id: The VM firewall id which is the
+                               source/destination of the traffic, depending on
+                               whether it's ingress/egress traffic.
+
+        :rtype: list of :class:`VMFirewallRule`
+        :return: A list of VMFirewall objects or an empty list if none
+                 found.
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, firewall, rule_id):
+        """
+        Delete an existing VMFirewall rule.
+
+        :type firewall: ``VMFirewall``
+        :param firewall: The firewall to which the rule is attached
+
+        :type rule_id: str
+        :param rule_id: The VM firewall rule to be deleted.
         """
         pass
 
@@ -1198,7 +1508,7 @@ class VMTypeService(PageableObjectMixin, CloudService):
     @abstractmethod
     def find(self, **kwargs):
         """
-        Searches for an instance by a given list of attributes.
+        Searches for instances by a given list of attributes.
 
         Supported attributes: name
 
@@ -1259,3 +1569,136 @@ class RegionService(PageableObjectMixin, CloudService):
         :return: a Region object
         """
         pass
+
+
+class GatewayService(CloudService):
+    """
+    Manage internet gateway resources.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get_or_create(self, network):
+        """
+        Creates new or returns an existing internet gateway for a network.
+
+        The returned gateway object can subsequently be attached to a router to
+        provide internet routing to a network.
+
+        :type  network: ``Network``
+        :param network: The network to which the gateway should be attached.
+
+        :rtype: ``object``  of :class:`.InternetGateway` or ``None``
+        :return: an InternetGateway object of ``None`` if not found.
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, network, gateway):
+        """
+        Delete a gateway.
+
+        :type  network: ``Network``
+        :param network: The network to which the gateway is attached.
+
+        :type gateway: :class:`.Gateway` object
+        :param gateway: Gateway object to delete.
+        """
+        pass
+
+    @abstractmethod
+    def list(self, network, limit=None, marker=None):
+        """
+        List all available internet gateways.
+
+        :type  network: ``Network``
+        :param network: The network to which the gateway is attached.
+
+        :rtype: ``list`` of :class:`.InternetGateway` or ``None``
+        :return: Current list of internet gateways.
+        """
+        pass
+
+
+class FloatingIPService(CloudService):
+    """
+    Base interface for a FloatingIP Service.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, gateway, fip_id):
+        """
+        Returns a FloatingIP given its ID or ``None`` if not found.
+
+        :type gateway: ``Gateway``
+        :param gateway: The gateway to which the Floating IP is attached
+
+        :type fip_id: ``str``
+        :param fip_id: The ID of the FloatingIP to retrieve.
+
+        :rtype: ``object`` of :class:`.FloatingIP`
+        :return: a FloatingIP object
+        """
+        pass
+
+    @abstractmethod
+    def list(self, gateway, limit=None, marker=None):
+        """
+        List floating (i.e., static) IP addresses.
+
+        :type gateway: ``Gateway``
+        :param gateway: The gateway to which the Floating IPs are attached
+
+        :rtype: ``list`` of :class:`.FloatingIP`
+        :return: list of FloatingIP objects
+        """
+        pass
+
+    @abstractmethod
+    def find(self, gateway, **kwargs):
+        """
+        Searches for a FloatingIP by a given list of attributes.
+
+        Supported attributes: name, public_ip
+
+        Example:
+
+        .. code-block:: python
+
+            fip = provider.networking.gateways.get('id').floating_ips.find(
+                        public_ip='public_ip')
+
+        :type gateway: ``Gateway``
+        :param gateway: The gateway to which the Floating IPs are attached
+
+        :rtype: List of ``object`` of :class:`.FloatingIP`
+        :return: A list of FloatingIP objects matching the supplied attributes.
+        """
+        pass
+
+    @abstractmethod
+    def create(self, gateway):
+        """
+        Allocate a new floating (i.e., static) IP address.
+
+        :type gateway: ``Gateway``
+        :param gateway: The gateway to which the Floating IP should be attached
+
+        :rtype: ``object`` of :class:`.FloatingIP`
+        :return:  A FloatingIP object
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, gateway, fip):
+        """
+        Delete an existing FloatingIP.
+
+        :type gateway: ``Gateway``
+        :param gateway: The gateway to which the Floating IP is attached
+
+        :type fip: ``str``
+        :param fip: The FloatingIP to be deleted.
+        """
+        pass

+ 401 - 0
cloudbridge/cloud/interfaces/subservices.py

@@ -0,0 +1,401 @@
+from abc import ABCMeta
+from abc import abstractmethod
+
+from cloudbridge.cloud.interfaces.resources import PageableObjectMixin
+
+
+class BucketObjectSubService(PageableObjectMixin):
+    """
+    A container service for objects within a bucket.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, name):
+        """
+        Retrieve a given object from this bucket.
+
+        :type name: ``str``
+        :param name: The identifier of the object to retrieve
+
+        :rtype: :class:``.BucketObject``
+        :return: The BucketObject or ``None`` if it cannot be found.
+        """
+        pass
+
+    @abstractmethod
+    # pylint:disable=arguments-differ
+    def list(self, limit=None, marker=None, prefix=None):
+        """
+        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: List of ``objects`` of :class:``.BucketObject``
+        :return: List of all available BucketObjects within this bucket.
+        """
+        pass
+
+    @abstractmethod
+    def find(self, **kwargs):
+        """
+        Search for an object by a given list of attributes.
+
+        Supported attributes: ``name``
+
+        :rtype: List of ``objects`` of :class:`.BucketObject`
+        :return: A list of BucketObjects matching the supplied attributes.
+
+        :type limit: ``int``
+        :param limit: Maximum number of elements to return.
+
+        :type marker: ``int``
+        :param marker: Fetch results after this offset.
+        """
+        pass
+
+    @abstractmethod
+    def create(self, name):
+        """
+        Create a new object within this bucket.
+
+        :rtype: :class:``.BucketObject``
+        :return: The newly created bucket object
+        """
+        pass
+
+
+class GatewaySubService(PageableObjectMixin):
+    """
+    Manage internet gateway resources.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get_or_create(self):
+        """
+        Creates new or returns an existing internet gateway for a network.
+
+        The returned gateway object can subsequently be attached to a router to
+        provide internet routing to a network.
+
+        :rtype: ``object``  of :class:`.InternetGateway` or ``None``
+        :return: an InternetGateway object of ``None`` if not found.
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, gateway):
+        """
+        Delete a gateway.
+
+        :type gateway: :class:`.Gateway` object
+        :param gateway: Gateway object to delete.
+        """
+        pass
+
+    @abstractmethod
+    def list(self, limit=None, marker=None):
+        """
+        List all available internet gateways.
+
+        :rtype: ``list`` of :class:`.InternetGateway` or ``None``
+        :return: Current list of internet gateways.
+        """
+        pass
+
+
+class FloatingIPSubService(PageableObjectMixin):
+    """
+    Base interface for a FloatingIP Service.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, fip_id):
+        """
+        Returns a FloatingIP given its ID or ``None`` if not found.
+
+        :type fip_id: ``str``
+        :param fip_id: The ID of the FloatingIP to retrieve.
+
+        :rtype: ``object`` of :class:`.FloatingIP`
+        :return: a FloatingIP object
+        """
+        pass
+
+    @abstractmethod
+    def list(self, limit=None, marker=None):
+        """
+        List floating (i.e., static) IP addresses.
+
+        :rtype: ``list`` of :class:`.FloatingIP`
+        :return: list of FloatingIP objects
+        """
+        pass
+
+    @abstractmethod
+    def find(self, **kwargs):
+        """
+        Searches for a FloatingIP by a given list of attributes.
+
+        Supported attributes: name, public_ip
+
+        Example:
+
+        .. code-block:: python
+
+            fip = provider.networking.gateways.get('id').floating_ips.find(
+                        public_ip='public_ip')
+
+
+        :rtype: List of ``object`` of :class:`.FloatingIP`
+        :return: A list of FloatingIP objects matching the supplied attributes.
+        """
+        pass
+
+    @abstractmethod
+    def create(self):
+        """
+        Allocate a new floating (i.e., static) IP address.
+
+        :rtype: ``object`` of :class:`.FloatingIP`
+        :return:  A FloatingIP object
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, fip_id):
+        """
+        Delete an existing FloatingIP.
+
+        :type fip_id: ``str``
+        :param fip_id: The ID of the FloatingIP to be deleted.
+        """
+        pass
+
+
+class VMFirewallRuleSubService(PageableObjectMixin):
+    """
+    Base interface for Firewall rules.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, rule_id):
+        """
+        Return a firewall rule given its ID.
+
+        Returns ``None`` if the rule does not exist.
+
+        Example:
+
+        .. code-block:: python
+
+            fw = provider.security.vm_firewalls.get('my_fw_id')
+            rule = fw.rules.get('rule_id')
+            print(rule.id, rule.label)
+
+        :type rule_id: str
+        :param rule_id: The ID of the desired firewall rule
+
+        :rtype: :class:`.FirewallRule`
+        :return:  a FirewallRule instance
+        """
+        pass
+
+    @abstractmethod
+    def list(self, limit=None, marker=None):
+        """
+        List all firewall rules associated with this firewall.
+
+        :rtype: ``list`` of :class:`.FirewallRule`
+        :return:  list of Firewall rule objects
+        """
+        pass
+
+    @abstractmethod
+    def create(self,  direction, protocol=None, from_port=None,
+               to_port=None, cidr=None, src_dest_fw=None):
+        """
+        Create a VM firewall rule.
+
+        If a matching rule already exists, return it.
+
+        Example:
+
+        .. code-block:: python
+            from cloudbridge.cloud.interfaces.resources import TrafficDirection
+            from cloudbridge.cloud.interfaces.resources import BaseNetwork
+
+            fw = provider.security.vm_firewalls.get('my_fw_id')
+            fw.rules.create(TrafficDirection.INBOUND, protocol='tcp',
+                            from_port=80, to_port=80,
+                            cidr=BaseNetwork.CB_DEFAULT_IPV4RANGE)
+            fw.rules.create(TrafficDirection.INBOUND, src_dest_fw=fw)
+            fw.rules.create(TrafficDirection.OUTBOUND, src_dest_fw=fw)
+
+        You need to pass in either ``src_dest_fw`` OR ``protocol`` AND
+        ``from_port``, ``to_port``, ``cidr``. In other words, either
+        you are authorizing another group or you are authorizing some
+        IP-based rule.
+
+        :type direction: :class:``.TrafficDirection``
+        :param direction: Either ``TrafficDirection.INBOUND`` |
+                          ``TrafficDirection.OUTBOUND``
+
+        :type protocol: ``str``
+        :param protocol: Either ``tcp`` | ``udp`` | ``icmp``.
+
+        :type from_port: ``int``
+        :param from_port: The beginning port number you are enabling.
+
+        :type to_port: ``int``
+        :param to_port: The ending port number you are enabling.
+
+        :type cidr: ``str`` or list of ``str``
+        :param cidr: The CIDR block you are providing access to.
+
+        :type src_dest_fw: :class:`.VMFirewall`
+        :param src_dest_fw: The VM firewall object which is the
+                            source/destination of the traffic, depending on
+                            whether it's ingress/egress traffic.
+
+        :rtype: :class:`.VMFirewallRule`
+        :return: Rule object if successful or ``None``.
+        """
+        pass
+
+    @abstractmethod
+    def find(self, **kwargs):
+        """
+        Find a firewall rule filtered by the given parameters.
+
+        :type label: str
+        :param label: The label of the VM firewall to retrieve.
+
+        :type protocol: ``str``
+        :param protocol: Either ``tcp`` | ``udp`` | ``icmp``.
+
+        :type from_port: ``int``
+        :param from_port: The beginning port number you are enabling.
+
+        :type to_port: ``int``
+        :param to_port: The ending port number you are enabling.
+
+        :type cidr: ``str`` or list of ``str``
+        :param cidr: The CIDR block you are providing access to.
+
+        :type src_dest_fw: :class:`.VMFirewall`
+        :param src_dest_fw: The VM firewall object which is the
+                            source/destination of the traffic, depending on
+                            whether it's ingress/egress traffic.
+
+        :type src_dest_fw_id: :class:`.str`
+        :param src_dest_fw_id: The VM firewall id which is the
+                               source/destination of the traffic, depending on
+                               whether it's ingress/egress traffic.
+
+        :rtype: list of :class:`VMFirewallRule`
+        :return: A list of VMFirewall objects or an empty list if none
+                 found.
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, rule_id):
+        """
+        Delete an existing VMFirewall rule.
+
+        :type rule_id: str
+        :param rule_id: The VM firewall rule to be deleted.
+        """
+        pass
+
+
+class SubnetSubService(PageableObjectMixin):
+    """
+    Base interface for a Subnet Service.
+    """
+    __metaclass__ = ABCMeta
+
+    @abstractmethod
+    def get(self, subnet_id):
+        """
+        Returns a Subnet given its ID or ``None`` if not found.
+
+        :type subnet_id: ``str``
+        :param subnet_id: The ID of the Subnet to retrieve.
+
+        :rtype: ``object`` of :class:`.Subnet`
+        :return: a Subnet object
+        """
+        pass
+
+    @abstractmethod
+    def list(self, limit=None, marker=None):
+        """
+        List subnets within the network holding this subservice.
+
+        :rtype: ``list`` of :class:`.Subnet`
+        :return: list of Subnet objects
+        """
+        pass
+
+    @abstractmethod
+    def find(self, **kwargs):
+        """
+        Searches for a Subnet by a given list of attributes.
+
+        Supported attributes: label
+
+        Example:
+
+        .. code-block:: python
+
+            subnet = provider.networking.networks.get('id').subnets.find(
+                        label='my-subnet')
+
+
+        :rtype: List of ``object`` of :class:`.Subnet`
+        :return: A list of Subnet objects matching the supplied attributes.
+        """
+        pass
+
+    @abstractmethod
+    def create(self, label, cidr_block, zone):
+        """
+        Create a new subnet within the network holding this subservice.
+
+        :type label: ``str``
+        :param label: The subnet label.
+
+        :type cidr_block: ``str``
+        :param cidr_block: CIDR block within the Network to assign to the
+                           subnet.
+
+        :type zone: ``str``
+        :param zone: A placement zone for the subnet. Some providers
+                     may not support this, in which case the value is ignored.
+
+        :rtype: ``object`` of :class:`.Subnet`
+        :return:  A Subnet object
+        """
+        pass
+
+    @abstractmethod
+    def delete(self, subnet_id):
+        """
+        Delete an existing Subnet.
+
+        :type subnet_id: ``str``
+        :param subnet_id: The ID of the Subnet to be deleted.
+        """
+        pass

+ 0 - 1
cloudbridge/cloud/providers/aws/__init__.py

@@ -3,4 +3,3 @@ Exports from this provider
 """
 
 from .provider import AWSCloudProvider  # noqa
-from .provider import MockAWSCloudProvider  # noqa

+ 19 - 2
cloudbridge/cloud/providers/aws/helpers.py

@@ -96,12 +96,14 @@ class BotoGenericService(object):
             if sr.resource.model.name == collection_model.resource.model.name)
         return getattr(self.boto_conn, resource_model.name)
 
-    def get(self, resource_id):
+    def get_raw(self, resource_id):
         """
         Returns a single resource.
 
         :type resource_id: ``str``
         :param resource_id: ID of the boto resource to fetch
+
+        :returns An unwrapped AWS resource
         """
         try:
             log.debug("Retrieving resource: %s with id: %s",
@@ -109,7 +111,7 @@ class BotoGenericService(object):
             obj = self.boto_resource(resource_id)
             obj.load()
             log.debug("Successfully Retrieved: %s", obj)
-            return self.cb_resource(self.provider, obj)
+            return obj
         except ClientError as exc:
             error_code = exc.response['Error']['Code']
             if any(status in error_code for status in
@@ -119,6 +121,21 @@ class BotoGenericService(object):
             else:
                 raise exc
 
+    def get(self, resource_id):
+        """
+        Returns a single resource.
+
+        :type resource_id: ``str``
+        :param resource_id: ID of the boto resource to fetch
+
+        :returns A CloudBridge wrapped resource
+        """
+        aws_res = self.get_raw(resource_id)
+        if aws_res:
+            return self.cb_resource(self.provider, aws_res)
+        else:
+            return None
+
     def _get_list_operation(self):
         """
         This function discovers the list operation for a particular resource

+ 0 - 64
cloudbridge/cloud/providers/aws/provider.py

@@ -3,17 +3,8 @@ import logging as log
 
 import boto3
 
-try:
-    # These are installed only for the case of a dev instance
-    import responses
-    from moto import mock_ec2
-    from moto import mock_s3
-except ImportError:
-    log.debug('[aws provider] moto library not available!')
-
 from cloudbridge.cloud.base import BaseCloudProvider
 from cloudbridge.cloud.base.helpers import get_env
-from cloudbridge.cloud.interfaces import TestMockHelperMixin
 
 from .services import AWSComputeService
 from .services import AWSNetworkingService
@@ -117,58 +108,3 @@ class AWSCloudProvider(BaseCloudProvider):
         '''Get an S3 resource object'''
         return self.session.resource(
             's3', region_name=self.region_name, **self.s3_cfg)
-
-
-class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
-
-    def __init__(self, config):
-        super(MockAWSCloudProvider, self).__init__(config)
-
-    def setUpMock(self):
-        """
-        Let Moto take over all socket communications
-        """
-        self.ec2mock = mock_ec2()
-        self.ec2mock.start()
-        self.s3mock = mock_s3()
-        self.s3mock.start()
-        responses.add(
-            responses.GET,
-            self.AWS_INSTANCE_DATA_DEFAULT_URL,
-            body=u"""
-[
-  {
-    "family": "General Purpose",
-    "enhanced_networking": false,
-    "vCPU": 1,
-    "generation": "current",
-    "ebs_iops": 0,
-    "network_performance": "Low",
-    "ebs_throughput": 0,
-    "vpc": {
-      "ips_per_eni": 2,
-      "max_enis": 2
-    },
-    "arch": [
-      "x86_64"
-    ],
-    "linux_virtualization_types": [
-        "HVM"
-    ],
-    "ebs_optimized": false,
-    "storage": null,
-    "max_bandwidth": 0,
-    "instance_type": "t2.nano",
-    "ECU": "variable",
-    "memory": 0.5,
-    "ebs_max_bandwidth": 0
-  }
-]
-""")
-
-    def tearDownMock(self):
-        """
-        Stop Moto intercepting all socket communications
-        """
-        self.s3mock.stop()
-        self.ec2mock.stop()

+ 29 - 227
cloudbridge/cloud/providers/aws/resources.py

@@ -7,14 +7,10 @@ import logging
 
 from botocore.exceptions import ClientError
 
-import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseBucket
-from cloudbridge.cloud.base.resources import BaseBucketContainer
 from cloudbridge.cloud.base.resources import BaseBucketObject
 from cloudbridge.cloud.base.resources import BaseFloatingIP
-from cloudbridge.cloud.base.resources import BaseFloatingIPContainer
-from cloudbridge.cloud.base.resources import BaseGatewayContainer
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInternetGateway
 from cloudbridge.cloud.base.resources import BaseKeyPair
@@ -28,11 +24,8 @@ from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSubnet
 from cloudbridge.cloud.base.resources import BaseVMFirewall
 from cloudbridge.cloud.base.resources import BaseVMFirewallRule
-from cloudbridge.cloud.base.resources import BaseVMFirewallRuleContainer
 from cloudbridge.cloud.base.resources import BaseVMType
 from cloudbridge.cloud.base.resources import BaseVolume
-from cloudbridge.cloud.base.resources import ClientPagedResultList
-from cloudbridge.cloud.interfaces.exceptions import InvalidValueException
 from cloudbridge.cloud.interfaces.resources import GatewayState
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
@@ -40,12 +33,15 @@ from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import RouterState
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SubnetState
-from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VolumeState
 
-from .helpers import BotoEC2Service
 from .helpers import find_tag_value
 from .helpers import trim_empty_params
+from .subservices import AWSBucketObjectSubService
+from .subservices import AWSFloatingIPSubService
+from .subservices import AWSGatewaySubService
+from .subservices import AWSSubnetSubService
+from .subservices import AWSVMFirewallRuleSubService
 
 log = logging.getLogger(__name__)
 
@@ -145,6 +141,7 @@ class AWSPlacementZone(BasePlacementZone):
         if isinstance(zone, AWSPlacementZone):
             # pylint:disable=protected-access
             self._aws_zone = zone._aws_zone
+            # pylint:disable=protected-access
             self._aws_region = zone._aws_region
         else:
             self._aws_zone = zone
@@ -281,9 +278,6 @@ class AWSInstance(BaseInstance):
     def reboot(self):
         self._ec2_instance.reboot()
 
-    def delete(self):
-        self._ec2_instance.terminate()
-
     @property
     def image_id(self):
         return self._ec2_instance.image_id
@@ -331,18 +325,16 @@ class AWSInstance(BaseInstance):
 
     def _get_fip(self, floating_ip):
         """Get a floating IP object based on the supplied allocation ID."""
-        return AWSFloatingIP(
-            self._provider, list(self._provider.ec2_conn.vpc_addresses.filter(
-                AllocationIds=[floating_ip]))[0])
+        return self._provider.networking._floating_ips.get(None, floating_ip)
 
     def add_floating_ip(self, floating_ip):
         fip = (floating_ip if isinstance(floating_ip, AWSFloatingIP)
                else self._get_fip(floating_ip))
+        # pylint:disable=protected-access
         params = trim_empty_params({
             'InstanceId': self.id,
             'PublicIp': None if self._ec2_instance.vpc_id else
             fip.public_ip,
-            # pylint:disable=protected-access
             'AllocationId': fip._ip.allocation_id})
         self._provider.ec2_conn.meta.client.associate_address(**params)
         self.refresh()
@@ -350,10 +342,10 @@ class AWSInstance(BaseInstance):
     def remove_floating_ip(self, floating_ip):
         fip = (floating_ip if isinstance(floating_ip, AWSFloatingIP)
                else self._get_fip(floating_ip))
+        # pylint:disable=protected-access
         params = trim_empty_params({
             'PublicIp': None if self._ec2_instance.vpc_id else
             fip.public_ip,
-            # pylint:disable=protected-access
             'AssociationId': fip._ip.association_id})
         self._provider.ec2_conn.meta.client.disassociate_address(**params)
         self.refresh()
@@ -497,9 +489,6 @@ class AWSVolume(BaseVolume):
         snap.wait_till_ready()
         return snap
 
-    def delete(self):
-        self._volume.delete()
-
     @property
     def state(self):
         if self._unknown_state:
@@ -601,9 +590,6 @@ class AWSSnapshot(BaseSnapshot):
             # set the status to unknown
             self._unknown_state = True
 
-    def delete(self):
-        self._snapshot.delete()
-
     def create_volume(self, placement, size=None, volume_type=None, iops=None):
         label = "from-snap-{0}".format(self.label or self.id)
         cb_vol = self._provider.storage.volumes.create(
@@ -625,7 +611,7 @@ class AWSVMFirewall(BaseVMFirewall):
 
     def __init__(self, provider, _vm_firewall):
         super(AWSVMFirewall, self).__init__(provider, _vm_firewall)
-        self._rule_container = AWSVMFirewallRuleContainer(provider, self)
+        self._rule_container = AWSVMFirewallRuleSubService(provider, self)
 
     @property
     def name(self):
@@ -648,6 +634,19 @@ class AWSVMFirewall(BaseVMFirewall):
         self._vm_firewall.create_tags(Tags=[{'Key': 'Name',
                                              'Value': value or ""}])
 
+    @property
+    def description(self):
+        try:
+            return find_tag_value(self._vm_firewall.tags, 'Description')
+        except ClientError:
+            return None
+
+    @description.setter
+    # pylint:disable=arguments-differ
+    def description(self, value):
+        self._vm_firewall.create_tags(Tags=[{'Key': 'Description',
+                                             'Value': value or ""}])
+
     @property
     def network_id(self):
         return self._vm_firewall.vpc_id
@@ -669,56 +668,6 @@ class AWSVMFirewall(BaseVMFirewall):
         return js
 
 
-class AWSVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
-
-    def __init__(self, provider, firewall):
-        super(AWSVMFirewallRuleContainer, self).__init__(provider, firewall)
-
-    def list(self, limit=None, marker=None):
-        # pylint:disable=protected-access
-        rules = [AWSVMFirewallRule(self.firewall,
-                                   TrafficDirection.INBOUND, r)
-                 for r in self.firewall._vm_firewall.ip_permissions]
-        rules = rules + [
-            AWSVMFirewallRule(
-                self.firewall, TrafficDirection.OUTBOUND, r)
-            for r in self.firewall._vm_firewall.ip_permissions_egress]
-        return ClientPagedResultList(self._provider, rules,
-                                     limit=limit, marker=marker)
-
-    def create(self,  direction, protocol=None, from_port=None,
-               to_port=None, cidr=None, src_dest_fw=None):
-        src_dest_fw_id = (
-            src_dest_fw.id if isinstance(src_dest_fw, AWSVMFirewall)
-            else src_dest_fw)
-
-        # pylint:disable=protected-access
-        ip_perm_entry = AWSVMFirewallRule._construct_ip_perms(
-            protocol, from_port, to_port, cidr, src_dest_fw_id)
-        # Filter out empty values to please Boto
-        ip_perms = [trim_empty_params(ip_perm_entry)]
-
-        try:
-            if direction == TrafficDirection.INBOUND:
-                # pylint:disable=protected-access
-                self.firewall._vm_firewall.authorize_ingress(
-                    IpPermissions=ip_perms)
-            elif direction == TrafficDirection.OUTBOUND:
-                # pylint:disable=protected-access
-                self.firewall._vm_firewall.authorize_egress(
-                    IpPermissions=ip_perms)
-            else:
-                raise InvalidValueException("direction", direction)
-            self.firewall.refresh()
-            return AWSVMFirewallRule(self.firewall, direction, ip_perm_entry)
-        except ClientError as ec2e:
-            if ec2e.response['Error']['Code'] == "InvalidPermission.Duplicate":
-                return AWSVMFirewallRule(
-                    self.firewall, direction, ip_perm_entry)
-            else:
-                raise ec2e
-
-
 class AWSVMFirewallRule(BaseVMFirewallRule):
 
     def __init__(self, parent_fw, direction, rule):
@@ -785,23 +734,6 @@ class AWSVMFirewallRule(BaseVMFirewallRule):
             ] if src_dest_fw_id else None
         }
 
-    def delete(self):
-        ip_perm_entry = self._construct_ip_perms(
-            self.protocol, self.from_port, self.to_port,
-            self.cidr, self.src_dest_fw_id)
-
-        # Filter out empty values to please Boto
-        ip_perms = [trim_empty_params(ip_perm_entry)]
-
-        # pylint:disable=protected-access
-        if self.direction == TrafficDirection.INBOUND:
-            self.firewall._vm_firewall.revoke_ingress(
-                IpPermissions=ip_perms)
-        else:
-            self.firewall._vm_firewall.revoke_egress(
-                IpPermissions=ip_perms)
-        self.firewall.refresh()
-
 
 class AWSBucketObject(BaseBucketObject):
 
@@ -875,7 +807,7 @@ class AWSBucket(BaseBucket):
     def __init__(self, provider, bucket):
         super(AWSBucket, self).__init__(provider)
         self._bucket = bucket
-        self._object_container = AWSBucketContainer(provider, self)
+        self._object_container = AWSBucketObjectSubService(provider, self)
 
     @property
     def id(self):
@@ -889,48 +821,6 @@ class AWSBucket(BaseBucket):
     def objects(self):
         return self._object_container
 
-    def delete(self, delete_contents=False):
-        self._bucket.delete()
-
-
-class AWSBucketContainer(BaseBucketContainer):
-
-    def __init__(self, provider, bucket):
-        super(AWSBucketContainer, self).__init__(provider, bucket)
-
-    def get(self, name):
-        try:
-            # pylint:disable=protected-access
-            obj = self.bucket._bucket.Object(name)
-            # load() throws an error if object does not exist
-            obj.load()
-            return AWSBucketObject(self._provider, obj)
-        except ClientError:
-            return None
-
-    def list(self, limit=None, marker=None, prefix=None):
-        if prefix:
-            # pylint:disable=protected-access
-            boto_objs = self.bucket._bucket.objects.filter(Prefix=prefix)
-        else:
-            # pylint:disable=protected-access
-            boto_objs = self.bucket._bucket.objects.all()
-        objects = [AWSBucketObject(self._provider, obj) for obj in boto_objs]
-        return ClientPagedResultList(self._provider, objects,
-                                     limit=limit, marker=marker)
-
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches),
-                                     limit=None, marker=None)
-
-    def create(self, name):
-        # pylint:disable=protected-access
-        obj = self.bucket._bucket.Object(name)
-        return AWSBucketObject(self._provider, obj)
-
 
 class AWSRegion(BaseRegion):
 
@@ -973,8 +863,9 @@ class AWSNetwork(BaseNetwork):
     def __init__(self, provider, network):
         super(AWSNetwork, self).__init__(provider)
         self._vpc = network
-        self._gtw_container = AWSGatewayContainer(provider, self)
         self._unknown_state = False
+        self._gtw_container = AWSGatewaySubService(provider, self)
+        self._subnet_svc = AWSSubnetSubService(provider, self)
 
     @property
     def id(self):
@@ -1017,12 +908,9 @@ class AWSNetwork(BaseNetwork):
     def cidr_block(self):
         return self._vpc.cidr_block
 
-    def delete(self):
-        self._vpc.delete()
-
     @property
     def subnets(self):
-        return [AWSSubnet(self._provider, s) for s in self._vpc.subnets.all()]
+        return self._subnet_svc
 
     def refresh(self):
         try:
@@ -1087,9 +975,6 @@ class AWSSubnet(BaseSubnet):
         return AWSPlacementZone(self._provider, self._subnet.availability_zone,
                                 self._provider.region_name)
 
-    def delete(self):
-        self._subnet.delete()
-
     @property
     def state(self):
         if self._unknown_state:
@@ -1110,31 +995,6 @@ class AWSSubnet(BaseSubnet):
             self._unknown_state = True
 
 
-class AWSFloatingIPContainer(BaseFloatingIPContainer):
-
-    def __init__(self, provider, gateway):
-        super(AWSFloatingIPContainer, self).__init__(provider, gateway)
-        self.svc = BotoEC2Service(provider=self._provider,
-                                  cb_resource=AWSFloatingIP,
-                                  boto_collection_name='vpc_addresses')
-
-    def get(self, fip_id):
-        log.debug("Getting AWS Floating IP Service with the id: %s", fip_id)
-        return self.svc.get(fip_id)
-
-    def list(self, limit=None, marker=None):
-        log.debug("Listing all floating IPs under gateway %s", self.gateway)
-        return self.svc.list(limit=limit, marker=marker)
-
-    def create(self):
-        log.debug("Creating a floating IP under gateway %s", self.gateway)
-        ip = self._provider.ec2_conn.meta.client.allocate_address(
-            Domain='vpc')
-        return AWSFloatingIP(
-            self._provider,
-            self._provider.ec2_conn.VpcAddress(ip.get('AllocationId')))
-
-
 class AWSFloatingIP(BaseFloatingIP):
 
     def __init__(self, provider, floating_ip):
@@ -1155,10 +1015,7 @@ class AWSFloatingIP(BaseFloatingIP):
 
     @property
     def in_use(self):
-        return True if self._ip.instance_id else False
-
-    def delete(self):
-        self._ip.release()
+        return True if self._ip.association_id else False
 
     def refresh(self):
         self._ip.reload()
@@ -1205,9 +1062,6 @@ class AWSRouter(BaseRouter):
     def network_id(self):
         return self._route_table.vpc_id
 
-    def delete(self):
-        self._route_table.delete()
-
     def attach_subnet(self, subnet):
         subnet_id = subnet.id if isinstance(subnet, AWSSubnet) else subnet
         self._route_table.associate_with_subnet(SubnetId=subnet_id)
@@ -1241,57 +1095,13 @@ class AWSRouter(BaseRouter):
             InternetGatewayId=gw_id, VpcId=self._route_table.vpc_id)
 
 
-class AWSGatewayContainer(BaseGatewayContainer):
-
-    def __init__(self, provider, network):
-        super(AWSGatewayContainer, self).__init__(provider, network)
-        self.svc = BotoEC2Service(provider=provider,
-                                  cb_resource=AWSInternetGateway,
-                                  boto_collection_name='internet_gateways')
-
-    def get_or_create_inet_gateway(self):
-        log.debug("Get or create inet gateway on net %s",
-                  self._network)
-        network_id = self._network.id if isinstance(
-            self._network, AWSNetwork) else self._network
-        # Don't filter by label because it may conflict with at least the
-        # default VPC that most accounts have but that network is typically
-        # without a name.
-        gtw = self.svc.find(filter_name='attachment.vpc-id',
-                            filter_value=network_id)
-        if gtw:
-            return gtw[0]  # There can be only one gtw attached to a VPC
-        # Gateway does not exist so create one and attach to the supplied net
-        cb_gateway = self.svc.create('create_internet_gateway')
-        cb_gateway._gateway.create_tags(
-            Tags=[{'Key': 'Name',
-                   'Value': AWSInternetGateway.CB_DEFAULT_INET_GATEWAY_NAME
-                   }])
-        cb_gateway._gateway.attach_to_vpc(VpcId=network_id)
-        return cb_gateway
-
-    def delete(self, gateway):
-        log.debug("Service deleting AWS Gateway %s", gateway)
-        gateway_id = gateway.id if isinstance(
-            gateway, AWSInternetGateway) else gateway
-        gateway = self.svc.get(gateway_id)
-        if gateway:
-            gateway.delete()
-
-    def list(self, limit=None, marker=None):
-        log.debug("Listing current AWS internet gateways for net %s.",
-                  self._network.id)
-        fltr = [{'Name': 'attachment.vpc-id', 'Values': [self._network.id]}]
-        return self.svc.list(limit=None, marker=None, Filters=fltr)
-
-
 class AWSInternetGateway(BaseInternetGateway):
 
     def __init__(self, provider, gateway):
         super(AWSInternetGateway, self).__init__(provider)
         self._gateway = gateway
         self._gateway.state = ''
-        self._fips_container = AWSFloatingIPContainer(provider, self)
+        self._fips_container = AWSFloatingIPSubService(provider, self)
 
     @property
     def id(self):
@@ -1320,14 +1130,6 @@ class AWSInternetGateway(BaseInternetGateway):
             return self._gateway.attachments[0].get('VpcId')
         return None
 
-    def delete(self):
-        try:
-            if self.network_id:
-                self._gateway.detach_from_vpc(VpcId=self.network_id)
-            self._gateway.delete()
-        except ClientError as e:
-            log.warn("Error deleting gateway {0}: {1}".format(self.id, e))
-
     @property
     def floating_ips(self):
         return self._fips_container

Разница между файлами не показана из-за своего большого размера
+ 466 - 195
cloudbridge/cloud/providers/aws/services.py


+ 39 - 0
cloudbridge/cloud/providers/aws/subservices.py

@@ -0,0 +1,39 @@
+import logging
+
+from cloudbridge.cloud.base.subservices import BaseBucketObjectSubService
+from cloudbridge.cloud.base.subservices import BaseFloatingIPSubService
+from cloudbridge.cloud.base.subservices import BaseGatewaySubService
+from cloudbridge.cloud.base.subservices import BaseSubnetSubService
+from cloudbridge.cloud.base.subservices import BaseVMFirewallRuleSubService
+
+log = logging.getLogger(__name__)
+
+
+class AWSBucketObjectSubService(BaseBucketObjectSubService):
+
+    def __init__(self, provider, bucket):
+        super(AWSBucketObjectSubService, self).__init__(provider, bucket)
+
+
+class AWSGatewaySubService(BaseGatewaySubService):
+
+    def __init__(self, provider, network):
+        super(AWSGatewaySubService, self).__init__(provider, network)
+
+
+class AWSVMFirewallRuleSubService(BaseVMFirewallRuleSubService):
+
+    def __init__(self, provider, firewall):
+        super(AWSVMFirewallRuleSubService, self).__init__(provider, firewall)
+
+
+class AWSFloatingIPSubService(BaseFloatingIPSubService):
+
+    def __init__(self, provider, gateway):
+        super(AWSFloatingIPSubService, self).__init__(provider, gateway)
+
+
+class AWSSubnetSubService(BaseSubnetSubService):
+
+    def __init__(self, provider, network):
+        super(AWSSubnetSubService, self).__init__(provider, network)

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

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

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

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

+ 48 - 271
cloudbridge/cloud/providers/azure/resources.py

@@ -3,7 +3,6 @@ DataTypes used by this provider
 """
 import collections
 import logging
-from uuid import uuid4
 
 from azure.common import AzureException
 from azure.mgmt.devtestlabs.models import GalleryImageReference
@@ -13,20 +12,41 @@ from msrestazure.azure_exceptions import CloudError
 
 import pysftp
 
-import cloudbridge.cloud.base.helpers as cb_helpers
-from cloudbridge.cloud.base.resources import BaseAttachmentInfo, \
-    BaseBucket, BaseBucketContainer, BaseBucketObject, BaseFloatingIP, \
-    BaseFloatingIPContainer, BaseGatewayContainer, BaseInstance, \
-    BaseInternetGateway, BaseKeyPair, BaseLaunchConfig, \
-    BaseMachineImage, BaseNetwork, BasePlacementZone, BaseRegion, BaseRouter, \
-    BaseSnapshot, BaseSubnet, BaseVMFirewall, BaseVMFirewallRule, \
-    BaseVMFirewallRuleContainer, BaseVMType, BaseVolume, ClientPagedResultList
-from cloudbridge.cloud.interfaces import InstanceState, VolumeState
-from cloudbridge.cloud.interfaces.resources import Instance, \
-    MachineImageState, NetworkState, RouterState, \
-    SnapshotState, SubnetState, TrafficDirection
+from cloudbridge.cloud.base.resources import BaseAttachmentInfo
+from cloudbridge.cloud.base.resources import BaseBucket
+from cloudbridge.cloud.base.resources import BaseBucketObject
+from cloudbridge.cloud.base.resources import BaseFloatingIP
+from cloudbridge.cloud.base.resources import BaseInstance
+from cloudbridge.cloud.base.resources import BaseInternetGateway
+from cloudbridge.cloud.base.resources import BaseKeyPair
+from cloudbridge.cloud.base.resources import BaseLaunchConfig
+from cloudbridge.cloud.base.resources import BaseMachineImage
+from cloudbridge.cloud.base.resources import BaseNetwork
+from cloudbridge.cloud.base.resources import BasePlacementZone
+from cloudbridge.cloud.base.resources import BaseRegion
+from cloudbridge.cloud.base.resources import BaseRouter
+from cloudbridge.cloud.base.resources import BaseSnapshot
+from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.base.resources import BaseVMFirewall
+from cloudbridge.cloud.base.resources import BaseVMFirewallRule
+from cloudbridge.cloud.base.resources import BaseVMType
+from cloudbridge.cloud.base.resources import BaseVolume
+from cloudbridge.cloud.interfaces import InstanceState
+from cloudbridge.cloud.interfaces import VolumeState
+from cloudbridge.cloud.interfaces.resources import Instance
+from cloudbridge.cloud.interfaces.resources import MachineImageState
+from cloudbridge.cloud.interfaces.resources import NetworkState
+from cloudbridge.cloud.interfaces.resources import RouterState
+from cloudbridge.cloud.interfaces.resources import SnapshotState
+from cloudbridge.cloud.interfaces.resources import SubnetState
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
 
 from . import helpers as azure_helpers
+from .subservices import AzureBucketObjectSubService
+from .subservices import AzureFloatingIPSubService
+from .subservices import AzureGatewaySubService
+from .subservices import AzureSubnetSubService
+from .subservices import AzureVMFirewallRuleSubService
 
 log = logging.getLogger(__name__)
 
@@ -36,11 +56,11 @@ class AzureVMFirewall(BaseVMFirewall):
         super(AzureVMFirewall, self).__init__(provider, vm_firewall)
         self._vm_firewall = vm_firewall
         self._vm_firewall.tags = self._vm_firewall.tags or {}
-        self._rule_container = AzureVMFirewallRuleContainer(provider, self)
+        self._rule_container = AzureVMFirewallRuleSubService(provider, self)
 
     @property
     def network_id(self):
-        return None
+        return self._vm_firewall.tags.get('network_id', None)
 
     @property
     def resource_id(self):
@@ -80,9 +100,6 @@ class AzureVMFirewall(BaseVMFirewall):
     def rules(self):
         return self._rule_container
 
-    def delete(self):
-        self._provider.azure_client.delete_vm_firewall(self.id)
-
     def refresh(self):
         """
         Refreshes the security group with tags if required.
@@ -105,70 +122,6 @@ class AzureVMFirewall(BaseVMFirewall):
         return js
 
 
-class AzureVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
-
-    def __init__(self, provider, firewall):
-        super(AzureVMFirewallRuleContainer, self).__init__(provider, firewall)
-
-    def list(self, limit=None, marker=None):
-        # Filter out firewall rules with priority < 3500 because values
-        # between 3500 and 4096 are assumed to be owned by cloudbridge
-        # default rules.
-        # pylint:disable=protected-access
-        rules = [AzureVMFirewallRule(self.firewall, rule) for rule
-                 in self.firewall._vm_firewall.security_rules
-                 if rule.priority < 3500]
-        return ClientPagedResultList(self._provider, rules,
-                                     limit=limit, marker=marker)
-
-    def create(self, direction, protocol=None, from_port=None, to_port=None,
-               cidr=None, src_dest_fw=None):
-        if protocol and from_port and to_port:
-            return self._create_rule(direction, protocol, from_port,
-                                     to_port, cidr)
-        elif src_dest_fw:
-            result = None
-            fw = (self._provider.security.vm_firewalls.get(src_dest_fw)
-                  if isinstance(src_dest_fw, str) else src_dest_fw)
-            for rule in fw.rules:
-                result = self._create_rule(
-                    rule.direction, rule.protocol, rule.from_port,
-                    rule.to_port, rule.cidr)
-            return result
-        else:
-            return None
-
-    def _create_rule(self, direction, protocol, from_port, to_port, cidr):
-
-        # If cidr is None, default values is set as 0.0.0.0/0
-        if not cidr:
-            cidr = '0.0.0.0/0'
-
-        count = len(self.firewall._vm_firewall.security_rules) + 1
-        rule_name = "cb-rule-" + str(count)
-        priority = 1000 + count
-        destination_port_range = str(from_port) + "-" + str(to_port)
-        source_port_range = '*'
-        destination_address_prefix = "*"
-        access = "Allow"
-        direction = ("Inbound" if direction == TrafficDirection.INBOUND
-                     else "Outbound")
-        parameters = {"priority": priority,
-                      "protocol": protocol,
-                      "source_port_range": source_port_range,
-                      "source_address_prefix": cidr,
-                      "destination_port_range": destination_port_range,
-                      "destination_address_prefix": destination_address_prefix,
-                      "access": access,
-                      "direction": direction}
-        result = self._provider.azure_client. \
-            create_vm_firewall_rule(self.firewall.id,
-                                    rule_name, parameters)
-        # pylint:disable=protected-access
-        self.firewall._vm_firewall.security_rules.append(result)
-        return AzureVMFirewallRule(self.firewall, result)
-
-
 # Tuple for port range
 PortRange = collections.namedtuple('PortRange', ['from_port', 'to_port'])
 
@@ -222,15 +175,6 @@ class AzureVMFirewallRule(BaseVMFirewallRule):
     def src_dest_fw(self):
         return self.firewall
 
-    def delete(self):
-        vm_firewall = self.firewall.name
-        self._provider.azure_client. \
-            delete_vm_firewall_rule(self.id, vm_firewall)
-        for i, o in enumerate(self.firewall._vm_firewall.security_rules):
-            if o.id == self.id:
-                del self.firewall._vm_firewall.security_rules[i]
-                break
-
 
 class AzureBucketObject(BaseBucketObject):
     def __init__(self, provider, container, key):
@@ -324,7 +268,7 @@ class AzureBucket(BaseBucket):
     def __init__(self, provider, bucket):
         super(AzureBucket, self).__init__(provider)
         self._bucket = bucket
-        self._object_container = AzureBucketContainer(provider, self)
+        self._object_container = AzureBucketObjectSubService(provider, self)
 
     @property
     def id(self):
@@ -337,12 +281,6 @@ class AzureBucket(BaseBucket):
         """
         return self._bucket.name
 
-    def delete(self, delete_contents=True):
-        """
-        Delete this bucket.
-        """
-        self._provider.azure_client.delete_container(self.name)
-
     def exists(self, name):
         """
         Determine if an object with given name exists in this bucket.
@@ -354,49 +292,6 @@ class AzureBucket(BaseBucket):
         return self._object_container
 
 
-class AzureBucketContainer(BaseBucketContainer):
-
-    def __init__(self, provider, bucket):
-        super(AzureBucketContainer, self).__init__(provider, bucket)
-
-    def get(self, key):
-        """
-        Retrieve a given object from this bucket.
-        """
-        try:
-            obj = self._provider.azure_client.get_blob(self.bucket.name,
-                                                       key)
-            return AzureBucketObject(self._provider, self.bucket, obj)
-        except AzureException as azureEx:
-            log.exception(azureEx)
-            return None
-
-    def list(self, limit=None, marker=None, prefix=None):
-        """
-        List all objects within this bucket.
-
-        :rtype: BucketObject
-        :return: List of all available BucketObjects within this bucket.
-        """
-        objects = [AzureBucketObject(self._provider, self.bucket, obj)
-                   for obj in
-                   self._provider.azure_client.list_blobs(
-                       self.bucket.name, prefix=prefix)]
-        return ClientPagedResultList(self._provider, objects,
-                                     limit=limit, marker=marker)
-
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
-
-    def create(self, name):
-        self._provider.azure_client.create_blob_from_text(
-            self.bucket.name, name, '')
-        return self.get(name)
-
-
 class AzureVolume(BaseVolume):
     VOLUME_STATE_MAP = {
         'InProgress': VolumeState.CREATING,
@@ -543,12 +438,6 @@ class AzureVolume(BaseVolume):
         return self._provider.storage.snapshots.create(label, self,
                                                        description)
 
-    def delete(self):
-        """
-        Delete this volume.
-        """
-        self._provider.azure_client.delete_disk(self.id)
-
     @property
     def state(self):
         return AzureVolume.VOLUME_STATE_MAP.get(
@@ -665,12 +554,6 @@ class AzureSnapshot(BaseSnapshot):
             # set the state to unknown
             self._state = 'unknown'
 
-    def delete(self):
-        """
-        Delete this snapshot.
-        """
-        self._provider.azure_client.delete_snapshot(self.id)
-
     def create_volume(self, placement=None,
                       size=None, volume_type=None, iops=None):
         """
@@ -822,28 +705,6 @@ class AzureMachineImage(BaseMachineImage):
                 self._state = "unknown"
 
 
-class AzureGatewayContainer(BaseGatewayContainer):
-    def __init__(self, provider, network):
-        super(AzureGatewayContainer, self).__init__(provider, network)
-        # Azure doesn't have a notion of a route table or an internet
-        # gateway as OS and AWS so create placeholder objects of the
-        # AzureInternetGateway here.
-        # http://bit.ly/2BqGdVh
-        # Singleton returned by the list method
-        self.gateway_singleton = AzureInternetGateway(self._provider, None,
-                                                      network)
-
-    def get_or_create_inet_gateway(self):
-        gateway = AzureInternetGateway(self._provider, None, self._network)
-        return gateway
-
-    def list(self, limit=None, marker=None):
-        return [self.gateway_singleton]
-
-    def delete(self, gateway):
-        pass
-
-
 class AzureNetwork(BaseNetwork):
     NETWORK_STATE_MAP = {
         'InProgress': NetworkState.PENDING,
@@ -856,7 +717,8 @@ class AzureNetwork(BaseNetwork):
         self._state = self._network.provisioning_state
         if not self._network.tags:
             self._network.tags = {}
-        self._gateway_service = AzureGatewayContainer(provider, self)
+        self._gateway_service = AzureGatewaySubService(provider, self)
+        self._subnet_svc = AzureSubnetSubService(provider, self)
 
     @property
     def id(self):
@@ -934,67 +796,18 @@ class AzureNetwork(BaseNetwork):
 
     @property
     def subnets(self):
-        """
-        List all the subnets in this network
-        :return:
-        """
-        return self._provider.networking.subnets.list(network=self.id)
-
-    def create_subnet(self, label, cidr_block, zone=None):
-        """
-        Create the subnet with cidr_block
-        :param cidr_block:
-        :param label:
-        :param zone:
-        :return:
-        """
-        return self._provider.networking.subnets. \
-            create(label=label, network=self.id, cidr_block=cidr_block)
+        return self._subnet_svc
 
     @property
     def gateways(self):
         return self._gateway_service
 
 
-class AzureFloatingIPContainer(BaseFloatingIPContainer):
-
-    def __init__(self, provider, gateway, network_id):
-        super(AzureFloatingIPContainer, self).__init__(provider, gateway)
-        self._network_id = network_id
-
-    def get(self, fip_id):
-        log.debug("Getting Azure Floating IP container with the id: %s",
-                  fip_id)
-        fip = [fip for fip in self if fip.id == fip_id]
-        return fip[0] if fip else None
-
-    def list(self, limit=None, marker=None):
-        floating_ips = [AzureFloatingIP(self._provider, floating_ip,
-                                        self._network_id)
-                        for floating_ip in self._provider.azure_client.
-                        list_floating_ips()]
-        return ClientPagedResultList(self._provider, floating_ips,
-                                     limit=limit, marker=marker)
-
-    def create(self):
-        public_ip_parameters = {
-            'location': self._provider.azure_client.region_name,
-            'public_ip_allocation_method': 'Static'
-        }
-
-        public_ip_name = 'cb-fip-' + uuid4().hex[:6]
-
-        floating_ip = self._provider.azure_client.\
-            create_floating_ip(public_ip_name, public_ip_parameters)
-        return AzureFloatingIP(self._provider, floating_ip, self._network_id)
-
-
 class AzureFloatingIP(BaseFloatingIP):
 
-    def __init__(self, provider, floating_ip, network_id):
+    def __init__(self, provider, floating_ip):
         super(AzureFloatingIP, self).__init__(provider)
         self._ip = floating_ip
-        self._network_id = network_id
 
     @property
     def id(self):
@@ -1021,16 +834,12 @@ class AzureFloatingIP(BaseFloatingIP):
     def in_use(self):
         return True if self._ip.ip_configuration else False
 
-    def delete(self):
-        """
-        Delete an existing floating ip.
-        """
-        self._provider.azure_client.delete_floating_ip(self.id)
-
     def refresh(self):
-        net = self._provider.networking.networks.get(self._network_id)
-        gw = net.gateways.get_or_create_inet_gateway()
-        fip = gw.floating_ips.get(self.id)
+        # Gateway is not needed as it doesn't exist in Azure, so just
+        # getting the Floating IP again from the client
+        # pylint:disable=protected-access
+        fip = self._provider.networking._floating_ips.get(None, self.id)
+        # pylint:disable=protected-access
         self._ip = fip._ip
 
 
@@ -1125,6 +934,7 @@ class AzureSubnet(BaseSubnet):
         # Although Subnet doesn't support labels, we use the parent Network's
         # tags to track the subnet's labels
         network = self.network
+        # pylint:disable=protected-access
         az_network = network._network
         return az_network.tags.get(self.tag_name, None)
 
@@ -1133,6 +943,7 @@ class AzureSubnet(BaseSubnet):
     def label(self, value):
         self.assert_valid_resource_label(value)
         network = self.network
+        # pylint:disable=protected-access
         az_network = network._network
         kwargs = {self.tag_name: value or ""}
         az_network.tags.update(**kwargs)
@@ -1164,9 +975,6 @@ class AzureSubnet(BaseSubnet):
     def network_id(self):
         return self._provider.azure_client.get_network_id_for_subnet(self.id)
 
-    def delete(self):
-        self._provider.azure_client.delete_subnet(self.id)
-
     @property
     def state(self):
         return self._SUBNET_STATE_MAP.get(self._state, NetworkState.UNKNOWN)
@@ -1307,30 +1115,6 @@ class AzureInstance(BaseInstance):
         """
         self._provider.azure_client.restart_vm(self.id)
 
-    def delete(self):
-        """
-        Permanently terminate this instance.
-        After deleting the VM. we are deleting the network interface
-        associated to the instance, public ip addresses associated to
-        the instance and also removing OS disk and data disks where
-        tag with name 'delete_on_terminate' has value True.
-        """
-        self._provider.azure_client.deallocate_vm(self.id)
-        self._provider.azure_client.delete_vm(self.id)
-        for public_ip_id in self._public_ip_ids:
-            self._provider.azure_client.delete_floating_ip(public_ip_id)
-        for nic_id in self._nic_ids:
-            self._provider.azure_client.delete_nic(nic_id)
-        for data_disk in self._vm.storage_profile.data_disks:
-            if data_disk.managed_disk:
-                if self._vm.tags.get('delete_on_terminate',
-                                     'False') == 'True':
-                    self._provider.azure_client.\
-                        delete_disk(data_disk.managed_disk.id)
-        if self._vm.storage_profile.os_disk.managed_disk:
-            self._provider.azure_client. \
-                delete_disk(self._vm.storage_profile.os_disk.managed_disk.id)
-
     @property
     def image_id(self):
         """
@@ -1625,9 +1409,6 @@ class AzureKeyPair(BaseKeyPair):
     def name(self):
         return self._key_pair.Name
 
-    def delete(self):
-        self._provider.azure_client.delete_public_key(self._key_pair)
-
 
 class AzureRouter(BaseRouter):
     def __init__(self, provider, route_table):
@@ -1684,9 +1465,6 @@ class AzureRouter(BaseRouter):
     def network_id(self):
         return None
 
-    def delete(self):
-        self._provider.azure_client.delete_route_table(self.name)
-
     def attach_subnet(self, subnet):
         self._provider.azure_client. \
             attach_subnet_to_route_table(subnet.id,
@@ -1720,8 +1498,7 @@ class AzureInternetGateway(BaseInternetGateway):
         self._network_id = gateway_net.id if isinstance(
             gateway_net, AzureNetwork) else gateway_net
         self._state = ''
-        self._fips_container = AzureFloatingIPContainer(
-            provider, self, self._network_id)
+        self._fips_container = AzureFloatingIPSubService(provider, self)
 
     @property
     def id(self):

Разница между файлами не показана из-за своего большого размера
+ 536 - 301
cloudbridge/cloud/providers/azure/services.py


+ 38 - 0
cloudbridge/cloud/providers/azure/subservices.py

@@ -0,0 +1,38 @@
+import logging
+
+from cloudbridge.cloud.base.subservices import BaseBucketObjectSubService
+from cloudbridge.cloud.base.subservices import BaseFloatingIPSubService
+from cloudbridge.cloud.base.subservices import BaseGatewaySubService
+from cloudbridge.cloud.base.subservices import BaseSubnetSubService
+from cloudbridge.cloud.base.subservices import BaseVMFirewallRuleSubService
+
+log = logging.getLogger(__name__)
+
+
+class AzureBucketObjectSubService(BaseBucketObjectSubService):
+
+    def __init__(self, provider, bucket):
+        super(AzureBucketObjectSubService, self).__init__(provider, bucket)
+
+
+class AzureGatewaySubService(BaseGatewaySubService):
+    def __init__(self, provider, network):
+        super(AzureGatewaySubService, self).__init__(provider, network)
+
+
+class AzureVMFirewallRuleSubService(BaseVMFirewallRuleSubService):
+
+    def __init__(self, provider, firewall):
+        super(AzureVMFirewallRuleSubService, self).__init__(provider, firewall)
+
+
+class AzureFloatingIPSubService(BaseFloatingIPSubService):
+
+    def __init__(self, provider, gateway):
+        super(AzureFloatingIPSubService, self).__init__(provider, gateway)
+
+
+class AzureSubnetSubService(BaseSubnetSubService):
+
+    def __init__(self, provider, network):
+        super(AzureSubnetSubService, self).__init__(provider, network)

+ 35 - 0
cloudbridge/cloud/providers/gcp/README.rst

@@ -0,0 +1,35 @@
+CloudBridge support for `Google Cloud Platform`_. Compute is provided by `Google
+Compute Engine`_ (GCE). Object storage is provided by `Google Cloud Storage`_
+(GCS).
+
+Security Groups
+~~~~~~~~~~~~~~~
+CloudBridge API lets you control incoming traffic to VM instances by creating
+VM firewalls, adding rules to VM firewalls, and then assigning instances to VM
+firewalls.
+
+GCP does this a little bit differently. GCP lets you assign `tags`_ to VM
+instances. Tags, then, can be used for networking purposes. In particular, you
+can create `firewall rules`_ to control incoming traffic to instances having a
+specific tag. So, to add GCP support to CloudBridge, we simulate VM firewalls by
+tags.
+
+To make this more clear, let us consider the example of adding a rule to a
+VM firewall. When you add a VM firewall rule from the CloudBridge API to a VM
+firewall ``vmf``, what really happens is that a firewall with one rule is
+created whose ``targetTags`` is ``[vmf]``. This makes sure that the rule
+applies to all instances that have ``vmf`` as a tag (in CloudBridge language
+instances belonging to the VM firewall ``vmf``).
+
+**Note**: This implementation does not take advantage of the full power of GCP
+firewall format and only creates firewalls with one rule and only can find or
+list firewalls with one rule. This should be OK as long as all firewalls are
+created through the CloudBridge API.
+
+.. _`Google Cloud Platform`: https://cloud.google.com/
+.. _`Google Compute Engine`: https://cloud.google.com/compute/docs
+.. _`Google Cloud Storage`: https://cloud.google.com/storage/docs
+.. _`tags`: https://cloud.google.com/compute/docs/reference/latest/instances/
+   setTags
+.. _`firewall rules`: https://cloud.google.com/compute/docs/
+   networking#firewall_rules

+ 5 - 0
cloudbridge/cloud/providers/gcp/__init__.py

@@ -0,0 +1,5 @@
+"""
+Exports from this provider
+"""
+
+from .provider import GCPCloudProvider  # noqa

+ 188 - 0
cloudbridge/cloud/providers/gcp/helpers.py

@@ -0,0 +1,188 @@
+import re
+
+from googleapiclient.errors import HttpError
+
+import tenacity
+
+from cloudbridge.cloud.interfaces.exceptions import ProviderInternalException
+
+
+def gcp_projects(provider):
+    return provider.gcp_compute.projects()
+
+
+def iter_all(resource, **kwargs):
+    token = None
+    while True:
+        response = resource.list(pageToken=token, **kwargs).execute()
+        for item in response.get('items', []):
+            yield item
+        if 'nextPageToken' not in response:
+            return
+        token = response['nextPageToken']
+
+
+def get_common_metadata(provider):
+    """
+    Get a project's commonInstanceMetadata entry
+    """
+    metadata = gcp_projects(provider).get(
+        project=provider.project_name).execute()
+    return metadata["commonInstanceMetadata"]
+
+
+def __if_fingerprint_differs(e):
+    # return True if the CloudError exception is due to subnet being in use
+    if isinstance(e, HttpError):
+        expected_message = 'Supplied fingerprint does not match current ' \
+                           'metadata fingerprint.'
+        # str wrapper required for Python 2.7
+        if expected_message in str(e.content):
+            return True
+    return False
+
+
+@tenacity.retry(stop=tenacity.stop_after_attempt(10),
+                retry=tenacity.retry_if_exception(__if_fingerprint_differs),
+                wait=tenacity.wait_exponential(max=10),
+                reraise=True)
+def gcp_metadata_save_op(provider, callback):
+    """
+    Carries out a metadata save operation. In GCP, a fingerprint based
+    locking mechanism is used to prevent lost updates. A new fingerprint
+    is returned each time metadata is retrieved. Therefore, this method
+    retrieves the metadata, invokes the provided callback with that
+    metadata, and saves the metadata using the original fingerprint
+    immediately afterwards, ensuring that update conflicts can be detected.
+    """
+    def _save_common_metadata(provider):
+        # get the latest metadata (so we get the latest fingerprint)
+        metadata = get_common_metadata(provider)
+        # allow callback to do processing on it
+        callback(metadata)
+        # save the metadata
+        operation = gcp_projects(provider).setCommonInstanceMetadata(
+            project=provider.project_name, body=metadata).execute()
+        provider.wait_for_operation(operation)
+
+    # Retry a few times if the fingerprints conflict
+    _save_common_metadata(provider)
+
+
+def modify_or_add_metadata_item(provider, key, value):
+    def _update_metadata_key(metadata):
+        entries = [item for item in metadata.get('items', [])
+                   if item['key'] == key]
+        if entries:
+            entries[-1]['value'] = value
+        else:
+            entry = {'key': key, 'value': value}
+            if 'items' not in metadata:
+                metadata['items'] = [entry]
+            else:
+                metadata['items'].append(entry)
+
+    gcp_metadata_save_op(provider, _update_metadata_key)
+
+
+# This function will raise an HttpError with message containing
+# "Metadata has duplicate key" if it's not unique, unlike the previous
+# method which either adds or updates the value corresponding to that key
+def add_metadata_item(provider, key, value):
+    def _add_metadata_key(metadata):
+        entry = {'key': key, 'value': value}
+        entries = metadata.get('items', [])
+        entries.append(entry)
+        # Reassign explicitly in case the original get returned [] although
+        # if not it will be already updated
+        metadata['items'] = entries
+
+    gcp_metadata_save_op(provider, _add_metadata_key)
+
+
+def find_matching_metadata_items(provider, key_regex):
+    metadata = get_common_metadata(provider)
+    items = metadata.get('items', [])
+    if not items:
+        return []
+    return [item for item in items
+            if re.search(key_regex, item['key'])]
+
+
+def get_metadata_item_value(provider, key):
+    metadata = get_common_metadata(provider)
+    entries = [item['value'] for item in metadata.get('items', [])
+               if item['key'] == key]
+    if entries:
+        return entries[-1]
+    else:
+        return None
+
+
+def remove_metadata_item(provider, key):
+    def _remove_metadata_by_key(metadata):
+        items = metadata.get('items', [])
+        # No metadata to delete
+        if not items:
+            return False
+        else:
+            entries = [item for item in metadata.get('items', [])
+                       if item['key'] != key]
+
+            # Make sure only one entry is deleted
+            if len(entries) < len(items) - 1:
+                raise ProviderInternalException("Multiple metadata entries "
+                                                "found for the same key {}"
+                                                .format(key))
+            # If none is deleted indicate so by returning False
+            elif len(entries) == len(items):
+                return False
+
+            else:
+                metadata['items'] = entries
+
+    gcp_metadata_save_op(provider, _remove_metadata_by_key)
+    return True
+
+
+def __if_label_fingerprint_differs(e):
+    # return True if the CloudError exception is due to subnet being in use
+    if isinstance(e, HttpError):
+        expected_message = 'Labels fingerprint either invalid or ' \
+                           'resource labels have changed'
+        # str wrapper required for Python 2.7
+        if expected_message in str(e.content):
+            return True
+    return False
+
+
+@tenacity.retry(stop=tenacity.stop_after_attempt(10),
+                retry=tenacity.retry_if_exception(
+                    __if_label_fingerprint_differs),
+                wait=tenacity.wait_exponential(max=10),
+                reraise=True)
+def change_label(resource, key, value, res_att, request):
+    resource.assert_valid_resource_label(value)
+    labels = getattr(resource, res_att).get("labels", {})
+    # The returned value from above command yields a unicode dict key, which
+    # cannot be simply cast into a str for py2 so pop the key and re-add it
+    # The casting needs to be done for all labels, as to support both
+    # description and label setting
+    labels[key] = str(value)
+    for k in labels.keys():
+        labels[str(k)] = str(labels.pop(k))
+
+    request_body = {
+        "labels": labels,
+        "labelFingerprint":
+            str(getattr(resource, res_att).get('labelFingerprint')),
+    }
+    try:
+        request.body = str(request_body)
+        request.body_size = len(str(request_body))
+        response = request.execute()
+        # pylint:disable=protected-access
+        resource._provider.wait_for_operation(
+            response, zone=getattr(resource, 'zone_name', None))
+    finally:
+        resource.refresh()

+ 369 - 0
cloudbridge/cloud/providers/gcp/provider.py

@@ -0,0 +1,369 @@
+"""
+Provider implementation based on google-api-python-client library
+for GCP.
+"""
+import json
+import logging
+import os
+import re
+import time
+from string import Template
+
+import googleapiclient
+from googleapiclient import discovery
+
+from oauth2client.client import GoogleCredentials
+from oauth2client.service_account import ServiceAccountCredentials
+
+from cloudbridge.cloud.base import BaseCloudProvider
+from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
+
+from .services import GCPComputeService
+from .services import GCPNetworkingService
+from .services import GCPSecurityService
+from .services import GCPStorageService
+
+log = logging.getLogger(__name__)
+
+
+class GCPResourceUrl(object):
+
+    def __init__(self, resource, connection):
+        self._resource = resource
+        self._connection = connection
+        self.parameters = {}
+
+    def get_resource(self):
+        """
+        The format of the returned resource is explained in details in
+        https://cloud.google.com/compute/docs/reference/latest/ and
+        https://cloud.google.com/storage/docs/json_api/v1/.
+
+        Example:
+            When requesting a subnet resource, the output looks like:
+
+            {'kind': 'compute#subnetwork',
+             'id': '6662746501848591938',
+             'creationTimestamp': '2017-10-13T12:53:17.445-07:00',
+             'name': 'testsubnet-2',
+             'network':
+                     'https://www.googleapis.com/compute/v1/projects/galaxy-on-gcp/global/networks/testnet',
+             'ipCidrRange': '10.128.0.0/20',
+             'gatewayAddress': '10.128.0.1',
+             'region':
+                     'https://www.googleapis.com/compute/v1/projects/galaxy-on-gcp/regions/us-central1',
+             'selfLink':
+                     'https://www.googleapis.com/compute/v1/projects/galaxy-on-gcp/regions/us-central1/subnetworks/testsubnet-2',
+             'privateIpGoogleAccess': false}
+        """
+        discovery_object = getattr(self._connection, self._resource)()
+        return discovery_object.get(**self.parameters).execute()
+
+
+class GCPResources(object):
+
+    def __init__(self, connection, **kwargs):
+        self._connection = connection
+        self._parameter_defaults = kwargs
+
+        # Resource descriptions are already pulled into the internal
+        # _resourceDesc field of the connection.
+        #
+        # FIX_IF_NEEDED: We could fetch compute resource descriptions from
+        # https://www.googleapis.com/discovery/v1/apis/compute/v1/rest and
+        # storage resource descriptions from
+        # https://www.googleapis.com/discovery/v1/apis/storage/v1/rest
+        # ourselves.
+        #
+        # Resource descriptions are in JSON format which are then parsed into a
+        # Python dictionary. The main fields we are interested are:
+        #
+        # {
+        #   "rootUrl": "https://www.googleapis.com/",
+        #   "servicePath": COMPUTE OR STORAGE SERVICE PATH
+        #   "resources": {
+        #     RESOURCE_NAME: {
+        #       "methods": {
+        #         "get": {
+        #           "path": RESOURCE PATH PATTERN
+        #           "parameters": {
+        #             PARAMETER: {
+        #               "pattern": REGEXP FOR VALID VALUES
+        #               ...
+        #             },
+        #             ...
+        #           },
+        #           "parameterOrder": [LIST OF PARAMETERS]
+        #         },
+        #         ...
+        #       }
+        #     },
+        #     ...
+        #   }
+        #   ...
+        # }
+        # pylint:disable=protected-access
+        desc = connection._resourceDesc
+        self._root_url = desc['rootUrl']
+        self._service_path = desc['servicePath']
+        self._resources = {}
+
+        # We will not mutate self._desc; it's OK to use items() in Python 2.x.
+        for resource, resource_desc in desc['resources'].items():
+            methods = resource_desc.get('methods', {})
+            if not methods.get('get'):
+                continue
+            method = methods['get']
+            parameters = method['parameterOrder']
+
+            # We would like to change a path like
+            # {project}/regions/{region}/addresses/{address} to a pattern like
+            # (PROJECT REGEX)/regions/(REGION REGEX)/addresses/(ADDRESS REGEX).
+            template = Template('${'.join(method['path'].split('{')))
+            mapping = {}
+            for parameter in parameters:
+                parameter_desc = method['parameters'][parameter]
+                if 'pattern' in parameter_desc:
+                    mapping[parameter] = '(%s)' % parameter_desc['pattern']
+                else:
+                    mapping[parameter] = '([^/]+)'
+            pattern = template.substitute(**mapping)
+
+            # Store the parameters and the regex pattern of this resource.
+            self._resources[resource] = {'parameters': parameters,
+                                         'pattern': re.compile(pattern)}
+
+    def parse_url(self, url):
+        """
+        Build a GCPResourceUrl from a resource's URL string. One can then call
+        the get() method on the returned object to fetch resource details from
+        GCP servers.
+
+        Example:
+            If the input url is the following
+
+            https://www.googleapis.com/compute/v1/projects/galaxy-on-gcp/regions/us-central1/subnetworks/testsubnet-2
+
+            then parse_url will return a GCPResourceURL and the parameters
+            field of the returned object will look like:
+
+            {'project': 'galaxy-on-gcp',
+             'region': 'us-central1',
+             'subnetwork': 'testsubnet-2'}
+        """
+        url = url.strip()
+        if url.startswith(self._root_url):
+            url = url[len(self._root_url):]
+        if url.startswith(self._service_path):
+            url = url[len(self._service_path):]
+
+        for resource, desc in self._resources.items():
+            m = re.match(desc['pattern'], url)
+            if m is None or len(m.group(0)) < len(url):
+                continue
+            out = GCPResourceUrl(resource, self._connection)
+            for index, parameter in enumerate(desc['parameters']):
+                out.parameters[parameter] = m.group(index + 1)
+            return out
+
+    def get_resource_url_with_default(self, resource, url_or_name, **kwargs):
+        """
+        Build a GCPResourceUrl from a service's name and resource url or name.
+        If the url_or_name is a valid GCP resource URL, then we build the
+        GCPResourceUrl object by parsing this URL. If the url_or_name is its
+        short name, then we build the GCPResourceUrl object by constructing
+        the resource URL with default project, region, zone values.
+        """
+        # If url_or_name is a valid GCP resource URL, then parse it.
+        if url_or_name.startswith(self._root_url):
+            return self.parse_url(url_or_name)
+        # Otherwise, construct resource URL with default values.
+        if resource not in self._resources:
+            log.warning('Unknown resource: %s', resource)
+            return None
+
+        parameter_defaults = self._parameter_defaults.copy()
+        parameter_defaults.update(kwargs)
+
+        parsed_url = GCPResourceUrl(resource, self._connection)
+        for key in self._resources[resource]['parameters']:
+            parsed_url.parameters[key] = parameter_defaults.get(
+                key, url_or_name)
+        return parsed_url
+
+
+class GCPCloudProvider(BaseCloudProvider):
+
+    PROVIDER_ID = 'gcp'
+
+    def __init__(self, config):
+        super(GCPCloudProvider, self).__init__(config)
+
+        # Disable warnings about file_cache not being available when using
+        # oauth2client >= 4.0.0.
+        logging.getLogger('googleapiclient.discovery_cache').setLevel(
+                logging.ERROR)
+
+        # Initialize cloud connection fields
+        self.credentials_file = self._get_config_value(
+                'gcp_service_creds_file',
+                os.getenv('GCP_SERVICE_CREDS_FILE'))
+        self.credentials_dict = self._get_config_value(
+                'gcp_service_creds_dict',
+                json.loads(os.getenv('GCP_SERVICE_CREDS_DICT', '{}')))
+        self.vm_default_user_name = self._get_config_value(
+            'gcp_vm_default_username',
+            os.getenv('GCP_VM_DEFAULT_USERNAME', "cbuser"))
+
+        # If 'gcp_service_creds_dict' is not passed in from config and
+        # self.credentials_file is available, read and parse the json file to
+        # self.credentials_dict.
+        if self.credentials_file and not self.credentials_dict:
+            with open(self.credentials_file) as creds_file:
+                self.credentials_dict = json.load(creds_file)
+        self.default_zone = self._get_config_value(
+            'gcp_default_zone',
+            os.environ.get('GCP_DEFAULT_ZONE') or 'us-central1-a')
+        self.region_name = self._get_config_value(
+            'gcp_region_name',
+            os.environ.get('GCP_DEFAULT_REGION') or 'us-central1')
+
+        if self.credentials_dict and 'project_id' in self.credentials_dict:
+            self.project_name = self.credentials_dict['project_id']
+        else:
+            self.project_name = os.environ.get('GCP_PROJECT_NAME')
+
+        # service connections, lazily initialized
+        self._gcp_compute = None
+        self._gcp_storage = None
+        self._credentials_cache = None
+        self._compute_resources_cache = None
+        self._storage_resources_cache = None
+
+        # Initialize provider services
+        self._compute = GCPComputeService(self)
+        self._security = GCPSecurityService(self)
+        self._networking = GCPNetworkingService(self)
+        self._storage = GCPStorageService(self)
+
+    @property
+    def compute(self):
+        return self._compute
+
+    @property
+    def networking(self):
+        return self._networking
+
+    @property
+    def security(self):
+        return self._security
+
+    @property
+    def storage(self):
+        return self._storage
+
+    @property
+    def gcp_compute(self):
+        if not self._gcp_compute:
+            self._gcp_compute = self._connect_gcp_compute()
+        return self._gcp_compute
+
+    @property
+    def gcp_storage(self):
+        if not self._gcp_storage:
+            self._gcp_storage = self._connect_gcp_storage()
+        return self._gcp_storage
+
+    @property
+    def _compute_resources(self):
+        if not self._compute_resources_cache:
+            self._compute_resources_cache = GCPResources(
+                    self.gcp_compute,
+                    project=self.project_name,
+                    region=self.region_name,
+                    zone=self.default_zone)
+        return self._compute_resources_cache
+
+    @property
+    def _storage_resources(self):
+        if not self._storage_resources_cache:
+            self._storage_resources_cache = GCPResources(self.gcp_storage)
+        return self._storage_resources_cache
+
+    @property
+    def _credentials(self):
+        if not self._credentials_cache:
+            if self.credentials_dict:
+                self._credentials_cache = (
+                        ServiceAccountCredentials.from_json_keyfile_dict(
+                                self.credentials_dict))
+            else:
+                self._credentials_cache = (
+                        GoogleCredentials.get_application_default())
+        return self._credentials_cache
+
+    def sign_blob(self, string_to_sign):
+        return self._credentials.sign_blob(string_to_sign)[1]
+
+    @property
+    def client_id(self):
+        return self._credentials.service_account_email
+
+    def _connect_gcp_storage(self):
+        return discovery.build('storage', 'v1', credentials=self._credentials,
+                               cache_discovery=False)
+
+    def _connect_gcp_compute(self):
+        return discovery.build('compute', 'v1', credentials=self._credentials,
+                               cache_discovery=False)
+
+    def wait_for_operation(self, operation, region=None, zone=None):
+        args = {'project': self.project_name, 'operation': operation['name']}
+        if not region and not zone:
+            operations = self.gcp_compute.globalOperations()
+        elif zone:
+            operations = self.gcp_compute.zoneOperations()
+            args['zone'] = zone
+        else:
+            operations = self.gcp_compute.regionOperations()
+            args['region'] = region
+
+        while True:
+            result = operations.get(**args).execute()
+            if result['status'] == 'DONE':
+                if 'error' in result:
+                    raise Exception(result['error'])
+                return result
+
+            time.sleep(0.5)
+
+    def parse_url(self, url):
+        out = self._compute_resources.parse_url(url)
+        return out if out else self._storage_resources.parse_url(url)
+
+    def get_resource(self, resource, url_or_name, **kwargs):
+        if not url_or_name:
+            return None
+        resource_url = (
+            self._compute_resources.get_resource_url_with_default(
+                resource, url_or_name, **kwargs) or
+            self._storage_resources.get_resource_url_with_default(
+                resource, url_or_name, **kwargs))
+        if resource_url is None:
+            return None
+        try:
+            return resource_url.get_resource()
+        except googleapiclient.errors.HttpError as http_error:
+            if http_error.resp.status in [404]:
+                # 404 = not found
+                return None
+            else:
+                raise
+
+    def authenticate(self):
+        try:
+            self.gcp_compute
+            return True
+        except Exception as e:
+            raise ProviderConnectionException(
+                'Authentication with Google cloud provider failed: %s', e)

+ 2028 - 0
cloudbridge/cloud/providers/gcp/resources.py

@@ -0,0 +1,2028 @@
+"""
+DataTypes used by this provider
+"""
+import base64
+import calendar
+import hashlib
+import inspect
+import io
+import logging
+import math
+import re
+import time
+import uuid
+from collections import namedtuple
+
+import googleapiclient
+
+from cloudbridge.cloud.base.resources import BaseAttachmentInfo
+from cloudbridge.cloud.base.resources import BaseBucket
+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 BaseInternetGateway
+from cloudbridge.cloud.base.resources import BaseKeyPair
+from cloudbridge.cloud.base.resources import BaseLaunchConfig
+from cloudbridge.cloud.base.resources import BaseMachineImage
+from cloudbridge.cloud.base.resources import BaseNetwork
+from cloudbridge.cloud.base.resources import BasePlacementZone
+from cloudbridge.cloud.base.resources import BaseRegion
+from cloudbridge.cloud.base.resources import BaseRouter
+from cloudbridge.cloud.base.resources import BaseSnapshot
+from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.base.resources import BaseVMFirewall
+from cloudbridge.cloud.base.resources import BaseVMFirewallRule
+from cloudbridge.cloud.base.resources import BaseVMType
+from cloudbridge.cloud.base.resources import BaseVolume
+from cloudbridge.cloud.interfaces.resources import GatewayState
+from cloudbridge.cloud.interfaces.resources import InstanceState
+from cloudbridge.cloud.interfaces.resources import MachineImageState
+from cloudbridge.cloud.interfaces.resources import NetworkState
+from cloudbridge.cloud.interfaces.resources import RouterState
+from cloudbridge.cloud.interfaces.resources import SnapshotState
+from cloudbridge.cloud.interfaces.resources import SubnetState
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
+from cloudbridge.cloud.interfaces.resources import VolumeState
+
+from . import helpers
+from .subservices import GCPBucketObjectSubService
+from .subservices import GCPFloatingIPSubService
+from .subservices import GCPGatewaySubService
+from .subservices import GCPSubnetSubService
+from .subservices import GCPVMFirewallRuleSubService
+
+# Older versions of Python do not have a built-in set data-structure.
+
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+log = logging.getLogger(__name__)
+
+
+class GCPKeyPair(BaseKeyPair):
+
+    KP_TAG_PREFIX = "cb_key_pair_"
+    KP_TAG_REGEX = re.compile("^" + KP_TAG_PREFIX + ".*")
+    GCPKeyInfo = namedtuple('GCPKeyInfo', ['name', 'public_key'])
+
+    def __init__(self, provider, kp_info, private_key=None):
+        super(GCPKeyPair, self).__init__(provider, None)
+        self._key_pair = kp_info
+        self._private_key = private_key
+
+    @property
+    def id(self):
+        return self._key_pair.name
+
+    @property
+    def name(self):
+        return self._key_pair.name
+
+    def delete(self):
+        self._provider.security.key_pairs.delete(self.id)
+
+    @property
+    def material(self):
+        return self._private_key
+
+
+class GCPVMType(BaseVMType):
+    def __init__(self, provider, instance_dict):
+        super(GCPVMType, self).__init__(provider)
+        self._inst_dict = instance_dict
+
+    @property
+    def resource_url(self):
+        return self._inst_dict.get('selfLink')
+
+    @property
+    def id(self):
+        return self._inst_dict.get('selfLink')
+
+    @property
+    def name(self):
+        return self._inst_dict.get('name')
+
+    @property
+    def family(self):
+        return self._inst_dict.get('kind')
+
+    @property
+    def vcpus(self):
+        return self._inst_dict.get('guestCpus')
+
+    @property
+    def ram(self):
+        return float("{0:.2f}".format(self._inst_dict.get('memoryMb') / 1024))
+
+    @property
+    def size_root_disk(self):
+        return 0
+
+    @property
+    def size_ephemeral_disks(self):
+        return int(self._inst_dict.get('maximumPersistentDisksSizeGb'))
+
+    @property
+    def num_ephemeral_disks(self):
+        return self._inst_dict.get('maximumPersistentDisks')
+
+    @property
+    def extra_data(self):
+        return {key: val for key, val in self._inst_dict.items()
+                if key not in ['id', 'name', 'kind', 'guestCpus', 'memoryMb',
+                               'maximumPersistentDisksSizeGb',
+                               'maximumPersistentDisks']}
+
+
+class GCPPlacementZone(BasePlacementZone):
+
+    def __init__(self, provider, zone):
+        super(GCPPlacementZone, self).__init__(provider)
+        self._zone = zone
+
+    @property
+    def id(self):
+        """
+        Get the zone id
+        :rtype: ``str``
+        :return: ID for this zone as returned by the cloud middleware.
+        """
+        return self._zone['selfLink']
+
+    @property
+    def name(self):
+        """
+        Get the zone name.
+        :rtype: ``str``
+        :return: Name for this zone as returned by the cloud middleware.
+        """
+        return self._zone['name']
+
+    @property
+    def region_name(self):
+        """
+        Get the region that this zone belongs to.
+        :rtype: ``str``
+        :return: Name of this zone's region as returned by the cloud middleware
+        """
+        parsed_region_url = self._provider.parse_url(self._zone['region'])
+        return parsed_region_url.parameters['region']
+
+
+class GCPRegion(BaseRegion):
+
+    def __init__(self, provider, gcp_region):
+        super(GCPRegion, self).__init__(provider)
+        self._gcp_region = gcp_region
+
+    @property
+    def id(self):
+        return self._gcp_region.get('selfLink')
+
+    @property
+    def name(self):
+        return self._gcp_region.get('name')
+
+    @property
+    def zones(self):
+        """
+        Accesss information about placement zones within this region.
+        """
+        zones_response = (self._provider
+                              .gcp_compute
+                              .zones()
+                              .list(project=self._provider.project_name)
+                              .execute())
+        zones = [zone for zone in zones_response['items']
+                 if zone['region'] == self._gcp_region['selfLink']]
+        return [GCPPlacementZone(self._provider, zone) for zone in zones]
+
+
+class GCPFirewallsDelegate(object):
+    _NETWORK_URL_PREFIX = 'global/networks/'
+
+    def __init__(self, provider):
+        self._provider = provider
+        self._list_response = None
+
+    @staticmethod
+    def tag_network_id(tag, network_name):
+        """
+        Generate an ID for a (tag, network name) pair.
+        """
+        md5 = hashlib.md5()
+        md5.update("{0}-{1}".format(tag, network_name).encode('ascii'))
+        return md5.hexdigest()
+
+    @property
+    def provider(self):
+        return self._provider
+
+    @property
+    def tag_networks(self):
+        """
+        List all (tag, network name) pairs that are in at least one firewall.
+        """
+        out = set()
+        for firewall in self.iter_firewalls():
+            network_name = self.network_name(firewall)
+            if network_name is not None:
+                out.add((firewall['targetTags'][0], network_name))
+        return out
+
+    def network_name(self, firewall):
+        """
+        Extract the network name of a firewall.
+        """
+        if 'network' not in firewall:
+            return GCPNetwork.CB_DEFAULT_NETWORK_LABEL
+        url = self._provider.parse_url(firewall['network'])
+        return url.parameters['network']
+
+    def get_tag_network_from_id(self, tag_network_id):
+        """
+        Map an ID back to the (tag, network name) pair.
+        """
+        for tag, network_name in self.tag_networks:
+            current_id = GCPFirewallsDelegate.tag_network_id(tag, network_name)
+            if current_id == tag_network_id:
+                return (tag, network_name)
+        return (None, None)
+
+    def delete_tag_network_with_id(self, tag_network_id):
+        """
+        Delete all firewalls in a given network with a specific target tag.
+        """
+        tag, network_name = self.get_tag_network_from_id(tag_network_id)
+        if tag is None:
+            return
+        for firewall in self.iter_firewalls(tag, network_name):
+            self._delete_firewall(firewall)
+        self._update_list_response()
+
+    def add_firewall(self, tag, direction, protocol, priority, port,
+                     src_dest_range, src_dest_tag, description, network_name):
+        """
+        Create a new firewall.
+        """
+        if self.find_firewall(
+                tag, direction, protocol, port, src_dest_range, src_dest_tag,
+                network_name) is not None:
+            return True
+        # Do not let the user accidentally open traffic from the world by not
+        # explicitly specifying the source.
+        if src_dest_tag is None and src_dest_range is None:
+            return False
+        firewall = {
+            'name': 'firewall-{0}'.format(uuid.uuid4()),
+            'network': GCPFirewallsDelegate._NETWORK_URL_PREFIX + network_name,
+            'allowed': [{'IPProtocol': str(protocol)}],
+            'targetTags': [tag]}
+        if description is not None:
+            firewall['description'] = description
+        if port is not None:
+            firewall['allowed'][0]['ports'] = [port]
+        if direction == TrafficDirection.INBOUND:
+            firewall['direction'] = 'INGRESS'
+            src_dest_str = 'source'
+        else:
+            firewall['direction'] = 'EGRESS'
+            src_dest_str = 'destination'
+        if src_dest_range is not None:
+            firewall[src_dest_str + 'Ranges'] = [src_dest_range]
+        if src_dest_tag is not None:
+            if direction == TrafficDirection.OUTBOUND:
+                log.warning('GCP does not support egress rules to network '
+                            'tags. Only IP ranges are acceptable.')
+            else:
+                firewall['sourceTags'] = [src_dest_tag]
+        if priority is not None:
+            firewall['priority'] = priority
+        project_name = self._provider.project_name
+        try:
+            response = (self._provider
+                            .gcp_compute
+                            .firewalls()
+                            .insert(project=project_name,
+                                    body=firewall)
+                            .execute())
+            self._provider.wait_for_operation(response)
+            # TODO: process the response and handle errors.
+        finally:
+            self._update_list_response()
+        return True
+
+    def find_firewall(self, tag, direction, protocol, port, src_dest_range,
+                      src_dest_tag, network_name):
+        """
+        Find a firewall with give parameters.
+        """
+        if src_dest_range is None and src_dest_tag is None:
+            src_dest_range = '0.0.0.0/0'
+        if direction == TrafficDirection.INBOUND:
+            src_dest_str = 'source'
+        else:
+            src_dest_str = 'destination'
+        for firewall in self.iter_firewalls(tag, network_name):
+            if firewall['allowed'][0]['IPProtocol'] != protocol:
+                continue
+            if not self._check_list_in_dict(firewall['allowed'][0], 'ports',
+                                            port):
+                continue
+            if not self._check_list_in_dict(firewall, src_dest_str + 'Ranges',
+                                            src_dest_range):
+                continue
+            if not self._check_list_in_dict(firewall, src_dest_str + 'Tags',
+                                            src_dest_tag):
+                continue
+            return firewall['id']
+        return None
+
+    def get_firewall_info(self, firewall_id):
+        """
+        Extract firewall properties to into a dictionary for easy of use.
+        """
+        info = {}
+        for firewall in self.iter_firewalls():
+            if firewall['id'] != firewall_id:
+                continue
+            if ('sourceRanges' in firewall and
+                    len(firewall['sourceRanges']) == 1):
+                info['src_dest_range'] = firewall['sourceRanges'][0]
+            elif ('destinationRanges' in firewall and
+                    len(firewall['destinationRanges']) == 1):
+                info['src_dest_range'] = firewall['destinationRanges'][0]
+            if 'sourceTags' in firewall and len(firewall['sourceTags']) == 1:
+                info['src_dest_tag'] = firewall['sourceTags'][0]
+            if 'targetTags' in firewall and len(firewall['targetTags']) == 1:
+                info['target_tag'] = firewall['targetTags'][0]
+            if 'IPProtocol' in firewall['allowed'][0]:
+                info['protocol'] = firewall['allowed'][0]['IPProtocol']
+            if ('ports' in firewall['allowed'][0] and
+                    len(firewall['allowed'][0]['ports']) == 1):
+                info['port'] = firewall['allowed'][0]['ports'][0]
+            info['network_name'] = self.network_name(firewall)
+            if 'direction' in firewall:
+                info['direction'] = firewall['direction']
+            if 'priority' in firewall:
+                info['priority'] = firewall['priority']
+            return info
+        return info
+
+    def delete_firewall_id(self, firewall_id):
+        """
+        Delete a firewall with a given ID.
+        """
+        for firewall in self.iter_firewalls():
+            if firewall['id'] == firewall_id:
+                self._delete_firewall(firewall)
+        self._update_list_response()
+
+    def iter_firewalls(self, tag=None, network_name=None):
+        """
+        Iterate through all firewalls. Can optionally iterate through firewalls
+        with a given tag and/or in a network.
+        """
+        if self._list_response is None:
+            self._update_list_response()
+        for firewall in self._list_response:
+            if ('targetTags' not in firewall or
+                    len(firewall['targetTags']) != 1):
+                continue
+            if 'allowed' not in firewall or len(firewall['allowed']) != 1:
+                continue
+            if tag is not None and firewall['targetTags'][0] != tag:
+                continue
+            if network_name is None:
+                yield firewall
+                continue
+            firewall_network_name = self.network_name(firewall)
+            if firewall_network_name == network_name:
+                yield firewall
+
+    def _delete_firewall(self, firewall):
+        """
+        Delete a given firewall.
+        """
+        project_name = self._provider.project_name
+        name = firewall['name']
+        response = (self._provider
+                        .gcp_compute
+                        .firewalls()
+                        .delete(project=project_name,
+                                firewall=name)
+                        .execute())
+        self._provider.wait_for_operation(response)
+        # TODO: process the response and handle errors.
+        tag_name = "_".join(["firewall", name, "label"])
+        if not helpers.remove_metadata_item(self._provider, tag_name):
+            log.warning('No label was found associated with this firewall '
+                        '"{}" when deleted.'.format(name))
+        return True
+
+    def _update_list_response(self):
+        """
+        Sync the local cache of all firewalls with the server.
+        """
+        self._list_response = list(
+                helpers.iter_all(self._provider.gcp_compute.firewalls(),
+                                 project=self._provider.project_name))
+
+    def _check_list_in_dict(self, dictionary, field_name, value):
+        """
+        Verify that a given field in a dictionary is a singlton list [value].
+        """
+        if field_name not in dictionary:
+            return value is None
+        if (value is None or len(dictionary[field_name]) != 1 or
+                dictionary[field_name][0] != value):
+            return False
+        return True
+
+
+class GCPVMFirewall(BaseVMFirewall):
+
+    def __init__(self, delegate, tag, network=None, description=None):
+        super(GCPVMFirewall, self).__init__(delegate.provider, tag)
+        self._delegate = delegate
+        self._description = description
+        if network is None:
+            self._network = (delegate.provider.networking.networks
+                             .get_or_create_default())
+        else:
+            self._network = network
+        self._rule_container = GCPVMFirewallRuleSubService(self._provider,
+                                                           self)
+
+    @property
+    def id(self):
+        """
+        Return the ID of this VM firewall which is determined based on the
+        network and the target tag corresponding to this VM firewall.
+        """
+        return GCPFirewallsDelegate.tag_network_id(self._vm_firewall,
+                                                   self._network.name)
+
+    @property
+    def name(self):
+        """
+        Return the name of the VM firewall which is the same as the
+        corresponding tag name.
+        """
+        return self._vm_firewall
+
+    @property
+    def label(self):
+        tag_name = "_".join(["firewall", self.name, "label"])
+        return helpers.get_metadata_item_value(self._provider, tag_name)
+
+    @label.setter
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        tag_name = "_".join(["firewall", self.name, "label"])
+        helpers.modify_or_add_metadata_item(self._provider, tag_name, value)
+
+    @property
+    def description(self):
+        """
+        The description of the VM firewall is even explicitly given when the
+        VM firewall is created or is determined from a VM firewall rule, i.e. a
+        GCP firewall, in the VM firewall.
+
+        If the GCP firewalls are created using this API, they all have the same
+        description.
+        """
+        if self._description is None:
+            for firewall in self._delegate.iter_firewalls(self._vm_firewall,
+                                                          self._network.name):
+                if 'description' in firewall:
+                    self._description = firewall['description']
+        if self._description is None:
+            self._description = ''
+        return self._description
+
+    @description.setter
+    def description(self, value):
+        # Change the description on all rules
+        for fw in self._delegate.iter_firewalls(self._vm_firewall,
+                                                self._network.name):
+            fw['description'] = value or ''
+            response = (self._provider
+                        .gcp_compute
+                        .firewalls()
+                        .update(project=self._provider.project_name,
+                                firewall=fw['name'],
+                                body=fw)
+                        .execute())
+            self._provider.wait_for_operation(response)
+        # Set back to None so that the next time the user gets it, it updates
+        # but don't force update here to avoid more overhead
+        self._description = None
+
+    @property
+    def network_id(self):
+        return self._network.id
+
+    @property
+    def rules(self):
+        return self._rule_container
+
+    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('_')}
+        json_rules = [r.to_json() for r in self.rules]
+        js['rules'] = json_rules
+        return js
+
+    def refresh(self):
+        fw = self._provider.security.vm_firewalls.get(self.id)
+        # restore all internal state
+        if fw:
+            # pylint:disable=protected-access
+            self._delegate = fw._delegate
+            # pylint:disable=protected-access
+            self._description = fw._description
+            # pylint:disable=protected-access
+            self._network = fw._network
+            # pylint:disable=protected-access
+            self._rule_container = fw._rule_container
+
+    @property
+    def network(self):
+        return self._network
+
+    @property
+    def delegate(self):
+        return self._delegate
+
+
+class GCPVMFirewallRule(BaseVMFirewallRule):
+
+    def __init__(self, parent_fw, rule):
+        super(GCPVMFirewallRule, self).__init__(parent_fw, rule)
+
+    @property
+    def id(self):
+        return self._rule
+
+    @property
+    def direction(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None:
+            return None
+        if 'direction' in info and info['direction'] == 'EGRESS':
+            return TrafficDirection.OUTBOUND
+        return TrafficDirection.INBOUND
+
+    @property
+    def protocol(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'protocol' not in info:
+            return None
+        return info['protocol']
+
+    @property
+    def from_port(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'port' not in info:
+            return 0
+        port = info['port']
+        if port.isdigit():
+            return int(port)
+        parts = port.split('-')
+        if len(parts) > 2 or len(parts) < 1:
+            return 0
+        if parts[0].isdigit():
+            return int(parts[0])
+        return 0
+
+    @property
+    def to_port(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'port' not in info:
+            return 0
+        port = info['port']
+        if port.isdigit():
+            return int(port)
+        parts = port.split('-')
+        if len(parts) > 2 or len(parts) < 1:
+            return 0
+        if parts[-1].isdigit():
+            return int(parts[-1])
+        return 0
+
+    @property
+    def cidr(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'src_dest_range' not in info:
+            return None
+        return info['src_dest_range']
+
+    @property
+    def src_dest_fw_id(self):
+        """
+        Return the VM firewall given access by this rule.
+        """
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'src_dest_tag' not in info:
+            return None
+        return GCPFirewallsDelegate.tag_network_id(info['src_dest_tag'],
+                                                   self.firewall.network.name)
+
+    @property
+    def src_dest_fw(self):
+        """
+        Return the VM firewall given access by this rule.
+        """
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'src_dest_tag' not in info:
+            return None
+        return GCPVMFirewall(
+                self.firewall.delegate, info['src_dest_tag'],
+                self.firewall.network)
+
+    @property
+    def priority(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        # The default firewall rule priority, when not specified, is 1000.
+        if info is None or 'priority' not in info:
+            return 1000
+        return info['priority']
+
+    def is_dummy_rule(self):
+        if self.priority != 65534:
+            return False
+        if self.direction != TrafficDirection.OUTBOUND:
+            return False
+        if self.protocol != 'tcp':
+            return False
+        if self.cidr != '0.0.0.0/0':
+            return False
+        return True
+
+
+class GCPMachineImage(BaseMachineImage):
+
+    IMAGE_STATE_MAP = {
+        'PENDING': MachineImageState.PENDING,
+        'READY': MachineImageState.AVAILABLE,
+        'FAILED': MachineImageState.ERROR
+    }
+
+    def __init__(self, provider, image):
+        super(GCPMachineImage, self).__init__(provider)
+        if isinstance(image, GCPMachineImage):
+            # pylint:disable=protected-access
+            self._gcp_image = image._gcp_image
+        else:
+            self._gcp_image = image
+
+    @property
+    def resource_url(self):
+        return self._gcp_image.get('selfLink')
+
+    @property
+    def id(self):
+        """
+        Get the image identifier.
+        :rtype: ``str``
+        :return: ID for this instance as returned by the cloud middleware.
+        """
+        return self._gcp_image.get('selfLink')
+
+    @property
+    def name(self):
+        """
+        Get the image name.
+        :rtype: ``str``
+        :return: Name for this image as returned by the cloud middleware.
+        """
+        return self._gcp_image['name']
+
+    @property
+    def label(self):
+        labels = self._gcp_image.get('labels')
+        return labels.get('cblabel', '') if labels else ''
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        req = (self._provider
+                   .gcp_compute
+                   .images()
+                   .setLabels(project=self._provider.project_name,
+                              resource=self.name,
+                              body={}))
+
+        helpers.change_label(self, 'cblabel', value, '_gcp_image', req)
+
+    @property
+    def description(self):
+        """
+        Get the image description.
+        :rtype: ``str``
+        :return: Description for this image as returned by the cloud middleware
+        """
+        return self._gcp_image.get('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
+        """
+        return int(math.ceil(float(self._gcp_image.get('diskSizeGb'))))
+
+    def delete(self):
+        """
+        Delete this image
+        """
+        (self._provider
+             .gcp_compute
+             .images()
+             .delete(project=self._provider.project_name,
+                     image=self.name)
+             .execute())
+
+    @property
+    def state(self):
+        return GCPMachineImage.IMAGE_STATE_MAP.get(
+            self._gcp_image['status'], MachineImageState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this instance by re-querying the cloud provider
+        for its latest state.
+        """
+        image = self._provider.compute.images.get(self.id)
+        if image:
+            # pylint:disable=protected-access
+            self._gcp_image = image._gcp_image
+        else:
+            # image no longer exists
+            self._gcp_image['status'] = MachineImageState.UNKNOWN
+
+
+class GCPInstance(BaseInstance):
+    # https://cloud.google.com/compute/docs/reference/latest/instances
+    # The status of the instance. One of the following values:
+    # PROVISIONING, STAGING, RUNNING, STOPPING, SUSPENDING, SUSPENDED,
+    # and TERMINATED.
+    INSTANCE_STATE_MAP = {
+        'PROVISIONING': InstanceState.PENDING,
+        'STAGING': InstanceState.PENDING,
+        'RUNNING': InstanceState.RUNNING,
+        'STOPPING': InstanceState.CONFIGURING,
+        'TERMINATED': InstanceState.STOPPED,
+        'SUSPENDING': InstanceState.CONFIGURING,
+        'SUSPENDED': InstanceState.STOPPED
+    }
+
+    def __init__(self, provider, gcp_instance):
+        super(GCPInstance, self).__init__(provider)
+        self._gcp_instance = gcp_instance
+        self._inet_gateway = None
+
+    @property
+    def resource_url(self):
+        return self._gcp_instance.get('selfLink')
+
+    @property
+    def id(self):
+        """
+        Get the instance identifier.
+
+        A GCP instance is uniquely identified by its selfLink, which is used
+        as its id.
+        """
+        return self._gcp_instance.get('selfLink')
+
+    @property
+    def name(self):
+        """
+        Get the instance name.
+        """
+        return self._gcp_instance['name']
+
+    @property
+    def label(self):
+        labels = self._gcp_instance.get('labels')
+        return labels.get('cblabel', '') if labels else ''
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        req = (self._provider
+                   .gcp_compute
+                   .instances()
+                   .setLabels(project=self._provider.project_name,
+                              zone=self.zone_name,
+                              instance=self.name,
+                              body={}))
+
+        helpers.change_label(self, 'cblabel', value, '_gcp_instance', req)
+
+    @property
+    def public_ips(self):
+        """
+        Get all the public IP addresses for this instance.
+        """
+        ips = []
+        network_interfaces = self._gcp_instance.get('networkInterfaces')
+        if network_interfaces is not None and len(network_interfaces) > 0:
+            access_configs = network_interfaces[0].get('accessConfigs')
+            if access_configs is not None and len(access_configs) > 0:
+                # https://cloud.google.com/compute/docs/reference/beta/instances
+                # An array of configurations for this interface. Currently,
+                # only one access config, ONE_TO_ONE_NAT, is supported. If
+                # there are no accessConfigs specified, then this instance will
+                # have no external internet access.
+                access_config = access_configs[0]
+                if 'natIP' in access_config:
+                    ips.append(access_config['natIP'])
+        for ip in self.inet_gateway.floating_ips:
+            if ip.in_use:
+                if ip.private_ip in self.private_ips:
+                    ips.append(ip.public_ip)
+        return ips
+
+    @property
+    def private_ips(self):
+        """
+        Get all the private IP addresses for this instance.
+        """
+        network_interfaces = self._gcp_instance.get('networkInterfaces')
+        if network_interfaces is None or len(network_interfaces) == 0:
+            return []
+        if 'networkIP' in network_interfaces[0]:
+            return [network_interfaces[0]['networkIP']]
+        else:
+            return []
+
+    @property
+    def vm_type_id(self):
+        """
+        Get the instance type name.
+        """
+        return self._gcp_instance.get('machineType')
+
+    @property
+    def vm_type(self):
+        """
+        Get the instance type.
+        """
+        machine_type_uri = self._gcp_instance.get('machineType')
+        if machine_type_uri is None:
+            return None
+        parsed_uri = self._provider.parse_url(machine_type_uri)
+        return GCPVMType(self._provider, parsed_uri.get_resource())
+
+    @property
+    def subnet_id(self):
+        """
+        Get the zone for this instance.
+        """
+        return (self._gcp_instance.get('networkInterfaces', [{}])[0]
+                .get('subnetwork'))
+
+    def reboot(self):
+        """
+        Reboot this instance.
+        """
+        if self.state == InstanceState.STOPPED:
+            (self._provider
+             .gcp_compute
+             .instances()
+             .start(project=self._provider.project_name,
+                    zone=self.zone_name,
+                    instance=self.name)
+             .execute())
+        else:
+            (self._provider
+             .gcp_compute
+             .instances()
+             .reset(project=self._provider.project_name,
+                    zone=self.zone_name,
+                    instance=self.name)
+             .execute())
+
+    def stop(self):
+        """
+        Stop this instance.
+        """
+        (self._provider
+         .gcp_compute
+         .instances()
+         .stop(project=self._provider.project_name,
+               zone=self.zone_name,
+               instance=self.name)
+         .execute())
+
+    @property
+    def image_id(self):
+        """
+        Get the image ID for this insance.
+        """
+        if 'disks' not in self._gcp_instance:
+            return None
+        for disk in self._gcp_instance['disks']:
+            if 'boot' in disk and disk['boot']:
+                disk_url = self._provider.parse_url(disk['source'])
+                return disk_url.get_resource().get('sourceImage')
+        return None
+
+    @property
+    def zone_id(self):
+        """
+        Get the placement zone id where this instance is running.
+        """
+        return self._gcp_instance.get('zone')
+
+    @property
+    def zone_name(self):
+        return self._provider.parse_url(self.zone_id).parameters['zone']
+
+    @property
+    def vm_firewalls(self):
+        """
+        Get the VM firewalls associated with this instance.
+        """
+        network_url = self._gcp_instance.get('networkInterfaces')[0].get(
+            'network')
+        url = self._provider.parse_url(network_url)
+        network_name = url.parameters['network']
+        if 'items' not in self._gcp_instance['tags']:
+            return []
+        tags = self._gcp_instance['tags']['items']
+        # Tags are mapped to non-empty VM firewalls under the instance network.
+        # Unmatched tags are ignored.
+        sgs = (self._provider.security
+               .vm_firewalls.find_by_network_and_tags(
+                   network_name, tags))
+        return sgs
+
+    @property
+    def vm_firewall_ids(self):
+        """
+        Get the VM firewall IDs associated with this instance.
+        """
+        sg_ids = []
+        for sg in self.vm_firewalls:
+            sg_ids.append(sg.id)
+        return sg_ids
+
+    @property
+    def key_pair_id(self):
+        """
+        Get the id of the key pair associated with this instance.
+        Assume there is only 1 key pair
+        """
+        # Get instance again to avoid stale metadata
+        ins = self._provider.compute.instances.get(self.id)
+        # pylint:disable=protected-access
+        meta = ins._gcp_instance.get('metadata', {})
+        if meta:
+            items = meta.get("items", [])
+            for item in items:
+                if item.get("key") == "ssh-keys":
+                    # The key pair name/id is stored last, after the public key
+                    return item.get("value").split(" ")[-1]
+        return None
+
+    @property
+    def inet_gateway(self):
+        if self._inet_gateway:
+            return self._inet_gateway
+        network_url = self._gcp_instance.get('networkInterfaces')[0].get(
+            'network')
+        network = self._provider.networking.networks.get(network_url)
+        self._inet_gateway = network.gateways.get_or_create()
+        return self._inet_gateway
+
+    def create_image(self, label):
+        """
+        Create a new image based on this instance.
+        """
+        self.assert_valid_resource_label(label)
+        name = self._generate_name_from_label(label, 'cb-img')
+        if 'disks' not in self._gcp_instance:
+            log.error('Failed to create image: no disks found.')
+            return
+        for disk in self._gcp_instance['disks']:
+            if 'boot' in disk and disk['boot']:
+                image_body = {
+                    'name': name,
+                    'sourceDisk': disk['source'],
+                    'labels': {'cblabel': label.replace(' ', '_').lower()},
+                }
+                operation = (self._provider
+                             .gcp_compute
+                             .images()
+                             .insert(project=self._provider.project_name,
+                                     body=image_body,
+                                     forceCreate=True)
+                             .execute())
+                self._provider.wait_for_operation(operation)
+                img = self._provider.get_resource('images', name)
+                return GCPMachineImage(self._provider, img) if img else None
+        log.error('Failed to create image: no boot disk found.')
+
+    def _get_existing_target_instance(self):
+        """
+        Return the target instance corresponding to this instance.
+
+        If there is no target instance for this instance, return None.
+        """
+        try:
+            for target_instance in helpers.iter_all(
+                    self._provider.gcp_compute.targetInstances(),
+                    project=self._provider.project_name,
+                    zone=self.zone_name):
+                url = self._provider.parse_url(target_instance['instance'])
+                if url.parameters['instance'] == self.name:
+                    return target_instance
+        except Exception as e:
+            log.warning('Exception while listing target instances: %s', e)
+        return None
+
+    def _get_target_instance(self):
+        """
+        Return the target instance corresponding to this instance.
+
+        If there is no target instance for this instance, create one.
+        """
+        existing_target_instance = self._get_existing_target_instance()
+        if existing_target_instance:
+            return existing_target_instance
+
+        # No targetInstance exists for this instance. Create one.
+        body = {'name': 'target-instance-{0}'.format(uuid.uuid4()),
+                'instance': self._gcp_instance['selfLink']}
+        try:
+            response = (self._provider
+                            .gcp_compute
+                            .targetInstances()
+                            .insert(project=self._provider.project_name,
+                                    zone=self.zone_name,
+                                    body=body)
+                            .execute())
+            self._provider.wait_for_operation(response, zone=self.zone_name)
+        except Exception as e:
+            log.warning('Exception while inserting a target instance: %s', e)
+            return None
+
+        # The following method should find the target instance that we
+        # successfully created above.
+        return self._get_existing_target_instance()
+
+    def _redirect_existing_rule(self, ip, target_instance):
+        """
+        Redirect the forwarding rule of the given IP to the given Instance.
+        """
+        new_zone = (self._provider.parse_url(target_instance['zone'])
+                                  .parameters['zone'])
+        new_name = target_instance['name']
+        new_url = target_instance['selfLink']
+        try:
+            for rule in helpers.iter_all(
+                    self._provider.gcp_compute.forwardingRules(),
+                    project=self._provider.project_name,
+                    region=ip.region_name):
+                if rule['IPAddress'] != ip.public_ip:
+                    continue
+                parsed_target_url = self._provider.parse_url(rule['target'])
+                old_zone = parsed_target_url.parameters['zone']
+                old_name = parsed_target_url.parameters['targetInstance']
+                if old_zone == new_zone and old_name == new_name:
+                    return True
+                response = (self._provider
+                                .gcp_compute
+                                .forwardingRules()
+                                .setTarget(
+                                    project=self._provider.project_name,
+                                    region=ip.region_name,
+                                    forwardingRule=rule['name'],
+                                    body={'target': new_url})
+                                .execute())
+                self._provider.wait_for_operation(response,
+                                                  region=ip.region_name)
+                return True
+        except Exception as e:
+            log.warning(
+                'Exception while listing/changing forwarding rules: %s', e)
+        return False
+
+    def _forward(self, ip, target_instance):
+        """
+        Forward the traffic to a given IP to a given instance.
+
+        If there is already a forwarding rule for the IP, it is redirected;
+        otherwise, a new forwarding rule is created.
+        """
+        if self._redirect_existing_rule(ip, target_instance):
+            return True
+        body = {'name': 'forwarding-rule-{0}'.format(uuid.uuid4()),
+                'IPAddress': ip.public_ip,
+                'target': target_instance['selfLink']}
+        try:
+            response = (self._provider
+                            .gcp_compute
+                            .forwardingRules()
+                            .insert(project=self._provider.project_name,
+                                    region=ip.region_name,
+                                    body=body)
+                            .execute())
+            self._provider.wait_for_operation(response, region=ip.region_name)
+        except Exception as e:
+            log.warning('Exception while inserting a forwarding rule: %s', e)
+            return False
+        return True
+
+    def _delete_existing_rule(self, ip, target_instance):
+        """
+        Stop forwarding traffic to an instance by deleting the forwarding rule.
+        """
+        zone = (self._provider.parse_url(target_instance['zone'])
+                              .parameters['zone'])
+        name = target_instance['name']
+        try:
+            for rule in helpers.iter_all(
+                    self._provider.gcp_compute.forwardingRules(),
+                    project=self._provider.project_name,
+                    region=ip.region_name):
+                if rule['IPAddress'] != ip.public_ip:
+                    continue
+                parsed_target_url = self._provider.parse_url(rule['target'])
+                temp_zone = parsed_target_url.parameters['zone']
+                temp_name = parsed_target_url.parameters['targetInstance']
+                if temp_zone != zone or temp_name != name:
+                    log.warning(
+                            '"%s" is forwarded to "%s" in zone "%s"',
+                            ip.public_ip, temp_name, temp_zone)
+                    return False
+                response = (self._provider
+                                .gcp_compute
+                                .forwardingRules()
+                                .delete(
+                                    project=self._provider.project_name,
+                                    region=ip.region_name,
+                                    forwardingRule=rule['name'])
+                                .execute())
+                self._provider.wait_for_operation(response,
+                                                  region=ip.region_name)
+                return True
+        except Exception as e:
+            log.warning(
+                'Exception while listing/deleting forwarding rules: %s', e)
+            return False
+        return True
+
+    def add_floating_ip(self, floating_ip):
+        """
+        Add an elastic IP address to this instance.
+        """
+        fip = (floating_ip if isinstance(floating_ip, GCPFloatingIP)
+               else self.inet_gateway.floating_ips.get(floating_ip))
+        if fip.in_use:
+            if fip.private_ip not in self.private_ips:
+                log.warning('Floating IP "%s" is not associated to "%s"',
+                            fip.public_ip, self.name)
+            return
+        target_instance = self._get_target_instance()
+        if not target_instance:
+            log.warning('Could not create a targetInstance for "%s"',
+                        self.name)
+            return
+        if not self._forward(fip, target_instance):
+            log.warning('Could not forward "%s" to "%s"',
+                        fip.public_ip, target_instance['selfLink'])
+
+    def remove_floating_ip(self, floating_ip):
+        """
+        Remove a elastic IP address from this instance.
+        """
+        fip = (floating_ip if isinstance(floating_ip, GCPFloatingIP)
+               else self.inet_gateway.floating_ips.get(floating_ip))
+        if not fip.in_use or fip.private_ip not in self.private_ips:
+            log.warning('Floating IP "%s" is not associated to "%s"',
+                        fip.public_ip, self.name)
+            return
+        target_instance = self._get_target_instance()
+        if not target_instance:
+            # We should not be here.
+            log.warning('Something went wrong! "%s" is associated to "%s" '
+                        'with no target instance', fip.public_ip, self.name)
+            return
+        if not self._delete_existing_rule(fip, target_instance):
+            log.warning(
+                'Could not remove floating IP "%s" from instance "%s"',
+                fip.public_ip, self.name)
+
+    @property
+    def state(self):
+        return GCPInstance.INSTANCE_STATE_MAP.get(
+            self._gcp_instance['status'], InstanceState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this instance by re-querying the cloud provider
+        for its latest state.
+        """
+        inst = self._provider.compute.instances.get(self.id)
+        if inst:
+            # pylint:disable=protected-access
+            self._gcp_instance = inst._gcp_instance
+        else:
+            # instance no longer exists
+            self._gcp_instance['status'] = InstanceState.UNKNOWN
+
+    def add_vm_firewall(self, sg):
+        tag = sg.name if isinstance(sg, GCPVMFirewall) else sg
+        tags = self._gcp_instance.get('tags', {}).get('items', [])
+        tags.append(tag)
+        self._set_tags(tags)
+
+    def remove_vm_firewall(self, sg):
+        tag = sg.name if isinstance(sg, GCPVMFirewall) else sg
+        tags = self._gcp_instance.get('tags', {}).get('items', [])
+        if tag in tags:
+            tags.remove(tag)
+            self._set_tags(tags)
+
+    def _set_tags(self, tags):
+        # Refresh to make sure we are using the most recent tags fingerprint.
+        self.refresh()
+        fingerprint = self._gcp_instance.get('tags', {}).get('fingerprint', '')
+        response = (self._provider
+                        .gcp_compute
+                        .instances()
+                        .setTags(project=self._provider.project_name,
+                                 zone=self.zone_name,
+                                 instance=self.name,
+                                 body={'items': tags,
+                                       'fingerprint': fingerprint})
+                        .execute())
+        self._provider.wait_for_operation(response, zone=self.zone_name)
+
+
+class GCPNetwork(BaseNetwork):
+
+    def __init__(self, provider, network):
+        super(GCPNetwork, self).__init__(provider)
+        self._network = network
+        self._gateway_container = GCPGatewaySubService(provider, self)
+        self._subnet_svc = GCPSubnetSubService(provider, self)
+
+    @property
+    def resource_url(self):
+        return self._network['selfLink']
+
+    @property
+    def id(self):
+        return self._network['selfLink']
+
+    @property
+    def name(self):
+        return self._network['name']
+
+    @property
+    def label(self):
+        tag_name = "_".join(["network", self.name, "label"])
+        return helpers.get_metadata_item_value(self._provider, tag_name)
+
+    @label.setter
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        tag_name = "_".join(["network", self.name, "label"])
+        helpers.modify_or_add_metadata_item(self._provider, tag_name, value)
+
+    @property
+    def external(self):
+        """
+        All GCP networks can be connected to the Internet.
+        """
+        return True
+
+    @property
+    def state(self):
+        """
+        When a GCP network created by the CloudBridge API, we wait until the
+        network is ready.
+        """
+        if self._network.get('status') == NetworkState.UNKNOWN:
+            return NetworkState.UNKNOWN
+        return NetworkState.AVAILABLE
+
+    @property
+    def cidr_block(self):
+        if 'IPv4Range' in self._network:
+            # This is a legacy network.
+            return self._network['IPv4Range']
+        return GCPNetwork.CB_DEFAULT_IPV4RANGE
+
+    @property
+    def subnets(self):
+        return self._subnet_svc
+
+    def refresh(self):
+        net = self._provider.networking.networks.get(self.id)
+        if net:
+            # pylint:disable=protected-access
+            self._network = net._network
+        else:
+            # network no longer exists
+            self._network['status'] = NetworkState.UNKNOWN
+
+    @property
+    def gateways(self):
+        return self._gateway_container
+
+
+class GCPFloatingIP(BaseFloatingIP):
+    _DEAD_INSTANCE = 'dead instance'
+
+    def __init__(self, provider, floating_ip):
+        super(GCPFloatingIP, self).__init__(provider)
+        self._ip = floating_ip
+        self._process_ip_users()
+
+    @property
+    def id(self):
+        return self._ip['selfLink']
+
+    @property
+    def region_name(self):
+        # We use regional IPs to simulate floating IPs not global IPs because
+        # global IPs can be forwarded only to load balancing resources, not to
+        # a specific instance. Find out the region to which the IP belongs.
+        url = self._provider.parse_url(self._ip['region'])
+        return url.parameters['region']
+
+    @property
+    def public_ip(self):
+        return self._ip.get('address')
+
+    @property
+    def private_ip(self):
+        if (not self._target_instance or
+                self._target_instance == GCPFloatingIP._DEAD_INSTANCE):
+            return None
+        return self._target_instance['networkInterfaces'][0]['networkIP']
+
+    @property
+    def in_use(self):
+        return True if self._target_instance else False
+
+    def refresh(self):
+        # pylint:disable=protected-access
+        fip = self._provider.networking._floating_ips.get(None, self.id)
+        # pylint:disable=protected-access
+        self._ip = fip._ip
+        self._process_ip_users()
+
+    def _process_ip_users(self):
+        self._rule = None
+        self._target_instance = None
+
+        if 'users' in self._ip and len(self._ip['users']) > 0:
+            provider = self._provider
+            if len(self._ip['users']) > 1:
+                log.warning('Address "%s" in use by more than one resource',
+                            self._ip.get('address'))
+            resource_parsed_url = provider.parse_url(self._ip['users'][0])
+            resource = resource_parsed_url.get_resource()
+            if resource['kind'] == 'compute#forwardingRule':
+                self._rule = resource
+                target = provider.parse_url(resource['target']).get_resource()
+                if target['kind'] == 'compute#targetInstance':
+                    url = provider.parse_url(target['instance'])
+                    try:
+                        self._target_instance = url.get_resource()
+                    except googleapiclient.errors.HttpError:
+                        self._target_instance = GCPFloatingIP._DEAD_INSTANCE
+                else:
+                    log.warning('Address "%s" is forwarded to a %s',
+                                self._ip.get('address'), target['kind'])
+            else:
+                log.warning('Address "%s" in use by a %s',
+                            self._ip.get('address'), resource['kind'])
+
+
+class GCPRouter(BaseRouter):
+
+    def __init__(self, provider, router):
+        super(GCPRouter, self).__init__(provider)
+        self._router = router
+
+    @property
+    def id(self):
+        return self._router['selfLink']
+
+    @property
+    def name(self):
+        return self._router['name']
+
+    @property
+    def label(self):
+        tag_name = "_".join(["router", self.name, "label"])
+        return helpers.get_metadata_item_value(self._provider, tag_name)
+
+    @label.setter
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        tag_name = "_".join(["router", self.name, "label"])
+        helpers.modify_or_add_metadata_item(self._provider, tag_name, value)
+
+    @property
+    def region_name(self):
+        parsed_url = self._provider.parse_url(self.id)
+        return parsed_url.parameters['region']
+
+    def refresh(self):
+        router = self._provider.networking.routers.get(self.id)
+        if router:
+            # pylint:disable=protected-access
+            self._router = router._router
+        else:
+            # router no longer exists
+            self._router['status'] = RouterState.UNKNOWN
+
+    @property
+    def state(self):
+        # If the router info is refreshed after it is deleted, its status will
+        # be UNKNOWN.
+        if self._router.get('status') == RouterState.UNKNOWN:
+            return RouterState.UNKNOWN
+        # GCP routers are always attached to a network.
+        return RouterState.ATTACHED
+
+    @property
+    def network_id(self):
+        parsed_url = self._provider.parse_url(self._router['network'])
+        network = parsed_url.get_resource()
+        return network['selfLink']
+
+    @property
+    def subnets(self):
+        network = self._provider.networking.networks.get(self.network_id)
+        return network.subnets
+
+    def attach_subnet(self, subnet):
+        if not isinstance(subnet, GCPSubnet):
+            subnet = self._provider.networking.subnets.get(subnet)
+        if subnet.network_id == self.network_id:
+            return
+        log.warning('Google Cloud Routers automatically learn new subnets '
+                    'in your VPC network and announces them to your '
+                    'on-premises network')
+
+    def detach_subnet(self, network_id):
+        log.warning('Cannot detach from subnet. Google Cloud Routers '
+                    'automatically learn new subnets in your VPC network '
+                    'and announces them to your on-premises network')
+
+    def attach_gateway(self, gateway):
+        pass
+
+    def detach_gateway(self, gateway):
+        pass
+
+
+class GCPInternetGateway(BaseInternetGateway):
+
+    def __init__(self, provider, gateway):
+        super(GCPInternetGateway, self).__init__(provider)
+        self._gateway = gateway
+        self._fip_container = GCPFloatingIPSubService(provider, self)
+
+    @property
+    def id(self):
+        return self._gateway['id']
+
+    @property
+    def name(self):
+        return self._gateway['name']
+
+    def refresh(self):
+        pass
+
+    @property
+    def state(self):
+        return GatewayState.AVAILABLE
+
+    @property
+    def network_id(self):
+        """
+        GCP internet gateways are not attached to a network.
+        """
+        return None
+
+    def delete(self):
+        pass
+
+    @property
+    def floating_ips(self):
+        return self._fip_container
+
+
+class GCPSubnet(BaseSubnet):
+
+    def __init__(self, provider, subnet):
+        super(GCPSubnet, self).__init__(provider)
+        self._subnet = subnet
+
+    @property
+    def id(self):
+        return self._subnet['selfLink']
+
+    @property
+    def name(self):
+        return self._subnet['name']
+
+    @property
+    def label(self):
+        tag_name = "_".join(["subnet", self.name, "label"])
+        return helpers.get_metadata_item_value(self._provider, tag_name)
+
+    @label.setter
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        tag_name = "_".join(["subnet", self.name, "label"])
+        helpers.modify_or_add_metadata_item(self._provider, tag_name, value)
+
+    @property
+    def cidr_block(self):
+        return self._subnet['ipCidrRange']
+
+    @property
+    def network_url(self):
+        return self._subnet['network']
+
+    @property
+    def network_id(self):
+        return self.network_url
+
+    @property
+    def region(self):
+        return self._subnet['region']
+
+    @property
+    def region_name(self):
+        parsed_url = self._provider.parse_url(self.id)
+        return parsed_url.parameters['region']
+
+    @property
+    def zone(self):
+        return None
+
+    @property
+    def state(self):
+        if self._subnet.get('status') == SubnetState.UNKNOWN:
+            return SubnetState.UNKNOWN
+        return SubnetState.AVAILABLE
+
+    def refresh(self):
+        subnet = self._provider.networking.subnets.get(self.id)
+        if subnet:
+            # pylint:disable=protected-access
+            self._subnet = subnet._subnet
+        else:
+            # subnet no longer exists
+            self._subnet['status'] = SubnetState.UNKNOWN
+
+
+class GCPVolume(BaseVolume):
+
+    VOLUME_STATE_MAP = {
+        'CREATING': VolumeState.CONFIGURING,
+        'FAILED': VolumeState.ERROR,
+        'READY': VolumeState.AVAILABLE,
+        'RESTORING': VolumeState.CONFIGURING,
+    }
+
+    def __init__(self, provider, volume):
+        super(GCPVolume, self).__init__(provider)
+        self._volume = volume
+
+    @property
+    def id(self):
+        return self._volume.get('selfLink')
+
+    @property
+    def name(self):
+        """
+        Get the volume name.
+        """
+        return self._volume.get('name')
+
+    @property
+    def label(self):
+        labels = self._volume.get('labels')
+        return labels.get('cblabel', '') if labels else ''
+
+    @label.setter
+    def label(self, value):
+        req = (self._provider
+                   .gcp_compute
+                   .disks()
+                   .setLabels(project=self._provider.project_name,
+                              zone=self.zone_name,
+                              resource=self.name,
+                              body={}))
+
+        helpers.change_label(self, 'cblabel', value, '_volume', req)
+
+    @property
+    def description(self):
+        labels = self._volume.get('labels')
+        if labels and 'description' in labels:
+            return labels.get('description', '')
+        return self._volume.get('description', '')
+
+    @description.setter
+    def description(self, value):
+        req = (self._provider
+               .gcp_compute
+               .disks()
+               .setLabels(project=self._provider.project_name,
+                          zone=self.zone_name,
+                          resource=self.name,
+                          body={}))
+
+        helpers.change_label(self, 'description', value, '_volume', req)
+
+    @property
+    def size(self):
+        return int(self._volume.get('sizeGb'))
+
+    @property
+    def create_time(self):
+        return self._volume.get('creationTimestamp')
+
+    @property
+    def zone_id(self):
+        return self._volume.get('zone')
+
+    @property
+    def zone_name(self):
+        return self._provider.parse_url(self.zone_id).parameters['zone']
+
+    @property
+    def source(self):
+        if 'sourceSnapshot' in self._volume:
+            snapshot_uri = self._volume.get('sourceSnapshot')
+            return GCPSnapshot(
+                    self._provider,
+                    self._provider.parse_url(snapshot_uri).get_resource())
+        if 'sourceImage' in self._volume:
+            image_uri = self._volume.get('sourceImage')
+            return GCPMachineImage(
+                    self._provider,
+                    self._provider.parse_url(image_uri).get_resource())
+        return None
+
+    @property
+    def attachments(self):
+        # GCP Persistent Disk supports multiple instances attaching a READ-ONLY
+        # disk. In cloudbridge, volume usage pattern is that a disk is attached
+        # to a single instance in a read-write mode. Therefore, we only check
+        # the first user of a disk.
+        if 'users' in self._volume and len(self._volume) > 0:
+            if len(self._volume) > 1:
+                log.warning("This volume is attached to multiple instances")
+            return BaseAttachmentInfo(self,
+                                      self._volume.get('users')[0],
+                                      None)
+        else:
+            return None
+
+    def attach(self, instance, device):
+        """
+        Attach this volume to an instance.
+
+        instance: The ID of an instance or an ``Instance`` object to
+                  which this volume will be attached.
+
+        To use the disk, the user needs to mount the disk so that the operating
+        system can use the available storage space.
+        https://cloud.google.com/compute/docs/disks/add-persistent-disk
+        """
+        attach_disk_body = {
+            "source": self.id,
+            "deviceName": device.split('/')[-1],
+        }
+        if not isinstance(instance, GCPInstance):
+            instance = self._provider.get_resource('instances', instance)
+        (self._provider
+             .gcp_compute
+             .instances()
+             .attachDisk(project=self._provider.project_name,
+                         zone=instance.zone_name,
+                         instance=instance.name,
+                         body=attach_disk_body)
+             .execute())
+
+    def detach(self, force=False):
+        """
+        Detach this volume from an instance.
+        """
+        # Check whether this volume is attached to an instance.
+        if not self.attachments:
+            return
+        parsed_uri = self._provider.parse_url(self.attachments.instance_id)
+        instance_data = parsed_uri.get_resource()
+        # Check whether the instance has this volume attached.
+        if 'disks' not in instance_data:
+            return
+        device_name = None
+        for disk in instance_data['disks']:
+            if ('source' in disk and 'deviceName' in disk and
+                    disk['source'] == self.id):
+                device_name = disk['deviceName']
+        if not device_name:
+            return
+        (self._provider
+             .gcp_compute
+             .instances()
+             .detachDisk(project=self._provider.project_name,
+                         zone=self.zone_name,
+                         instance=instance_data.get('name'),
+                         deviceName=device_name)
+             .execute())
+
+    def create_snapshot(self, label, description=None):
+        """
+        Create a snapshot of this Volume.
+        """
+        return self._provider.storage.snapshots.create(
+            label, self, description)
+
+    @property
+    def state(self):
+        if len(self._volume.get('users', [])) > 0:
+            return VolumeState.IN_USE
+        return GCPVolume.VOLUME_STATE_MAP.get(
+            self._volume.get('status'), VolumeState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this volume by re-querying the cloud provider
+        for its latest state.
+        """
+        vol = self._provider.storage.volumes.get(self.id)
+        if vol:
+            # pylint:disable=protected-access
+            self._volume = vol._volume
+        else:
+            # volume no longer exists
+            self._volume['status'] = VolumeState.UNKNOWN
+
+
+class GCPSnapshot(BaseSnapshot):
+
+    SNAPSHOT_STATE_MAP = {
+        'PENDING': SnapshotState.PENDING,
+        'READY': SnapshotState.AVAILABLE,
+    }
+
+    def __init__(self, provider, snapshot):
+        super(GCPSnapshot, self).__init__(provider)
+        self._snapshot = snapshot
+
+    @property
+    def id(self):
+        return self._snapshot.get('selfLink')
+
+    @property
+    def name(self):
+        """
+        Get the snapshot name.
+        """
+        return self._snapshot.get('name')
+
+    @property
+    def label(self):
+        labels = self._snapshot.get('labels')
+        return labels.get('cblabel', '') if labels else ''
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        req = (self._provider
+                   .gcp_compute
+                   .snapshots()
+                   .setLabels(project=self._provider.project_name,
+                              resource=self.name,
+                              body={}))
+
+        helpers.change_label(self, 'cblabel', value, '_snapshot', req)
+
+    @property
+    def description(self):
+        labels = self._snapshot.get('labels')
+        if labels and 'description' in labels:
+            return labels.get('description', '')
+        return self._snapshot.get('description', '')
+
+    @description.setter
+    def description(self, value):
+        req = (self._provider
+               .gcp_compute
+               .snapshots()
+               .setLabels(project=self._provider.project_name,
+                          resource=self.name,
+                          body={}))
+
+        helpers.change_label(self, 'description', value, '_snapshot', req)
+
+    @property
+    def size(self):
+        return int(self._snapshot.get('diskSizeGb'))
+
+    @property
+    def volume_id(self):
+        return self._snapshot.get('sourceDisk')
+
+    @property
+    def create_time(self):
+        return self._snapshot.get('creationTimestamp')
+
+    @property
+    def state(self):
+        return GCPSnapshot.SNAPSHOT_STATE_MAP.get(
+            self._snapshot.get('status'), SnapshotState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this snapshot by re-querying the cloud provider
+        for its latest state.
+        """
+        snap = self._provider.storage.snapshots.get(self.id)
+        if snap:
+            # pylint:disable=protected-access
+            self._snapshot = snap._snapshot
+        else:
+            # snapshot no longer exists
+            self._snapshot['status'] = SnapshotState.UNKNOWN
+
+    def create_volume(self, placement, size=None, volume_type=None, iops=None):
+        """
+        Create a new Volume from this Snapshot.
+
+        Args:
+            placement: GCP zone name, e.g. 'us-central1-f'.
+            size: The size of the new volume, in GiB (optional). Defaults to
+                the size of the snapshot.
+            volume_type: Type of persistent disk. Either 'pd-standard' or
+                'pd-ssd'.
+            iops: Not supported by GCP.
+        """
+        zone_name = placement
+        if isinstance(placement, GCPPlacementZone):
+            zone_name = placement.name
+        vol_type = 'zones/{0}/diskTypes/{1}'.format(
+            zone_name,
+            'pd-standard' if (volume_type != 'pd-standard' or
+                              volume_type != 'pd-ssd') else volume_type)
+        disk_body = {
+            'name': ('created-from-{0}'.format(self.name))[:63],
+            'sizeGb': size if size is not None else self.size,
+            'type': vol_type,
+            'sourceSnapshot': self.id
+        }
+        operation = (self._provider
+                         .gcp_compute
+                         .disks()
+                         .insert(project=self._provider.project_name,
+                                 zone=zone_name,
+                                 body=disk_body)
+                         .execute())
+        return self._provider.storage.volumes.get(
+            operation.get('targetLink'))
+
+
+class GCPBucketObject(BaseBucketObject):
+
+    def __init__(self, provider, bucket, obj):
+        super(GCPBucketObject, self).__init__(provider)
+        self._bucket = bucket
+        self._obj = obj
+
+    @property
+    def id(self):
+        return self._obj['selfLink']
+
+    @property
+    def name(self):
+        return self._obj['name']
+
+    @property
+    def size(self):
+        return int(self._obj['size'])
+
+    @property
+    def last_modified(self):
+        return self._obj['updated']
+
+    def iter_content(self):
+        return io.BytesIO(self._provider
+                              .gcp_storage
+                              .objects()
+                              .get_media(bucket=self._obj['bucket'],
+                                         object=self.name)
+                              .execute())
+
+    def upload(self, data):
+        """
+        Set the contents of this object to the given text.
+        """
+        if type(data) is str:
+            data = data.encode()
+        media_body = googleapiclient.http.MediaIoBaseUpload(
+                io.BytesIO(data), mimetype='plain/text')
+        # pylint:disable=protected-access
+        response = (self._provider
+                        .storage._bucket_objects
+                        ._create_object_with_media_body(self._bucket,
+                                                        self.name,
+                                                        media_body))
+        if response:
+            self._obj = response
+
+    def upload_from_file(self, path):
+        """
+        Upload a binary file.
+        """
+        with open(path, 'rb') as f:
+            media_body = googleapiclient.http.MediaIoBaseUpload(
+                    f, 'application/octet-stream')
+            # pylint:disable=protected-access
+            response = (self._provider
+                        .storage._bucket_objects
+                        ._create_object_with_media_body(self._bucket,
+                                                        self.name,
+                                                        media_body))
+            if response:
+                self._obj = response
+
+    def delete(self):
+        (self._provider
+             .gcp_storage
+             .objects()
+             .delete(bucket=self._obj['bucket'], object=self.name)
+             .execute())
+
+    def generate_url(self, expires_in):
+        """
+        Generates a signed URL accessible to everyone.
+        """
+        expiration = calendar.timegm(time.gmtime()) + expires_in
+        signed_signature = self._provider.sign_blob(
+            'GET\n\n\n%d\n/%s/%s' %
+            (expiration, self._obj['bucket'], self.name))
+        encoded_signature = base64.b64encode(signed_signature).decode("utf-8")
+        url_encoded_signature = (encoded_signature.replace('+', '%2B')
+                                                  .replace('/', '%2F'))
+        return ('https://storage.googleapis.com/%s/%s?GoogleAccessId=%s'
+                '&Expires=%d&Signature=%s' % (self._obj['bucket'], self.name,
+                                              self._provider.client_id,
+                                              expiration,
+                                              url_encoded_signature))
+
+    def refresh(self):
+        # pylint:disable=protected-access
+        self._obj = self.bucket.objects.get(self.id)._obj
+
+
+class GCPBucket(BaseBucket):
+
+    def __init__(self, provider, bucket):
+        super(GCPBucket, self).__init__(provider)
+        self._bucket = bucket
+        self._object_container = GCPBucketObjectSubService(provider, self)
+
+    @property
+    def id(self):
+        return self._bucket['selfLink']
+
+    @property
+    def name(self):
+        """
+        Get this bucket's name.
+        """
+        return self._bucket['name']
+
+    @property
+    def objects(self):
+        return self._object_container
+
+
+class GCPLaunchConfig(BaseLaunchConfig):
+
+    def __init__(self, provider):
+        super(GCPLaunchConfig, self).__init__(provider)

+ 1642 - 0
cloudbridge/cloud/providers/gcp/services.py

@@ -0,0 +1,1642 @@
+import io
+import ipaddress
+import json
+import logging
+import time
+import uuid
+
+import googleapiclient
+
+from cloudbridge.cloud.base import helpers as cb_helpers
+from cloudbridge.cloud.base.middleware import dispatch
+from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.resources import ServerPagedResultList
+from cloudbridge.cloud.base.services import BaseBucketObjectService
+from cloudbridge.cloud.base.services import BaseBucketService
+from cloudbridge.cloud.base.services import BaseComputeService
+from cloudbridge.cloud.base.services import BaseFloatingIPService
+from cloudbridge.cloud.base.services import BaseGatewayService
+from cloudbridge.cloud.base.services import BaseImageService
+from cloudbridge.cloud.base.services import BaseInstanceService
+from cloudbridge.cloud.base.services import BaseKeyPairService
+from cloudbridge.cloud.base.services import BaseNetworkService
+from cloudbridge.cloud.base.services import BaseNetworkingService
+from cloudbridge.cloud.base.services import BaseRegionService
+from cloudbridge.cloud.base.services import BaseRouterService
+from cloudbridge.cloud.base.services import BaseSecurityService
+from cloudbridge.cloud.base.services import BaseSnapshotService
+from cloudbridge.cloud.base.services import BaseStorageService
+from cloudbridge.cloud.base.services import BaseSubnetService
+from cloudbridge.cloud.base.services import BaseVMFirewallRuleService
+from cloudbridge.cloud.base.services import BaseVMFirewallService
+from cloudbridge.cloud.base.services import BaseVMTypeService
+from cloudbridge.cloud.base.services import BaseVolumeService
+from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
+from cloudbridge.cloud.interfaces.exceptions import InvalidParamException
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
+from cloudbridge.cloud.interfaces.resources import VMFirewall
+from cloudbridge.cloud.providers.gcp import helpers
+
+from .resources import GCPBucket
+from .resources import GCPBucketObject
+from .resources import GCPFirewallsDelegate
+from .resources import GCPFloatingIP
+from .resources import GCPInstance
+from .resources import GCPInternetGateway
+from .resources import GCPKeyPair
+from .resources import GCPLaunchConfig
+from .resources import GCPMachineImage
+from .resources import GCPNetwork
+from .resources import GCPPlacementZone
+from .resources import GCPRegion
+from .resources import GCPRouter
+from .resources import GCPSnapshot
+from .resources import GCPSubnet
+from .resources import GCPVMFirewall
+from .resources import GCPVMFirewallRule
+from .resources import GCPVMType
+from .resources import GCPVolume
+
+log = logging.getLogger(__name__)
+
+
+class GCPSecurityService(BaseSecurityService):
+
+    def __init__(self, provider):
+        super(GCPSecurityService, self).__init__(provider)
+
+        # Initialize provider services
+        self._key_pairs = GCPKeyPairService(provider)
+        self._vm_firewalls = GCPVMFirewallService(provider)
+        self._vm_firewall_rule_svc = GCPVMFirewallRuleService(provider)
+
+    @property
+    def key_pairs(self):
+        return self._key_pairs
+
+    @property
+    def vm_firewalls(self):
+        return self._vm_firewalls
+
+    @property
+    def _vm_firewall_rules(self):
+        return self._vm_firewall_rule_svc
+
+
+class GCPKeyPairService(BaseKeyPairService):
+
+    def __init__(self, provider):
+        super(GCPKeyPairService, self).__init__(provider)
+
+    @dispatch(event="provider.security.key_pairs.get",
+              priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
+    def get(self, key_pair_id):
+        """
+        Returns a KeyPair given its ID.
+        """
+        for kp in self:
+            if kp.id == key_pair_id:
+                return kp
+        else:
+            return None
+
+    @dispatch(event="provider.security.key_pairs.list",
+              priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        key_pairs = []
+        for item in helpers.find_matching_metadata_items(
+                self.provider, GCPKeyPair.KP_TAG_REGEX):
+            metadata_value = json.loads(item['value'])
+            kp_info = GCPKeyPair.GCPKeyInfo(**metadata_value)
+            key_pairs.append(GCPKeyPair(self.provider, kp_info))
+        return ClientPagedResultList(self.provider, key_pairs,
+                                     limit=limit, marker=marker)
+
+    @dispatch(event="provider.security.key_pairs.find",
+              priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
+    def find(self, **kwargs):
+        """
+        Searches for a key pair by a given list of attributes.
+        """
+        obj_list = self
+        filters = ['id', 'name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, ", ".join(filters)))
+
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
+
+    @dispatch(event="provider.security.key_pairs.create",
+              priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
+    def create(self, name, public_key_material=None):
+        GCPKeyPair.assert_valid_resource_name(name)
+        private_key = None
+        if not public_key_material:
+            public_key_material, private_key = cb_helpers.generate_key_pair()
+        # TODO: Add support for other formats not assume ssh-rsa
+        elif "ssh-rsa" not in public_key_material:
+            public_key_material = "ssh-rsa {}".format(public_key_material)
+        kp_info = GCPKeyPair.GCPKeyInfo(name, public_key_material)
+        metadata_value = json.dumps(kp_info._asdict())
+        try:
+            helpers.add_metadata_item(self.provider,
+                                      GCPKeyPair.KP_TAG_PREFIX + name,
+                                      metadata_value)
+            return GCPKeyPair(self.provider, kp_info, private_key)
+        except googleapiclient.errors.HttpError as err:
+            if err.resp.get('content-type', '').startswith('application/json'):
+                message = (json.loads(err.content).get('error', {})
+                           .get('errors', [{}])[0].get('message'))
+                if "duplicate keys" in message:
+                    raise DuplicateResourceException(
+                        'A KeyPair with name {0} already exists'.format(name))
+            raise
+
+    @dispatch(event="provider.security.key_pairs.delete",
+              priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
+    def delete(self, key_pair):
+        key_pair = (key_pair if isinstance(key_pair, GCPKeyPair) else
+                    self.get(key_pair))
+        if key_pair:
+            helpers.remove_metadata_item(
+                self.provider, GCPKeyPair.KP_TAG_PREFIX + key_pair.name)
+
+
+class GCPVMFirewallService(BaseVMFirewallService):
+
+    def __init__(self, provider):
+        super(GCPVMFirewallService, self).__init__(provider)
+        self._delegate = GCPFirewallsDelegate(provider)
+
+    @dispatch(event="provider.security.vm_firewalls.get",
+              priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
+    def get(self, vm_firewall_id):
+        tag, network_name = \
+            self._delegate.get_tag_network_from_id(vm_firewall_id)
+        if tag is None:
+            return None
+        network = self.provider.networking.networks.get(network_name)
+        return GCPVMFirewall(self._delegate, tag, network)
+
+    @dispatch(event="provider.security.vm_firewalls.list",
+              priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        vm_firewalls = []
+        for tag, network_name in self._delegate.tag_networks:
+            network = self.provider.networking.networks.get(
+                    network_name)
+            vm_firewall = GCPVMFirewall(self._delegate, tag, network)
+            vm_firewalls.append(vm_firewall)
+        return ClientPagedResultList(self.provider, vm_firewalls,
+                                     limit=limit, marker=marker)
+
+    @dispatch(event="provider.security.vm_firewalls.create",
+              priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
+    def create(self, label, network, description=None):
+        GCPVMFirewall.assert_valid_resource_label(label)
+        network = (network if isinstance(network, GCPNetwork)
+                   else self.provider.networking.networks.get(network))
+        fw = GCPVMFirewall(self._delegate, label, network, description)
+        fw.label = label
+        # This rule exists implicitly. Add it explicitly so that the firewall
+        # is not empty and the rule is shown by list/get/find methods.
+        # pylint:disable=protected-access
+        self.provider.security._vm_firewall_rules.create_with_priority(
+            fw, direction=TrafficDirection.OUTBOUND, protocol='tcp',
+            priority=65534, cidr='0.0.0.0/0')
+        return fw
+
+    @dispatch(event="provider.security.vm_firewalls.delete",
+              priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
+    def delete(self, vm_firewall):
+        fw_id = (vm_firewall.id if isinstance(vm_firewall, GCPVMFirewall)
+                 else vm_firewall)
+        return self._delegate.delete_tag_network_with_id(fw_id)
+
+    def find_by_network_and_tags(self, network_name, tags):
+        """
+        Finds non-empty VM firewalls by network name and VM firewall names
+        (tags). If no matching VM firewall is found, an empty list is returned.
+        """
+        vm_firewalls = []
+        for tag, net_name in self._delegate.tag_networks:
+            if network_name != net_name:
+                continue
+            if tag not in tags:
+                continue
+            network = self.provider.networking.networks.get(net_name)
+            vm_firewalls.append(
+                GCPVMFirewall(self._delegate, tag, network))
+        return vm_firewalls
+
+
+class GCPVMFirewallRuleService(BaseVMFirewallRuleService):
+
+    def __init__(self, provider):
+        super(GCPVMFirewallRuleService, self).__init__(provider)
+        self._dummy_rule = None
+
+    @dispatch(event="provider.security.vm_firewall_rules.list",
+              priority=BaseVMFirewallRuleService.STANDARD_EVENT_PRIORITY)
+    def list(self, firewall, limit=None, marker=None):
+        rules = []
+        for fw in firewall.delegate.iter_firewalls(
+                firewall.name, firewall.network.name):
+            rule = GCPVMFirewallRule(firewall, fw['id'])
+            if rule.is_dummy_rule():
+                self._dummy_rule = rule
+            else:
+                rules.append(rule)
+        return ClientPagedResultList(self.provider, rules,
+                                     limit=limit, marker=marker)
+
+    @property
+    def dummy_rule(self):
+        if not self._dummy_rule:
+            self.list()
+        return self._dummy_rule
+
+    @staticmethod
+    def to_port_range(from_port, to_port):
+        if from_port is not None and to_port is not None:
+            return '%d-%d' % (from_port, to_port)
+        elif from_port is not None:
+            return from_port
+        else:
+            return to_port
+
+    def create_with_priority(self, firewall, direction, protocol, priority,
+                             from_port=None, to_port=None, cidr=None,
+                             src_dest_fw=None):
+        port = GCPVMFirewallRuleService.to_port_range(from_port, to_port)
+        src_dest_tag = None
+        src_dest_fw_id = None
+        if src_dest_fw:
+            src_dest_tag = src_dest_fw.name
+            src_dest_fw_id = src_dest_fw.id
+        if not firewall.delegate.add_firewall(
+                firewall.name, direction, protocol, priority, port, cidr,
+                src_dest_tag, firewall.description,
+                firewall.network.name):
+            return None
+        rules = self.find(firewall, direction=direction, protocol=protocol,
+                          from_port=from_port, to_port=to_port, cidr=cidr,
+                          src_dest_fw_id=src_dest_fw_id)
+        if len(rules) < 1:
+            return None
+        return rules[0]
+
+    @dispatch(event="provider.security.vm_firewall_rules.create",
+              priority=BaseVMFirewallRuleService.STANDARD_EVENT_PRIORITY)
+    def create(self, firewall, direction, protocol, from_port=None,
+               to_port=None, cidr=None, src_dest_fw=None):
+        return self.create_with_priority(firewall, direction, protocol,
+                                         1000, from_port, to_port, cidr,
+                                         src_dest_fw)
+
+    @dispatch(event="provider.security.vm_firewall_rules.delete",
+              priority=BaseVMFirewallRuleService.STANDARD_EVENT_PRIORITY)
+    def delete(self, firewall, rule):
+        rule = (rule if isinstance(rule, GCPVMFirewallRule)
+                else self.get(firewall, rule))
+        if rule.is_dummy_rule():
+            return True
+        firewall.delegate.delete_firewall_id(rule._rule)
+
+
+class GCPVMTypeService(BaseVMTypeService):
+
+    def __init__(self, provider):
+        super(GCPVMTypeService, self).__init__(provider)
+
+    @property
+    def instance_data(self):
+        response = (self.provider
+                        .gcp_compute
+                        .machineTypes()
+                        .list(project=self.provider.project_name,
+                              zone=self.provider.default_zone)
+                        .execute())
+        return response['items']
+
+    @dispatch(event="provider.compute.vm_types.get",
+              priority=BaseVMTypeService.STANDARD_EVENT_PRIORITY)
+    def get(self, vm_type_id):
+        vm_type = self.provider.get_resource('machineTypes', vm_type_id)
+        return GCPVMType(self.provider, vm_type) if vm_type else None
+
+    @dispatch(event="provider.compute.vm_types.find",
+              priority=BaseVMTypeService.STANDARD_EVENT_PRIORITY)
+    def find(self, **kwargs):
+        matched_inst_types = []
+        for inst_type in self.instance_data:
+            is_match = True
+            for key, value in kwargs.items():
+                if key not in inst_type:
+                    raise InvalidParamException(
+                        "Unrecognised parameters for search: %s." % key)
+                if inst_type.get(key) != value:
+                    is_match = False
+                    break
+            if is_match:
+                matched_inst_types.append(
+                    GCPVMType(self.provider, inst_type))
+        return matched_inst_types
+
+    @dispatch(event="provider.compute.vm_types.list",
+              priority=BaseVMTypeService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        inst_types = [GCPVMType(self.provider, inst_type)
+                      for inst_type in self.instance_data]
+        return ClientPagedResultList(self.provider, inst_types,
+                                     limit=limit, marker=marker)
+
+
+class GCPRegionService(BaseRegionService):
+
+    def __init__(self, provider):
+        super(GCPRegionService, self).__init__(provider)
+
+    @dispatch(event="provider.compute.regions.get",
+              priority=BaseRegionService.STANDARD_EVENT_PRIORITY)
+    def get(self, region_id):
+        region = self.provider.get_resource('regions', region_id,
+                                            region=region_id)
+        return GCPRegion(self.provider, region) if region else None
+
+    @dispatch(event="provider.compute.regions.list",
+              priority=BaseRegionService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        max_result = limit if limit is not None and limit < 500 else 500
+        regions_response = (self.provider
+                                .gcp_compute
+                                .regions()
+                                .list(project=self.provider.project_name,
+                                      maxResults=max_result,
+                                      pageToken=marker)
+                                .execute())
+        regions = [GCPRegion(self.provider, region)
+                   for region in regions_response['items']]
+        if len(regions) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(regions))
+        return ServerPagedResultList('nextPageToken' in regions_response,
+                                     regions_response.get('nextPageToken'),
+                                     False, data=regions)
+
+    @property
+    def current(self):
+        return self.get(self.provider.region_name)
+
+
+class GCPImageService(BaseImageService):
+
+    def __init__(self, provider):
+        super(GCPImageService, self).__init__(provider)
+        self._public_images = None
+
+    _PUBLIC_IMAGE_PROJECTS = ['centos-cloud', 'coreos-cloud', 'debian-cloud',
+                              'opensuse-cloud', 'ubuntu-os-cloud', 'cos-cloud']
+
+    def _retrieve_public_images(self):
+        if self._public_images is not None:
+            return
+        self._public_images = []
+        for project in GCPImageService._PUBLIC_IMAGE_PROJECTS:
+            for image in helpers.iter_all(
+                    self.provider.gcp_compute.images(), project=project):
+                self._public_images.append(
+                    GCPMachineImage(self.provider, image))
+
+    def get(self, image_id):
+        """
+        Returns an Image given its id
+        """
+        image = self.provider.get_resource('images', image_id)
+        if image:
+            return GCPMachineImage(self.provider, image)
+        self._retrieve_public_images()
+        for public_image in self._public_images:
+            if public_image.id == image_id or public_image.name == image_id:
+                return public_image
+        return None
+
+    def find(self, limit=None, marker=None, **kwargs):
+        """
+        Searches for an image by a given list of attributes
+        """
+        label = kwargs.pop('label', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
+
+        # Retrieve all available images by setting limit to sys.maxsize
+        images = [image for image in self if image.label == label]
+        return ClientPagedResultList(self.provider, images,
+                                     limit=limit, marker=marker)
+
+    def list(self, limit=None, marker=None):
+        """
+        List all images.
+        """
+        self._retrieve_public_images()
+        images = []
+        if (self.provider.project_name not in
+                GCPImageService._PUBLIC_IMAGE_PROJECTS):
+            for image in helpers.iter_all(
+                    self.provider.gcp_compute.images(),
+                    project=self.provider.project_name):
+                images.append(GCPMachineImage(self.provider, image))
+        images.extend(self._public_images)
+        return ClientPagedResultList(self.provider, images,
+                                     limit=limit, marker=marker)
+
+
+class GCPInstanceService(BaseInstanceService):
+
+    def __init__(self, provider):
+        super(GCPInstanceService, self).__init__(provider)
+
+    @dispatch(event="provider.compute.instances.create",
+              priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
+    def create(self, label, image, vm_type, subnet, zone=None,
+               key_pair=None, vm_firewalls=None, user_data=None,
+               launch_config=None, **kwargs):
+        """
+        Creates a new virtual machine instance.
+        """
+        GCPInstance.assert_valid_resource_name(label)
+        zone_name = self.provider.default_zone
+        if zone:
+            if not isinstance(zone, GCPPlacementZone):
+                zone = GCPPlacementZone(
+                    self.provider,
+                    self.provider.get_resource('zones', zone))
+            zone_name = zone.name
+        if not isinstance(vm_type, GCPVMType):
+            vm_type = self.provider.compute.vm_types.get(vm_type)
+
+        network_interface = {'accessConfigs': [{'type': 'ONE_TO_ONE_NAT',
+                                                'name': 'External NAT'}]}
+        if subnet:
+            network_interface['subnetwork'] = subnet.id
+        else:
+            network_interface['network'] = 'global/networks/default'
+
+        num_roots = 0
+        disks = []
+        boot_disk = None
+        if isinstance(launch_config, GCPLaunchConfig):
+            for disk in launch_config.block_devices:
+                if not disk.source:
+                    volume_name = 'disk-{0}'.format(uuid.uuid4())
+                    volume_size = disk.size if disk.size else 1
+                    volume = self.provider.storage.volumes.create(
+                        volume_name, volume_size, zone)
+                    volume.wait_till_ready()
+                    source_field = 'source'
+                    source_value = volume.id
+                elif isinstance(disk.source, GCPMachineImage):
+                    source_field = 'initializeParams'
+                    # Explicitly set diskName; otherwise, instance label will
+                    # be used by default which may collide with existing disks.
+                    source_value = {
+                        'sourceImage': disk.source.id,
+                        'diskName': 'image-disk-{0}'.format(uuid.uuid4()),
+                        'diskSizeGb': disk.size if disk.size else 20}
+                elif isinstance(disk.source, GCPVolume):
+                    source_field = 'source'
+                    source_value = disk.source.id
+                elif isinstance(disk.source, GCPSnapshot):
+                    volume = disk.source.create_volume(zone, size=disk.size)
+                    volume.wait_till_ready()
+                    source_field = 'source'
+                    source_value = volume.id
+                else:
+                    log.warning('Unknown disk source')
+                    continue
+                autoDelete = True
+                if disk.delete_on_terminate is not None:
+                    autoDelete = disk.delete_on_terminate
+                num_roots += 1 if disk.is_root else 0
+                if disk.is_root and not boot_disk:
+                    boot_disk = {'boot': True,
+                                 'autoDelete': autoDelete,
+                                 source_field: source_value}
+                else:
+                    disks.append({'boot': False,
+                                  'autoDelete': autoDelete,
+                                  source_field: source_value})
+
+        if num_roots > 1:
+            log.warning('The launch config contains %d boot disks. Will '
+                        'use the first one', num_roots)
+        if image:
+            if boot_disk:
+                log.warning('A boot image is given while the launch config '
+                            'contains a boot disk, too. The launch config '
+                            'will be used.')
+            else:
+                if not isinstance(image, GCPMachineImage):
+                    image = self.provider.compute.images.get(image)
+                # Explicitly set diskName; otherwise, instance name will be
+                # used by default which may conflict with existing disks.
+                boot_disk = {
+                    'boot': True,
+                    'autoDelete': True,
+                    'initializeParams': {
+                        'sourceImage': image.id,
+                        'diskName': 'image-disk-{0}'.format(uuid.uuid4())}}
+
+        if not boot_disk:
+            log.warning('No boot disk is given for instance %s.', label)
+            return None
+        # The boot disk must be the first disk attached to the instance.
+        disks.insert(0, boot_disk)
+
+        config = {
+            'name': GCPInstance._generate_name_from_label(label, 'cb-inst'),
+            'machineType': vm_type.resource_url,
+            'disks': disks,
+            'networkInterfaces': [network_interface]
+        }
+
+        if vm_firewalls and isinstance(vm_firewalls, list):
+            vm_firewall_names = []
+            if isinstance(vm_firewalls[0], VMFirewall):
+                vm_firewall_names = [f.name for f in vm_firewalls]
+            elif isinstance(vm_firewalls[0], str):
+                vm_firewall_names = vm_firewalls
+            if len(vm_firewall_names) > 0:
+                config['tags'] = {}
+                config['tags']['items'] = vm_firewall_names
+
+        if user_data:
+            entry = {'key': 'user-data', 'value': user_data}
+            config['metadata'] = {'items': [entry]}
+
+        if key_pair:
+            if not isinstance(key_pair, GCPKeyPair):
+                key_pair = self._provider.security.key_pairs.get(key_pair)
+            if key_pair:
+                kp = key_pair._key_pair
+                kp_entry = {
+                    "key": "ssh-keys",
+                    # Format is not removed from public key portion
+                    "value": "{}:{} {}".format(
+                        self.provider.vm_default_user_name,
+                        kp.public_key,
+                        kp.name)
+                    }
+                meta = config.get('metadata', {})
+                if meta:
+                    items = meta.get('items', [])
+                    items.append(kp_entry)
+                else:
+                    config['metadata'] = {'items': [kp_entry]}
+
+        config['labels'] = {'cblabel': label}
+
+        operation = (self.provider
+                         .gcp_compute.instances()
+                         .insert(project=self.provider.project_name,
+                                 zone=zone_name,
+                                 body=config)
+                         .execute())
+        instance_id = operation.get('targetLink')
+        self.provider.wait_for_operation(operation, zone=zone_name)
+        cb_inst = self.get(instance_id)
+        return cb_inst
+
+    @dispatch(event="provider.compute.instances.get",
+              priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
+    def get(self, instance_id):
+        """
+        Returns an instance given its name. Returns None
+        if the object does not exist.
+
+        A GCP instance is uniquely identified by its selfLink, which is used
+        as its id.
+        """
+        instance = self.provider.get_resource('instances', instance_id)
+        return GCPInstance(self.provider, instance) if instance else None
+
+    @dispatch(event="provider.compute.instances.find",
+              priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
+    def find(self, limit=None, marker=None, **kwargs):
+        """
+        Searches for instances by instance label.
+        :return: a list of Instance objects
+        """
+        label = kwargs.pop('label', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
+
+        instances = [instance for instance in self.list()
+                     if instance.label == label]
+        return ClientPagedResultList(self.provider, instances,
+                                     limit=limit, marker=marker)
+
+    @dispatch(event="provider.compute.instances.list",
+              priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        """
+        List all instances.
+        """
+        # For GCP API, Acceptable values are 0 to 500, inclusive.
+        # (Default: 500).
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_compute
+                        .instances()
+                        .list(project=self.provider.project_name,
+                              zone=self.provider.default_zone,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        instances = [GCPInstance(self.provider, inst)
+                     for inst in response.get('items', [])]
+        if len(instances) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(instances))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=instances)
+
+    @dispatch(event="provider.compute.instances.delete",
+              priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
+    def delete(self, instance):
+        instance = (instance if isinstance(instance, GCPInstance) else
+                    self.get(instance))
+        if instance:
+            (self._provider
+             .gcp_compute
+             .instances()
+             .delete(project=self.provider.project_name,
+                     zone=instance.zone_name,
+                     instance=instance.name)
+             .execute())
+
+    def create_launch_config(self):
+        return GCPLaunchConfig(self.provider)
+
+
+class GCPComputeService(BaseComputeService):
+    # TODO: implement GCPComputeService
+    def __init__(self, provider):
+        super(GCPComputeService, self).__init__(provider)
+        self._instance_svc = GCPInstanceService(self.provider)
+        self._vm_type_svc = GCPVMTypeService(self.provider)
+        self._region_svc = GCPRegionService(self.provider)
+        self._images_svc = GCPImageService(self.provider)
+
+    @property
+    def images(self):
+        return self._images_svc
+
+    @property
+    def vm_types(self):
+        return self._vm_type_svc
+
+    @property
+    def instances(self):
+        return self._instance_svc
+
+    @property
+    def regions(self):
+        return self._region_svc
+
+
+class GCPNetworkingService(BaseNetworkingService):
+
+    def __init__(self, provider):
+        super(GCPNetworkingService, self).__init__(provider)
+        self._network_service = GCPNetworkService(self.provider)
+        self._subnet_service = GCPSubnetService(self.provider)
+        self._router_service = GCPRouterService(self.provider)
+        self._gateway_service = GCPGatewayService(self.provider)
+        self._floating_ip_service = GCPFloatingIPService(self.provider)
+
+    @property
+    def networks(self):
+        return self._network_service
+
+    @property
+    def subnets(self):
+        return self._subnet_service
+
+    @property
+    def routers(self):
+        return self._router_service
+
+    @property
+    def _gateways(self):
+        return self._gateway_service
+
+    @property
+    def _floating_ips(self):
+        return self._floating_ip_service
+
+
+class GCPNetworkService(BaseNetworkService):
+
+    def __init__(self, provider):
+        super(GCPNetworkService, self).__init__(provider)
+
+    @dispatch(event="provider.networking.networks.get",
+              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def get(self, network_id):
+        network = self.provider.get_resource('networks', network_id)
+        return GCPNetwork(self.provider, network) if network else None
+
+    @dispatch(event="provider.networking.networks.find",
+              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def find(self, limit=None, marker=None, **kwargs):
+        """
+        GCP networks are global. There is at most one network with a given
+        name.
+        """
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=limit, marker=marker)
+
+    @dispatch(event="provider.networking.networks.list",
+              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None, filter=None):
+        # TODO: Decide whether we keep filter in 'list'
+        networks = []
+        response = (self.provider
+                        .gcp_compute
+                        .networks()
+                        .list(project=self.provider.project_name,
+                              filter=filter)
+                        .execute())
+        for network in response.get('items', []):
+            networks.append(GCPNetwork(self.provider, network))
+        return ClientPagedResultList(self.provider, networks,
+                                     limit=limit, marker=marker)
+
+    @dispatch(event="provider.networking.networks.create",
+              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def create(self, label, cidr_block):
+        """
+        Creates an auto mode VPC network with default subnets. It is possible
+        to add additional subnets later.
+        """
+        GCPNetwork.assert_valid_resource_label(label)
+        name = GCPNetwork._generate_name_from_label(label, 'cbnet')
+        body = {'name': name}
+        # This results in a custom mode network
+        body['autoCreateSubnetworks'] = False
+        response = (self.provider
+                        .gcp_compute
+                        .networks()
+                        .insert(project=self.provider.project_name,
+                                body=body)
+                        .execute())
+        self.provider.wait_for_operation(response)
+        cb_net = self.get(name)
+        cb_net.label = label
+        return cb_net
+
+    def get_or_create_default(self):
+        default_nets = self.provider.networking.networks.find(
+            label=GCPNetwork.CB_DEFAULT_NETWORK_LABEL)
+        if default_nets:
+            return default_nets[0]
+        else:
+            log.info("Creating a CloudBridge-default network labeled %s",
+                     GCPNetwork.CB_DEFAULT_NETWORK_LABEL)
+            return self.create(
+                label=GCPNetwork.CB_DEFAULT_NETWORK_LABEL,
+                cidr_block=GCPNetwork.CB_DEFAULT_IPV4RANGE)
+
+    @dispatch(event="provider.networking.networks.delete",
+              priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
+    def delete(self, network):
+        # Accepts network object
+        if isinstance(network, GCPNetwork):
+            name = network.name
+        # Accepts both name and ID
+        elif 'googleapis' in network:
+            name = network.split('/')[-1]
+        else:
+            name = network
+        response = (self.provider
+                        .gcp_compute
+                        .networks()
+                        .delete(project=self.provider.project_name,
+                                network=name)
+                        .execute())
+        self.provider.wait_for_operation(response)
+        # Remove label
+        tag_name = "_".join(["network", name, "label"])
+        if not helpers.remove_metadata_item(self.provider, tag_name):
+            log.warning('No label was found associated with this network '
+                        '"{}" when deleted.'.format(network))
+        return True
+
+
+class GCPRouterService(BaseRouterService):
+
+    def __init__(self, provider):
+        super(GCPRouterService, self).__init__(provider)
+
+    @dispatch(event="provider.networking.routers.get",
+              priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
+    def get(self, router_id):
+        router = self.provider.get_resource(
+            'routers', router_id, region=self.provider.region_name)
+        return GCPRouter(self.provider, router) if router else None
+
+    @dispatch(event="provider.networking.routers.find",
+              priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
+    def find(self, limit=None, marker=None, **kwargs):
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=limit, marker=marker)
+
+    @dispatch(event="provider.networking.routers.list",
+              priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        region = self.provider.region_name
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_compute
+                        .routers()
+                        .list(project=self.provider.project_name,
+                              region=region,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        routers = []
+        for router in response.get('items', []):
+            routers.append(GCPRouter(self.provider, router))
+        if len(routers) > max_result:
+            log.warning('Expected at most %d results; go %d',
+                        max_result, len(routers))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=routers)
+
+    @dispatch(event="provider.networking.routers.create",
+              priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
+    def create(self, label, network):
+        log.debug("Creating GCP Router Service with params "
+                  "[label: %s network: %s]", label, network)
+        GCPRouter.assert_valid_resource_label(label)
+        name = GCPRouter._generate_name_from_label(label, 'cb-router')
+
+        if not isinstance(network, GCPNetwork):
+            network = self.provider.networking.networks.get(network)
+        network_url = network.resource_url
+        region_name = self.provider.region_name
+        response = (self.provider
+                        .gcp_compute
+                        .routers()
+                        .insert(project=self.provider.project_name,
+                                region=region_name,
+                                body={'name': name,
+                                      'network': network_url})
+                        .execute())
+        self.provider.wait_for_operation(response, region=region_name)
+        cb_router = self.get(name)
+        cb_router.label = label
+        return cb_router
+
+    @dispatch(event="provider.networking.routers.delete",
+              priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
+    def delete(self, router):
+        r = router if isinstance(router, GCPRouter) else self.get(router)
+        if r:
+            (self.provider
+             .gcp_compute
+             .routers()
+             .delete(project=self.provider.project_name,
+                     region=r.region_name,
+                     router=r.name)
+             .execute())
+            # Remove label
+            tag_name = "_".join(["router", r.name, "label"])
+            if not helpers.remove_metadata_item(self.provider, tag_name):
+                log.warning('No label was found associated with this router '
+                            '"{}" when deleted.'.format(r.name))
+
+    def _get_in_region(self, router_id, region=None):
+        region_name = self.provider.region_name
+        if region:
+            if not isinstance(region, GCPRegion):
+                region = self.provider.compute.regions.get(region)
+            region_name = region.name
+        router = self.provider.get_resource(
+            'routers', router_id, region=region_name)
+        return GCPRouter(self.provider, router) if router else None
+
+
+class GCPSubnetService(BaseSubnetService):
+
+    def __init__(self, provider):
+        super(GCPSubnetService, self).__init__(provider)
+
+    @dispatch(event="provider.networking.subnets.get",
+              priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
+    def get(self, subnet_id):
+        subnet = self.provider.get_resource('subnetworks', subnet_id)
+        return GCPSubnet(self.provider, subnet) if subnet else None
+
+    @dispatch(event="provider.networking.subnets.list",
+              priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
+    def list(self, network=None, zone=None, limit=None, marker=None):
+        """
+        If the zone is not given, we list all subnets in the default region.
+        """
+        filter = None
+        if network is not None:
+            network = (network if isinstance(network, GCPNetwork)
+                       else self.provider.networking.networks.get(network))
+            filter = 'network eq %s' % network.resource_url
+        if zone:
+            region_name = self._zone_to_region(zone)
+        else:
+            region_name = self.provider.region_name
+        subnets = []
+        response = (self.provider
+                        .gcp_compute
+                        .subnetworks()
+                        .list(project=self.provider.project_name,
+                              region=region_name,
+                              filter=filter)
+                        .execute())
+        for subnet in response.get('items', []):
+            subnets.append(GCPSubnet(self.provider, subnet))
+        return ClientPagedResultList(self.provider, subnets,
+                                     limit=limit, marker=marker)
+
+    @dispatch(event="provider.networking.subnets.create",
+              priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
+    def create(self, label, network, cidr_block, zone):
+        """
+        GCP subnets are regional. The region is inferred from the zone;
+        otherwise, the default region, as set in the
+        provider, is used.
+
+        If a subnet with overlapping IP range exists already, we return that
+        instead of creating a new subnet. In this case, other parameters, i.e.
+        the name and the zone, are ignored.
+        """
+        GCPSubnet.assert_valid_resource_label(label)
+        name = GCPSubnet._generate_name_from_label(label, 'cbsubnet')
+        region_name = self._zone_to_region(zone)
+#         for subnet in self.iter(network=network):
+#            if BaseNetwork.cidr_blocks_overlap(subnet.cidr_block, cidr_block):
+#                 if subnet.region_name != region_name:
+#                     log.error('Failed to create subnetwork in region %s: '
+#                                  'the given IP range %s overlaps with a '
+#                                  'subnetwork in a different region %s',
+#                                  region_name, cidr_block, subnet.region_name)
+#                     return None
+#                 return subnet
+#             if subnet.label == label and subnet.region_name == region_name:
+#                 return subnet
+
+        body = {'ipCidrRange': cidr_block,
+                'name': name,
+                'network': network.resource_url,
+                'region': region_name
+                }
+        response = (self.provider
+                        .gcp_compute
+                        .subnetworks()
+                        .insert(project=self.provider.project_name,
+                                region=region_name,
+                                body=body)
+                        .execute())
+        self.provider.wait_for_operation(response, region=region_name)
+        cb_subnet = self.get(name)
+        cb_subnet.label = label
+        return cb_subnet
+
+    @dispatch(event="provider.networking.subnets.delete",
+              priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
+    def delete(self, subnet):
+        sn = subnet if isinstance(subnet, GCPSubnet) else self.get(subnet)
+        if not sn:
+            return
+        response = (self.provider
+                    .gcp_compute
+                    .subnetworks()
+                    .delete(project=self.provider.project_name,
+                            region=sn.region_name,
+                            subnetwork=sn.name)
+                    .execute())
+        self.provider.wait_for_operation(response, region=sn.region_name)
+        # Remove label
+        tag_name = "_".join(["subnet", sn.name, "label"])
+        if not helpers.remove_metadata_item(self._provider, tag_name):
+            log.warning('No label was found associated with this subnet '
+                        '"{}" when deleted.'.format(sn.name))
+
+    def get_or_create_default(self, zone):
+        """
+        Return an existing or create a new subnet for the supplied zone.
+
+        In GCP, subnets are a regional resource so a single subnet can services
+        an entire region. The supplied zone parameter is used to derive the
+        parent region under which the default subnet then exists.
+        """
+        # In case the supplied zone param is `None`, resort to the default one
+        region = self._zone_to_region(zone or self.provider.default_zone,
+                                      return_name_only=False)
+        # Check if a default subnet already exists for the given region/zone
+        for sn in self.find(label=GCPSubnet.CB_DEFAULT_SUBNET_LABEL):
+            if sn.region == region.id:
+                return sn
+        # No default subnet in the supplied zone. Look for a default network,
+        # then create a subnet whose address space does not overlap with any
+        # other existing subnets. If there are existing subnets, this process
+        # largely assumes the subnet address spaces are contiguous when it
+        # does the calculations (e.g., 10.0.0.0/24, 10.0.1.0/24).
+        cidr_block = GCPSubnet.CB_DEFAULT_SUBNET_IPV4RANGE
+        net = self.provider.networking.networks.get_or_create_default()
+        if net.subnets:
+            max_sn = net.subnets[0]
+            # Find the maximum address subnet address space within the network
+            for esn in net.subnets:
+                if (ipaddress.ip_network(esn.cidr_block) >
+                        ipaddress.ip_network(max_sn.cidr_block)):
+                    max_sn = esn
+            max_sn_ipa = ipaddress.ip_network(max_sn.cidr_block)
+            # Find the next available subnet after the max one, based on the
+            # max subnet size
+            next_sn_address = (
+                next(max_sn_ipa.hosts()) + max_sn_ipa.num_addresses - 1)
+            cidr_block = "{}/{}".format(next_sn_address, max_sn_ipa.prefixlen)
+        sn = self.provider.networking.subnets.create(
+                label=GCPSubnet.CB_DEFAULT_SUBNET_LABEL,
+                cidr_block=cidr_block, network=net, zone=zone)
+        router = self.provider.networking.routers.get_or_create_default(net)
+        router.attach_subnet(sn)
+        gateway = net.gateways.get_or_create()
+        router.attach_gateway(gateway)
+        return sn
+
+    def _zone_to_region(self, zone, return_name_only=True):
+        """
+        Given a GCP zone, return parent region.
+
+        Supplied `zone` param can be a `str` or `GCPPlacementZone`.
+
+        If ``return_name_only`` is set, return the region name as a string;
+        otherwise, return a GCPRegion object.
+        """
+        region_name = self.provider.region_name
+        if zone:
+            if isinstance(zone, GCPPlacementZone):
+                region_name = zone.region_name
+            else:
+                region_name = zone[:-2]
+        if return_name_only:
+            return region_name
+        return self.provider.compute.regions.get(region_name)
+
+
+class GCPStorageService(BaseStorageService):
+
+    def __init__(self, provider):
+        super(GCPStorageService, self).__init__(provider)
+
+        # Initialize provider services
+        self._volume_svc = GCPVolumeService(self.provider)
+        self._snapshot_svc = GCPSnapshotService(self.provider)
+        self._bucket_svc = GCPBucketService(self.provider)
+        self._bucket_obj_svc = GCPBucketObjectService(self.provider)
+
+    @property
+    def volumes(self):
+        return self._volume_svc
+
+    @property
+    def snapshots(self):
+        return self._snapshot_svc
+
+    @property
+    def buckets(self):
+        return self._bucket_svc
+
+    @property
+    def _bucket_objects(self):
+        return self._bucket_obj_svc
+
+
+class GCPVolumeService(BaseVolumeService):
+
+    def __init__(self, provider):
+        super(GCPVolumeService, self).__init__(provider)
+
+    @dispatch(event="provider.storage.volumes.get",
+              priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
+    def get(self, volume_id):
+        vol = self.provider.get_resource('disks', volume_id)
+        return GCPVolume(self.provider, vol) if vol else None
+
+    @dispatch(event="provider.storage.volumes.find",
+              priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
+    def find(self, limit=None, marker=None, **kwargs):
+        """
+        Searches for a volume by a given list of attributes.
+        """
+        label = kwargs.pop('label', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
+
+        filtr = 'labels.cblabel eq ' + label
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_compute
+                        .disks()
+                        .list(project=self.provider.project_name,
+                              zone=self.provider.default_zone,
+                              filter=filtr,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        gcp_vols = [GCPVolume(self.provider, vol)
+                    for vol in response.get('items', [])]
+        if len(gcp_vols) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(gcp_vols))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=gcp_vols)
+
+    @dispatch(event="provider.storage.volumes.list",
+              priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        """
+        List all volumes.
+
+        limit: The maximum number of volumes to return. The returned
+               ResultList's is_truncated property can be used to determine
+               whether more records are available.
+        """
+        # For GCP API, Acceptable values are 0 to 500, inclusive.
+        # (Default: 500).
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_compute
+                        .disks()
+                        .list(project=self.provider.project_name,
+                              zone=self.provider.default_zone,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        gcp_vols = [GCPVolume(self.provider, vol)
+                    for vol in response.get('items', [])]
+        if len(gcp_vols) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(gcp_vols))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=gcp_vols)
+
+    @dispatch(event="provider.storage.volumes.create",
+              priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
+    def create(self, label, size, zone, snapshot=None, description=None):
+        GCPVolume.assert_valid_resource_label(label)
+        name = GCPVolume._generate_name_from_label(label, 'cb-vol')
+        if not isinstance(zone, GCPPlacementZone):
+            zone = GCPPlacementZone(
+                self.provider,
+                self.provider.get_resource('zones', zone))
+        zone_name = zone.name
+        snapshot_id = snapshot.id if isinstance(
+            snapshot, GCPSnapshot) and snapshot else snapshot
+        labels = {'cblabel': label}
+        if description:
+            labels['description'] = description
+        disk_body = {
+            'name': name,
+            'sizeGb': size,
+            'type': 'zones/{0}/diskTypes/{1}'.format(zone_name, 'pd-standard'),
+            'sourceSnapshot': snapshot_id,
+            'labels': labels
+        }
+        operation = (self.provider
+                         .gcp_compute
+                         .disks()
+                         .insert(
+                             project=self._provider.project_name,
+                             zone=zone_name,
+                             body=disk_body)
+                         .execute())
+        cb_vol = self.get(operation.get('targetLink'))
+        return cb_vol
+
+    @dispatch(event="provider.storage.volumes.delete",
+              priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
+    def delete(self, volume):
+        volume = volume if isinstance(volume, GCPVolume) else self.get(volume)
+        if volume:
+            (self._provider.gcp_compute
+                           .disks()
+                           .delete(project=self.provider.project_name,
+                                   zone=volume.zone_name,
+                                   disk=volume.name)
+                           .execute())
+
+
+class GCPSnapshotService(BaseSnapshotService):
+
+    def __init__(self, provider):
+        super(GCPSnapshotService, self).__init__(provider)
+
+    @dispatch(event="provider.storage.snapshots.get",
+              priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
+    def get(self, snapshot_id):
+        snapshot = self.provider.get_resource('snapshots', snapshot_id)
+        return GCPSnapshot(self.provider, snapshot) if snapshot else None
+
+    @dispatch(event="provider.storage.snapshots.find",
+              priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
+    def find(self, limit=None, marker=None, **kwargs):
+        label = kwargs.pop('label', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'label'))
+
+        filtr = 'labels.cblabel eq ' + label
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_compute
+                        .snapshots()
+                        .list(project=self.provider.project_name,
+                              filter=filtr,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        snapshots = [GCPSnapshot(self.provider, snapshot)
+                     for snapshot in response.get('items', [])]
+        if len(snapshots) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(snapshots))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=snapshots)
+
+    @dispatch(event="provider.storage.snapshots.list",
+              priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_compute
+                        .snapshots()
+                        .list(project=self.provider.project_name,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        snapshots = [GCPSnapshot(self.provider, snapshot)
+                     for snapshot in response.get('items', [])]
+        if len(snapshots) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(snapshots))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=snapshots)
+
+    @dispatch(event="provider.storage.snapshots.create",
+              priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
+    def create(self, label, volume, description=None):
+        GCPSnapshot.assert_valid_resource_label(label)
+        name = GCPSnapshot._generate_name_from_label(label, 'cbsnap')
+        volume_name = volume.name if isinstance(volume, GCPVolume) else volume
+        labels = {'cblabel': label}
+        if description:
+            labels['description'] = description
+        snapshot_body = {
+            "name": name,
+            "labels": labels
+        }
+        operation = (self.provider
+                         .gcp_compute
+                         .disks()
+                         .createSnapshot(
+                             project=self.provider.project_name,
+                             zone=self.provider.default_zone,
+                             disk=volume_name, body=snapshot_body)
+                         .execute())
+        if 'zone' not in operation:
+            return None
+        self.provider.wait_for_operation(operation,
+                                         zone=self.provider.default_zone)
+        cb_snap = self.get(name)
+        return cb_snap
+
+    @dispatch(event="provider.storage.snapshots.delete",
+              priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
+    def delete(self, snapshot):
+        snapshot = (snapshot if isinstance(snapshot, GCPSnapshot)
+                    else self.get(snapshot))
+        if snapshot:
+            (self.provider
+                 .gcp_compute
+                 .snapshots()
+                 .delete(project=self.provider.project_name,
+                         snapshot=snapshot.name)
+                 .execute())
+
+
+class GCPBucketService(BaseBucketService):
+
+    def __init__(self, provider):
+        super(GCPBucketService, self).__init__(provider)
+
+    @dispatch(event="provider.storage.buckets.get",
+              priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def get(self, bucket_id):
+        """
+        Returns a bucket given its ID. Returns ``None`` if the bucket
+        does not exist or if the user does not have permission to access the
+        bucket.
+        """
+        bucket = self.provider.get_resource('buckets', bucket_id)
+        return GCPBucket(self.provider, bucket) if bucket else None
+
+    @dispatch(event="provider.storage.buckets.find",
+              priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def find(self, limit=None, marker=None, **kwargs):
+        name = kwargs.pop('name', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise InvalidParamException(
+                "Unrecognised parameters for search: %s. Supported "
+                "attributes: %s" % (kwargs, 'name'))
+
+        buckets = [bucket for bucket in self if name in bucket.name]
+        return ClientPagedResultList(self.provider, buckets, limit=limit,
+                                     marker=marker)
+
+    @dispatch(event="provider.storage.buckets.list",
+              priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def list(self, limit=None, marker=None):
+        """
+        List all containers.
+        """
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_storage
+                        .buckets()
+                        .list(project=self.provider.project_name,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        buckets = []
+        for bucket in response.get('items', []):
+            buckets.append(GCPBucket(self.provider, bucket))
+        if len(buckets) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(buckets))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=buckets)
+
+    @dispatch(event="provider.storage.buckets.create",
+              priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def create(self, name, location=None):
+        GCPBucket.assert_valid_resource_name(name)
+        body = {'name': name}
+        if location:
+            body['location'] = location
+        try:
+            response = (self.provider
+                            .gcp_storage
+                            .buckets()
+                            .insert(project=self.provider.project_name,
+                                    body=body)
+                            .execute())
+            # GCP has a rate limit of 1 operation per 2 seconds for bucket
+            # creation/deletion: https://cloud.google.com/storage/quotas.
+            # Throttle here to avoid future failures.
+            time.sleep(2)
+            return GCPBucket(self.provider, response)
+        except googleapiclient.errors.HttpError as http_error:
+            # 409 = conflict
+            if http_error.resp.status in [409]:
+                raise DuplicateResourceException(
+                    'Bucket already exists with name {0}'.format(name))
+            else:
+                raise
+
+    @dispatch(event="provider.storage.buckets.delete",
+              priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
+    def delete(self, bucket):
+        """
+        Delete this bucket.
+        """
+        b = bucket if isinstance(bucket, GCPBucket) else self.get(bucket)
+        if b:
+            (self.provider
+                 .gcp_storage
+                 .buckets()
+                 .delete(bucket=b.name)
+                 .execute())
+            # GCP has a rate limit of 1 operation per 2 seconds for bucket
+            # creation/deletion: https://cloud.google.com/storage/quotas.
+            # Throttle here to avoid future failures.
+            time.sleep(2)
+
+
+class GCPBucketObjectService(BaseBucketObjectService):
+
+    def __init__(self, provider):
+        super(GCPBucketObjectService, self).__init__(provider)
+
+    def get(self, bucket, name):
+        """
+        Retrieve a given object from this bucket.
+        """
+        obj = self.provider.get_resource('objects', name,
+                                         bucket=bucket.name)
+        return GCPBucketObject(self.provider, bucket, obj) if obj else None
+
+    def list(self, bucket, limit=None, marker=None, prefix=None):
+        """
+        List all objects within this bucket.
+        """
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_storage
+                        .objects()
+                        .list(bucket=bucket.name,
+                              prefix=prefix if prefix else '',
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        objects = []
+        for obj in response.get('items', []):
+            objects.append(GCPBucketObject(self.provider, bucket, obj))
+        if len(objects) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(objects))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=objects)
+
+    def find(self, bucket, limit=None, marker=None, **kwargs):
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, bucket.objects)
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=limit, marker=marker)
+
+    def _create_object_with_media_body(self, bucket, name, media_body):
+        response = (self.provider
+                    .gcp_storage
+                    .objects()
+                    .insert(bucket=bucket.name,
+                            body={'name': name},
+                            media_body=media_body)
+                    .execute())
+        return response
+
+    def create(self, bucket, name):
+        response = self._create_object_with_media_body(
+                            bucket,
+                            name,
+                            googleapiclient.http.MediaIoBaseUpload(
+                                io.BytesIO(b''), mimetype='plain/text'))
+        return GCPBucketObject(self._provider,
+                               bucket,
+                               response) if response else None
+
+
+class GCPGatewayService(BaseGatewayService):
+    _DEFAULT_GATEWAY_NAME = 'default-internet-gateway'
+    _GATEWAY_URL_PREFIX = 'global/gateways/'
+
+    def __init__(self, provider):
+        super(GCPGatewayService, self).__init__(provider)
+        self._default_internet_gateway = GCPInternetGateway(
+            provider,
+            {'id': (GCPGatewayService._GATEWAY_URL_PREFIX +
+                    GCPGatewayService._DEFAULT_GATEWAY_NAME),
+             'name': GCPGatewayService._DEFAULT_GATEWAY_NAME})
+
+    @dispatch(event="provider.networking.gateways.get_or_create",
+              priority=BaseGatewayService.STANDARD_EVENT_PRIORITY)
+    def get_or_create(self, network):
+        return self._default_internet_gateway
+
+    @dispatch(event="provider.networking.gateways.delete",
+              priority=BaseGatewayService.STANDARD_EVENT_PRIORITY)
+    def delete(self, network, gateway):
+        pass
+
+    @dispatch(event="provider.networking.gateways.list",
+              priority=BaseGatewayService.STANDARD_EVENT_PRIORITY)
+    def list(self, network, limit=None, marker=None):
+        gws = [self._default_internet_gateway]
+        return ClientPagedResultList(self._provider,
+                                     gws,
+                                     limit=limit, marker=marker)
+
+
+class GCPFloatingIPService(BaseFloatingIPService):
+
+    def __init__(self, provider):
+        super(GCPFloatingIPService, self).__init__(provider)
+
+    @dispatch(event="provider.networking.floating_ips.get",
+              priority=BaseFloatingIPService.STANDARD_EVENT_PRIORITY)
+    def get(self, gateway, floating_ip_id):
+        fip = self.provider.get_resource('addresses', floating_ip_id)
+        return (GCPFloatingIP(self.provider, fip)
+                if fip else None)
+
+    @dispatch(event="provider.networking.floating_ips.list",
+              priority=BaseFloatingIPService.STANDARD_EVENT_PRIORITY)
+    def list(self, gateway, limit=None, marker=None):
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gcp_compute
+                        .addresses()
+                        .list(project=self.provider.project_name,
+                              region=self.provider.region_name,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        ips = [GCPFloatingIP(self.provider, ip)
+               for ip in response.get('items', [])]
+        if len(ips) > max_result:
+            log.warning('Expected at most %d results; got %d',
+                        max_result, len(ips))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=ips)
+
+    @dispatch(event="provider.networking.floating_ips.create",
+              priority=BaseFloatingIPService.STANDARD_EVENT_PRIORITY)
+    def create(self, gateway):
+        region_name = self.provider.region_name
+        ip_name = 'ip-{0}'.format(uuid.uuid4())
+        response = (self.provider
+                    .gcp_compute
+                    .addresses()
+                    .insert(project=self.provider.project_name,
+                            region=region_name,
+                            body={'name': ip_name})
+                    .execute())
+        self.provider.wait_for_operation(response, region=region_name)
+        return self.get(gateway, ip_name)
+
+    @dispatch(event="provider.networking.floating_ips.delete",
+              priority=BaseFloatingIPService.STANDARD_EVENT_PRIORITY)
+    def delete(self, gateway, fip):
+        fip = (fip if isinstance(fip, GCPFloatingIP)
+               else self.get(gateway, fip))
+        project_name = self.provider.project_name
+        # First, delete the forwarding rule, if there is any.
+        # pylint:disable=protected-access
+        if fip._rule:
+            response = (self.provider
+                        .gcp_compute
+                        .forwardingRules()
+                        .delete(project=project_name,
+                                region=fip.region_name,
+                                forwardingRule=fip._rule['name'])
+                        .execute())
+            self.provider.wait_for_operation(response,
+                                             region=fip.region_name)
+
+        # Release the address.
+        response = (self.provider
+                    .gcp_compute
+                    .addresses()
+                    .delete(project=project_name,
+                            region=fip.region_name,
+                            address=fip._ip['name'])
+                    .execute())
+        self.provider.wait_for_operation(response,
+                                         region=fip.region_name)

+ 39 - 0
cloudbridge/cloud/providers/gcp/subservices.py

@@ -0,0 +1,39 @@
+import logging
+
+from cloudbridge.cloud.base.subservices import BaseBucketObjectSubService
+from cloudbridge.cloud.base.subservices import BaseFloatingIPSubService
+from cloudbridge.cloud.base.subservices import BaseGatewaySubService
+from cloudbridge.cloud.base.subservices import BaseSubnetSubService
+from cloudbridge.cloud.base.subservices import BaseVMFirewallRuleSubService
+
+
+log = logging.getLogger(__name__)
+
+
+class GCPBucketObjectSubService(BaseBucketObjectSubService):
+
+    def __init__(self, provider, bucket):
+        super(GCPBucketObjectSubService, self).__init__(provider, bucket)
+
+
+class GCPGatewaySubService(BaseGatewaySubService):
+    def __init__(self, provider, network):
+        super(GCPGatewaySubService, self).__init__(provider, network)
+
+
+class GCPVMFirewallRuleSubService(BaseVMFirewallRuleSubService):
+
+    def __init__(self, provider, firewall):
+        super(GCPVMFirewallRuleSubService, self).__init__(provider, firewall)
+
+
+class GCPFloatingIPSubService(BaseFloatingIPSubService):
+
+    def __init__(self, provider, gateway):
+        super(GCPFloatingIPSubService, self).__init__(provider, gateway)
+
+
+class GCPSubnetSubService(BaseSubnetSubService):
+
+    def __init__(self, provider, network):
+        super(GCPSubnetSubService, self).__init__(provider, network)

+ 5 - 0
cloudbridge/cloud/providers/mock/__init__.py

@@ -0,0 +1,5 @@
+"""
+Exports from this provider
+"""
+
+from .provider import MockAWSCloudProvider  # noqa

+ 100 - 0
cloudbridge/cloud/providers/mock/provider.py

@@ -0,0 +1,100 @@
+"""
+    Provider implementation based on the moto library (mock boto). This mock
+    provider is useful for running tests against cloudbridge but should not
+    be used in tandem with other providers, in particular the AWS provider.
+    This is because instantiating this provider will result in all calls to
+    boto being hijacked, which will cause AWS to malfunction.
+    See notes below.
+"""
+from moto import mock_ec2
+from moto import mock_s3
+
+import responses
+
+from ..aws import AWSCloudProvider
+from ...interfaces.provider import TestMockHelperMixin
+
+
+class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
+    """
+    Using this mock driver will result in all boto communications being
+    hijacked. As a result, this mock driver and the AWS driver cannot be used
+    at the same time. Do not instantiate the mock driver if you plan to use
+    the AWS provider within the same python process. Alternatively, call
+    provider.tearDownMock() to stop the hijacking.
+    """
+    PROVIDER_ID = 'mock'
+
+    def __init__(self, config):
+        self.setUpMock()
+        super(MockAWSCloudProvider, self).__init__(config)
+
+    def setUpMock(self):
+        """
+        Let Moto take over all socket communications
+        """
+        self.ec2mock = mock_ec2()
+        self.ec2mock.start()
+        self.s3mock = mock_s3()
+        self.s3mock.start()
+        responses.add(
+            responses.GET,
+            self.AWS_INSTANCE_DATA_DEFAULT_URL,
+            body=u"""
+[
+  {
+    "family": "General Purpose",
+    "enhanced_networking": false,
+    "vCPU": 1,
+    "generation": "current",
+    "ebs_iops": 0,
+    "network_performance": "Low",
+    "ebs_throughput": 0,
+    "vpc": {
+      "ips_per_eni": 2,
+      "max_enis": 2
+    },
+    "arch": [
+      "x86_64"
+    ],
+    "linux_virtualization_types": [
+        "HVM"
+    ],
+    "pricing": {
+        "us-east-1": {
+            "linux": {
+                "ondemand": "0.0058",
+                "reserved": {
+                    "yrTerm1Convertible.allUpfront": "0.003881",
+                    "yrTerm1Convertible.noUpfront": "0.0041",
+                    "yrTerm1Convertible.partialUpfront": "0.003941",
+                    "yrTerm1Standard.allUpfront": "0.003311",
+                    "yrTerm1Standard.noUpfront": "0.0036",
+                    "yrTerm1Standard.partialUpfront": "0.003412",
+                    "yrTerm3Convertible.allUpfront": "0.002626",
+                    "yrTerm3Convertible.noUpfront": "0.0029",
+                    "yrTerm3Convertible.partialUpfront": "0.002632",
+                    "yrTerm3Standard.allUpfront": "0.002169",
+                    "yrTerm3Standard.noUpfront": "0.0025",
+                    "yrTerm3Standard.partialUpfront": "0.002342"
+                }
+            }
+        }
+    },
+    "ebs_optimized": false,
+    "storage": null,
+    "max_bandwidth": 0,
+    "instance_type": "t2.nano",
+    "ECU": "variable",
+    "memory": 0.5,
+    "ebs_max_bandwidth": 0
+  }
+]
+""")
+
+    def tearDownMock(self):
+        """
+        Stop Moto intercepting all socket communications
+        """
+        self.s3mock.stop()
+        self.ec2mock.stop()

+ 96 - 296
cloudbridge/cloud/providers/openstack/resources.py

@@ -5,6 +5,8 @@ import inspect
 import ipaddress
 import logging
 import os
+import re
+
 try:
     from urllib.parse import urlparse
     from urllib.parse import urljoin
@@ -14,26 +16,17 @@ except ImportError:  # python 2
 
 from keystoneclient.v3.regions import Region
 
-from neutronclient.common.exceptions import PortNotFoundClient
-
 import novaclient.exceptions as novaex
 
-from openstack.exceptions import HttpException
-from openstack.exceptions import NotFoundException
-from openstack.exceptions import ResourceNotFound
-
 import swiftclient
-from swiftclient.service import SwiftService, SwiftUploadObject
+from swiftclient.service import SwiftService
+from swiftclient.service import SwiftUploadObject
 from swiftclient.utils import generate_temp_url
 
-import cloudbridge.cloud.base.helpers as cb_helpers
 from cloudbridge.cloud.base.resources import BaseAttachmentInfo
 from cloudbridge.cloud.base.resources import BaseBucket
-from cloudbridge.cloud.base.resources import BaseBucketContainer
 from cloudbridge.cloud.base.resources import BaseBucketObject
 from cloudbridge.cloud.base.resources import BaseFloatingIP
-from cloudbridge.cloud.base.resources import BaseFloatingIPContainer
-from cloudbridge.cloud.base.resources import BaseGatewayContainer
 from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInternetGateway
 from cloudbridge.cloud.base.resources import BaseKeyPair
@@ -46,11 +39,8 @@ from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSubnet
 from cloudbridge.cloud.base.resources import BaseVMFirewall
 from cloudbridge.cloud.base.resources import BaseVMFirewallRule
-from cloudbridge.cloud.base.resources import BaseVMFirewallRuleContainer
 from cloudbridge.cloud.base.resources import BaseVMType
 from cloudbridge.cloud.base.resources import BaseVolume
-from cloudbridge.cloud.base.resources import ClientPagedResultList
-from cloudbridge.cloud.interfaces.exceptions import InvalidValueException
 from cloudbridge.cloud.interfaces.resources import GatewayState
 from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
@@ -60,7 +50,12 @@ from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SubnetState
 from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VolumeState
-from cloudbridge.cloud.providers.openstack import helpers as oshelpers
+
+from .subservices import OpenStackBucketObjectSubService
+from .subservices import OpenStackFloatingIPSubService
+from .subservices import OpenStackGatewaySubService
+from .subservices import OpenStackSubnetSubService
+from .subservices import OpenStackVMFirewallRuleSubService
 
 ONE_GIG = 1048576000  # in bytes
 FIVE_GIG = ONE_GIG * 5  # in bytes
@@ -157,7 +152,8 @@ class OpenStackMachineImage(BaseMachineImage):
         log.debug("Refreshing OpenStack Machine Image")
         image = self._provider.compute.images.get(self.id)
         if image:
-            self._os_image = image._os_image  # pylint:disable=protected-access
+            # pylint:disable=protected-access
+            self._os_image = image._os_image
         else:
             # The image no longer exists and cannot be refreshed.
             # set the status to unknown
@@ -368,17 +364,6 @@ class OpenStackInstance(BaseInstance):
         """
         self._os_instance.reboot()
 
-    def delete(self):
-        """
-        Permanently delete this instance.
-        """
-        # delete the port we created when launching
-        # Assumption: it's the first interface in the list
-        iface_list = self._os_instance.interface_list()
-        if iface_list:
-            self._provider.neutron.delete_port(iface_list[0].port_id)
-        self._os_instance.delete()
-
     @property
     def image_id(self):
         """
@@ -461,9 +446,7 @@ class OpenStackInstance(BaseInstance):
 
     def _get_fip(self, floating_ip):
         """Get a floating IP object based on the supplied ID."""
-        return OpenStackFloatingIP(
-            self._provider,
-            self._provider.os_conn.network.get_ip(floating_ip))
+        return self._provider.networking._floating_ips.get(floating_ip)
 
     def add_floating_ip(self, floating_ip):
         """
@@ -665,12 +648,6 @@ class OpenStackVolume(BaseVolume):
         return self._provider.storage.snapshots.create(
             label, self, description=description)
 
-    def delete(self):
-        """
-        Delete this volume.
-        """
-        self._volume.delete()
-
     @property
     def state(self):
         return OpenStackVolume.VOLUME_STATE_MAP.get(
@@ -684,6 +661,7 @@ class OpenStackVolume(BaseVolume):
         vol = self._provider.storage.volumes.get(
             self.id)
         if vol:
+            # pylint:disable=protected-access
             self._volume = vol._volume  # pylint:disable=protected-access
         else:
             # The volume no longer exists and cannot be refreshed.
@@ -766,18 +744,13 @@ class OpenStackSnapshot(BaseSnapshot):
         snap = self._provider.storage.snapshots.get(
             self.id)
         if snap:
-            self._snapshot = snap._snapshot  # pylint:disable=protected-access
+            # pylint:disable=protected-access
+            self._snapshot = snap._snapshot
         else:
             # The snapshot no longer exists and cannot be refreshed.
             # set the status to unknown
             self._snapshot.status = 'unknown'
 
-    def delete(self):
-        """
-        Delete this snapshot.
-        """
-        self._snapshot.delete()
-
     def create_volume(self, placement, size=None, volume_type=None, iops=None):
         """
         Create a new Volume from this Snapshot.
@@ -792,50 +765,6 @@ class OpenStackSnapshot(BaseSnapshot):
         return cb_vol
 
 
-class OpenStackGatewayContainer(BaseGatewayContainer):
-    """For OpenStack, an internet gateway is a just an 'external' network."""
-
-    def __init__(self, provider, network):
-        super(OpenStackGatewayContainer, self).__init__(provider, network)
-
-    def _check_fip_connectivity(self, external_net):
-        # Due to current limitations in OpenStack:
-        # https://bugs.launchpad.net/neutron/+bug/1743480, it's not
-        # possible to differentiate between floating ip networks and provider
-        # external networks. Therefore, we systematically step through
-        # all available networks and perform an assignment test to infer valid
-        # floating ip nets.
-        dummy_router = self._provider.networking.routers.create(
-            label='cb-conn-test-router', network=self._network)
-        with cb_helpers.cleanup_action(lambda: dummy_router.delete()):
-            try:
-                dummy_router.attach_gateway(external_net)
-                return True
-            except Exception:
-                return False
-
-    def get_or_create_inet_gateway(self):
-        """For OS, inet gtw is any net that has `external` property set."""
-        external_nets = (n for n in self._provider.networking.networks
-                         if n.external)
-        for net in external_nets:
-            if self._check_fip_connectivity(net):
-                return OpenStackInternetGateway(self._provider, net)
-        return None
-
-    def delete(self, gateway):
-        log.debug("Deleting OpenStack Gateway: %s", gateway)
-        gateway.delete()
-
-    def list(self, limit=None, marker=None):
-        log.debug("OpenStack listing of all current internet gateways")
-        igl = [OpenStackInternetGateway(self._provider, n)
-               for n in self._provider.networking.networks
-               if n.external and self._check_fip_connectivity(n)]
-        return ClientPagedResultList(self._provider, igl, limit=limit,
-                                     marker=marker)
-
-
 class OpenStackNetwork(BaseNetwork):
 
     # Ref: https://github.com/openstack/neutron/blob/master/neutron/plugins/
@@ -854,7 +783,8 @@ class OpenStackNetwork(BaseNetwork):
     def __init__(self, provider, network):
         super(OpenStackNetwork, self).__init__(provider)
         self._network = network
-        self._gateway_service = OpenStackGatewayContainer(provider, self)
+        self._gateway_service = OpenStackGatewaySubService(provider, self)
+        self._subnet_svc = OpenStackSubnetSubService(provider, self)
 
     @property
     def id(self):
@@ -869,7 +799,7 @@ class OpenStackNetwork(BaseNetwork):
         return self._network.get('name', None)
 
     @label.setter
-    def label(self, value):  # pylint:disable=arguments-differ
+    def label(self, value):
         """
         Set the network label.
         """
@@ -894,26 +824,9 @@ class OpenStackNetwork(BaseNetwork):
         # OpenStack does not define a CIDR block for networks
         return ''
 
-    def delete(self):
-        if not self.external and self.id in str(
-                self._provider.neutron.list_networks()):
-            # If there are ports associated with the network, it won't delete
-            ports = self._provider.neutron.list_ports(
-                network_id=self.id).get('ports', [])
-            for port in ports:
-                try:
-                    self._provider.neutron.delete_port(port.get('id'))
-                except PortNotFoundClient:
-                    # Ports could have already been deleted if instances
-                    # are terminated etc. so exceptions can be safely ignored
-                    pass
-            self._provider.neutron.delete_network(self.id)
-
     @property
     def subnets(self):
-        subnets = (self._provider.neutron.list_subnets(network_id=self.id)
-                   .get('subnets', []))
-        return [OpenStackSubnet(self._provider, subnet) for subnet in subnets]
+        return self._subnet_svc
 
     def refresh(self):
         """Refresh the state of this network by re-querying the provider."""
@@ -922,8 +835,8 @@ class OpenStackNetwork(BaseNetwork):
             # pylint:disable=protected-access
             self._network = network._network
         else:
-            # subnet no longer exists
-            self._network.state = NetworkState.UNKNOWN
+            # Network no longer exists
+            self._network = {}
 
     @property
     def gateways(self):
@@ -976,10 +889,6 @@ class OpenStackSubnet(BaseSubnet):
         """
         return None
 
-    def delete(self):
-        if self.id in str(self._provider.neutron.list_subnets()):
-            self._provider.neutron.delete_subnet(self.id)
-
     @property
     def state(self):
         return SubnetState.UNKNOWN if self._state == SubnetState.UNKNOWN \
@@ -996,33 +905,6 @@ class OpenStackSubnet(BaseSubnet):
             self._state = SubnetState.UNKNOWN
 
 
-class OpenStackFloatingIPContainer(BaseFloatingIPContainer):
-
-    def __init__(self, provider, gateway):
-        super(OpenStackFloatingIPContainer, self).__init__(provider, gateway)
-
-    def get(self, fip_id):
-        try:
-            return OpenStackFloatingIP(
-                self._provider, self._provider.os_conn.network.get_ip(fip_id))
-        except (ResourceNotFound, NotFoundException):
-            log.debug("Floating IP %s not found.", fip_id)
-            return None
-
-    def list(self, limit=None, marker=None):
-        fips = [OpenStackFloatingIP(self._provider, fip)
-                for fip in self._provider.os_conn.network.ips(
-                    floating_network_id=self.gateway.id
-                )]
-        return ClientPagedResultList(self._provider, fips,
-                                     limit=limit, marker=marker)
-
-    def create(self):
-        return OpenStackFloatingIP(
-            self._provider, self._provider.os_conn.network.create_ip(
-                floating_network_id=self.gateway.id))
-
-
 class OpenStackFloatingIP(BaseFloatingIP):
 
     def __init__(self, provider, floating_ip):
@@ -1045,17 +927,18 @@ class OpenStackFloatingIP(BaseFloatingIP):
     def in_use(self):
         return bool(self._ip.port_id)
 
-    def delete(self):
-        self._ip.delete(self._provider.os_conn.session)
-
     def refresh(self):
         net = self._provider.networking.networks.get(
             self._ip.floating_network_id)
-        gw = net.gateways.get_or_create_inet_gateway()
+        gw = net.gateways.get_or_create()
         fip = gw.floating_ips.get(self.id)
         # pylint:disable=protected-access
         self._ip = fip._ip
 
+    @property
+    def _gateway_id(self):
+        return self._ip.floating_network_id
+
 
 class OpenStackRouter(BaseRouter):
 
@@ -1065,7 +948,7 @@ class OpenStackRouter(BaseRouter):
 
     @property
     def id(self):
-        return self._router.get('id', None)
+        return getattr(self._router, 'id', None)
 
     @property
     def name(self):
@@ -1073,7 +956,7 @@ class OpenStackRouter(BaseRouter):
 
     @property
     def label(self):
-        return self._router.get('name', None)
+        return self._router.name
 
     @label.setter
     def label(self, value):  # pylint:disable=arguments-differ
@@ -1081,65 +964,60 @@ class OpenStackRouter(BaseRouter):
         Set the router label.
         """
         self.assert_valid_resource_label(value)
-        self._provider.neutron.update_router(
-            self.id, {'router': {'name': value or ""}})
-        self.refresh()
+        self._router = self._provider.os_conn.update_router(self.id, value)
 
     def refresh(self):
-        self._router = self._provider.neutron.show_router(self.id)['router']
+        self._router = self._provider.os_conn.get_router(self.id)
 
     @property
     def state(self):
-        if self._router.get('external_gateway_info'):
+        if self._router.external_gateway_info:
             return RouterState.ATTACHED
         return RouterState.DETACHED
 
     @property
     def network_id(self):
-        if self.state == RouterState.ATTACHED:
-            return self._router.get('external_gateway_info', {}).get(
-                'network_id', None)
+        ports = self._provider.os_conn.list_ports(
+            filters={'device_id': self.id})
+        if ports:
+            return ports[0].network_id
         return None
 
-    def delete(self):
-        self._provider.neutron.delete_router(self.id)
-
     def attach_subnet(self, subnet):
-        router_interface = {'subnet_id': subnet.id}
-        ret = self._provider.neutron.add_interface_router(
-            self.id, router_interface)
+        ret = self._provider.os_conn.add_router_interface(
+            self._router.toDict(), subnet.id)
         if subnet.id in ret.get('subnet_ids', ""):
             return True
         return False
 
     def detach_subnet(self, subnet):
-        router_interface = {'subnet_id': subnet.id}
-        ret = self._provider.neutron.remove_interface_router(
-            self.id, router_interface)
-        if subnet.id in ret.get('subnet_ids', ""):
+        ret = self._provider.os_conn.remove_router_interface(
+            self._router.toDict(), subnet.id)
+        if not ret or subnet.id not in ret.get('subnet_ids', ""):
             return True
         return False
 
     @property
     def subnets(self):
-        # A router and a subnet are linked via a port, so traverse all ports
-        # to find a list of subnets associated with the current router.
+        # A router and a subnet are linked via a port, so traverse ports
+        # associated with the current router to find a list of subnets
+        # associated with it.
         subnets = []
-        for prt in self._provider.neutron.list_ports().get('ports'):
-            if prt.get('device_id') == self.id and \
-               prt.get('device_owner') == 'network:router_interface':
-                for fixed_ip in prt.get('fixed_ips'):
-                    subnets.append(self._provider.networking.subnets.get(
-                        fixed_ip.get('subnet_id')))
+        for port in self._provider.os_conn.list_ports(
+                filters={'device_id': self.id}):
+            for fixed_ip in port.fixed_ips:
+                subnets.append(self._provider.networking.subnets.get(
+                    fixed_ip.get('subnet_id')))
         return subnets
 
     def attach_gateway(self, gateway):
-        self._provider.neutron.add_gateway_router(
-            self.id, {'network_id': gateway.id})
+        self._provider.os_conn.update_router(
+            self.id, ext_gateway_net_id=gateway.id)
 
     def detach_gateway(self, gateway):
-        self._provider.neutron.remove_gateway_router(
-            self.id).get('router', self._router)
+        # TODO: OpenStack SDK Connection object doesn't appear to have a method
+        # for detaching/clearing the external gateway.
+        self._provider.neutron.remove_gateway_router(self.id)
 
 
 class OpenStackInternetGateway(BaseInternetGateway):
@@ -1158,7 +1036,7 @@ class OpenStackInternetGateway(BaseInternetGateway):
             # pylint:disable=protected-access
             gateway_net = gateway_net._network
         self._gateway_net = gateway_net
-        self._fips_container = OpenStackFloatingIPContainer(provider, self)
+        self._fips_container = OpenStackFloatingIPSubService(provider, self)
 
     @property
     def id(self):
@@ -1187,10 +1065,6 @@ class OpenStackInternetGateway(BaseInternetGateway):
         return self.GATEWAY_STATE_MAP.get(
             self._gateway_net.state, GatewayState.UNKNOWN)
 
-    def delete(self):
-        """Do nothing on openstack"""
-        pass
-
     @property
     def floating_ips(self):
         return self._fips_container
@@ -1203,19 +1077,54 @@ class OpenStackKeyPair(BaseKeyPair):
 
 
 class OpenStackVMFirewall(BaseVMFirewall):
+    _network_id_tag = "CB-auto-associated-network-id: "
 
     def __init__(self, provider, vm_firewall):
         super(OpenStackVMFirewall, self).__init__(provider, vm_firewall)
-        self._rule_svc = OpenStackVMFirewallRuleContainer(provider, self)
+        self._rule_svc = OpenStackVMFirewallRuleSubService(provider, self)
 
     @property
     def network_id(self):
         """
-        OpenStack does not associate a SG with a network so default to None.
+        OpenStack does not associate a fw with a network so extract from desc.
 
-        :return: Always return ``None``.
+        :return: The network ID supplied when this firewall was created or
+                 `None` if ID cannot be identified.
         """
-        return None
+        # Extracting networking ID from description
+        exp = ".*\\[" + self._network_id_tag + "([^\\]]*)\\].*"
+        matches = re.match(exp, self._description)
+        if matches:
+            return matches.group(1)
+        # We generally simulate a network being associated with a firewall;
+        # however, because of some networking specificity in Nectar, we must
+        # allow `None` return value as well in case an ID was not discovered.
+        else:
+            return None
+
+    @property
+    def _description(self):
+        return self._vm_firewall.description or ""
+
+    @property
+    def description(self):
+        desc_fragment = " [{}{}]".format(self._network_id_tag,
+                                         self.network_id)
+        desc = self._description
+        if desc:
+            return desc.replace(desc_fragment, "")
+        else:
+            return None
+
+    @description.setter
+    def description(self, value):
+        if not value:
+            value = ""
+        value += " [{}{}]".format(self._network_id_tag,
+                                  self.network_id)
+        self._provider.os_conn.network.update_security_group(
+            self.id, description=value)
+        self.refresh()
 
     @property
     def name(self):
@@ -1240,9 +1149,6 @@ class OpenStackVMFirewall(BaseVMFirewall):
     def rules(self):
         return self._rule_svc
 
-    def delete(self):
-        return self._vm_firewall.delete(self._provider.os_conn.session)
-
     def refresh(self):
         self._vm_firewall = self._provider.os_conn.network.get_security_group(
             self.id)
@@ -1255,55 +1161,6 @@ class OpenStackVMFirewall(BaseVMFirewall):
         return js
 
 
-class OpenStackVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
-
-    def __init__(self, provider, firewall):
-        super(OpenStackVMFirewallRuleContainer, self).__init__(
-            provider, firewall)
-
-    def list(self, limit=None, marker=None):
-        # pylint:disable=protected-access
-        rules = [OpenStackVMFirewallRule(self.firewall, r)
-                 for r in self.firewall._vm_firewall.security_group_rules]
-        return ClientPagedResultList(self._provider, rules,
-                                     limit=limit, marker=marker)
-
-    def create(self,  direction, protocol=None, from_port=None,
-               to_port=None, cidr=None, src_dest_fw=None):
-        src_dest_fw_id = (src_dest_fw.id if isinstance(src_dest_fw,
-                                                       OpenStackVMFirewall)
-                          else src_dest_fw)
-
-        try:
-            if direction == TrafficDirection.INBOUND:
-                os_direction = 'ingress'
-            elif direction == TrafficDirection.OUTBOUND:
-                os_direction = 'egress'
-            else:
-                raise InvalidValueException("direction", direction)
-            # pylint:disable=protected-access
-            rule = self._provider.os_conn.network.create_security_group_rule(
-                security_group_id=self.firewall.id,
-                direction=os_direction,
-                port_range_max=to_port,
-                port_range_min=from_port,
-                protocol=protocol,
-                remote_ip_prefix=cidr,
-                remote_group_id=src_dest_fw_id)
-            self.firewall.refresh()
-            return OpenStackVMFirewallRule(self.firewall, rule.to_dict())
-        except HttpException as e:
-            self.firewall.refresh()
-            # 409=Conflict, raised for duplicate rule
-            if e.status_code == 409:
-                existing = self.find(direction=direction, protocol=protocol,
-                                     from_port=from_port, to_port=to_port,
-                                     cidr=cidr, src_dest_fw_id=src_dest_fw_id)
-                return existing[0]
-            else:
-                raise e
-
-
 class OpenStackVMFirewallRule(BaseVMFirewallRule):
 
     def __init__(self, parent_fw, rule):
@@ -1353,10 +1210,6 @@ class OpenStackVMFirewallRule(BaseVMFirewallRule):
             return self._provider.security.vm_firewalls.get(fw_id)
         return None
 
-    def delete(self):
-        self._provider.os_conn.network.delete_security_group_rule(self.id)
-        self.firewall.refresh()
-
 
 class OpenStackBucketObject(BaseBucketObject):
 
@@ -1477,7 +1330,8 @@ class OpenStackBucket(BaseBucket):
     def __init__(self, provider, bucket):
         super(OpenStackBucket, self).__init__(provider)
         self._bucket = bucket
-        self._object_container = OpenStackBucketContainer(provider, self)
+        self._object_container = OpenStackBucketObjectSubService(provider,
+                                                                 self)
 
     @property
     def id(self):
@@ -1490,57 +1344,3 @@ class OpenStackBucket(BaseBucket):
     @property
     def objects(self):
         return self._object_container
-
-    def delete(self, delete_contents=False):
-        self._provider.swift.delete_container(self.name)
-
-
-class OpenStackBucketContainer(BaseBucketContainer):
-
-    def __init__(self, provider, bucket):
-        super(OpenStackBucketContainer, self).__init__(provider, bucket)
-
-    def get(self, name):
-        """
-        Retrieve a given object from this bucket.
-        """
-        # Swift always returns a reference for the container first,
-        # followed by a list containing references to objects.
-        _, object_list = self._provider.swift.get_container(
-            self.bucket.name, prefix=name)
-        # Loop through list of objects looking for an exact name vs. a prefix
-        for obj in object_list:
-            if obj.get('name') == name:
-                return OpenStackBucketObject(self._provider,
-                                             self.bucket,
-                                             obj)
-        return None
-
-    def list(self, limit=None, marker=None, prefix=None):
-        """
-        List all objects within this bucket.
-
-        :rtype: BucketObject
-        :return: List of all available BucketObjects within this bucket.
-        """
-        _, object_list = self._provider.swift.get_container(
-            self.bucket.name,
-            limit=oshelpers.os_result_limit(self._provider, limit),
-            marker=marker, prefix=prefix)
-        cb_objects = [OpenStackBucketObject(
-            self._provider, self.bucket, obj) for obj in object_list]
-
-        return oshelpers.to_server_paged_list(
-            self._provider,
-            cb_objects,
-            limit)
-
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['name']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-        return ClientPagedResultList(self._provider, list(matches))
-
-    def create(self, object_name):
-        self._provider.swift.put_object(self.bucket.name, object_name, None)
-        return self.get(object_name)

Разница между файлами не показана из-за своего большого размера
+ 426 - 227
cloudbridge/cloud/providers/openstack/services.py


+ 41 - 0
cloudbridge/cloud/providers/openstack/subservices.py

@@ -0,0 +1,41 @@
+import logging
+
+from cloudbridge.cloud.base.subservices import BaseBucketObjectSubService
+from cloudbridge.cloud.base.subservices import BaseFloatingIPSubService
+from cloudbridge.cloud.base.subservices import BaseGatewaySubService
+from cloudbridge.cloud.base.subservices import BaseSubnetSubService
+from cloudbridge.cloud.base.subservices import BaseVMFirewallRuleSubService
+
+
+log = logging.getLogger(__name__)
+
+
+class OpenStackBucketObjectSubService(BaseBucketObjectSubService):
+
+    def __init__(self, provider, bucket):
+        super(OpenStackBucketObjectSubService, self).__init__(provider, bucket)
+
+
+class OpenStackGatewaySubService(BaseGatewaySubService):
+
+    def __init__(self, provider, network):
+        super(OpenStackGatewaySubService, self).__init__(provider, network)
+
+
+class OpenStackFloatingIPSubService(BaseFloatingIPSubService):
+
+    def __init__(self, provider, gateway):
+        super(OpenStackFloatingIPSubService, self).__init__(provider, gateway)
+
+
+class OpenStackVMFirewallRuleSubService(BaseVMFirewallRuleSubService):
+
+    def __init__(self, provider, firewall):
+        super(OpenStackVMFirewallRuleSubService, self).__init__(
+            provider, firewall)
+
+
+class OpenStackSubnetSubService(BaseSubnetSubService):
+
+    def __init__(self, provider, network):
+        super(OpenStackSubnetSubService, self).__init__(provider, network)

+ 0 - 5
cloudbridge/test/__init__.py

@@ -1,9 +1,4 @@
 """
 Use ``python setup.py test`` to run these unit tests (alternatively, use
 ``python -m unittest test``).
-
-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.
 """

+ 97 - 72
cloudbridge/test/helpers/__init__.py

@@ -1,17 +1,17 @@
 import functools
+import operator
 import os
 import sys
-import traceback
 import unittest
 import uuid
-from contextlib import contextmanager
 
-import six
-
-from cloudbridge.cloud.base.helpers import get_env
+from cloudbridge.cloud.base import helpers as cb_helpers
 from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
+from cloudbridge.cloud.interfaces.resources import FloatingIpState
+from cloudbridge.cloud.interfaces.resources import NetworkState
+from cloudbridge.cloud.interfaces.resources import SubnetState
 
 
 def parse_bool(val):
@@ -21,41 +21,6 @@ def parse_bool(val):
         return False
 
 
-@contextmanager
-def cleanup_action(cleanup_func):
-    """
-    Context manager to carry out a given
-    cleanup action after carrying out a set
-    of tasks, or when an exception occurs.
-    If any errors occur during the cleanup
-    action, those are ignored, and the original
-    traceback is preserved.
-
-    :params func: This function is called if
-    an exception occurs or at the end of the
-    context block. If any exceptions raised
-        by func are ignored.
-    Usage:
-        with cleanup_action(lambda e: print("Oops!")):
-            do_something()
-    """
-    try:
-        yield
-    except Exception:
-        ex_class, ex_val, ex_traceback = sys.exc_info()
-        try:
-            cleanup_func()
-        except Exception as e:
-            print("Error during exception cleanup: {0}".format(e))
-            traceback.print_exc()
-        six.reraise(ex_class, ex_val, ex_traceback)
-    try:
-        cleanup_func()
-    except Exception as e:
-        print("Error during cleanup: {0}".format(e))
-        traceback.print_exc()
-
-
 def skipIfNoService(services):
     """
     A decorator for skipping tests if the provider
@@ -78,27 +43,68 @@ def skipIfNoService(services):
     return wrap
 
 
+def skipIfPython(op, major, minor):
+    """
+    A decorator for skipping tests if the python
+    version doesn't match
+    """
+    def stringToOperator(op):
+        op_map = {
+            "=": operator.eq,
+            "==": operator.eq,
+            "<": operator.lt,
+            "<=": operator.le,
+            ">": operator.gt,
+            ">=": operator.ge,
+        }
+        return op_map.get(op)
+
+    def wrap(func):
+        """
+        The actual wrapper
+        """
+        @functools.wraps(func)
+        def wrapper(self, *args, **kwargs):
+            op_func = stringToOperator(op)
+            if op_func(sys.version_info, (major, minor)):
+                self.skipTest(
+                    "Skipping test because python version {0} is {1} expected"
+                    " version {2}".format(sys.version_info[:2],
+                                          op, (major, minor)))
+            func(self, *args, **kwargs)
+        return wrapper
+    return wrap
+
+
 TEST_DATA_CONFIG = {
     "AWSCloudProvider": {
         # Match the ami value with entry in custom_amis.json for use with moto
-        "image": get_env('CB_IMAGE_AWS', 'ami-aa2ea6d0'),
-        "vm_type": get_env('CB_VM_TYPE_AWS', 't2.nano'),
-        "placement": get_env('CB_PLACEMENT_AWS', 'us-east-1a'),
+        "image": cb_helpers.get_env('CB_IMAGE_AWS', 'ami-aa2ea6d0'),
+        "vm_type": cb_helpers.get_env('CB_VM_TYPE_AWS', 't2.nano'),
+        "placement": cb_helpers.get_env('CB_PLACEMENT_AWS', 'us-east-1a'),
     },
-    "OpenStackCloudProvider": {
-        "image": os.environ.get('CB_IMAGE_OS',
-                                'c66bdfa1-62b1-43be-8964-e9ce208ac6a5'),
-        "vm_type": os.environ.get('CB_VM_TYPE_OS', 'm1.tiny'),
-        "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
+    'OpenStackCloudProvider': {
+        'image': cb_helpers.get_env('CB_IMAGE_OS',
+                                    'c66bdfa1-62b1-43be-8964-e9ce208ac6a5'),
+        "vm_type": cb_helpers.get_env('CB_VM_TYPE_OS', 'm1.tiny'),
+        "placement": cb_helpers.get_env('CB_PLACEMENT_OS', 'nova'),
+    },
+    'GCPCloudProvider': {
+        'image': cb_helpers.get_env(
+            'CB_IMAGE_GCP',
+            'https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/'
+            'global/images/ubuntu-1710-artful-v20180126'),
+        'vm_type': cb_helpers.get_env('CB_VM_TYPE_GCP', 'f1-micro'),
+        'placement': cb_helpers.get_env('GCP_DEFAULT_ZONE', 'us-central1-a'),
     },
     "AzureCloudProvider": {
         "placement":
-            get_env('CB_PLACEMENT_AZURE', 'eastus'),
+            cb_helpers.get_env('CB_PLACEMENT_AZURE', 'eastus'),
         "image":
-            get_env('CB_IMAGE_AZURE',
-                    'Canonical:UbuntuServer:16.04.0-LTS:latest'),
+            cb_helpers.get_env('CB_IMAGE_AZURE',
+                               'Canonical:UbuntuServer:16.04.0-LTS:latest'),
         "vm_type":
-            get_env('CB_VM_TYPE_AZURE', 'Basic_A2'),
+            cb_helpers.get_env('CB_VM_TYPE_AZURE', 'Basic_A2'),
     }
 }
 
@@ -108,6 +114,8 @@ def get_provider_test_data(provider, key):
         return TEST_DATA_CONFIG.get("AWSCloudProvider").get(key)
     elif "OpenStackCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("OpenStackCloudProvider").get(key)
+    elif "GCPCloudProvider" in provider.name:
+        return TEST_DATA_CONFIG.get("GCPCloudProvider").get(key)
     elif "AzureCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("AzureCloudProvider").get(key)
     return None
@@ -121,14 +129,33 @@ def get_or_create_default_subnet(provider):
         zone=get_provider_test_data(provider, 'placement'))
 
 
-def delete_test_network(network):
+def cleanup_subnet(subnet):
+    if subnet:
+        subnet.delete()
+        subnet.wait_for([SubnetState.UNKNOWN],
+                        terminal_states=[SubnetState.ERROR])
+
+
+def cleanup_network(network):
     """
     Delete the supplied network, first deleting any contained subnets.
     """
-    with cleanup_action(lambda: network.delete()):
-        for sn in network.subnets:
-            with cleanup_action(lambda: sn.delete()):
-                pass
+    if network:
+        try:
+            for sn in network.subnets:
+                with cb_helpers.cleanup_action(lambda: cleanup_subnet(sn)):
+                    pass
+        finally:
+            network.delete()
+            network.wait_for([NetworkState.UNKNOWN],
+                             terminal_states=[NetworkState.ERROR])
+
+
+def cleanup_fip(fip):
+    if fip:
+        fip.delete()
+        fip.wait_for([FloatingIpState.UNKNOWN],
+                     terminal_states=[FloatingIpState.ERROR])
 
 
 def get_test_gateway(provider):
@@ -139,14 +166,14 @@ def get_test_gateway(provider):
     """
     sn = get_or_create_default_subnet(provider)
     net = sn.network
-    return net.gateways.get_or_create_inet_gateway()
+    return net.gateways.get_or_create()
 
 
-def delete_test_gateway(gateway):
+def cleanup_gateway(gateway):
     """
     Delete the supplied network and gateway.
     """
-    with cleanup_action(lambda: gateway.delete()):
+    with cb_helpers.cleanup_action(lambda: gateway.delete()):
         pass
 
 
@@ -186,7 +213,7 @@ def get_test_fixtures_folder():
     return os.path.join(os.path.dirname(__file__), '../fixtures/')
 
 
-def delete_test_instance(instance):
+def delete_instance(instance):
     if instance:
         instance.delete()
         instance.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN],
@@ -196,12 +223,13 @@ def delete_test_instance(instance):
 def cleanup_test_resources(instance=None, vm_firewall=None,
                            key_pair=None, network=None):
     """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: vm_firewall.delete()
-                                if vm_firewall else None):
-                delete_test_instance(instance)
+    with cb_helpers.cleanup_action(
+            lambda: cleanup_network(network) if network else None):
+        with cb_helpers.cleanup_action(
+                lambda: key_pair.delete() if key_pair else None):
+            with cb_helpers.cleanup_action(
+                    lambda: vm_firewall.delete() if vm_firewall else None):
+                delete_instance(instance)
 
 
 def get_uuid():
@@ -228,12 +256,9 @@ class ProviderTestBase(unittest.TestCase):
             return 1
 
     def create_provider_instance(self):
-        provider_name = get_env("CB_TEST_PROVIDER", "aws")
-        use_mock_drivers = parse_bool(
-            os.environ.get("CB_USE_MOCK_PROVIDERS", "True"))
+        provider_name = cb_helpers.get_env("CB_TEST_PROVIDER", "aws")
         factory = CloudProviderFactory()
-        provider_class = factory.get_provider_class(provider_name,
-                                                    get_mock=use_mock_drivers)
+        provider_class = factory.get_provider_class(provider_name)
         config = {'default_wait_interval':
                   self.get_provider_wait_interval(provider_class),
                   'default_result_limit': 5}

+ 5 - 3
cloudbridge/test/helpers/standard_interface_tests.py

@@ -9,8 +9,10 @@ import uuid
 
 import tenacity
 
+from cloudbridge.cloud.base import helpers as cb_helpers
 from cloudbridge.cloud.interfaces.exceptions \
     import InvalidNameException
+from cloudbridge.cloud.interfaces.exceptions import InvalidParamException
 from cloudbridge.cloud.interfaces.resources import LabeledCloudResource
 from cloudbridge.cloud.interfaces.resources import ObjectLifeCycleMixin
 from cloudbridge.cloud.interfaces.resources import ResultList
@@ -100,7 +102,7 @@ def check_find_non_existent(test, service, obj):
         find_objs = service.find(label="random_imagined_obj_name", **args)
     else:
         find_objs = service.find(name="random_imagined_obj_name")
-    with test.assertRaises(TypeError):
+    with test.assertRaises(InvalidParamException):
         service.find(notaparameter="random_imagined_obj_name")
     test.assertTrue(
         len(find_objs) == 0,
@@ -117,7 +119,7 @@ def check_get(test, service, obj):
 
 def check_get_non_existent(test, service):
     # check get
-    get_objs = service.get(str(uuid.uuid4()))
+    get_objs = service.get('tmp-' + str(uuid.uuid4()))
     test.assertIsNone(
         get_objs,
         "Get non-existent object for %s returned unexpected objects: %s"
@@ -329,7 +331,7 @@ def check_crud(test, service, iface, label_prefix,
     """
 
     obj = None
-    with helpers.cleanup_action(lambda: cleanup_func(obj)):
+    with cb_helpers.cleanup_action(lambda: cleanup_func(obj)):
         label = "{0}-{1}".format(label_prefix, helpers.get_uuid())
         if not skip_name_check:
             check_create(test, service, iface, label_prefix,

+ 36 - 28
cloudbridge/test/test_block_store_service.py

@@ -2,6 +2,7 @@ import time
 
 import six
 
+from cloudbridge.cloud.base import helpers as cb_helpers
 from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.interfaces import SnapshotState
 from cloudbridge.cloud.interfaces import VolumeState
@@ -19,12 +20,29 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['storage.volumes', 'storage.volumes'])
+    def test_storage_services_event_pattern(self):
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.storage.volumes._service_event_pattern,
+            "provider.storage.volumes",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("volumes",
+                                     "provider.storage.volumes",
+                                     self.provider.storage.volumes.
+                                     _service_event_pattern))
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.storage.snapshots._service_event_pattern,
+            "provider.storage.snapshots",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("snapshots",
+                                     "provider.storage.snapshots",
+                                     self.provider.storage.snapshots.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['storage.volumes'])
     def test_crud_volume(self):
-        """
-        Create a new volume, check whether the expected values are set,
-        and delete it
-        """
         def create_vol(label):
             return self.provider.storage.volumes.create(
                 label, 1,
@@ -47,14 +65,11 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.volumes'])
     def test_attach_detach_volume(self):
-        """
-        Create a new volume, and attempt to attach it to an instance
-        """
         label = "cb-attachvol-{0}".format(helpers.get_uuid())
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         test_instance = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance)):
             subnet = helpers.get_or_create_default_subnet(
                 self.provider)
@@ -63,7 +78,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
             test_vol = self.provider.storage.volumes.create(
                 label, 1, test_instance.zone_id)
-            with helpers.cleanup_action(lambda: test_vol.delete()):
+            with cb_helpers.cleanup_action(lambda: test_vol.delete()):
                 test_vol.wait_till_ready()
                 test_vol.attach(test_instance, '/dev/sda2')
                 test_vol.wait_for(
@@ -76,15 +91,12 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.volumes'])
     def test_volume_properties(self):
-        """
-        Test volume properties
-        """
         label = "cb-volprops-{0}".format(helpers.get_uuid())
         vol_desc = 'newvoldesc1'
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         test_instance = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance)):
             subnet = helpers.get_or_create_default_subnet(
                 self.provider)
@@ -93,7 +105,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
             test_vol = self.provider.storage.volumes.create(
                 label, 1, test_instance.zone_id, description=vol_desc)
-            with helpers.cleanup_action(lambda: test_vol.delete()):
+            with cb_helpers.cleanup_action(lambda: test_vol.delete()):
                 test_vol.wait_till_ready()
                 self.assertTrue(
                     isinstance(test_vol.size, six.integer_types) and
@@ -119,7 +131,8 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                 self.assertEqual(test_vol.attachments.volume, test_vol)
                 self.assertEqual(test_vol.attachments.instance_id,
                                  test_instance.id)
-                if not self.provider.PROVIDER_ID == 'azure':
+                if (self.provider.PROVIDER_ID != 'azure' and
+                        self.provider.PROVIDER_ID != 'gcp'):
                     self.assertEqual(test_vol.attachments.device,
                                      "/dev/sda2")
                 test_vol.detach()
@@ -136,16 +149,14 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.snapshots'])
     def test_crud_snapshot(self):
-        """
-        Create a new volume, create a snapshot of the volume, and check
-        whether list_snapshots properly detects the new snapshot.
-        Delete everything afterwards.
-        """
+        # Create a new volume, create a snapshot of the volume, and check
+        # whether list_snapshots properly detects the new snapshot.
+        # Delete everything afterwards.
         label = "cb-crudsnap-{0}".format(helpers.get_uuid())
         test_vol = self.provider.storage.volumes.create(
             label, 1,
             helpers.get_provider_test_data(self.provider, "placement"))
-        with helpers.cleanup_action(lambda: test_vol.delete()):
+        with cb_helpers.cleanup_action(lambda: test_vol.delete()):
             test_vol.wait_till_ready()
 
             def create_snap(label):
@@ -180,14 +191,11 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.snapshots'])
     def test_snapshot_properties(self):
-        """
-        Test snapshot properties
-        """
         label = "cb-snapprop-{0}".format(helpers.get_uuid())
         test_vol = self.provider.storage.volumes.create(
             label, 1,
             helpers.get_provider_test_data(self.provider, "placement"))
-        with helpers.cleanup_action(lambda: test_vol.delete()):
+        with cb_helpers.cleanup_action(lambda: test_vol.delete()):
             test_vol.wait_till_ready()
             snap_label = "cb-snap-{0}".format(label)
             test_snap = test_vol.create_snapshot(label=snap_label,
@@ -199,7 +207,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     snap.wait_for([SnapshotState.UNKNOWN],
                                   terminal_states=[SnapshotState.ERROR])
 
-            with helpers.cleanup_action(lambda: cleanup_snap(test_snap)):
+            with cb_helpers.cleanup_action(lambda: cleanup_snap(test_snap)):
                 test_snap.wait_till_ready()
                 self.assertTrue(isinstance(test_vol.size, six.integer_types))
                 self.assertEqual(
@@ -225,11 +233,11 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                     sv_label, 1,
                     helpers.get_provider_test_data(self.provider, "placement"),
                     snapshot=test_snap)
-                with helpers.cleanup_action(lambda: snap_vol.delete()):
+                with cb_helpers.cleanup_action(lambda: snap_vol.delete()):
                     snap_vol.wait_till_ready()
 
                 # Test volume creation from a snapshot (via Snapshot)
                 snap_vol2 = test_snap.create_volume(
                     helpers.get_provider_test_data(self.provider, "placement"))
-                with helpers.cleanup_action(lambda: snap_vol2.delete()):
+                with cb_helpers.cleanup_action(lambda: snap_vol2.delete()):
                     snap_vol2.wait_till_ready()

+ 31 - 72
cloudbridge/test/test_cloud_factory.py

@@ -6,86 +6,63 @@ from cloudbridge.cloud.factory import CloudProviderFactory
 from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from cloudbridge.cloud.interfaces.provider import CloudProvider
 from cloudbridge.cloud.providers.aws import AWSCloudProvider
-from cloudbridge.cloud.providers.aws.provider import MockAWSCloudProvider
-
-from cloudbridge.test import helpers
-
 
 class CloudFactoryTestCase(unittest.TestCase):
 
     _multiprocess_can_split_ = True
 
     def test_create_provider_valid(self):
-        """
-        Creating a provider with a known name should return
-        a valid implementation
-        """
+        # Creating a provider with a known name should return
+        # a valid implementation
         self.assertIsInstance(CloudProviderFactory().create_provider(
             factory.ProviderList.AWS, {}),
             interfaces.CloudProvider,
             "create_provider did not return a valid VM type")
 
     def test_create_provider_invalid(self):
-        """
-        Creating a provider with an invalid name should raise a
-        NotImplementedError
-        """
+        # Creating a provider with an invalid name should raise a
+        # NotImplementedError
         with self.assertRaises(NotImplementedError):
             CloudProviderFactory().create_provider("ec23", {})
 
-    def test_find_provider_mock_valid(self):
-        """
-        Searching for a provider with a known mock driver should return
-        an implementation implementing helpers.TestMockHelperMixin
-        """
-        mock = CloudProviderFactory().get_provider_class(
-            factory.ProviderList.AWS, get_mock=True)
-        self.assertTrue(
-            issubclass(
-                mock,
-                helpers.TestMockHelperMixin),
-            "Expected mock for AWS but class does not implement mock provider")
-        for cls in CloudProviderFactory().get_all_provider_classes(
-                get_mock=False):
-            self.assertTrue(
-                not issubclass(
-                    cls,
-                    TestMockHelperMixin),
-                "Did not expect mock but %s implements mock provider" %
-                cls)
-
     def test_get_provider_class_valid(self):
-        """
-        Searching for a provider class with a known name should return a valid
-        class
-        """
+        # Searching for a provider class with a known name should return a
+        # valid class
         self.assertEqual(CloudProviderFactory().get_provider_class(
             factory.ProviderList.AWS), AWSCloudProvider)
 
     def test_get_provider_class_invalid(self):
-        """
-        Searching for a provider class with an invalid name should
-        return None
-        """
+        # Searching for a provider class with an invalid name should
+        # return None
         self.assertIsNone(CloudProviderFactory().get_provider_class("aws1"))
 
+    def test_find_provider_include_mocks(self):
+        self.assertTrue(
+            any(cls for cls
+                in CloudProviderFactory().get_all_provider_classes()
+                if issubclass(cls, TestMockHelperMixin)),
+            "expected to find at least one mock provider")
+
+    def test_find_provider_exclude_mocks(self):
+        for cls in CloudProviderFactory().get_all_provider_classes(
+                ignore_mocks=True):
+            self.assertTrue(
+                not issubclass(cls, TestMockHelperMixin),
+                "Did not expect mock but %s implements mock provider" % cls)
+
     def test_register_provider_class_invalid(self):
-        """
-        Attempting to register an invalid test class should be ignored
-        """
+        # Attempting to register an invalid test class should be ignored
         class DummyClass(object):
             PROVIDER_ID = 'aws'
 
         factory = CloudProviderFactory()
         factory.register_provider_class(DummyClass)
         self.assertTrue(DummyClass not in
-                        factory.get_all_provider_classes(get_mock=False))
+                        factory.get_all_provider_classes())
 
     def test_register_provider_class_double(self):
-        """
-        Attempting to register the same class twice should register second
-        instance
-        """
+        # Attempting to register the same class twice should register second
+        # instance
         class DummyClass(CloudProvider):
             PROVIDER_ID = 'aws'
 
@@ -93,35 +70,17 @@ class CloudFactoryTestCase(unittest.TestCase):
         factory.list_providers()
         factory.register_provider_class(DummyClass)
         self.assertTrue(DummyClass in
-                        factory.get_all_provider_classes(get_mock=False))
+                        factory.get_all_provider_classes())
         self.assertTrue(AWSCloudProvider not in
-                        factory.get_all_provider_classes(get_mock=False))
-
-    def test_register_mock_provider_class_double(self):
-        """
-        Attempting to register the same mock provider twice should register
-        only the second instance
-        """
-        class DummyClass(CloudProvider, TestMockHelperMixin):
-            PROVIDER_ID = 'aws'
-
-        factory = CloudProviderFactory()
-        factory.list_providers()
-        factory.register_provider_class(DummyClass)
-        self.assertTrue(DummyClass in
-                        factory.get_all_provider_classes(get_mock=True))
-        self.assertTrue(MockAWSCloudProvider not in
-                        factory.get_all_provider_classes(get_mock=True))
+                        factory.get_all_provider_classes())
 
     def test_register_provider_class_without_id(self):
-        """
-        Attempting to register a class without a PROVIDER_ID attribute
-        should be ignored.
-        """
+        # Attempting to register a class without a PROVIDER_ID attribute
+        # should be ignored.
         class DummyClass(CloudProvider):
             pass
 
         factory = CloudProviderFactory()
         factory.register_provider_class(DummyClass)
         self.assertTrue(DummyClass not in
-                        factory.get_all_provider_classes(get_mock=False))
+                        factory.get_all_provider_classes())

+ 6 - 3
cloudbridge/test/test_cloud_helpers.py

@@ -80,25 +80,28 @@ class CloudHelpersTestCase(ProviderTestBase):
             results.data
 
     def test_type_validation(self):
-        """
-        Make sure internal type checking implementation properly sets types.
-        """
+        # Make sure internal type checking implementation properly sets types.
         self.provider.config['text_type_check'] = 'test-text'
+        # pylint:disable=protected-access
         config_value = self.provider._get_config_value('text_type_check', None)
         self.assertIsInstance(config_value, six.string_types)
 
+        # pylint:disable=protected-access
         env_value = self.provider._get_config_value(
             'some_config_value', get_env('MOTO_AMIS_PATH'))
         self.assertIsInstance(env_value, six.string_types)
 
+        # pylint:disable=protected-access
         none_value = self.provider._get_config_value(
             'some_config_value', get_env('MISSING_ENV', None))
         self.assertIsNone(none_value)
 
+        # pylint:disable=protected-access
         bool_value = self.provider._get_config_value(
             'some_config_value', get_env('MISSING_ENV', True))
         self.assertIsInstance(bool_value, bool)
 
+        # pylint:disable=protected-access
         int_value = self.provider._get_config_value(
             'default_result_limit', None)
         self.assertIsInstance(int_value, int)

+ 58 - 35
cloudbridge/test/test_compute_service.py

@@ -2,6 +2,8 @@ import ipaddress
 
 import six
 
+from cloudbridge.cloud.base import helpers as cb_helpers
+from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import InvalidConfigurationException
@@ -19,6 +21,18 @@ class CloudComputeServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['compute.instances'])
+    def test_storage_services_event_pattern(self):
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.compute.instances._service_event_pattern,
+            "provider.compute.instances",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("instances",
+                                     "provider.compute.instances",
+                                     self.provider.compute.instances.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['compute.instances', 'networking.networks'])
     def test_crud_instance(self):
         label = "cb-instcrud-{0}".format(helpers.get_uuid())
@@ -77,13 +91,13 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         test_instance = None
         fw = None
         kp = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance, fw, kp)):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             kp = self.provider.security.key_pairs.create(name=label)
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
             test_instance = helpers.get_test_instance(self.provider,
                                                       label, key_pair=kp,
                                                       vm_firewalls=[fw],
@@ -230,7 +244,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
            label, 1,
            helpers.get_provider_test_data(self.provider,
                                           "placement"))
-        with helpers.cleanup_action(lambda: test_vol.delete()):
+        with cb_helpers.cleanup_action(lambda: test_vol.delete()):
             test_vol.wait_till_ready()
             test_snap = test_vol.create_snapshot(label=label,
                                                  description=label)
@@ -241,7 +255,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                     snap.wait_for([SnapshotState.UNKNOWN],
                                   terminal_states=[SnapshotState.ERROR])
 
-            with helpers.cleanup_action(lambda: cleanup_snap(test_snap)):
+            with cb_helpers.cleanup_action(lambda: cleanup_snap(test_snap)):
                 test_snap.wait_till_ready()
 
                 lc = self.provider.compute.instances.create_launch_config()
@@ -276,21 +290,25 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                     "vm_type")
                 vm_type = self.provider.compute.vm_types.find(
                     name=vm_type_name)[0]
-                for _ in range(vm_type.num_ephemeral_disks):
+                # Some providers, e.g. GCP, has a limit on total number of
+                # attached disks; it does not matter how many of them are
+                # ephemeral or persistent. So, wee keep in mind that we have
+                # attached 4 disks already, and add ephemeral disks accordingly
+                # to not exceed the limit.
+                for _ in range(vm_type.num_ephemeral_disks - 4):
                     lc.add_ephemeral_device()
 
                 subnet = helpers.get_or_create_default_subnet(
                     self.provider)
 
-                inst = helpers.create_test_instance(
-                    self.provider,
-                    label,
-                    subnet=subnet,
-                    launch_config=lc)
-
-                with helpers.cleanup_action(lambda:
-                                            helpers.delete_test_instance(
-                                                inst)):
+                inst = None
+                with cb_helpers.cleanup_action(
+                        lambda: helpers.delete_instance(inst)):
+                    inst = helpers.create_test_instance(
+                        self.provider,
+                        label,
+                        subnet=subnet,
+                        launch_config=lc)
                     try:
                         inst.wait_till_ready()
                     except WaitStateException as e:
@@ -309,19 +327,19 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         net = None
         test_inst = None
         fw = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 instance=test_inst, vm_firewall=fw, network=net)):
             net = self.provider.networking.networks.create(
-                label=label, cidr_block='10.0.0.0/16')
+                label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
             cidr = '10.0.1.0/24'
-            subnet = net.create_subnet(label=label, cidr_block=cidr,
-                                       zone=helpers.get_provider_test_data(
-                                                    self.provider,
-                                                    'placement'))
+            subnet = net.subnets.create(label=label, cidr_block=cidr,
+                                        zone=helpers.get_provider_test_data(
+                                                     self.provider,
+                                                     'placement'))
             test_inst = helpers.get_test_instance(self.provider, label,
                                                   subnet=subnet)
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             # Check adding a VM firewall to a running instance
             test_inst.add_vm_firewall(fw)
@@ -341,26 +359,30 @@ class CloudComputeServiceTestCase(ProviderTestBase):
 
             # check floating ips
             router = self.provider.networking.routers.create(label, net)
-            gateway = net.gateways.get_or_create_inet_gateway()
+            gateway = net.gateways.get_or_create()
 
             def cleanup_router(router, gateway):
-                with helpers.cleanup_action(lambda: router.delete()):
-                    with helpers.cleanup_action(lambda: gateway.delete()):
+                with cb_helpers.cleanup_action(lambda: router.delete()):
+                    with cb_helpers.cleanup_action(lambda: gateway.delete()):
                         router.detach_subnet(subnet)
                         router.detach_gateway(gateway)
 
-            with helpers.cleanup_action(lambda: cleanup_router(router,
-                                                               gateway)):
+            with cb_helpers.cleanup_action(lambda: cleanup_router(router,
+                                                                  gateway)):
                 router.attach_subnet(subnet)
                 router.attach_gateway(gateway)
-                # check whether adding an elastic ip works
-                fip = gateway.floating_ips.create()
-                self.assertFalse(
-                    fip.in_use,
-                    "Newly created floating IP address should not be in use.")
-
-                with helpers.cleanup_action(lambda: fip.delete()):
-                    with helpers.cleanup_action(
+                fip = None
+
+                with cb_helpers.cleanup_action(
+                        lambda: helpers.cleanup_fip(fip)):
+                    # check whether adding an elastic ip works
+                    fip = gateway.floating_ips.create()
+                    self.assertFalse(
+                        fip.in_use,
+                        "Newly created floating IP %s should not be in use." %
+                        fip.public_ip)
+
+                    with cb_helpers.cleanup_action(
                             lambda: test_inst.remove_floating_ip(fip)):
                         test_inst.add_floating_ip(fip)
                         test_inst.refresh()
@@ -370,7 +392,8 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                         fip.refresh()
                         self.assertTrue(
                             fip.in_use,
-                            "Attached floating IP address should be in use.")
+                            "Attached floating IP %s address should be in use."
+                            % fip.public_ip)
                     test_inst.refresh()
                     test_inst.reboot()
                     test_inst.wait_till_ready()

+ 16 - 9
cloudbridge/test/test_image_service.py

@@ -1,5 +1,7 @@
+from cloudbridge.cloud.base import helpers as cb_helpers
 from cloudbridge.cloud.interfaces import MachineImageState
-from cloudbridge.cloud.interfaces.resources import Instance, MachineImage
+from cloudbridge.cloud.interfaces.resources import Instance
+from cloudbridge.cloud.interfaces.resources import MachineImage
 
 from cloudbridge.test import helpers
 from cloudbridge.test.helpers import ProviderTestBase
@@ -10,14 +12,19 @@ class CloudImageServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['compute.images'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(self.provider.compute.images._service_event_pattern,
+                         "provider.compute.images",
+                         "Event pattern for {} service should be '{}', "
+                         "but found '{}'.".format("images",
+                                                  "provider.compute.images",
+                                                  self.provider.compute.images.
+                                                  _service_event_pattern))
+
     @helpers.skipIfNoService(['compute.images', 'networking.networks',
                               'compute.instances'])
     def test_create_and_list_image(self):
-        """
-        Create a new image and check whether that image can be listed.
-        This covers waiting till the image is ready, checking that the image
-        label is the expected one and whether list_images is functional.
-        """
         instance_label = "cb-crudimage-{0}".format(helpers.get_uuid())
         img_inst_label = "cb-crudimage-{0}".format(helpers.get_uuid())
 
@@ -50,8 +57,8 @@ class CloudImageServiceTestCase(ProviderTestBase):
 
         def create_instance_from_image(img):
             img_instance = None
-            with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
-                    img_instance)):
+            with cb_helpers.cleanup_action(
+                    lambda: helpers.cleanup_test_resources(img_instance)):
                 img_instance = self.provider.compute.instances.create(
                     img_inst_label, img,
                     helpers.get_provider_test_data(self.provider, 'vm_type'),
@@ -80,7 +87,7 @@ class CloudImageServiceTestCase(ProviderTestBase):
                                 "private ip should"
                                 " contain a valid value")
 
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 test_instance)):
             subnet = helpers.get_or_create_default_subnet(
                 self.provider)

+ 7 - 13
cloudbridge/test/test_interface.py

@@ -14,32 +14,24 @@ class CloudInterfaceTestCase(ProviderTestBase):
     _multiprocess_can_split_ = True
 
     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
         assert self.provider.name
 
     def test_has_service_valid_service_type(self):
-        """
-        has_service with a valid service type should return
-        a boolean and raise no exceptions
-        """
+        # has_service with a valid service type should return
+        # a boolean and raise no exceptions
         for key, value in interfaces.CloudServiceType.__dict__.items():
             if not key.startswith("__"):
                 self.provider.has_service(value)
 
     def test_has_service_invalid_service_type(self):
-        """
-        has_service with an invalid service type should return False
-        """
+        # has_service with an invalid service type should return False
         self.assertFalse(
             self.provider.has_service("NON_EXISTENT_SERVICE"),
             "has_service should not return True for a non-existent service")
 
     def test_library_version(self):
-        """
-        Check that the library version can be retrieved.
-        """
+        # Check that the library version can be retrieved.
         self.assertIsNotNone(cloudbridge.get_version(),
                              "Did not get library version.")
 
@@ -62,6 +54,8 @@ class CloudInterfaceTestCase(ProviderTestBase):
             cloned_config['os_password'] = "cb_dummy"
         elif self.provider.PROVIDER_ID == 'azure':
             cloned_config['azure_subscription_id'] = "cb_dummy"
+        elif self.provider.PROVIDER_ID == 'gcp':
+            cloned_config['gcp_service_creds_dict'] = {'dummy': 'dict'}
 
         with self.assertRaises(ProviderConnectionException):
             cloned_provider = CloudProviderFactory().create_provider(

+ 110 - 52
cloudbridge/test/test_network_service.py

@@ -1,3 +1,5 @@
+from cloudbridge.cloud.base import helpers as cb_helpers
+from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.interfaces.resources import FloatingIP
 from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.resources import NetworkState
@@ -15,20 +17,53 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['networking.subnets',
+                              'networking.networks',
+                              'networking.routers'])
+    def test_storage_services_event_pattern(self):
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.networking.networks._service_event_pattern,
+            "provider.networking.networks",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("networks",
+                                     "provider.networking.networks",
+                                     self.provider.networking.networks.
+                                     _service_event_pattern))
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.networking.subnets._service_event_pattern,
+            "provider.networking.subnets",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("subnets",
+                                     "provider.networking.subnets",
+                                     self.provider.networking.subnets.
+                                     _service_event_pattern))
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.networking.routers._service_event_pattern,
+            "provider.networking.routers",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("routers",
+                                     "provider.networking.routers",
+                                     self.provider.networking.routers.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['networking.networks'])
     def test_crud_network(self):
 
         def create_net(label):
             return self.provider.networking.networks.create(
-                label=label, cidr_block='10.0.0.0/16')
+                label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
 
         def cleanup_net(net):
             if net:
                 net.delete()
-                net.refresh()
+                net.wait_for([NetworkState.UNKNOWN],
+                             terminal_states=[NetworkState.ERROR])
                 self.assertTrue(
                     net.state == NetworkState.UNKNOWN,
-                    "Network.state must be unknown when refreshing after "
+                    "Network.state must be unknown after "
                     "a delete but got %s"
                     % net.state)
 
@@ -40,10 +75,8 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         label = 'cb-propnetwork-{0}'.format(helpers.get_uuid())
         subnet_label = 'cb-propsubnet-{0}'.format(helpers.get_uuid())
         net = self.provider.networking.networks.create(
-            label=label, cidr_block='10.0.0.0/16')
-        with helpers.cleanup_action(
-            lambda: net.delete()
-        ):
+            label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_network(net)):
             net.wait_till_ready()
             self.assertEqual(
                 net.state, 'available',
@@ -52,16 +85,16 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             sit.check_repr(self, net)
 
             self.assertIn(
-                net.cidr_block, ['', '10.0.0.0/16'],
-                "Network CIDR %s does not contain the expected value."
-                % net.cidr_block)
+                net.cidr_block, ['', BaseNetwork.CB_DEFAULT_IPV4RANGE],
+                "Network CIDR %s does not contain the expected value %s."
+                % (net.cidr_block, BaseNetwork.CB_DEFAULT_IPV4RANGE))
 
             cidr = '10.0.20.0/24'
-            sn = net.create_subnet(
+            sn = net.subnets.create(
                 label=subnet_label, cidr_block=cidr,
                 zone=helpers.get_provider_test_data(self.provider,
                                                     'placement'))
-            with helpers.cleanup_action(lambda: sn.delete()):
+            with cb_helpers.cleanup_action(lambda: helpers.cleanup_subnet(sn)):
                 self.assertTrue(
                     sn in net.subnets,
                     "Subnet ID %s should be listed in network subnets %s."
@@ -74,7 +107,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 )
 
                 self.assertListEqual(
-                    net.subnets, [sn],
+                    list(net.subnets), [sn],
                     "Network should have exactly one subnet: %s." % sn.id)
 
                 self.assertEqual(
@@ -89,14 +122,19 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
                 self.assertEqual(
                     cidr, sn.cidr_block,
-                    "Subnet's CIDR %s should match the specified one %s." % (
+                    "Should be exact cidr block that was requested")
+
+                self.assertTrue(
+                    BaseNetwork.cidr_blocks_overlap(cidr, sn.cidr_block),
+                    "Subnet's CIDR %s should overlap the specified one %s." % (
                         sn.cidr_block, cidr))
 
     def test_crud_subnet(self):
         # Late binding will make sure that create_subnet gets the
         # correct value
-        sn = helpers.get_or_create_default_subnet(self.provider)
-        net = sn.network
+        net = self.provider.networking.networks.create(
+                  label="cb-crudsubnet",
+                  cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
 
         def create_subnet(label):
             return self.provider.networking.subnets.create(
@@ -106,13 +144,23 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
         def cleanup_subnet(subnet):
             if subnet:
+                net = subnet.network
                 subnet.delete()
-                subnet.refresh()
+                subnet.wait_for([SubnetState.UNKNOWN],
+                                terminal_states=[SubnetState.ERROR])
                 self.assertTrue(
                     subnet.state == SubnetState.UNKNOWN,
-                    "Subnet.state must be unknown when refreshing after "
+                    "Subnet.state must be unknown after "
                     "a delete but got %s"
                     % subnet.state)
+                net.delete()
+                net.wait_for([NetworkState.UNKNOWN],
+                             terminal_states=[NetworkState.ERROR])
+                self.assertTrue(
+                    net.state == NetworkState.UNKNOWN,
+                    "Network.state must be unknown after "
+                    "a delete but got %s"
+                    % net.state)
 
         sit.check_crud(self, self.provider.networking.subnets, Subnet,
                        "cb-crudsubnet", create_subnet, cleanup_subnet)
@@ -129,8 +177,8 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             if fip:
                 gw.floating_ips.delete(fip.id)
 
-        with helpers.cleanup_action(
-                lambda: helpers.delete_test_gateway(gw)):
+        with cb_helpers.cleanup_action(
+                lambda: helpers.cleanup_gateway(gw)):
             sit.check_crud(self, gw.floating_ips, FloatingIP,
                            "cb-crudfip", create_fip, cleanup_fip,
                            skip_name_check=True)
@@ -140,9 +188,9 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         gw = helpers.get_test_gateway(
             self.provider)
         fip = gw.floating_ips.create()
-        with helpers.cleanup_action(
-                lambda: helpers.delete_test_gateway(gw)):
-            with helpers.cleanup_action(lambda: fip.delete()):
+        with cb_helpers.cleanup_action(
+                lambda: helpers.cleanup_gateway(gw)):
+            with cb_helpers.cleanup_action(lambda: fip.delete()):
                 fipl = list(gw.floating_ips)
                 self.assertIn(fip, fipl)
                 # 2016-08: address filtering not implemented in moto
@@ -163,10 +211,14 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
     def test_crud_router(self):
 
         def _cleanup(net, subnet, router, gateway):
-            with helpers.cleanup_action(lambda: net.delete()):
-                with helpers.cleanup_action(lambda: router.delete()):
-                    with helpers.cleanup_action(lambda: subnet.delete()):
-                        with helpers.cleanup_action(lambda: gateway.delete()):
+            with cb_helpers.cleanup_action(
+                    lambda: helpers.cleanup_network(net)):
+                with cb_helpers.cleanup_action(
+                        lambda: helpers.cleanup_subnet(subnet)):
+                    with cb_helpers.cleanup_action(
+                            lambda: router.delete()):
+                        with cb_helpers.cleanup_action(
+                                lambda: helpers.cleanup_gateway(gateway)):
                             router.detach_subnet(subnet)
                             router.detach_gateway(gateway)
 
@@ -177,43 +229,49 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         sn = None
         router = None
         gteway = None
-        with helpers.cleanup_action(lambda: _cleanup(net, sn, router, gteway)):
+        with cb_helpers.cleanup_action(
+                lambda: _cleanup(net, sn, router, gteway)):
             net = self.provider.networking.networks.create(
-                label=label, cidr_block='10.0.0.0/16')
+                label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
             router = self.provider.networking.routers.create(label=label,
                                                              network=net)
             cidr = '10.0.15.0/24'
-            sn = net.create_subnet(label=label, cidr_block=cidr,
-                                   zone=helpers.get_provider_test_data(
-                                       self.provider, 'placement'))
+            sn = net.subnets.create(label=label, cidr_block=cidr,
+                                    zone=helpers.get_provider_test_data(
+                                        self.provider, 'placement'))
 
             # Check basic router properties
             sit.check_standard_behaviour(
                 self, self.provider.networking.routers, router)
-            self.assertEqual(
-                router.state, RouterState.DETACHED,
-                "Router {0} state {1} should be {2}.".format(
-                    router.id, router.state, RouterState.DETACHED))
-
-#             self.assertFalse(
-#                 router.network_id,
-#                 "Router {0} should not be assoc. with a network {1}".format(
-#                     router.id, router.network_id))
-
-            self.assertTrue(
-                len(router.subnets) == 0,
-                "No subnet should be attached to router {1}".format(sn, router)
-            )
-            router.attach_subnet(sn)
-            self.assertTrue(
-                len(router.subnets) == 1,
-                "Subnet {0} not attached to router {1}".format(sn, router)
-            )
-            gteway = net.gateways.get_or_create_inet_gateway()
+            if (self.provider.PROVIDER_ID != 'gcp'):
+                self.assertEqual(
+                    router.state, RouterState.DETACHED,
+                    "Router {0} state {1} should be {2}.".format(
+                        router.id, router.state, RouterState.DETACHED))
+
+#                 self.assertEqual(
+#                     router.network_id, net.id,  "Router {0} should be assoc."
+#                     " with network {1}, but is associated with {2}"
+#                     .format(router.id, net.id, router.network_id))
+
+                self.assertTrue(
+                    len(router.subnets) == 0,
+                    "No subnet should be attached to router {1}".format(
+                        sn, router)
+                )
+                router.attach_subnet(sn)
+                self.assertTrue(
+                    len(router.subnets) == 1,
+                    "Subnet {0} not attached to router {1}".format(sn, router)
+                )
+            gteway = net.gateways.get_or_create()
             router.attach_gateway(gteway)
             # TODO: add a check for routes after that's been implemented
 
         sit.check_delete(self, self.provider.networking.routers, router)
+        # Also make sure that linked resources were properly cleaned up
+        sit.check_delete(self, self.provider.networking.subnets, sn)
+        sit.check_delete(self, self.provider.networking.networks, net)
 
     @helpers.skipIfNoService(['networking.networks'])
     def test_default_network(self):

+ 17 - 17
cloudbridge/test/test_object_life_cycle.py

@@ -1,3 +1,4 @@
+from cloudbridge.cloud.base import helpers as cb_helpers
 from cloudbridge.cloud.interfaces import VolumeState
 from cloudbridge.cloud.interfaces.exceptions import WaitStateException
 
@@ -11,25 +12,24 @@ class CloudObjectLifeCycleTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.volumes'])
     def test_object_life_cycle(self):
-        """
-        Test object life cycle methods by using a volume.
-        """
+        # Test object life cycle methods by using a volume.
         label = "cb-objlifecycle-{0}".format(helpers.get_uuid())
-        test_vol = self.provider.storage.volumes.create(
-            label, 1,
-            helpers.get_provider_test_data(self.provider, "placement"))
+        test_vol = None
+        with cb_helpers.cleanup_action(lambda: test_vol.delete()):
+            test_vol = self.provider.storage.volumes.create(
+                label, 1,
+                helpers.get_provider_test_data(self.provider, "placement"))
+
+            # Waiting for an invalid timeout should raise an exception
+            with self.assertRaises(AssertionError):
+                test_vol.wait_for([VolumeState.ERROR], timeout=-1, interval=1)
+            with self.assertRaises(AssertionError):
+                test_vol.wait_for([VolumeState.ERROR], timeout=1, interval=-1)
+
+            # If interval < timeout, an exception should be raised
+            with self.assertRaises(AssertionError):
+                test_vol.wait_for([VolumeState.ERROR], timeout=10, interval=20)
 
-        # Waiting for an invalid timeout should raise an exception
-        with self.assertRaises(AssertionError):
-            test_vol.wait_for([VolumeState.ERROR], timeout=-1, interval=1)
-        with self.assertRaises(AssertionError):
-            test_vol.wait_for([VolumeState.ERROR], timeout=1, interval=-1)
-
-        # If interval < timeout, an exception should be raised
-        with self.assertRaises(AssertionError):
-            test_vol.wait_for([VolumeState.ERROR], timeout=10, interval=20)
-
-        with helpers.cleanup_action(lambda: test_vol.delete()):
             test_vol.wait_till_ready()
             # Hitting a terminal state should raise an exception
             with self.assertRaises(WaitStateException):

+ 42 - 28
cloudbridge/test/test_object_store_service.py

@@ -7,6 +7,7 @@ from unittest import skip
 
 import requests
 
+from cloudbridge.cloud.base import helpers as cb_helpers
 from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
 from cloudbridge.cloud.interfaces.provider import TestMockHelperMixin
 from cloudbridge.cloud.interfaces.resources import Bucket
@@ -21,12 +22,29 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['storage._bucket_objects', 'storage.buckets'])
+    def test_storage_services_event_pattern(self):
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.storage.buckets._service_event_pattern,
+            "provider.storage.buckets",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("buckets",
+                                     "provider.storage.buckets",
+                                     self.provider.storage.buckets.
+                                     _service_event_pattern))
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.storage._bucket_objects._service_event_pattern,
+            "provider.storage._bucket_objects",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("bucket_objects",
+                                     "provider.storage._bucket_objects",
+                                     self.provider.storage._bucket_objects.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['storage.buckets'])
     def test_crud_bucket(self):
-        """
-        Create a new bucket, check whether the expected values are set,
-        and delete it.
-        """
 
         def create_bucket(name):
             return self.provider.storage.buckets.create(name)
@@ -61,7 +79,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
             if bucket_obj:
                 bucket_obj.delete()
 
-        with helpers.cleanup_action(lambda: test_bucket.delete()):
+        with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
             name = "cb-crudbucketobj-{0}".format(helpers.get_uuid())
             test_bucket = self.provider.storage.buckets.create(name)
 
@@ -71,11 +89,9 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['storage.buckets'])
     def test_crud_bucket_object_properties(self):
-        """
-        Create a new bucket, upload some contents into the bucket, and
-        check whether list properly detects the new content.
-        Delete everything afterwards.
-        """
+        # Create a new bucket, upload some contents into the bucket, and
+        # check whether list properly detects the new content.
+        # Delete everything afterwards.
         name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
         test_bucket = self.provider.storage.buckets.create(name)
 
@@ -83,12 +99,12 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
         objects = test_bucket.objects.list()
         self.assertEqual([], objects)
 
-        with helpers.cleanup_action(lambda: test_bucket.delete()):
+        with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name_prefix = "hello"
             obj_name = obj_name_prefix + "_world.txt"
             obj = test_bucket.objects.create(obj_name)
 
-            with helpers.cleanup_action(lambda: obj.delete()):
+            with cb_helpers.cleanup_action(lambda: obj.delete()):
                 # TODO: This is wrong. We shouldn't have to have a separate
                 # call to upload some content before being able to delete
                 # the content. Maybe the create_object method should accept
@@ -137,11 +153,11 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
         name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
         test_bucket = self.provider.storage.buckets.create(name)
 
-        with helpers.cleanup_action(lambda: test_bucket.delete()):
+        with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name = "hello_upload_download.txt"
             obj = test_bucket.objects.create(obj_name)
 
-            with helpers.cleanup_action(lambda: obj.delete()):
+            with cb_helpers.cleanup_action(lambda: obj.delete()):
                 content = b"Hello World. Here's some content."
                 # TODO: Upload and download methods accept different parameter
                 # types. Need to make this consistent - possibly provider
@@ -160,11 +176,11 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
         name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
         test_bucket = self.provider.storage.buckets.create(name)
 
-        with helpers.cleanup_action(lambda: test_bucket.delete()):
+        with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name = "hello_upload_download.txt"
             obj = test_bucket.objects.create(obj_name)
 
-            with helpers.cleanup_action(lambda: obj.delete()):
+            with cb_helpers.cleanup_action(lambda: obj.delete()):
                 content = b"Hello World. Generate a url."
                 obj.upload(content)
                 target_stream = BytesIO()
@@ -182,11 +198,11 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
         name = "cbtestbucketobjs-{0}".format(helpers.get_uuid())
         test_bucket = self.provider.storage.buckets.create(name)
 
-        with helpers.cleanup_action(lambda: test_bucket.delete()):
+        with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
             obj_name = "hello_upload_download.txt"
             obj = test_bucket.objects.create(obj_name)
 
-            with helpers.cleanup_action(lambda: obj.delete()):
+            with cb_helpers.cleanup_action(lambda: obj.delete()):
                 test_file = os.path.join(
                     helpers.get_test_fixtures_folder(), 'logo.jpg')
                 obj.upload_from_file(test_file)
@@ -195,30 +211,28 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
                 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")
+    @skip("Skip unless you want to test objects bigger than 5GB")
     @helpers.skipIfNoService(['storage.buckets'])
     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.
-        """
+        # 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)):
+        with cb_helpers.cleanup_action(lambda: os.remove(six_gig_file)):
             download_file = "{0}/cbtestfile-{1}".format(temp_dir, file_name)
             bucket_name = "cbtestbucketlargeobjs-{0}".format(
                                                             helpers.get_uuid())
             test_bucket = self.provider.storage.buckets.create(bucket_name)
-            with helpers.cleanup_action(lambda: test_bucket.delete()):
+            with cb_helpers.cleanup_action(lambda: test_bucket.delete()):
                 test_obj = test_bucket.objects.create(file_name)
-                with helpers.cleanup_action(lambda: test_obj.delete()):
+                with cb_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(
+                    with cb_helpers.cleanup_action(
                             lambda: os.remove(download_file)):
                         with open(download_file, 'wb') as f:
                             test_obj.save_content(f)

+ 12 - 13
cloudbridge/test/test_region_service.py

@@ -11,12 +11,20 @@ class CloudRegionServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['compute.regions'])
+    def test_storage_services_event_pattern(self):
+        # pylint:disable=protected-access
+        self.assertEqual(
+            self.provider.compute.regions._service_event_pattern,
+            "provider.compute.regions",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("regions",
+                                     "provider.compute.regions",
+                                     self.provider.compute.regions.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['compute.regions'])
     def test_get_and_list_regions(self):
-        """
-        Test whether the region listing methods work,
-        and whether zones are returned appropriately.
-        """
         regions = list(self.provider.compute.regions)
         sit.check_standard_behaviour(
             self, self.provider.compute.regions, regions[-1])
@@ -32,27 +40,18 @@ class CloudRegionServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['compute.regions'])
     def test_regions_unique(self):
-        """
-        Regions should not return duplicate items
-        """
         regions = self.provider.compute.regions.list()
         unique_regions = set([region.id for region in regions])
         self.assertTrue(len(regions) == len(list(unique_regions)))
 
     @helpers.skipIfNoService(['compute.regions'])
     def test_current_region(self):
-        """
-        RegionService.current should return a valid region
-        """
         current_region = self.provider.compute.regions.current
         self.assertIsInstance(current_region, Region)
         self.assertTrue(current_region in self.provider.compute.regions)
 
     @helpers.skipIfNoService(['compute.regions'])
     def test_zones(self):
-        """
-        Test whether regions return the correct zone information
-        """
         zone_find_count = 0
         test_zone = helpers.get_provider_test_data(self.provider, "placement")
         for region in self.provider.compute.regions:

+ 41 - 25
cloudbridge/test/test_security_service.py

@@ -15,6 +15,27 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['security.vm_firewalls'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(
+            self.provider.security.key_pairs.
+            _service_event_pattern,
+            "provider.security.key_pairs",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("key_pairs",
+                                     "provider.security.key_pairs",
+                                     self.provider.security.
+                                     key_pairs.
+                                     _service_event_pattern))
+        self.assertEqual(
+            self.provider.security.vm_firewalls._service_event_pattern,
+            "provider.security.vm_firewalls",
+            "Event pattern for {} service should be '{}', "
+            "but found '{}'.".format("vm_firewalls",
+                                     "provider.security.vm_firewalls",
+                                     self.provider.security.vm_firewalls.
+                                     _service_event_pattern))
+
     @helpers.skipIfNoService(['security.key_pairs'])
     def test_crud_key_pair_service(self):
 
@@ -23,7 +44,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
         def cleanup_kp(kp):
             if kp:
-                self.provider.security.key_pairs.delete(key_pair_id=kp.id)
+                self.provider.security.key_pairs.delete(kp.id)
 
         def extra_tests(kp):
             # Recreating existing keypair should raise an exception
@@ -38,7 +59,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
     def test_key_pair_properties(self):
         name = 'cb-kpprops-{0}'.format(helpers.get_uuid())
         kp = self.provider.security.key_pairs.create(name=name)
-        with helpers.cleanup_action(lambda: kp.delete()):
+        with cb_helpers.cleanup_action(lambda: kp.delete()):
             self.assertIsNotNone(
                 kp.material,
                 "KeyPair material is empty but it should not be.")
@@ -54,7 +75,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         public_key, _ = cb_helpers.generate_key_pair()
         kp = self.provider.security.key_pairs.create(
             name=name, public_key_material=public_key)
-        with helpers.cleanup_action(lambda: kp.delete()):
+        with cb_helpers.cleanup_action(lambda: kp.delete()):
             self.assertIsNone(kp.material, "Private KeyPair material should"
                               " be None when key is imported.")
 
@@ -66,14 +87,19 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
         def create_fw(label):
             return self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
         def cleanup_fw(fw):
             if fw:
                 fw.delete()
 
+        def network_id_test(fw):
+            # Checking that the network ID is returned correctly
+            self.assertEqual(fw.network_id, net.id)
+
         sit.check_crud(self, self.provider.security.vm_firewalls,
-                       VMFirewall, "cb-crudfw", create_fw, cleanup_fw)
+                       VMFirewall, "cb-crudfw", create_fw, cleanup_fw,
+                       extra_test_func=network_id_test)
 
     @helpers.skipIfNoService(['security.vm_firewalls'])
     def test_vm_firewall_properties(self):
@@ -82,12 +108,12 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         fw = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 vm_firewall=fw)):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             self.assertEqual(label, fw.description)
 
@@ -99,9 +125,9 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         net = subnet.network
 
         fw = None
-        with helpers.cleanup_action(lambda: fw.delete()):
+        with cb_helpers.cleanup_action(lambda: fw.delete()):
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             def create_fw_rule(label):
                 return fw.rules.create(
@@ -123,12 +149,12 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         fw = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 vm_firewall=fw)):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, protocol='tcp',
@@ -146,13 +172,13 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         fw = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 vm_firewall=fw)):
 
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, protocol='tcp',
@@ -170,22 +196,12 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         # Declare these variables and late binding will allow
         # the cleanup method access to the most current values
         fw = None
-        with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
+        with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 vm_firewall=fw)):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
-            rules = list(fw.rules)
-            self.assertTrue(
-                # TODO: This should be made consistent across all providers.
-                # Currently, OpenStack creates two rules, one for IPV6 and
-                # another for IPV4
-                len(rules) >= 1, "Expected a single VM firewall rule allowing"
-                " all outbound traffic. Got {0}.".format(rules))
-            self.assertEqual(
-                rules[0].direction, TrafficDirection.OUTBOUND,
-                "Expected rule to be outbound. Got {0}.".format(rules))
+                label=label, description=label, network=net.id)
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, src_dest_fw=fw,
                 protocol='tcp', from_port=1, to_port=65535)

+ 14 - 5
cloudbridge/test/test_vm_types_service.py

@@ -9,6 +9,17 @@ class CloudVMTypesServiceTestCase(ProviderTestBase):
 
     _multiprocess_can_split_ = True
 
+    @helpers.skipIfNoService(['compute.vm_types'])
+    def test_storage_services_event_pattern(self):
+        self.assertEqual(self.provider.compute.vm_types._service_event_pattern,
+                         "provider.compute.vm_types",
+                         "Event pattern for {} service should be '{}', "
+                         "but found '{}'.".format("vm_types",
+                                                  "provider.compute.vm_types",
+                                                  self.provider.compute.
+                                                  vm_types.
+                                                  _service_event_pattern))
+
     @helpers.skipIfNoService(['compute.vm_types'])
     def test_vm_type_properties(self):
 
@@ -63,11 +74,9 @@ class CloudVMTypesServiceTestCase(ProviderTestBase):
 
     @helpers.skipIfNoService(['compute.vm_types'])
     def test_vm_types_standard(self):
-        """
-        Searching for an instance by name should return an
-        VMType object and searching for a non-existent
-        object should return an empty iterator
-        """
+        # Searching for an instance by name should return an
+        # VMType object and searching for a non-existent
+        # object should return an empty iterator
         vm_type_name = helpers.get_provider_test_data(
             self.provider,
             "vm_type")

BIN
credentials.tar.gz.enc


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

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

+ 7 - 5
docs/conf.py

@@ -55,7 +55,7 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'cloudbridge'
-copyright = u'2017, GVL and Galaxy Projects'
+copyright = u'2019, GVL and Galaxy Projects'
 author = u'GVL and Galaxy Projects'
 
 # The version info for the project you're documenting, acts as replacement for
@@ -63,9 +63,9 @@ author = u'GVL and Galaxy Projects'
 # built documents.
 #
 # The short X.Y version.
-version = '0.1'
+version = '1.0.2'
 # The full version, including alpha/beta/rc tags.
-release = '0.1'
+release = '1.0.2'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -121,7 +121,9 @@ html_theme = 'sphinx_rtd_theme'
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+html_theme_options = {
+    'style_external_links': True
+}
 
 # Add any paths that contain custom themes here, relative to this directory.
 #html_theme_path = []
@@ -146,7 +148,7 @@ html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+# html_static_path = ['_static']
 
 # Add any extra paths that contain custom files (such as robots.txt or
 # .htaccess) here, relative to this directory. These files are copied

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


+ 50 - 40
docs/getting_started.rst

@@ -64,7 +64,7 @@ OpenStack (with Keystone authentication v3):
               'os_user_domain_name': 'domain name'}
     provider = CloudProviderFactory().create_provider(ProviderList.OPENSTACK,
                                                       config)
-    image_id = 'acb53109-941f-4593-9bf8-4a53cb9e0739'  # Ubuntu 16.04 @ Jetstream
+    image_id = '470d2fba-d20b-47b0-a89a-ab725cd09f8b'  # Ubuntu 18.04@Jetstream
 
 Azure:
 
@@ -79,6 +79,18 @@ Azure:
     provider = CloudProviderFactory().create_provider(ProviderList.AZURE, config)
     image_id = 'Canonical:UbuntuServer:16.04.0-LTS:latest'  # Ubuntu 16.04
 
+Google Compute Cloud:
+
+.. code-block:: python
+
+    from cloudbridge.cloud.factory import CloudProviderFactory, ProviderList
+
+    config = {'gcp_project_name': 'project name',
+              'gcp_service_creds_file': 'service_file.json',
+              'gcp_default_zone': 'us-east1-b',  # Use desired value
+              'gcp_region_name': 'us-east1'}  # Use desired value
+    provider = CloudProviderFactory().create_provider(ProviderList.GCP, config)
+    image_id = 'https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/global/images/ubuntu-1804-bionic-v20181222'
 
 List some resources
 -------------------
@@ -86,14 +98,14 @@ Once you have a reference to a provider, explore the cloud platform:
 
 .. code-block:: python
 
-    provider.security.firewalls.list()
+    provider.security.vm_firewalls.list()
     provider.compute.vm_types.list()
     provider.storage.snapshots.list()
     provider.storage.buckets.list()
 
 This will demonstrate the fact that the library was properly installed and your
-provider object is setup correctly but it is not very interesting. Therefore,
-let's create a new instance we can ssh into using a key pair.
+provider object is setup correctly. By itself, those commands are not very
+interesting so let's create a new instance we can ssh into using a key pair.
 
 Create a key pair
 -----------------
@@ -103,8 +115,8 @@ on disk as a read-only file.
 .. code-block:: python
 
     import os
-    kp = provider.security.key_pairs.create('cloudbridge-intro')
-    with open('cloudbridge_intro.pem', 'w') as f:
+    kp = provider.security.key_pairs.create('cb-keypair')
+    with open('cloudbridge_intro.pem', 'wb') as f:
         f.write(kp.material)
     os.chmod('cloudbridge_intro.pem', 0o400)
 
@@ -117,11 +129,13 @@ attaching an internet gateway to the subnet via a router.
 .. code-block:: python
 
     net = provider.networking.networks.create(cidr_block='10.0.0.0/16',
-                                              label='my-network')
-    sn = net.create_subnet(cidr_block='10.0.0.0/28', label='my-subnet')
-    router = provider.networking.routers.create(network=net, label='my-router')
+                                              label='cb-network')
+    zone = provider.compute.regions.get(provider.region_name).zones[0]
+    sn = net.subnets.create(
+        cidr_block='10.0.0.0/28', label='cb-subnet', zone=zone)
+    router = provider.networking.routers.create(network=net, label='cb-router')
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway()
+    gateway = net.gateways.get_or_create()
     router.attach_gateway(gateway)
 
 
@@ -135,8 +149,8 @@ a private network.
 
     from cloudbridge.cloud.interfaces.resources import TrafficDirection
     fw = provider.security.vm_firewalls.create(
-        label='cloudbridge-intro', description='A VM firewall used by
-        CloudBridge', network_id=net.id)
+        label='cb-firewall', description='A VM firewall used by
+        CloudBridge', network=net)
     fw.rules.create(TrafficDirection.INBOUND, 'tcp', 22, 22, '0.0.0.0/0')
 
 Launch an instance
@@ -148,12 +162,11 @@ also add the network interface as a launch argument.
 .. code-block:: python
 
     img = provider.compute.images.get(image_id)
-    zone = provider.compute.regions.get(provider.region_name).zones[0]
     vm_type = sorted([t for t in provider.compute.vm_types
                       if t.vcpus >= 2 and t.ram >= 4],
                       key=lambda x: x.vcpus*x.ram)[0]
     inst = provider.compute.instances.create(
-        image=img, vm_type=vm_type, label='cloudbridge-intro',
+        image=img, vm_type=vm_type, label='cb-instance',
         subnet=sn, zone=zone, key_pair=kp, vm_firewalls=[fw])
     # Wait until ready
     inst.wait_till_ready()  # This is a blocking call
@@ -180,9 +193,10 @@ earlier.
 
 .. code-block:: python
 
-    fip = gateway.floating_ips.create()
-    inst.add_floating_ip(fip)
-    inst.refresh()
+    if not inst.public_ips:
+        fip = gateway.floating_ips.create()
+        inst.add_floating_ip(fip)
+        inst.refresh()
     inst.public_ips
     # [u'54.166.125.219']
 
@@ -197,59 +211,55 @@ unique, multiple resources of the same type could use the same label, thus the
 `find` method always returns a list, while the `get` method returns a single
 object. While the methods are similar across resources, they are explicitly
 listed in order to help map each resource with the service that handles it.
+Note that labeled resources allow to find by label, while unlabeled
+resources find by name or their special properties (eg: public_ip for
+floating IPs). For more detailed information on the types of resources and
+their provider mappings, see :doc:`topics/resource_types_and_mapping`.
 
 .. code-block:: python
 
     # Key Pair
     kp = provider.security.key_pairs.get('keypair ID')
-    kp_list = provider.security.key_pairs.find(name='cloudbridge-intro')
-    kp = kp_list[0]
+    kp = provider.security.key_pairs.find(name='cb-keypair')[0]
+
+    # Floating IPs
+    fip = gateway.floating_ips.get('FloatingIP ID')
+    # Find using public IP address
+    fip_list = gateway.floating_ips.find(public_ip='IP address')
+    # Find using name (the behavior of the `name` property can be 
+    # cloud-dependent). More details can be found `here <topics/resource_types_and_mapping.html>`
+    fip_list = gateway.floating_ips.find(name='cb-fip')[0]
 
     # Network
     net = provider.networking.networks.get('network ID')
-    net_list = provider.networking.networks.find(name='my-network')
     net_list = provider.networking.networks.find(label='my-network')
     net = net_list[0]
 
     # Subnet
     sn = provider.networking.subnets.get('subnet ID')
     # Unknown network
-    sn_list = provider.networking.subnets.find(name='my-subnet')
-    sn_list = provider.networking.subnets.find(label='my-subnet')
+    sn_list = provider.networking.subnets.find(label='cb-subnet')
     # Known network
-    sn_list = provider.networking.subnets.find(network=net.id, name='my-subnet')
     sn_list = provider.networking.subnets.find(network=net.id,
-                                               label='my-subnet')
+                                               label='cb-subnet')
     sn = sn_list(0)
 
     # Router
     router = provider.networking.routers.get('router ID')
-    router_list = provider.networking.routers.find(name='my-router')
-    router_list = provider.networking.routers.find(label='my-router')
+    router_list = provider.networking.routers.find(label='cb-router')
     router = router_list[0]
 
     # Gateway
-    gateway = net.gateways.get_or_create_inet_gateway()
-
-    # Floating IPs
-    fip = gateway.floating_ips.get('FloatingIP ID')
-    # Find using public IP address
-    fip_list = gateway.floating_ips.find(public_ip='IP address')
-    # Find using name or tag
-    fip_list = net.gateways.floating_ips.find(name='my-fip')
-    fip_list = net.gateways.floating_ips.find(label='my-fip')
-    fip = fip_list[0]
+    gateway = net.gateways.get_or_create()
 
     # Firewall
     fw = provider.security.vm_firewalls.get('firewall ID')
-    fw_list = provider.security.vm_firewalls.find(name='cloudbridge-intro')
-    fw_list = provider.security.vm_firewalls.find(label='cloudbridge-intro')
+    fw_list = provider.security.vm_firewalls.find(label='cb-firewall')
     fw = fw_list[0]
 
     # Instance
     inst = provider.compute.instances.get('instance ID')
-    inst_list = provider.compute.instances.list(name='cloudbridge-intro')
-    inst_list = provider.compute.instances.list(label='cloudbridge-intro')
+    inst_list = provider.compute.instances.list(label='cb-instance')
     inst = inst_list[0]
 
 

+ 168 - 0
docs/topics/aws_mapping.rst

@@ -0,0 +1,168 @@
+Detailed AWS Type and Resource Mappings
+=======================================
+
+AWS Dashboard
+-------------
+AWS has a particular dashboard as resources are found within different
+services. The following table lists the dashboard location of each resource,
+and the below screenshot shows how the switch between the various services.
+
++------------------------+-----+
+| Instance               | EC2 |
++------------------------+-----+
+| MachineImage (Private) | EC2 |
++------------------------+-----+
+| Volume                 | EC2 |
++------------------------+-----+
+| Snapshot               | EC2 |
++------------------------+-----+
+| VMFirewall             | EC2 |
++------------------------+-----+
+| FloatingIP             | EC2 |
++------------------------+-----+
+| KeyPair                | EC2 |
++------------------------+-----+
+| VMFirewallRule         | EC2 |
++------------------------+-----+
+| Network                | VPC |
++------------------------+-----+
+| Subnet                 | VPC |
++------------------------+-----+
+| Router                 | VPC |
++------------------------+-----+
+| InternetGateway        | VPC |
++------------------------+-----+
+| Bucket                 | S2  |
++------------------------+-----+
+| BucketObject           | S2  |
++------------------------+-----+
+
+.. figure:: captures/aws-services-dash.png
+   :alt: EC2, VPC, and S3
+
+   Resources in AWS are separated into three dashboards depending on the
+   type of service handling the resources
+
+
+AWS - Labeled Resources
+-----------------------
++------------------------+-------------------+----------------+----------------+----------+
+| Labeled Resource       | AWS Resource Type | CB ID          | CB Name        | CB Label |
++========================+===================+================+================+==========+
+| AWSInstance            | Instance          | Instance ID    | Instance ID    | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSMachineImage        | AMI               | AMI ID         | AMI Name       | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSNetwork             | VPC               | VPC ID         | VPC ID         | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSSubnet              | Subnet            | Subnet ID      | Subnet ID      | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSRouter              | Route Table       | Route Table ID | Route Table ID | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSVolume              | Volume            | Volume ID      | Volume ID      | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSSnapshot            | Snapshot          | Snapshot ID    | Snapshot ID    | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+| AWSVMFirewall          | Security Group    | Group ID       | Group Name     | tag:Name |
++------------------------+-------------------+----------------+----------------+----------+
+
+The resources listed above are labeled, they thus have both the `name` and
+`label` properties in CloudBridge. These resources require a mandatory `label`
+parameter at creation. For all labeled resources, the `label` property in AWS
+maps to the tag with `key:Name`. However, unlike in Azure where all resources
+have names, only some AWS resources have an unchangeable name by which to
+identify them. Thus, for most AWS resources, the `name` property maps to the
+ID, in order to preserve the concept of names being a unique identifier,
+even if they are not easily readable in this context. For resources that do
+support naming in AWS, the `name` will be generated from the `label` given at
+creation, consisting of up to 55 characters from the label, followed by a UUID.
+The label property can subsequently be changed, but the name property will
+be set at creation and remain unchanged. Finally, labeled resources support
+a `label` parameter for the `find` method in their corresponding services.
+The below screenshots will help map these properties to AWS objects in the
+web portal.
+
+.. figure:: captures/aws-instance-dash.png
+   :alt: name, ID, and label properties for AWS EC2 Instances
+
+   The CloudBridge `name` and `ID` properties map to the unchangeable
+   resource ID in AWS when the resource does not allow for an unchangeable
+   name. The `label` property maps to the tag with key 'Name' for all
+   resources in AWS. By default, this label will appear in the first
+   column.
+
+.. figure:: captures/aws-ami-dash.png
+   :alt: name, ID, and label properties for AWS EC2 AMIs
+
+   When an AWS resource allows for an unchangeable name, the CloudBridge
+   `ID` property maps to the Resource ID, while the `Name` property maps to
+   the Resource Name. The `label` property maps to the tag with key 'Name'
+   for all resources in AWS. By default, this label will appear in the first
+   column.
+
+
+AWS - Unlabeled Resources
+---------------------------
++-----------------------+--------------------+-------+---------+----------+
+| Unlabeled Resource    | AWS Resource Type  | CB ID | CB Name | CB Label |
++=======================+====================+=======+=========+==========+
+| AWSKeyPair            | Key Pair           | Name  | Name    | -        |
++-----------------------+--------------------+-------+---------+----------+
+| AWSBucket             | Bucket             | Name  | Name    | -        |
++-----------------------+--------------------+-------+---------+----------+
+| AWSBucketObject       | Bucket Object      | Key   | Key     | -        |
++-----------------------+--------------------+-------+---------+----------+
+
+The resources listed above are unlabeled. They thus only have the `name`
+property in CloudBridge. These resources require a mandatory `name`
+parameter at creation, which will directly map to the unchangeable `name`
+property. Additionally, for these resources, the `ID` property also maps to
+the `name` in AWS, as these resources don't have an `ID` in the
+traditional sense and can be located by name. Finally, unlabeled resources
+support a `name` parameter for the `find` method in their corresponding
+services.
+
+.. figure:: captures/aws-bucket.png
+   :alt: list of buckets on AWS dashboard
+
+   Buckets can be found in the Amazon S3 portal. BucketObjects are contained
+   within each Bucket.
+
+
+AWS - Special Unlabeled Resources
+-----------------------------------
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| Unlabeled Resource | AWS Resource Type      | CB ID | CB Name                                                                | CB Label |
++====================+========================+=======+========================================================================+==========+
+| AWSFloatingIP      | Elastic IP             | ID    | [public_ip]                                                            | -        |
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| AWSInternetGateway | Internet Gateway       | ID    | tag:Name                                                               | -        |
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| AWSVMFirewallRule  | Network Security Rules | ID    | Generated: [direction]-[protocol]-[from_port]-[to_port]-[cidr]-[fw_id] | -        |
++--------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+
+While these resources are similarly unlabeled, they do not follow the same
+general rules as the ones listed above. Firstly, they differ by the fact
+that they take neither a `name` nor a `label` parameter at creation.
+Moreover, each of them has other special properties.
+
+The FloatingIP resource has a traditional resource ID, but instead of a
+traditional name, its `name` property maps to its Public IP.
+Moreover, the corresponding `find` method for Floating IPs can thus help
+find a resource by `Public IP Address`.
+
+In terms of the gateway, given that gateways are not their own objects in
+other providers, we do not treat them like labeled resources in AWS although
+they could support labels. Thus, the internet gateway create method does not
+take a name parameter, and the `name` property is set automatically to a
+default value. Note that since this value is stored in the tag with key Name,
+the AWS dashboard does allow for its modification, although that is not
+encouraged as the default name is expected for the
+`get_or_create` method.
+
+Finally, Firewall Rules in AWS differ from traditional unlabeled resources
+by the fact that they do not take a `name` parameter at creation, and the
+`name` property is automatically generated from the rule's properties, as
+shown above. These rules can be found within each Firewall (i.e. Security
+Group) in the AWS EC2 portal, and will not have any name in the AWS dashboard
+

+ 221 - 0
docs/topics/azure_mapping.rst

@@ -0,0 +1,221 @@
+Detailed Azure Type and Resource Mappings
+=========================================
+
+Azure - Labeled Resources
+-------------------------
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| Labeled CloudBridge Resource          | Azure Resource Type    | CB ID | CB Name                | CB Label                           |
++=======================================+========================+=======+========================+====================================+
+| AzureInstance                         | Virtual Machine        | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureMachineImage (Private)           | Image                  | ID    | Name                   | tag:Label                          |
+| AzureMachineImage (Marketplace Image) | VirtualMachineImage    | ID    | URN                    | URN                                |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureNetwork                          | Virtual Network        | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureSubnet                           | Subnet                 | ID    | NetworkName/SubnetName | Network:tag:SubnetLabel_SubnetName |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureRouter                           | Route Table            | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureVolume                           | Disk                   | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureSnapshot                         | Snapshot               | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+| AzureVMFirewall                       | Network security group | ID    | Name                   | tag:Label                          |
++---------------------------------------+------------------------+-------+------------------------+------------------------------------+
+
+The resources listed above are labeled, they thus have both the `name` and
+`label` properties in CloudBridge. These resources require a mandatory `label`
+parameter at creation. The `label` will then be used to create the `name`,
+which will consist of up to 55 characters from the label, followed by a UUID.
+The label property can subsequently be changed, but the name property will
+remain unchanged, as it is part of the ID. Finally, labeled resources support
+a `label` parameter for the `find` method in their corresponding services.
+The below screenshots will help map these properties to Azure objects in the
+web portal.
+Additionally, although Azure Security Groups are not associated with a
+specific network, such an association is done in CloudBridge, due to its
+necessity in AWS. As such, the VMFirewall creation method requires a
+`network` parameter and the association is accomplished in OpenStack through
+a tag with the key `network_id`.
+
+.. figure:: captures/az-label-dash.png
+   :alt: name and label properties in Azure portal
+
+   The CloudBridge `name` property always maps to the unchangeable resource
+   name in Azure. The `label` property maps to the tag with key 'Label' for
+   most resources in Azure. By default, this label will appear in the tags
+   column, but can also be made into its own column, using the feature
+   pointed out in the screenshot above.
+
+.. figure:: captures/az-net-id.png
+   :alt: network id in Azure portal
+
+   The CloudBridge `ID` property most often maps to the Resource ID in Azure,
+   which can be found under the properties tab within a resource. The above
+   screenshot shows where to find a resource's ID in Azure's web portal.
+
+.. figure:: captures/az-net-label.png
+   :alt: network label in Azure portal
+
+   The CloudBridge `label` property most often maps to the tag with key
+   'Label' in Azure, which can be found under the tags tab within a resource.
+   The above screenshot shows where to find a resource's label in Azure's
+   web portal.
+
+Two labeled resources are exceptions to the general trends presented above,
+namely public images (i.e. Azure Marketplace Images) and subnets.
+
+These public images can be found in the Azure Marketplace, and cannot be
+found on a user's dashboard. A Marketplace Image can be passed either by URN,
+or by public ID, and does not need to be linked to a user. While all
+Marketplace images will not be be listed by the find or list methods at the
+moment, a pre-set list of popular images is built into CloudBridge for that
+purpose. However, one can choose to list all Marketplace Images using the
+`list_marketplace_images` function in the azure client. Specifically,
+this can be done as follows:
+
+.. code-block:: python
+
+    # List all images
+    # Note that in September 2018, around 10 minutes of wall time were required
+    # to fetch the entire list
+    provider.azure_client.list_marketplace_images()
+    # List all images published by Canonical
+    provider.azure_client.list_marketplace_images(publisher='Canonical')
+    # List all Ubuntu images
+    provider.azure_client.list_marketplace_images(publisher='Canonical',
+                                                  offer='UbuntuServer')
+    # List all Ubuntu 16.04 images
+    provider.azure_client.list_marketplace_images(publisher='Canonical',
+                                                  offer='UbuntuServer',
+                                                  sku='16.04.0-LTS')
+    # The ID of the listed object can then be used to retrieve an instance
+    img = provider.compute.images.get
+            ('/Subscriptions/{subscriptionID}/Providers/Microsoft.Compute/\
+            Locations/{regionName}/Publishers/Canonical/ArtifactTypes/VMImage\
+            /Offers/UbuntuServer/Skus/16.04.0-LTS/Versions/16.04.201808140')
+    # The URN can also be used instead if it is already known
+    # When the latest version is desired, it can be retrieved with the
+    # keyword 'latest' in the URN without specifying a version
+    img = provider.compute.images.get(
+          'Canonical:UbuntuServer:16.04.0-LTS:latest')
+
+
+Given that these resources are not owned by the user, they can only be
+referenced and all setters will silently pass. CloudBridge properties `name`
+and `label` will map to the URN, while the `ID` will map to the public `ID`.
+It is also important to note that some of these resources are paid and
+required a plan to use, while others are free but likewise require accepting
+certain terms before being used. These plans and terms are passed and
+accepted silently by CloudBridge in order to keep the code cloud-independent.
+We therefore encourage using the
+`marketplace website <https://azuremarketplace.microsoft.com/en-us>`_
+to view the images and plan details before using them in CloudBridge.
+
+Additionally, Subnets are a particular resource in Azure because they are
+not simply found in the Resource Group like most resources, but are rather
+nested within a network. Moreover, Subnets do not support tags in Azure.
+However, they remain a labeled resource in CloudBridge, which was
+accomplished by creating Network tags holding Subnet labels in Azure. The
+below screenshots will show how to find Subnets and their labels in the
+Azure web portal.
+
+.. figure:: captures/az-subnet-name.png
+   :alt: subnet name in Azure portal
+
+   The CloudBridge `name` property for Subnets corresponds to the
+   unchangeable Resource Name in Azure. However, unlike other resources
+   where the Azure Name maps directly to the `name` property alone, a Subnet's
+   `name` property returns the Network's name and the Subnet's name,
+   separated by a slash, thus having the format [networkName]/[subnetName].
+   Subnets are additionally not found in the default resource list, but are
+   rather nested within a Network, in the Subnets tab as shown above.
+
+.. figure:: captures/az-subnet-label.png
+   :alt: subnet label in Azure portal
+
+   The CloudBridge `label` property most often maps to the tag with key
+   'Label' in Azure, which can be found under the tags tab within a resource.
+   However, given that Subnets can't hold tags themselves, we set their tags
+   in the Network with which they are associated. The tag name 'Label' thus
+   corresponds to the Network's label, while each contained Subnet will have
+   a corresponding tag with the name 'SubnetLabel_[subnetName]'.
+
+
+Azure - Unlabeled Resources
+---------------------------
++--------------------+----------------------------------------+-------+---------+----------+
+| Unlabeled Resource | Azure Resource Type                    | CB ID | CB Name | CB Label |
++====================+========================================+=======+=========+==========+
+| AzureKeyPair       | StorageAccount:Table                   | Name  | Name    | -        |
++--------------------+----------------------------------------+-------+---------+----------+
+| AzureBucket        | StorageAccount:BlobContainer           | Name  | Name    | -        |
++--------------------+----------------------------------------+-------+---------+----------+
+| AzureBucketObject  | StorageAccount:BlobContainer:BlockBlob | Name  | Name    | -        |
++--------------------+----------------------------------------+-------+---------+----------+
+
+The resources listed above are unlabeled. They thus only have the `name`
+property in CloudBridge. These resources require a mandatory `name`
+parameter at creation, which will directly map to the unchangeable `name`
+property. Additionally, for these resources, the `ID` property also maps to
+the `name` in Azure, as these resources don't have an `ID` in the
+traditional sense and can be located simply by name. Finally, unlabeled
+resources support a `name` parameter for the `find` method in their
+corresponding services.
+
+.. figure:: captures/az-storacc.png
+   :alt: storage account in Azure portal
+
+   Bucket and Key Pair objects are different than other resources in Azure,
+   as they are not resources simply residing in a resource group, but are
+   rather found in a storage account. As a result of this difference, these
+   resources do not support labels, and cannot be seen on the default
+   dashboard. In order to find these resources in the Azure web portal, one
+   must head to the storage account containing them, and look in the `Blobs`
+   and `Tables` services respectively for `Buckets` and `KeyPairs`.
+
+
+Azure - Special Unlabeled Resources
+-----------------------------------
++-------------------------+------------------------+--------------------+--------------------+----------+
+| Unlabeled Resource      | Azure Resource Type    | CB ID              | CB Name            | CB Label |
++=========================+========================+====================+====================+==========+
+| AzureFloatingIP         | Public IP Address      | ID                 | [public_ip]        | -        |
++-------------------------+------------------------+--------------------+--------------------+----------+
+| AzureInternetGateway    | None                   | cb-gateway-wrapper | cb-gateway-wrapper | -        |
++-------------------------+------------------------+--------------------+--------------------+----------+
+| AzureVMFirewallRule     | Network Security Rules | ID                 | name               | -        |
++-------------------------+------------------------+--------------------+--------------------+----------+
+
+While these resources are similarly unlabeled, they do not follow the same
+general rules as the ones listed above. Firstly, they differ by the fact
+that they take neither a `name` nor a `label` parameter at creation.
+Moreover, each of them has other special properties.
+
+The FloatingIP resource has a traditional resource ID, but instead of a
+traditional name, its `name` property maps to its Public IP. Thus, the name
+seen in the Azure web portal will not map to the CloudBridge name, but will
+rather be auto-generated, while the Azure `IP Address` will map to CloudBridge
+name. Moreover, the corresponding `find` method for Floating IPs can thus help
+find a resource by `Public IP Address`, and the get method also accepts a
+'Public IP' instead of an 'ID'.
+
+In terms of the gateway, one of the major discrepancies in Azure is the
+non-existence of an InternetGateway. In fact, Azure resources are exposed
+with no need for an Internet gateway. However, in order to keep resources
+consistent across providers, the CloudBridge Gateway resource exists
+regardless of provider. For Azure, the gateway object created through
+CloudBridge will not appear on the dashboard, but will rather be a cached
+CloudBridge-level wrapper object.
+For a succinct comparison between AWS Gateways and Azure, see `this answer
+<https://social.msdn.microsoft.com/Forums/en-US/
+814ccee0-9fbb-4c04-8135-49d0aaea5f38/
+equivalent-of-aws-internet-gateways-in-azure?
+forum=WAVirtualMachinesVirtualNetwork>`_.
+
+Finally, Firewall Rules in Azure differ from traditional unlabeled
+resources by the fact that they do not take a `name` parameter at creation.
+These rules can be found within each Firewall (i.e. Security Group) in the
+Azure web portal, and will have an automatically generated `name` of the form
+'cb-rule-[int]'.

BIN
docs/topics/captures/aws-ami-dash.png


BIN
docs/topics/captures/aws-bucket.png


BIN
docs/topics/captures/aws-instance-dash.png


BIN
docs/topics/captures/aws-services-dash.png


BIN
docs/topics/captures/az-app-1.png


BIN
docs/topics/captures/az-app-2.png


BIN
docs/topics/captures/az-app-3.png


BIN
docs/topics/captures/az-app-4.png


BIN
docs/topics/captures/az-app-5.png


BIN
docs/topics/captures/az-app-6.png


BIN
docs/topics/captures/az-app-7.png


BIN
docs/topics/captures/az-dir-1.png


BIN
docs/topics/captures/az-dir-2.png


BIN
docs/topics/captures/az-label-dash.png


BIN
docs/topics/captures/az-net-id.png


BIN
docs/topics/captures/az-net-label.png


BIN
docs/topics/captures/az-role-1.png


BIN
docs/topics/captures/az-role-2.png


BIN
docs/topics/captures/az-role-3.png


BIN
docs/topics/captures/az-storacc.png


BIN
docs/topics/captures/az-sub-1.png


BIN
docs/topics/captures/az-sub-2.png


BIN
docs/topics/captures/az-subnet-label.png


BIN
docs/topics/captures/az-subnet-name.png


BIN
docs/topics/captures/gce-sa-1.png


BIN
docs/topics/captures/gce-sa-2.png


BIN
docs/topics/captures/gce-sa-3.png


BIN
docs/topics/captures/gce-sa-4.png


BIN
docs/topics/captures/gce-sa-5.png


BIN
docs/topics/captures/os-instance-dash.png


BIN
docs/topics/captures/os-kp-dash.png


+ 0 - 121
docs/topics/dashboard.rst

@@ -1,121 +0,0 @@
-Dashboard Mapping
-=================
-
-Cross-Platform Concepts
------------------------
-
-Given cloudbridge's goal to work uniformly across cloud providers, some
-compromises were necessary in order to bridge the many differences between
-providers' resources and features. Notably, in order to create a robust and
-conceptually consistent cross-cloud library, resources were given three main
-properties: ID, name, and label.
-The `ID` corresponds to a unique identifier that can be reliably used to
-reference a resource. Users can safely use an ID knowing that it will always
-point to the same resource.
-The `name` property corresponds to an unchangeable and unique designation for
-a particular resource. This property is meant to be, in some ways, a more
-human-readable identifier. However, when no conceptually comparable property
-exists for a given resource in a particular provider, the ID is returned
-instead, as is the case for OpenStack resources. When the name can be
-determined by a user at resource creation, either the name parameter will be
-used for resources that support it, or the label will be used, when provided
-as a prefix, with an appended uuid to ensure that the name remains unique.
-The `label` property, conversely, is a changeable value that does not need
-to be unique. Unlike the name property, it is not used to identify a
-particular resource, but rather label a resource for easier distinction. It
-is however important to note that not all resources support labels. When
-supported, labels given at creation will also be used as a prefix to the name.
-
-Properties per Resource per Provider
-------------------------------------
-The sections below will present a summary table detailing the cloudbridge
-properties implemented for each resource, and their corresponding value in
-the provider's dashboard.
-
-Azure
------
-+-----------------------------------+-------+---------------+---------------+
-| CloudServiceType                 	| CB_ID	| CB_Name      	| CB_Label  	|
-+===================================+=======+===============+===============+
-| Instance                         	| ID   	| Name       	| Tags:Label 	|
-+-----------------------------------+-------+---------------+---------------+
-| MachineImage (Private)           	| ID   	| Name       	| Tags:Label 	|
-| MachineImage (Gallery Reference) 	| URN  	| URN        	| URN        	|
-+-----------------------------------+-------+---------------+---------------+
-| Network                          	| ID   	| Name       	| Tags:Label 	|
-+-----------------------------------+-------+---------------+---------------+
-| Subnet                           	| ID   	| Name       	| Tags:Label 	|
-+-----------------------------------+-------+---------------+---------------+
-| FloatingIP                       	| ID   	| Name       	| Tags:Label 	|
-+-----------------------------------+-------+---------------+---------------+
-| Router                           	| ID   	| Name       	| Tags:Label 	|
-+-----------------------------------+-------+---------------+---------------+
-| InternetGateway                  	| None 	| None       	| -          	|
-+-----------------------------------+-------+---------------+---------------+
-| Volume                           	| ID   	| Name       	| Tags:Label 	|
-+-----------------------------------+-------+---------------+---------------+
-| Snapshot                         	| ID   	| Name       	| Tags:Label 	|
-+-----------------------------------+-------+---------------+---------------+
-| KeyPair                          	| Name 	| Name       	| -          	|
-+-----------------------------------+-------+---------------+---------------+
-| VMFirewall                       	| ID   	| Name       	| Tags:Label 	|
-+-----------------------------------+-------+---------------+---------------+
-| VMFirewallRule                   	| ID   	| Name       	| -          	|
-+-----------------------------------+-------+---------------+---------------+
-| Bucket                           	| Name 	| Name       	| -          	|
-+-----------------------------------+-------+---------------+---------------+
-| BucketObject                     	| Name 	| Name       	| -          	|
-+-----------------------------------+-------+---------------+---------------+
-
-One of the major discrepancies in Azure is the non-existence of an Internet
-Gateway. In fact, Azure resources are automatically exposed to the internet,
-and thus an internet gateway object is not necessary for this purpose. Thus,
-a gateway object created through cloudbridge in Azure will not appear on the
-dashboard, as a cloudbridge-level wrapper object is returned when trying to
-create or get a gateway, but no object corresponds to that concept in Azure.
-For a succinct comparison between AWS Gateways and Azure, see:
-https://social.msdn.microsoft.com/Forums/en-US/
-814ccee0-9fbb-4c04-8135-49d0aaea5f38/
-equivalent-of-aws-internet-gateways-in-azure?
-forum=WAVirtualMachinesVirtualNetwork
-
-
-.. figure:: captures/az-label-dash.png
-   :scale: 50 %
-   :alt: name and label properties in Azure portal
-
-   The cloudbridge `name` property always maps to the unchangeable resource
-   name in Azure. The `label` property maps to the tag with key 'Label' in
-   Azure. By default, this label will appear in the tags column, but can also
-   be made into its own column, following the button indicated in the
-   screenshot above.
-
-.. figure:: captures/az-net-id.png
-   :scale: 50 %
-   :alt: network id in Azure portal
-
-   The cloudbridge `ID` property most often maps to the Resource ID in Azure,
-   which can be found under the properties tab within a resource. The above
-   screenshot shows where to find a resource's label in Azure's web portal
-
-.. figure:: captures/az-net-label.png
-   :scale: 50 %
-   :alt: network label in Azure portal
-
-   The cloudbridge `label` property most often maps to the tag with key
-   'Label' in Azure, which can be found under the tags tab within a resource.
-   The above screenshot shows where to find a resource's label in Azure's
-   web portal.
-
-.. figure:: captures/az-storacc.png
-   :scale: 50 %
-   :alt: storage account in Azure portal
-
-   Bucket and Key Pair objects are different than other resources in Azure,
-   as they are not resources residing in a resource group, but rather reside
-   in a storage account. As a result of this difference, these resources do
-   not support labels, and cannot be seen on the default dashboard. In order
-   to find these resources in the Azure web portal, one must head to the
-   storage account containing them, and look in the `Blobs` and `Tables`
-   services respectively for `Buckets` and `KeyPairs`.
-

+ 3 - 3
docs/topics/design_decisions.rst

@@ -41,8 +41,8 @@ Resource identification, naming, and labeling
   continued to use id and name, with the name being changeable for some
   resources, and read-only in others.
 
-  As CloudBridge evolved and support was added for Azure and GCE, things only
-  became more complex. Some providers (e.g. Azure and GCE) used a user-provided
+  As CloudBridge evolved and support was added for Azure and GCP, things only
+  became more complex. Some providers (e.g. Azure and GCP) used a user-provided
   value instead of an auto-generated value as an `id`, which would also be
   displayed in their respective dashboards as `Name`. This meant that they were
   treating their servers as individually named pets, instead of adopting the
@@ -65,7 +65,7 @@ Resource identification, naming, and labeling
   **Second iteration**
   However, it soon became apparent that this overloaded terminology was
   continuing to cause confusion. The `id` property in CloudBridge mapped to the
-  unchangeable `name` property in Azure and GCE, and the *name* property in
+  unchangeable `name` property in Azure and GCP, and the *name* property in
   cloudbridge sometimes mapped to a *tag* in certain providers, and a *name* in
   other providers and they were sometimes read-only, sometimes writable. In an
   attempt to disambiguate these concepts, it was then decided that perhaps

+ 219 - 0
docs/topics/event_system.rst

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

+ 7 - 7
docs/topics/launch.rst

@@ -49,7 +49,7 @@ Once we have all the desired pieces, we'll use them to launch an instance:
 .. code-block:: python
 
     inst = provider.compute.instances.create(
-        name='cloudbridge-vpc', image=img, vm_type=vm_type,
+        label='cloudbridge-vpc', image=img, vm_type=vm_type,
         subnet=subnet, zone=zone, key_pair=kp, vm_firewalls=[fw])
 
 Private networking
@@ -63,16 +63,16 @@ that subnet.
 .. code-block:: python
 
     net = self.provider.networking.networks.create(
-        name='my-network', cidr_block='10.0.0.0/16')
-    sn = net.create_subnet(name='my-subnet', cidr_block='10.0.0.0/28')
+        label='my-network', cidr_block='10.0.0.0/16')
+    sn = net.subnets.create(label='my-subnet', cidr_block='10.0.0.0/28')
     # make sure subnet has internet access
-    router = self.provider.networking.routers.create(network=net, name='my-router')
+    router = self.provider.networking.routers.create(label='my-router', network=net)
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway()
+    gateway = net.gateways.get_or_create()
     router.attach_gateway(gateway)
 
     inst = provider.compute.instances.create(
-        name='cloudbridge-vpc', image=img, vm_type=vm_type,
+        label='cloudbridge-vpc', image=img, vm_type=vm_type,
         subnet=sn, zone=zone, key_pair=kp, vm_firewalls=[fw])
 
 For more information on how to create and setup a private network, take a look
@@ -94,7 +94,7 @@ refer to :class:`.LaunchConfig`.
     lc = provider.compute.instances.create_launch_config()
     lc.add_volume_device(source=img, size=11, is_root=True)
     inst = provider.compute.instances.create(
-        name='cloudbridge-bdm', image=img,  vm_type=vm_type,
+        label='cloudbridge-bdm', image=img,  vm_type=vm_type,
         launch_config=lc, key_pair=kp, vm_firewalls=[fw],
         subnet=subnet, zone=zone)
 

+ 30 - 27
docs/topics/networking.rst

@@ -8,11 +8,11 @@ All CloudBridge deployed VMs must be deployed into a particular subnet.
 If you do not explicitly specify a private network to use when launching an
 instance, CloudBridge will attempt to use a default one. A 'default' network is
 one tagged as such by the native API. If such tag or functionality does not
-exist, CloudBridge will look for one with a predefined name (by default, called
-'CloudBridgeNet', which can be overridden with environment variable
-``CB_DEFAULT_NETWORK_NAME``).
+exist, CloudBridge will look for one with a predefined label (by default,
+called 'cloudbridge-net', which can be overridden with environment variable
+``CB_DEFAULT_NETWORK_LABEL``).
 
-Once a VM is deployed, cloudbridge's networking capabilities must address
+Once a VM is deployed, CloudBridge's networking capabilities must address
 several common scenarios.
 
 1. Allowing internet access from a launched VM
@@ -25,7 +25,7 @@ several common scenarios.
 
    Alternatively, the user may want to allow the instance to be contactable
    from the internet. In a more complex scenario, a user may want to deploy
-   VMS into several subnets, and deploy a gateway, jump host or bastion host
+   VMs into several subnets, and deploy a gateway, jump host, or bastion host
    to access other VMs which are not directly connected to the internet. In
    the latter scenario, the gateway/jump host/bastion host will need to be
    contactable over the internet.
@@ -37,19 +37,18 @@ several common scenarios.
    subnets depending on their tier. For example, consider the following
    scenario:
 
-   - Tier 1/Subnet 1 - Web Server Needs to be externally accessible over the
+   - Tier 1/Subnet 1 - Web Server needs to be externally accessible over the
      internet. However, in this particular scenario, the web server itself does
      not need access to the internet.
 
-   - Tier 2/Subnet 2 - Application Server The Application server must only be
-     able to communicate with the database server in Subnet 3, and receive
-     communication from the Web Server in Subnet 1. However, we assume a
-     special case here where the application server needs to access the
-     internet.
+   - Tier 2/Subnet 2 - Application Server must only be able to communicate with
+     the database server in Subnet 3, and receive communication from the Web
+     Server in Subnet 1. However, we assume a special case here where the
+     application server needs to access the internet.
 
-   - Tier 3/Subnet 3 - Database Server The database server must only be able to
-     receive incoming traffic from Tier 2, but must not be able to make
-     outgoing traffic outside of its subnet.
+   - Tier 3/Subnet 3 - Database Server must only be able to receive incoming
+     traffic from Tier 2, but must not be able to make outgoing traffic outside
+     of its subnet.
 
    At present, CloudBridge does not provide support for this scenario,
    primarily because OpenStack's FwaaS (Firewall-as-a-Service) is not widely
@@ -58,25 +57,29 @@ several common scenarios.
 1. Allowing internet access from a launched VM
 ----------------------------------------------
 Creating a private network is a simple, one-line command but appropriately
-connecting it so that it has uniform Internet access across all providers
+connecting it so that it has uniform internet access across all providers
 is a multi-step process:
 (1) create a network; (2) create a subnet within this network; (3) create a
-router; (4) attach the router to the subnet and (5) attach the router to the
+router; (4) attach the router to the subnet; and (5) attach the router to the
 internet gateway.
 
 When creating a network, we need to set an address pool. Any subsequent
 subnets you create must have a CIDR block that falls within the parent
-network's CIDR block. Below, we'll create a subnet starting from the beginning
-of the block and allow up to 16 IP addresses within a subnet (``/28``).
+network's CIDR block. CloudBridge also defines a default IPv4 network range in
+``BaseNetwork.CB_DEFAULT_IPV4RANGE``. Below, we'll create a subnet starting
+from the beginning of the block and allow up to 16 IP addresses within a
+subnet (``/28``).
 
 .. code-block:: python
 
     net = provider.networking.networks.create(
-        name='my-network', cidr_block='10.0.0.0/16')
-    sn = net.create_subnet(name='my-subnet', cidr_block='10.0.0.0/28', zone=zone)
-    router = provider.networking.routers.create(network=net, name='my-router')
+        label='my-network', cidr_block='10.0.0.0/16')
+    sn = net.subnets.create(label='my-subnet',
+                            cidr_block='10.0.0.0/28',
+                            zone=zone)
+    router = provider.networking.routers.create(label='my-router', network=net)
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway()
+    gateway = net.gateways.get_or_create()
     router.attach_gateway(gateway)
 
 
@@ -87,14 +90,14 @@ The additional step that's required here is to assign a floating IP to the VM:
 .. code-block:: python
 
     net = provider.networking.networks.create(
-        name='my-network', cidr_block='10.0.0.0/16')
-    sn = net.create_subnet(name='my-subnet', cidr_block='10.0.0.0/28', zone=zone)
+        label='my-network', cidr_block='10.0.0.0/16')
+    sn = net.subnets.create(label='my-subnet', cidr_block='10.0.0.0/28', zone=zone)
 
-    vm = provider.compute.instances.create('my-inst', subnet=sn, zone=zone, ...)
+    vm = provider.compute.instances.create(label='my-inst', subnet=sn, zone=zone, ...)
 
-    router = provider.networking.routers.create(network=net, name='my-router')
+    router = provider.networking.routers.create(label='my-router', network=net)
     router.attach_subnet(sn)
-    gateway = net.gateways.get_or_create_inet_gateway()
+    gateway = net.gateways.get_or_create()
     router.attach_gateway(gateway)
 
     fip = provider.networking.floating_ips.create()

+ 3 - 3
docs/topics/object_storage.rst

@@ -1,11 +1,11 @@
 Working with object storage
-==========================
+===========================
 Object storage provides a simple way to store and retrieve large amounts of
 unstructured data over HTTP. Object Storage is also referred to as Blob (Binary
 Large OBject) Storage by Azure, and Simple Storage Service (S3) by Amazon.
 
 Typically, you would store your objects within a Bucket, as it is known in
-AWS and GCE. A Bucket is also called a Container in OpenStack and Azure. In
+AWS and GCP. A Bucket is also called a Container in OpenStack and Azure. In
 CloudBridge, we use the term Bucket.
 
 Storing objects in a bucket
@@ -66,5 +66,5 @@ Once a provider is obtained, you can access the container as usual:
 .. code-block:: python
 
     bucket = provider.storage.buckets.get(container)
-    obj = bucket.create_object('my_object.txt')
+    obj = bucket.objects.create('my_object.txt')
     obj.upload_from_file(source)

+ 111 - 0
docs/topics/os_mapping.rst

@@ -0,0 +1,111 @@
+Detailed OpenStack Type and Resource Mappings
+=============================================
+
+OpenStack - Labeled Resources
+-----------------------------
++------------------------+------------------------+-----------+----------------+----------+
+| Labeled Resource       | OS Resource Type       | CB ID     | CB Name        | CB Label |
++========================+========================+===========+================+==========+
+| OpenStackInstance      | Instance               | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackMachineImage  | Image                  | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackNetwork       | Network                | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackSubnet        | Subnet                 | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackRouter        | Router                 | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackVolume        | Volume                 | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackSnapshot      | Snapshot               | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+| OpenStackVMFirewall    | Security Group         | ID        | ID             | Name     |
++------------------------+------------------------+-----------+----------------+----------+
+
+The resources listed above are labeled, they thus have both the `name` and
+`label` properties in CloudBridge. These resources require a mandatory `label`
+parameter at creation. For all labeled resources, the `label` property in
+OpenStack maps to the Name attribute. However, unlike in Azure or AWS, no
+resource has an unchangeable name by which to identify it in our OpenStack
+implementation. The `name` property will therefore map to the ID, preserving
+its role as an unchangeable identifier even though not easily readable in this
+context. Finally, labeled resources support a `label` parameter for the `find`
+method in their corresponding services. The below screenshots will help map
+these properties to OpenStack objects in the web portal. Additionally, although
+OpenStack Security Groups are not associated with a specific network, such an
+association is done in CloudBridge, due to its necessity in AWS. As such, the
+VMFirewall creation method requires a `network` parameter and the association
+is accomplished in OpenStack through the description, by appending the
+following string to the user-provided description (if any) at creation:
+"[CB-AUTO-associated-network-id: associated_net_id]"
+
+.. figure:: captures/os-instance-dash.png
+   :alt: name, ID, and label properties for OS Instances
+
+   The CloudBridge `name` and `ID` properties map to the unchangeable
+   resource ID in OpenStack as resources do not allow for an unchangeable
+   name. The `label` property maps to the 'Name' for all resources in
+   OpenStack. By default, this label will appear in the first column.
+
+
+OpenStack - Unlabeled Resources
+-------------------------------
++-----------------------+------------------------+-------+---------+----------+
+| Unlabeled Resource    | OS Resource Type       | CB ID | CB Name | CB Label |
++=======================+========================+=======+=========+==========+
+| OpenStackKeyPair      | Key Pair               | Name  | Name    | -        |
++-----------------------+------------------------+-------+---------+----------+
+| OpenStackBucket       | Object Store Container | Name  | Name    | -        |
++-----------------------+------------------------+-------+---------+----------+
+| OpenStackBucketObject | Object                 | Name  | Name    | -        |
++-----------------------+------------------------+-------+---------+----------+
+
+The resources listed above are unlabeled. They thus only have the `name`
+property in CloudBridge. These resources require a mandatory `name`
+parameter at creation, which will directly map to the unchangeable `name`
+property. Additionally, for these resources, the `ID` property also maps to
+the `name` in OpenStack, as these resources don't have an `ID` in the
+traditional sense and can be identified by name. Finally, unlabeled resources
+support a `name` parameter for the `find` method in their corresponding
+services.
+
+.. figure:: captures/os-kp-dash.png
+   :alt: KeyPair details on OS dashboard
+
+   KeyPairs and other unlabeled resources in OpenStack have `name` that is
+   unique and unmodifiable. The `ID` will thus map to the `name` property when
+   no other `ID` exists for that OpenStack resource.
+
+
+OpenStack - Special Unlabeled Resources
+---------------------------------------
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| Unlabeled Resource       | OS Resource Type       | CB ID | CB Name                                                                | CB Label |
++==========================+========================+=======+========================================================================+==========+
+| OpenStackFloatingIP      | Floating IP            | ID    | [public_ip]                                                            | -        |
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| OpenStackInternetGateway | Network `public`       | ID    | 'public'                                                               | -        |
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+| OpenStackVMFirewallRule  | Security Group Rule    | ID    | Generated: [direction]-[protocol]-[from_port]-[to_port]-[cidr]-[fw_id] | -        |
++--------------------------+------------------------+-------+------------------------------------------------------------------------+----------+
+
+While these resources are similarly unlabeled, they do not follow the same
+general rules as the ones listed before. Firstly, they differ by the fact
+that they take neither a `name` nor a `label` parameter at creation.
+Moreover, each of them has other special properties.
+
+The FloatingIP resource has a traditional resource ID, but instead of a
+traditional name, its `name` property maps to its Public IP.
+Moreover, the corresponding `find` method for Floating IPs can thus help
+find a resource by `Public IP Address`.
+
+In terms of the gateway in OpenStack, it maps to the network named 'public.'
+Thus, the internet gateway create method does not take a name parameter, and
+the `name` property will be 'public'.
+
+Finally, Firewall Rules in OpenStack differ from traditional unlabeled resources
+by the fact that they do not take a `name` parameter at creation, and the
+`name` property is automatically generated from the rule's properties, as
+shown above. These rules can be found within each Firewall (i.e. Security
+Group) in the web portal, and will not have any name in the OpenStack dashboard.

+ 2 - 0
docs/topics/overview.rst

@@ -6,6 +6,7 @@ Introductions to all the key parts of CloudBridge you'll need to know:
    :maxdepth: 1
 
     How to install CloudBridge <install.rst>
+    Procuring access credentials <procuring_credentials.rst>
     Connection and authentication setup <setup.rst>
     Launching instances <launch.rst>
     Networking <networking.rst>
@@ -13,4 +14,5 @@ Introductions to all the key parts of CloudBridge you'll need to know:
     Paging and iteration <paging_and_iteration.rst>
     Using block storage <block_storage.rst>
     Using object storage <object_storage.rst>
+    Resource types and mapping <resource_types_and_mapping.rst>
     Troubleshooting <troubleshooting.rst>

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