Ver Fonte

Merge pull request #114 from CloudVE/gce

GCE merge to master
Nuwan Goonasekera há 7 anos atrás
pai
commit
ed8de43c5a

+ 1 - 0
.gitignore

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

+ 30 - 23
.travis.yml

@@ -2,38 +2,45 @@ dist: trusty
 language: python
 language: python
 cache:
 cache:
   directories:
   directories:
-    - $HOME/.cache/pip
-    - $TRAVIS_BUILD_DIR/.tox
+  - "$HOME/.cache/pip"
+  - "$TRAVIS_BUILD_DIR/.tox"
 os:
 os:
-  - linux
-#  - osx
+- linux
 matrix:
 matrix:
   fast_finish: true
   fast_finish: true
   allow_failures:
   allow_failures:
-    - os: osx
+  - os: osx
   include:
   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
+  - 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-gce
+  - python: pypy-5.3.1
+    env: TOX_ENV=pypy-openstack
 env:
 env:
   global:
   global:
     - PYTHONUNBUFFERED=True
     - PYTHONUNBUFFERED=True
 before_install:
 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
       case "$TRAVIS_EVENT_TYPE" in
         push|pull_request)
         push|pull_request)

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

@@ -265,12 +265,16 @@ class BasePageableObjectMixin(PageableObjectMixin):
     """
     """
 
 
     def __iter__(self):
     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:
         if result_list.supports_server_paging:
             for result in result_list:
             for result in result_list:
                 yield result
                 yield result
             while result_list.is_truncated:
             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:
                 for result in result_list:
                     yield result
                     yield result
         else:
         else:
@@ -771,10 +775,25 @@ class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
 
     CB_DEFAULT_NETWORK_LABEL = os.environ.get('CB_DEFAULT_NETWORK_LABEL',
     CB_DEFAULT_NETWORK_LABEL = os.environ.get('CB_DEFAULT_NETWORK_LABEL',
                                               'cloudbridge-net')
                                               'cloudbridge-net')
+    CB_DEFAULT_IPV4RANGE = os.environ.get('CB_DEFAULT_IPV4RANGE',
+                                          '10.0.0.0/16')
 
 
     def __init__(self, provider):
     def __init__(self, provider):
         super(BaseNetwork, self).__init__(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):
     def wait_till_ready(self, timeout=None, interval=None):
         self.wait_for(
         self.wait_for(
             [NetworkState.AVAILABLE],
             [NetworkState.AVAILABLE],
@@ -797,6 +816,8 @@ class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
 
 
     CB_DEFAULT_SUBNET_LABEL = os.environ.get('CB_DEFAULT_SUBNET_LABEL',
     CB_DEFAULT_SUBNET_LABEL = os.environ.get('CB_DEFAULT_SUBNET_LABEL',
                                              'cloudbridge-subnet')
                                              'cloudbridge-subnet')
+    CB_DEFAULT_SUBNET_IPV4RANGE = os.environ.get('CB_DEFAULT_SUBNET_IPV4RANGE',
+                                                 '10.0.0.0/24')
 
 
     def __init__(self, provider):
     def __init__(self, provider):
         super(BaseSubnet, self).__init__(provider)
         super(BaseSubnet, self).__init__(provider)

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

@@ -80,6 +80,20 @@ class BaseVMFirewallService(
     def __init__(self, provider):
     def __init__(self, provider):
         super(BaseVMFirewallService, self).__init__(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):
 class BaseStorageService(StorageService, BaseCloudService):
 
 
@@ -207,8 +221,6 @@ class BaseSubnetService(
         return ClientPagedResultList(self._provider, list(matches))
         return ClientPagedResultList(self._provider, list(matches))
 
 
     def get_or_create_default(self, zone):
     def get_or_create_default(self, zone):
-        default_cidr = '10.0.0.0/24'
-
         # Look for a CB-default subnet
         # Look for a CB-default subnet
         matches = self.find(label=BaseSubnet.CB_DEFAULT_SUBNET_LABEL)
         matches = self.find(label=BaseSubnet.CB_DEFAULT_SUBNET_LABEL)
         if matches:
         if matches:
@@ -217,7 +229,7 @@ class BaseSubnetService(
         # No provider-default Subnet exists, try to create it (net + subnets)
         # No provider-default Subnet exists, try to create it (net + subnets)
         network = self.provider.networking.networks.get_or_create_default()
         network = self.provider.networking.networks.get_or_create_default()
         subnet = self.create(BaseSubnet.CB_DEFAULT_SUBNET_LABEL, network,
         subnet = self.create(BaseSubnet.CB_DEFAULT_SUBNET_LABEL, network,
-                             default_cidr, zone)
+                             BaseSubnet.CB_DEFAULT_SUBNET_IPV4RANGE, zone)
         return subnet
         return subnet
 
 
 
 

+ 2 - 1
cloudbridge/cloud/factory.py

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

+ 4 - 2
cloudbridge/cloud/interfaces/resources.py

@@ -460,7 +460,7 @@ class ResultList(list):
         """
         """
         Indicate whether this ``ResultList`` supports server side paging.
         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
         paging and the data property provides direct access to all available
         data.
         data.
         """
         """
@@ -2063,10 +2063,12 @@ class VMFirewallRuleContainer(PageableObjectMixin):
 
 
         .. code-block:: python
         .. code-block:: python
             from cloudbridge.cloud.interfaces.resources import TrafficDirection
             from cloudbridge.cloud.interfaces.resources import TrafficDirection
+            from cloudbridge.cloud.interfaces.resources import BaseNetwork
 
 
             fw = provider.security.vm_firewalls.get('my_fw_id')
             fw = provider.security.vm_firewalls.get('my_fw_id')
             fw.rules.create(TrafficDirection.INBOUND, protocol='tcp',
             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.INBOUND, src_dest_fw=fw)
             fw.rules.create(TrafficDirection.OUTBOUND, src_dest_fw=fw)
             fw.rules.create(TrafficDirection.OUTBOUND, src_dest_fw=fw)
 
 

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

@@ -643,7 +643,7 @@ class NetworkService(PageableObjectMixin, CloudService):
                            subnets you create fall within this initially
                            subnets you create fall within this initially
                            specified range. Note that the block size should be
                            specified range. Note that the block size should be
                            between a /16 netmask (65,536 IP addresses) and /28
                            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`
         :rtype: ``object`` of :class:`.Network`
         :return:  A Network object
         :return:  A Network object
@@ -1198,7 +1198,7 @@ class VMTypeService(PageableObjectMixin, CloudService):
     @abstractmethod
     @abstractmethod
     def find(self, **kwargs):
     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
         Supported attributes: name
 
 

+ 16 - 4
cloudbridge/cloud/providers/aws/services.py

@@ -1,4 +1,5 @@
 """Services implemented by the AWS provider."""
 """Services implemented by the AWS provider."""
+import ipaddress
 import logging
 import logging
 import string
 import string
 
 
@@ -747,7 +748,7 @@ class AWSNetworkService(BaseNetworkService):
                      AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
                      AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
             return self.provider.networking.networks.create(
             return self.provider.networking.networks.create(
                 label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL,
                 label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL,
-                cidr_block='10.0.0.0/16')
+                cidr_block=AWSNetwork.CB_DEFAULT_IPV4RANGE)
 
 
 
 
 class AWSSubnetService(BaseSubnetService):
 class AWSSubnetService(BaseSubnetService):
@@ -867,12 +868,23 @@ class AWSSubnetService(BaseSubnetService):
         # Create a subnet in each of the region's zones
         # Create a subnet in each of the region's zones
         region = self.provider.compute.regions.get(self.provider.region_name)
         region = self.provider.compute.regions.get(self.provider.region_name)
         default_sn = None
         default_sn = None
+
+        # Determine how many subnets we'll need for the default network and the
+        # number of available zones.
+        ip_net = ipaddress.ip_network(AWSNetwork.CB_DEFAULT_IPV4RANGE.decode())
+        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))):
         for i, z in reversed(list(enumerate(region.zones))):
             sn_label = "{0}-{1}".format(AWSSubnet.CB_DEFAULT_SUBNET_LABEL,
             sn_label = "{0}-{1}".format(AWSSubnet.CB_DEFAULT_SUBNET_LABEL,
                                         z.id[-1])
                                         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
             # Create a route table entry between the SN and the inet gateway
             # See note above about why this is commented
             # See note above about why this is commented
             # default_router.attach_subnet(sn)
             # default_router.attach_subnet(sn)

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

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

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

@@ -0,0 +1,139 @@
+# based on http://stackoverflow.com/a/39126754
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization as crypt_serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+from googleapiclient.errors import HttpError
+
+import tenacity
+
+from cloudbridge.cloud.interfaces.exceptions import ProviderInternalException
+
+
+def gce_projects(provider):
+    return provider.gce_compute.projects()
+
+
+def generate_key_pair():
+    key_pair = rsa.generate_private_key(
+        backend=default_backend(),
+        public_exponent=65537,
+        key_size=2048)
+    private_key = key_pair.private_bytes(
+        crypt_serialization.Encoding.PEM,
+        crypt_serialization.PrivateFormat.PKCS8,
+        crypt_serialization.NoEncryption())
+    public_key = key_pair.public_key().public_bytes(
+        crypt_serialization.Encoding.OpenSSH,
+        crypt_serialization.PublicFormat.OpenSSH)
+    return private_key.decode(), public_key.decode()
+
+
+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)
+
+
+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

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

@@ -0,0 +1,361 @@
+"""
+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.environ.get('GCE_SERVICE_CREDS_FILE'))
+        self.credentials_dict = self._get_config_value(
+                'gce_service_creds_dict',
+                json.loads(os.getenv('GCE_SERVICE_CREDS_DICT', '{}')))
+        # 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:
+            cb.log.warning(
+                "googleapiclient.errors.HttpError: {0}".format(http_error))
+            return None
+
+    def authenticate(self):
+        try:
+            self.gce_compute
+            return True
+        except Exception as e:
+            raise ProviderConnectionException(
+                'Authentication with Google cloud provider failed: %s', e)

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

@@ -0,0 +1,2476 @@
+"""
+DataTypes used by this provider
+"""
+import base64
+import calendar
+import hashlib
+import inspect
+import io
+import math
+import time
+import uuid
+
+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
+
+
+class GCEKeyPair(BaseKeyPair):
+
+    def __init__(self, provider, kp_id, kp_name, private_key=None):
+        super(GCEKeyPair, self).__init__(provider, None)
+        self._kp_id = kp_id
+        self._kp_name = kp_name
+        self._private_key = private_key
+
+    @property
+    def id(self):
+        return self._kp_id
+
+    @property
+    def name(self):
+        return self._kp_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):
+    DEFAULT_NETWORK = 'default'
+    _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 GCEFirewallsDelegate.DEFAULT_NETWORK
+        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.
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return False
+        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
+        try:
+            response = (self._provider
+                            .gce_compute
+                            .firewalls()
+                            .delete(project=project_name,
+                                    firewall=firewall['name'])
+                            .execute())
+            self._provider.wait_for_operation(response)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return False
+        # 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_by_name(
+                    GCEFirewallsDelegate.DEFAULT_NETWORK)
+        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()
+
+    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
+
+    @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)
+        request_body = {
+            'labels': {'cblabel': value.replace(' ', '_').lower()},
+            'labelFingerprint': self._gce_image.get('labelFingerprint'),
+        }
+        try:
+            (self._provider
+                 .gce_compute
+                 .images()
+                 .setLabels(project=self._provider.project_name,
+                            resource=self.name,
+                            body=request_body)
+                 .execute())
+        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)
+        request_body = {
+            'labels': {'cblabel': value.replace(' ', '_').lower()},
+            'labelFingerprint': self._gce_instance.get('labelFingerprint'),
+        }
+        try:
+            (self._provider
+                 .gce_compute
+                 .instances()
+                 .setLabels(project=self._provider.project_name,
+                            zone=self._provider.default_zone,
+                            instance=self.name,
+                            body=request_body)
+                 .execute())
+        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.
+        """
+        response = None
+        if self.state == InstanceState.STOPPED:
+            response = (self._provider
+                            .gce_compute
+                            .instances()
+                            .start(project=self._provider.project_name,
+                                   zone=self.zone_name,
+                                   instance=self.name)
+                            .execute())
+        else:
+            response = (self._provider
+                            .gce_compute
+                            .instances()
+                            .reset(project=self._provider.project_name,
+                                   zone=self.zone_name,
+                                   instance=self.name)
+                            .execute())
+        self._provider.wait_for_operation(response, zone=self.zone_name)
+
+    def delete(self):
+        """
+        Permanently terminate this instance.
+        """
+        name = self.name
+        response = (self._provider
+                        .gce_compute
+                        .instances()
+                        .delete(project=self._provider.project_name,
+                                zone=self.zone_name,
+                                instance=name)
+                        .execute())
+        self._provider.wait_for_operation(response, zone=self.zone_name)
+        self._gce_instance = {'name': name, 'status': 'UNKNOWN'}
+
+    def stop(self):
+        """
+        Stop this instance.
+        """
+        response = (self._provider
+                        .gce_compute
+                        .instances()
+                        .stop(project=self._provider.project_name,
+                              zone=self.zone_name,
+                              instance=self.name)
+                        .execute())
+        self._provider.wait_for_operation(response, zone=self.zone_name)
+
+    @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.
+        For GCE, since keys apply to all instances, return first
+        key in metadata.
+        """
+        try:
+            kp = next(iter(self._provider.security.key_pairs))
+            return kp.id if kp else None
+        except StopIteration:
+            return None
+
+    @key_pair_id.setter
+    # pylint:disable=arguments-differ
+    def key_pair_id(self, value):
+        self.assert_valid_resource_label(value)
+        key_pair = None
+        if not isinstance(value, GCEKeyPair):
+            key_pair = self._provider.security.key_pairs.get(value)
+        if key_pair:
+            key_pair_name = key_pair.name
+        kp = None
+        for kpi in self._provider.security.key_pairs._iter_gce_key_pairs(
+                self._provider):
+            if kpi.email == key_pair_name:
+                kp = kpi
+                break
+        if kp:
+            kp_items = [{
+                "key": "ssh-keys",
+                # FIXME: ssh username & key format are fixed here while they
+                # should correspond to the operating system, or be customizable
+                "value": "ubuntu:ssh-rsa {0} {1}".format(kp.public_key,
+                                                         kp.email)
+            }]
+            config = {
+                "items": kp_items,
+                "fingerprint": self._gce_instance['metadata']['fingerprint']
+            }
+            try:
+                (self._provider
+                    .gce_compute
+                    .instances()
+                    .setMetadata(project=self._provider.project_name,
+                                 zone=self._provider.default_zone,
+                                 instance=self.name,
+                                 body=config)
+                    .execute())
+            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 corrsponding 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 label(self):
+    #     return self._network.get('description')
+
+    # @label.setter
+    # # pylint:disable=arguments-differ
+    # def label(self, value):
+    #     self.assert_valid_resource_label(value)
+    #     body = {'description': value}
+    #     response = (self._provider
+    #                 .gce_compute
+    #                 .networks()
+    #                 .patch(project=self._provider.project_name,
+    #                        network=self.name,
+    #                        body=body)
+    #                 .execute())
+    #     self._provider.wait_for_operation(response)
+    #     self._network['description'] = value
+
+#     @property
+#     def label(self):
+#         labels = self._network.get('labels')
+#         return labels.get('cblabel', '') if labels else ''
+#
+#     @label.setter
+#     # pylint:disable=arguments-differ
+#     def label(self, value):
+#         request_body = {
+#             'labels': {'cblabel': value.replace(' ', '_').lower()},
+#             'labelFingerprint': self._network.get('labelFingerprint'),
+#         }
+#         try:
+#             (self._provider
+#                  .gce_compute
+#                  .networks()
+#                  .setLabels(project=self._provider.project_name,
+#                             zone=self._provider.default_zone,
+#                             resource=self.name,
+#                             body=request_body)
+#                  .execute())
+#         except Exception as e:
+#             cb.log.warning('Exception while setting network label: %s. '
+#                            'Check for invalid characters in label. '
+#                            'Should conform to RFC1035.', e)
+#             raise e
+#         self.refresh()
+
+    @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 'status' in self._network and self._network['status'] == '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
+        try:
+            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)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return None
+
+    def create(self):
+        region_name = self._provider.region_name
+        ip_name = 'ip-{0}'.format(uuid.uuid4())
+        try:
+            response = (self._provider
+                            .gce_compute
+                            .addresses()
+                            .insert(project=self._provider.project_name,
+                                    region=region_name,
+                                    body={'name': ip_name})
+                            .execute())
+            if 'error' in response:
+                return None
+            self._provider.wait_for_operation(response, region=region_name)
+            return self.get(ip_name)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return None
+
+
+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['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['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['address'], target['kind'])
+            else:
+                cb.log.warning('Address "%s" in use by a %s',
+                               self._ip['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):
+        return self._router.get('description')
+
+    @label.setter
+    # pylint:disable=arguments-differ
+    def label(self, value):
+        self.assert_valid_resource_label(value)
+        request_body = {
+            'description': value.replace(' ', '_').lower()
+        }
+        try:
+            (self._provider
+                 .gce_compute
+                 .routers()
+                 .patch(project=self._provider.project_name,
+                        region=self.region_name,
+                        router=self.name,
+                        body=request_body)
+                 .execute())
+        except Exception as e:
+            cb.log.warning('Exception while setting router label: %s. '
+                           'Check for invalid characters in label. '
+                           'Should conform to RFC1035.', e)
+            raise e
+        self.refresh()
+
+    @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 'status' in self._router and self._router['status'] == '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):
+        response = (self._provider
+                        .gce_compute
+                        .routers()
+                        .delete(project=self._provider.project_name,
+                                region=self.region_name,
+                                router=self.name)
+                        .execute())
+        self._provider.wait_for_operation(response, region=self.region_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 'status' in self._subnet and self._subnet['status'] == '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)
+        request_body = {
+            'labels': {'cblabel': value.replace(' ', '_').lower()},
+            'labelFingerprint': self._volume.get('labelFingerprint'),
+        }
+        try:
+            (self._provider
+                 .gce_compute
+                 .disks()
+                 .setLabels(project=self._provider.project_name,
+                            zone=self._provider.default_zone,
+                            resource=self.name,
+                            body=request_body)
+                 .execute())
+        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):
+        request_body = {
+            'labels': {'description': value.replace(' ', '_').lower()},
+            'labelFingerprint': self._volume.get('labelFingerprint'),
+        }
+        try:
+            (self._provider
+                 .gce_compute
+                 .disks()
+                 .setLabels(project=self._provider.project_name,
+                            zone=self._provider.default_zone,
+                            resource=self.name,
+                            body=request_body)
+                 .execute())
+        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 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._provider.default_zone,
+                         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.
+        """
+        response = (self._provider
+                    .gce_compute
+                    .disks()
+                    .delete(project=self._provider.project_name,
+                            zone=self._provider.default_zone,
+                            disk=self.name)
+                    .execute())
+        self._provider.wait_for_operation(
+            response, zone=self._provider.default_zone)
+
+    @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)
+        request_body = {
+            'labels': {'cblabel': value.replace(' ', '_').lower()},
+            'labelFingerprint': self._snapshot.get('labelFingerprint'),
+        }
+        try:
+            (self._provider
+                 .gce_compute
+                 .snapshots()
+                 .setLabels(project=self._provider.project_name,
+                            resource=self.name,
+                            body=request_body)
+                 .execute())
+        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):
+        request_body = {
+            'labels': {'description': value.replace(' ', '_').lower()},
+            'labelFingerprint': self._snapshot.get('labelFingerprint'),
+        }
+        try:
+            (self._provider
+                 .gce_compute
+                 .snapshots()
+                 .setLabels(project=self._provider.project_name,
+                            resource=self.name,
+                            body=request_body)
+                 .execute())
+        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.
+        """
+        response = (self._provider
+                    .gce_compute
+                    .snapshots()
+                    .delete(project=self._provider.project_name,
+                            snapshot=self.name)
+                    .execute())
+        self._provider.wait_for_operation(response)
+
+    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.
+        """
+        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=0):
+        """
+        Generates a signed URL accessible to everyone.
+        """
+        expiration = calendar.timegm(time.gmtime()) + 2 * 24 * 60 * 60
+        signature = self._provider.sign_blob(
+                'GET\n\n\n%d\n/%s/%s' %
+                (expiration, self._obj['bucket'], self.name))
+        encoded_signature = base64.b64encode(signature)
+        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
+        try:
+            response = (self._provider
+                            .gcs_storage
+                            .objects()
+                            .list(bucket=self.bucket.name,
+                                  prefix=prefix if prefix else '',
+                                  maxResults=max_result,
+                                  pageToken=marker)
+                            .execute())
+            if 'error' in response:
+                return ServerPagedResultList(False, None, False, data=[])
+            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)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return ServerPagedResultList(False, None, False, data=[])
+
+    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(''), mimetype='plain/text'))
+        return GCSObject(self._provider, self, response) if response else None
+
+    def create_object_with_media_body(self, name, media_body):
+        try:
+            response = (self._provider
+                            .gcs_storage
+                            .objects()
+                            .insert(bucket=self.name,
+                                    body={'name': name},
+                                    media_body=media_body)
+                            .execute())
+            if 'error' in response:
+                return None
+            return response
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return None
+
+
+class GCELaunchConfig(BaseLaunchConfig):
+
+    def __init__(self, provider):
+        super(GCELaunchConfig, self).__init__(provider)

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

@@ -0,0 +1,1296 @@
+import hashlib
+import logging
+import time
+import uuid
+from collections import namedtuple
+
+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):
+
+    GCEKeyInfo = namedtuple('GCEKeyInfo', 'format public_key email')
+
+    def __init__(self, provider):
+        super(GCEKeyPairService, self).__init__(provider)
+
+    def _iter_gce_key_pairs(self, provider):
+        """
+        Iterates through the project's metadata, yielding a GCEKeyInfo object
+        for each entry in commonInstanceMetaData/items
+        """
+        metadata = helpers.get_common_metadata(provider)
+        for kpinfo in self._iter_gce_ssh_keys(metadata):
+            yield kpinfo
+
+    def _get_or_add_sshkey_entry(self, metadata):
+        """
+        Get the ssh-keys entry from commonInstanceMetadata/items.
+        If an entry does not exist, adds a new empty entry
+        """
+        sshkey_entry = None
+        entries = [item for item in metadata.get('items', [])
+                   if item['key'] == 'ssh-keys']
+        if entries:
+            sshkey_entry = entries[0]
+        else:  # add a new entry
+            sshkey_entry = {'key': 'ssh-keys', 'value': ''}
+            if 'items' not in metadata:
+                metadata['items'] = [sshkey_entry]
+            else:
+                metadata['items'].append(sshkey_entry)
+        return sshkey_entry
+
+    def _iter_gce_ssh_keys(self, metadata):
+        """
+        Iterates through the ssh keys given a commonInstanceMetadata dict,
+        yielding a GCEKeyInfo object for each entry in
+        commonInstanceMetaData/items
+        """
+        sshkeys = self._get_or_add_sshkey_entry(metadata)["value"]
+        for key in sshkeys.split("\n"):
+            # elems should be "ssh-rsa <public_key> <email>"
+            elems = key.split(" ")
+            if elems and elems[0]:  # ignore blank lines
+                yield GCEKeyPairService.GCEKeyInfo(
+                        elems[0], elems[1], elems[2])
+
+    def update_kps_in_metadata(self, provider, callback):
+        def _process_kps_from_metadata(metadata):
+            # add a new entry if one doesn't exist
+            sshkey_entry = self._get_or_add_sshkey_entry(metadata)
+            gce_kp_list = callback(self._iter_gce_ssh_keys(metadata))
+
+            entry = ""
+            for gce_kp in gce_kp_list:
+                entry = entry + u"{0} {1} {2}\n".format(gce_kp.format,
+                                                        gce_kp.public_key,
+                                                        gce_kp.email)
+            sshkey_entry["value"] = entry.rstrip()
+
+        helpers.gce_metadata_save_op(provider, _process_kps_from_metadata)
+
+    def gce_kp_to_id(self, gce_kp):
+        """
+        Accept a GCEKeyInfo object and return a unique
+        ID for it
+        """
+        md5 = hashlib.md5()
+        md5.update(gce_kp.public_key.encode())
+        return md5.hexdigest()
+
+    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 gce_kp in self._iter_gce_key_pairs(self.provider):
+            kp_id = self.gce_kp_to_id(gce_kp)
+            key_pairs.append(GCEKeyPair(self.provider, kp_id, gce_kp.email))
+        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.
+        """
+        kp_name = kwargs.get('name', None)
+        kp_id = kwargs.get('id', None)
+        for parameter in kwargs:
+            if parameter not in ('id', 'name'):
+                cb.log.error('Unrecognised parameters for search: %s. '
+                             'Supported attributes: id, name', parameter)
+
+        out = []
+        for kp in self:
+            if kp_name is not None and kp.name != kp_name:
+                continue
+            if kp_id is not None and kp.id != kp_id:
+                continue
+            out.append(kp)
+        return out
+
+    def create(self, name, public_key_material=None):
+        GCEKeyPair.assert_valid_resource_name(name)
+
+        if self.find(name=name):
+            raise DuplicateResourceException(
+                'A KeyPair with the same name %s exists', name)
+        private_key = None
+        if not public_key_material:
+            private_key, public_key_material = helpers.generate_key_pair()
+        parts = public_key_material.split(' ')
+        if len(parts) == 2:
+            public_key_material = parts[1]
+        kp_info = GCEKeyPairService.GCEKeyInfo(
+            '%s:ssh-rsa' % name, public_key_material, name)
+
+        def _add_kp(gce_kp_generator):
+            kp_list = []
+            # Add the new key pair
+            kp_list.append(kp_info)
+            for gce_kp in gce_kp_generator:
+                kp_list.append(gce_kp)
+            return kp_list
+
+        self.update_kps_in_metadata(self.provider, _add_kp)
+        return GCEKeyPair(self.provider, self.gce_kp_to_id(kp_info), name,
+                          private_key)
+
+    def delete(self, key_pair_id):
+
+        def _delete_key(gce_kp_generator):
+            kp_list = []
+            for gce_kp in gce_kp_generator:
+                if self.gce_kp_to_id(gce_kp) == key_pair_id:
+                    continue
+                else:
+                    kp_list.append(gce_kp)
+            return kp_list
+
+        self.update_kps_in_metadata(self.provider, _delete_key)
+
+
+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_by_name(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_by_name(
+                    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 find(self, name, limit=None, marker=None):
+        """
+        Finds a non-empty VM firewall. If a VM firewall with the given name
+        does not exist, or if it does not contain any rules, an empty list is
+        returned.
+        """
+        out = []
+        for tag, network_name in self._delegate.tag_networks:
+            if tag == name:
+                network = self.provider.networking.networks.get_by_name(
+                        network_name)
+                out.append(GCEVMFirewall(self._delegate, name, network))
+        return out
+
+    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_by_name(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.iteritems():
+                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 = []
+        try:
+            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))
+        except googleapiclient.errors.HttpError as http_error:
+                cb.log.warning("googleapiclient.errors.HttpError: {0}".format(
+                    http_error))
+
+    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):
+            try:
+                for image in helpers.iter_all(
+                        self.provider.gce_compute.images(),
+                        project=self.provider.project_name):
+                    images.append(GCEMachineImage(self.provider, image))
+            except googleapiclient.errors.HttpError as http_error:
+                cb.log.warning(
+                    "googleapiclient.errors.HttpError: {0}".format(http_error))
+        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=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
+        try:
+            operation = (self.provider
+                             .gce_compute.instances()
+                             .insert(project=self.provider.project_name,
+                                     zone=zone_name,
+                                     body=config)
+                             .execute())
+        except googleapiclient.errors.HttpError as http_error:
+            # If the operation request fails, the API will raise
+            # googleapiclient.errors.HttpError.
+            cb.log.warning(
+                "googleapiclient.errors.HttpError: {0}".format(http_error))
+            return None
+        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 get_by_name(self, network_name):
+        # Get already works with name
+        # TODO: Decide if we neet to keep this function altogether/add it
+        # everywhere?
+        if network_name:
+            return self.get(network_name)
+        else:
+            return None
+
+    def list(self, limit=None, marker=None, filter=None):
+        # TODO: Decide whether we keep filter in 'list'
+        networks = []
+        try:
+            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))
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+        return ClientPagedResultList(self.provider, networks,
+                                     limit=limit, marker=marker)
+
+    def _create(self, label, cidr_block, create_subnetworks):
+        """
+        Possible values for 'create_subnetworks' are:
+
+        None: For creating a legacy (non-subnetted) network.
+        True: For creating an auto mode VPC network. This also creates a
+              subnetwork in every region.
+        False: For creating a custom mode VPC network. Subnetworks should be
+               created manually.
+        """
+        GCENetwork.assert_valid_resource_label(label)
+        if create_subnetworks is not None and cidr_block is not None:
+            cb.log.warning('cidr_block is ignored in non-legacy networks. '
+                           'Auto mode networks use the default CIDR of '
+                           '%s. For custom networks, you should create subnets'
+                           'in each region with explicit CIDR blocks',
+                           GCENetwork.DEFAULT_IPV4RANGE)
+            cidr_block = None
+        name = GCENetwork._generate_name_from_label(label, 'cbnet')
+        body = {'name': name}
+        if cidr_block:
+            body['IPv4Range'] = cidr_block
+        else:
+            body['autoCreateSubnetworks'] = create_subnetworks
+        try:
+            response = (self.provider
+                            .gce_compute
+                            .networks()
+                            .insert(project=self.provider.project_name,
+                                    body=body)
+                            .execute())
+            if 'error' in response:
+                return None
+            self.provider.wait_for_operation(response)
+            return self.get(name)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return None
+
+    def create(self, label, cidr_block):
+        """
+        Creates an auto mode VPC network with default subnets. It is possible
+        to add additional subnets later.
+        """
+        cb_net = self._create(label, cidr_block, False)
+        cb_net.label = label
+        return cb_net
+
+    def get_or_create_default(self):
+        return self._create(GCEFirewallsDelegate.DEFAULT_NETWORK, None, True)
+
+    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
+        try:
+            response = (self.provider
+                            .gce_compute
+                            .networks()
+                            .delete(project=self.provider.project_name,
+                                    network=name)
+                            .execute())
+            if 'error' in response:
+                return False
+            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))
+        except googleapiclient.errors.HttpError as http_error:
+            log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return False
+        return True
+
+
+class GCERouterService(BaseRouterService):
+
+    def __init__(self, provider):
+        super(GCERouterService, self).__init__(provider)
+
+    def get(self, router_id):
+        return self._get_in_region(router_id)
+
+    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
+        try:
+            response = (self.provider
+                            .gce_compute
+                            .routers()
+                            .insert(project=self.provider.project_name,
+                                    region=region_name,
+                                    body={'name': name,
+                                          'network': network_url,
+                                          'description': label})
+                            .execute())
+            if 'error' in response:
+                return None
+            self.provider.wait_for_operation(response, region=region_name)
+            return self._get_in_region(name, region_name)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return None
+
+    def delete(self, router):
+        region_name = self.provider.region_name
+        name = router.name if isinstance(router, GCERouter) else router
+        response = (self.provider
+                        .gce_compute
+                        .routers()
+                        .delete(project=self.provider.project_name,
+                                region=region_name,
+                                router=name)
+                        .execute())
+        self._provider.wait_for_operation(response, region=region_name)
+
+    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.list():
+                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
+                }
+        try:
+            response = (self.provider
+                            .gce_compute
+                            .subnetworks()
+                            .insert(project=self.provider.project_name,
+                                    region=region_name,
+                                    body=body)
+                            .execute())
+            if 'error' in response:
+                cb.log.warning('Error while creating a subnet: %s',
+                               response['error'])
+                return None
+            self.provider.wait_for_operation(response, region=region_name)
+            cb_subnet = self.get(name)
+            cb_subnet.label = label
+            return cb_subnet
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return None
+
+    def get_or_create_default(self, zone=None):
+        """
+        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.
+        """
+        network = self.provider.networking.networks.get_or_create_default()
+        subnets = list(self.iter(network=network, zone=zone))
+        if len(subnets) > 1:
+            cb.log.warning('The default network has more than one subnetwork '
+                           'in a region')
+        if len(subnets) > 0:
+            return subnets[0]
+        cb.log.warning('The default network has no subnetwork in a region')
+        return None
+
+    def delete(self, subnet):
+        try:
+            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)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+
+    def _zone_to_region_name(self, zone):
+        if zone:
+            if not isinstance(zone, GCEPlacementZone):
+                zone = GCEPlacementZone(
+                    self.provider,
+                    self.provider.get_resource('zones', zone, zone=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=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
+        try:
+            response = (self.provider
+                            .gcs_storage
+                            .buckets()
+                            .list(project=self.provider.project_name,
+                                  maxResults=max_result,
+                                  pageToken=marker)
+                            .execute())
+            if 'error' in response:
+                return ServerPagedResultList(False, None, False, data=[])
+            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)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return ServerPagedResultList(False, None, False, data=[])
+
+    def create(self, name, location=None):
+        """
+        Create a new bucket and returns it. Returns None if creation fails.
+        """
+        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())
+            if 'error' in response:
+                return None
+            # 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:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
+            return None

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

@@ -1203,7 +1203,7 @@ class OpenStackKeyPair(BaseKeyPair):
 
 
 
 
 class OpenStackVMFirewall(BaseVMFirewall):
 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):
     def __init__(self, provider, vm_firewall):
         super(OpenStackVMFirewall, self).__init__(provider, vm_firewall)
         super(OpenStackVMFirewall, self).__init__(provider, vm_firewall)
@@ -1212,15 +1212,23 @@ class OpenStackVMFirewall(BaseVMFirewall):
     @property
     @property
     def network_id(self):
     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
         # Best way would be to use regex, but using this hacky way to avoid
         # importing the re package
         # 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\
         net_id = self._description\
                      .split(" [{}".format(self._network_id_tag))[-1]\
                      .split(" [{}".format(self._network_id_tag))[-1]\
                      .split(']')[0]
                      .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
         return net_id
 
 
     @property
     @property

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

@@ -218,30 +218,22 @@ class OpenStackVMFirewallService(BaseVMFirewallService):
                   "[label: %s network id: %s description: %s]", label,
                   "[label: %s network id: %s description: %s]", label,
                   network, description)
                   network, description)
         net = network.id if isinstance(network, Network) else network
         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:
         if not description:
             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(
         sg = self.provider.os_conn.network.create_security_group(
-            name=label, description=description or label)
+            name=label, description=description)
         if sg:
         if sg:
             return OpenStackVMFirewall(self.provider, sg)
             return OpenStackVMFirewall(self.provider, sg)
         return None
         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):
     def delete(self, group_id):
         log.debug("Deleting OpenStack Firewall with the id: %s", group_id)
         log.debug("Deleting OpenStack Firewall with the id: %s", group_id)
         firewall = self.get(group_id)
         firewall = self.get(group_id)
@@ -884,7 +876,7 @@ class OpenStackSubnetService(BaseSubnetService):
             net = self.provider.networking.networks.get_or_create_default()
             net = self.provider.networking.networks.get_or_create_default()
             sn = self.provider.networking.subnets.create(
             sn = self.provider.networking.subnets.create(
                 label=OpenStackSubnet.CB_DEFAULT_SUBNET_LABEL,
                 label=OpenStackSubnet.CB_DEFAULT_SUBNET_LABEL,
-                cidr_block='10.0.0.0/24',
+                cidr_block=OpenStackSubnet.CB_DEFAULT_SUBNET_IPV4RANGE,
                 network=net)
                 network=net)
             router = self.provider.networking.routers.get_or_create_default(
             router = self.provider.networking.routers.get_or_create_default(
                 net)
                 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'}
               'os_user_domain_name': 'domain name'}
     provider = CloudProviderFactory().create_provider(ProviderList.OPENSTACK,
     provider = CloudProviderFactory().create_provider(ProviderList.OPENSTACK,
                                                       config)
                                                       config)
-    image_id = 'acb53109-941f-4593-9bf8-4a53cb9e0739'  # Ubuntu 16.04 @ Jetstream
+    image_id = '470d2fba-d20b-47b0-a89a-ab725cd09f8b'  # Ubuntu 18.04@Jetstream
 
 
 Azure:
 Azure:
 
 
@@ -79,6 +79,18 @@ Azure:
     provider = CloudProviderFactory().create_provider(ProviderList.AZURE, config)
     provider = CloudProviderFactory().create_provider(ProviderList.AZURE, config)
     image_id = 'Canonical:UbuntuServer:16.04.0-LTS:latest'  # Ubuntu 16.04
     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
 List some resources
 -------------------
 -------------------
@@ -86,14 +98,14 @@ Once you have a reference to a provider, explore the cloud platform:
 
 
 .. code-block:: python
 .. code-block:: python
 
 
-    provider.security.firewalls.list()
+    provider.security.vm_firewalls.list()
     provider.compute.vm_types.list()
     provider.compute.vm_types.list()
     provider.storage.snapshots.list()
     provider.storage.snapshots.list()
     provider.storage.buckets.list()
     provider.storage.buckets.list()
 
 
 This will demonstrate the fact that the library was properly installed and your
 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
 Create a key pair
 -----------------
 -----------------
@@ -103,8 +115,8 @@ on disk as a read-only file.
 .. code-block:: python
 .. code-block:: python
 
 
     import os
     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)
         f.write(kp.material)
     os.chmod('cloudbridge_intro.pem', 0o400)
     os.chmod('cloudbridge_intro.pem', 0o400)
 
 
@@ -117,9 +129,11 @@ attaching an internet gateway to the subnet via a router.
 .. code-block:: python
 .. code-block:: python
 
 
     net = provider.networking.networks.create(cidr_block='10.0.0.0/16',
     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)
     router.attach_subnet(sn)
     gateway = net.gateways.get_or_create_inet_gateway()
     gateway = net.gateways.get_or_create_inet_gateway()
     router.attach_gateway(gateway)
     router.attach_gateway(gateway)
@@ -135,8 +149,8 @@ a private network.
 
 
     from cloudbridge.cloud.interfaces.resources import TrafficDirection
     from cloudbridge.cloud.interfaces.resources import TrafficDirection
     fw = provider.security.vm_firewalls.create(
     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')
     fw.rules.create(TrafficDirection.INBOUND, 'tcp', 22, 22, '0.0.0.0/0')
 
 
 Launch an instance
 Launch an instance
@@ -148,12 +162,11 @@ also add the network interface as a launch argument.
 .. code-block:: python
 .. code-block:: python
 
 
     img = provider.compute.images.get(image_id)
     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
     vm_type = sorted([t for t in provider.compute.vm_types
                       if t.vcpus >= 2 and t.ram >= 4],
                       if t.vcpus >= 2 and t.ram >= 4],
                       key=lambda x: x.vcpus*x.ram)[0]
                       key=lambda x: x.vcpus*x.ram)[0]
     inst = provider.compute.instances.create(
     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])
         subnet=sn, zone=zone, key_pair=kp, vm_firewalls=[fw])
     # Wait until ready
     # Wait until ready
     inst.wait_till_ready()  # This is a blocking call
     inst.wait_till_ready()  # This is a blocking call
@@ -180,9 +193,10 @@ earlier.
 
 
 .. code-block:: python
 .. 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
     inst.public_ips
     # [u'54.166.125.219']
     # [u'54.166.125.219']
 
 
@@ -206,8 +220,7 @@ their provider mappings, see :doc:`topics/resource_types_and_mappings`.
 
 
     # Key Pair
     # Key Pair
     kp = provider.security.key_pairs.get('keypair ID')
     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
     # Floating IPs
     fip = gateway.floating_ips.get('FloatingIP ID')
     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')
     fip_list = gateway.floating_ips.find(public_ip='IP address')
     # Find using name (the behavior of the `name` property can be 
     # 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>`
     # 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
     # Network
     net = provider.networking.networks.get('network ID')
     net = provider.networking.networks.get('network ID')
@@ -226,15 +238,15 @@ their provider mappings, see :doc:`topics/resource_types_and_mappings`.
     # Subnet
     # Subnet
     sn = provider.networking.subnets.get('subnet ID')
     sn = provider.networking.subnets.get('subnet ID')
     # Unknown network
     # Unknown network
-    sn_list = provider.networking.subnets.find(label='my-subnet')
+    sn_list = provider.networking.subnets.find(label='cb-subnet')
     # Known network
     # Known network
     sn_list = provider.networking.subnets.find(network=net.id,
     sn_list = provider.networking.subnets.find(network=net.id,
-                                               label='my-subnet')
+                                               label='cb-subnet')
     sn = sn_list(0)
     sn = sn_list(0)
 
 
     # Router
     # Router
     router = provider.networking.routers.get('router ID')
     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]
     router = router_list[0]
 
 
     # Gateway
     # Gateway
@@ -242,12 +254,12 @@ their provider mappings, see :doc:`topics/resource_types_and_mappings`.
 
 
     # Firewall
     # Firewall
     fw = provider.security.vm_firewalls.get('firewall ID')
     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]
     fw = fw_list[0]
 
 
     # Instance
     # Instance
     inst = provider.compute.instances.get('instance ID')
     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]
     inst = inst_list[0]
 
 
 
 

+ 4 - 2
docs/topics/networking.rst

@@ -66,8 +66,10 @@ internet gateway.
 
 
 When creating a network, we need to set an address pool. Any subsequent
 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
 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
 .. code-block:: python
 
 

+ 81 - 26
docs/topics/setup.rst

@@ -9,10 +9,44 @@ be provided in one of following ways:
 3. Configuration file
 3. Configuration file
 
 
 Procuring access credentials
 Procuring access credentials
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-For Azure, Create service principle credentials from the following link : 
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+**Microsoft Azure**
+
+For Microsoft Azure, create service principle credentials following
+instructions from the link below:
 https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal#check-azure-subscription-permissions
 https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal#check-azure-subscription-permissions
 
 
+**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"
+    }
+
 
 
 Providing access credentials through environment variables
 Providing access credentials through environment variables
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -39,7 +73,7 @@ OS_PROJECT_NAME      OS_STORAGE_URL
 OS_REGION_NAME       OS_AUTH_TOKEN
 OS_REGION_NAME       OS_AUTH_TOKEN
 ===================  ==================
 ===================  ==================
 
 
-**Azure**
+**Microsoft Azure**
 
 
 Note that managing resources in Azure requires a Resource Group. If a
 Note that managing resources in Azure requires a Resource Group. If a
 Resource Group is not provided as part of the configuration, cloudbridge will
 Resource Group is not provided as part of the configuration, cloudbridge will
@@ -63,6 +97,19 @@ AZURE_TENANT            AZURE_VM_DEFAULT_USER_NAME
                         AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
                         AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
 ======================  ==================
 ======================  ==================
 
 
+
+**Google**
+
+=======================  ==================
+Mandatory variables      Optional Variables
+=======================  ==================
+GCE_SERVICE_CREDS_FILE   GCE_PROJECT_NAME
+           or            GCE_DEFAULT_ZONE
+GCE_SERVICE_CREDS_DICT   GCE_REGION_NAME
+=======================  ==================
+
+
+
 Once the environment variables are set, you can create a connection as follows:
 Once the environment variables are set, you can create a connection as follows:
 
 
 .. code-block:: python
 .. code-block:: python
@@ -138,10 +185,11 @@ Providing access credentials in a file
 CloudBridge can also read credentials from a file on your local file system.
 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
 The file should be placed in one of two locations: ``/etc/cloudbridge.ini`` or
 ``~/.cloudbridge``. Each set of credentials should be delineated with the
 ``~/.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
 .. code-block:: bash
 
 
@@ -164,22 +212,29 @@ In addition to the provider specific configuration variables above, there are
 some general configuration environment variables that apply to CloudBridge as
 some general configuration environment variables that apply to CloudBridge as
 a whole
 a whole
 
 
-======================== ======================================================
-Variable		                            Description
-======================== ======================================================
-CB_DEBUG                 Setting ``CB_DEBUG=True`` will cause detailed debug
-                         output to be printed for each provider (including HTTP
-                         traces).
-CB_USE_MOCK_PROVIDERS    Setting this to ``True`` will cause the CloudBridge
-                         test suite to use mock drivers when available.
-CB_TEST_PROVIDER         Set this value to a valid :class:`.ProviderList` value
-                         such as ``aws``, to limit tests to that provider only.
-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 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.
+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.
+=========================== ===================================================

+ 2 - 1
setup.py

@@ -40,6 +40,7 @@ REQS_AZURE = ['msrest>=0.5.4,<0.6',
               'azure-storage-blob==1.3.1',
               'azure-storage-blob==1.3.1',
               'azure-cosmosdb-table==1.0.4',
               'azure-cosmosdb-table==1.0.4',
               'pysftp==0.2.9']
               'pysftp==0.2.9']
+REQS_GCP = ['google-api-python-client', 'oauth2client']
 REQS_OPENSTACK = [
 REQS_OPENSTACK = [
     'openstacksdk>=0.12.0,<=0.17',
     'openstacksdk>=0.12.0,<=0.17',
     'python-novaclient>=7.0.0,<=11.0',
     'python-novaclient>=7.0.0,<=11.0',
@@ -49,7 +50,7 @@ REQS_OPENSTACK = [
     'python-neutronclient>=6.0.0,<=6.9',
     'python-neutronclient>=6.0.0,<=6.9',
     'python-keystoneclient>=3.13.0,<=3.17'
     '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
 # httpretty is required with/for moto 1.0.0 or AWS tests fail
 REQS_DEV = ([
 REQS_DEV = ([
     'tox>=2.1.1',
     'tox>=2.1.1',

+ 11 - 2
test/helpers/__init__.py

@@ -85,12 +85,19 @@ TEST_DATA_CONFIG = {
         "vm_type": get_env('CB_VM_TYPE_AWS', 't2.nano'),
         "vm_type": get_env('CB_VM_TYPE_AWS', 't2.nano'),
         "placement": get_env('CB_PLACEMENT_AWS', 'us-east-1a'),
         "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'),
                                 'c66bdfa1-62b1-43be-8964-e9ce208ac6a5'),
         "vm_type": os.environ.get('CB_VM_TYPE_OS', 'm1.tiny'),
         "vm_type": os.environ.get('CB_VM_TYPE_OS', 'm1.tiny'),
         "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
         "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": {
     "AzureCloudProvider": {
         "placement":
         "placement":
             get_env('CB_PLACEMENT_AZURE', 'eastus'),
             get_env('CB_PLACEMENT_AZURE', 'eastus'),
@@ -108,6 +115,8 @@ def get_provider_test_data(provider, key):
         return TEST_DATA_CONFIG.get("AWSCloudProvider").get(key)
         return TEST_DATA_CONFIG.get("AWSCloudProvider").get(key)
     elif "OpenStackCloudProvider" in provider.name:
     elif "OpenStackCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("OpenStackCloudProvider").get(key)
         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:
     elif "AzureCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("AzureCloudProvider").get(key)
         return TEST_DATA_CONFIG.get("AzureCloudProvider").get(key)
     return None
     return None

+ 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):
 def check_get_non_existent(test, service):
     # check get
     # check get
-    get_objs = service.get(str(uuid.uuid4()))
+    get_objs = service.get('tmp-' + str(uuid.uuid4()))
     test.assertIsNone(
     test.assertIsNone(
         get_objs,
         get_objs,
         "Get non-existent object for %s returned unexpected objects: %s"
         "Get non-existent object for %s returned unexpected objects: %s"

+ 2 - 1
test/test_block_store_service.py

@@ -119,7 +119,8 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                 self.assertEqual(test_vol.attachments.volume, test_vol)
                 self.assertEqual(test_vol.attachments.volume, test_vol)
                 self.assertEqual(test_vol.attachments.instance_id,
                 self.assertEqual(test_vol.attachments.instance_id,
                                  test_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,
                     self.assertEqual(test_vol.attachments.device,
                                      "/dev/sda2")
                                      "/dev/sda2")
                 test_vol.detach()
                 test_vol.detach()

+ 20 - 10
test/test_compute_service.py

@@ -2,6 +2,7 @@ import ipaddress
 
 
 import six
 import six
 
 
+from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import InvalidConfigurationException
 from cloudbridge.cloud.interfaces import InvalidConfigurationException
@@ -83,7 +84,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             net = subnet.network
             net = subnet.network
             kp = self.provider.security.key_pairs.create(name=label)
             kp = self.provider.security.key_pairs.create(name=label)
             fw = self.provider.security.vm_firewalls.create(
             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,
             test_instance = helpers.get_test_instance(self.provider,
                                                       label, key_pair=kp,
                                                       label, key_pair=kp,
                                                       vm_firewalls=[fw],
                                                       vm_firewalls=[fw],
@@ -276,7 +277,12 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                     "vm_type")
                     "vm_type")
                 vm_type = self.provider.compute.vm_types.find(
                 vm_type = self.provider.compute.vm_types.find(
                     name=vm_type_name)[0]
                     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()
                     lc.add_ephemeral_device()
 
 
                 subnet = helpers.get_or_create_default_subnet(
                 subnet = helpers.get_or_create_default_subnet(
@@ -312,7 +318,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 instance=test_inst, vm_firewall=fw, network=net)):
                 instance=test_inst, vm_firewall=fw, network=net)):
             net = self.provider.networking.networks.create(
             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'
             cidr = '10.0.1.0/24'
             subnet = net.create_subnet(label=label, cidr_block=cidr,
             subnet = net.create_subnet(label=label, cidr_block=cidr,
                                        zone=helpers.get_provider_test_data(
                                        zone=helpers.get_provider_test_data(
@@ -321,7 +327,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             test_inst = helpers.get_test_instance(self.provider, label,
             test_inst = helpers.get_test_instance(self.provider, label,
                                                   subnet=subnet)
                                                   subnet=subnet)
             fw = self.provider.security.vm_firewalls.create(
             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
             # Check adding a VM firewall to a running instance
             test_inst.add_vm_firewall(fw)
             test_inst.add_vm_firewall(fw)
@@ -353,13 +359,16 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                                                                gateway)):
                                                                gateway)):
                 router.attach_subnet(subnet)
                 router.attach_subnet(subnet)
                 router.attach_gateway(gateway)
                 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: fip.delete()):
                 with helpers.cleanup_action(lambda: fip.delete()):
+                    # 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(
                     with helpers.cleanup_action(
                             lambda: test_inst.remove_floating_ip(fip)):
                             lambda: test_inst.remove_floating_ip(fip)):
                         test_inst.add_floating_ip(fip)
                         test_inst.add_floating_ip(fip)
@@ -370,7 +379,8 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                         fip.refresh()
                         fip.refresh()
                         self.assertTrue(
                         self.assertTrue(
                             fip.in_use,
                             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.refresh()
                     test_inst.reboot()
                     test_inst.reboot()
                     test_inst.wait_till_ready()
                     test_inst.wait_till_ready()

+ 2 - 1
test/test_image_service.py

@@ -1,5 +1,6 @@
 from cloudbridge.cloud.interfaces import MachineImageState
 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 import helpers
 from test.helpers import ProviderTestBase
 from test.helpers import ProviderTestBase

+ 2 - 0
test/test_interface.py

@@ -62,6 +62,8 @@ class CloudInterfaceTestCase(ProviderTestBase):
             cloned_config['os_password'] = "cb_dummy"
             cloned_config['os_password'] = "cb_dummy"
         elif self.provider.PROVIDER_ID == 'azure':
         elif self.provider.PROVIDER_ID == 'azure':
             cloned_config['azure_subscription_id'] = "cb_dummy"
             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):
         with self.assertRaises(ProviderConnectionException):
             cloned_provider = CloudProviderFactory().create_provider(
             cloned_provider = CloudProviderFactory().create_provider(

+ 23 - 19
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 FloatingIP
 from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.resources import Network
 from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import NetworkState
@@ -20,7 +21,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
 
         def create_net(label):
         def create_net(label):
             return self.provider.networking.networks.create(
             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):
         def cleanup_net(net):
             if net:
             if net:
@@ -40,10 +41,8 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         label = 'cb-propnetwork-{0}'.format(helpers.get_uuid())
         label = 'cb-propnetwork-{0}'.format(helpers.get_uuid())
         subnet_label = 'cb-propsubnet-{0}'.format(helpers.get_uuid())
         subnet_label = 'cb-propsubnet-{0}'.format(helpers.get_uuid())
         net = self.provider.networking.networks.create(
         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: net.delete()):
             net.wait_till_ready()
             net.wait_till_ready()
             self.assertEqual(
             self.assertEqual(
                 net.state, 'available',
                 net.state, 'available',
@@ -52,9 +51,9 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             sit.check_repr(self, net)
             sit.check_repr(self, net)
 
 
             self.assertIn(
             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'
             cidr = '10.0.20.0/24'
             sn = net.create_subnet(
             sn = net.create_subnet(
@@ -89,7 +88,11 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
 
                 self.assertEqual(
                 self.assertEqual(
                     cidr, sn.cidr_block,
                     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))
                         sn.cidr_block, cidr))
 
 
     def test_crud_subnet(self):
     def test_crud_subnet(self):
@@ -179,7 +182,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         gteway = None
         gteway = None
         with helpers.cleanup_action(lambda: _cleanup(net, sn, router, gteway)):
         with helpers.cleanup_action(lambda: _cleanup(net, sn, router, gteway)):
             net = self.provider.networking.networks.create(
             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,
             router = self.provider.networking.routers.create(label=label,
                                                              network=net)
                                                              network=net)
             cidr = '10.0.15.0/24'
             cidr = '10.0.15.0/24'
@@ -190,15 +193,16 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             # Check basic router properties
             # Check basic router properties
             sit.check_standard_behaviour(
             sit.check_standard_behaviour(
                 self, self.provider.networking.routers, router)
                 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))
+            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.assertFalse(
+                    router.network_id,
+                    "Router {0} should not be assoc. with network {1}".format(
+                            router.id, router.network_id))
 
 
             self.assertTrue(
             self.assertTrue(
                 len(router.subnets) == 0,
                 len(router.subnets) == 0,

+ 5 - 15
test/test_security_service.py

@@ -92,7 +92,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
             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)
             self.assertEqual(label, fw.description)
 
 
@@ -106,7 +106,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         fw = None
         fw = None
         with helpers.cleanup_action(lambda: fw.delete()):
         with helpers.cleanup_action(lambda: fw.delete()):
             fw = self.provider.security.vm_firewalls.create(
             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):
             def create_fw_rule(label):
                 return fw.rules.create(
                 return fw.rules.create(
@@ -133,7 +133,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
             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(
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, protocol='tcp',
                 direction=TrafficDirection.INBOUND, protocol='tcp',
@@ -157,7 +157,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
             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(
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, protocol='tcp',
                 direction=TrafficDirection.INBOUND, protocol='tcp',
@@ -180,17 +180,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
             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(
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, src_dest_fw=fw,
                 direction=TrafficDirection.INBOUND, src_dest_fw=fw,
                 protocol='tcp', from_port=1, to_port=65535)
                 protocol='tcp', from_port=1, to_port=65535)

+ 3 - 1
tox.ini

@@ -12,7 +12,7 @@
 # mock providers.
 # mock providers.
 
 
 [tox]
 [tox]
-envlist = {py27,py36,pypy}-{aws,azure,openstack}
+envlist = {py27,py36,pypy}-{aws,azure,gce,openstack}
 
 
 [testenv]
 [testenv]
 commands = flake8 cloudbridge test setup.py
 commands = flake8 cloudbridge test setup.py
@@ -22,11 +22,13 @@ setenv =
     MOTO_AMIS_PATH=./test/fixtures/custom_amis.json
     MOTO_AMIS_PATH=./test/fixtures/custom_amis.json
     aws: CB_TEST_PROVIDER=aws
     aws: CB_TEST_PROVIDER=aws
     azure: CB_TEST_PROVIDER=azure
     azure: CB_TEST_PROVIDER=azure
+    gce: CB_TEST_PROVIDER=gce
     openstack: CB_TEST_PROVIDER=openstack
     openstack: CB_TEST_PROVIDER=openstack
 passenv =
 passenv =
     CB_USE_MOCK_PROVIDERS PYTHONUNBUFFERED
     CB_USE_MOCK_PROVIDERS PYTHONUNBUFFERED
     aws: CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS AWS_ACCESS_KEY AWS_SECRET_KEY
     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
     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
     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 =
 deps =
     -rrequirements.txt
     -rrequirements.txt