almahmoud 7 лет назад
Родитель
Сommit
0c7977779b
74 измененных файлов с 4963 добавлено и 510 удалено
  1. 1 0
      .gitignore
  2. 26 24
      .travis.yml
  3. 20 28
      README.rst
  4. 28 0
      az-new.pem
  5. 24 3
      cloudbridge/cloud/base/resources.py
  6. 15 3
      cloudbridge/cloud/base/services.py
  7. 2 1
      cloudbridge/cloud/factory.py
  8. 10 8
      cloudbridge/cloud/interfaces/resources.py
  9. 2 2
      cloudbridge/cloud/interfaces/services.py
  10. 24 2
      cloudbridge/cloud/providers/aws/provider.py
  11. 35 17
      cloudbridge/cloud/providers/aws/services.py
  12. 3 2
      cloudbridge/cloud/providers/azure/resources.py
  13. 0 14
      cloudbridge/cloud/providers/azure/services.py
  14. 35 0
      cloudbridge/cloud/providers/gce/README.rst
  15. 5 0
      cloudbridge/cloud/providers/gce/__init__.py
  16. 145 0
      cloudbridge/cloud/providers/gce/helpers.py
  17. 367 0
      cloudbridge/cloud/providers/gce/provider.py
  18. 2438 0
      cloudbridge/cloud/providers/gce/resources.py
  19. 1141 0
      cloudbridge/cloud/providers/gce/services.py
  20. 11 3
      cloudbridge/cloud/providers/openstack/resources.py
  21. 10 18
      cloudbridge/cloud/providers/openstack/services.py
  22. BIN
      credentials.tar.gz.enc
  23. 37 25
      docs/getting_started.rst
  24. 3 7
      docs/topics/aws_mapping.rst
  25. 3 9
      docs/topics/azure_mapping.rst
  26. BIN
      docs/topics/captures/aws-ami-dash.png
  27. BIN
      docs/topics/captures/aws-bucket.png
  28. BIN
      docs/topics/captures/aws-instance-dash.png
  29. BIN
      docs/topics/captures/aws-services-dash.png
  30. BIN
      docs/topics/captures/az-app-1.png
  31. BIN
      docs/topics/captures/az-app-2.png
  32. BIN
      docs/topics/captures/az-app-3.png
  33. BIN
      docs/topics/captures/az-app-4.png
  34. BIN
      docs/topics/captures/az-app-5.png
  35. BIN
      docs/topics/captures/az-app-6.png
  36. BIN
      docs/topics/captures/az-app-7.png
  37. BIN
      docs/topics/captures/az-dir-1.png
  38. BIN
      docs/topics/captures/az-dir-2.png
  39. BIN
      docs/topics/captures/az-label-dash.png
  40. BIN
      docs/topics/captures/az-net-id.png
  41. BIN
      docs/topics/captures/az-net-label.png
  42. BIN
      docs/topics/captures/az-role-1.png
  43. BIN
      docs/topics/captures/az-role-2.png
  44. BIN
      docs/topics/captures/az-role-3.png
  45. BIN
      docs/topics/captures/az-storacc.png
  46. BIN
      docs/topics/captures/az-sub-1.png
  47. BIN
      docs/topics/captures/az-sub-2.png
  48. BIN
      docs/topics/captures/az-subnet-label.png
  49. BIN
      docs/topics/captures/az-subnet-name.png
  50. BIN
      docs/topics/captures/os-instance-dash.png
  51. BIN
      docs/topics/captures/os-kp-dash.png
  52. 6 6
      docs/topics/launch.rst
  53. 26 25
      docs/topics/networking.rst
  54. 3 5
      docs/topics/os_mapping.rst
  55. 164 0
      docs/topics/procuring_credentials.rst
  56. 4 3
      docs/topics/release_process.rst
  57. 4 4
      docs/topics/resource_types_and_mapping.rst
  58. 165 83
      docs/topics/setup.rst
  59. 4 4
      setup.py
  60. 42 11
      test/helpers/__init__.py
  61. 1 1
      test/helpers/standard_interface_tests.py
  62. 5 19
      test/test_block_store_service.py
  63. 17 35
      test/test_cloud_factory.py
  64. 1 3
      test/test_cloud_helpers.py
  65. 29 20
      test/test_compute_service.py
  66. 2 6
      test/test_image_service.py
  67. 7 13
      test/test_interface.py
  68. 64 42
      test/test_network_service.py
  69. 14 15
      test/test_object_life_cycle.py
  70. 7 15
      test/test_object_store_service.py
  71. 0 13
      test/test_region_service.py
  72. 5 15
      test/test_security_service.py
  73. 3 5
      test/test_vm_types_service.py
  74. 5 1
      tox.ini

+ 1 - 0
.gitignore

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

+ 26 - 24
.travis.yml

@@ -2,38 +2,38 @@ 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-gce
+  - 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-gce
+  - 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)
@@ -67,7 +67,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 +88,5 @@ after_script:
            coveralls & codecov & wait
            ;;
       esac
+notifications:
+    webhooks: https://coveralls.io/webhook

+ 20 - 28
README.rst

@@ -20,47 +20,39 @@ to write conditional code for each cloud.
 
 .. |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
+.. |aws-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/5
               :target: https://travis-ci.org/CloudVE/cloudbridge
-.. |aws-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/7
-              :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
+.. |azure-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/6
                 :target: https://travis-ci.org/CloudVE/cloudbridge/branches
 
-.. |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
+.. |gce-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/3
               :target: https://travis-ci.org/CloudVE/cloudbridge/branches
-.. |gce-pypy| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/gce/9
+.. |gce-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/7
               :target: https://travis-ci.org/CloudVE/cloudbridge/branches
 
+.. |os-py27| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/4
+             :target: https://travis-ci.org/CloudVE/cloudbridge
+.. |os-py36| image:: https://travis-matrix-badges.herokuapp.com/repos/CloudVE/cloudbridge/branches/master/8
+             :target: https://travis-ci.org/CloudVE/cloudbridge
+
 
 Build Status Tests
 ~~~~~~~~~~~~~~~~~~
 
-+--------------------------+--------------+--------------+--------------+
-| **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** | py27         | py36         |
++--------------------------+--------------+--------------+
+| **AWS**                  | |aws-py27|   | |aws-py36|   |
++--------------------------+--------------+--------------+
+| **OpenStack**            | |os-py27|    | |os-py36|    |
++--------------------------+--------------+--------------+
+| **Azure**                | |azure-py27| | |azure-py36| |
++--------------------------+--------------+--------------+
+| **GCE (alpha)**          | |gce-py27|   | |gce-py36|   |
++--------------------------+--------------+--------------+
 
 Installation
 ~~~~~~~~~~~~

+ 28 - 0
az-new.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrCv+bQZ03ve0i
+PBzovkiyhWl04c+ORTnh+2INreavDkNZTcq3jmDYSWosQCFXspAEIFb6bA/XONtm
+Wbdjd/bl1F7izBwEbHpyuUqopecjwf1em0h/amL8LUHuQBue4e9rwPJeRZAnjbma
+ow4K6Jr5jRriRy1EBYsKD+dV7p4oYYHbzO318ZY8WQbtoV2zIonHtw0t3Qlaeo4f
+UGDeTWIqyCS0wgvnIpWUUUNxdGSt5IFoVATvbK216VYU1ZLsGUK3UNIBdIaye++m
+l6DehD1vqn7yENS+3LyRWToHiFykpvJQRsHeCzWgoUehuw7VvZx2hEY+Pckgm/OH
+5TziMoH/AgMBAAECggEAYyFxuSrTEr5p1GCgpKe7/HNkBlDleWBXxshqMOFsBGMw
+125FohLLwC70cQcODAS6gRm5L1Q37hXO2sf+iJC7sMJf5/RB/LJeWWVYhtv4VXoV
+/aHj56ziB6O3RX00ROZPW9Us40RP4wy52IzOE7ZyM1X7kc+MxZKs3b0P53bvhRjj
+bn7q5+skI/hsobXa6PGsGnRFjVNlh7GkCuI4K2IRmRInntWN6mwedGoODiYDCSbj
+ZRdBK0wM1ysCDt96Vo3Y/zDXw50N/wVvgR6p4uZrP25wdobdh9E7ZHbPMjixd2SK
+YtTagzFYd+/y2XLnl15xbbTSwtXO8syqeI1OXshhSQKBgQDZVQoGiJJ1+UvooKJ1
+zcKrI+tlf71nkW5ZcaMFIL3wOkfObx7LqvKg4qFwMI9DbZz14Olc4E7vpZrOK4s1
+BB1heou1rKOQzGwmDkzF/Rmv4kSIsHCgWAltyHyp4pPIgrQorpu82a8TIQGsNkpv
+wceve/yhUtXtpfavwsynV5IXjQKBgQDJeZiWprGZ7B3jyokoBu9f2UmJB67hwmvW
+4g6+AOdD+rvtGLxPt3qzoXJy61oxd1FSJV4XdVu8FmMbLY/HeOxcaHO6KYcBuEnT
+3Nlehr/mFNTCWKBwsRtjnOH1DaSZTujxMt6y0UJe3ZMDAjSYkL+2Lr3pD9L0g3h7
+uQULuyYGuwKBgQCjDmxxLnJEVQAgzrRVEkVlxZNNGaPvWmuia+HVbRVUcUWCmlhO
+kjlrf92DoMs72beTumUbq6dNocAdwwm0gxvQEPNVIOuvLdBOVRhAy35EOc0ViNtX
+UfXxCiSOCVK7c2TbyPVLoT77aR4qKKl8Y7T/8lQ+bTnYDVah09NcEhWn2QKBgEgR
+of6JxE/uAUchhOx3hpr/q+lDgVAqbs7f8CwRQKi1gRGv3nSZil/FKZ5oB8xYBRPg
+YAb+KQFf9LWdF6hf3hS0HGmEP1HjLS2G+45vnydbXjvi3glRoR2B2eYBqlZYvsTA
+0J6gciBjHBpgaJYGeT1e/PnoeiqS+L8xENgAavrPAoGAHENhCbJ5cu8vvPxPSBMl
+17QJVnJKkR3YwkaBN/G6X2HtdbeREBhkjiHK9ZLRd+G02t09JVyRJPfiClUjbZZW
+GTv3H8WG/1t0SRXs0rLoJlz7lHVJedutgtg8lophuhTu3zjIkfJmN43Pc7JofVnc
+VYIJnrbaMG1TRSqhGG9nv8g=
+-----END PRIVATE KEY-----

+ 24 - 3
cloudbridge/cloud/base/resources.py

@@ -265,12 +265,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:
@@ -785,10 +789,25 @@ 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],
@@ -796,7 +815,7 @@ class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
             timeout=timeout,
             interval=interval)
 
-    def create_subnet(self, label, cidr_block, zone=None):
+    def create_subnet(self, label, cidr_block, zone):
         return self._provider.networking.subnets.create(
             label=label, network=self, cidr_block=cidr_block, zone=zone)
 
@@ -811,6 +830,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)

+ 15 - 3
cloudbridge/cloud/base/services.py

@@ -107,6 +107,20 @@ class BaseVMFirewallService(
     def __init__(self, provider):
         super(BaseVMFirewallService, self).__init__(provider)
 
+    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 BaseStorageService(StorageService, BaseCloudService):
 
@@ -383,8 +397,6 @@ class BaseSubnetService(
         return ClientPagedResultList(self._provider, list(matches))
 
     def get_or_create_default(self, zone):
-        default_cidr = '10.0.0.0/24'
-
         # Look for a CB-default subnet
         matches = self.find(label=BaseSubnet.CB_DEFAULT_SUBNET_LABEL)
         if matches:
@@ -393,7 +405,7 @@ class BaseSubnetService(
         # 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,
-                             default_cidr, zone)
+                             BaseSubnet.CB_DEFAULT_SUBNET_IPV4RANGE, zone)
         return subnet
 
 

+ 2 - 1
cloudbridge/cloud/factory.py

@@ -14,8 +14,9 @@ log = logging.getLogger(__name__)
 
 class ProviderList(object):
     AWS = 'aws'
-    OPENSTACK = 'openstack'
     AZURE = 'azure'
+    GCE = 'gce'
+    OPENSTACK = 'openstack'
 
 
 class CloudProviderFactory(object):

+ 10 - 8
cloudbridge/cloud/interfaces/resources.py

@@ -460,7 +460,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.
         """
@@ -950,7 +950,7 @@ class Network(ObjectLifeCycleMixin, LabeledCloudResource):
         pass
 
     @abstractmethod
-    def create_subnet(self, label, cidr_block, zone=None):
+    def create_subnet(self, label, cidr_block, zone):
         """
         Create a new network subnet and associate it with this Network.
 
@@ -1101,7 +1101,7 @@ class FloatingIPContainer(PageableObjectMixin):
         """
         Searches for a FloatingIP by a given list of attributes.
 
-        Supported attributes: label, public_ip
+        Supported attributes: name, public_ip
 
         Example:
 
@@ -2063,10 +2063,12 @@ class VMFirewallRuleContainer(PageableObjectMixin):
 
         .. 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='10.0.0.0/16')
+                            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)
 
@@ -2334,11 +2336,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.

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

@@ -643,7 +643,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
@@ -1298,7 +1298,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
 

+ 24 - 2
cloudbridge/cloud/providers/aws/provider.py

@@ -5,11 +5,12 @@ 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
+
+    import responses
 except ImportError:
-    log.debug('[aws provider] moto library not available!')
+    log.debug("Development library moto is not installed.")
 
 from cloudbridge.cloud.base import BaseCloudProvider
 from cloudbridge.cloud.base.helpers import get_env
@@ -155,6 +156,27 @@ class MockAWSCloudProvider(AWSCloudProvider, TestMockHelperMixin):
     "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,

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

@@ -1,4 +1,5 @@
 """Services implemented by the AWS provider."""
+import ipaddress
 import logging
 import string
 
@@ -445,33 +446,31 @@ class AWSImageService(BaseImageService):
 
     def find(self, **kwargs):
         # Filter by name or label
-        label = kwargs.get('label', None)
+        label = kwargs.pop('label', None)
         # Popped here, not used in the generic find
         owner = kwargs.pop('owners', None)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs, 'label'))
+
         extra_args = {}
         if owner:
             extra_args.update(Owners=owner)
 
-        obj_list = []
-
         # The original list is made by combining both searches by "tag:Name"
         # and "AMI name" to allow for searches of public images
         if label:
             log.debug("Searching for AWS Image Service %s", label)
+            obj_list = []
             obj_list.extend(self.svc.find(filter_name='name',
                                           filter_value=label, **extra_args))
             obj_list.extend(self.svc.find(filter_name='tag:Name',
                                           filter_value=label, **extra_args))
-
-        if not label:
-            obj_list = self
-
-        # Add name filter for the generic find method, to allow searching
-        # through AMI names for a match (public images will likely have an
-        # AMI name and no tag:Name)
-        kwargs.update({'name': label})
-        filters = ['label', 'name']
-        return cb_helpers.generic_find(filters, kwargs, obj_list)
+            return obj_list
+        else:
+            return []
 
     def list(self, filter_by_owner=True, limit=None, marker=None):
         return self.svc.list(Owners=['self'] if filter_by_owner else
@@ -778,7 +777,7 @@ class AWSNetworkService(BaseNetworkService):
                      AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
             return self.provider.networking.networks.create(
                 label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL,
-                cidr_block='10.0.0.0/16')
+                cidr_block=AWSNetwork.CB_DEFAULT_IPV4RANGE)
 
 
 class AWSSubnetService(BaseSubnetService):
@@ -898,12 +897,31 @@ class AWSSubnetService(BaseSubnetService):
         # Create a subnet in each of the region's zones
         region = self.provider.compute.regions.get(self.provider.region_name)
         default_sn = None
+
+        # Determine how many subnets we'll need for the default network and the
+        # number of available zones. We need to derive a non-overlapping
+        # network size for each subnet within the parent net so figure those
+        # subnets here. `<net>.subnets` method will do this but we need to give
+        # it a prefix. Determining that prefix depends on the size of the
+        # network and should be incorporate the number of zones. So iterate
+        # over potential number of subnets until enough can be created to
+        # accommodate the number of available zones. That is where the fixed
+        # number comes from in the for loop as that many iterations will yield
+        # more potential subnets than any region has zones.
+        ip_net = ipaddress.ip_network(AWSNetwork.CB_DEFAULT_IPV4RANGE)
+        for x in range(5):
+            if len(region.zones) <= len(list(ip_net.subnets(
+                    prefixlen_diff=x))):
+                prefixlen_diff = x
+                break
+        subnets = list(ip_net.subnets(prefixlen_diff=prefixlen_diff))
+
         for i, z in reversed(list(enumerate(region.zones))):
             sn_label = "{0}-{1}".format(AWSSubnet.CB_DEFAULT_SUBNET_LABEL,
                                         z.id[-1])
-            log.info("Creating default CloudBridge subnet %s", sn_label)
-            sn = self.create(
-                sn_label, default_net, '10.0.{0}.0/24'.format(i), z)
+            log.info("Creating a default CloudBridge subnet %s: %s" %
+                     (sn_label, str(subnets[i])))
+            sn = self.create(sn_label, default_net, str(subnets[i]), z)
             # Create a route table entry between the SN and the inet gateway
             # See note above about why this is commented
             # default_router.attach_subnet(sn)

+ 3 - 2
cloudbridge/cloud/providers/azure/resources.py

@@ -903,7 +903,7 @@ class AzureNetwork(BaseNetwork):
         """
         return self._provider.networking.subnets.list(network=self.id)
 
-    def create_subnet(self, label, cidr_block, zone=None):
+    def create_subnet(self, label, cidr_block, zone):
         """
         Create the subnet with cidr_block
         :param cidr_block:
@@ -912,7 +912,8 @@ class AzureNetwork(BaseNetwork):
         :return:
         """
         return self._provider.networking.subnets. \
-            create(label=label, network=self.id, cidr_block=cidr_block)
+            create(label=label, network=self.id, cidr_block=cidr_block,
+                   zone=zone)
 
     @property
     def gateways(self):

+ 0 - 14
cloudbridge/cloud/providers/azure/services.py

@@ -109,20 +109,6 @@ class AzureVMFirewallService(BaseVMFirewallService):
         cb_fw = AzureVMFirewall(self.provider, fw)
         return cb_fw
 
-    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 [])
-
     def delete(self, group_id):
         self.provider.azure_client.delete_vm_firewall(group_id)
 

+ 35 - 0
cloudbridge/cloud/providers/gce/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`_
+(GCE).
+
+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.
+
+GCE does this a little bit differently. GCE 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 GCE 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 GCE
+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/gce/__init__.py

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

+ 145 - 0
cloudbridge/cloud/providers/gce/helpers.py

@@ -0,0 +1,145 @@
+import re
+
+from googleapiclient.errors import HttpError
+
+import tenacity
+
+from cloudbridge.cloud.interfaces.exceptions import ProviderInternalException
+
+
+def gce_projects(provider):
+    return provider.gce_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 = gce_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 gce_metadata_save_op(provider, callback):
+    """
+    Carries out a metadata save operation. In GCE, 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 = gce_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)
+
+    gce_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
+
+    gce_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
+
+    gce_metadata_save_op(provider, _remove_metadata_by_key)
+    return True

+ 367 - 0
cloudbridge/cloud/providers/gce/provider.py

@@ -0,0 +1,367 @@
+"""
+Provider implementation based on google-api-python-client library
+for GCE.
+"""
+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
+
+import cloudbridge as cb
+from cloudbridge.cloud.base import BaseCloudProvider
+from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
+
+from .services import GCEComputeService
+from .services import GCENetworkingService
+from .services import GCESecurityService
+from .services import GCPStorageService
+
+
+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]
+        #         },
+        #         ...
+        #       }
+        #     },
+        #     ...
+        #   }
+        #   ...
+        # }
+        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:
+            cb.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 GCECloudProvider(BaseCloudProvider):
+
+    PROVIDER_ID = 'gce'
+
+    def __init__(self, config):
+        super(GCECloudProvider, 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(
+                'gce_service_creds_file',
+                os.getenv('GCE_SERVICE_CREDS_FILE'))
+        self.credentials_dict = self._get_config_value(
+                'gce_service_creds_dict',
+                json.loads(os.getenv('GCE_SERVICE_CREDS_DICT', '{}')))
+        self.vm_default_user_name = self._get_config_value(
+            'gce_vm_default_username',
+            os.getenv('GCE_VM_DEFAULT_USERNAME', "cbuser"))
+
+        # If 'gce_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(
+            'gce_default_zone',
+            os.environ.get('GCE_DEFAULT_ZONE') or 'us-central1-a')
+        self.region_name = self._get_config_value(
+            'gce_region_name',
+            os.environ.get('GCE_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('GCE_PROJECT_NAME')
+
+        # service connections, lazily initialized
+        self._gce_compute = None
+        self._gcs_storage = None
+        self._credentials_cache = None
+        self._compute_resources_cache = None
+        self._storage_resources_cache = None
+
+        # Initialize provider services
+        self._compute = GCEComputeService(self)
+        self._security = GCESecurityService(self)
+        self._networking = GCENetworkingService(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 gce_compute(self):
+        if not self._gce_compute:
+            self._gce_compute = self._connect_gce_compute()
+        return self._gce_compute
+
+    @property
+    def gcs_storage(self):
+        if not self._gcs_storage:
+            self._gcs_storage = self._connect_gcs_storage()
+        return self._gcs_storage
+
+    @property
+    def _compute_resources(self):
+        if not self._compute_resources_cache:
+            self._compute_resources_cache = GCPResources(
+                    self.gce_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.gcs_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_gcs_storage(self):
+        return discovery.build('storage', 'v1', credentials=self._credentials,
+                               cache_discovery=False)
+
+    def _connect_gce_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.gce_compute.globalOperations()
+        elif zone:
+            operations = self.gce_compute.zoneOperations()
+            args['zone'] = zone
+        else:
+            operations = self.gce_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.gce_compute
+            return True
+        except Exception as e:
+            raise ProviderConnectionException(
+                'Authentication with Google cloud provider failed: %s', e)

+ 2438 - 0
cloudbridge/cloud/providers/gce/resources.py

@@ -0,0 +1,2438 @@
+"""
+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
+
+import cloudbridge as cb
+import cloudbridge.cloud.base.helpers as cb_helpers
+from cloudbridge.cloud.base.resources import BaseAttachmentInfo
+from cloudbridge.cloud.base.resources import BaseBucket
+from cloudbridge.cloud.base.resources import BaseBucketContainer
+from cloudbridge.cloud.base.resources import BaseBucketObject
+from cloudbridge.cloud.base.resources import BaseFloatingIP
+from cloudbridge.cloud.base.resources import BaseFloatingIPContainer
+from cloudbridge.cloud.base.resources import BaseGatewayContainer
+from cloudbridge.cloud.base.resources import BaseInstance
+from cloudbridge.cloud.base.resources import BaseInternetGateway
+from cloudbridge.cloud.base.resources import BaseKeyPair
+from cloudbridge.cloud.base.resources import BaseLaunchConfig
+from cloudbridge.cloud.base.resources import BaseMachineImage
+from cloudbridge.cloud.base.resources import BaseNetwork
+from cloudbridge.cloud.base.resources import BasePlacementZone
+from cloudbridge.cloud.base.resources import BaseRegion
+from cloudbridge.cloud.base.resources import BaseRouter
+from cloudbridge.cloud.base.resources import BaseSnapshot
+from cloudbridge.cloud.base.resources import BaseSubnet
+from cloudbridge.cloud.base.resources import BaseVMFirewall
+from cloudbridge.cloud.base.resources import BaseVMFirewallRule
+from cloudbridge.cloud.base.resources import BaseVMFirewallRuleContainer
+from cloudbridge.cloud.base.resources import BaseVMType
+from cloudbridge.cloud.base.resources import BaseVolume
+from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.resources import ServerPagedResultList
+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 cloudbridge.cloud.providers.gce import helpers
+
+# 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 GCEKeyPair(BaseKeyPair):
+
+    KP_TAG_PREFIX = "cb_key_pair_"
+    KP_TAG_REGEX = re.compile("^" + KP_TAG_PREFIX + ".*")
+    GCEKeyInfo = namedtuple('GCEKeyInfo', ['name', 'public_key'])
+
+    def __init__(self, provider, kp_info, private_key=None):
+        super(GCEKeyPair, 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 GCEVMType(BaseVMType):
+    def __init__(self, provider, instance_dict):
+        super(GCEVMType, 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 GCEPlacementZone(BasePlacementZone):
+
+    def __init__(self, provider, zone):
+        super(GCEPlacementZone, 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 GCERegion(BaseRegion):
+
+    def __init__(self, provider, gce_region):
+        super(GCERegion, self).__init__(provider)
+        self._gce_region = gce_region
+
+    @property
+    def id(self):
+        return self._gce_region.get('selfLink')
+
+    @property
+    def name(self):
+        return self._gce_region.get('name')
+
+    @property
+    def zones(self):
+        """
+        Accesss information about placement zones within this region.
+        """
+        zones_response = (self._provider
+                              .gce_compute
+                              .zones()
+                              .list(project=self._provider.project_name)
+                              .execute())
+        zones = [zone for zone in zones_response['items']
+                 if zone['region'] == self._gce_region['selfLink']]
+        return [GCEPlacementZone(self._provider, zone) for zone in zones]
+
+
+class GCEFirewallsDelegate(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 GCENetwork.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 = GCEFirewallsDelegate.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': GCEFirewallsDelegate._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:
+                cb.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
+                            .gce_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
+        response = (self._provider
+                        .gce_compute
+                        .firewalls()
+                        .delete(project=project_name,
+                                firewall=firewall['name'])
+                        .execute())
+        self._provider.wait_for_operation(response)
+        # TODO: process the response and handle errors.
+        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.gce_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 GCEVMFirewall(BaseVMFirewall):
+
+    def __init__(self, delegate, tag, network=None, description=None):
+        super(GCEVMFirewall, 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 = GCEVMFirewallRuleContainer(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 GCEFirewallsDelegate.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)
+        # TODO: Add removing metadata to delete function
+
+    @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
+        GCE firewall, in the VM firewall.
+
+        If the GCE 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
+
+    @property
+    def network_id(self):
+        return self._network.id
+
+    @property
+    def rules(self):
+        return self._rule_container
+
+    def delete(self):
+        for rule in self._rule_container:
+            rule.delete()
+        self._rule_container.dummy_rule.force_delete()
+        # Remove label
+        tag_name = "_".join(["firewall", self.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(self.name))
+
+    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
+            self._description = fw._description
+            self._network = fw._network
+            self._rule_container = fw._rule_container
+
+    @property
+    def network(self):
+        return self._network
+
+    @property
+    def delegate(self):
+        return self._delegate
+
+
+class GCEVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
+
+    def __init__(self, firewall):
+        super(GCEVMFirewallRuleContainer, self).__init__(
+                firewall.delegate.provider, firewall)
+        self._dummy_rule = None
+
+    def list(self, limit=None, marker=None):
+        rules = []
+        for firewall in self.firewall.delegate.iter_firewalls(
+                self.firewall.name, self.firewall.network.name):
+            rule = GCEVMFirewallRule(self.firewall, firewall['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, direction, protocol, priority,
+                             from_port=None, to_port=None, cidr=None,
+                             src_dest_fw=None):
+        port = GCEVMFirewallRuleContainer.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 self.firewall.delegate.add_firewall(
+                self.firewall.name, direction, protocol, priority, port, cidr,
+                src_dest_tag, self.firewall.description,
+                self.firewall.network.name):
+            return None
+        rules = self.find(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]
+
+    def create(self, direction, protocol, from_port=None, to_port=None,
+               cidr=None, src_dest_fw=None):
+        return self.create_with_priority(direction, protocol, 1000, from_port,
+                                         to_port, cidr, src_dest_fw)
+
+
+class GCEVMFirewallRule(BaseVMFirewallRule):
+
+    def __init__(self, parent_fw, rule):
+        super(GCEVMFirewallRule, 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 GCEFirewallsDelegate.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 GCEVMFirewall(
+                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
+
+    def delete(self):
+        if (self.is_dummy_rule()):
+            return
+        self.force_delete()
+
+    def force_delete(self):
+        self.firewall.delegate.delete_firewall_id(self._rule)
+
+
+class GCEMachineImage(BaseMachineImage):
+
+    IMAGE_STATE_MAP = {
+        'PENDING': MachineImageState.PENDING,
+        'READY': MachineImageState.AVAILABLE,
+        'FAILED': MachineImageState.ERROR
+    }
+
+    def __init__(self, provider, image):
+        super(GCEMachineImage, self).__init__(provider)
+        if isinstance(image, GCEMachineImage):
+            # pylint:disable=protected-access
+            self._gce_image = image._gce_image
+        else:
+            self._gce_image = image
+
+    @property
+    def resource_url(self):
+        return self._gce_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._gce_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._gce_image['name']
+
+    @property
+    def label(self):
+        labels = self._gce_image.get('labels')
+        return labels.get('cblabel', '') if labels else ''
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        # Refresh to update fingerprint and current labels
+        self.refresh()
+        labels = self._gce_image.get('labels', {})
+        labels['cblabel'] = value
+        request_body = {
+            'labels': labels,
+            'labelFingerprint': self._gce_image.get('labelFingerprint'),
+        }
+        try:
+            response = (self._provider
+                        .gce_compute
+                        .images()
+                        .setLabels(project=self._provider.project_name,
+                                   resource=self.name,
+                                   body=request_body)
+                        .execute())
+            self._provider.wait_for_operation(response)
+        except Exception as e:
+            cb.log.warning('Exception while setting image label: %s. '
+                           'Check for invalid characters in label. '
+                           'Should conform to RFC1035.', e)
+            raise e
+        self.refresh()
+
+    @property
+    def description(self):
+        """
+        Get the image description.
+        :rtype: ``str``
+        :return: Description for this image as returned by the cloud middleware
+        """
+        return self._gce_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._gce_image.get('diskSizeGb'))))
+
+    def delete(self):
+        """
+        Delete this image
+        """
+        (self._provider
+             .gce_compute
+             .images()
+             .delete(project=self._provider.project_name,
+                     image=self.name)
+             .execute())
+
+    @property
+    def state(self):
+        return GCEMachineImage.IMAGE_STATE_MAP.get(
+            self._gce_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._gce_image = image._gce_image
+        else:
+            # image no longer exists
+            self._gce_image['status'] = MachineImageState.UNKNOWN
+
+
+class GCEInstance(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, gce_instance):
+        super(GCEInstance, self).__init__(provider)
+        self._gce_instance = gce_instance
+        self._inet_gateway = None
+
+    @property
+    def resource_url(self):
+        return self._gce_instance.get('selfLink')
+
+    @property
+    def id(self):
+        """
+        Get the instance identifier.
+
+        A GCE instance is uniquely identified by its selfLink, which is used
+        as its id.
+        """
+        return self._gce_instance.get('selfLink')
+
+    @property
+    def name(self):
+        """
+        Get the instance name.
+        """
+        return self._gce_instance['name']
+
+    @property
+    def label(self):
+        labels = self._gce_instance.get('labels')
+        return labels.get('cblabel', '') if labels else ''
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        # Refresh to update fingerprint and current labels
+        self.refresh()
+        labels = self._gce_instance.get('labels', {})
+        labels['cblabel'] = value
+        request_body = {
+            'labels': labels,
+            'labelFingerprint': self._gce_instance.get('labelFingerprint'),
+        }
+        try:
+            response = (self._provider
+                        .gce_compute
+                        .instances()
+                        .setLabels(project=self._provider.project_name,
+                                   zone=self.zone_name,
+                                   instance=self.name,
+                                   body=request_body)
+                        .execute())
+            self._provider.wait_for_operation(response, zone=self.zone_name)
+        except Exception as e:
+            cb.log.warning('Exception while setting instance label: %s. '
+                           'Check for invalid characters in label. '
+                           'Should conform to RFC1035.', e)
+            raise e
+        self.refresh()
+
+    @property
+    def public_ips(self):
+        """
+        Get all the public IP addresses for this instance.
+        """
+        ips = []
+        network_interfaces = self._gce_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._gce_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._gce_instance.get('machineType')
+
+    @property
+    def vm_type(self):
+        """
+        Get the instance type.
+        """
+        machine_type_uri = self._gce_instance.get('machineType')
+        if machine_type_uri is None:
+            return None
+        parsed_uri = self._provider.parse_url(machine_type_uri)
+        return GCEVMType(self._provider, parsed_uri.get_resource())
+
+    @property
+    def subnet_id(self):
+        """
+        Get the zone for this instance.
+        """
+        return (self._gce_instance.get('networkInterfaces', [{}])[0]
+                .get('subnetwork'))
+
+    def reboot(self):
+        """
+        Reboot this instance.
+        """
+        if self.state == InstanceState.STOPPED:
+            (self._provider
+             .gce_compute
+             .instances()
+             .start(project=self._provider.project_name,
+                    zone=self.zone_name,
+                    instance=self.name)
+             .execute())
+        else:
+            (self._provider
+             .gce_compute
+             .instances()
+             .reset(project=self._provider.project_name,
+                    zone=self.zone_name,
+                    instance=self.name)
+             .execute())
+
+    def delete(self):
+        """
+        Permanently terminate this instance.
+        """
+        name = self.name
+        (self._provider
+         .gce_compute
+         .instances()
+         .delete(project=self._provider.project_name,
+                 zone=self.zone_name,
+                 instance=name)
+         .execute())
+
+    def stop(self):
+        """
+        Stop this instance.
+        """
+        (self._provider
+         .gce_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._gce_instance:
+            return None
+        for disk in self._gce_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._gce_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._gce_instance.get('networkInterfaces')[0].get(
+            'network')
+        url = self._provider.parse_url(network_url)
+        network_name = url.parameters['network']
+        if 'items' not in self._gce_instance['tags']:
+            return []
+        tags = self._gce_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)
+        meta = ins._gce_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
+
+    @key_pair_id.setter
+    # pylint:disable=arguments-differ
+    def key_pair_id(self, value):
+        key_pair = value
+        if not isinstance(value, GCEKeyPair):
+            key_pair = self._provider.security.key_pairs.get(value)
+        if key_pair:
+            kp = key_pair._key_pair
+            kp_items = [{
+                "key": "ssh-keys",
+                # Format is not removed from public key portion
+                "value": "{}:{} {}".format(self._provider.vm_default_user_name,
+                                           kp.public_key,
+                                           kp.name)
+            }]
+            config = {
+                "items": kp_items,
+                "fingerprint": self._gce_instance['metadata']['fingerprint']
+            }
+            try:
+                response = (self._provider
+                            .gce_compute
+                            .instances()
+                            .setMetadata(project=self._provider.project_name,
+                                         zone=self.zone_name,
+                                         instance=self.name,
+                                         body=config)
+                            .execute())
+                self._provider.wait_for_operation(response,
+                                                  zone=self.zone_name)
+            except Exception as e:
+                cb.log.warning('Exception while setting instance key pair: %s',
+                               e)
+                raise e
+            self.refresh()
+
+    @property
+    def inet_gateway(self):
+        if self._inet_gateway:
+            return self._inet_gateway
+        network_url = self._gce_instance.get('networkInterfaces')[0].get(
+            'network')
+        network = self._provider.networking.networks.get(network_url)
+        self._inet_gateway = network.gateways.get_or_create_inet_gateway()
+        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._gce_instance:
+            cb.log.error('Failed to create image: no disks found.')
+            return
+        for disk in self._gce_instance['disks']:
+            if 'boot' in disk and disk['boot']:
+                image_body = {
+                    'name': name,
+                    'sourceDisk': disk['source'],
+                    'labels': {'cblabel': label.replace(' ', '_').lower()},
+                }
+                operation = (self._provider
+                             .gce_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 GCEMachineImage(self._provider, img) if img else None
+        cb.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.gce_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:
+            cb.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._gce_instance['selfLink']}
+        try:
+            response = (self._provider
+                            .gce_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:
+            cb.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.gce_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
+                                .gce_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:
+            cb.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
+                            .gce_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:
+            cb.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.gce_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:
+                    cb.log.warning(
+                            '"%s" is forwarded to "%s" in zone "%s"',
+                            ip.public_ip, temp_name, temp_zone)
+                    return False
+                response = (self._provider
+                                .gce_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:
+            cb.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, GCEFloatingIP)
+               else self.inet_gateway.floating_ips.get(floating_ip))
+        if fip.in_use:
+            if fip.private_ip not in self.private_ips:
+                cb.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:
+            cb.log.warning('Could not create a targetInstance for "%s"',
+                           self.name)
+            return
+        if not self._forward(fip, target_instance):
+            cb.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, GCEFloatingIP)
+               else self.inet_gateway.floating_ips.get(floating_ip))
+        if not fip.in_use or fip.private_ip not in self.private_ips:
+            cb.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.
+            cb.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):
+            cb.log.warning(
+                'Could not remove floating IP "%s" from instance "%s"',
+                fip.public_ip, self.name)
+
+    @property
+    def state(self):
+        return GCEInstance.INSTANCE_STATE_MAP.get(
+            self._gce_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._gce_instance = inst._gce_instance
+        else:
+            # instance no longer exists
+            self._gce_instance['status'] = InstanceState.UNKNOWN
+
+    def add_vm_firewall(self, sg):
+        tag = sg.name if isinstance(sg, GCEVMFirewall) else sg
+        tags = self._gce_instance.get('tags', {}).get('items', [])
+        tags.append(tag)
+        self._set_tags(tags)
+
+    def remove_vm_firewall(self, sg):
+        tag = sg.name if isinstance(sg, GCEVMFirewall) else sg
+        tags = self._gce_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._gce_instance.get('tags', {}).get('fingerprint', '')
+        response = (self._provider
+                        .gce_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 GCENetwork(BaseNetwork):
+
+    def __init__(self, provider, network):
+        super(GCENetwork, self).__init__(provider)
+        self._network = network
+        self._gateway_container = GCEGatewayContainer(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 GCENetwork.CB_DEFAULT_IPV4RANGE
+
+    @property
+    def subnets(self):
+        return list(self._provider.networking.subnets.iter(network=self))
+
+    def delete(self):
+        self._provider.networking.networks.delete(self)
+
+    def create_subnet(self, label, cidr_block, zone):
+        return self._provider.networking.subnets.create(
+            label, self, cidr_block, zone)
+
+    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 GCEFloatingIPContainer(BaseFloatingIPContainer):
+
+    def __init__(self, provider, gateway):
+        super(GCEFloatingIPContainer, self).__init__(provider, gateway)
+
+    def get(self, floating_ip_id):
+        fip = self._provider.get_resource('addresses', floating_ip_id)
+        return (GCEFloatingIP(self._provider, self.gateway, fip)
+                if fip else None)
+
+    def list(self, limit=None, marker=None):
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self._provider
+                        .gce_compute
+                        .addresses()
+                        .list(project=self._provider.project_name,
+                              region=self._provider.region_name,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        ips = [GCEFloatingIP(self._provider, self.gateway, ip)
+               for ip in response.get('items', [])]
+        if len(ips) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(ips))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=ips)
+
+    def create(self):
+        region_name = self._provider.region_name
+        ip_name = 'ip-{0}'.format(uuid.uuid4())
+        response = (self._provider
+                    .gce_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(ip_name)
+
+
+class GCEFloatingIP(BaseFloatingIP):
+    _DEAD_INSTANCE = 'dead instance'
+
+    def __init__(self, provider, gateway, floating_ip):
+        super(GCEFloatingIP, self).__init__(provider)
+        self._gateway = gateway
+        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 == GCEFloatingIP._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 delete(self):
+        project_name = self._provider.project_name
+        # First, delete the forwarding rule, if there is any.
+        if self._rule:
+            response = (self._provider
+                            .gce_compute
+                            .forwardingRules()
+                            .delete(project=project_name,
+                                    region=self.region_name,
+                                    forwardingRule=self._rule['name'])
+                            .execute())
+            self._provider.wait_for_operation(response,
+                                              region=self.region_name)
+
+        # Release the address.
+        response = (self._provider
+                        .gce_compute
+                        .addresses()
+                        .delete(project=project_name,
+                                region=self.region_name,
+                                address=self._ip['name'])
+                        .execute())
+        self._provider.wait_for_operation(response, region=self.region_name)
+
+    def refresh(self):
+        fip = self._gateway.floating_ips.get(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:
+                cb.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 = GCEFloatingIP._DEAD_INSTANCE
+                else:
+                    cb.log.warning('Address "%s" is forwarded to a %s',
+                                   self._ip.get('address'), target['kind'])
+            else:
+                cb.log.warning('Address "%s" in use by a %s',
+                               self._ip.get('address'), resource['kind'])
+
+
+class GCERouter(BaseRouter):
+
+    def __init__(self, provider, router):
+        super(GCERouter, 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
+        # GCE 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 delete(self):
+        operation = (self._provider
+                     .gce_compute
+                     .routers()
+                     .delete(project=self._provider.project_name,
+                             region=self.region_name,
+                             router=self.name)
+                     .execute())
+        self._provider.wait_for_operation(operation, region=self.region_name)
+        # Remove label
+        tag_name = "_".join(["router", self.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(self.name))
+
+    def attach_subnet(self, subnet):
+        if not isinstance(subnet, GCESubnet):
+            subnet = self._provider.networking.subnets.get(subnet)
+        if subnet.network_id == self.network_id:
+            return
+        cb.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):
+        cb.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 GCEGatewayContainer(BaseGatewayContainer):
+    _DEFAULT_GATEWAY_NAME = 'default-internet-gateway'
+    _GATEWAY_URL_PREFIX = 'global/gateways/'
+
+    def __init__(self, provider, network):
+        super(GCEGatewayContainer, self).__init__(provider, network)
+        self._default_internet_gateway = GCEInternetGateway(
+            provider,
+            {'id': (GCEGatewayContainer._GATEWAY_URL_PREFIX +
+                    GCEGatewayContainer._DEFAULT_GATEWAY_NAME),
+             'name': GCEGatewayContainer._DEFAULT_GATEWAY_NAME})
+
+    def get_or_create_inet_gateway(self, name=None):
+        return self._default_internet_gateway
+
+    def delete(self, gateway):
+        pass
+
+    def list(self, limit=None, marker=None):
+        return ClientPagedResultList(self._provider,
+                                     [self._default_internet_gateway],
+                                     limit=limit, marker=marker)
+
+
+class GCEInternetGateway(BaseInternetGateway):
+
+    def __init__(self, provider, gateway):
+        super(GCEInternetGateway, self).__init__(provider)
+        self._gateway = gateway
+        self._fip_container = GCEFloatingIPContainer(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):
+        """
+        GCE internet gateways are not attached to a network.
+        """
+        return None
+
+    def delete(self):
+        pass
+
+    @property
+    def floating_ips(self):
+        return self._fip_container
+
+
+class GCESubnet(BaseSubnet):
+
+    def __init__(self, provider, subnet):
+        super(GCESubnet, 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
+
+    def delete(self):
+        return self._provider.networking.subnets.delete(self)
+
+    @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 GCEVolume(BaseVolume):
+
+    VOLUME_STATE_MAP = {
+        'CREATING': VolumeState.CONFIGURING,
+        'FAILED': VolumeState.ERROR,
+        'READY': VolumeState.AVAILABLE,
+        'RESTORING': VolumeState.CONFIGURING,
+    }
+
+    def __init__(self, provider, volume):
+        super(GCEVolume, 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):
+        self.assert_valid_resource_label(value)
+        # Refresh to update fingerprint and current labels
+        self.refresh()
+        labels = self._volume.get('labels', {})
+        labels['cblabel'] = value
+        request_body = {
+            'labels': labels,
+            'labelFingerprint': self._volume.get('labelFingerprint'),
+        }
+        try:
+            response = (self._provider
+                        .gce_compute
+                        .disks()
+                        .setLabels(project=self._provider.project_name,
+                                   zone=self.zone_name,
+                                   resource=self.name,
+                                   body=request_body)
+                        .execute())
+            self._provider.wait_for_operation(response, zone=self.zone_name)
+        except Exception as e:
+            cb.log.warning('Exception while setting volume name: %s. '
+                           'Check for invalid characters in name. '
+                           'Should conform to RFC1035.', e)
+            raise e
+        self.refresh()
+
+    @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):
+        # Refresh to update fingerprint and current labels
+        self.refresh()
+        labels = self._volume.get('labels', {})
+        labels['description'] = value.replace(' ', '_').lower()
+        request_body = {
+            'labels': labels,
+            'labelFingerprint': self._volume.get('labelFingerprint'),
+        }
+        try:
+            response = (self._provider
+                        .gce_compute
+                        .disks()
+                        .setLabels(project=self._provider.project_name,
+                                   zone=self.zone_name,
+                                   resource=self.name,
+                                   body=request_body)
+                        .execute())
+            self._provider.wait_for_operation(response,
+                                              zone=self.zone_name)
+        except Exception as e:
+            cb.log.warning('Exception while setting volume description: %s. '
+                           'Check for invalid characters in description. '
+                           'Should confirm to RFC1035.', e)
+            raise e
+        self.refresh()
+
+    @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 GCESnapshot(
+                    self._provider,
+                    self._provider.parse_url(snapshot_uri).get_resource())
+        if 'sourceImage' in self._volume:
+            image_uri = self._volume.get('sourceImage')
+            return GCEMachineImage(
+                    self._provider,
+                    self._provider.parse_url(image_uri).get_resource())
+        return None
+
+    @property
+    def attachments(self):
+        # GCE 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:
+                cb.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, GCEInstance):
+            instance = self._provider.get_resource('instances', instance)
+        (self._provider
+             .gce_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
+             .gce_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)
+
+    def delete(self):
+        """
+        Delete this volume.
+        """
+        (self._provider
+         .gce_compute
+         .disks()
+         .delete(project=self._provider.project_name,
+                 zone=self.zone_name,
+                 disk=self.name)
+         .execute())
+
+    @property
+    def state(self):
+        if len(self._volume.get('users', [])) > 0:
+            return VolumeState.IN_USE
+        return GCEVolume.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 GCESnapshot(BaseSnapshot):
+
+    SNAPSHOT_STATE_MAP = {
+        'PENDING': SnapshotState.PENDING,
+        'READY': SnapshotState.AVAILABLE,
+    }
+
+    def __init__(self, provider, snapshot):
+        super(GCESnapshot, 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):
+        self.assert_valid_resource_label(value)
+        # Refresh to update fingerprint and current labels
+        self.refresh()
+        labels = self._snapshot.get('labels', {})
+        labels['cblabel'] = value
+        request_body = {
+            'labels': labels,
+            'labelFingerprint': self._snapshot.get('labelFingerprint'),
+        }
+        try:
+            response = (self._provider
+                        .gce_compute
+                        .snapshots()
+                        .setLabels(project=self._provider.project_name,
+                                   resource=self.name,
+                                   body=request_body)
+                        .execute())
+            self._provider.wait_for_operation(response)
+        except Exception as e:
+            cb.log.warning('Exception while setting snapshot label: %s. '
+                           'Check for invalid characters in label. '
+                           'Should conform to RFC1035.', e)
+            raise e
+        self.refresh()
+
+    @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):
+        # Refresh to update fingerprint and current labels
+        self.refresh()
+        labels = self._snapshot.get('labels', {})
+        labels['description'] = value.replace(' ', '_').lower()
+        request_body = {
+            'labels': labels,
+            'labelFingerprint': self._snapshot.get('labelFingerprint'),
+        }
+        try:
+            response = (self._provider
+                        .gce_compute
+                        .snapshots()
+                        .setLabels(project=self._provider.project_name,
+                                   resource=self.name,
+                                   body=request_body)
+                        .execute())
+            self._provider.wait_for_operation(response)
+        except Exception as e:
+            cb.log.warning('Exception while setting volume description: %s. '
+                           'Check for invalid characters in description. '
+                           'Should confirm to RFC1035.', e)
+            raise e
+        self.refresh()
+
+    @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 GCESnapshot.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 delete(self):
+        """
+        Delete this snapshot.
+        """
+        (self._provider
+         .gce_compute
+         .snapshots()
+         .delete(project=self._provider.project_name,
+                 snapshot=self.name)
+         .execute())
+
+    def create_volume(self, placement, size=None, volume_type=None, iops=None):
+        """
+        Create a new Volume from this Snapshot.
+
+        Args:
+            placement: GCE 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 GCE.
+        """
+        zone_name = placement
+        if isinstance(placement, GCEPlacementZone):
+            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
+                         .gce_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 GCSObject(BaseBucketObject):
+
+    def __init__(self, provider, bucket, obj):
+        super(GCSObject, 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
+                              .gcs_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')
+        response = self._bucket.create_object_with_media_body(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')
+            response = self._bucket.create_object_with_media_body(self.name,
+                                                                  media_body)
+            if response:
+                self._obj = response
+
+    def delete(self):
+        (self._provider
+             .gcs_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 GCSBucketContainer(BaseBucketContainer):
+
+    def __init__(self, provider, bucket):
+        super(GCSBucketContainer, self).__init__(provider, bucket)
+
+    def get(self, name):
+        """
+        Retrieve a given object from this bucket.
+        """
+        obj = self._provider.get_resource('objects', name,
+                                          bucket=self.bucket.name)
+        return GCSObject(self._provider, self.bucket, obj) if obj else None
+
+    def list(self, limit=None, marker=None, prefix=None):
+        """
+        List all objects within this bucket.
+        """
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self._provider
+                        .gcs_storage
+                        .objects()
+                        .list(bucket=self.bucket.name,
+                              prefix=prefix if prefix else '',
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        objects = []
+        for obj in response.get('items', []):
+            objects.append(GCSObject(self._provider, self.bucket, obj))
+        if len(objects) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(objects))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=objects)
+
+    def find(self, **kwargs):
+        obj_list = self.list()
+        filters = ['name']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches),
+                                     limit=None, marker=None)
+
+    def create(self, name):
+        return self.bucket.create_object(name)
+
+
+class GCSBucket(BaseBucket):
+
+    def __init__(self, provider, bucket):
+        super(GCSBucket, self).__init__(provider)
+        self._bucket = bucket
+        self._object_container = GCSBucketContainer(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
+
+    def delete(self, delete_contents=False):
+        """
+        Delete this bucket.
+        """
+        (self._provider
+             .gcs_storage
+             .buckets()
+             .delete(bucket=self.name)
+             .execute())
+        # GCS has a rate limit of 1 operation per 2 seconds for bucket
+        # creation/deletion: https://cloud.google.com/storage/quotas.  Throttle
+        # here to avoid future failures.
+        time.sleep(2)
+
+    def create_object(self, name):
+        """
+        Create an empty plain text object.
+        """
+        response = self.create_object_with_media_body(
+            name,
+            googleapiclient.http.MediaIoBaseUpload(
+                    io.BytesIO(b''), mimetype='plain/text'))
+        return GCSObject(self._provider, self, response) if response else None
+
+    def create_object_with_media_body(self, name, media_body):
+        response = (self._provider
+                        .gcs_storage
+                        .objects()
+                        .insert(bucket=self.name,
+                                body={'name': name},
+                                media_body=media_body)
+                        .execute())
+        return response
+
+
+class GCELaunchConfig(BaseLaunchConfig):
+
+    def __init__(self, provider):
+        super(GCELaunchConfig, self).__init__(provider)

+ 1141 - 0
cloudbridge/cloud/providers/gce/services.py

@@ -0,0 +1,1141 @@
+import json
+import logging
+import time
+import uuid
+
+import googleapiclient
+
+import cloudbridge as cb
+from cloudbridge.cloud.base import helpers as cb_helpers
+from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.resources import ServerPagedResultList
+from cloudbridge.cloud.base.services import BaseBucketService
+from cloudbridge.cloud.base.services import BaseComputeService
+from cloudbridge.cloud.base.services import BaseImageService
+from cloudbridge.cloud.base.services import BaseInstanceService
+from cloudbridge.cloud.base.services import BaseKeyPairService
+from cloudbridge.cloud.base.services import BaseNetworkService
+from cloudbridge.cloud.base.services import BaseNetworkingService
+from cloudbridge.cloud.base.services import BaseRegionService
+from cloudbridge.cloud.base.services import BaseRouterService
+from cloudbridge.cloud.base.services import BaseSecurityService
+from cloudbridge.cloud.base.services import BaseSnapshotService
+from cloudbridge.cloud.base.services import BaseStorageService
+from cloudbridge.cloud.base.services import BaseSubnetService
+from cloudbridge.cloud.base.services import BaseVMFirewallService
+from cloudbridge.cloud.base.services import BaseVMTypeService
+from cloudbridge.cloud.base.services import BaseVolumeService
+from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
+from cloudbridge.cloud.interfaces.resources import VMFirewall
+from cloudbridge.cloud.providers.gce import helpers
+
+from .resources import GCEFirewallsDelegate
+from .resources import GCEInstance
+from .resources import GCEKeyPair
+from .resources import GCELaunchConfig
+from .resources import GCEMachineImage
+from .resources import GCENetwork
+from .resources import GCEPlacementZone
+from .resources import GCERegion
+from .resources import GCERouter
+from .resources import GCESnapshot
+from .resources import GCESubnet
+from .resources import GCEVMFirewall
+from .resources import GCEVMType
+from .resources import GCEVolume
+from .resources import GCSBucket
+
+log = logging.getLogger(__name__)
+
+
+class GCESecurityService(BaseSecurityService):
+
+    def __init__(self, provider):
+        super(GCESecurityService, self).__init__(provider)
+
+        # Initialize provider services
+        self._key_pairs = GCEKeyPairService(provider)
+        self._vm_firewalls = GCEVMFirewallService(provider)
+
+    @property
+    def key_pairs(self):
+        return self._key_pairs
+
+    @property
+    def vm_firewalls(self):
+        return self._vm_firewalls
+
+
+class GCEKeyPairService(BaseKeyPairService):
+
+    def __init__(self, provider):
+        super(GCEKeyPairService, self).__init__(provider)
+
+    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
+
+    def list(self, limit=None, marker=None):
+        key_pairs = []
+        for item in helpers.find_matching_metadata_items(
+                self.provider, GCEKeyPair.KP_TAG_REGEX):
+            metadata_value = json.loads(item['value'])
+            kp_info = GCEKeyPair.GCEKeyInfo(**metadata_value)
+            key_pairs.append(GCEKeyPair(self.provider, kp_info))
+        return ClientPagedResultList(self.provider, key_pairs,
+                                     limit=limit, marker=marker)
+
+    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 TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs,
+                                                           ", ".join(filters)))
+
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
+
+    def create(self, name, public_key_material=None):
+        GCEKeyPair.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 = GCEKeyPair.GCEKeyInfo(name, public_key_material)
+        metadata_value = json.dumps(kp_info._asdict())
+        try:
+            helpers.add_metadata_item(self.provider,
+                                      GCEKeyPair.KP_TAG_PREFIX + name,
+                                      metadata_value)
+            return GCEKeyPair(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
+
+    def delete(self, key_pair_id):
+        kp = self.get(key_pair_id)
+        if kp:
+            helpers.remove_metadata_item(
+                self.provider, GCEKeyPair.KP_TAG_PREFIX + kp.name)
+
+
+class GCEVMFirewallService(BaseVMFirewallService):
+
+    def __init__(self, provider):
+        super(GCEVMFirewallService, self).__init__(provider)
+        self._delegate = GCEFirewallsDelegate(provider)
+
+    def get(self, group_id):
+        tag, network_name = self._delegate.get_tag_network_from_id(group_id)
+        if tag is None:
+            return None
+        network = self.provider.networking.networks.get(network_name)
+        return GCEVMFirewall(self._delegate, tag, network)
+
+    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 = GCEVMFirewall(self._delegate, tag, network)
+            vm_firewalls.append(vm_firewall)
+        return ClientPagedResultList(self.provider, vm_firewalls,
+                                     limit=limit, marker=marker)
+
+    def create(self, label, description, network=None):
+        GCEVMFirewall.assert_valid_resource_label(label)
+        network = (network if isinstance(network, GCENetwork)
+                   else self.provider.networking.networks.get(network))
+        fw = GCEVMFirewall(self._delegate, label, network, description)
+        # This rule exists implicitly. Add it explicitly so that the firewall
+        # is not empty and the rule is shown by list/get/find methods.
+        fw.rules.create_with_priority(
+                direction=TrafficDirection.OUTBOUND, protocol='tcp',
+                priority=65534, cidr='0.0.0.0/0')
+        fw.label = label
+        return fw
+
+    def delete(self, group_id):
+        return self._delegate.delete_tag_network_with_id(group_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(
+                GCEVMFirewall(self._delegate, tag, network))
+        return vm_firewalls
+
+
+class GCEVMTypeService(BaseVMTypeService):
+
+    def __init__(self, provider):
+        super(GCEVMTypeService, self).__init__(provider)
+
+    @property
+    def instance_data(self):
+        response = (self.provider
+                        .gce_compute
+                        .machineTypes()
+                        .list(project=self.provider.project_name,
+                              zone=self.provider.default_zone)
+                        .execute())
+        return response['items']
+
+    def get(self, vm_type_id):
+        vm_type = self.provider.get_resource('machineTypes', vm_type_id)
+        return GCEVMType(self.provider, vm_type) if vm_type else None
+
+    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 TypeError("The attribute key is not valid.")
+                if inst_type.get(key) != value:
+                    is_match = False
+                    break
+            if is_match:
+                matched_inst_types.append(
+                    GCEVMType(self.provider, inst_type))
+        return matched_inst_types
+
+    def list(self, limit=None, marker=None):
+        inst_types = [GCEVMType(self.provider, inst_type)
+                      for inst_type in self.instance_data]
+        return ClientPagedResultList(self.provider, inst_types,
+                                     limit=limit, marker=marker)
+
+
+class GCERegionService(BaseRegionService):
+
+    def __init__(self, provider):
+        super(GCERegionService, self).__init__(provider)
+
+    def get(self, region_id):
+        region = self.provider.get_resource('regions', region_id,
+                                            region=region_id)
+        return GCERegion(self.provider, region) if region else None
+
+    def list(self, limit=None, marker=None):
+        max_result = limit if limit is not None and limit < 500 else 500
+        regions_response = (self.provider
+                                .gce_compute
+                                .regions()
+                                .list(project=self.provider.project_name,
+                                      maxResults=max_result,
+                                      pageToken=marker)
+                                .execute())
+        regions = [GCERegion(self.provider, region)
+                   for region in regions_response['items']]
+        if len(regions) > max_result:
+            cb.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 GCEImageService(BaseImageService):
+
+    def __init__(self, provider):
+        super(GCEImageService, self).__init__(provider)
+        self._public_images = None
+
+    _PUBLIC_IMAGE_PROJECTS = ['centos-cloud', 'coreos-cloud', 'debian-cloud',
+                              'opensuse-cloud', 'ubuntu-os-cloud']
+
+    def _retrieve_public_images(self):
+        if self._public_images is not None:
+            return
+        self._public_images = []
+        for project in GCEImageService._PUBLIC_IMAGE_PROJECTS:
+            for image in helpers.iter_all(
+                    self.provider.gce_compute.images(), project=project):
+                self._public_images.append(
+                    GCEMachineImage(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 GCEMachineImage(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, label, limit=None, marker=None):
+        """
+        Searches for an image by a given list of attributes
+        """
+        filters = {'label': label}
+        # Retrieve all available images by setting limit to sys.maxsize
+        images = [image for image in self if image.label == filters['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
+                GCEImageService._PUBLIC_IMAGE_PROJECTS):
+            for image in helpers.iter_all(
+                    self.provider.gce_compute.images(),
+                    project=self.provider.project_name):
+                images.append(GCEMachineImage(self.provider, image))
+        images.extend(self._public_images)
+        return ClientPagedResultList(self.provider, images,
+                                     limit=limit, marker=marker)
+
+
+class GCEInstanceService(BaseInstanceService):
+
+    def __init__(self, provider):
+        super(GCEInstanceService, self).__init__(provider)
+
+    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.
+        """
+        GCEInstance.assert_valid_resource_name(label)
+        zone_name = self.provider.default_zone
+        if zone:
+            if not isinstance(zone, GCEPlacementZone):
+                zone = GCEPlacementZone(
+                    self.provider,
+                    self.provider.get_resource('zones', zone))
+            zone_name = zone.name
+        if not isinstance(vm_type, GCEVMType):
+            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, GCELaunchConfig):
+            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, GCEMachineImage):
+                    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())}
+                elif isinstance(disk.source, GCEVolume):
+                    source_field = 'source'
+                    source_value = disk.source.id
+                elif isinstance(disk.source, GCESnapshot):
+                    volume = disk.source.create_volume(zone, size=disk.size)
+                    volume.wait_till_ready()
+                    source_field = 'source'
+                    source_value = volume.id
+                else:
+                    cb.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:
+            cb.log.warning('The launch config contains %d boot disks. Will '
+                           'use the first one', num_roots)
+        if image:
+            if boot_disk:
+                cb.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, GCEMachineImage):
+                    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:
+            cb.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': GCEInstance._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
+
+        operation = (self.provider
+                         .gce_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)
+        cb_inst.label = label
+        if key_pair:
+            cb_inst.key_pair_id = key_pair
+        return cb_inst
+
+    def get(self, instance_id):
+        """
+        Returns an instance given its name. Returns None
+        if the object does not exist.
+
+        A GCE instance is uniquely identified by its selfLink, which is used
+        as its id.
+        """
+        instance = self.provider.get_resource('instances', instance_id)
+        return GCEInstance(self.provider, instance) if instance else None
+
+    def find(self, label, limit=None, marker=None):
+        """
+        Searches for instances by instance label.
+        :return: a list of Instance objects
+        """
+        instances = [instance for instance in self.list()
+                     if instance.label == label]
+        if limit and len(instances) > limit:
+            instances = instances[:limit]
+        return instances
+
+    def list(self, limit=None, marker=None):
+        """
+        List all instances.
+        """
+        # For GCE API, Acceptable values are 0 to 500, inclusive.
+        # (Default: 500).
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gce_compute
+                        .instances()
+                        .list(project=self.provider.project_name,
+                              zone=self.provider.default_zone,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        instances = [GCEInstance(self.provider, inst)
+                     for inst in response.get('items', [])]
+        if len(instances) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(instances))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=instances)
+
+    def create_launch_config(self):
+        return GCELaunchConfig(self.provider)
+
+
+class GCEComputeService(BaseComputeService):
+    # TODO: implement GCEComputeService
+    def __init__(self, provider):
+        super(GCEComputeService, self).__init__(provider)
+        self._instance_svc = GCEInstanceService(self.provider)
+        self._vm_type_svc = GCEVMTypeService(self.provider)
+        self._region_svc = GCERegionService(self.provider)
+        self._images_svc = GCEImageService(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 GCENetworkingService(BaseNetworkingService):
+
+    def __init__(self, provider):
+        super(GCENetworkingService, self).__init__(provider)
+        self._network_service = GCENetworkService(self.provider)
+        self._subnet_service = GCESubnetService(self.provider)
+        self._router_service = GCERouterService(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
+
+
+class GCENetworkService(BaseNetworkService):
+
+    def __init__(self, provider):
+        super(GCENetworkService, self).__init__(provider)
+
+    def get(self, network_id):
+        network = self.provider.get_resource('networks', network_id)
+        return GCENetwork(self.provider, network) if network else None
+
+    def find(self, limit=None, marker=None, **kwargs):
+        """
+        GCE 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))
+
+    def list(self, limit=None, marker=None, filter=None):
+        # TODO: Decide whether we keep filter in 'list'
+        networks = []
+        response = (self.provider
+                        .gce_compute
+                        .networks()
+                        .list(project=self.provider.project_name,
+                              filter=filter)
+                        .execute())
+        for network in response.get('items', []):
+            networks.append(GCENetwork(self.provider, network))
+        return ClientPagedResultList(self.provider, networks,
+                                     limit=limit, marker=marker)
+
+    def create(self, label, cidr_block):
+        """
+        Creates an auto mode VPC network with default subnets. It is possible
+        to add additional subnets later.
+        """
+        GCENetwork.assert_valid_resource_label(label)
+        name = GCENetwork._generate_name_from_label(label, 'cbnet')
+        body = {'name': name}
+        # This results in a custom mode network
+        body['autoCreateSubnetworks'] = False
+        response = (self.provider
+                        .gce_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=GCENetwork.CB_DEFAULT_NETWORK_LABEL)
+        if default_nets:
+            return default_nets[0]
+        else:
+            log.info("Creating a CloudBridge-default network labeled %s",
+                     GCENetwork.CB_DEFAULT_NETWORK_LABEL)
+            return self.create(
+                label=GCENetwork.CB_DEFAULT_NETWORK_LABEL,
+                cidr_block=GCENetwork.CB_DEFAULT_IPV4RANGE)
+
+    def delete(self, network):
+        # Accepts network object
+        if isinstance(network, GCENetwork):
+            name = network.name
+        # Accepts both name and ID
+        elif 'googleapis' in network:
+            name = network.split('/')[-1]
+        else:
+            name = network
+        response = (self.provider
+                        .gce_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.name))
+        return True
+
+
+class GCERouterService(BaseRouterService):
+
+    def __init__(self, provider):
+        super(GCERouterService, self).__init__(provider)
+
+    def get(self, router_id):
+        router = self.provider.get_resource(
+            'routers', router_id, region=self.provider.region_name)
+        return GCERouter(self.provider, router) if router else None
+
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['name', 'label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+        return ClientPagedResultList(self._provider, list(matches))
+
+    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
+                        .gce_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(GCERouter(self.provider, router))
+        if len(routers) > max_result:
+            cb.log.warning('Expected at most %d results; go %d',
+                           max_result, len(routers))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=routers)
+
+    def create(self, label, network):
+        log.debug("Creating GCE Router Service with params "
+                  "[label: %s network: %s]", label, network)
+        GCERouter.assert_valid_resource_label(label)
+        name = GCERouter._generate_name_from_label(label, 'cb-router')
+
+        if not isinstance(network, GCENetwork):
+            network = self.provider.networking.networks.get(network)
+        network_url = network.resource_url
+        region_name = self.provider.region_name
+        response = (self.provider
+                        .gce_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
+
+    def delete(self, router):
+        region_name = self.provider.region_name
+        name = router.name if isinstance(router, GCERouter) else router
+        (self.provider
+         .gce_compute
+         .routers()
+         .delete(project=self.provider.project_name,
+                 region=region_name,
+                 router=name)
+         .execute())
+
+    def _get_in_region(self, router_id, region=None):
+        region_name = self.provider.region_name
+        if region:
+            if not isinstance(region, GCERegion):
+                region = self.provider.compute.regions.get(region)
+            region_name = region.name
+        router = self.provider.get_resource(
+            'routers', router_id, region=region_name)
+        return GCERouter(self.provider, router) if router else None
+
+
+class GCESubnetService(BaseSubnetService):
+
+    def __init__(self, provider):
+        super(GCESubnetService, self).__init__(provider)
+
+    def get(self, subnet_id):
+        subnet = self.provider.get_resource('subnetworks', subnet_id)
+        return GCESubnet(self.provider, subnet) if subnet else None
+
+    def list(self, network=None, zone=None, limit=None, marker=None):
+        """
+        If the zone is not given, we list all subnetworks, in all regions.
+        """
+        filter = None
+        if network is not None:
+            network = (network if isinstance(network, GCENetwork)
+                       else self.provider.networking.networks.get(network))
+            filter = 'network eq %s' % network.resource_url
+        region_names = []
+        if zone:
+            region_names.append(self._zone_to_region_name(zone))
+        else:
+            for r in self.provider.compute.regions:
+                region_names.append(r.name)
+        subnets = []
+        for region_name in region_names:
+            response = (self.provider
+                            .gce_compute
+                            .subnetworks()
+                            .list(project=self.provider.project_name,
+                                  region=region_name,
+                                  filter=filter)
+                            .execute())
+            for subnet in response.get('items', []):
+                subnets.append(GCESubnet(self.provider, subnet))
+        return ClientPagedResultList(self.provider, subnets,
+                                     limit=limit, marker=marker)
+
+    def create(self, label, network, cidr_block, zone):
+        """
+        GCE 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.
+        """
+        GCESubnet.assert_valid_resource_label(label)
+        name = GCESubnet._generate_name_from_label(label, 'cbsubnet')
+        region_name = self._zone_to_region_name(zone)
+#         for subnet in self.iter(network=network):
+#            if BaseNetwork.cidr_blocks_overlap(subnet.cidr_block, cidr_block):
+#                 if subnet.region_name != region_name:
+#                     cb.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
+                        .gce_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
+
+    def get_or_create_default(self, zone):
+        """
+        Every GCP project comes with a default auto mode VPC network. An auto
+        mode VPC network has exactly one subnetwork per region. This method
+        returns the subnetwork of the default network that spans the given
+        zone.
+        """
+        sn = self.find(label=GCESubnet.CB_DEFAULT_SUBNET_LABEL)
+        if sn:
+            return sn[0]
+        # No default subnet look for default network, then create subnet
+        net = self.provider.networking.networks.get_or_create_default()
+        sn = self.provider.networking.subnets.create(
+                label=GCESubnet.CB_DEFAULT_SUBNET_LABEL,
+                cidr_block=GCESubnet.CB_DEFAULT_SUBNET_IPV4RANGE,
+                network=net, zone=zone)
+        router = self.provider.networking.routers.get_or_create_default(net)
+        router.attach_subnet(sn)
+        gateway = net.gateways.get_or_create_inet_gateway()
+        router.attach_gateway(gateway)
+        return sn
+
+    def delete(self, subnet):
+        response = (self.provider
+                    .gce_compute
+                    .subnetworks()
+                    .delete(project=self.provider.project_name,
+                            region=subnet.region_name,
+                            subnetwork=subnet.name)
+                    .execute())
+        self.provider.wait_for_operation(response, region=subnet.region_name)
+        # Remove label
+        tag_name = "_".join(["subnet", subnet.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(subnet.name))
+
+    def _zone_to_region_name(self, zone):
+        if zone:
+            if not isinstance(zone, GCEPlacementZone):
+                zone = GCEPlacementZone(
+                    self.provider,
+                    self.provider.get_resource('zones', zone))
+            return zone.region_name
+        return self.provider.region_name
+
+
+class GCPStorageService(BaseStorageService):
+
+    def __init__(self, provider):
+        super(GCPStorageService, self).__init__(provider)
+
+        # Initialize provider services
+        self._volume_svc = GCEVolumeService(self.provider)
+        self._snapshot_svc = GCESnapshotService(self.provider)
+        self._bucket_svc = GCSBucketService(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
+
+
+class GCEVolumeService(BaseVolumeService):
+
+    def __init__(self, provider):
+        super(GCEVolumeService, self).__init__(provider)
+
+    def get(self, volume_id):
+        """
+        Returns a volume given its id.
+        """
+        vol = self.provider.get_resource('disks', volume_id)
+        return GCEVolume(self.provider, vol) if vol else None
+
+    def find(self, label, limit=None, marker=None):
+        """
+        Searches for a volume by a given list of attributes.
+        """
+        filtr = 'labels.cblabel eq ' + label
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gce_compute
+                        .disks()
+                        .list(project=self.provider.project_name,
+                              zone=self.provider.default_zone,
+                              filter=filtr,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        gce_vols = [GCEVolume(self.provider, vol)
+                    for vol in response.get('items', [])]
+        if len(gce_vols) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(gce_vols))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=gce_vols)
+
+    def list(self, limit=None, marker=None):
+        """
+        List all volumes.
+
+        limit: The maximum number of volumes to return. The returned
+               ResultList's is_truncated property can be used to determine
+               whether more records are available.
+        """
+        # For GCE API, Acceptable values are 0 to 500, inclusive.
+        # (Default: 500).
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gce_compute
+                        .disks()
+                        .list(project=self.provider.project_name,
+                              zone=self.provider.default_zone,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        gce_vols = [GCEVolume(self.provider, vol)
+                    for vol in response.get('items', [])]
+        if len(gce_vols) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(gce_vols))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=gce_vols)
+
+    def create(self, label, size, zone, snapshot=None, description=None):
+        """
+        Creates a new volume.
+
+        Argument `name` must be 1-63 characters long, and comply with RFC1035.
+        Specifically, the name must be 1-63 characters long and match the
+        regular expression [a-z]([-a-z0-9]*[a-z0-9])? which means the first
+        character must be a lowercase letter, and all following characters must
+        be a dash, lowercase letter, or digit, except the last character, which
+        cannot be a dash.
+        """
+        log.debug("Creating GCE Volume with parameters "
+                  "[label: %s size: %s zone: %s snapshot: %s "
+                  "description: %s]", label, size, zone, snapshot,
+                  description)
+        GCEVolume.assert_valid_resource_label(label)
+        name = GCEVolume._generate_name_from_label(label, 'cb-vol')
+        if not isinstance(zone, GCEPlacementZone):
+            zone = GCEPlacementZone(
+                self.provider,
+                self.provider.get_resource('zones', zone))
+        zone_name = zone.name
+        snapshot_id = snapshot.id if isinstance(
+            snapshot, GCESnapshot) and snapshot else snapshot
+        disk_body = {
+            'name': name,
+            'sizeGb': size,
+            'type': 'zones/{0}/diskTypes/{1}'.format(zone_name, 'pd-standard'),
+            'sourceSnapshot': snapshot_id,
+            'description': description,
+        }
+        operation = (self.provider
+                         .gce_compute
+                         .disks()
+                         .insert(
+                             project=self._provider.project_name,
+                             zone=zone_name,
+                             body=disk_body)
+                         .execute())
+        cb_vol = self.get(operation.get('targetLink'))
+        cb_vol.label = label
+        return cb_vol
+
+
+class GCESnapshotService(BaseSnapshotService):
+
+    def __init__(self, provider):
+        super(GCESnapshotService, self).__init__(provider)
+
+    def get(self, snapshot_id):
+        """
+        Returns a snapshot given its id.
+        """
+        snapshot = self.provider.get_resource('snapshots', snapshot_id)
+        return GCESnapshot(self.provider, snapshot) if snapshot else None
+
+    def find(self, label, limit=None, marker=None):
+        """
+        Searches for a snapshot by a given list of attributes.
+        """
+        filtr = 'labels.cblabel eq ' + label
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gce_compute
+                        .snapshots()
+                        .list(project=self.provider.project_name,
+                              filter=filtr,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        snapshots = [GCESnapshot(self.provider, snapshot)
+                     for snapshot in response.get('items', [])]
+        if len(snapshots) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(snapshots))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=snapshots)
+
+    def list(self, limit=None, marker=None):
+        """
+        List all snapshots.
+        """
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = (self.provider
+                        .gce_compute
+                        .snapshots()
+                        .list(project=self.provider.project_name,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        snapshots = [GCESnapshot(self.provider, snapshot)
+                     for snapshot in response.get('items', [])]
+        if len(snapshots) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(snapshots))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=snapshots)
+
+    def create(self, label, volume, description=None):
+        """
+        Creates a new snapshot of a given volume.
+        """
+        GCESnapshot.assert_valid_resource_label(label)
+        name = GCESnapshot._generate_name_from_label(label, 'cbsnap')
+        volume_name = volume.name if isinstance(volume, GCEVolume) else volume
+        snapshot_body = {
+            "name": name,
+            "description": description
+        }
+        operation = (self.provider
+                         .gce_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)
+        cb_snap.label = label
+        return cb_snap
+
+
+class GCSBucketService(BaseBucketService):
+
+    def __init__(self, provider):
+        super(GCSBucketService, self).__init__(provider)
+
+    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 GCSBucket(self.provider, bucket) if bucket else None
+
+    def find(self, name, limit=None, marker=None):
+        """
+        Searches in bucket names for a substring.
+        """
+        buckets = [bucket for bucket in self if name in bucket.name]
+        return ClientPagedResultList(self.provider, buckets, limit=limit,
+                                     marker=marker)
+
+    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
+                        .gcs_storage
+                        .buckets()
+                        .list(project=self.provider.project_name,
+                              maxResults=max_result,
+                              pageToken=marker)
+                        .execute())
+        buckets = []
+        for bucket in response.get('items', []):
+            buckets.append(GCSBucket(self.provider, bucket))
+        if len(buckets) > max_result:
+            cb.log.warning('Expected at most %d results; got %d',
+                           max_result, len(buckets))
+        return ServerPagedResultList('nextPageToken' in response,
+                                     response.get('nextPageToken'),
+                                     False, data=buckets)
+
+    def create(self, name, location=None):
+        GCSBucket.assert_valid_resource_name(name)
+        body = {'name': name}
+        if location:
+            body['location'] = location
+        try:
+            response = (self.provider
+                            .gcs_storage
+                            .buckets()
+                            .insert(project=self.provider.project_name,
+                                    body=body)
+                            .execute())
+            # GCS has a rate limit of 1 operation per 2 seconds for bucket
+            # creation/deletion: https://cloud.google.com/storage/quotas.
+            # Throttle here to avoid future failures.
+            time.sleep(2)
+            return GCSBucket(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

+ 11 - 3
cloudbridge/cloud/providers/openstack/resources.py

@@ -1203,7 +1203,7 @@ class OpenStackKeyPair(BaseKeyPair):
 
 
 class OpenStackVMFirewall(BaseVMFirewall):
-    _network_id_tag = "CB-AUTO-associated-network-id: "
+    _network_id_tag = "CB-auto-associated-network-id: "
 
     def __init__(self, provider, vm_firewall):
         super(OpenStackVMFirewall, self).__init__(provider, vm_firewall)
@@ -1212,15 +1212,23 @@ class OpenStackVMFirewall(BaseVMFirewall):
     @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.
         """
         # Best way would be to use regex, but using this hacky way to avoid
         # importing the re package
+        # FIXME: This doesn't work as soon as the _description doesn't conform
+        # to this rigid string structure.
         net_id = self._description\
                      .split(" [{}".format(self._network_id_tag))[-1]\
                      .split(']')[0]
+        # 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.
+        if not net_id:
+            return None
         return net_id
 
     @property

+ 10 - 18
cloudbridge/cloud/providers/openstack/services.py

@@ -220,30 +220,22 @@ class OpenStackVMFirewallService(BaseVMFirewallService):
                   "[label: %s network id: %s description: %s]", label,
                   network, description)
         net = network.id if isinstance(network, Network) else network
+        # We generally simulate a network being associated with a firewall
+        # by storing the supplied value in the firewall description field that
+        # is not modifiable after creation; however, because of some networking
+        # specificity in Nectar, we must also allow an empty network id value.
+        if not net:
+            net = ""
         if not description:
             description = ""
-        description += "[{}{}]".format(OpenStackVMFirewall._network_id_tag,
-                                       net)
+        description += " [{}{}]".format(OpenStackVMFirewall._network_id_tag,
+                                        net)
         sg = self.provider.os_conn.network.create_security_group(
-            name=label, description=description or label)
+            name=label, description=description)
         if sg:
             return OpenStackVMFirewall(self.provider, sg)
         return None
 
-    def find(self, **kwargs):
-        label = kwargs.pop('label', None)
-
-        # All kwargs should have been popped at this time.
-        if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs, 'label'))
-
-        log.debug("Searching for %s", label)
-        sgs = [self.provider.os_conn.network.find_security_group(label)]
-        results = [OpenStackVMFirewall(self.provider, sg)
-                   for sg in sgs if sg]
-        return ClientPagedResultList(self.provider, results)
-
     def delete(self, group_id):
         log.debug("Deleting OpenStack Firewall with the id: %s", group_id)
         firewall = self.get(group_id)
@@ -931,7 +923,7 @@ class OpenStackSubnetService(BaseSubnetService):
             net = self.provider.networking.networks.get_or_create_default()
             sn = self.provider.networking.subnets.create(
                 label=OpenStackSubnet.CB_DEFAULT_SUBNET_LABEL,
-                cidr_block='10.0.0.0/24',
+                cidr_block=OpenStackSubnet.CB_DEFAULT_SUBNET_IPV4RANGE,
                 network=net)
             router = self.provider.networking.routers.get_or_create_default(
                 net)

BIN
credentials.tar.gz.enc


+ 37 - 25
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 = {'gce_project_name': 'project name',
+              'gce_service_creds_file': 'service_file.json',
+              'gce_default_zone': 'us-east1-b',  # Use desired value
+              'gce_region_name': 'us-east1'}  # Use desired value
+    provider = CloudProviderFactory().create_provider(ProviderList.GCE, 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,9 +129,11 @@ 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.create_subnet(
+        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()
     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=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']
 
@@ -206,8 +220,7 @@ their provider mappings, see :doc:`topics/resource_types_and_mappings`.
 
     # 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')
@@ -215,8 +228,7 @@ their provider mappings, see :doc:`topics/resource_types_and_mappings`.
     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 = net.gateways.floating_ips.find(name='my-fip')
-    fip = fip_list[0]
+    fip_list = gateway.floating_ips.find(name='cb-fip')[0]
 
     # Network
     net = provider.networking.networks.get('network ID')
@@ -226,15 +238,15 @@ their provider mappings, see :doc:`topics/resource_types_and_mappings`.
     # Subnet
     sn = provider.networking.subnets.get('subnet ID')
     # Unknown network
-    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,
-                                               label='my-subnet')
+                                               label='cb-subnet')
     sn = sn_list(0)
 
     # Router
     router = provider.networking.routers.get('router ID')
-    router_list = provider.networking.routers.find(label='my-router')
+    router_list = provider.networking.routers.find(label='cb-router')
     router = router_list[0]
 
     # Gateway
@@ -242,12 +254,12 @@ their provider mappings, see :doc:`topics/resource_types_and_mappings`.
 
     # Firewall
     fw = provider.security.vm_firewalls.get('firewall ID')
-    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(label='cloudbridge-intro')
+    inst_list = provider.compute.instances.list(label='cb-instance')
     inst = inst_list[0]
 
 

+ 3 - 7
docs/topics/aws_mapping.rst

@@ -35,7 +35,6 @@ and the below screenshot shows how the switch between the various services.
 +------------------------+-----+
 
 .. figure:: captures/aws-services-dash.png
-   :scale: 50 %
    :alt: EC2, VPC, and S3
 
    Resources in AWS are separated into three dashboards depending on the
@@ -46,7 +45,7 @@ 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 |
@@ -81,7 +80,6 @@ The below screenshots will help map these properties to AWS objects in the
 web portal.
 
 .. figure:: captures/aws-instance-dash.png
-   :scale: 50 %
    :alt: name, ID, and label properties for AWS EC2 Instances
 
    The CloudBridge `name` and `ID` properties map to the unchangeable
@@ -91,7 +89,6 @@ web portal.
    column.
 
 .. figure:: captures/az-ami-dash.png
-   :scale: 50 %
    :alt: name, ID, and label properties for AWS EC2 AMIs
 
    When an AWS resource allows for an unchangeable name, the CloudBridge
@@ -105,7 +102,7 @@ AWS - Unlabeled Resources
 ---------------------------
 +-----------------------+--------------------+-------+---------+----------+
 | Unlabeled Resource    | AWS Resource Type  | CB ID | CB Name | CB Label |
-+-----------------------+--------------------+-------+---------+----------+
++=======================+====================+=======+=========+==========+
 | AWSKeyPair            | Key Pair           | Name  | Name    | -        |
 +-----------------------+--------------------+-------+---------+----------+
 | AWSBucket             | Bucket             | Name  | Name    | -        |
@@ -123,7 +120,6 @@ support a `name` parameter for the `find` method in their corresponding
 services.
 
 .. figure:: captures/aws-bucket.png
-   :scale: 50 %
    :alt: list of buckets on AWS dashboard
 
    Buckets can be found in the Amazon S3 portal. BucketObjects are contained
@@ -134,7 +130,7 @@ 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                                                               | -        |

+ 3 - 9
docs/topics/azure_mapping.rst

@@ -2,7 +2,7 @@ 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                          |
@@ -37,7 +37,6 @@ necessity in AWS. As such, the VMFirewall creation method requires a
 a tag with the key `network_id`.
 
 .. 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
@@ -47,7 +46,6 @@ a tag with the key `network_id`.
    pointed out 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,
@@ -55,7 +53,6 @@ a tag with the key `network_id`.
    screenshot shows where to find a resource's ID 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
@@ -121,7 +118,6 @@ below screenshots will show how to find Subnets and their labels in the
 Azure web portal.
 
 .. figure:: captures/az-subnet-name.png
-   :scale: 50 %
    :alt: subnet name in Azure portal
 
    The CloudBridge `name` property for Subnets corresponds to the
@@ -133,7 +129,6 @@ Azure web portal.
    rather nested within a Network, in the Subnets tab as shown above.
 
 .. figure:: captures/az-subnet-label.png
-   :scale: 50 %
    :alt: subnet label in Azure portal
 
    The CloudBridge `label` property most often maps to the tag with key
@@ -148,7 +143,7 @@ Azure - Unlabeled Resources
 ---------------------------
 +--------------------+----------------------------------------+-------+---------+----------+
 | Unlabeled Resource | Azure Resource Type                    | CB ID | CB Name | CB Label |
-+--------------------+----------------------------------------+-------+---------+----------+
++====================+========================================+=======+=========+==========+
 | AzureKeyPair       | StorageAccount:Table                   | Name  | Name    | -        |
 +--------------------+----------------------------------------+-------+---------+----------+
 | AzureBucket        | StorageAccount:BlobContainer           | Name  | Name    | -        |
@@ -166,7 +161,6 @@ resources support a `name` parameter for the `find` method in their
 corresponding services.
 
 .. 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,
@@ -182,7 +176,7 @@ 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 | -        |

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/os-instance-dash.png


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


+ 6 - 6
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.create_subnet(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()
     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)
 

+ 26 - 25
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,23 +57,25 @@ 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.create_subnet(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()
     router.attach_gateway(gateway)
@@ -87,12 +88,12 @@ 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.create_subnet(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()
     router.attach_gateway(gateway)

+ 3 - 5
docs/topics/os_mapping.rst

@@ -2,7 +2,7 @@ OpenStack - Labeled Resources
 -----------------------
 +------------------------+------------------------+-----------+----------------+----------+
 | Labeled Resource       | OS Resource Type       | CB ID     | CB Name        | CB Label |
-+------------------------+------------------------+-----------+----------------+----------+
++========================+========================+===========+================+==========+
 | OpenStackInstance      | Instance               | ID        | ID             | Name     |
 +------------------------+------------------------+-----------+----------------+----------+
 | OpenStackMachineImage  | Image                  | ID        | ID             | Name     |
@@ -38,7 +38,6 @@ the description, by appending the following string to the user-provided descript
 (if any) at creation: "[CB-AUTO-associated-network-id: associated_net_id]"
 
 .. figure:: captures/os-instance-dash.png
-   :scale: 50 %
    :alt: name, ID, and label properties for OS Instances
 
    The CloudBridge `name` and `ID` properties map to the unchangeable
@@ -51,7 +50,7 @@ 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    | -        |
@@ -69,7 +68,6 @@ support a `name` parameter for the `find` method in their corresponding
 services.
 
 .. figure:: captures/os-kp-dash.png
-   :scale: 50 %
    :alt: KeyPair details on OS dashboard
 
    KeyPairs and other unlabeled resources in OpenStack have `name` that is
@@ -81,7 +79,7 @@ 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'                                                               | -        |

+ 164 - 0
docs/topics/procuring_credentials.rst

@@ -0,0 +1,164 @@
+
+Procuring access credentials
+----------------------------
+To initialize a connection to a cloud and get a provider object, you will
+need to provide the cloud's access credentials to CloudBridge. This page
+will walk you through the process of procuring credentials. For more
+information on providing these credentials to CloudBridge, see
+`Providing Access Credentials <setup.html>`_.
+
+**Microsoft Azure**
+
+The page linked below from the Microsoft Documentation was used to create this
+section, and can be followed instead of this CloudBridge-specific documentation
+to procure Azure credentials for other purposes.
+https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal
+
+In order to follow the isntructions below, one needs a Microsoft Azure
+account and Subscription, as well as portal access.
+The following section will walk you through the process of creating an
+application, which is required for API access, as well as help you locate
+all required Azure credentials for CloudBridge.
+
+Azure credentials require a `Subscription ID` which can be procured through
+`Subscriptions` in the Azure portal.
+
+.. figure:: captures/az-sub-1.png
+   :alt: Azure Subscriptions 1
+
+   Subscriptions can be accessed by searching, or by choosing `Subscriptions`
+   in the `All Services` window
+
+.. figure:: captures/az-sub-2.png
+   :alt: Azure Subscriptions 2
+
+   The `Subscription ID` will be found in the list of subscriptions
+
+
+Another piece of credentials that already exists on any Azure account is a
+`Tenant ID` which will correspond to the `Active Directory ID`, which can be
+procured through the Azure Active Directory's `Properties`.
+
+.. figure:: captures/az-dir-1.png
+   :alt: Azure Directory 1
+
+   The `Azure Active Directory` can be access by searching, or by choosing
+   `Azure Active Directory` in the `All Services` window. `Azure Active
+   Directory` is also a default favorite on the sidebar
+
+.. figure:: captures/az-dir-2.png
+   :alt: Azure Directory 2
+
+   The `Directory ID` will be found in the Directory's `Properties` section
+
+
+In order to access the API, an application needs to be registered and a key
+needs to be created. After creating an application through the
+`App Registrations` window under the `Active Directory`, the `Application
+ID` of the app will correspond to the `Client ID` in CloudBridge, and the
+generated value of its key, will correspond to the `Secret`.
+
+
+.. figure:: captures/az-app-1.png
+   :alt: Azure App 1
+
+   `App Registrations` can be access by searching, or through choosing `App
+   Registrations` under `Azure Active Directory`
+
+.. figure:: captures/az-app-2.png
+   :alt: Azure App 2
+
+   The `New Application Registration` button will allow users to create a
+   new application
+
+.. figure:: captures/az-app-3.png
+   :alt: Azure App 3
+
+   The `Name` has to be unique within the subscription and will be used to
+   identify the `Application` later on. The `Sign-on URL` can be any
+   URL-looking string. It does not have to point towards anything.
+
+.. figure:: captures/az-app-4.png
+   :alt: Azure App 4
+
+   After creating the application, one must select it, after which the
+   `Application ID` will map to the `Client ID` in CloudBridge
+
+.. figure:: captures/az-app-5.png
+   :alt: Azure App 5
+
+   In the application's `Settings` panel, under the `Keys` section, one will
+   be able to create a new `Secret`
+
+.. figure:: captures/az-app-6.png
+   :alt: Azure App 6
+
+   Any name can be given to the key, and any expiration date, after which
+   the `Save` button will generate the `Key` which will correspond to the
+   `Secret` in CloudBridge
+
+.. figure:: captures/az-app-7.png
+   :alt: Azure App 7
+
+   The value of the key will correspond to the `Secret` in CloudBridge and
+   needs to be saved at creation-time
+
+
+Finally, in order to have appropriate permissions, you must assign an
+appropriate role to the newly created application. Permissions can be
+assigned at the level of the Subscription, or at the level of each Resource
+Group. `Contributor` access is recommended for general use in order to have
+sufficient permissions to create and manage all types of resources, but
+specific roles can also be assigned for more limited access.
+
+
+.. figure:: captures/az-role-1.png
+   :alt: Azure Roles 1
+
+   Subscription-level access will allow the application to access resources
+   from multiple resource groups
+
+.. figure:: captures/az-role-2.png
+   :alt: Azure Roles 2
+
+   When roles are set at the level of the Resource Group, one must specify
+   this Resource Group as part of the credentials, as the application will
+   not have enough permissions to create a Resource Group
+
+.. figure:: captures/az-role-3.png
+   :alt: Azure Roles 3
+
+   Adding a role assignment to the application will give it appropriate
+   permissions to manage resources
+
+
+**Google**
+
+For Google Compute Engine, create a service account following instructions
+from the link below:
+https://cloud.google.com/iam/docs/creating-managing-service-accounts#creating_a_service_account
+
+Once created, grant the account appropriate permissions for your use through
+roles, and create a key, choosing JSON format, when prompted. These
+credentials can then be used with CloudBridge through the variables shown
+in the sections below.
+
+The JSON credentials file will have a similar form to the example shown
+below, and can either be passed through an absolute path to the file, or
+through a variable containing the JSON dictionary itself.
+
+
+.. code-block:: json
+
+    {
+      "type": "service_account",
+      "project_id": "my-project",
+      "private_key_id": "b12321312441245gerg245245g42c245g254t425",
+      "private_key": "-----BEGIN PRIVATE KEY-----\nMIICWgIBAAKBgE1EJDPKM/2wck/CZYCS7F2cXoHXDBhXYtdeV+h70Nk+ABs6scAV\nApYoobJAVpDeL+lutYAwtbscNz5K915DiNEkBf48LhfBWc5ea07OnClOGC9zASja\nif6ujIdhbITaNat9rdG939gQWqyaDW4wzYfvurhfmxICNgZA1YpWco1HAgMBAAEC\ngYAc+vLtLelEPNsTSWGS0Qiwr8bOwl75/kTHbM5iF5ak9NlLXT9wQTEgKwtC9VjC\nq2OjFXAkLaDsFlAuICYaCBCXn1nUqNoYhaSEQNwGnWIz376letXg/mX+BALSPMFR\nhE6mbdmaL4OV1X8j8uf2VcrLfVFCCZfhPu/TM5D6bVFYoQJBAJRHNKYU/csAB/NE\nzScJBv7PltOAoYpxbyFZb1rWcV9mAn34382b0YBXbp3Giqvifs/teudUbRpAzzLm\n5gr8tzECQQCFZh4tNIzeZZYUqkQxrxgqnnONey1hX7K+BlGyC6n2o26sE+I7cLij\n2kbuWoSFMAIdM2Hextv9k+ZrwUas4V33AkAfi9Korvib0sLeP7oB3wrM9W9aShiU\nMrP4/WUSh2MRb8uB74v123vD+VYAXTgtf3+JTzYBt1WK61TpuHQizEdRAkBjt8hL\nBoNfJBUicXz0nuyzvyql0jREG+NjhRnAvFNbGSR74Yk14bdEVMC9IFD7tr190pEQ\nlRqR3eNbHWmVhgpVAkBgveeM73R1tFXS6UosBtfDI1zut44Ce0RoADOIxjXqgjOi\nXSrevYvoKCl09yhLNAnKD+QvT/YbshW/jibYXwdj\n-----END PRIVATE KEY-----",
+      "client_email": "service-name@my-project.iam.gserviceaccount.com",
+      "client_id": "13451345134513451345",
+      "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+      "token_uri": "https://oauth2.googleapis.com/token",
+      "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+      "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-name%40my-project.iam.gserviceaccount.com"
+    }

+ 4 - 3
docs/topics/release_process.rst

@@ -22,7 +22,7 @@ Release Process
    ``git shortlog <last release hash>..HEAD``
 
 5. Release to PyPi.
-   (make sure you have run `pip install wheel`)
+   (make sure you have run `pip install wheel twine`)
    First, test release with PyPI staging server as described in:
    https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/
 
@@ -32,14 +32,15 @@ Release Process
 
    # remove stale files or wheel might package them
    rm -r build dist
-   python setup.py sdist upload
-   python setup.py bdist_wheel upload
+   python setup.py sdist bdist_wheel
+   twine upload -r pypi dist/cloudbridge-1.0.0*
 
 6. Tag release and make a GitHub release.
 
 .. code-block:: bash
 
    git tag -a v1.0.0 -m "Release 1.0.0"
+   git push
    git push --tags
 
 7. Increment version number in ``cloudbridge/__init__.py`` to ``version-dev``

+ 4 - 4
docs/topics/resource_types_and_mapping.rst

@@ -34,7 +34,7 @@ whether they support a `label` property.
 
 +-------------------+---------------------+
 | Labeled Resources | Unlabeled Resources | 
-+-------------------+---------------------+
++===================+=====================+
 | Instance          | Key Pair            |
 +-------------------+---------------------+
 | MachineImage      | Bucket              |
@@ -60,6 +60,6 @@ properties to provider objects, as well as some useful dashboard navigation.
 These sections will thus present summary tables delineating the different types of
 CloudBridge resources, as well as present some design decisions made to
 preserve consistency across providers:
--`Detailed Azure Mappings <azure_mapping.html>`
--`Detailed AWS Mappings <aws_mapping.html>`
--`Detailed OpenStack Mappings <os_mapping.html>`
+-`Detailed Azure Mappings <azure_mapping.html>`_
+-`Detailed AWS Mappings <aws_mapping.html>`_
+-`Detailed OpenStack Mappings <os_mapping.html>`_

+ 165 - 83
docs/topics/setup.rst

@@ -1,45 +1,64 @@
 Setup
 -----
 To initialize a connection to a cloud and get a provider object, you will
-need to provide the cloud's access credentials to CloudBridge. These may
-be provided in one of following ways:
+need to provide the cloud's access credentials to CloudBridge. For more
+details on how to create and find these credentials, see `Procuring Access
+Credentials <procuring_credentials.html>`_. Once available, these may be
+provided in one of following ways:
 
 1. Environment variables
 2. A dictionary
 3. Configuration file
 
-Procuring access credentials
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-For Azure, Create service principle credentials from the following link : 
-https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal#check-azure-subscription-permissions
-
 
 Providing access credentials through environment variables
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 The following environment variables must be set, depending on the provider in use.
 
+
 **Amazon**
 
-===================  ==================
-Mandatory variables  Optional Variables
-===================  ==================
-AWS_ACCESS_KEY
-AWS_SECRET_KEY
-===================  ==================
++---------------------+
+| Mandatory variables |
++=====================+
+| AWS_ACCESS_KEY      |
++---------------------+
+| AWS_SECRET_KEY      |
++---------------------+
 
-**Openstack**
 
-===================  ==================
-Mandatory variables  Optional Variables
-===================  ==================
-OS_AUTH_URL			 NOVA_SERVICE_NAME
-OS_USERNAME			 OS_COMPUTE_API_VERSION
-OS_PASSWORD			 OS_VOLUME_API_VERSION
-OS_PROJECT_NAME      OS_STORAGE_URL
-OS_REGION_NAME       OS_AUTH_TOKEN
-===================  ==================
+**Openstack**
 
-**Azure**
++---------------------+
+| Mandatory variables |
++=====================+
+| OS_AUTH_URL         |
++---------------------+
+| OS_USERNAME         |
++---------------------+
+| OS_PASSWORD         |
++---------------------+
+| OS_PROJECT_NAME     |
++---------------------+
+| OS_REGION_NAME      |
++---------------------+
+
++------------------------+
+| Optional Variables     |
++========================+
+| NOVA_SERVICE_NAME      |
++------------------------+
+| OS_COMPUTE_API_VERSION |
++------------------------+
+| OS_VOLUME_API_VERSION  |
++------------------------+
+| OS_STORAGE_URL         |
++------------------------+
+| OS_AUTH_TOKEN          |
++------------------------+
+
+
+**Microsoft Azure**
 
 Note that managing resources in Azure requires a Resource Group. If a
 Resource Group is not provided as part of the configuration, cloudbridge will
@@ -53,15 +72,52 @@ when initializing the relevant services. This operation similarly requires a
 "contributor" or "owner" role.
 For more information on roles, see: https://docs.microsoft.com/en-us/azure/role-based-access-control/overview
 
-======================  ==================
-Mandatory variables     Optional Variables
-======================  ==================
-AZURE_SUBSCRIPTION_ID   AZURE_REGION_NAME
-AZURE_CLIENT_ID         AZURE_RESOURCE_GROUP
-AZURE_SECRET            AZURE_STORAGE_ACCOUNT
-AZURE_TENANT            AZURE_VM_DEFAULT_USER_NAME
-                        AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
-======================  ==================
++-----------------------+
+| Mandatory variables   |
++=======================+
+| AZURE_SUBSCRIPTION_ID |
++-----------------------+
+| AZURE_CLIENT_ID       |
++-----------------------+
+| AZURE_SECRET          |
++-----------------------+
+| AZURE_TENANT          |
++-----------------------+
+
++-------------------------------------+
+| Optional Variables                  |
++=====================================+
+| AZURE_REGION_NAME                   |
++-------------------------------------+
+| AZURE_RESOURCE_GROUP                |
++-------------------------------------+
+| AZURE_STORAGE_ACCOUNT               |
++-------------------------------------+
+| AZURE_VM_DEFAULT_USER_NAME          |
++-------------------------------------+
+| AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME |
++-------------------------------------+
+
+
+**Google**
+
++------------------------+
+| Mandatory variables    |
++========================+
+| GCE_SERVICE_CREDS_FILE |
+| or                     |
+| GCE_SERVICE_CREDS_DICT |
++------------------------+
+
++--------------------+
+| Optional Variables |
++====================+
+| GCE_PROJECT_NAME   |
++--------------------+
+| GCE_DEFAULT_ZONE   |
++--------------------+
+| GCE_REGION_NAME    |
++--------------------+
 
 Once the environment variables are set, you can create a connection as follows:
 
@@ -98,39 +154,51 @@ will override environment values.
 Some optional configuration values can only be provided through the config
 dictionary. These are listed below for each provider.
 
+
 **CloudBridge**
 
-====================  ==================
-Variable		      Description
-====================  ==================
-default_result_limit  Number of results that a ``.list()`` method should return.
-                      Defaults to 50.
-====================  ==================
++----------------------+------------------------------------------------------------+
+| Variable		       | Description                                                |
++======================+============================================================+
+| default_result_limit | Number of results that a ``.list()`` method should return. |
+|                      | Defaults to 50.                                            |
++----------------------+------------------------------------------------------------+
 
 
 **Amazon**
 
-====================  ==================
-Variable		      Description
-====================  ==================
-aws_session_token     Session key for your AWS account (if using temporary
-                      credentials).
-ec2_is_secure         True to use an SSL connection. Default is ``True``.
-ec2_region_name       Default region name. Defaults to ``us-east-1``.
-ec2_region_endpoint   Endpoint to use. Default is ``ec2.us-east-1.amazonaws.com``.
-ec2_port              EC2 connection port. Does not need to be specified unless
-                      EC2 service is running on an alternative port.
-ec2_conn_path	      Connection path. Defaults to ``/``.
-ec2_validate_certs    Whether to use SSL certificate verification. Default is
-                      ``False``.
-s3_is_secure          True to use an SSL connection. Default is ``True``.
-s3_host               Host connection endpoint. Default is ``s3.amazonaws.com``.
-s3_port               Host connection port. Does not need to be specified unless
-                      S3 service is running on an alternative port.
-s3_conn_path          Connection path. Defaults to ``/``.
-s3_validate_certs     Whether to use SSL certificate verification. Default is
-                      ``False``.
-====================  ==================
++---------------------+--------------------------------------------------------------+
+| Variable		      | Description		      	      	      	      	      	     |
++=====================+==============================================================+
+| aws_session_token   | Session key for your AWS account (if using temporary   	     |
+|                     | credentials).   	      	      	      	      	      	 |
++---------------------+--------------------------------------------------------------+
+| ec2_is_secure       | True to use an SSL connection. Default is ``True``.   	     |
++---------------------+--------------------------------------------------------------+
+| ec2_region_name     | Default region name. Defaults to ``us-east-1``.   	       	 |
++---------------------+--------------------------------------------------------------+
+| ec2_region_endpoint | Endpoint to use. Default is ``ec2.us-east-1.amazonaws.com``. |
++---------------------+--------------------------------------------------------------+
+| ec2_port            | EC2 connection port. Does not need to be specified unless    |
+|                     | EC2 service is running on an alternative port.   	       	 |
++---------------------+--------------------------------------------------------------+
+| ec2_conn_path	      | Connection path. Defaults to ``/``.   	       	      	     |
++---------------------+--------------------------------------------------------------+
+| ec2_validate_certs  | Whether to use SSL certificate verification. Default is   	 |
+|                     | ``False``.   	       	      	      	      	      	     |
++---------------------+--------------------------------------------------------------+
+| s3_is_secure        | True to use an SSL connection. Default is ``True``.   	     |
++---------------------+--------------------------------------------------------------+
+| s3_host             | Host connection endpoint. Default is ``s3.amazonaws.com``.   |
++---------------------+--------------------------------------------------------------+
+| s3_port             | Host connection port. Does not need to be specified unless   |
+|                     | S3 service is running on an alternative port.   	         |
++---------------------+--------------------------------------------------------------+
+| s3_conn_path        | Connection path. Defaults to ``/``.   	                     |
++---------------------+--------------------------------------------------------------+
+| s3_validate_certs   | Whether to use SSL certificate verification. Default is   	 |
+|                     | ``False``.   	                                             |
++---------------------+--------------------------------------------------------------+
 
 
 Providing access credentials in a file
@@ -138,10 +206,11 @@ Providing access credentials in a file
 CloudBridge can also read credentials from a file on your local file system.
 The file should be placed in one of two locations: ``/etc/cloudbridge.ini`` or
 ``~/.cloudbridge``. Each set of credentials should be delineated with the
-provider ID (e.g., ``openstack``, ``aws``, ``azure``) with the necessary credentials
-being supplied in YAML format. Note that only one set of credentials per
-cloud provider type can be supplied (i.e., via this method, it is not possible
-to provide credentials for two different OpenStack clouds).
+provider ID (e.g., ``openstack``, ``aws``, ``azure``, ``gce``) with the
+necessary credentials being supplied in YAML format. Note that only one set
+of credentials per cloud provider type can be supplied (i.e., via this
+method, it is not possible to provide credentials for two different
+OpenStack clouds).
 
 .. code-block:: bash
 
@@ -164,22 +233,35 @@ In addition to the provider specific configuration variables above, there are
 some general configuration environment variables that apply to CloudBridge as
 a whole
 
-======================== ======================================================
-Variable		                            Description
-======================== ======================================================
-CB_DEBUG                 Setting ``CB_DEBUG=True`` will cause detailed debug
-                         output to be printed for each provider (including HTTP
-                         traces).
-CB_USE_MOCK_PROVIDERS    Setting this to ``True`` will cause the CloudBridge
-                         test suite to use mock drivers when available.
-CB_TEST_PROVIDER         Set this value to a valid :class:`.ProviderList` value
-                         such as ``aws``, to limit tests to that provider only.
-CB_DEFAULT_SUBNET_LABEL  Name to be used for a subnet that will be considered
-                         the 'default' by the library. This default will be
-                         used only in cases there is no subnet marked as the
-                         default by the provider.
-CB_DEFAULT_NETWORK_LABEL Name to be used for a network that will be considered
-                         the 'default' by the library. This default will be
-                         used only in cases there is no network marked as the
-                         default by the provider.
-======================== ======================================================
++-----------------------------+------------------------------------------------------+
+| Variable                    | Description                                          |
++=============================+======================================================+
+| CB_DEBUG                    | Setting ``CB_DEBUG=True`` will cause detailed        |
+|                             | debugoutput to be printed for each provider          |
+|                             | (including HTTP traces).                             |
++-----------------------------+------------------------------------------------------+
+| CB_USE_MOCK_PROVIDERS       | Setting this to ``True`` will cause the CloudBridge  |
+|                             | test suite to use mock drivers when available.       |
++-----------------------------+------------------------------------------------------+
+| CB_TEST_PROVIDER            | Set this value to a valid :class:`.ProviderList`     |
+|                             | value such as ``aws``, to limit tests to that        |
+|                             | provider only.                                       |
++-----------------------------+------------------------------------------------------+
+| CB_DEFAULT_SUBNET_LABEL     | Name to be used for a subnet that will be            |
+|                             | considered the 'default' by the library. This        |
+|                             | default will be used only in cases there is no       |
+|                             | subnet marked as the default by the provider.        |
++-----------------------------+------------------------------------------------------+
+| CB_DEFAULT_NETWORK_LABEL    | Name to be used for a network that will be           |
+|                             | considered the 'default' by the library. This        |
+|                             | default will be used only in cases there is no       |
+|                             | network marked as the default by the provider.       |
++-----------------------------+------------------------------------------------------+
+| CB_DEFAULT_IPV4RANGE        | The default IPv4 range when creating networks if     |
+|                             | one is not provided. This value is also used in      |
+|                             | tests.                                               |
++-----------------------------+------------------------------------------------------+
+| CB_DEFAULT_SUBNET_IPV4RANGE | The default subnet IPv4 range used by CloudBridge    |
+|                             | if one is not specified by the user. Tests do not    |
+|                             | respect this variable.                               |
++-----------------------------+------------------------------------------------------+

+ 4 - 4
setup.py

@@ -25,7 +25,7 @@ REQS_BASE = [
     'cachetools>=2.1.0',
     'deprecated>=1.2.3'
 ]
-REQS_AWS = ['boto3<1.8.0']
+REQS_AWS = ['boto3>=1.9.86']
 # Install azure>=3.0.0 package to find which of the azure libraries listed
 # below are compatible with each other. List individual libraries instead
 # of using the azure umbrella package to speed up installation.
@@ -40,6 +40,7 @@ REQS_AZURE = ['msrest>=0.5.4,<0.6',
               'azure-storage-blob==1.3.1',
               'azure-cosmosdb-table==1.0.4',
               'pysftp==0.2.9']
+REQS_GCP = ['google-api-python-client', 'oauth2client']
 REQS_OPENSTACK = [
     'openstacksdk>=0.12.0,<=0.17',
     'python-novaclient>=7.0.0,<=11.0',
@@ -49,7 +50,7 @@ REQS_OPENSTACK = [
     'python-neutronclient>=6.0.0,<=6.9',
     'python-keystoneclient>=3.13.0,<=3.17'
 ]
-REQS_FULL = REQS_BASE + REQS_AWS + REQS_AZURE + REQS_OPENSTACK
+REQS_FULL = REQS_BASE + REQS_AWS + REQS_AZURE + REQS_GCP + REQS_OPENSTACK
 # httpretty is required with/for moto 1.0.0 or AWS tests fail
 REQS_DEV = ([
     'tox>=2.1.1',
@@ -92,7 +93,6 @@ setup(
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: Implementation :: CPython',
-        'Programming Language :: Python :: Implementation :: PyPy'],
+        'Programming Language :: Python :: Implementation :: CPython'],
     test_suite="test"
 )

+ 42 - 11
test/helpers/__init__.py

@@ -12,6 +12,9 @@ from cloudbridge.cloud.base.helpers import get_env
 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):
@@ -85,12 +88,19 @@ TEST_DATA_CONFIG = {
         "vm_type": get_env('CB_VM_TYPE_AWS', 't2.nano'),
         "placement": get_env('CB_PLACEMENT_AWS', 'us-east-1a'),
     },
-    "OpenStackCloudProvider": {
-        "image": os.environ.get('CB_IMAGE_OS',
+    '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'),
     },
+    'GCECloudProvider': {
+        'image': ('https://www.googleapis.com/compute/v1/'
+                  'projects/ubuntu-os-cloud/global/images/'
+                  'ubuntu-1710-artful-v20180126'),
+        'vm_type': 'f1-micro',
+        'placement': os.environ.get('GCE_DEFAULT_ZONE', 'us-central1-a'),
+    },
     "AzureCloudProvider": {
         "placement":
             get_env('CB_PLACEMENT_AZURE', 'eastus'),
@@ -108,6 +118,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 "GCECloudProvider" in provider.name:
+        return TEST_DATA_CONFIG.get("GCECloudProvider").get(key)
     elif "AzureCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("AzureCloudProvider").get(key)
     return None
@@ -121,14 +133,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 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):
@@ -142,7 +173,7 @@ def get_test_gateway(provider):
     return net.gateways.get_or_create_inet_gateway()
 
 
-def delete_test_gateway(gateway):
+def cleanup_gateway(gateway):
     """
     Delete the supplied network and gateway.
     """
@@ -186,7 +217,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 +227,12 @@ 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)
+    with cleanup_action(lambda: cleanup_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)
+                delete_instance(instance)
 
 
 def get_uuid():

+ 1 - 1
test/helpers/standard_interface_tests.py

@@ -117,7 +117,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"

+ 5 - 19
test/test_block_store_service.py

@@ -21,10 +21,6 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
 
     @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,9 +43,6 @@ 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
@@ -76,9 +69,6 @@ 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
@@ -119,7 +109,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 != 'gce'):
                     self.assertEqual(test_vol.attachments.device,
                                      "/dev/sda2")
                 test_vol.detach()
@@ -136,11 +127,9 @@ 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,
@@ -180,9 +169,6 @@ 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,

+ 17 - 35
test/test_cloud_factory.py

@@ -16,28 +16,22 @@ 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
-        """
+        # 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(
@@ -55,24 +49,18 @@ class CloudFactoryTestCase(unittest.TestCase):
                 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_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'
 
@@ -82,10 +70,8 @@ class CloudFactoryTestCase(unittest.TestCase):
                         factory.get_all_provider_classes(get_mock=False))
 
     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'
 
@@ -98,10 +84,8 @@ class CloudFactoryTestCase(unittest.TestCase):
                         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
-        """
+        # Attempting to register the same mock provider twice should register
+        # only the second instance
         class DummyClass(CloudProvider, TestMockHelperMixin):
             PROVIDER_ID = 'aws'
 
@@ -114,10 +98,8 @@ class CloudFactoryTestCase(unittest.TestCase):
                         factory.get_all_provider_classes(get_mock=True))
 
     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
 

+ 1 - 3
test/test_cloud_helpers.py

@@ -80,9 +80,7 @@ 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'
         config_value = self.provider._get_config_value('text_type_check', None)
         self.assertIsInstance(config_value, six.string_types)

+ 29 - 20
test/test_compute_service.py

@@ -2,6 +2,7 @@ import ipaddress
 
 import six
 
+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
@@ -83,7 +84,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             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],
@@ -276,21 +277,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 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:
@@ -312,7 +317,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         with 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(
@@ -321,7 +326,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             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)
@@ -353,13 +358,16 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                                                                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.")
+                fip = None
+
+                with 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 helpers.cleanup_action(lambda: fip.delete()):
                     with helpers.cleanup_action(
                             lambda: test_inst.remove_floating_ip(fip)):
                         test_inst.add_floating_ip(fip)
@@ -370,7 +378,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()

+ 2 - 6
test/test_image_service.py

@@ -1,5 +1,6 @@
 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 test import helpers
 from test.helpers import ProviderTestBase
@@ -13,11 +14,6 @@ class CloudImageServiceTestCase(ProviderTestBase):
     @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())
 

+ 7 - 13
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 == 'gce':
+            cloned_config['gce_service_creds_dict'] = {'dummy': 'dict'}
 
         with self.assertRaises(ProviderConnectionException):
             cloned_provider = CloudProviderFactory().create_provider(

+ 64 - 42
test/test_network_service.py

@@ -1,3 +1,4 @@
+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
@@ -20,15 +21,16 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
         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 +42,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 helpers.cleanup_action(lambda: helpers.cleanup_network(net)):
             net.wait_till_ready()
             self.assertEqual(
                 net.state, 'available',
@@ -52,16 +52,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(
                 label=subnet_label, cidr_block=cidr,
                 zone=helpers.get_provider_test_data(self.provider,
                                                     'placement'))
-            with helpers.cleanup_action(lambda: sn.delete()):
+            with helpers.cleanup_action(lambda: helpers.cleanup_subnet(sn)):
                 self.assertTrue(
                     sn in net.subnets,
                     "Subnet ID %s should be listed in network subnets %s."
@@ -89,14 +89,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 +111,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)
@@ -130,7 +145,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                 gw.floating_ips.delete(fip.id)
 
         with helpers.cleanup_action(
-                lambda: helpers.delete_test_gateway(gw)):
+                lambda: helpers.cleanup_gateway(gw)):
             sit.check_crud(self, gw.floating_ips, FloatingIP,
                            "cb-crudfip", create_fip, cleanup_fip,
                            skip_name_check=True)
@@ -141,7 +156,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             self.provider)
         fip = gw.floating_ips.create()
         with helpers.cleanup_action(
-                lambda: helpers.delete_test_gateway(gw)):
+                lambda: helpers.cleanup_gateway(gw)):
             with helpers.cleanup_action(lambda: fip.delete()):
                 fipl = list(gw.floating_ips)
                 self.assertIn(fip, fipl)
@@ -163,10 +178,12 @@ 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 helpers.cleanup_action(lambda: helpers.cleanup_network(net)):
+                with helpers.cleanup_action(
+                        lambda: helpers.cleanup_subnet(subnet)):
+                    with helpers.cleanup_action(lambda: router.delete()):
+                        with helpers.cleanup_action(
+                                lambda: helpers.cleanup_gateway(gateway)):
                             router.detach_subnet(subnet)
                             router.detach_gateway(gateway)
 
@@ -179,7 +196,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         gteway = None
         with 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'
@@ -190,30 +207,35 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             # 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)
-            )
+            if (self.provider.PROVIDER_ID != 'gce'):
+                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_inet_gateway()
             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):

+ 14 - 15
test/test_object_life_cycle.py

@@ -11,25 +11,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 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)
+            # 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)
+            # 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):

+ 7 - 15
test/test_object_store_service.py

@@ -23,10 +23,6 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
     @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)
@@ -71,11 +67,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)
 
@@ -195,14 +189,12 @@ 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)

+ 0 - 13
test/test_region_service.py

@@ -13,10 +13,6 @@ class CloudRegionServiceTestCase(ProviderTestBase):
 
     @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 +28,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:

+ 5 - 15
test/test_security_service.py

@@ -92,7 +92,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             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)
 
@@ -106,7 +106,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         fw = None
         with 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(
@@ -133,7 +133,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             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',
@@ -157,7 +157,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             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',
@@ -180,17 +180,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             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)

+ 3 - 5
test/test_vm_types_service.py

@@ -63,11 +63,9 @@ class CloudVMTypeServiceTestCase(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")

+ 5 - 1
tox.ini

@@ -12,7 +12,7 @@
 # mock providers.
 
 [tox]
-envlist = {py27,py36,pypy}-{aws,azure,openstack}
+envlist = {py27,py36,pypy}-{aws,azure,gce,openstack}
 
 [testenv]
 commands = flake8 cloudbridge test setup.py
@@ -20,13 +20,17 @@ commands = flake8 cloudbridge test setup.py
            nosetests -v --nocapture --nologcapture --logging-format='%(asctime)s [%(levelname)s] %(name)s: %(message)s' {posargs}
 setenv =
     MOTO_AMIS_PATH=./test/fixtures/custom_amis.json
+    # Fix for moto import issue: https://github.com/travis-ci/travis-ci/issues/7940
+    BOTO_CONFIG=/dev/null
     aws: CB_TEST_PROVIDER=aws
     azure: CB_TEST_PROVIDER=azure
+    gce: CB_TEST_PROVIDER=gce
     openstack: CB_TEST_PROVIDER=openstack
 passenv =
     CB_USE_MOCK_PROVIDERS PYTHONUNBUFFERED
     aws: CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS AWS_ACCESS_KEY AWS_SECRET_KEY
     azure: AZURE_SUBSCRIPTION_ID AZURE_CLIENT_ID AZURE_SECRET AZURE_TENANT AZURE_REGION_NAME AZURE_RESOURCE_GROUP AZURE_STORAGE_ACCOUNT AZURE_VM_DEFAULT_USER_NAME AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
+    gce: CB_IMAGE_GCE CB_INSTANCE_TYPE_GCE CB_PLACEMENT_GCE GCE_DEFAULT_REGION GCE_DEFAULT_ZONE GCE_PROJECT_NAME GCE_SERVICE_CREDS_FILE GCE_SERVICE_CREDS_DICT
     openstack:  CB_IMAGE_OS CB_INSTANCE_TYPE_OS CB_PLACEMENT_OS OS_AUTH_URL OS_PASSWORD OS_PROJECT_NAME OS_TENANT_NAME OS_USERNAME OS_REGION_NAME OS_USER_DOMAIN_NAME OS_PROJECT_DOMAIN_NAME NOVA_SERVICE_NAME
 deps =
     -rrequirements.txt