You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by to...@apache.org on 2013/07/13 01:45:19 UTC

[04/10] Add new driver for Google Compute Engine.

http://git-wip-us.apache.org/repos/asf/libcloud/blob/b0782175/libcloud/compute/drivers/gce.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/gce.py b/libcloud/compute/drivers/gce.py
new file mode 100644
index 0000000..c46b66d
--- /dev/null
+++ b/libcloud/compute/drivers/gce.py
@@ -0,0 +1,1794 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Module for Google Compute Engine Driver.
+"""
+from __future__ import with_statement
+
+import datetime
+import time
+import sys
+import os
+import getpass
+
+from libcloud.common.google import GoogleResponse
+from libcloud.common.google import GoogleBaseConnection
+
+from libcloud.compute.base import Node, NodeDriver, NodeImage, NodeLocation
+from libcloud.compute.base import NodeSize, StorageVolume, UuidMixin
+from libcloud.compute.providers import Provider
+from libcloud.compute.types import NodeState, LibcloudError
+
+API_VERSION = 'v1beta15'
+DEFAULT_TASK_COMPLETION_TIMEOUT = 180
+
+
+def timestamp_to_datetime(timestamp):
+    """
+    Return a datetime object that corresponds to the time in an RFC3339
+    timestamp.
+
+    @param  timestamp: RFC3339 timestamp string
+    @type   timestamp: C{str}
+
+    @return:  Datetime object corresponding to timestamp
+    @rtype:   C{datetime}
+    """
+    # We remove timezone offset and microseconds (Python 2.5 strptime doesn't
+    # support %f)
+    ts = datetime.datetime.strptime(timestamp[:-10], '%Y-%m-%dT%H:%M:%S')
+    tz_hours = int(timestamp[-5:-3])
+    tz_mins = int(timestamp[-2:]) * int(timestamp[-6:-5] + '1')
+    tz_delta = datetime.timedelta(hours=tz_hours, minutes=tz_mins)
+    return ts + tz_delta
+
+
+class GCEError(LibcloudError):
+    """Base class for general GCE Errors"""
+    def __init__(self, code, value):
+        self.code = code
+        self.value = value
+
+    def __repr__(self):
+        return repr(self.code) + ": " + repr(self.value)
+
+
+class GCEKnownError(GCEError):
+    """Base class for GCE Errors that can be classified"""
+    def __init__(self, value):
+        self.value = value
+
+    def __repr__(self):
+        return repr(self.value)
+
+
+class QuotaExceededError(GCEKnownError):
+    pass
+
+
+class ResourceExistsError(GCEKnownError):
+    pass
+
+
+class GCEResponse(GoogleResponse):
+    pass
+
+
+class GCEConnection(GoogleBaseConnection):
+    """Connection class for the GCE driver."""
+    host = 'www.googleapis.com'
+    responseCls = GCEResponse
+
+    def __init__(self, user_id, key, secure, auth_type=None,
+                 credential_file=None, project=None, **kwargs):
+        self.scope = ['https://www.googleapis.com/auth/compute']
+        super(GCEConnection, self).__init__(user_id, key, secure=secure,
+                                            auth_type=auth_type,
+                                            credential_file=credential_file,
+                                            **kwargs)
+        self.request_path = '/compute/%s/projects/%s' % (API_VERSION,
+                                                         project)
+
+
+class GCEAddress(UuidMixin):
+    """A GCE Static address."""
+    def __init__(self, id, name, address, region, driver, extra=None):
+        self.id = str(id)
+        self.name = name
+        self.address = address
+        self.region = region
+        self.driver = driver
+        self.extra = extra
+        UuidMixin.__init__(self)
+
+    def __repr__(self):
+        return '<GCEAddress id="%s" name="%s" address="%s">' % (
+            self.id, self.name, self.address)
+
+    def destroy(self):
+        """
+        Destroy this address.
+
+        @return: True if successful
+        @rtype:  C{bool}
+        """
+        return self.driver.ex_destroy_address(address=self)
+
+
+class GCEFailedNode(object):
+    """Dummy Node object for nodes that are not created."""
+    def __init__(self, name, error):
+        self.name = name
+        self.error = error
+
+    def __repr__(self):
+        return '<GCEFailedNode name="%s" error_code="%s">' % (
+            self.name, self.error['code'])
+
+
+class GCEFirewall(UuidMixin):
+    """A GCE Firewall rule class."""
+    def __init__(self, id, name, allowed, network, source_ranges, source_tags,
+                 driver, extra=None):
+        self.id = str(id)
+        self.name = name
+        self.network = network
+        self.allowed = allowed
+        self.source_ranges = source_ranges
+        self.source_tags = source_tags
+        self.driver = driver
+        self.extra = extra
+        UuidMixin.__init__(self)
+
+    def __repr__(self):
+        return '<GCEFirewall id="%s" name="%s" network="%s">' % (
+            self.id, self.name, self.network.name)
+
+    def destroy(self):
+        """
+        Destroy this firewall.
+
+        @return: True if successful
+        @rtype:  C{bool}
+        """
+        return self.driver.ex_destroy_firewall(firewall=self)
+
+
+class GCENetwork(UuidMixin):
+    """A GCE Network object class."""
+    def __init__(self, id, name, cidr, driver, extra=None):
+        self.id = str(id)
+        self.name = name
+        self.cidr = cidr
+        self.driver = driver
+        self.extra = extra
+        UuidMixin.__init__(self)
+
+    def __repr__(self):
+        return '<GCENetwork id="%s" name="%s" cidr="%s">' % (
+            self.id, self.name, self.cidr)
+
+    def destroy(self):
+        """
+        Destroy this newtwork
+
+        @return: True if successful
+        @rtype:  C{bool}
+        """
+        return self.driver.ex_destroy_network(network=self)
+
+
+class GCENodeSize(NodeSize):
+    """A GCE Node Size (MachineType) class."""
+    def __init__(self, id, name, ram, disk, bandwidth, price, driver,
+                 extra=None):
+        self.extra = extra
+        super(GCENodeSize, self).__init__(id, name, ram, disk, bandwidth,
+                                          price, driver)
+
+
+class GCEProject(UuidMixin):
+    """GCE Project information."""
+    def __init__(self, id, name, metadata, quotas, driver, extra=None):
+        self.id = str(id)
+        self.name = name
+        self.metadata = metadata
+        self.quotas = quotas
+        self.driver = driver
+        self.extra = extra
+        UuidMixin.__init__(self)
+
+    def _repr__(self):
+        return '<GCEProject id="%s" name="%s">' % (self.id, self.name)
+
+
+class GCEZone(NodeLocation):
+    """Subclass of NodeLocation to provide additional information."""
+    def __init__(self, id, name, status, maintenance_windows, quotas,
+                 deprecated, driver, extra=None):
+        self.status = status
+        self.maintenance_windows = maintenance_windows
+        self.quotas = quotas
+        self.deprecated = deprecated
+        self.extra = extra
+        country = name.split('-')[0]
+        super(GCEZone, self).__init__(id=str(id), name=name, country=country,
+                                      driver=driver)
+
+    def _now(self):
+        """
+        Returns current UTC time.
+
+        Can be overridden in unittests.
+        """
+        return datetime.datetime.utcnow()
+
+    def _get_next_maint(self):
+        """
+        Returns the next Maintenance Window.
+
+        @return:  A dictionary containing maintenance window info
+                  The dictionary contains 4 keys with values of type C{str}
+                      - C{name}: The name of the maintence window
+                      - C{description}: Description of the maintenance window
+                      - C{beginTime}: RFC3339 Timestamp
+                      - C{endTime}: RFC3339 Timestamp
+        @rtype:   C{dict}
+        """
+        begin = None
+        next_window = None
+        if len(self.maintenance_windows) == 1:
+            return self.maintenance_windows[0]
+        for mw in self.maintenance_windows:
+            begin_next = timestamp_to_datetime(mw['beginTime'])
+            if (not begin) or (begin_next < begin):
+                begin = begin_next
+                next_window = mw
+        return next_window
+
+    def _get_time_until_mw(self):
+        """
+        Returns time until next maintenance window.
+
+        @return:  Time until next maintenance window
+        @rtype:   C{datetime.timedelta}
+        """
+        next_window = self._get_next_maint()
+        now = self._now()
+        next_begin = timestamp_to_datetime(next_window['beginTime'])
+        return next_begin - now
+
+    def _get_next_mw_duration(self):
+        """
+        Returns the duration of the next maintenance window.
+
+        @return:  Duration of next maintenance window
+        @rtype:   C{datetime.timedelta}
+        """
+        next_window = self._get_next_maint()
+        next_begin = timestamp_to_datetime(next_window['beginTime'])
+        next_end = timestamp_to_datetime(next_window['endTime'])
+        return next_end - next_begin
+
+    @property
+    def time_until_mw(self):
+        """
+        Returns the time until the next Maintenance Window as a
+        datetime.timedelta object.
+        """
+        return self._get_time_until_mw()
+
+    @property
+    def next_mw_duration(self):
+        """
+        Returns the duration of the next Maintenance Window as a
+        datetime.timedelta object.
+        """
+        return self._get_next_mw_duration()
+
+    def __repr__(self):
+        return '<GCEZone id="%s" name="%s" status="%s">' % (self.id, self.name,
+                                                            self.status)
+
+
+class GCENodeDriver(NodeDriver):
+    """
+    Base class for GCE Node Driver.
+    """
+    connectionCls = GCEConnection
+    api_name = 'googleapis'
+    name = "Google Compute Engine"
+    type = Provider.GCE
+    website = 'https://www.googleapis.com/'
+
+    NODE_STATE_MAP = {
+        "PROVISIONING": NodeState.PENDING,
+        "STAGING": NodeState.PENDING,
+        "RUNNING": NodeState.RUNNING,
+        "STOPPED": NodeState.TERMINATED,
+        "TERMINATED": NodeState.TERMINATED
+    }
+
+    def __init__(self, user_id, key, datacenter=None, project=None,
+                 auth_type=None, **kwargs):
+        """
+        @param  user_id: The email address (for service accounts) or Client ID
+                         (for installed apps) to be used for authentication.
+        @type   user_id: C{str}
+
+        @param  key: The RSA Key (for service accounts) or file path containing
+                     key or Client Secret (for installed apps) to be used for
+                     authentication.
+        @type   key: C{str}
+
+        @keyword  datacenter: The name of the datacenter (zone) used for
+                              operations.
+        @type     datacenter: C{str}
+
+        @keyword  project: Your GCE project name. (required)
+        @type     project: C{str}
+
+        @keyword  auth_type: Accepted values are "SA" or "IA"
+                             ("Service Account" or "Installed Application").
+                             If not supplied, auth_type will be guessed based
+                             on value of user_id.
+        @type     auth_type: C{str}
+        """
+        self.auth_type = auth_type
+        self.project = project
+        if not self.project:
+            raise ValueError('Project name must be specified using '
+                             '"project" keyword.')
+        super(GCENodeDriver, self).__init__(user_id, key, **kwargs)
+
+        # Cache Zone information to reduce API calls and increase speed
+        self.base_path = '/compute/%s/projects/%s' % (API_VERSION,
+                                                      self.project)
+        self.zone_list = self.ex_list_zones()
+        self.zone_dict = {}
+        for zone in self.zone_list:
+            self.zone_dict[zone.name] = zone
+        if datacenter:
+            self.zone = self.ex_get_zone(datacenter)
+        else:
+            self.zone = None
+
+    def _ex_connection_class_kwargs(self):
+        return {'auth_type': self.auth_type,
+                'project': self.project}
+
+    def _categorize_error(self, error):
+        """
+        Parse error message returned from GCE operation and raise the
+        appropriate Exception.
+
+        @param  error: Error dictionary from a GCE Operations response
+        @type   error: C{dict}
+        """
+        err = error['errors'][0]
+        message = err['message']
+        code = err['code']
+        if code == 'QUOTA_EXCEEDED':
+            raise QuotaExceededError(message)
+        elif code == 'RESOURCE_ALREADY_EXISTS':
+            raise ResourceExistsError(message)
+        else:
+            raise GCEError(code, message)
+
+    def _find_zone(self, name, res_type, region=False):
+        """
+        Find the zone for a named resource.
+
+        @param  name: Name of resource to find
+        @type   name: C{str}
+
+        @param  res_type: Type of resource to find.
+                          Examples include: 'disks', 'instances' or 'addresses'
+        @type   res_type: C{str}
+
+        @keyword  region: If True, find a region instead of a zone.
+        @keyword  region: C{bool}
+
+        @return:  Name of zone (or region) that the resource is in.
+        @rtype:   C{str}
+        """
+        request = '/aggregated/%s' % res_type
+        res_list = self.connection.request(request).object
+        for k, v in res_list['items'].items():
+            for res in v.get(res_type, []):
+                if res['name'] == name:
+                    if region:
+                        return k.replace('regions/', '')
+                    else:
+                        return k.replace('zones/', '')
+
+    def _match_images(self, project, partial_name):
+        """
+        Find the latest image, given a partial name.
+
+        For example, providing 'debian-7' will return the image object for the
+        most recent image with a name that starts with 'debian-7' in the
+        supplied project.  If no project is given, it will search your own
+        project.
+
+        @param  project:  The name of the project to search for images.
+                          Examples include: 'debian-cloud' and 'centos-cloud'.
+        @type   project:  C{str} or C{None}
+
+        @param  partial_name: The full name or beginning of a name for an
+                              image.
+        @type   partial_name: C{str}
+
+        @return:  The latest image object that maches the partial name.
+        @rtype:   L{NodeImage}
+        """
+        project_images = self.list_images(project)
+        partial_match = []
+        for image in project_images:
+            if image.name == partial_name:
+                return image
+            if image.name.startswith(partial_name):
+                ts = timestamp_to_datetime(image.extra['creationTimestamp'])
+                if not partial_match or partial_match[0] < ts:
+                    partial_match = [ts, image]
+
+        if partial_match:
+            return partial_match[1]
+
+    def ex_list_addresses(self, region=None):
+        """
+        Return a list of static addreses for a region or all.
+
+        @keyword  region: The region to return addresses from. For example:
+                          'us-central1'.  If None, will return addresses from
+                          region of self.zone.  If 'all', will return all
+                          addresses.
+        @type     region: C{str} or C{None}
+
+        @return: A list of static address objects.
+        @rtype: C{list} of L{GCEAddress}
+        """
+        list_addresses = []
+        if region is None and self.zone:
+            region = '-'.join(self.zone.name.split('-')[:-1])
+        elif region == 'all':
+            region = None
+
+        if region is None:
+            request = '/aggregated/addresses'
+        else:
+            request = '/regions/%s/addresses' % region
+
+        response = self.connection.request(request, method='GET').object
+
+        if 'items' in response:
+            # The aggregated result returns dictionaries for each region
+            if region is None:
+                for v in response['items'].values():
+                    region_addresses = [self._to_address(a) for a in
+                                        v.get('addresses', [])]
+                    list_addresses.extend(region_addresses)
+            else:
+                list_addresses = [self._to_address(a) for a in
+                                  response['items']]
+        return list_addresses
+
+    def ex_list_firewalls(self):
+        """
+        Return the list of firewalls.
+
+        @return: A list of firewall objects.
+        @rtype: C{list} of L{GCEFirewall}
+        """
+        list_firewalls = []
+        request = '/global/firewalls'
+        response = self.connection.request(request, method='GET').object
+        list_firewalls = [self._to_firewall(f) for f in
+                          response.get('items', [])]
+        return list_firewalls
+
+    def list_images(self, ex_project=None):
+        """
+        Return a list of image objects for a project.
+
+        @keyword  ex_project: Optional alternate project name.
+        @type     ex_project: C{str} or C{None}
+
+        @return:  List of NodeImage objects
+        @rtype:   C{list} of L{NodeImage}
+        """
+        list_images = []
+        request = '/global/images'
+        if ex_project is None:
+            response = self.connection.request(request, method='GET').object
+        else:
+            # Save the connection request_path
+            save_request_path = self.connection.request_path
+            # Override the connection request path
+            new_request_path = save_request_path.replace(self.project,
+                                                         ex_project)
+            self.connection.request_path = new_request_path
+            response = self.connection.request(request, method='GET').object
+            # Restore the connection request_path
+            self.connection.request_path = save_request_path
+        list_images = [self._to_node_image(i) for i in
+                       response.get('items', [])]
+        return list_images
+
+    def list_locations(self):
+        """
+        Return a list of locations (zones).
+
+        The L{ex_list_zones} method returns more comprehensive results, but
+        this is here for compatibility.
+
+        @return: List of NodeLocation objects
+        @rtype: C{list} of L{NodeLocation}
+        """
+        list_locations = []
+        request = '/zones'
+        response = self.connection.request(request, method='GET').object
+        list_locations = [self._to_node_location(l) for l in response['items']]
+        return list_locations
+
+    def ex_list_networks(self):
+        """
+        Return the list of networks.
+
+        @return: A list of network objects.
+        @rtype: C{list} of L{GCENetwork}
+        """
+        list_networks = []
+        request = '/global/networks'
+        response = self.connection.request(request, method='GET').object
+        list_networks = [self._to_network(n) for n in
+                         response.get('items', [])]
+        return list_networks
+
+    def list_nodes(self, ex_zone=None):
+        """
+        Return a list of nodes in the current zone or all zones.
+
+        @keyword  ex_zone:  Optional zone name or 'all'
+        @type     ex_zone:  C{str} or L{GCEZone} or L{NodeLocation} or C{None}
+
+        @return:  List of Node objects
+        @rtype:   C{list} of L{Node}
+        """
+        list_nodes = []
+        # Use provided zone or default zone
+        zone = ex_zone or self.zone
+        # Setting ex_zone to 'all' overrides the default zone
+        if zone == 'all':
+            zone = None
+        if zone is None:
+            request = '/aggregated/instances'
+        elif hasattr(zone, 'name'):
+            request = '/zones/%s/instances' % zone.name
+        else:
+            request = '/zones/%s/instances' % zone
+
+        response = self.connection.request(request, method='GET').object
+
+        if 'items' in response:
+            # The aggregated response returns a dict for each zone
+            if zone is None:
+                for v in response['items'].values():
+                    zone_nodes = [self._to_node(i) for i in
+                                  v.get('instances', [])]
+                    list_nodes.extend(zone_nodes)
+            else:
+                list_nodes = [self._to_node(i) for i in response['items']]
+        return list_nodes
+
+    def list_sizes(self, location=None):
+        """
+        Return a list of sizes (machineTypes) in a zone.
+
+        @keyword  location: Location or Zone for sizes
+        @type     location: C{str} or L{GCEZone} or L{NodeLocation} or C{None}
+
+        @return:  List of GCENodeSize objects
+        @rtype:   C{list} of L{GCENodeSize}
+        """
+        list_sizes = []
+        location = location or self.zone
+        if location == 'all':
+            location = None
+        if location is None:
+            request = '/aggregated/machineTypes'
+        elif hasattr(location, 'name'):
+            request = '/zones/%s/machineTypes' % location.name
+        else:
+            request = '/zones/%s/machineTypes' % location
+
+        response = self.connection.request(request, method='GET').object
+
+        if 'items' in response:
+            # The aggregated response returns a dict for each zone
+            if location is None:
+                for v in response['items'].values():
+                    zone_sizes = [self._to_node_size(s) for s in
+                                  v.get('machineTypes', [])]
+                    list_sizes.extend(zone_sizes)
+            else:
+                list_sizes = [self._to_node_size(s) for s in response['items']]
+        return list_sizes
+
+    def list_volumes(self, ex_zone=None):
+        """
+        Return a list of volumes for a zone or all.
+
+        Will return list from provided zone, or from the default zone unless
+        given the value of 'all'.
+
+        @keyword  region: The zone to return volumes from.
+        @type     region: C{str} or L{GCEZone} or L{NodeLocation} or C{None}
+
+        @return: A list of volume objects.
+        @rtype: C{list} of L{StorageVolume}
+        """
+        list_volumes = []
+        zone = ex_zone or self.zone
+        if zone == 'all':
+            zone = None
+        if zone is None:
+            request = '/aggregated/disks'
+        elif hasattr(zone, 'name'):
+            request = '/zones/%s/disks' % zone.name
+        else:
+            request = '/zones/%s/disks' % zone
+
+        response = self.connection.request(request, method='GET').object
+        if 'items' in response:
+            # The aggregated response returns a dict for each zone
+            if zone is None:
+                for v in response['items'].values():
+                    zone_volumes = [self._to_storage_volume(d) for d in
+                                    v.get('disks', [])]
+                    list_volumes.extend(zone_volumes)
+            else:
+                list_volumes = [self._to_storage_volume(d) for d in
+                                response['items']]
+        return list_volumes
+
+    def ex_list_zones(self):
+        """
+        Return the list of zones.
+
+        @return: A list of zone objects.
+        @rtype: C{list} of L{GCEZone}
+        """
+        list_zones = []
+        request = '/zones'
+        response = self.connection.request(request, method='GET').object
+        list_zones = [self._to_zone(z) for z in response['items']]
+        return list_zones
+
+    def ex_create_address(self, name, region=None):
+        """
+        Create a static address in a region.
+
+        @param  name: Name of static address
+        @type   name: C{str}
+
+        @param  region: Name of region for the addres (e.g. 'us-central1')
+        @type   region: C{str}
+
+        @return:  Static Address object
+        @rtype:   L{GCEAddress}
+        """
+        if region is None and self.zone:
+            region = '-'.join(self.zone.name.split('-')[:-1])
+        elif region is None:
+            raise GCEError('REGION_NOT_SPECIFIED',
+                           'Region must be provided for an address')
+        address_data = {'name': name}
+        request = '/regions/%s/addresses' % region
+        response = self.connection.async_request(request, method='POST',
+                                                 data=address_data).object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        return self.ex_get_address(name, region=region)
+
+    def ex_create_firewall(self, name, allowed, network='default',
+                           source_ranges=None, source_tags=None):
+        """
+        Create a firewall on a network.
+
+        Firewall rules should be supplied in the "allowed" field.  This is a
+        list of dictionaries formated like so ("ports" is optional)::
+            [{"IPProtocol": "<protocol string or number>",
+              "ports": [ "<port_numbers or ranges>"}]
+
+        For example, to allow tcp on port 8080 and udp on all ports, 'allowed'
+        would be::
+            [{"IPProtocol": "tcp",
+              "ports": ["8080"]},
+             {"IPProtocol": "udp"}]
+        See U{Firewall Reference<https://developers.google.com/compute/docs/
+        reference/latest/firewalls/insert>} for more information.
+
+        @param  name: Name of the firewall to be created
+        @type   name: C{str}
+
+        @param  allowed: List of dictionaries with rules
+        @type   allowed: C{list} of C{dict}
+
+        @keyword  network: The network that the firewall applies to.
+        @type     network: C{str} or L{GCENetwork}
+
+        @keyword  source_ranges: A list of IP ranges in CIDR format that the
+                                 firewall should apply to.
+        @type     source_ranges: C{list} of C{str}
+
+        @keyword  source_tags: A list of instance tags which the rules apply
+        @type     source_tags: C{list} of C{str}
+
+        @return:  Firewall object
+        @rtype:   L{GCEFirewall}
+        """
+        firewall_data = {}
+        if not hasattr(network, 'name'):
+            nw = self.ex_get_network(network)
+        else:
+            nw = network
+
+        firewall_data['name'] = name
+        firewall_data['allowed'] = allowed
+        firewall_data['network'] = nw.extra['selfLink']
+        if source_ranges is not None:
+            firewall_data['sourceRanges'] = source_ranges
+        if source_tags is not None:
+            firewall_data['sourceTags'] = source_tags
+
+        request = '/global/firewalls'
+
+        response = self.connection.async_request(request, method='POST',
+                                                 data=firewall_data).object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        return self.ex_get_firewall(name)
+
+    def ex_create_network(self, name, cidr):
+        """
+        Create a network.
+
+        @param  name: Name of network to be created
+        @type   name: C{str}
+
+        @param  cidr: Address range of network in CIDR format.
+        @type  cidr: C{str}
+
+        @return:  Network object
+        @rtype:   L{GCENetwork}
+        """
+        network_data = {}
+        network_data['name'] = name
+        network_data['IPv4Range'] = cidr
+
+        request = '/global/networks'
+
+        response = self.connection.async_request(request, method='POST',
+                                                 data=network_data).object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+
+        return self.ex_get_network(name)
+
+    def _create_node_req(self, name, size, image, location, network,
+                         tags=None, metadata=None, boot_disk=None):
+        """
+        Returns a request and body to create a new node.  This is a helper
+        method to suppor both L{create_node} and L{ex_create_multiple_nodes}.
+
+        @param  name: The name of the node to create.
+        @type   name: C{str}
+
+        @param  size: The machine type to use.
+        @type   size: L{GCENodeSize}
+
+        @param  image: The image to use to create the node (or, if using a
+                       persistent disk, the image the disk was created from).
+        @type   image: L{NodeImage}
+
+        @param  location: The location (zone) to create the node in.
+        @type   location: L{NodeLocation} or L{GCEZone}
+
+        @param  network: The network to associate with the node.
+        @type   network: L{GCENetwork}
+
+        @keyword  tags: A list of tags to assiciate with the node.
+        @type     tags: C{list} of C{str}
+
+        @keyword  metadata: Metadata dictionary for instance.
+        @type     metadata: C{dict}
+
+        @keyword  boot_disk:  Persistent boot disk to attach
+        @type     L{StorageVolume}
+
+        @return:  A tuple containing a request string and a node_data dict.
+        @rtype:   C{tuple} of C{str} and C{dict}
+        """
+        node_data = {}
+        node_data['machineType'] = size.extra['selfLink']
+        node_data['name'] = name
+        if tags:
+            node_data['tags'] = {'items': tags}
+        if metadata:
+            node_data['metadata'] = metadata
+        if boot_disk:
+            disks = [{'kind': 'compute#attachedDisk',
+                      'boot': True,
+                      'type': 'PERSISTENT',
+                      'mode': 'READ_WRITE',
+                      'deviceName': boot_disk.name,
+                      'zone': boot_disk.extra['zone'].extra['selfLink'],
+                      'source': boot_disk.extra['selfLink']}]
+            node_data['disks'] = disks
+            node_data['kernel'] = image.extra['preferredKernel']
+        else:
+            node_data['image'] = image.extra['selfLink']
+
+        ni = [{'kind': 'compute#instanceNetworkInterface',
+               'accessConfigs': [{'name': 'External NAT',
+                                  'type': 'ONE_TO_ONE_NAT'}],
+               'network': network.extra['selfLink']}]
+        node_data['networkInterfaces'] = ni
+
+        request = '/zones/%s/instances' % location.name
+
+        return request, node_data
+
+    def create_node(self, name, size, image, location=None,
+                    ex_network='default', ex_tags=None, ex_metadata=None,
+                    ex_boot_disk=None):
+        """
+        Create a new node and return a node object for the node.
+
+        @param  name: The name of the node to create.
+        @type   name: C{str}
+
+        @param  size: The machine type to use.
+        @type   size: C{str} or L{GCENodeSize}
+
+        @param  image: The image to use to create the node (or, if attaching
+                       a persistent disk, the image used to create the disk)
+        @type   image: C{str} or L{NodeImage}
+
+        @keyword  location: The location (zone) to create the node in.
+        @type     location: C{str} or L{NodeLocation} or L{GCEZone} or C{None}
+
+        @keyword  ex_network: The network to associate with the node.
+        @type     ex_network: C{str} or L{GCENetwork}
+
+        @keyword  ex_tags: A list of tags to assiciate with the node.
+        @type     ex_tags: C{list} of C{str} or C{None}
+
+        @keyword  ex_metadata: Metadata dictionary for instance.
+        @type     ex_metadata: C{dict} or C{None}
+
+        @keyword  ex_boot_disk: The boot disk to attach to the instance.
+        @type     ex_boot_disk: L{StorageVolume}
+
+        @return:  A Node object for the new node.
+        @rtype:   L{Node}
+        """
+        location = location or self.zone
+        if not hasattr(location, 'name'):
+            location = self.ex_get_zone(location)
+        if not hasattr(size, 'name'):
+            size = self.ex_get_size(size, location)
+        if not hasattr(ex_network, 'name'):
+            ex_network = self.ex_get_network(ex_network)
+        if not hasattr(image, 'name'):
+            image = self.ex_get_image(image)
+
+        request, node_data = self._create_node_req(name, size, image,
+                                                   location, ex_network,
+                                                   ex_tags, ex_metadata,
+                                                   ex_boot_disk)
+        response = self.connection.async_request(request, method='POST',
+                                                 data=node_data).object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+
+        return self.ex_get_node(name, location.name)
+
+    def ex_create_multiple_nodes(self, base_name, size, image, number,
+                                 location=None, ex_network='default',
+                                 ex_tags=None, ex_metadata=None,
+                                 ignore_errors=True,
+                                 timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
+        """
+        Create multiple nodes and return a list of Node objects.
+
+        Nodes will be named with the base name and a number.  For example, if
+        the base name is 'libcloud' and you create 3 nodes, they will be
+        named::
+            libcloud-000
+            libcloud-001
+            libcloud-002
+
+        @param  base_name: The base name of the nodes to create.
+        @type   base_name: C{str}
+
+        @param  size: The machine type to use.
+        @type   size: C{str} or L{GCENodeSize}
+
+        @param  image: The image to use to create the nodes.
+        @type   image: C{str} or L{NodeImage}
+
+        @param  number: The number of nodes to create.
+        @type   number: C{int}
+
+        @keyword  location: The location (zone) to create the nodes in.
+        @type     location: C{str} or L{NodeLocation} or L{GCEZone} or C{None}
+
+        @keyword  ex_network: The network to associate with the nodes.
+        @type     ex_network: C{str} or L{GCENetwork}
+
+        @keyword  ex_tags: A list of tags to assiciate with the nodes.
+        @type     ex_tags: C{list} of C{str} or C{None}
+
+        @keyword  ex_metadata: Metadata dictionary for instances.
+        @type     ex_metadata: C{dict} or C{None}
+
+        @keyword  ignore_errors: If True, don't raise Exceptions if one or
+                                 more nodes fails.
+        @type     ignore_errors: C{bool}
+
+        @keyword  timeout: The number of seconds to wait for all nodes to be
+                           created before timing out.
+
+        @return:  A list of Node objects for the new nodes.
+        @rtype:   C{list} of L{Node}
+        """
+        node_data = {}
+        location = location or self.zone
+        if not hasattr(location, 'name'):
+            location = self.ex_get_zone(location)
+        if not hasattr(size, 'name'):
+            size = self.ex_get_size(size, location)
+        if not hasattr(ex_network, 'name'):
+            ex_network = self.ex_get_network(ex_network)
+        if not hasattr(image, 'name'):
+            image = self.ex_get_image(image)
+
+        node_list = [None] * number
+        responses = []
+        for i in range(number):
+            name = '%s-%03d' % (base_name, i)
+            request, node_data = self._create_node_req(name, size, image,
+                                                       location, ex_network,
+                                                       ex_tags, ex_metadata)
+            response = self.connection.request(request, method='POST',
+                                               data=node_data)
+            responses.append(response.object)
+
+        start_time = time.time()
+        complete = False
+        while not complete:
+            if (time.time() - start_time >= timeout):
+                raise Exception("Timeout (%s sec) while waiting for multiple "
+                                "instances")
+            complete = True
+            for i, operation in enumerate(responses):
+                if operation is None:
+                    continue
+                response = self.connection.request(
+                    operation['selfLink']).object
+                if response['status'] == 'DONE':
+                    responses[i] = None
+                    name = '%s-%03d' % (base_name, i)
+                    if 'error' in response:
+                        if ignore_errors:
+                            error = response['error']['errors'][0]
+                            node_list[i] = GCEFailedNode(name, error)
+                        else:
+                            self._categorize_error(response['error'])
+                    else:
+                        node_list[i] = self.ex_get_node(name, location.name)
+                else:
+                    complete = False
+                    time.sleep(2)
+        return node_list
+
+    def create_volume(self, size, name, location=None, image=None,
+                      snapshot=None):
+        """
+        Create a volume (disk).
+
+        @param  size: Size of volume to create (in GB). Can be None if image
+                      or snapshot is supplied.
+        @type   size: C{int} or C{str} or C{None}
+
+        @param  name: Name of volume to create
+        @type   name: C{str}
+
+        @keyword  location: Location (zone) to create the volume in
+        @type     location: C{str} or L{GCEZone} or L{NodeLocation} or C{None}
+
+        @keyword  image: Image to create disk from.
+        @type     image: L{NodeImage} or C{str} or C{None}
+
+        @keyword  snapshot: Snapshot to create image from
+        @type     snapshot: C{str}
+
+        @return:  Storage Volume object
+        @rtype:   L{StorageVolume}
+        """
+        volume_data = {}
+        params = None
+        volume_data['name'] = name
+        if size:
+            volume_data['sizeGb'] = str(size)
+        if image:
+            if not hasattr(image, 'name'):
+                image = self.ex_get_image(image)
+            params = {'sourceImage': image.extra['selfLink']}
+        if snapshot:
+            volume_data['sourceSnapshot'] = snapshot
+        location = location or self.zone
+        if not hasattr(location, 'name'):
+            location = self.ex_get_zone(location)
+        request = '/zones/%s/disks' % location.name
+        response = self.connection.async_request(request, method='POST',
+                                                 data=volume_data,
+                                                 params=params).object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+
+        return self.ex_get_volume(name)
+
+    def ex_update_firewall(self, firewall):
+        """
+        Update a firewall with new values.
+
+        To update, change the attributes of the firewall object and pass the
+        updated object to the method.
+
+        @param  firewall: A firewall object with updated values.
+        @type   firewall: L{GCEFirewall}
+
+        @return:  An object representing the new state of the firewall.
+        @rtype:   L{GCEFirewall}
+        """
+        firewall_data = {}
+        firewall_data['name'] = firewall.name
+        firewall_data['allowed'] = firewall.allowed
+        firewall_data['network'] = firewall.network.extra['selfLink']
+        if firewall.source_ranges:
+            firewall_data['sourceRanges'] = firewall.source_ranges
+        if firewall.source_tags:
+            firewall_data['sourceTags'] = firewall.source_tags
+        if firewall.extra['description']:
+            firewall_data['description'] = firewall.extra['description']
+
+        request = '/global/firewalls/%s' % firewall.name
+
+        response = self.connection.async_request(request, method='PUT',
+                                                 data=firewall_data).object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+
+        return self.ex_get_firewall(firewall.name)
+
+    def reboot_node(self, node):
+        """
+        Reboot a node.
+
+        @param  node: Node to be rebooted
+        @type   node: L{Node}
+
+        @return:  True if successful, False if not
+        @rtype:   C{bool}
+        """
+        request = '/zones/%s/instances/%s/reset' % (node.extra['zone'].name,
+                                                    node.name)
+        response = self.connection.async_request(request, method='POST',
+                                                 data='ignored').object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        else:
+            return True
+
+    def ex_set_node_tags(self, node, tags):
+        """
+        Set the tags on a Node instance.
+
+        Note that this updates the node object directly.
+
+        @param  node: Node object
+        @type   node: L{Node}
+
+        @param  tags: List of tags to apply to the object
+        @type   tags: C{list} of C{str}
+
+        @return:  True if successful
+        @rtype:   C{bool}
+        """
+        request = '/zones/%s/instances/%s/setTags' % (node.extra['zone'].name,
+                                                      node.name)
+
+        tags_data = {}
+        tags_data['items'] = tags
+        tags_data['fingerprint'] = node.extra['tags_fingerprint']
+
+        response = self.connection.async_request(request, method='POST',
+                                                 data=tags_data).object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        else:
+            new_node = self.ex_get_node(node.name)
+            node.extra['tags'] = new_node.extra['tags']
+            node.extra['tags_fingerprint'] = new_node.extra['tags_fingerprint']
+            return True
+
+    def deploy_node(self, name, size, image, script, location=None,
+                    ex_network='default', ex_tags=None):
+        """
+        Create a new node and run a script on start-up.
+
+        @param  name: The name of the node to create.
+        @type   name: C{str}
+
+        @param  size: The machine type to use.
+        @type   size: C{str} or L{GCENodeSize}
+
+        @param  image: The image to use to create the node.
+        @type   image: C{str} or L{NodeImage}
+
+        @param  script: File path to start-up script
+        @type   script: C{str}
+
+        @keyword  location: The location (zone) to create the node in.
+        @type     location: C{str} or L{NodeLocation} or L{GCEZone} or C{None}
+
+        @keyword  ex_network: The network to associate with the node.
+        @type     ex_network: C{str} or L{GCENetwork}
+
+        @keyword  ex_tags: A list of tags to assiciate with the node.
+        @type     ex_tags: C{list} of C{str} or C{None}
+
+        @return:  A Node object for the new node.
+        @rtype:   L{Node}
+        """
+        with open(script, 'r') as f:
+            script_data = f.read()
+        metadata = {'items': [{'key': 'startup-script',
+                               'value': script_data}]}
+
+        return self.create_node(name, size, image, location=location,
+                                ex_network=ex_network, ex_tags=ex_tags,
+                                ex_metadata=metadata)
+
+    def attach_volume(self, node, volume, device=None, ex_mode=None,
+                      ex_boot=False):
+        """
+        Attach a volume to a node.
+
+        If volume is None, a scratch disk will be created and attached.
+
+        @param  node: The node to attach the volume to
+        @type   node: L{Node}
+
+        @param  volume: The volume to attach. If none, a scratch disk will be
+                        attached.
+        @type   volume: L{StorageVolume} or C{None}
+
+        @keyword  device: The device name to attach the volume as. Defaults to
+                          volume name.
+        @type     device: C{str}
+
+        @keyword  ex_mode: Either 'READ_WRITE' or 'READ_ONLY'
+        @type     ex_mode: C{str}
+
+        @keyword  ex_boot: If true, disk will be attached as a boot disk
+        @type     ex_boot: C{bool}
+
+        @return:  True if successful
+        @rtype:   C{bool}
+        """
+        volume_data = {}
+        if volume is None:
+            volume_data['type'] = 'SCRATCH'
+        else:
+            volume_data['type'] = 'PERSISTENT'
+            volume_data['source'] = volume.extra['selfLink']
+        volume_data['kind'] = 'compute#attachedDisk'
+        volume_data['mode'] = ex_mode or 'READ_WRITE'
+
+        if device:
+            volume_data['deviceName'] = device
+        else:
+            volume_data['deviceName'] = volume.name
+
+        volume_data['boot'] = ex_boot
+
+        request = '/zones/%s/instances/%s/attachDisk' % (
+            node.extra['zone'].name, node.name)
+        response = self.connection.async_request(request, method='POST',
+                                                 data=volume_data).object
+        if 'error' in response:
+            self._cateforize_error(response['error'])
+        else:
+            return True
+
+    def detach_volume(self, volume, ex_node=None):
+        """
+        Detach a volume from a node.
+
+        @param  volume: Volume object to detach
+        @type   volume: L{StorageVolume}
+
+        @keyword  ex_node: Node object to detach volume from (required)
+        @type     ex_node: L{Node}
+
+        @return:  True if successful
+        @rtype:   C{bool}
+        """
+        if not ex_node:
+            return False
+        request = '/zones/%s/instances/%s/detachDisk?deviceName=%s' % (
+            ex_node.extra['zone'].name, ex_node.name, volume.name)
+
+        response = self.connection.async_request(request, method='POST',
+                                                 data='ignored').object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        else:
+            return True
+
+    def ex_destroy_address(self, address):
+        """
+        Destroy a static address.
+
+        @param  address: Address object to destroy
+        @type   address: L{GCEAddress}
+
+        @return:  True if successful
+        @rtype:   C{bool}
+        """
+        request = '/regions/%s/addresses/%s' % (address.region, address.name)
+
+        response = self.connection.async_request(request,
+                                                 method='DELETE').object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        else:
+            return True
+
+    def ex_destroy_firewall(self, firewall):
+        """
+        Destroy a firewall.
+
+        @param  firewall: Firewall object to destroy
+        @type   firewall: L{GCEFirewall}
+
+        @return:  True if successful
+        @rtype:   C{bool}
+        """
+        request = '/global/firewalls/%s' % firewall.name
+        response = self.connection.async_request(request,
+                                                 method='DELETE').object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        else:
+            return True
+
+    def ex_destroy_network(self, network):
+        """
+        Destroy a network.
+
+        @param  network: Network object to destroy
+        @type   network: L{GCENetwork}
+
+        @return:  True if successful
+        @rtype:   C{bool}
+        """
+        request = '/global/networks/%s' % network.name
+        response = self.connection.async_request(request,
+                                                 method='DELETE').object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        else:
+            return True
+
+    def destroy_node(self, node):
+        """
+        Destroy a node.
+
+        @param  node: Node object to destroy
+        @type   node: L{Node}
+
+        @return:  True if successful
+        @rtype:   C{bool}
+        """
+        request = '/zones/%s/instances/%s' % (node.extra['zone'].name,
+                                              node.name)
+        response = self.connection.async_request(request,
+                                                 method='DELETE').object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        else:
+            return True
+
+    def ex_destroy_multiple_nodes(self, nodelist, ignore_errors=True,
+                                  timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
+        """
+        Destroy multiple nodes at once.
+
+        @param  nodelist: List of nodes to destroy
+        @type   nodelist: C{list} of L{Node}
+
+        @keyword  ignore_errors: If true, don't raise an exception if one or
+                                 more nodes fails to be destroyed.
+        @type     ignore_errors: C{bool}
+
+        @keyword  timeout: Number of seconds to wait for all nodes to be
+                           destroyed.
+        @type     timeout: C{int}
+
+        @return:  A list of boolean values.  One for each node.  True means
+                  that the node was successfully destroyed.
+        @rtype:   C{list} of C{bool}
+        """
+        responses = []
+        success = [False] * len(nodelist)
+        complete = False
+        start_time = time.time()
+        for node in nodelist:
+            request = '/zones/%s/instances/%s' % (node.extra['zone'].name,
+                                                  node.name)
+            response = self.connection.request(request, method='DELETE').object
+            responses.append(response)
+
+        while not complete:
+            if (time.time() - start_time >= timeout):
+                raise Exception("Timeout (%s sec) while waiting to delete "
+                                "multiple instances")
+            complete = True
+            for i, operation in enumerate(responses):
+                if operation is None:
+                    continue
+                response = self.connection.request(
+                    operation['selfLink']).object
+                if response['status'] == 'DONE':
+                    responses[i] = None
+                    if 'error' in response:
+                        if ignore_errors:
+                            success[i] = False
+                        else:
+                            self._categorize_error(response['error'])
+                    else:
+                        success[i] = True
+                else:
+                    complete = False
+                    time.sleep(2)
+        return success
+
+    def destroy_volume(self, volume):
+        """
+        Destroy a volume.
+
+        @param  volume: Volume object to destroy
+        @type   volume: L{StorageVolume}
+
+        @return:  True if successful
+        @rtype:   C{bool}
+        """
+        request = '/zones/%s/disks/%s' % (volume.extra['zone'].name,
+                                          volume.name)
+        response = self.connection.async_request(request,
+                                                 method='DELETE').object
+        if 'error' in response:
+            self._categorize_error(response['error'])
+        else:
+            return True
+
+    def ex_get_address(self, name, region=None):
+        """
+        Return an Address object based on an address name and optional region.
+
+        @param  name: The name of the address
+        @type   name: C{str}
+
+        @keyword  region: The region to search for the address in
+        @type     region: C{str} or C{None}
+
+        @return:  An Address object for the address
+        @rtype:   L{GCEAddress}
+        """
+        address_region = region or self._find_zone(name, 'addresses',
+                                                   region=True)
+        request = '/regions/%s/addresses/%s' % (address_region, name)
+        response = self.connection.request(request, method='GET').object
+        return self._to_address(response)
+
+    def ex_get_firewall(self, name):
+        """
+        Return a Firewall object based on the firewall name.
+
+        @param  name: The name of the firewall
+        @type   name: C{str}
+
+        @return:  A GCEFirewall object
+        @rtype:   L{GCEFirewall}
+        """
+        request = '/global/firewalls/%s' % name
+        response = self.connection.request(request, method='GET').object
+        return self._to_firewall(response)
+
+    def ex_get_image(self, partial_name):
+        """
+        Return an NodeImage object based on the name or link provided.
+
+        @param  partial_name: The name, partial name, or full path of a GCE
+                              image.
+        @type   partial_name: C{str}
+
+        @return:  NodeImage object based on provided information
+        @rtype:   L{NodeImage}
+        """
+        if partial_name.startswith('https://'):
+            response = self.connection.request(partial_name, method='GET')
+            return self._to_node_image(response.object)
+        image = self._match_images(None, partial_name)
+        if not image:
+            if partial_name.startswith('debian'):
+                image = self._match_images('debian-cloud', partial_name)
+            elif partial_name.startswith('centos'):
+                image = self._match_images('centos-cloud', partial_name)
+
+        return image
+
+    def ex_get_network(self, name):
+        """
+        Return a Network object based on a network name.
+
+        @param  name: The name of the network
+        @type   name: C{str}
+
+        @return:  A Network object for the network
+        @rtype:   L{GCENetwork}
+        """
+        request = '/global/networks/%s' % name
+        response = self.connection.request(request, method='GET').object
+        return self._to_network(response)
+
+    def ex_get_node(self, name, zone=None):
+        """
+        Return a Node object based on a node name and optional zone.
+
+        @param  name: The name of the node
+        @type   name: C{str}
+
+        @keyword  zone: The zone to search for the node in
+        @type     zone: C{str} or L{GCEZone} or L{NodeLocation} or C{None}
+
+        @return:  A Node object for the node
+        @rtype:   L{Node}
+        """
+        zone = zone or self.zone or self._find_zone(name, 'instances')
+        if not hasattr(zone, 'name'):
+            zone = self.ex_get_zone(zone)
+        request = '/zones/%s/instances/%s' % (zone.name, name)
+        response = self.connection.request(request, method='GET').object
+        return self._to_node(response)
+
+    def ex_get_project(self):
+        """
+        Return a Project object with project-wide information.
+
+        @return:  A GCEProject object
+        @rtype:   L{GCEProject}
+        """
+        response = self.connection.request('', method='GET').object
+        return self._to_project(response)
+
+    def ex_get_size(self, name, zone=None):
+        """
+        Return a size object based on a machine type name and zone.
+
+        @param  name: The name of the node
+        @type   name: C{str}
+
+        @keyword  zone: The zone to search for the machine type in
+        @type     zone: C{str} or L{GCEZone} or L{NodeLocation} or C{None}
+
+        @return:  A GCENodeSize object for the machine type
+        @rtype:   L{GCENodeSize}
+        """
+        zone = zone or self.zone
+        if not hasattr(zone, 'name'):
+            zone = self.ex_get_zone(zone)
+        request = '/zones/%s/machineTypes/%s' % (zone.name, name)
+        response = self.connection.request(request, method='GET').object
+        return self._to_node_size(response)
+
+    def ex_get_volume(self, name, zone=None):
+        """
+        Return a Volume object based on a volume name and optional zone.
+
+        @param  name: The name of the volume
+        @type   name: C{str}
+
+        @keyword  zone: The zone to search for the volume in
+        @type     zone: C{str} or L{GCEZone} or L{NodeLocation} or C{None}
+
+        @return:  A StorageVolume object for the volume
+        @rtype:   L{StorageVolume}
+        """
+        zone = zone or self.zone or self.find_zone(name, 'disks')
+        if not hasattr(zone, 'name'):
+            zone = self.ex_get_zone(zone)
+        request = '/zones/%s/disks/%s' % (zone.name, name)
+        response = self.connection.request(request, method='GET').object
+        return self._to_storage_volume(response)
+
+    def ex_get_zone(self, name):
+        """
+        Return a Zone object based on the zone name.
+
+        @param  name: The name of the zone.
+        @type   name: C{str}
+
+        @return:  A GCEZone object for the zone
+        @rtype:   L{GCEZone}
+        """
+        if name.startswith('https://'):
+            short_name = name.split('/')[-1]
+            request = name
+        else:
+            short_name = name
+            request = '/zones/%s' % name
+        # Check zone cache first
+        if short_name in self.zone_dict:
+            return self.zone_dict[short_name]
+        # Otherwise, look up zone information
+        response = self.connection.request(request, method='GET').object
+        return self._to_zone(response)
+
+    def _to_address(self, address):
+        """
+        Return an Address object from the json-response dictionary.
+
+        @param  address: The dictionary describing the address.
+        @type   address: C{dict}
+
+        @return: Address object
+        @rtype: L{GCEAddress}
+        """
+        extra = {}
+
+        extra['selfLink'] = address['selfLink']
+        extra['status'] = address['status']
+        extra['region'] = address['region']
+        extra['creationTimestamp'] = address['creationTimestamp']
+        region = address['region'].split('/')[-1]
+
+        return GCEAddress(id=address['id'], name=address['name'],
+                          address=address['address'],
+                          region=region, driver=self, extra=extra)
+
+    def _to_firewall(self, firewall):
+        """
+        Return a Firewall object from the json-response dictionary.
+
+        @param  firewall: The dictionary describing the firewall.
+        @type   firewall: C{dict}
+
+        @return: Firewall object
+        @rtype: L{GCEFirewall}
+        """
+        extra = {}
+        extra['selfLink'] = firewall['selfLink']
+        extra['creationTimestamp'] = firewall['creationTimestamp']
+        extra['description'] = firewall.get('description')
+        extra['network_name'] = firewall['network'].split('/')[-1]
+
+        network = self.ex_get_network(extra['network_name'])
+        source_ranges = firewall.get('sourceRanges')
+        source_tags = firewall.get('sourceTags')
+
+        return GCEFirewall(id=firewall['id'], name=firewall['name'],
+                           allowed=firewall['allowed'], network=network,
+                           source_ranges=source_ranges,
+                           source_tags=source_tags,
+                           driver=self, extra=extra)
+
+    def _to_network(self, network):
+        """
+        Return a Network object from the json-response dictionary.
+
+        @param  network: The dictionary describing the network.
+        @type   network: C{dict}
+
+        @return: Network object
+        @rtype: L{GCENetwork}
+        """
+        extra = {}
+
+        extra['selfLink'] = network['selfLink']
+        extra['gatewayIPv4'] = network['gatewayIPv4']
+        extra['description'] = network.get('description')
+        extra['creationTimestamp'] = network['creationTimestamp']
+
+        return GCENetwork(id=network['id'], name=network['name'],
+                          cidr=network['IPv4Range'],
+                          driver=self, extra=extra)
+
+    def _to_node_image(self, image):
+        """
+        Return an Image object from the json-response dictionary.
+
+        @param  image: The dictionary describing the image.
+        @type   image: C{dict}
+
+        @return: Image object
+        @rtype: L{NodeImage}
+        """
+        extra = {}
+        extra['preferredKernel'] = image['preferredKernel']
+        extra['description'] = image['description']
+        extra['creationTimestamp'] = image['creationTimestamp']
+        extra['selfLink'] = image['selfLink']
+        return NodeImage(id=image['id'], name=image['name'], driver=self,
+                         extra=extra)
+
+    def _to_node_location(self, location):
+        """
+        Return a Location object from the json-response dictionary.
+
+        @param  location: The dictionary describing the location.
+        @type   location: C{dict}
+
+        @return: Location object
+        @rtype: L{NodeLocation}
+        """
+        return NodeLocation(id=location['id'], name=location['name'],
+                            country=location['name'].split('-')[0],
+                            driver=self)
+
+    def _to_node(self, node):
+        """
+        Return a Node object from the json-response dictionary.
+
+        @param  node: The dictionary describing the node.
+        @type   node: C{dict}
+
+        @return: Node object
+        @rtype: L{Node}
+        """
+        public_ips = []
+        private_ips = []
+        extra = {}
+
+        extra['status'] = node['status']
+        extra['description'] = node.get('description')
+        extra['zone'] = self.ex_get_zone(node['zone'])
+        extra['image'] = node.get('image')
+        extra['disks'] = node['disks']
+        extra['networkInterfaces'] = node['networkInterfaces']
+        extra['id'] = node['id']
+        extra['selfLink'] = node['selfLink']
+        extra['name'] = node['name']
+        extra['metadata'] = node['metadata']
+        extra['tags_fingerprint'] = node['tags']['fingerprint']
+
+        if 'items' in node['tags']:
+            tags = node['tags']['items']
+        else:
+            tags = []
+        extra['tags'] = tags
+
+        for network_interface in node['networkInterfaces']:
+            private_ips.append(network_interface['networkIP'])
+            for access_config in network_interface['accessConfigs']:
+                public_ips.append(access_config['natIP'])
+
+        return Node(id=node['id'], name=node['name'],
+                    state=self.NODE_STATE_MAP[node['status']],
+                    public_ips=public_ips, private_ips=private_ips,
+                    driver=self, size=node['machineType'],
+                    image=node.get('image'), extra=extra)
+
+    def _to_node_size(self, machine_type):
+        """
+        Return a Size object from the json-response dictionary.
+
+        @param  machine_type: The dictionary describing the machine.
+        @type   machine_type: C{dict}
+
+        @return: Size object
+        @rtype: L{GCENodeSize}
+        """
+        extra = {}
+        extra['selfLink'] = machine_type['selfLink']
+        extra['zone'] = self.ex_get_zone(machine_type['zone'])
+        extra['description'] = machine_type['description']
+        extra['guestCpus'] = machine_type['guestCpus']
+        extra['creationTimestamp'] = machine_type['creationTimestamp']
+        try:
+            price = self._get_size_price(size_id=machine_type['name'])
+        except KeyError:
+            price = None
+
+        return GCENodeSize(id=machine_type['id'], name=machine_type['name'],
+                           ram=machine_type['memoryMb'],
+                           disk=machine_type['imageSpaceGb'],
+                           bandwidth=0, price=price, driver=self, extra=extra)
+
+    def _to_project(self, project):
+        """
+        Return a Project object from the json-response dictionary.
+
+        @param  project: The dictionary describing the project.
+        @type   project: C{dict}
+
+        @return: Project object
+        @rtype: L{GCEProject}
+        """
+        extra = {}
+        extra['selfLink'] = project['selfLink']
+        extra['creationTimestamp'] = project['creationTimestamp']
+        extra['description'] = project['description']
+        metadata = project['commonInstanceMetadata'].get('items')
+
+        return GCEProject(id=project['id'], name=project['name'],
+                          metadata=metadata, quotas=project['quotas'],
+                          driver=self, extra=extra)
+
+    def _to_storage_volume(self, volume):
+        """
+        Return a Volume object from the json-response dictionary.
+
+        @param  volume: The dictionary describing the volume.
+        @type   volume: C{dict}
+
+        @return: Volume object
+        @rtype: L{StorageVolume}
+        """
+        extra = {}
+        extra['selfLink'] = volume['selfLink']
+        extra['zone'] = self.ex_get_zone(volume['zone'])
+        extra['status'] = volume['status']
+        extra['creationTimestamp'] = volume['creationTimestamp']
+
+        return StorageVolume(id=volume['id'], name=volume['name'],
+                             size=volume['sizeGb'], driver=self, extra=extra)
+
+    def _to_zone(self, zone):
+        """
+        Return a Zone object from the json-response dictionary.
+
+        @param  zone: The dictionary describing the zone.
+        @type   zone: C{dict}
+
+        @return: Zone object
+        @rtype: L{GCEZone}
+        """
+        extra = {}
+        extra['selfLink'] = zone['selfLink']
+        extra['creationTimestamp'] = zone['creationTimestamp']
+        extra['description'] = zone['description']
+
+        deprecated = zone.get('deprecated')
+
+        return GCEZone(id=zone['id'], name=zone['name'], status=zone['status'],
+                       maintenance_windows=zone['maintenanceWindows'],
+                       quotas=zone['quotas'], deprecated=deprecated,
+                       driver=self, extra=extra)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/b0782175/libcloud/compute/providers.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/providers.py b/libcloud/compute/providers.py
index d4f7a11..fbc9c63 100644
--- a/libcloud/compute/providers.py
+++ b/libcloud/compute/providers.py
@@ -65,6 +65,8 @@ DRIVERS = {
         ('libcloud.compute.drivers.cloudsigma', 'CloudSigmaZrhNodeDriver'),
     Provider.CLOUDSIGMA_US:
         ('libcloud.compute.drivers.cloudsigma', 'CloudSigmaLvsNodeDriver'),
+    Provider.GCE:
+        ('libcloud.compute.drivers.gce', 'GCENodeDriver'),
     Provider.GOGRID:
         ('libcloud.compute.drivers.gogrid', 'GoGridNodeDriver'),
     Provider.RACKSPACE:

http://git-wip-us.apache.org/repos/asf/libcloud/blob/b0782175/libcloud/compute/types.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/types.py b/libcloud/compute/types.py
index 15cb266..c8e2fb4 100644
--- a/libcloud/compute/types.py
+++ b/libcloud/compute/types.py
@@ -46,6 +46,7 @@ class Provider(object):
     @cvar RACKSPACE: Rackspace next-gen OpenStack based Cloud Servers
     @cvar RACKSPACE_FIRST_GEN: Rackspace First Gen Cloud Servers
     @cvar SLICEHOST: Slicehost.com
+    @cvar GCE: Google Compute Engine
     @cvar GOGRID: GoGrid
     @cvar VPSNET: VPS.net
     @cvar LINODE: Linode.com
@@ -75,6 +76,7 @@ class Provider(object):
     EC2 = 'ec2_us_east'
     RACKSPACE = 'rackspace'
     SLICEHOST = 'slicehost'
+    GCE = 'gce'
     GOGRID = 'gogrid'
     VPSNET = 'vpsnet'
     LINODE = 'linode'

http://git-wip-us.apache.org/repos/asf/libcloud/blob/b0782175/libcloud/test/common/test_google.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_google.py b/libcloud/test/common/test_google.py
new file mode 100644
index 0000000..35da4b2
--- /dev/null
+++ b/libcloud/test/common/test_google.py
@@ -0,0 +1,241 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Tests for Google Connection classes.
+"""
+import datetime
+import sys
+import unittest
+
+try:
+    import simplejson as json
+except ImportError:
+    import json
+
+from libcloud.utils.py3 import httplib
+
+from libcloud.test import MockHttp, LibcloudTestCase, MockResponse
+from libcloud.common.google import (GoogleAuthError,
+                                    GoogleBaseAuthConnection,
+                                    GoogleInstalledAppAuthConnection,
+                                    GoogleServiceAcctAuthConnection,
+                                    GoogleBaseConnection)
+from libcloud.test.secrets import GCE_PARAMS, GCE_KEYWORD_PARAMS
+
+# Skip some tests if PyCrypto is unavailable
+try:
+    from Crypto.Hash import SHA256
+except ImportError:
+    SHA256 = None
+
+
+class MockJsonResponse(object):
+    def __init__(self, body):
+        self.object = body
+
+
+class GoogleBaseAuthConnectionTest(LibcloudTestCase):
+    """
+    Tests for GoogleBaseAuthConnection
+    """
+    GoogleBaseAuthConnection._now = lambda x: datetime.datetime(2013, 6, 26,
+                                                                19, 0, 0)
+
+    def setUp(self):
+        GoogleBaseAuthConnection.conn_classes = (GoogleAuthMockHttp,
+                                                 GoogleAuthMockHttp)
+        self.mock_scope = ['https://www.googleapis.com/auth/foo']
+        kwargs = {'scope': self.mock_scope}
+        self.conn = GoogleInstalledAppAuthConnection(*GCE_PARAMS,
+                                                     **kwargs)
+
+    def test_add_default_headers(self):
+        old_headers = {}
+        expected_headers = {
+            'Content-Type': 'application/x-www-form-urlencoded',
+            'Host': 'accounts.google.com'}
+        new_headers = self.conn.add_default_headers(old_headers)
+        self.assertEqual(new_headers, expected_headers)
+
+    def test_token_request(self):
+        request_body = {'code': 'asdf', 'client_id': self.conn.user_id,
+                        'client_secret': self.conn.key,
+                        'redirect_uri': self.conn.redirect_uri,
+                        'grant_type': 'authorization_code'}
+        new_token = self.conn._token_request(request_body)
+        self.assertEqual(new_token['access_token'], 'installedapp')
+        self.assertEqual(new_token['expire_time'], '2013-06-26T20:00:00Z')
+
+
+class GoogleInstalledAppAuthConnectionTest(LibcloudTestCase):
+    """
+    Tests for GoogleInstalledAppAuthConnection
+    """
+    GoogleInstalledAppAuthConnection.get_code = lambda x: '1234'
+
+    def setUp(self):
+        GoogleInstalledAppAuthConnection.conn_classes = (GoogleAuthMockHttp,
+                                                         GoogleAuthMockHttp)
+        self.mock_scope = ['https://www.googleapis.com/auth/foo']
+        kwargs = {'scope': self.mock_scope}
+        self.conn = GoogleInstalledAppAuthConnection(*GCE_PARAMS,
+                                                     **kwargs)
+
+    def test_refresh_token(self):
+        # This token info doesn't have a refresh token, so a new token will be
+        # requested
+        token_info1 = {'access_token': 'tokentoken', 'token_type': 'Bearer',
+                       'expires_in': 3600}
+        new_token1 = self.conn.refresh_token(token_info1)
+        self.assertEqual(new_token1['access_token'], 'installedapp')
+
+        # This token info has a refresh token, so it will be able to be
+        # refreshed.
+        token_info2 = {'access_token': 'tokentoken', 'token_type': 'Bearer',
+                       'expires_in': 3600, 'refresh_token': 'refreshrefresh'}
+        new_token2 = self.conn.refresh_token(token_info2)
+        self.assertEqual(new_token2['access_token'], 'refreshrefresh')
+
+        # Both sets should have refresh info
+        self.assertTrue('refresh_token' in new_token1)
+        self.assertTrue('refresh_token' in new_token2)
+
+
+class GoogleBaseConnectionTest(LibcloudTestCase):
+    """
+    Tests for GoogleBaseConnection
+    """
+    GoogleBaseConnection._get_token_info_from_file = lambda x: None
+    GoogleBaseConnection._write_token_info_to_file = lambda x: None
+    GoogleInstalledAppAuthConnection.get_code = lambda x: '1234'
+    GoogleServiceAcctAuthConnection.get_new_token = \
+        lambda x: x._token_request({})
+    GoogleBaseConnection._now = lambda x: datetime.datetime(2013, 6, 26,
+                                                            19, 0, 0)
+
+    def setUp(self):
+        GoogleBaseAuthConnection.conn_classes = (GoogleAuthMockHttp,
+                                                 GoogleAuthMockHttp)
+        self.mock_scope = ['https://www.googleapis.com/auth/foo']
+        kwargs = {'scope': self.mock_scope, 'auth_type': 'IA'}
+        self.conn = GoogleBaseConnection(*GCE_PARAMS, **kwargs)
+
+    def test_auth_type(self):
+        self.assertRaises(GoogleAuthError, GoogleBaseConnection, *GCE_PARAMS,
+                          **{'auth_type': 'XX'})
+
+        kwargs = {'scope': self.mock_scope}
+
+        if SHA256:
+            kwargs['auth_type'] = 'SA'
+            conn1 = GoogleBaseConnection(*GCE_PARAMS, **kwargs)
+            self.assertTrue(isinstance(conn1.auth_conn,
+                                       GoogleServiceAcctAuthConnection))
+
+        kwargs['auth_type'] = 'IA'
+        conn2 = GoogleBaseConnection(*GCE_PARAMS, **kwargs)
+        self.assertTrue(isinstance(conn2.auth_conn,
+                                   GoogleInstalledAppAuthConnection))
+
+    def test_add_default_headers(self):
+        old_headers = {}
+        new_expected_headers = {'Content-Type': 'application/json',
+                                'Host': 'www.googleapis.com'}
+        new_headers = self.conn.add_default_headers(old_headers)
+        self.assertEqual(new_headers, new_expected_headers)
+
+    def test_pre_connect_hook(self):
+        old_params = {}
+        old_headers = {}
+        new_expected_params = {}
+        new_expected_headers = {'Authorization': 'Bearer installedapp'}
+        new_params, new_headers = self.conn.pre_connect_hook(old_params,
+                                                             old_headers)
+        self.assertEqual(new_params, new_expected_params)
+        self.assertEqual(new_headers, new_expected_headers)
+
+    def test_encode_data(self):
+        data = {'key': 'value'}
+        json_data = '{"key": "value"}'
+        encoded_data = self.conn.encode_data(data)
+        self.assertEqual(encoded_data, json_data)
+
+    def test_has_completed(self):
+        body1 = {"endTime": "2013-06-26T10:05:07.630-07:00",
+                 "id": "3681664092089171723",
+                 "kind": "compute#operation",
+                 "status": "DONE",
+                 "targetId": "16211908079305042870"}
+        body2 = {"endTime": "2013-06-26T10:05:07.630-07:00",
+                 "id": "3681664092089171723",
+                 "kind": "compute#operation",
+                 "status": "RUNNING",
+                 "targetId": "16211908079305042870"}
+        response1 = MockJsonResponse(body1)
+        response2 = MockJsonResponse(body2)
+        self.assertTrue(self.conn.has_completed(response1))
+        self.assertFalse(self.conn.has_completed(response2))
+
+    def test_get_poll_request_kwargs(self):
+        body = {"endTime": "2013-06-26T10:05:07.630-07:00",
+                "id": "3681664092089171723",
+                "kind": "compute#operation",
+                "selfLink": "https://www.googleapis.com/operations-test"}
+        response = MockJsonResponse(body)
+        expected_kwargs = {'action':
+                           'https://www.googleapis.com/operations-test'}
+        kwargs = self.conn.get_poll_request_kwargs(response, None, {})
+        self.assertEqual(kwargs, expected_kwargs)
+
+    def test_morph_action_hook(self):
+        self.conn.request_path = '/compute/apiver/project/project-name'
+        action1 = ('https://www.googleapis.com/compute/apiver/project'
+                   '/project-name/instances')
+        action2 = '/instances'
+        expected_request = '/compute/apiver/project/project-name/instances'
+        request1 = self.conn.morph_action_hook(action1)
+        request2 = self.conn.morph_action_hook(action2)
+        self.assertEqual(request1, expected_request)
+        self.assertEqual(request2, expected_request)
+
+
+class GoogleAuthMockHttp(MockHttp):
+    """
+    Mock HTTP Class for Google Auth Connections.
+    """
+    json_hdr = {'content-type': 'application/json; charset=UTF-8'}
+
+    def _o_oauth2_token(self, method, url, body, headers):
+        token_info = {'access_token': 'tokentoken',
+                      'token_type': 'Bearer',
+                      'expires_in': 3600}
+        refresh_token = {'access_token': 'refreshrefresh',
+                         'token_type': 'Bearer',
+                         'expires_in': 3600}
+        ia_token = {'access_token': 'installedapp',
+                    'token_type': 'Bearer',
+                    'expires_in': 3600,
+                    'refresh_token': 'refreshrefresh'}
+        if 'code' in body:
+            body = json.dumps(ia_token)
+        elif 'refresh_token' in body:
+            body = json.dumps(refresh_token)
+        else:
+            body = json.dumps(token_info)
+        return (httplib.OK, body, self.json_hdr, httplib.responses[httplib.OK])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/b0782175/libcloud/test/compute/fixtures/gce/aggregated_addresses.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/aggregated_addresses.json b/libcloud/test/compute/fixtures/gce/aggregated_addresses.json
new file mode 100644
index 0000000..6ac222e
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/aggregated_addresses.json
@@ -0,0 +1,71 @@
+{
+  "id": "projects/project_name/aggregated/addresses",
+  "items": {
+    "regions/europe-west1": {
+      "addresses": [
+        {
+          "address": "192.158.29.247",
+          "creationTimestamp": "2013-06-26T09:51:47.506-07:00",
+          "description": "",
+          "id": "10955781597205896134",
+          "kind": "compute#address",
+          "name": "libcloud-demo-europe-address",
+          "region": "https://www.googleapis.com/compute/v1beta15/projects/project_name/regions/europe-west1",
+          "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/regions/europe-west1/addresses/libcloud-demo-europe-address",
+          "status": "RESERVED"
+        }
+      ]
+    },
+    "regions/us-central1": {
+      "addresses": [
+        {
+          "address": "173.255.113.20",
+          "creationTimestamp": "2013-06-26T12:21:40.625-07:00",
+          "description": "",
+          "id": "01531551729918243104",
+          "kind": "compute#address",
+          "name": "lcaddress",
+          "region": "https://www.googleapis.com/compute/v1beta15/projects/project_name/regions/us-central1",
+          "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/regions/us-central1/addresses/lcaddress",
+          "status": "RESERVED"
+        },
+        {
+          "address": "108.59.82.4",
+          "creationTimestamp": "2013-06-26T09:48:31.184-07:00",
+          "description": "",
+          "id": "17634862894218443422",
+          "kind": "compute#address",
+          "name": "libcloud-demo-address",
+          "region": "https://www.googleapis.com/compute/v1beta15/projects/project_name/regions/us-central1",
+          "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/regions/us-central1/addresses/libcloud-demo-address",
+          "status": "RESERVED"
+        },
+        {
+          "address": "173.255.114.104",
+          "creationTimestamp": "2013-06-04T16:28:43.764-07:00",
+          "description": "",
+          "id": "11879548153827627972",
+          "kind": "compute#address",
+          "name": "testaddress",
+          "region": "https://www.googleapis.com/compute/v1beta15/projects/project_name/regions/us-central1",
+          "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/regions/us-central1/addresses/testaddress",
+          "status": "RESERVED"
+        }
+      ]
+    },
+    "regions/us-central2": {
+      "warning": {
+        "code": "NO_RESULTS_ON_PAGE",
+        "data": [
+          {
+            "key": "scope",
+            "value": "regions/us-central2"
+          }
+        ],
+        "message": "There are no results for scope 'regions/us-central2' on this page."
+      }
+    }
+  },
+  "kind": "compute#addressAggregatedList",
+  "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/aggregated/addresses"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/b0782175/libcloud/test/compute/fixtures/gce/aggregated_disks.json
----------------------------------------------------------------------
diff --git a/libcloud/test/compute/fixtures/gce/aggregated_disks.json b/libcloud/test/compute/fixtures/gce/aggregated_disks.json
new file mode 100644
index 0000000..1190ab4
--- /dev/null
+++ b/libcloud/test/compute/fixtures/gce/aggregated_disks.json
@@ -0,0 +1,81 @@
+{
+  "id": "projects/project_name/aggregated/disks",
+  "items": {
+    "zones/europe-west1-a": {
+      "disks": [
+        {
+          "creationTimestamp": "2013-06-26T09:50:22.508-07:00",
+          "id": "0811494794539478718",
+          "kind": "compute#disk",
+          "name": "libcloud-demo-europe-boot-disk",
+          "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/zones/europe-west1-a/disks/libcloud-demo-europe-boot-disk",
+          "sizeGb": "10",
+          "status": "READY",
+          "zone": "https://www.googleapis.com/compute/v1beta15/projects/project_name/zones/europe-west1-a"
+        }
+      ]
+    },
+    "zones/europe-west1-b": {
+      "warning": {
+        "code": "NO_RESULTS_ON_PAGE",
+        "data": [
+          {
+            "key": "scope",
+            "value": "zones/europe-west1-b"
+          }
+        ],
+        "message": "There are no results for scope 'zones/europe-west1-b' on this page."
+      }
+    },
+    "zones/us-central1-a": {
+      "disks": [
+        {
+          "creationTimestamp": "2013-06-25T10:57:34.305-07:00",
+          "id": "14383387450728762434",
+          "kind": "compute#disk",
+          "name": "test-disk",
+          "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/zones/us-central1-a/disks/test-disk",
+          "sizeGb": "10",
+          "status": "READY",
+          "zone": "https://www.googleapis.com/compute/v1beta15/projects/project_name/zones/us-central1-a"
+        },
+        {
+          "creationTimestamp": "2013-06-26T09:47:09.178-07:00",
+          "id": "10880026303683859871",
+          "kind": "compute#disk",
+          "name": "libcloud-demo-boot-disk",
+          "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/zones/us-central1-a/disks/libcloud-demo-boot-disk",
+          "sizeGb": "10",
+          "status": "READY",
+          "zone": "https://www.googleapis.com/compute/v1beta15/projects/project_name/zones/us-central1-a"
+        }
+      ]
+    },
+    "zones/us-central1-b": {
+      "warning": {
+        "code": "NO_RESULTS_ON_PAGE",
+        "data": [
+          {
+            "key": "scope",
+            "value": "zones/us-central1-b"
+          }
+        ],
+        "message": "There are no results for scope 'zones/us-central1-b' on this page."
+      }
+    },
+    "zones/us-central2-a": {
+      "warning": {
+        "code": "NO_RESULTS_ON_PAGE",
+        "data": [
+          {
+            "key": "scope",
+            "value": "zones/us-central2-a"
+          }
+        ],
+        "message": "There are no results for scope 'zones/us-central2-a' on this page."
+      }
+    }
+  },
+  "kind": "compute#diskAggregatedList",
+  "selfLink": "https://www.googleapis.com/compute/v1beta15/projects/project_name/aggregated/disks"
+}
\ No newline at end of file