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/10/04 18:02:59 UTC

[3/5] GCE Loadbalancer Support and improvements in the compute

http://git-wip-us.apache.org/repos/asf/libcloud/blob/2115f9f6/libcloud/compute/drivers/gce.py
----------------------------------------------------------------------
diff --git a/libcloud/compute/drivers/gce.py b/libcloud/compute/drivers/gce.py
index 3b4f69e..b9c7ebe 100644
--- a/libcloud/compute/drivers/gce.py
+++ b/libcloud/compute/drivers/gce.py
@@ -25,7 +25,9 @@ import getpass
 
 from libcloud.common.google import GoogleResponse
 from libcloud.common.google import GoogleBaseConnection
+from libcloud.common.google import ResourceNotFoundError
 
+from libcloud.common.types import MalformedResponseError
 from libcloud.compute.base import Node, NodeDriver, NodeImage, NodeLocation
 from libcloud.compute.base import NodeSize, StorageVolume, UuidMixin
 from libcloud.compute.providers import Provider
@@ -55,33 +57,6 @@ def timestamp_to_datetime(timestamp):
     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
 
@@ -129,13 +104,53 @@ class GCEAddress(UuidMixin):
 
 class GCEFailedNode(object):
     """Dummy Node object for nodes that are not created."""
-    def __init__(self, name, error):
+    def __init__(self, name, error, code):
         self.name = name
         self.error = error
+        self.code = code
 
     def __repr__(self):
         return '<GCEFailedNode name="%s" error_code="%s">' % (
-            self.name, self.error['code'])
+            self.name, self.code)
+
+
+class GCEHealthCheck(UuidMixin):
+    """A GCE Http Health Check class."""
+    def __init__(self, id, name, path, port, interval, timeout,
+                 unhealthy_threshold, healthy_threshold, driver, extra=None):
+        self.id = str(id)
+        self.name = name
+        self.path = path
+        self.port = port
+        self.interval = interval
+        self.timeout = timeout
+        self.unhealthy_threshold = unhealthy_threshold
+        self.healthy_threshold = healthy_threshold
+        self.driver = driver
+        self.extra = extra
+        UuidMixin.__init__(self)
+
+    def __repr__(self):
+        return '<GCEHealthCheck id="%s" name="%s" path="%s" port="%s">' % (
+            self.id, self.name, self.path, self.port)
+
+    def destroy(self):
+        """
+        Destroy this Health Check.
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        return self.driver.ex_destroy_healthcheck(healthcheck=self)
+
+    def update(self):
+        """
+        Commit updated healthcheck values.
+
+        :return:  Updated Healthcheck object
+        :rtype:   :class:`GCEHealthcheck`
+        """
+        return self.driver.ex_update_healthcheck(healthcheck=self)
 
 
 class GCEFirewall(UuidMixin):
@@ -165,6 +180,42 @@ class GCEFirewall(UuidMixin):
         """
         return self.driver.ex_destroy_firewall(firewall=self)
 
+    def update(self):
+        """
+        Commit updated firewall values.
+
+        :return:  Updated Firewall object
+        :rtype:   :class:`GCEFirewall`
+        """
+        return self.driver.ex_update_firewall(firewall=self)
+
+
+class GCEForwardingRule(UuidMixin):
+    def __init__(self, id, name, region, address, protocol, targetpool, driver,
+                 extra=None):
+        self.id = str(id)
+        self.name = name
+        self.region = region
+        self.address = address
+        self.protocol = protocol
+        self.targetpool = targetpool
+        self.driver = driver
+        self.extra = extra
+        UuidMixin.__init__(self)
+
+    def __repr__(self):
+        return '<GCEForwardingRule id="%s" name="%s" address="%s">' % (
+            self.id, self.name, self.address)
+
+    def destroy(self):
+        """
+        Destroy this Forwarding Rule
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        return self.driver.ex_destroy_forwarding_rule(forwarding_rule=self)
+
 
 class GCENetwork(UuidMixin):
     """A GCE Network object class."""
@@ -210,10 +261,105 @@ class GCEProject(UuidMixin):
         self.extra = extra
         UuidMixin.__init__(self)
 
-    def _repr__(self):
+    def __repr__(self):
         return '<GCEProject id="%s" name="%s">' % (self.id, self.name)
 
 
+class GCERegion(UuidMixin):
+    def __init__(self, id, name, status, zones, quotas, deprecated, driver,
+                 extra=None):
+        self.id = str(id)
+        self.name = name
+        self.status = status
+        self.zones = zones
+        self.quotas = quotas
+        self.deprecated = deprecated
+        self.driver = driver
+        self.extra = extra
+        UuidMixin.__init__(self)
+
+    def __repr__(self):
+        return '<GCERegion id="%s" name="%s", status="%s">' % (
+            self.id, self.name, self.status)
+
+
+class GCETargetPool(UuidMixin):
+    def __init__(self, id, name, region, healthchecks, nodes, driver,
+                 extra=None):
+        self.id = str(id)
+        self.name = name
+        self.region = region
+        self.healthchecks = healthchecks
+        self.nodes = nodes
+        self.driver = driver
+        self.extra = extra
+        UuidMixin.__init__(self)
+
+    def __repr__(self):
+        return '<GCETargetPool id="%s" name="%s" region="%s">' % (
+            self.id, self.name, self.region.name)
+
+    def add_node(self, node):
+        """
+        Add a node to this target pool.
+
+        :param  node: Node to add
+        :type   node: ``str`` or :class:`Node`
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        return self.driver.ex_targetpool_add_node(targetpool=self, node=node)
+
+    def remove_node(self, node):
+        """
+        Remove a node from this target pool.
+
+        :param  node: Node to remove
+        :type   node: ``str`` or :class:`Node`
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        return self.driver.ex_targetpool_remove_node(targetpool=self,
+                                                     node=node)
+
+    def add_healthcheck(self, healthcheck):
+        """
+        Add a healthcheck to this target pool.
+
+        :param  healthcheck: Healthcheck to add
+        :type   healthcheck: ``str`` or :class:`GCEHealthCheck`
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        return self.driver.ex_targetpool_add_healthcheck(
+            targetpool=self, healthcheck=healthcheck)
+
+    def remove_healthcheck(self, healthcheck):
+        """
+        Remove a healthcheck from this target pool.
+
+        :param  healthcheck: Healthcheck to remove
+        :type   healthcheck: ``str`` or :class:`GCEHealthCheck`
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        return self.driver.ex_targetpool_remove_healthcheck(
+            targetpool=self, healthcheck=healthcheck)
+
+    def destroy(self):
+        """
+        Destroy this Target Pool
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        return self.driver.ex_destroy_targetpool(targetpool=self)
+
+
 class GCEZone(NodeLocation):
     """Subclass of NodeLocation to provide additional information."""
     def __init__(self, id, name, status, maintenance_windows, quotas,
@@ -305,7 +451,15 @@ class GCEZone(NodeLocation):
 
 class GCENodeDriver(NodeDriver):
     """
-    Base class for GCE Node Driver.
+    GCE Node Driver class.
+
+    This is the primary driver for interacting with Google Compute Engine.  It
+    contains all of the standard libcloud methods, plus additional ex_* methods
+    for more features.
+
+    Note that many methods allow either objects or strings (or lists of
+    objects/strings).  In most cases, passing strings instead of objects will
+    result in additional GCE API calls.
     """
     connectionCls = GCEConnection
     api_name = 'googleapis'
@@ -318,6 +472,7 @@ class GCENodeDriver(NodeDriver):
         "STAGING": NodeState.PENDING,
         "RUNNING": NodeState.RUNNING,
         "STOPPED": NodeState.TERMINATED,
+        "STOPPING": NodeState.TERMINATED,
         "TERMINATED": NodeState.TERMINATED
     }
 
@@ -353,7 +508,8 @@ class GCENodeDriver(NodeDriver):
                              '"project" keyword.')
         super(GCENodeDriver, self).__init__(user_id, key, **kwargs)
 
-        # Cache Zone information to reduce API calls and increase speed
+        # Cache Zone and Region 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()
@@ -365,31 +521,81 @@ class GCENodeDriver(NodeDriver):
         else:
             self.zone = None
 
+        self.region_list = self.ex_list_regions()
+        self.region_dict = {}
+        for region in self.region_list:
+            self.region_dict[region.name] = region
+
+        if self.zone:
+            self.region = self._get_region_from_zone(self.zone)
+        else:
+            self.region = None
+
     def _ex_connection_class_kwargs(self):
         return {'auth_type': self.auth_type,
                 'project': self.project}
 
-    def _categorize_error(self, error):
+    def _catch_error(self, ignore_errors=False):
         """
-        Parse error message returned from GCE operation and raise the
-        appropriate Exception.
+        Catch an exception and raise it unless asked to ignore it.
 
-        :param  error: Error dictionary from a GCE Operations response
-        :type   error: ``dict``
+        :keyword  ignore_errors: If true, just return the error.  Otherwise,
+                                 raise the error.
+        :type     ignore_errors: ``bool``
+
+        :return:  The exception that was raised.
+        :rtype:   :class:`Exception`
         """
-        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)
+        e = sys.exc_info()[1]
+        if ignore_errors:
+            return e
         else:
-            raise GCEError(code, message)
+            raise e
+
+    def _get_components_from_path(self, path):
+        """
+        Return a dictionary containing name & zone/region from a request path.
+
+        :param  path: HTTP request path (e.g.
+                      '/project/pjt-name/zones/us-central1-a/instances/mynode')
+        :type   path: ``str``
 
-    def _find_zone(self, name, res_type, region=False):
+        :return:  Dictionary containing name and zone/region of resource
+        :rtype    ``dict``
         """
-        Find the zone for a named resource.
+        region = None
+        zone = None
+        glob = False
+        components = path.split('/')
+        name = components[-1]
+        if components[-4] == 'regions':
+            region = components[-3]
+        elif components[-4] == 'zones':
+            zone = components[-3]
+        elif components[-3] == 'global':
+            glob = True
+
+        return {'name': name, 'region': region, 'zone': zone, 'global': glob}
+
+    def _get_region_from_zone(self, zone):
+        """
+        Return the Region object that contains the given Zone object.
+
+        :param  zone: Zone object
+        :type   zone: :class:`GCEZone`
+
+        :return:  Region object that contains the zone
+        :rtype:   :class:`GCERegion`
+        """
+        for region in self.region_list:
+            zones = [z.name for z in region.zones]
+            if zone.name in zones:
+                return region
+
+    def _find_zone_or_region(self, name, res_type, region=False,
+                             res_name=None):
+        """
+        Find the zone or region for a named resource.
 
         :param  name: Name of resource to find
         :type   name: ``str``
@@ -398,21 +604,36 @@ class GCENodeDriver(NodeDriver):
                           Examples include: 'disks', 'instances' or 'addresses'
         :type   res_type: ``str``
 
-        :keyword  region: If True, find a region instead of a zone.
-        :keyword  region: ``bool``
+        :keyword  region: If True, search regions instead of zones
+        :type     region: ``bool``
 
-        :return:  Name of zone (or region) that the resource is in.
-        :rtype:   ``str``
+        :keyword  res_name: The name of the resource type for error messages.
+                            Examples: 'Volume', 'Node', 'Address'
+        :keyword  res_name: ``str``
+
+        :return:  Zone/Region object for the zone/region for the resource.
+        :rtype:   :class:`GCEZone` or :class:`GCERegion`
         """
-        request = '/aggregated/%s' % res_type
+        if region:
+            rz = 'region'
+        else:
+            rz = 'zone'
+        rz_name = None
+        res_name = res_name or res_type
+        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/', '')
+                    rz_name = k.replace('%ss/' % (rz), '')
+                    break
+        if not rz_name:
+            raise ResourceNotFoundError(
+                '%s \'%s\' not found in any %s.' % (res_name, name, rz),
+                None, None)
+        else:
+            getrz = getattr(self, 'ex_get_%s' % (rz))
+            return getrz(rz_name)
 
     def _match_images(self, project, partial_name):
         """
@@ -431,8 +652,9 @@ class GCENodeDriver(NodeDriver):
                               image.
         :type   partial_name: ``str``
 
-        :return:  The latest image object that maches the partial name.
-        :rtype:   :class:`NodeImage`
+        :return:  The latest image object that maches the partial name or None
+                  if no matching image is found.
+        :rtype:   :class:`NodeImage` or ``None``
         """
         project_images = self.list_images(project)
         partial_match = []
@@ -447,6 +669,44 @@ class GCENodeDriver(NodeDriver):
         if partial_match:
             return partial_match[1]
 
+    def _set_region(self, region):
+        """
+        Return the region to use for listing resources.
+
+        :param  region: A name, region object, None, or 'all'
+        :type   region: ``str`` or :class:`GCERegion` or ``None``
+
+        :return:  A region object or None if all regions should be considered
+        :rtype:   :class:`GCERegion` or ``None``
+        """
+        region = region or self.region
+
+        if region == 'all' or region is None:
+            return None
+
+        if not hasattr(region, 'name'):
+            region = self.ex_get_region(region)
+        return region
+
+    def _set_zone(self, zone):
+        """
+        Return the zone to use for listing resources.
+
+        :param  zone: A name, zone object, None, or 'all'
+        :type   region: ``str`` or :class:`GCEZone` or ``None``
+
+        :return:  A zone object or None if all zones should be considered
+        :rtype:   :class:`GCEZone` or ``None``
+        """
+        zone = zone or self.zone
+
+        if zone == 'all' or zone is None:
+            return None
+
+        if not hasattr(zone, 'name'):
+            zone = self.ex_get_zone(zone)
+        return zone
+
     def ex_list_addresses(self, region=None):
         """
         Return a list of static addreses for a region or all.
@@ -461,16 +721,11 @@ class GCENodeDriver(NodeDriver):
         :rtype: ``list`` of :class:`GCEAddress`
         """
         list_addresses = []
-        if region is None and self.zone:
-            region = '-'.join(self.zone.name.split('-')[:-1])
-        elif region == 'all':
-            region = None
-
+        region = self._set_region(region)
         if region is None:
             request = '/aggregated/addresses'
         else:
-            request = '/regions/%s/addresses' % region
-
+            request = '/regions/%s/addresses' % (region.name)
         response = self.connection.request(request, method='GET').object
 
         if 'items' in response:
@@ -485,6 +740,20 @@ class GCENodeDriver(NodeDriver):
                                   response['items']]
         return list_addresses
 
+    def ex_list_healthchecks(self):
+        """
+        Return the list of health checks.
+
+        :return: A list of health check objects.
+        :rtype: ``list`` of :class:`GCEHealthCheck`
+        """
+        list_healthchecks = []
+        request = '/global/httpHealthChecks'
+        response = self.connection.request(request, method='GET').object
+        list_healthchecks = [self._to_healthcheck(h) for h in
+                             response.get('items', [])]
+        return list_healthchecks
+
     def ex_list_firewalls(self):
         """
         Return the list of firewalls.
@@ -499,6 +768,41 @@ class GCENodeDriver(NodeDriver):
                           response.get('items', [])]
         return list_firewalls
 
+    def ex_list_forwarding_rules(self, region=None):
+        """
+        Return the list of forwarding rules for a region or all.
+
+        :keyword  region: The region to return forwarding rules from.  For
+                          example: 'us-central1'.  If None, will return
+                          forwarding rules from the region of self.region
+                          (which is based on self.zone).  If 'all', will
+                          return all forwarding rules.
+        :type     region: ``str`` or :class:`GCERegion` or ``None``
+
+        :return: A list of forwarding rule objects.
+        :rtype: ``list`` of :class:`GCEForwardingRule`
+        """
+        list_forwarding_rules = []
+        region = self._set_region(region)
+        if region is None:
+            request = '/aggregated/forwardingRules'
+        else:
+            request = '/regions/%s/forwardingRules' % (region.name)
+        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_forwarding_rules = [self._to_forwarding_rule(f) for
+                                               f in v.get('forwardingRules',
+                                                          [])]
+                    list_forwarding_rules.extend(region_forwarding_rules)
+            else:
+                list_forwarding_rules = [self._to_forwarding_rule(f) for f in
+                                         response['items']]
+        return list_forwarding_rules
+
     def list_images(self, ex_project=None):
         """
         Return a list of image objects for a project.
@@ -568,17 +872,11 @@ class GCENodeDriver(NodeDriver):
         :rtype:   ``list`` of :class:`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
+        zone = self._set_zone(ex_zone)
         if zone is None:
             request = '/aggregated/instances'
-        elif hasattr(zone, 'name'):
-            request = '/zones/%s/instances' % zone.name
         else:
-            request = '/zones/%s/instances' % zone
+            request = '/zones/%s/instances' % (zone.name)
 
         response = self.connection.request(request, method='GET').object
 
@@ -593,6 +891,19 @@ class GCENodeDriver(NodeDriver):
                 list_nodes = [self._to_node(i) for i in response['items']]
         return list_nodes
 
+    def ex_list_regions(self):
+        """
+        Return the list of regions.
+
+        :return: A list of region objects.
+        :rtype: ``list`` of :class:`GCERegion`
+        """
+        list_regions = []
+        request = '/regions'
+        response = self.connection.request(request, method='GET').object
+        list_regions = [self._to_region(r) for r in response['items']]
+        return list_regions
+
     def list_sizes(self, location=None):
         """
         Return a list of sizes (machineTypes) in a zone.
@@ -604,21 +915,17 @@ class GCENodeDriver(NodeDriver):
         :rtype:   ``list`` of :class:`GCENodeSize`
         """
         list_sizes = []
-        location = location or self.zone
-        if location == 'all':
-            location = None
-        if location is None:
+        zone = self._set_zone(location)
+        if zone is None:
             request = '/aggregated/machineTypes'
-        elif hasattr(location, 'name'):
-            request = '/zones/%s/machineTypes' % location.name
         else:
-            request = '/zones/%s/machineTypes' % location
+            request = '/zones/%s/machineTypes' % (zone.name)
 
         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:
+            if zone is None:
                 for v in response['items'].values():
                     zone_sizes = [self._to_node_size(s) for s in
                                   v.get('machineTypes', [])]
@@ -627,6 +934,33 @@ class GCENodeDriver(NodeDriver):
                 list_sizes = [self._to_node_size(s) for s in response['items']]
         return list_sizes
 
+    def ex_list_targetpools(self, region=None):
+        """
+        Return the list of target pools.
+
+        :return:  A list of target pool objects
+        :rtype:   ``list`` of :class:`GCETargetPool`
+        """
+        list_targetpools = []
+        region = self._set_region(region)
+        if region is None:
+            request = '/aggregated/targetPools'
+        else:
+            request = '/regions/%s/targetPools' % (region.name)
+        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_targetpools = [self._to_targetpool(t) for t in
+                                          v.get('targetPools', [])]
+                    list_targetpools.extend(region_targetpools)
+            else:
+                list_targetpools = [self._to_targetpool(t) for t in
+                                    response['items']]
+        return list_targetpools
+
     def list_volumes(self, ex_zone=None):
         """
         Return a list of volumes for a zone or all.
@@ -634,22 +968,18 @@ class GCENodeDriver(NodeDriver):
         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: ``str`` or :class:`GCEZone` or :class:`NodeLocation` or ``None``
+        :keyword  ex_zone: The zone to return volumes from.
+        :type     ex_zone: ``str`` or :class:`GCEZone` or :class:`NodeLocation` or ``None``
 
         :return: A list of volume objects.
         :rtype: ``list`` of :class:`StorageVolume`
         """
         list_volumes = []
-        zone = ex_zone or self.zone
-        if zone == 'all':
-            zone = None
+        zone = self._set_zone(ex_zone)
         if zone is None:
             request = '/aggregated/disks'
-        elif hasattr(zone, 'name'):
-            request = '/zones/%s/disks' % zone.name
         else:
-            request = '/zones/%s/disks' % zone
+            request = '/zones/%s/disks' % (zone.name)
 
         response = self.connection.request(request, method='GET').object
         if 'items' in response:
@@ -684,25 +1014,80 @@ class GCENodeDriver(NodeDriver):
         :param  name: Name of static address
         :type   name: ``str``
 
-        :param  region: Name of region for the addres (e.g. 'us-central1')
-        :type   region: ``str``
+        :keyword  region: Name of region for the address (e.g. 'us-central1')
+        :type     region: ``str`` or :class:`GCERegion`
 
         :return:  Static Address object
         :rtype:   :class:`GCEAddress`
         """
-        if region is None and self.zone:
-            region = '-'.join(self.zone.name.split('-')[:-1])
+        region = region or self.region
+        if not hasattr(region, 'name'):
+            region = self.ex_get_region(region)
         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
+        request = '/regions/%s/addresses' % (region.name)
         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_healthcheck(self, name, host=None, path=None, port=None,
+                              interval=None, timeout=None,
+                              unhealthy_threshold=None,
+                              healthy_threshold=None):
+        """
+        Create an Http Health Check.
+
+        :param  name: Name of health check
+        :type   name: ``str``
+
+        :keyword  host: Hostname of health check requst.  Defaults to empty and
+                        public IP is used instead.
+        :type     host: ``str``
+
+        :keyword  path: The request path for the check.  Defaults to /.
+        :type     path: ``str``
+
+        :keyword  port: The TCP port number for the check.  Defaults to 80.
+        :type     port: ``int``
+
+        :keyword  interval: How often (in seconds) to check.  Defaults to 5.
+        :type     interval: ``int``
+
+        :keyword  timeout: How long to wait before failing. Defaults to 5.
+        :type     timeout: ``int``
+
+        :keyword  unhealthy_threshold: How many failures before marking
+                                       unhealthy.  Defaults to 2.
+        :type     unhealthy_threshold: ``int``
+
+        :keyword  healthy_threshold: How many successes before marking as
+                                     healthy.  Defaults to 2.
+        :type     healthy_threshold: ``int``
+
+        :return:  Health Check object
+        :rtype:   :class:`GCEHealthCheck`
+        """
+        hc_data = {}
+        hc_data['name'] = name
+        if host:
+            hc_data['host'] = host
+        # As of right now, the 'default' values aren't getting set when called
+        # through the API, so set them explicitly
+        hc_data['requestPath'] = path or '/'
+        hc_data['port'] = port or 80
+        hc_data['checkIntervalSec'] = interval or 5
+        hc_data['timeoutSec'] = timeout or 5
+        hc_data['unhealthyThreshold'] = unhealthy_threshold or 2
+        hc_data['healthyThreshold'] = healthy_threshold or 2
+
+        request = '/global/httpHealthChecks'
+
+        response = self.connection.async_request(request, method='POST',
+                                                 data=hc_data).object
+        return self.ex_get_healthcheck(name)
+
     def ex_create_firewall(self, name, allowed, network='default',
                            source_ranges=None, source_tags=None):
         """
@@ -731,7 +1116,8 @@ class GCENodeDriver(NodeDriver):
         :type     network: ``str`` or :class:`GCENetwork`
 
         :keyword  source_ranges: A list of IP ranges in CIDR format that the
-                                 firewall should apply to.
+                                 firewall should apply to. Defaults to
+                                 ['0.0.0.0/0']
         :type     source_ranges: ``list`` of ``str``
 
         :keyword  source_tags: A list of instance tags which the rules apply
@@ -749,8 +1135,7 @@ class GCENodeDriver(NodeDriver):
         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
+        firewall_data['sourceRanges'] = source_ranges or ['0.0.0.0/0']
         if source_tags is not None:
             firewall_data['sourceTags'] = source_tags
 
@@ -758,10 +1143,63 @@ class GCENodeDriver(NodeDriver):
 
         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_forwarding_rule(self, name, targetpool, region=None,
+                                  protocol='tcp', port_range=None,
+                                  address=None):
+        """
+        Create a forwarding rule.
+
+        :param  name: Name of forwarding rule to be created
+        :type   name: ``str``
+
+        :param  targetpool: Target pool to apply the rule to
+        :param  targetpool: ``str`` or :class:`GCETargetPool`
+
+        :keyword  region: Region to create the forwarding rule in.  Defaults to
+                          self.region
+        :type     region: ``str`` or :class:`GCERegion`
+
+        :keyword  protocol: Should be 'tcp' or 'udp'
+        :type     protocol: ``str``
+
+        :keyword  port_range: Optional single port number or range separated
+                              by a dash.  Examples: '80', '5000-5999'.
+        :type     port_range: ``str``
+
+        :keyword  address: Optional static address for forwarding rule. Must be
+                           in same region.
+        :type     address: ``str`` or :class:`GCEAddress`
+
+        :return:  Forwarding Rule object
+        :rtype:   :class:`GCEForwardingRule`
+        """
+        forwarding_rule_data = {}
+        region = region or self.region
+        if not hasattr(region, 'name'):
+            region = self.ex_get_region(region)
+        if not hasattr(targetpool, 'name'):
+            targetpool = self.ex_get_targetpool(targetpool, region)
+
+        forwarding_rule_data['name'] = name
+        forwarding_rule_data['region'] = region.extra['selfLink']
+        forwarding_rule_data['target'] = targetpool.extra['selfLink']
+        forwarding_rule_data['protocol'] = protocol.upper()
+        if address:
+            if not hasattr(address, 'name'):
+                address = self.ex_get_address(address, region)
+            forwarding_rule_data['IPAddress'] = address.extra['selfLink']
+        if port_range:
+            forwarding_rule_data['portRange'] = port_range
+
+        request = '/regions/%s/forwardingRules' % (region.name)
+
+        response = self.connection.async_request(
+            request, method='POST', data=forwarding_rule_data).object
+
+        return self.ex_get_forwarding_rule(name)
+
     def ex_create_network(self, name, cidr):
         """
         Create a network.
@@ -783,13 +1221,12 @@ class GCENodeDriver(NodeDriver):
 
         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):
+                         tags=None, metadata=None, boot_disk=None,
+                         persistent_disk=False):
         """
         Returns a request and body to create a new node.  This is a helper
         method to suppor both :class:`create_node` and :class:`ex_create_multiple_nodes`.
@@ -816,9 +1253,14 @@ class GCENodeDriver(NodeDriver):
         :keyword  metadata: Metadata dictionary for instance.
         :type     metadata: ``dict``
 
-        :keyword  boot_disk:  Persistent boot disk to attach
+        :keyword  boot_disk:  Persistent boot disk to attach.
         :type     :class:`StorageVolume`
 
+        :keyword  persistent_disk: If True, create a persistent disk instead of
+                                   an ephemeral one.  Has no effect if
+                                   boot_disk is specified.
+        :type     persistent_disk: ``bool``
+
         :return:  A tuple containing a request string and a node_data dict.
         :rtype:   ``tuple`` of ``str`` and ``dict``
         """
@@ -829,6 +1271,9 @@ class GCENodeDriver(NodeDriver):
             node_data['tags'] = {'items': tags}
         if metadata:
             node_data['metadata'] = metadata
+        if (not boot_disk) and persistent_disk:
+            boot_disk = self.create_volume(None, name, location=location,
+                                           image=image)
         if boot_disk:
             disks = [{'kind': 'compute#attachedDisk',
                       'boot': True,
@@ -848,13 +1293,13 @@ class GCENodeDriver(NodeDriver):
                'network': network.extra['selfLink']}]
         node_data['networkInterfaces'] = ni
 
-        request = '/zones/%s/instances' % location.name
+        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):
+                    ex_boot_disk=None, ex_persistent_disk=False):
         """
         Create a new node and return a node object for the node.
 
@@ -881,7 +1326,12 @@ class GCENodeDriver(NodeDriver):
         :type     ex_metadata: ``dict`` or ``None``
 
         :keyword  ex_boot_disk: The boot disk to attach to the instance.
-        :type     ex_boot_disk: :class:`StorageVolume`
+        :type     ex_boot_disk: :class:`StorageVolume` or ``str``
+
+        :keyword  ex_persistent_disk: If True, create a persistent_disk instead
+                                      of a ephemeral one.  Has no effect if
+                                      ex_boot_disk is specified.
+        :type     ex_persistent_disk: ``bool``
 
         :return:  A Node object for the new node.
         :rtype:   :class:`Node`
@@ -899,18 +1349,17 @@ class GCENodeDriver(NodeDriver):
         request, node_data = self._create_node_req(name, size, image,
                                                    location, ex_network,
                                                    ex_tags, ex_metadata,
-                                                   ex_boot_disk)
+                                                   ex_boot_disk,
+                                                   ex_persistent_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,
+                                 ignore_errors=True, ex_persistent_disk=False,
                                  timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
         """
         Create multiple nodes and return a list of Node objects.
@@ -950,6 +1399,10 @@ class GCENodeDriver(NodeDriver):
                                  more nodes fails.
         :type     ignore_errors: ``bool``
 
+        :keyword  persistent_disk: If True, create persistent boot disks
+                                   instead of ephemeral ones.
+        :type     persistent_disk: ``bool``
+
         :keyword  timeout: The number of seconds to wait for all nodes to be
                            created before timing out.
 
@@ -971,9 +1424,9 @@ class GCENodeDriver(NodeDriver):
         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)
+            request, node_data = self._create_node_req(
+                name, size, image, location, ex_network, ex_tags, ex_metadata,
+                persistent_disk=ex_persistent_disk)
             response = self.connection.request(request, method='POST',
                                                data=node_data)
             responses.append(response.object)
@@ -988,17 +1441,19 @@ class GCENodeDriver(NodeDriver):
             for i, operation in enumerate(responses):
                 if operation is None:
                     continue
-                response = self.connection.request(
-                    operation['selfLink']).object
+                error = None
+                try:
+                    response = self.connection.request(
+                        operation['selfLink']).object
+                except:
+                    e = self._catch_error(ignore_errors=ignore_errors)
+                    error = e.value
+                    code = e.code
                 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'])
+                    if error:
+                        node_list[i] = GCEFailedNode(name, error, code)
                     else:
                         node_list[i] = self.ex_get_node(name, location.name)
                 else:
@@ -1006,6 +1461,56 @@ class GCENodeDriver(NodeDriver):
                     time.sleep(2)
         return node_list
 
+    def ex_create_targetpool(self, name, region=None, healthchecks=None,
+                             nodes=None):
+        """
+        Create a target pool.
+
+        :param  name: Name of target pool
+        :type   name: ``str``
+
+        :keyword  region: Region to create the target pool in. Defaults to
+                          self.region
+        :type     region: ``str`` or :class:`GCERegion` or ``None``
+
+        :keyword  healthchecks: Optional list of health checks to attach
+        :type     healthchecks: ``list`` of ``str`` or :class:`GCEHealthCheck`
+
+        :keyword  nodes:  Optional list of nodes to attach to the pool
+        :type     nodes:  ``list`` of ``str`` or :class:`Node`
+
+        :return:  Target Pool object
+        :rtype:   :class:`GCETargetPool`
+        """
+        region = region or self.region
+        targetpool_data = {}
+        targetpool_data['name'] = name
+        if not hasattr(region, 'name'):
+            region = self.ex_get_region(region)
+        targetpool_data['region'] = region.extra['selfLink']
+
+        if healthchecks:
+            if not hasattr(healthchecks[0], 'name'):
+                hc_list = [self.ex_get_healthcheck(h).extra['selfLink'] for h
+                           in healthchecks]
+            else:
+                hc_list = [h.extra['selfLink'] for h in healthchecks]
+            targetpool_data['healthChecks'] = hc_list
+        if nodes:
+            if not hasattr(nodes[0], 'name'):
+                node_list = [self.ex_get_node(n, 'all').extra['selfLink'] for n
+                             in nodes]
+            else:
+                node_list = [n.extra['selfLink'] for n in nodes]
+            targetpool_data['instances'] = node_list
+
+        request = '/regions/%s/targetPools' % (region.name)
+
+        response = self.connection.async_request(request, method='POST',
+                                                 data=targetpool_data).object
+
+        return self.ex_get_targetpool(name, region)
+
     def create_volume(self, size, name, location=None, image=None,
                       snapshot=None):
         """
@@ -1024,7 +1529,7 @@ class GCENodeDriver(NodeDriver):
         :keyword  image: Image to create disk from.
         :type     image: :class:`NodeImage` or ``str`` or ``None``
 
-        :keyword  snapshot: Snapshot to create image from
+        :keyword  snapshot: Snapshot to create image from (needs full URI)
         :type     snapshot: ``str``
 
         :return:  Storage Volume object
@@ -1039,19 +1544,53 @@ class GCENodeDriver(NodeDriver):
             if not hasattr(image, 'name'):
                 image = self.ex_get_image(image)
             params = {'sourceImage': image.extra['selfLink']}
+            volume_data['description'] = 'Image: %s' % (
+                image.extra['selfLink'])
         if snapshot:
             volume_data['sourceSnapshot'] = snapshot
+            volume_data['description'] = 'Snapshot: %s' % (snapshot)
         location = location or self.zone
         if not hasattr(location, 'name'):
             location = self.ex_get_zone(location)
-        request = '/zones/%s/disks' % location.name
+        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)
+        return self.ex_get_volume(name, location)
+
+    def ex_update_healthcheck(self, healthcheck):
+        """
+        Update a health check with new values.
+
+        To update, change the attributes of the health check object and pass
+        the updated object to the method.
+
+        :param  healthcheck: A healthcheck object with updated values.
+        :type   healthcheck: :class:`GCEHealthCheck`
+
+        :return:  An object representing the new state of the health check.
+        :rtype:   :class:`GCEHealthCheck`
+        """
+        hc_data = {}
+        hc_data['name'] = healthcheck.name
+        hc_data['requestPath'] = healthcheck.path
+        hc_data['port'] = healthcheck.port
+        hc_data['checkIntervalSec'] = healthcheck.interval
+        hc_data['timeoutSec'] = healthcheck.timeout
+        hc_data['unhealthyThreshold'] = healthcheck.unhealthy_threshold
+        hc_data['healthyThreshold'] = healthcheck.healthy_threshold
+        if healthcheck.extra['host']:
+            hc_data['host'] = healthcheck.extra['host']
+        if healthcheck.extra['description']:
+            hc_data['description'] = healthcheck.extra['description']
+
+        request = '/global/httpHealthChecks/%s' % (healthcheck.name)
+
+        response = self.connection.async_request(request, method='PUT',
+                                                 data=hc_data).object
+
+        return self.ex_get_healthcheck(healthcheck.name)
 
     def ex_update_firewall(self, firewall):
         """
@@ -1077,15 +1616,134 @@ class GCENodeDriver(NodeDriver):
         if firewall.extra['description']:
             firewall_data['description'] = firewall.extra['description']
 
-        request = '/global/firewalls/%s' % firewall.name
+        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 ex_targetpool_add_node(self, targetpool, node):
+        """
+        Add a node to a target pool.
+
+        :param  targetpool: The targetpool to add node to
+        :type   targetpool: ``str`` or :class:`GCETargetPool`
+
+        :param  node: The node to add
+        :type   node: ``str`` or :class:`Node`
+
+        :returns: True if successful
+        :rtype:   ``bool``
+        """
+        if not hasattr(targetpool, 'name'):
+            targetpool = self.ex_get_targetpool(targetpool)
+        if not hasattr(node, 'name'):
+            node = self.ex_get_node(node, 'all')
+
+        targetpool_data = {'instance': node.extra['selfLink']}
+
+        request = '/regions/%s/targetPools/%s/addInstance' % (
+            targetpool.region.name, targetpool.name)
+        response = self.connection.async_request(request, method='POST',
+                                                 data=targetpool_data).object
+        targetpool.nodes.append(node)
+        return True
+
+    def ex_targetpool_add_healthcheck(self, targetpool, healthcheck):
+        """
+        Add a health check to a target pool.
+
+        :param  targetpool: The targetpool to add health check to
+        :type   targetpool: ``str`` or :class:`GCETargetPool`
+
+        :param  healthcheck: The healthcheck to add
+        :type   healthcheck: ``str`` or :class:`GCEHealthCheck`
+
+        :returns: True if successful
+        :rtype:   ``bool``
+        """
+        if not hasattr(targetpool, 'name'):
+            targetpool = self.ex_get_targetpool(targetpool)
+        if not hasattr(healthcheck, 'name'):
+            healthcheck = self.ex_get_healthcheck(healthcheck)
+
+        targetpool_data = {'healthCheck': healthcheck.extra['selfLink']}
+
+        request = '/regions/%s/targetPools/%s/addHealthCheck' % (
+            targetpool.region.name, targetpool.name)
+        response = self.connection.async_request(request, method='POST',
+                                                 data=targetpool_data).object
+        targetpool.healthchecks.append(healthcheck)
+        return True
+
+    def ex_targetpool_remove_node(self, targetpool, node):
+        """
+        Remove a node from a target pool.
+
+        :param  targetpool: The targetpool to remove node from
+        :type   targetpool: ``str`` or :class:`GCETargetPool`
+
+        :param  node: The node to remove
+        :type   node: ``str`` or :class:`Node`
+
+        :returns: True if successful
+        :rtype:   ``bool``
+        """
+        if not hasattr(targetpool, 'name'):
+            targetpool = self.ex_get_targetpool(targetpool)
+        if not hasattr(node, 'name'):
+            node = self.ex_get_node(node, 'all')
+
+        targetpool_data = {'instance': node.extra['selfLink']}
+
+        request = '/regions/%s/targetPools/%s/removeInstance' % (
+            targetpool.region.name, targetpool.name)
+        response = self.connection.async_request(request, method='POST',
+                                                 data=targetpool_data).object
+        # Remove node object from node list
+        index = None
+        for i, nd in enumerate(targetpool.nodes):
+            if nd.name == node.name:
+                index = i
+                break
+        if index is not None:
+            targetpool.nodes.pop(index)
+        return True
+
+    def ex_targetpool_remove_healthcheck(self, targetpool, healthcheck):
+        """
+        Remove a health check from a target pool.
+
+        :param  targetpool: The targetpool to remove health check from
+        :type   targetpool: ``str`` or :class:`GCETargetPool`
+
+        :param  healthcheck: The healthcheck to remove
+        :type   healthcheck: ``str`` or :class:`GCEHealthCheck`
+
+        :returns: True if successful
+        :rtype:   ``bool``
+        """
+        if not hasattr(targetpool, 'name'):
+            targetpool = self.ex_get_targetpool(targetpool)
+        if not hasattr(healthcheck, 'name'):
+            healthcheck = self.ex_get_healthcheck(healthcheck)
+
+        targetpool_data = {'healthCheck': healthcheck.extra['selfLink']}
+
+        request = '/regions/%s/targetPools/%s/removeHealthCheck' % (
+            targetpool.region.name, targetpool.name)
+        response = self.connection.async_request(request, method='POST',
+                                                 data=targetpool_data).object
+        # Remove healthcheck object from healthchecks list
+        index = None
+        for i, hc in enumerate(targetpool.healthchecks):
+            if hc.name == healthcheck.name:
+                index = i
+        if index is not None:
+            targetpool.healthchecks.pop(index)
+        return True
+
     def reboot_node(self, node):
         """
         Reboot a node.
@@ -1100,10 +1758,7 @@ class GCENodeDriver(NodeDriver):
                                                     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
+        return True
 
     def ex_set_node_tags(self, node, tags):
         """
@@ -1129,13 +1784,10 @@ class GCENodeDriver(NodeDriver):
 
         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
+        new_node = self.ex_get_node(node.name, node.extra['zone'])
+        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):
@@ -1222,10 +1874,7 @@ class GCENodeDriver(NodeDriver):
             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
+        return True
 
     def detach_volume(self, volume, ex_node=None):
         """
@@ -1247,10 +1896,7 @@ class GCENodeDriver(NodeDriver):
 
         response = self.connection.async_request(request, method='POST',
                                                  data='ignored').object
-        if 'error' in response:
-            self._categorize_error(response['error'])
-        else:
-            return True
+        return True
 
     def ex_destroy_address(self, address):
         """
@@ -1262,14 +1908,27 @@ class GCENodeDriver(NodeDriver):
         :return:  True if successful
         :rtype:   ``bool``
         """
-        request = '/regions/%s/addresses/%s' % (address.region, address.name)
+        request = '/regions/%s/addresses/%s' % (address.region.name,
+                                                address.name)
 
         response = self.connection.async_request(request,
                                                  method='DELETE').object
-        if 'error' in response:
-            self._categorize_error(response['error'])
-        else:
-            return True
+        return True
+
+    def ex_destroy_healthcheck(self, healthcheck):
+        """
+        Destroy a healthcheck.
+
+        :param  healthcheck: Health check object to destroy
+        :type   healthcheck: :class:`GCEHealthCheck`
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        request = '/global/httpHealthChecks/%s' % (healthcheck.name)
+        response = self.connection.async_request(request,
+                                                 method='DELETE').object
+        return True
 
     def ex_destroy_firewall(self, firewall):
         """
@@ -1281,13 +1940,26 @@ class GCENodeDriver(NodeDriver):
         :return:  True if successful
         :rtype:   ``bool``
         """
-        request = '/global/firewalls/%s' % firewall.name
+        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
+        return True
+
+    def ex_destroy_forwarding_rule(self, forwarding_rule):
+        """
+        Destroy a forwarding rule.
+
+        :param  forwarding_rule: Forwarding Rule object to destroy
+        :type   forwarding_rule: :class:`GCEForwardingRule`
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        request = '/regions/%s/forwardingRules/%s' % (
+            forwarding_rule.region.name, forwarding_rule.name)
+        response = self.connection.async_request(request,
+                                                 method='DELETE').object
+        return True
 
     def ex_destroy_network(self, network):
         """
@@ -1299,13 +1971,10 @@ class GCENodeDriver(NodeDriver):
         :return:  True if successful
         :rtype:   ``bool``
         """
-        request = '/global/networks/%s' % network.name
+        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
+        return True
 
     def destroy_node(self, node):
         """
@@ -1321,10 +1990,7 @@ class GCENodeDriver(NodeDriver):
                                               node.name)
         response = self.connection.async_request(request,
                                                  method='DELETE').object
-        if 'error' in response:
-            self._categorize_error(response['error'])
-        else:
-            return True
+        return True
 
     def ex_destroy_multiple_nodes(self, nodelist, ignore_errors=True,
                                   timeout=DEFAULT_TASK_COMPLETION_TIMEOUT):
@@ -1353,7 +2019,12 @@ class GCENodeDriver(NodeDriver):
         for node in nodelist:
             request = '/zones/%s/instances/%s' % (node.extra['zone'].name,
                                                   node.name)
-            response = self.connection.request(request, method='DELETE').object
+            try:
+                response = self.connection.request(request,
+                                                   method='DELETE').object
+            except:
+                e = self._catch_error(ignore_errors=ignore_errors)
+                response = None
             responses.append(response)
 
         while not complete:
@@ -1364,22 +2035,38 @@ class GCENodeDriver(NodeDriver):
             for i, operation in enumerate(responses):
                 if operation is None:
                     continue
-                response = self.connection.request(
-                    operation['selfLink']).object
+                no_errors = True
+                try:
+                    response = self.connection.request(
+                        operation['selfLink']).object
+                except:
+                    e = self._catch_error(ignore_errors=ignore_errors)
+                    no_errors = False
                 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
+                    success[i] = no_errors
                 else:
                     complete = False
                     time.sleep(2)
         return success
 
+    def ex_destroy_targetpool(self, targetpool):
+        """
+        Destroy a target pool.
+
+        :param  targetpool: TargetPool object to destroy
+        :type   targetpool: :class:`GCETargetPool`
+
+        :return:  True if successful
+        :rtype:   ``bool``
+        """
+        request = '/regions/%s/targetPools/%s' % (targetpool.region.name,
+                                                  targetpool.name)
+
+        response = self.connection.async_request(request,
+                                                 method='DELETE').object
+        return True
+
     def destroy_volume(self, volume):
         """
         Destroy a volume.
@@ -1394,10 +2081,7 @@ class GCENodeDriver(NodeDriver):
                                           volume.name)
         response = self.connection.async_request(request,
                                                  method='DELETE').object
-        if 'error' in response:
-            self._categorize_error(response['error'])
-        else:
-            return True
+        return True
 
     def ex_get_address(self, name, region=None):
         """
@@ -1406,18 +2090,33 @@ class GCENodeDriver(NodeDriver):
         :param  name: The name of the address
         :type   name: ``str``
 
-        :keyword  region: The region to search for the address in
-        :type     region: ``str`` or ``None``
+        :keyword  region: The region to search for the address in (set to
+                          'all' to search all regions)
+        :type     region: ``str`` :class:`GCERegion` or ``None``
 
         :return:  An Address object for the address
         :rtype:   :class:`GCEAddress`
         """
-        address_region = region or self._find_zone(name, 'addresses',
-                                                   region=True)
-        request = '/regions/%s/addresses/%s' % (address_region, name)
+        region = self._set_region(region) or self._find_zone_or_region(
+            name, 'addresses', region=True, res_name='Address')
+        request = '/regions/%s/addresses/%s' % (region.name, name)
         response = self.connection.request(request, method='GET').object
         return self._to_address(response)
 
+    def ex_get_healthcheck(self, name):
+        """
+        Return a HealthCheck object based on the healthcheck name.
+
+        :param  name: The name of the healthcheck
+        :type   name: ``str``
+
+        :return:  A GCEHealthCheck object
+        :rtype:   :class:`GCEHealthCheck`
+        """
+        request = '/global/httpHealthChecks/%s' % (name)
+        response = self.connection.request(request, method='GET').object
+        return self._to_healthcheck(response)
+
     def ex_get_firewall(self, name):
         """
         Return a Firewall object based on the firewall name.
@@ -1428,10 +2127,30 @@ class GCENodeDriver(NodeDriver):
         :return:  A GCEFirewall object
         :rtype:   :class:`GCEFirewall`
         """
-        request = '/global/firewalls/%s' % name
+        request = '/global/firewalls/%s' % (name)
         response = self.connection.request(request, method='GET').object
         return self._to_firewall(response)
 
+    def ex_get_forwarding_rule(self, name, region=None):
+        """
+        Return a Forwarding Rule object based on the forwarding rule name.
+
+        :param  name: The name of the forwarding rule
+        :type   name: ``str``
+
+        :keyword  region: The region to search for the rule in (set to 'all'
+                          to search all regions).
+        :type     region: ``str`` or ``None``
+
+        :return:  A GCEForwardingRule object
+        :rtype:   :class:`GCEForwardingRule`
+        """
+        region = self._set_region(region) or self._find_zone_or_region(
+            name, 'forwardingRules', region=True, res_name='ForwardingRule')
+        request = '/regions/%s/forwardingRules/%s' % (region.name, name)
+        response = self.connection.request(request, method='GET').object
+        return self._to_forwarding_rule(response)
+
     def ex_get_image(self, partial_name):
         """
         Return an NodeImage object based on the name or link provided.
@@ -1440,8 +2159,9 @@ class GCENodeDriver(NodeDriver):
                               image.
         :type   partial_name: ``str``
 
-        :return:  NodeImage object based on provided information
-        :rtype:   :class:`NodeImage`
+        :return:  NodeImage object based on provided information or None if an
+                  image with that name is not found.
+        :rtype:   :class:`NodeImage` or ``None``
         """
         if partial_name.startswith('https://'):
             response = self.connection.request(partial_name, method='GET')
@@ -1465,7 +2185,7 @@ class GCENodeDriver(NodeDriver):
         :return:  A Network object for the network
         :rtype:   :class:`GCENetwork`
         """
-        request = '/global/networks/%s' % name
+        request = '/global/networks/%s' % (name)
         response = self.connection.request(request, method='GET').object
         return self._to_network(response)
 
@@ -1476,15 +2196,15 @@ class GCENodeDriver(NodeDriver):
         :param  name: The name of the node
         :type   name: ``str``
 
-        :keyword  zone: The zone to search for the node in
+        :keyword  zone: The zone to search for the node in.  If set to 'all',
+                        search all zones for the instance.
         :type     zone: ``str`` or :class:`GCEZone` or :class:`NodeLocation` or ``None``
 
         :return:  A Node object for the node
         :rtype:   :class:`Node`
         """
-        zone = zone or self.zone or self._find_zone(name, 'instances')
-        if not hasattr(zone, 'name'):
-            zone = self.ex_get_zone(zone)
+        zone = self._set_zone(zone) or self._find_zone_or_region(
+            name, 'instances', res_name='Node')
         request = '/zones/%s/instances/%s' % (zone.name, name)
         response = self.connection.request(request, method='GET').object
         return self._to_node(response)
@@ -1526,19 +2246,62 @@ class GCENodeDriver(NodeDriver):
         :param  name: The name of the volume
         :type   name: ``str``
 
-        :keyword  zone: The zone to search for the volume in
+        :keyword  zone: The zone to search for the volume in (set to 'all' to
+                        search all zones)
         :type     zone: ``str`` or :class:`GCEZone` or :class:`NodeLocation` or ``None``
 
         :return:  A StorageVolume object for the volume
         :rtype:   :class:`StorageVolume`
         """
-        zone = zone or self.zone or self.find_zone(name, 'disks')
-        if not hasattr(zone, 'name'):
-            zone = self.ex_get_zone(zone)
+        zone = self._set_zone(zone) or self._find_zone_or_region(
+            name, 'disks', res_name='Volume')
         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_region(self, name):
+        """
+        Return a Region object based on the region name.
+
+        :param  name: The name of the region.
+        :type   name: ``str``
+
+        :return:  A GCERegion object for the region
+        :rtype:   :class:`GCERegion`
+        """
+        if name.startswith('https://'):
+            short_name = self._get_components_from_path(name)['name']
+            request = name
+        else:
+            short_name = name
+            request = '/regions/%s' % (name)
+        # Check region cache first
+        if short_name in self.region_dict:
+            return self.region_dict[short_name]
+        # Otherwise, look up region information
+        response = self.connection.request(request, method='GET').object
+        return self._to_region(response)
+
+    def ex_get_targetpool(self, name, region=None):
+        """
+        Return a TargetPool object based on a name and optional region.
+
+        :param  name: The name of the target pool
+        :type   name: ``str``
+
+        :keyword  region: The region to search for the target pool in (set to
+                          'all' to search all regions).
+        :type     region: ``str`` or :class:`GCERegion` or ``None``
+
+        :return:  A TargetPool object for the pool
+        :rtype:   :class:`GCETargetPool`
+        """
+        region = self._set_region(region) or self._find_zone_or_region(
+            name, 'targetPools', region=True, res_name='TargetPool')
+        request = '/regions/%s/targetPools/%s' % (region.name, name)
+        response = self.connection.request(request, method='GET').object
+        return self._to_targetpool(response)
+
     def ex_get_zone(self, name):
         """
         Return a Zone object based on the zone name.
@@ -1546,20 +2309,23 @@ class GCENodeDriver(NodeDriver):
         :param  name: The name of the zone.
         :type   name: ``str``
 
-        :return:  A GCEZone object for the zone
-        :rtype:   :class:`GCEZone`
+        :return:  A GCEZone object for the zone or None if not found
+        :rtype:   :class:`GCEZone` or ``None``
         """
         if name.startswith('https://'):
-            short_name = name.split('/')[-1]
+            short_name = self._get_components_from_path(name)['name']
             request = name
         else:
             short_name = name
-            request = '/zones/%s' % 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
+        try:
+            response = self.connection.request(request, method='GET').object
+        except ResourceNotFoundError:
+            return None
         return self._to_zone(response)
 
     def _to_address(self, address):
@@ -1574,16 +2340,41 @@ class GCENodeDriver(NodeDriver):
         """
         extra = {}
 
+        region = self.ex_get_region(address['region'])
+
         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_healthcheck(self, healthcheck):
+        """
+        Return a HealthCheck object from the json-response dictionary.
+
+        :param  healthcheck: The dictionary describing the healthcheck.
+        :type   healthcheck: ``dict``
+
+        :return: HealthCheck object
+        :rtype: :class:`GCEHealthCheck`
+        """
+        extra = {}
+        extra['selfLink'] = healthcheck['selfLink']
+        extra['creationTimestamp'] = healthcheck['creationTimestamp']
+        extra['description'] = healthcheck.get('description')
+        extra['host'] = healthcheck.get('host')
+
+        return GCEHealthCheck(
+            id=healthcheck['id'], name=healthcheck['name'],
+            path=healthcheck['requestPath'], port=healthcheck['port'],
+            interval=healthcheck['checkIntervalSec'],
+            timeout=healthcheck['timeoutSec'],
+            unhealthy_threshold=healthcheck['unhealthyThreshold'],
+            healthy_threshold=healthcheck['healthyThreshold'],
+            driver=self, extra=extra)
+
     def _to_firewall(self, firewall):
         """
         Return a Firewall object from the json-response dictionary.
@@ -1598,7 +2389,8 @@ class GCENodeDriver(NodeDriver):
         extra['selfLink'] = firewall['selfLink']
         extra['creationTimestamp'] = firewall['creationTimestamp']
         extra['description'] = firewall.get('description')
-        extra['network_name'] = firewall['network'].split('/')[-1]
+        extra['network_name'] = self._get_components_from_path(
+            firewall['network'])['name']
 
         network = self.ex_get_network(extra['network_name'])
         source_ranges = firewall.get('sourceRanges')
@@ -1610,6 +2402,34 @@ class GCENodeDriver(NodeDriver):
                            source_tags=source_tags,
                            driver=self, extra=extra)
 
+    def _to_forwarding_rule(self, forwarding_rule):
+        """
+        Return a Forwarding Rule object from the json-response dictionary.
+
+        :param  forwarding_rule: The dictionary describing the rule.
+        :type   forwarding_rule: ``dict``
+
+        :return: ForwardingRule object
+        :rtype: :class:`GCEForwardingRule`
+        """
+        extra = {}
+        # Use .get to work around a current API bug.
+        extra['selfLink'] = forwarding_rule.get('selfLink')
+        extra['portRange'] = forwarding_rule['portRange']
+        extra['creationTimestamp'] = forwarding_rule['creationTimestamp']
+        extra['description'] = forwarding_rule.get('description')
+
+        region = self.ex_get_region(forwarding_rule['region'])
+        targetpool = self.ex_get_targetpool(
+            self._get_components_from_path(forwarding_rule['target'])['name'])
+
+        return GCEForwardingRule(id=forwarding_rule['id'],
+                                 name=forwarding_rule['name'], region=region,
+                                 address=forwarding_rule['IPAddress'],
+                                 protocol=forwarding_rule['IPProtocol'],
+                                 targetpool=targetpool,
+                                 driver=self, extra=extra)
+
     def _to_network(self, network):
         """
         Return a Network object from the json-response dictionary.
@@ -1681,6 +2501,7 @@ class GCENodeDriver(NodeDriver):
         extra['description'] = node.get('description')
         extra['zone'] = self.ex_get_zone(node['zone'])
         extra['image'] = node.get('image')
+        extra['machineType'] = node['machineType']
         extra['disks'] = node['disks']
         extra['networkInterfaces'] = node['networkInterfaces']
         extra['id'] = node['id']
@@ -1700,11 +2521,18 @@ class GCENodeDriver(NodeDriver):
             for access_config in network_interface['accessConfigs']:
                 public_ips.append(access_config['natIP'])
 
+        # For the node attributes, use just machine and image names, not full
+        # paths.  Full paths are available in the "extra" dict.
+        if extra['image']:
+            image = self._get_components_from_path(extra['image'])['name']
+        else:
+            image = None
+        size = self._get_components_from_path(node['machineType'])['name']
+
         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)
+                    driver=self, size=size, image=image, extra=extra)
 
     def _to_node_size(self, machine_type):
         """
@@ -1752,6 +2580,33 @@ class GCENodeDriver(NodeDriver):
                           metadata=metadata, quotas=project['quotas'],
                           driver=self, extra=extra)
 
+    def _to_region(self, region):
+        """
+        Return a Region object from the json-response dictionary.
+
+        :param  region: The dictionary describing the region.
+        :type   region: ``dict``
+
+        :return: Region object
+        :rtype: :class:`GCERegion`
+        """
+        extra = {}
+        extra['selfLink'] = region['selfLink']
+        extra['creationTimestamp'] = region['creationTimestamp']
+        extra['description'] = region['description']
+
+        quotas = region.get('quotas')
+        zones = [self.ex_get_zone(z) for z in region['zones']]
+        # Work around a bug that will occasionally list missing zones in the
+        # region output
+        zones = [z for z in zones if z is not None]
+        deprecated = region.get('deprecated')
+
+        return GCERegion(id=region['id'], name=region['name'],
+                         status=region['status'], zones=zones,
+                         quotas=quotas, deprecated=deprecated,
+                         driver=self, extra=extra)
+
     def _to_storage_volume(self, volume):
         """
         Return a Volume object from the json-response dictionary.
@@ -1767,10 +2622,43 @@ class GCENodeDriver(NodeDriver):
         extra['zone'] = self.ex_get_zone(volume['zone'])
         extra['status'] = volume['status']
         extra['creationTimestamp'] = volume['creationTimestamp']
+        extra['description'] = volume.get('description')
 
         return StorageVolume(id=volume['id'], name=volume['name'],
                              size=volume['sizeGb'], driver=self, extra=extra)
 
+    def _to_targetpool(self, targetpool):
+        """
+        Return a Target Pool object from the json-response dictionary.
+
+        :param  targetpool: The dictionary describing the volume.
+        :type   targetpool: ``dict``
+
+        :return: Target Pool object
+        :rtype:  :class:`GCETargetPool`
+        """
+        extra = {}
+        extra['selfLink'] = targetpool['selfLink']
+        extra['description'] = targetpool.get('description')
+        region = self.ex_get_region(targetpool['region'])
+        healthcheck_list = [self.ex_get_healthcheck(h.split('/')[-1]) for h
+                            in targetpool.get('healthChecks', [])]
+        node_list = []
+        for n in targetpool.get('instances', []):
+            # Nodes that do not exist can be part of a target pool.  If the
+            # node does not exist, use the URL of the node instead of the node
+            # object.
+            comp = self._get_components_from_path(n)
+            try:
+                node = self.ex_get_node(comp['name'], comp['zone'])
+            except ResourceNotFoundError:
+                node = n
+            node_list.append(node)
+
+        return GCETargetPool(id=targetpool['id'], name=targetpool['name'],
+                             region=region, healthchecks=healthcheck_list,
+                             nodes=node_list, driver=self, extra=extra)
+
     def _to_zone(self, zone):
         """
         Return a Zone object from the json-response dictionary.
@@ -1789,6 +2677,6 @@ class GCENodeDriver(NodeDriver):
         deprecated = zone.get('deprecated')
 
         return GCEZone(id=zone['id'], name=zone['name'], status=zone['status'],
-                       maintenance_windows=zone['maintenanceWindows'],
+                       maintenance_windows=zone.get('maintenanceWindows'),
                        quotas=zone['quotas'], deprecated=deprecated,
                        driver=self, extra=extra)