فهرست منبع

Merge pull request #114 from CloudVE/gce

GCE merge to master
Nuwan Goonasekera 7 سال پیش
والد
کامیت
ed8de43c5a

+ 1 - 0
.gitignore

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

+ 30 - 23
.travis.yml

@@ -2,38 +2,45 @@ dist: trusty
 language: python
 cache:
   directories:
-    - $HOME/.cache/pip
-    - $TRAVIS_BUILD_DIR/.tox
+  - "$HOME/.cache/pip"
+  - "$TRAVIS_BUILD_DIR/.tox"
 os:
-  - linux
-#  - osx
+- linux
 matrix:
   fast_finish: true
   allow_failures:
-    - os: osx
+  - os: osx
   include:
-    - python: 2.7
-      env: TOX_ENV=py27-aws
-    - python: 2.7
-      env: TOX_ENV=py27-azure
-    - python: 2.7
-      env: TOX_ENV=py27-openstack
-    - python: 3.6
-      env: TOX_ENV=py36-aws
-    - python: 3.6
-      env: TOX_ENV=py36-azure
-    - python: 3.6
-      env: TOX_ENV=py36-openstack
-    - python: pypy-5.3.1
-      env: TOX_ENV=pypy-aws
-    - python: pypy-5.3.1
-      env: TOX_ENV=pypy-azure
-    - python: pypy-5.3.1
-      env: TOX_ENV=pypy-openstack
+  - python: 2.7
+    env: TOX_ENV=py27-aws
+  - python: 2.7
+    env: TOX_ENV=py27-azure
+  - python: 2.7
+    env: TOX_ENV=py27-gce
+  - python: 2.7
+    env: TOX_ENV=py27-openstack
+  - python: 3.6
+    env: TOX_ENV=py36-aws
+  - python: 3.6
+    env: TOX_ENV=py36-azure
+  - python: 3.6
+    env: TOX_ENV=py36-gce
+  - python: 3.6
+    env: TOX_ENV=py36-openstack
+  - 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:
   global:
     - PYTHONUNBUFFERED=True
 before_install:
+  - openssl aes-256-cbc -K $encrypted_b3fcf6d0737c_key -iv $encrypted_b3fcf6d0737c_iv
+      -in credentials.tar.gz.enc -out credentials.tar.gz -d
     - |
       case "$TRAVIS_EVENT_TYPE" in
         push|pull_request)

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

@@ -265,12 +265,16 @@ class BasePageableObjectMixin(PageableObjectMixin):
     """
 
     def __iter__(self):
-        result_list = self.list()
+        for result in self.iter():
+            yield result
+
+    def iter(self, **kwargs):
+        result_list = self.list(**kwargs)
         if result_list.supports_server_paging:
             for result in result_list:
                 yield result
             while result_list.is_truncated:
-                result_list = self.list(marker=result_list.marker)
+                result_list = self.list(marker=result_list.marker, **kwargs)
                 for result in result_list:
                     yield result
         else:
@@ -771,10 +775,25 @@ class BaseNetwork(BaseCloudResource, BaseObjectLifeCycleMixin, Network):
 
     CB_DEFAULT_NETWORK_LABEL = os.environ.get('CB_DEFAULT_NETWORK_LABEL',
                                               'cloudbridge-net')
+    CB_DEFAULT_IPV4RANGE = os.environ.get('CB_DEFAULT_IPV4RANGE',
+                                          '10.0.0.0/16')
 
     def __init__(self, provider):
         super(BaseNetwork, self).__init__(provider)
 
+    @staticmethod
+    def cidr_blocks_overlap(block1, block2):
+        common_length = min(int(block1.split('/')[1]),
+                            int(block2.split('/')[1]))
+
+        p1 = [format(int(b), '08b') for b in block1.split('/')[0].split('.')]
+        prefix1 = ''.join(p1)[:common_length]
+
+        p2 = [format(int(b), '08b') for b in block2.split('/')[0].split('.')]
+        prefix2 = ''.join(p2)[:common_length]
+
+        return prefix1 == prefix2
+
     def wait_till_ready(self, timeout=None, interval=None):
         self.wait_for(
             [NetworkState.AVAILABLE],
@@ -797,6 +816,8 @@ class BaseSubnet(BaseCloudResource, BaseObjectLifeCycleMixin, Subnet):
 
     CB_DEFAULT_SUBNET_LABEL = os.environ.get('CB_DEFAULT_SUBNET_LABEL',
                                              'cloudbridge-subnet')
+    CB_DEFAULT_SUBNET_IPV4RANGE = os.environ.get('CB_DEFAULT_SUBNET_IPV4RANGE',
+                                                 '10.0.0.0/24')
 
     def __init__(self, provider):
         super(BaseSubnet, self).__init__(provider)

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

@@ -80,6 +80,20 @@ class BaseVMFirewallService(
     def __init__(self, provider):
         super(BaseVMFirewallService, self).__init__(provider)
 
+    def find(self, **kwargs):
+        obj_list = self
+        filters = ['label']
+        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
+
+        # All kwargs should have been popped at this time.
+        if len(kwargs) > 0:
+            raise TypeError("Unrecognised parameters for search: %s."
+                            " Supported attributes: %s" % (kwargs,
+                                                           ", ".join(filters)))
+
+        return ClientPagedResultList(self.provider,
+                                     matches if matches else [])
+
 
 class BaseStorageService(StorageService, BaseCloudService):
 
@@ -207,8 +221,6 @@ class BaseSubnetService(
         return ClientPagedResultList(self._provider, list(matches))
 
     def get_or_create_default(self, zone):
-        default_cidr = '10.0.0.0/24'
-
         # Look for a CB-default subnet
         matches = self.find(label=BaseSubnet.CB_DEFAULT_SUBNET_LABEL)
         if matches:
@@ -217,7 +229,7 @@ class BaseSubnetService(
         # No provider-default Subnet exists, try to create it (net + subnets)
         network = self.provider.networking.networks.get_or_create_default()
         subnet = self.create(BaseSubnet.CB_DEFAULT_SUBNET_LABEL, network,
-                             default_cidr, zone)
+                             BaseSubnet.CB_DEFAULT_SUBNET_IPV4RANGE, zone)
         return subnet
 
 

+ 2 - 1
cloudbridge/cloud/factory.py

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

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

@@ -460,7 +460,7 @@ class ResultList(list):
         """
         Indicate whether this ``ResultList`` supports server side paging.
 
-        If server side paging is not supported, the result will useclient side
+        If server side paging is not supported, the result will use client side
         paging and the data property provides direct access to all available
         data.
         """
@@ -2063,10 +2063,12 @@ class VMFirewallRuleContainer(PageableObjectMixin):
 
         .. code-block:: python
             from cloudbridge.cloud.interfaces.resources import TrafficDirection
+            from cloudbridge.cloud.interfaces.resources import BaseNetwork
 
             fw = provider.security.vm_firewalls.get('my_fw_id')
             fw.rules.create(TrafficDirection.INBOUND, protocol='tcp',
-                            from_port=80, to_port=80, cidr='10.0.0.0/16')
+                            from_port=80, to_port=80,
+                            cidr=BaseNetwork.CB_DEFAULT_IPV4RANGE)
             fw.rules.create(TrafficDirection.INBOUND, src_dest_fw=fw)
             fw.rules.create(TrafficDirection.OUTBOUND, src_dest_fw=fw)
 

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

@@ -643,7 +643,7 @@ class NetworkService(PageableObjectMixin, CloudService):
                            subnets you create fall within this initially
                            specified range. Note that the block size should be
                            between a /16 netmask (65,536 IP addresses) and /28
-                           netmask (16 IP addresses). e.g. 10.0.0.0/16
+                           netmask (16 IP addresses), e.g. 10.0.0.0/16.
 
         :rtype: ``object`` of :class:`.Network`
         :return:  A Network object
@@ -1198,7 +1198,7 @@ class VMTypeService(PageableObjectMixin, CloudService):
     @abstractmethod
     def find(self, **kwargs):
         """
-        Searches for an instance by a given list of attributes.
+        Searches for instances by a given list of attributes.
 
         Supported attributes: name
 

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

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

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

@@ -109,20 +109,6 @@ class AzureVMFirewallService(BaseVMFirewallService):
         cb_fw = AzureVMFirewall(self.provider, fw)
         return cb_fw
 
-    def find(self, **kwargs):
-        obj_list = self
-        filters = ['label']
-        matches = cb_helpers.generic_find(filters, kwargs, obj_list)
-
-        # All kwargs should have been popped at this time.
-        if len(kwargs) > 0:
-            raise TypeError("Unrecognised parameters for search: %s."
-                            " Supported attributes: %s" % (kwargs,
-                                                           ", ".join(filters)))
-
-        return ClientPagedResultList(self.provider,
-                                     matches if matches else [])
-
     def delete(self, group_id):
         self.provider.azure_client.delete_vm_firewall(group_id)
 

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

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

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

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

+ 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):
-    _network_id_tag = "CB-AUTO-associated-network-id: "
+    _network_id_tag = "CB-auto-associated-network-id: "
 
     def __init__(self, provider, vm_firewall):
         super(OpenStackVMFirewall, self).__init__(provider, vm_firewall)
@@ -1212,15 +1212,23 @@ class OpenStackVMFirewall(BaseVMFirewall):
     @property
     def network_id(self):
         """
-        OpenStack does not associate a SG with a network so default to None.
+        OpenStack does not associate a fw with a network so extract from desc.
 
-        :return: Always return ``None``.
+        :return: The network ID supplied when this firewall was created or
+                 `None` if ID cannot be identified.
         """
         # Best way would be to use regex, but using this hacky way to avoid
         # importing the re package
+        # FIXME: This doesn't work as soon as the _description doesn't conform
+        # to this rigid string structure.
         net_id = self._description\
                      .split(" [{}".format(self._network_id_tag))[-1]\
                      .split(']')[0]
+        # We generally simulate a network being associated with a firewall;
+        # however, because of some networking specificity in Nectar, we must
+        # allow `None` return value as well in case an ID was not discovered.
+        if not net_id:
+            return None
         return net_id
 
     @property

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

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

BIN
credentials.tar.gz.enc


+ 37 - 25
docs/getting_started.rst

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

+ 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
 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
 

+ 81 - 26
docs/topics/setup.rst

@@ -9,10 +9,44 @@ be provided in one of following ways:
 3. Configuration file
 
 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
 
+**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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -39,7 +73,7 @@ OS_PROJECT_NAME      OS_STORAGE_URL
 OS_REGION_NAME       OS_AUTH_TOKEN
 ===================  ==================
 
-**Azure**
+**Microsoft Azure**
 
 Note that managing resources in Azure requires a Resource Group. If a
 Resource Group is not provided as part of the configuration, cloudbridge will
@@ -63,6 +97,19 @@ AZURE_TENANT            AZURE_VM_DEFAULT_USER_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:
 
 .. 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.
 The file should be placed in one of two locations: ``/etc/cloudbridge.ini`` or
 ``~/.cloudbridge``. Each set of credentials should be delineated with the
-provider ID (e.g., ``openstack``, ``aws``, ``azure``) with the necessary credentials
-being supplied in YAML format. Note that only one set of credentials per
-cloud provider type can be supplied (i.e., via this method, it is not possible
-to provide credentials for two different OpenStack clouds).
+provider ID (e.g., ``openstack``, ``aws``, ``azure``, ``gce``) with the
+necessary credentials being supplied in YAML format. Note that only one set
+of credentials per cloud provider type can be supplied (i.e., via this
+method, it is not possible to provide credentials for two different
+OpenStack clouds).
 
 .. code-block:: bash
 
@@ -164,22 +212,29 @@ In addition to the provider specific configuration variables above, there are
 some general configuration environment variables that apply to CloudBridge as
 a whole
 
-======================== ======================================================
-Variable		                            Description
-======================== ======================================================
-CB_DEBUG                 Setting ``CB_DEBUG=True`` will cause detailed debug
-                         output to be printed for each provider (including HTTP
-                         traces).
-CB_USE_MOCK_PROVIDERS    Setting this to ``True`` will cause the CloudBridge
-                         test suite to use mock drivers when available.
-CB_TEST_PROVIDER         Set this value to a valid :class:`.ProviderList` value
-                         such as ``aws``, to limit tests to that provider only.
-CB_DEFAULT_SUBNET_LABEL  Name to be used for a subnet that will be considered
-                         the 'default' by the library. This default will be
-                         used only in cases there is no subnet marked as the
-                         default by the provider.
-CB_DEFAULT_NETWORK_LABEL Name to be used for a network that will be considered
-                         the 'default' by the library. This default will be
-                         used only in cases there is no network marked as the
-                         default by the provider.
-======================== ======================================================
+=========================== ===================================================
+Variable                                    Description
+=========================== ===================================================
+CB_DEBUG                    Setting ``CB_DEBUG=True`` will cause detailed 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-cosmosdb-table==1.0.4',
               'pysftp==0.2.9']
+REQS_GCP = ['google-api-python-client', 'oauth2client']
 REQS_OPENSTACK = [
     'openstacksdk>=0.12.0,<=0.17',
     'python-novaclient>=7.0.0,<=11.0',
@@ -49,7 +50,7 @@ REQS_OPENSTACK = [
     'python-neutronclient>=6.0.0,<=6.9',
     'python-keystoneclient>=3.13.0,<=3.17'
 ]
-REQS_FULL = REQS_BASE + REQS_AWS + REQS_AZURE + REQS_OPENSTACK
+REQS_FULL = REQS_BASE + REQS_AWS + REQS_AZURE + REQS_GCP + REQS_OPENSTACK
 # httpretty is required with/for moto 1.0.0 or AWS tests fail
 REQS_DEV = ([
     'tox>=2.1.1',

+ 11 - 2
test/helpers/__init__.py

@@ -85,12 +85,19 @@ TEST_DATA_CONFIG = {
         "vm_type": get_env('CB_VM_TYPE_AWS', 't2.nano'),
         "placement": get_env('CB_PLACEMENT_AWS', 'us-east-1a'),
     },
-    "OpenStackCloudProvider": {
-        "image": os.environ.get('CB_IMAGE_OS',
+    'OpenStackCloudProvider': {
+        'image': os.environ.get('CB_IMAGE_OS',
                                 'c66bdfa1-62b1-43be-8964-e9ce208ac6a5'),
         "vm_type": os.environ.get('CB_VM_TYPE_OS', 'm1.tiny'),
         "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
     },
+    'GCECloudProvider': {
+        'image': ('https://www.googleapis.com/compute/v1/'
+                  'projects/ubuntu-os-cloud/global/images/'
+                  'ubuntu-1710-artful-v20180126'),
+        'vm_type': 'f1-micro',
+        'placement': os.environ.get('GCE_DEFAULT_ZONE', 'us-central1-a'),
+    },
     "AzureCloudProvider": {
         "placement":
             get_env('CB_PLACEMENT_AZURE', 'eastus'),
@@ -108,6 +115,8 @@ def get_provider_test_data(provider, key):
         return TEST_DATA_CONFIG.get("AWSCloudProvider").get(key)
     elif "OpenStackCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("OpenStackCloudProvider").get(key)
+    elif "GCECloudProvider" in provider.name:
+        return TEST_DATA_CONFIG.get("GCECloudProvider").get(key)
     elif "AzureCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("AzureCloudProvider").get(key)
     return None

+ 1 - 1
test/helpers/standard_interface_tests.py

@@ -117,7 +117,7 @@ def check_get(test, service, obj):
 
 def check_get_non_existent(test, service):
     # check get
-    get_objs = service.get(str(uuid.uuid4()))
+    get_objs = service.get('tmp-' + str(uuid.uuid4()))
     test.assertIsNone(
         get_objs,
         "Get non-existent object for %s returned unexpected objects: %s"

+ 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.instance_id,
                                  test_instance.id)
-                if not self.provider.PROVIDER_ID == 'azure':
+                if (self.provider.PROVIDER_ID != 'azure' and
+                        self.provider.PROVIDER_ID != 'gce'):
                     self.assertEqual(test_vol.attachments.device,
                                      "/dev/sda2")
                 test_vol.detach()

+ 20 - 10
test/test_compute_service.py

@@ -2,6 +2,7 @@ import ipaddress
 
 import six
 
+from cloudbridge.cloud.base.resources import BaseNetwork
 from cloudbridge.cloud.factory import ProviderList
 from cloudbridge.cloud.interfaces import InstanceState
 from cloudbridge.cloud.interfaces import InvalidConfigurationException
@@ -83,7 +84,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             net = subnet.network
             kp = self.provider.security.key_pairs.create(name=label)
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
             test_instance = helpers.get_test_instance(self.provider,
                                                       label, key_pair=kp,
                                                       vm_firewalls=[fw],
@@ -276,7 +277,12 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                     "vm_type")
                 vm_type = self.provider.compute.vm_types.find(
                     name=vm_type_name)[0]
-                for _ in range(vm_type.num_ephemeral_disks):
+                # Some providers, e.g. GCP, has a limit on total number of
+                # attached disks; it does not matter how many of them are
+                # ephemeral or persistent. So, wee keep in mind that we have
+                # attached 4 disks already, and add ephemeral disks accordingly
+                # to not exceed the limit.
+                for _ in range(vm_type.num_ephemeral_disks - 4):
                     lc.add_ephemeral_device()
 
                 subnet = helpers.get_or_create_default_subnet(
@@ -312,7 +318,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
         with helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
                 instance=test_inst, vm_firewall=fw, network=net)):
             net = self.provider.networking.networks.create(
-                label=label, cidr_block='10.0.0.0/16')
+                label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
             cidr = '10.0.1.0/24'
             subnet = net.create_subnet(label=label, cidr_block=cidr,
                                        zone=helpers.get_provider_test_data(
@@ -321,7 +327,7 @@ class CloudComputeServiceTestCase(ProviderTestBase):
             test_inst = helpers.get_test_instance(self.provider, label,
                                                   subnet=subnet)
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             # Check adding a VM firewall to a running instance
             test_inst.add_vm_firewall(fw)
@@ -353,13 +359,16 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                                                                gateway)):
                 router.attach_subnet(subnet)
                 router.attach_gateway(gateway)
-                # check whether adding an elastic ip works
-                fip = gateway.floating_ips.create()
-                self.assertFalse(
-                    fip.in_use,
-                    "Newly created floating IP address should not be in use.")
+                fip = None
 
                 with helpers.cleanup_action(lambda: 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(
                             lambda: test_inst.remove_floating_ip(fip)):
                         test_inst.add_floating_ip(fip)
@@ -370,7 +379,8 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                         fip.refresh()
                         self.assertTrue(
                             fip.in_use,
-                            "Attached floating IP address should be in use.")
+                            "Attached floating IP %s address should be in use."
+                            % fip.public_ip)
                     test_inst.refresh()
                     test_inst.reboot()
                     test_inst.wait_till_ready()

+ 2 - 1
test/test_image_service.py

@@ -1,5 +1,6 @@
 from cloudbridge.cloud.interfaces import MachineImageState
-from cloudbridge.cloud.interfaces.resources import Instance, MachineImage
+from cloudbridge.cloud.interfaces.resources import Instance
+from cloudbridge.cloud.interfaces.resources import MachineImage
 
 from test import helpers
 from test.helpers import ProviderTestBase

+ 2 - 0
test/test_interface.py

@@ -62,6 +62,8 @@ class CloudInterfaceTestCase(ProviderTestBase):
             cloned_config['os_password'] = "cb_dummy"
         elif self.provider.PROVIDER_ID == 'azure':
             cloned_config['azure_subscription_id'] = "cb_dummy"
+        elif self.provider.PROVIDER_ID == 'gce':
+            cloned_config['gce_service_creds_dict'] = {'dummy': 'dict'}
 
         with self.assertRaises(ProviderConnectionException):
             cloned_provider = CloudProviderFactory().create_provider(

+ 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 Network
 from cloudbridge.cloud.interfaces.resources import NetworkState
@@ -20,7 +21,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
         def create_net(label):
             return self.provider.networking.networks.create(
-                label=label, cidr_block='10.0.0.0/16')
+                label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
 
         def cleanup_net(net):
             if net:
@@ -40,10 +41,8 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         label = 'cb-propnetwork-{0}'.format(helpers.get_uuid())
         subnet_label = 'cb-propsubnet-{0}'.format(helpers.get_uuid())
         net = self.provider.networking.networks.create(
-            label=label, cidr_block='10.0.0.0/16')
-        with helpers.cleanup_action(
-            lambda: net.delete()
-        ):
+            label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
+        with helpers.cleanup_action(lambda: net.delete()):
             net.wait_till_ready()
             self.assertEqual(
                 net.state, 'available',
@@ -52,9 +51,9 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             sit.check_repr(self, net)
 
             self.assertIn(
-                net.cidr_block, ['', '10.0.0.0/16'],
-                "Network CIDR %s does not contain the expected value."
-                % net.cidr_block)
+                net.cidr_block, ['', BaseNetwork.CB_DEFAULT_IPV4RANGE],
+                "Network CIDR %s does not contain the expected value %s."
+                % (net.cidr_block, BaseNetwork.CB_DEFAULT_IPV4RANGE))
 
             cidr = '10.0.20.0/24'
             sn = net.create_subnet(
@@ -89,7 +88,11 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
 
                 self.assertEqual(
                     cidr, sn.cidr_block,
-                    "Subnet's CIDR %s should match the specified one %s." % (
+                    "Should be exact cidr block that was requested")
+
+                self.assertTrue(
+                    BaseNetwork.cidr_blocks_overlap(cidr, sn.cidr_block),
+                    "Subnet's CIDR %s should overlap the specified one %s." % (
                         sn.cidr_block, cidr))
 
     def test_crud_subnet(self):
@@ -179,7 +182,7 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
         gteway = None
         with helpers.cleanup_action(lambda: _cleanup(net, sn, router, gteway)):
             net = self.provider.networking.networks.create(
-                label=label, cidr_block='10.0.0.0/16')
+                label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
             router = self.provider.networking.routers.create(label=label,
                                                              network=net)
             cidr = '10.0.15.0/24'
@@ -190,15 +193,16 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             # Check basic router properties
             sit.check_standard_behaviour(
                 self, self.provider.networking.routers, router)
-            self.assertEqual(
-                router.state, RouterState.DETACHED,
-                "Router {0} state {1} should be {2}.".format(
-                    router.id, router.state, RouterState.DETACHED))
-
-#             self.assertFalse(
-#                 router.network_id,
-#                 "Router {0} should not be assoc. with a network {1}".format(
-#                     router.id, router.network_id))
+            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(
                 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)
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             self.assertEqual(label, fw.description)
 
@@ -106,7 +106,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         fw = None
         with helpers.cleanup_action(lambda: fw.delete()):
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             def create_fw_rule(label):
                 return fw.rules.create(
@@ -133,7 +133,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, protocol='tcp',
@@ -157,7 +157,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
+                label=label, description=label, network=net.id)
 
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, protocol='tcp',
@@ -180,17 +180,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             subnet = helpers.get_or_create_default_subnet(self.provider)
             net = subnet.network
             fw = self.provider.security.vm_firewalls.create(
-                label=label, description=label, network_id=net.id)
-            rules = list(fw.rules)
-            self.assertTrue(
-                # TODO: This should be made consistent across all providers.
-                # Currently, OpenStack creates two rules, one for IPV6 and
-                # another for IPV4
-                len(rules) >= 1, "Expected a single VM firewall rule allowing"
-                " all outbound traffic. Got {0}.".format(rules))
-            self.assertEqual(
-                rules[0].direction, TrafficDirection.OUTBOUND,
-                "Expected rule to be outbound. Got {0}.".format(rules))
+                label=label, description=label, network=net.id)
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, src_dest_fw=fw,
                 protocol='tcp', from_port=1, to_port=65535)

+ 3 - 1
tox.ini

@@ -12,7 +12,7 @@
 # mock providers.
 
 [tox]
-envlist = {py27,py36,pypy}-{aws,azure,openstack}
+envlist = {py27,py36,pypy}-{aws,azure,gce,openstack}
 
 [testenv]
 commands = flake8 cloudbridge test setup.py
@@ -22,11 +22,13 @@ setenv =
     MOTO_AMIS_PATH=./test/fixtures/custom_amis.json
     aws: CB_TEST_PROVIDER=aws
     azure: CB_TEST_PROVIDER=azure
+    gce: CB_TEST_PROVIDER=gce
     openstack: CB_TEST_PROVIDER=openstack
 passenv =
     CB_USE_MOCK_PROVIDERS PYTHONUNBUFFERED
     aws: CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS AWS_ACCESS_KEY AWS_SECRET_KEY
     azure: AZURE_SUBSCRIPTION_ID AZURE_CLIENT_ID AZURE_SECRET AZURE_TENANT AZURE_REGION_NAME AZURE_RESOURCE_GROUP AZURE_STORAGE_ACCOUNT AZURE_VM_DEFAULT_USER_NAME AZURE_PUBLIC_KEY_STORAGE_TABLE_NAME
+    gce: CB_IMAGE_GCE CB_INSTANCE_TYPE_GCE CB_PLACEMENT_GCE GCE_DEFAULT_REGION GCE_DEFAULT_ZONE GCE_PROJECT_NAME GCE_SERVICE_CREDS_FILE GCE_SERVICE_CREDS_DICT
     openstack:  CB_IMAGE_OS CB_INSTANCE_TYPE_OS CB_PLACEMENT_OS OS_AUTH_URL OS_PASSWORD OS_PROJECT_NAME OS_TENANT_NAME OS_USERNAME OS_REGION_NAME OS_USER_DOMAIN_NAME OS_PROJECT_DOMAIN_NAME NOVA_SERVICE_NAME
 deps =
     -rrequirements.txt