Explorar el Código

Merge pull request #130 from chiniforooshan/18-07-05

GCP: Fixing tests and applying API changes
Nuwan Goonasekera hace 7 años
padre
commit
fe7d702d9b

+ 10 - 6
cloudbridge/cloud/base/resources.py

@@ -356,12 +356,16 @@ class BasePageableObjectMixin(PageableObjectMixin):
     """
     """
 
 
     def __iter__(self):
     def __iter__(self):
-        result_list = self.list()
+        for result in self.iter():
+            yield result
+
+    def iter(self, **kwargs):
+        result_list = self.list(**kwargs)
         if result_list.supports_server_paging:
         if result_list.supports_server_paging:
             for result in result_list:
             for result in result_list:
                 yield result
                 yield result
             while result_list.is_truncated:
             while result_list.is_truncated:
-                result_list = self.list(marker=result_list.marker)
+                result_list = self.list(marker=result_list.marker, **kwargs)
                 for result in result_list:
                 for result in result_list:
                     yield result
                     yield result
         else:
         else:
@@ -863,8 +867,8 @@ class BaseBucketObject(BaseCloudResource, BucketObject):
                 self.name == other.name)
                 self.name == other.name)
 
 
     def __repr__(self):
     def __repr__(self):
-        return "<CB-{0}: {1}>".format(self.__class__.__name__,
-                                      self.name)
+        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
+                                            self.name, self.id)
 
 
 
 
 class BaseBucket(BaseCloudResource, Bucket):
 class BaseBucket(BaseCloudResource, Bucket):
@@ -902,8 +906,8 @@ class BaseBucket(BaseCloudResource, Bucket):
                 self.name == other.name)
                 self.name == other.name)
 
 
     def __repr__(self):
     def __repr__(self):
-        return "<CB-{0}: {1}>".format(self.__class__.__name__,
-                                      self.name)
+        return "<CB-{0}: {1} ({2})>".format(self.__class__.__name__,
+                                            self.name, self.id)
 
 
 
 
 class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):
 class BaseBucketContainer(BasePageableObjectMixin, BucketContainer):

+ 46 - 11
cloudbridge/cloud/providers/gce/provider.py

@@ -11,6 +11,7 @@ from string import Template
 
 
 import cloudbridge as cb
 import cloudbridge as cb
 from cloudbridge.cloud.base import BaseCloudProvider
 from cloudbridge.cloud.base import BaseCloudProvider
+from cloudbridge.cloud.interfaces.exceptions import ProviderConnectionException
 
 
 import googleapiclient
 import googleapiclient
 from googleapiclient import discovery
 from googleapiclient import discovery
@@ -229,6 +230,9 @@ class GCECloudProvider(BaseCloudProvider):
         # service connections, lazily initialized
         # service connections, lazily initialized
         self._gce_compute = None
         self._gce_compute = None
         self._gcs_storage = None
         self._gcs_storage = None
+        self._credentials_cache = None
+        self._compute_resources_cache = None
+        self._storage_resources_cache = None
 
 
         # Initialize provider services
         # Initialize provider services
         self._compute = GCEComputeService(self)
         self._compute = GCEComputeService(self)
@@ -236,12 +240,6 @@ class GCECloudProvider(BaseCloudProvider):
         self._networking = GCENetworkingService(self)
         self._networking = GCENetworkingService(self)
         self._storage = GCPStorageService(self)
         self._storage = GCPStorageService(self)
 
 
-        self._compute_resources = GCPResources(self.gce_compute,
-                                               project=self.project_name,
-                                               region=self.region_name,
-                                               zone=self.default_zone)
-        self._storage_resources = GCPResources(self.gcs_storage)
-
     @property
     @property
     def compute(self):
     def compute(self):
         return self._compute
         return self._compute
@@ -270,13 +268,40 @@ class GCECloudProvider(BaseCloudProvider):
             self._gcs_storage = self._connect_gcs_storage()
             self._gcs_storage = self._connect_gcs_storage()
         return self._gcs_storage
         return self._gcs_storage
 
 
+    @property
+    def _compute_resources(self):
+        if not self._compute_resources_cache:
+            self._compute_resources_cache = GCPResources(
+                    self.gce_compute,
+                    project=self.project_name,
+                    region=self.region_name,
+                    zone=self.default_zone)
+        return self._compute_resources_cache
+
+    @property
+    def _storage_resources(self):
+        if not self._storage_resources_cache:
+            self._storage_resources_cache = GCPResources(self.gcs_storage)
+        return self._storage_resources_cache
+
     @property
     @property
     def _credentials(self):
     def _credentials(self):
-        if self.credentials_dict:
-            return ServiceAccountCredentials.from_json_keyfile_dict(
-                self.credentials_dict)
-        else:
-            return GoogleCredentials.get_application_default()
+        if not self._credentials_cache:
+            if self.credentials_dict:
+                self._credentials_cache = (
+                        ServiceAccountCredentials.from_json_keyfile_dict(
+                                self.credentials_dict))
+            else:
+                self._credentials_cache = (
+                        GoogleCredentials.get_application_default())
+        return self._credentials_cache
+
+    def sign_blob(self, string_to_sign):
+        return self._credentials.sign_blob(string_to_sign)[1]
+
+    @property
+    def client_id(self):
+        return self._credentials.service_account_email
 
 
     def _connect_gcs_storage(self):
     def _connect_gcs_storage(self):
         return discovery.build('storage', 'v1', credentials=self._credentials,
         return discovery.build('storage', 'v1', credentials=self._credentials,
@@ -311,6 +336,8 @@ class GCECloudProvider(BaseCloudProvider):
         return out if out else self._storage_resources.parse_url(url)
         return out if out else self._storage_resources.parse_url(url)
 
 
     def get_resource(self, resource, url_or_name, **kwargs):
     def get_resource(self, resource, url_or_name, **kwargs):
+        if not url_or_name:
+            return None
         resource_url = (
         resource_url = (
             self._compute_resources.get_resource_url_with_default(
             self._compute_resources.get_resource_url_with_default(
                 resource, url_or_name, **kwargs) or
                 resource, url_or_name, **kwargs) or
@@ -324,3 +351,11 @@ class GCECloudProvider(BaseCloudProvider):
             cb.log.warning(
             cb.log.warning(
                 "googleapiclient.errors.HttpError: {0}".format(http_error))
                 "googleapiclient.errors.HttpError: {0}".format(http_error))
             return None
             return None
+
+    def authenticate(self):
+        try:
+            self.gce_compute
+            return True
+        except Exception as e:
+            raise ProviderConnectionException(
+                'Authentication with Google cloud provider failed: %s', e)

+ 313 - 213
cloudbridge/cloud/providers/gce/resources.py

@@ -1,11 +1,13 @@
 """
 """
 DataTypes used by this provider
 DataTypes used by this provider
 """
 """
+import base64
+import calendar
 import hashlib
 import hashlib
 import inspect
 import inspect
 import io
 import io
-import json
 import math
 import math
+import time
 import uuid
 import uuid
 
 
 import cloudbridge as cb
 import cloudbridge as cb
@@ -30,6 +32,7 @@ from cloudbridge.cloud.base.resources import BaseSnapshot
 from cloudbridge.cloud.base.resources import BaseSubnet
 from cloudbridge.cloud.base.resources import BaseSubnet
 from cloudbridge.cloud.base.resources import BaseVMFirewall
 from cloudbridge.cloud.base.resources import BaseVMFirewall
 from cloudbridge.cloud.base.resources import BaseVMFirewallRule
 from cloudbridge.cloud.base.resources import BaseVMFirewallRule
+from cloudbridge.cloud.base.resources import BaseVMFirewallRuleContainer
 from cloudbridge.cloud.base.resources import BaseVMType
 from cloudbridge.cloud.base.resources import BaseVMType
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.base.resources import BaseVolume
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
@@ -41,6 +44,7 @@ from cloudbridge.cloud.interfaces.resources import NetworkState
 from cloudbridge.cloud.interfaces.resources import RouterState
 from cloudbridge.cloud.interfaces.resources import RouterState
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SnapshotState
 from cloudbridge.cloud.interfaces.resources import SubnetState
 from cloudbridge.cloud.interfaces.resources import SubnetState
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.providers.gce import helpers
 from cloudbridge.cloud.providers.gce import helpers
 
 
@@ -55,11 +59,11 @@ except NameError:
 
 
 class GCEKeyPair(BaseKeyPair):
 class GCEKeyPair(BaseKeyPair):
 
 
-    def __init__(self, provider, kp_id, kp_name, kp_material=None):
+    def __init__(self, provider, kp_id, kp_name, private_key=None):
         super(GCEKeyPair, self).__init__(provider, None)
         super(GCEKeyPair, self).__init__(provider, None)
         self._kp_id = kp_id
         self._kp_id = kp_id
         self._kp_name = kp_name
         self._kp_name = kp_name
-        self._kp_material = kp_material
+        self._private_key = private_key
 
 
     @property
     @property
     def id(self):
     def id(self):
@@ -67,8 +71,7 @@ class GCEKeyPair(BaseKeyPair):
 
 
     @property
     @property
     def name(self):
     def name(self):
-        # use e-mail as keyname if possible, or ID if not
-        return self._kp_name or self.id
+        return self._kp_name
 
 
     def delete(self):
     def delete(self):
         svc = self._provider.security.key_pairs
         svc = self._provider.security.key_pairs
@@ -86,11 +89,7 @@ class GCEKeyPair(BaseKeyPair):
 
 
     @property
     @property
     def material(self):
     def material(self):
-        return self._kp_material
-
-    @material.setter
-    def material(self, value):
-        self._kp_material = value
+        return self._private_key
 
 
 
 
 class GCEVMType(BaseVMType):
 class GCEVMType(BaseVMType):
@@ -269,31 +268,44 @@ class GCEFirewallsDelegate(object):
             self._delete_firewall(firewall)
             self._delete_firewall(firewall)
         self._update_list_response()
         self._update_list_response()
 
 
-    def add_firewall(self, tag, ip_protocol, port, source_range, source_tag,
-                     description, network_name):
+    def add_firewall(self, tag, direction, protocol, priority, port,
+                     src_dest_range, src_dest_tag, description, network_name):
         """
         """
         Create a new firewall.
         Create a new firewall.
         """
         """
-        if self.find_firewall(tag, ip_protocol, port, source_range,
-                              source_tag, network_name) is not None:
+        if self.find_firewall(
+                tag, direction, protocol, port, src_dest_range, src_dest_tag,
+                network_name) is not None:
             return True
             return True
         # Do not let the user accidentally open traffic from the world by not
         # Do not let the user accidentally open traffic from the world by not
         # explicitly specifying the source.
         # explicitly specifying the source.
-        if source_tag is None and source_range is None:
+        if src_dest_tag is None and src_dest_range is None:
             return False
             return False
         firewall = {
         firewall = {
             'name': 'firewall-{0}'.format(uuid.uuid4()),
             'name': 'firewall-{0}'.format(uuid.uuid4()),
             'network': GCEFirewallsDelegate._NETWORK_URL_PREFIX + network_name,
             'network': GCEFirewallsDelegate._NETWORK_URL_PREFIX + network_name,
-            'allowed': [{'IPProtocol': str(ip_protocol)}],
+            'allowed': [{'IPProtocol': str(protocol)}],
             'targetTags': [tag]}
             'targetTags': [tag]}
         if description is not None:
         if description is not None:
             firewall['description'] = description
             firewall['description'] = description
         if port is not None:
         if port is not None:
             firewall['allowed'][0]['ports'] = [port]
             firewall['allowed'][0]['ports'] = [port]
-        if source_range is not None:
-            firewall['sourceRanges'] = [source_range]
-        if source_tag is not None:
-            firewall['sourceTags'] = [source_tag]
+        if direction == TrafficDirection.INBOUND:
+            firewall['direction'] = 'INGRESS'
+            src_dest_str = 'source'
+        else:
+            firewall['direction'] = 'EGRESS'
+            src_dest_str = 'destination'
+        if src_dest_range is not None:
+            firewall[src_dest_str + 'Ranges'] = [src_dest_range]
+        if src_dest_tag is not None:
+            if direction == TrafficDirection.OUTBOUND:
+                cb.log.warning('GCP does not support egress rules to network '
+                               'tags. Only IP ranges are acceptable.')
+            else:
+                firewall['sourceTags'] = [src_dest_tag]
+        if priority is not None:
+            firewall['priority'] = priority
         project_name = self._provider.project_name
         project_name = self._provider.project_name
         try:
         try:
             response = (self._provider
             response = (self._provider
@@ -311,24 +323,28 @@ class GCEFirewallsDelegate(object):
             self._update_list_response()
             self._update_list_response()
         return True
         return True
 
 
-    def find_firewall(self, tag, ip_protocol, port, source_range, source_tag,
-                      network_name):
+    def find_firewall(self, tag, direction, protocol, port, src_dest_range,
+                      src_dest_tag, network_name):
         """
         """
         Find a firewall with give parameters.
         Find a firewall with give parameters.
         """
         """
-        if source_range is None and source_tag is None:
-            source_range = '0.0.0.0/0'
+        if src_dest_range is None and src_dest_tag is None:
+            src_dest_range = '0.0.0.0/0'
+        if direction == TrafficDirection.INBOUND:
+            src_dest_str = 'source'
+        else:
+            src_dest_str = 'destination'
         for firewall in self.iter_firewalls(tag, network_name):
         for firewall in self.iter_firewalls(tag, network_name):
-            if firewall['allowed'][0]['IPProtocol'] != ip_protocol:
+            if firewall['allowed'][0]['IPProtocol'] != protocol:
                 continue
                 continue
             if not self._check_list_in_dict(firewall['allowed'][0], 'ports',
             if not self._check_list_in_dict(firewall['allowed'][0], 'ports',
                                             port):
                                             port):
                 continue
                 continue
-            if not self._check_list_in_dict(firewall, 'sourceRanges',
-                                            source_range):
+            if not self._check_list_in_dict(firewall, src_dest_str + 'Ranges',
+                                            src_dest_range):
                 continue
                 continue
-            if not self._check_list_in_dict(firewall, 'sourceTags',
-                                            source_tag):
+            if not self._check_list_in_dict(firewall, src_dest_str + 'Tags',
+                                            src_dest_tag):
                 continue
                 continue
             return firewall['id']
             return firewall['id']
         return None
         return None
@@ -343,17 +359,24 @@ class GCEFirewallsDelegate(object):
                 continue
                 continue
             if ('sourceRanges' in firewall and
             if ('sourceRanges' in firewall and
                     len(firewall['sourceRanges']) == 1):
                     len(firewall['sourceRanges']) == 1):
-                info['source_range'] = firewall['sourceRanges'][0]
+                info['src_dest_range'] = firewall['sourceRanges'][0]
+            elif ('destinationRanges' in firewall and
+                    len(firewall['destinationRanges']) == 1):
+                info['src_dest_range'] = firewall['destinationRanges'][0]
             if 'sourceTags' in firewall and len(firewall['sourceTags']) == 1:
             if 'sourceTags' in firewall and len(firewall['sourceTags']) == 1:
-                info['source_tag'] = firewall['sourceTags'][0]
+                info['src_dest_tag'] = firewall['sourceTags'][0]
             if 'targetTags' in firewall and len(firewall['targetTags']) == 1:
             if 'targetTags' in firewall and len(firewall['targetTags']) == 1:
                 info['target_tag'] = firewall['targetTags'][0]
                 info['target_tag'] = firewall['targetTags'][0]
             if 'IPProtocol' in firewall['allowed'][0]:
             if 'IPProtocol' in firewall['allowed'][0]:
-                info['ip_protocol'] = firewall['allowed'][0]['IPProtocol']
+                info['protocol'] = firewall['allowed'][0]['IPProtocol']
             if ('ports' in firewall['allowed'][0] and
             if ('ports' in firewall['allowed'][0] and
                     len(firewall['allowed'][0]['ports']) == 1):
                     len(firewall['allowed'][0]['ports']) == 1):
                 info['port'] = firewall['allowed'][0]['ports'][0]
                 info['port'] = firewall['allowed'][0]['ports'][0]
             info['network_name'] = self.network_name(firewall)
             info['network_name'] = self.network_name(firewall)
+            if 'direction' in firewall:
+                info['direction'] = firewall['direction']
+            if 'priority' in firewall:
+                info['priority'] = firewall['priority']
             return info
             return info
         return info
         return info
 
 
@@ -431,13 +454,14 @@ class GCEVMFirewall(BaseVMFirewall):
 
 
     def __init__(self, delegate, tag, network=None, description=None):
     def __init__(self, delegate, tag, network=None, description=None):
         super(GCEVMFirewall, self).__init__(delegate.provider, tag)
         super(GCEVMFirewall, self).__init__(delegate.provider, tag)
-        self._description = description
         self._delegate = delegate
         self._delegate = delegate
+        self._description = description
         if network is None:
         if network is None:
             self._network = delegate.provider.networking.networks.get_by_name(
             self._network = delegate.provider.networking.networks.get_by_name(
                     GCEFirewallsDelegate.DEFAULT_NETWORK)
                     GCEFirewallsDelegate.DEFAULT_NETWORK)
         else:
         else:
             self._network = network
             self._network = network
+        self._rule_container = GCEVMFirewallRuleContainer(self)
 
 
     @property
     @property
     def id(self):
     def id(self):
@@ -466,13 +490,14 @@ class GCEVMFirewall(BaseVMFirewall):
         If the GCE firewalls are created using this API, they all have the same
         If the GCE firewalls are created using this API, they all have the same
         description.
         description.
         """
         """
-        if self._description is not None:
-            return self._description
-        for firewall in self._delegate.iter_firewalls(self._vm_firewall,
-                                                      self._network.name):
-            if 'description' in firewall:
-                return firewall['description']
-        return None
+        if self._description is None:
+            for firewall in self._delegate.iter_firewalls(self._vm_firewall,
+                                                          self._network.name):
+                if 'description' in firewall:
+                    self._description = firewall['description']
+        if self._description is None:
+            self._description = ''
+        return self._description
 
 
     @property
     @property
     def network_id(self):
     def network_id(self):
@@ -480,11 +505,53 @@ class GCEVMFirewall(BaseVMFirewall):
 
 
     @property
     @property
     def rules(self):
     def rules(self):
-        out = []
-        for firewall in self._delegate.iter_firewalls(self._vm_firewall,
-                                                      self._network.name):
-            out.append(GCEVMFirewallRule(self._delegate, firewall['id']))
-        return out
+        return self._rule_container
+
+    def delete(self):
+        for rule in self._rule_container:
+            rule.delete()
+        self._rule_container.dummy_rule.force_delete()
+
+    def to_json(self):
+        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
+        js = {k: v for(k, v) in attr if not k.startswith('_')}
+        json_rules = [r.to_json() for r in self.rules]
+        js['rules'] = json_rules
+        return js
+
+    @property
+    def network(self):
+        return self._network
+
+    @property
+    def delegate(self):
+        return self._delegate
+
+
+class GCEVMFirewallRuleContainer(BaseVMFirewallRuleContainer):
+
+    def __init__(self, firewall):
+        super(GCEVMFirewallRuleContainer, self).__init__(
+                firewall.delegate.provider, firewall)
+        self._dummy_rule = None
+
+    def list(self, limit=None, marker=None):
+        rules = []
+        for firewall in self.firewall.delegate.iter_firewalls(
+                self.firewall.name, self.firewall.network.name):
+            rule = GCEVMFirewallRule(self.firewall, firewall['id'])
+            if rule.is_dummy_rule():
+                self._dummy_rule = rule
+            else:
+                rules.append(rule)
+        return ClientPagedResultList(self._provider, rules,
+                                     limit=limit, marker=marker)
+
+    @property
+    def dummy_rule(self):
+        if not self._dummy_rule:
+            self.list()
+        return self._dummy_rule
 
 
     @staticmethod
     @staticmethod
     def to_port_range(from_port, to_port):
     def to_port_range(from_port, to_port):
@@ -495,75 +562,61 @@ class GCEVMFirewall(BaseVMFirewall):
         else:
         else:
             return to_port
             return to_port
 
 
-    def add_rule(self, ip_protocol, from_port=None, to_port=None,
-                 cidr_ip=None, src_group=None):
-        port = GCEVMFirewall.to_port_range(from_port, to_port)
-        src_tag = src_group.name if src_group is not None else None
-        self._delegate.add_firewall(self._vm_firewall, ip_protocol, port,
-                                    cidr_ip, src_tag, self.description,
-                                    self._network.name)
-        return self.get_rule(ip_protocol, from_port, to_port, cidr_ip,
-                             src_group)
-
-    def get_rule(self, ip_protocol=None, from_port=None, to_port=None,
-                 cidr_ip=None, src_group=None):
-        port = GCEVMFirewall.to_port_range(from_port, to_port)
-        src_tag = src_group.name if src_group is not None else None
-        firewall_id = self._delegate.find_firewall(
-                self._vm_firewall, ip_protocol, port, cidr_ip, src_tag,
-                self._network.name)
-        if firewall_id is None:
+    def create_with_priority(self, direction, protocol, priority,
+                             from_port=None, to_port=None, cidr=None,
+                             src_dest_fw=None):
+        port = GCEVMFirewallRuleContainer.to_port_range(from_port, to_port)
+        src_dest_tag = None
+        src_dest_fw_id = None
+        if src_dest_fw:
+            src_dest_tag = src_dest_fw.name
+            src_dest_fw_id = src_dest_fw.id
+        if not self.firewall.delegate.add_firewall(
+                self.firewall.name, direction, protocol, priority, port, cidr,
+                src_dest_tag, self.firewall.description,
+                self.firewall.network.name):
             return None
             return None
-        return GCEVMFirewallRule(self._delegate, firewall_id)
-
-    def to_json(self):
-        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
-        js = {k: v for(k, v) in attr if not k.startswith('_')}
-        json_rules = [r.to_json() for r in self.rules]
-        js['rules'] = [json.loads(r) for r in json_rules]
-        return json.dumps(js, sort_keys=True)
+        rules = self.find(direction=direction, protocol=protocol,
+                          from_port=from_port, to_port=to_port, cidr=cidr,
+                          src_dest_fw_id=src_dest_fw_id)
+        if len(rules) < 1:
+            return None
+        return rules[0]
 
 
-    def delete(self):
-        for rule in self.rules:
-            rule.delete()
+    def create(self, direction, protocol, from_port=None, to_port=None,
+               cidr=None, src_dest_fw=None):
+        return self.create_with_priority(direction, protocol, 1000, from_port,
+                                         to_port, cidr, src_dest_fw)
 
 
 
 
 class GCEVMFirewallRule(BaseVMFirewallRule):
 class GCEVMFirewallRule(BaseVMFirewallRule):
 
 
-    def __init__(self, delegate, firewall_id):
-        super(GCEVMFirewallRule, self).__init__(
-                delegate.provider, firewall_id, None)
-        self._delegate = delegate
-
-    @property
-    def parent(self):
-        """
-        Return the VM firewall to which this rule belongs.
-        """
-        info = self._delegate.get_firewall_info(self._rule)
-        if info is None:
-            return None
-        if 'target_tag' not in info or info['network_name'] is None:
-            return None
-        network = self._delegate.network.get_by_name(info['network_name'])
-        if network is None:
-            return None
-        return GCEVMFirewall(self._delegate, info['target_tag'], network)
+    def __init__(self, parent_fw, rule):
+        super(GCEVMFirewallRule, self).__init__(parent_fw, rule)
 
 
     @property
     @property
     def id(self):
     def id(self):
         return self._rule
         return self._rule
 
 
     @property
     @property
-    def ip_protocol(self):
-        info = self._delegate.get_firewall_info(self._rule)
-        if info is None or 'ip_protocol' not in info:
+    def direction(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None:
+            return None
+        if 'direction' in info and info['direction'] == 'EGRESS':
+            return TrafficDirection.OUTBOUND
+        return TrafficDirection.INBOUND
+
+    @property
+    def protocol(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'protocol' not in info:
             return None
             return None
-        return info['ip_protocol']
+        return info['protocol']
 
 
     @property
     @property
     def from_port(self):
     def from_port(self):
-        info = self._delegate.get_firewall_info(self._rule)
+        info = self.firewall.delegate.get_firewall_info(self._rule)
         if info is None or 'port' not in info:
         if info is None or 'port' not in info:
             return 0
             return 0
         port = info['port']
         port = info['port']
@@ -578,7 +631,7 @@ class GCEVMFirewallRule(BaseVMFirewallRule):
 
 
     @property
     @property
     def to_port(self):
     def to_port(self):
-        info = self._delegate.get_firewall_info(self._rule)
+        info = self.firewall.delegate.get_firewall_info(self._rule)
         if info is None or 'port' not in info:
         if info is None or 'port' not in info:
             return 0
             return 0
         port = info['port']
         port = info['port']
@@ -592,40 +645,61 @@ class GCEVMFirewallRule(BaseVMFirewallRule):
         return 0
         return 0
 
 
     @property
     @property
-    def cidr_ip(self):
+    def cidr(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'src_dest_range' not in info:
+            return None
+        return info['src_dest_range']
+
+    @property
+    def src_dest_fw_id(self):
         """
         """
-        Return the IP of machines from which this rule allows traffic.
+        Return the VM firewall given access by this rule.
         """
         """
-        info = self._delegate.get_firewall_info(self._rule)
-        if info is None or 'source_range' not in info:
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'src_dest_tag' not in info:
             return None
             return None
-        return info['source_range']
+        return GCEFirewallsDelegate.tag_network_id(info['src_dest_tag'],
+                                                   self.firewall.network.name)
 
 
     @property
     @property
-    def group(self):
+    def src_dest_fw(self):
         """
         """
-        Return the VM firewall from which this rule allows traffic.
+        Return the VM firewall given access by this rule.
         """
         """
-        info = self._delegate.get_firewall_info(self._rule)
-        if info is None:
-            return None
-        if 'source_tag' not in info or info['network_name'] is None:
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        if info is None or 'src_dest_tag' not in info:
             return None
             return None
-        network = self._delegate.provider.networking.networks.get_by_name(
-                info['network_name'])
-        if network is None:
-            return None
-        return GCEVMFirewall(self._delegate, info['source_tag'], network)
+        return GCEVMFirewall(
+                self.firewall.delegate, info['src_dest_tag'],
+                self.firewall.network)
 
 
-    def to_json(self):
-        attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
-        js = {k: v for(k, v) in attr if not k.startswith('_')}
-        js['group'] = self.group.id if self.group else ''
-        js['parent'] = self.parent.id if self.parent else ''
-        return json.dumps(js, sort_keys=True)
+    @property
+    def priority(self):
+        info = self.firewall.delegate.get_firewall_info(self._rule)
+        # The default firewall rule priority, when not specified, is 1000.
+        if info is None or 'priority' not in info:
+            return 1000
+        return info['priority']
+
+    def is_dummy_rule(self):
+        if self.priority != 65534:
+            return False
+        if self.direction != TrafficDirection.OUTBOUND:
+            return False
+        if self.protocol != 'tcp':
+            return False
+        if self.cidr != '0.0.0.0/0':
+            return False
+        return True
 
 
     def delete(self):
     def delete(self):
-        self._delegate.delete_firewall_id(self._rule)
+        if (self.is_dummy_rule()):
+            return
+        self.force_delete()
+
+    def force_delete(self):
+        self.firewall.delegate.delete_firewall_id(self._rule)
 
 
 
 
 class GCEMachineImage(BaseMachineImage):
 class GCEMachineImage(BaseMachineImage):
@@ -706,9 +780,10 @@ class GCEMachineImage(BaseMachineImage):
         Refreshes the state of this instance by re-querying the cloud provider
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         for its latest state.
         """
         """
+        name = self.name
         self._gce_image = self._provider.get_resource('images', self.id)
         self._gce_image = self._provider.get_resource('images', self.id)
         if not self._gce_image:
         if not self._gce_image:
-            self._gce_image = {'status': 'UNKNOWN'}
+            self._gce_image = {'name': name, 'status': 'UNKNOWN'}
 
 
 
 
 class GCEInstance(BaseInstance):
 class GCEInstance(BaseInstance):
@@ -758,12 +833,12 @@ class GCEInstance(BaseInstance):
         """
         """
         Set the instance name.
         Set the instance name.
         """
         """
-        GCEInstance.assert_valid_resource_name(name)
+        GCEInstance.assert_valid_resource_name(value)
         # In GCE, the name of the instance is provided by the client when
         # In GCE, the name of the instance is provided by the client when
         # initially creating the resource. The name cannot be changed after
         # initially creating the resource. The name cannot be changed after
         # the instance is created.
         # the instance is created.
         cb.log.warning("Setting instance name after it is created is not "
         cb.log.warning("Setting instance name after it is created is not "
-                       "supported by this provider.")
+                       "supported by this provider: %s", value)
 
 
     @property
     @property
     def public_ips(self):
     def public_ips(self):
@@ -784,7 +859,7 @@ class GCEInstance(BaseInstance):
                 if 'natIP' in access_config:
                 if 'natIP' in access_config:
                     ips.append(access_config['natIP'])
                     ips.append(access_config['natIP'])
         for ip in self.inet_gateway.floating_ips:
         for ip in self.inet_gateway.floating_ips:
-            if ip.in_use():
+            if ip.in_use:
                 if ip.private_ip in self.private_ips:
                 if ip.private_ip in self.private_ips:
                     ips.append(ip.public_ip)
                     ips.append(ip.public_ip)
         return ips
         return ips
@@ -847,14 +922,16 @@ class GCEInstance(BaseInstance):
         """
         """
         Permanently terminate this instance.
         Permanently terminate this instance.
         """
         """
+        name = self.name
         response = (self._provider
         response = (self._provider
                         .gce_compute
                         .gce_compute
                         .instances()
                         .instances()
                         .delete(project=self._provider.project_name,
                         .delete(project=self._provider.project_name,
                                 zone=self.zone_name,
                                 zone=self.zone_name,
-                                instance=self.name)
+                                instance=name)
                         .execute())
                         .execute())
         self._provider.wait_for_operation(response, zone=self.zone_name)
         self._provider.wait_for_operation(response, zone=self.zone_name)
+        self._gce_instance = {'name': name, 'status': 'UNKNOWN'}
 
 
     def stop(self):
     def stop(self):
         """
         """
@@ -927,7 +1004,10 @@ class GCEInstance(BaseInstance):
         """
         """
         Get the name of the key pair associated with this instance.
         Get the name of the key pair associated with this instance.
         """
         """
-        return self._provider.security.key_pairs.name
+        try:
+            return next(self._provider.security.key_pairs)
+        except StopIteration:
+            return None
 
 
     @property
     @property
     def inet_gateway(self):
     def inet_gateway(self):
@@ -974,7 +1054,7 @@ class GCEInstance(BaseInstance):
         try:
         try:
             for target_instance in helpers.iter_all(
             for target_instance in helpers.iter_all(
                     self._provider.gce_compute.targetInstances(),
                     self._provider.gce_compute.targetInstances(),
-                    project=self.name,
+                    project=self._provider.project_name,
                     zone=self.zone_name):
                     zone=self.zone_name):
                 url = self._provider.parse_url(target_instance['instance'])
                 url = self._provider.parse_url(target_instance['instance'])
                 if url.parameters['instance'] == self.name:
                 if url.parameters['instance'] == self.name:
@@ -1000,7 +1080,7 @@ class GCEInstance(BaseInstance):
             response = (self._provider
             response = (self._provider
                             .gce_compute
                             .gce_compute
                             .targetInstances()
                             .targetInstances()
-                            .insert(project=self.name,
+                            .insert(project=self._provider.project_name,
                                     zone=self.zone_name,
                                     zone=self.zone_name,
                                     body=body)
                                     body=body)
                             .execute())
                             .execute())
@@ -1117,54 +1197,46 @@ class GCEInstance(BaseInstance):
             return False
             return False
         return True
         return True
 
 
-    def add_floating_ip(self, ip_address):
+    def add_floating_ip(self, floating_ip):
         """
         """
         Add an elastic IP address to this instance.
         Add an elastic IP address to this instance.
         """
         """
-        for ip in self.inet_gateway.floating_ips:
-            if ip.public_ip == ip_address:
-                if ip.in_use():
-                    if ip.private_ip not in self.private_ips:
-                        cb.log.warning(
-                            'Floating IP "%s" is already associated to "%s".',
-                            ip_address, self.name)
-                    return
-                target_instance = self._get_target_instance()
-                if not target_instance:
-                    cb.log.warning(
-                            'Could not create a targetInstance for "%s"',
-                            self.name)
-                    return
-                if not self._forward(ip, target_instance):
-                    cb.log.warning('Could not forward "%s" to "%s"',
-                                   ip.public_ip, target_instance['selfLink'])
-                return
-        cb.log.warning('Floating IP "%s" does not exist.', ip_address)
+        fip = (floating_ip if isinstance(floating_ip, GCEFloatingIP)
+               else self.inet_gateway.floating_ips.get(floating_ip))
+        if fip.in_use:
+            if fip.private_ip not in self.private_ips:
+                cb.log.warning('Floating IP "%s" is not associated to "%s"',
+                               fip.public_ip, self.name)
+            return
+        target_instance = self._get_target_instance()
+        if not target_instance:
+            cb.log.warning('Could not create a targetInstance for "%s"',
+                           self.name)
+            return
+        if not self._forward(fip, target_instance):
+            cb.log.warning('Could not forward "%s" to "%s"',
+                           fip.public_ip, target_instance['selfLink'])
 
 
-    def remove_floating_ip(self, ip_address):
+    def remove_floating_ip(self, floating_ip):
         """
         """
         Remove a elastic IP address from this instance.
         Remove a elastic IP address from this instance.
         """
         """
-        for ip in self.inet_gateway.floating_ips:
-            if ip.public_ip == ip_address:
-                if not ip.in_use() or ip.private_ip not in self.private_ips:
-                    cb.log.warning(
-                        'Floating IP "%s" is not associated to "%s".',
-                        ip_address, self.name)
-                    return
-                target_instance = self._get_target_instance()
-                if not target_instance:
-                    # We should not be here.
-                    cb.log.warning('Something went wrong! "%s" is associated '
-                                   'to "%s" with no target instance',
-                                   ip_address, self.name)
-                    return
-                if not self._delete_existing_rule(ip, target_instance):
-                    cb.log.warning(
-                        'Could not remove floating IP "%s" from instance "%s"',
-                        ip.public_ip, self.name)
-                return
-        cb.log.warning('Floating IP "%s" does not exist.', ip_address)
+        fip = (floating_ip if isinstance(floating_ip, GCEFloatingIP)
+               else self.inet_gateway.floating_ips.get(floating_ip))
+        if not fip.in_use or fip.private_ip not in self.private_ips:
+            cb.log.warning('Floating IP "%s" is not associated to "%s"',
+                           fip.public_ip, self.name)
+            return
+        target_instance = self._get_target_instance()
+        if not target_instance:
+            # We should not be here.
+            cb.log.warning('Something went wrong! "%s" is associated to "%s" '
+                           'with no target instance', fip.public_ip, self.name)
+            return
+        if not self._delete_existing_rule(fip, target_instance):
+            cb.log.warning(
+                'Could not remove floating IP "%s" from instance "%s"',
+                fip.public_ip, self.name)
 
 
     @property
     @property
     def state(self):
     def state(self):
@@ -1176,9 +1248,10 @@ class GCEInstance(BaseInstance):
         Refreshes the state of this instance by re-querying the cloud provider
         Refreshes the state of this instance by re-querying the cloud provider
         for its latest state.
         for its latest state.
         """
         """
+        name = self.name
         self._gce_instance = self._provider.get_resource('instances', self.id)
         self._gce_instance = self._provider.get_resource('instances', self.id)
         if not self._gce_instance:
         if not self._gce_instance:
-            self._gce_instance = {'status': 'UNKNOWN'}
+            self._gce_instance = {'name': name, 'status': 'UNKNOWN'}
 
 
     def add_vm_firewall(self, sg):
     def add_vm_firewall(self, sg):
         tag = sg.name if isinstance(sg, GCEVMFirewall) else sg
         tag = sg.name if isinstance(sg, GCEVMFirewall) else sg
@@ -1255,7 +1328,7 @@ class GCENetwork(BaseNetwork):
 
 
     @property
     @property
     def subnets(self):
     def subnets(self):
-        return self._provider.networking.subnets.list(network=self)
+        return list(self._provider.networking.subnets.iter(network=self))
 
 
     def delete(self):
     def delete(self):
         try:
         try:
@@ -1345,37 +1418,7 @@ class GCEFloatingIP(BaseFloatingIP):
     def __init__(self, provider, floating_ip):
     def __init__(self, provider, floating_ip):
         super(GCEFloatingIP, self).__init__(provider)
         super(GCEFloatingIP, self).__init__(provider)
         self._ip = floating_ip
         self._ip = floating_ip
-
-        # We use regional IPs to simulate floating IPs not global IPs because
-        # global IPs can be forwarded only to load balancing resources, not to
-        # a specific instance. Find out the region to which the IP belongs.
-        url = provider.parse_url(self._ip['region'])
-        self._region_name = url.parameters['region']
-
-        # Check if the address is used by a resource.
-        self._rule = None
-        self._target_instance = None
-        if 'users' in floating_ip and len(floating_ip['users']) > 0:
-            if len(floating_ip['users']) > 1:
-                cb.log.warning('Address "%s" in use by more than one resource',
-                               floating_ip['address'])
-            resource_parsed_url = provider.parse_url(floating_ip['users'][0])
-            resource = resource_parsed_url.get_resource()
-            if resource['kind'] == 'compute#forwardingRule':
-                self._rule = resource
-                target = provider.parse_url(resource['target']).get_resource()
-                if target['kind'] == 'compute#targetInstance':
-                    url = provider.parse_url(target['instance'])
-                    try:
-                        self._target_instance = url.get_resource()
-                    except googleapiclient.errors.HttpError:
-                        self._target_instance = GCEFloatingIP._DEAD_INSTANCE
-                else:
-                    cb.log.warning('Address "%s" is forwarded to a %s',
-                                   floating_ip['address'], target['kind'])
-            else:
-                cb.log.warning('Address "%s" in use by a %s',
-                               floating_ip['address'], resource['kind'])
+        self._process_ip_users()
 
 
     @property
     @property
     def id(self):
     def id(self):
@@ -1383,7 +1426,11 @@ class GCEFloatingIP(BaseFloatingIP):
 
 
     @property
     @property
     def region_name(self):
     def region_name(self):
-        return self._region_name
+        # We use regional IPs to simulate floating IPs not global IPs because
+        # global IPs can be forwarded only to load balancing resources, not to
+        # a specific instance. Find out the region to which the IP belongs.
+        url = self._provider.parse_url(self._ip['region'])
+        return url.parameters['region']
 
 
     @property
     @property
     def public_ip(self):
     def public_ip(self):
@@ -1396,6 +1443,7 @@ class GCEFloatingIP(BaseFloatingIP):
             return None
             return None
         return self._target_instance['networkInterfaces'][0]['networkIP']
         return self._target_instance['networkInterfaces'][0]['networkIP']
 
 
+    @property
     def in_use(self):
     def in_use(self):
         return True if self._target_instance else False
         return True if self._target_instance else False
 
 
@@ -1407,26 +1455,55 @@ class GCEFloatingIP(BaseFloatingIP):
                             .gce_compute
                             .gce_compute
                             .forwardingRules()
                             .forwardingRules()
                             .delete(project=project_name,
                             .delete(project=project_name,
-                                    region=self._region_name,
+                                    region=self.region_name,
                                     forwardingRule=self._rule['name'])
                                     forwardingRule=self._rule['name'])
                             .execute())
                             .execute())
             self._provider.wait_for_operation(response,
             self._provider.wait_for_operation(response,
-                                              region=self._region_name)
+                                              region=self.region_name)
 
 
         # Release the address.
         # Release the address.
         response = (self._provider
         response = (self._provider
                         .gce_compute
                         .gce_compute
                         .addresses()
                         .addresses()
                         .delete(project=project_name,
                         .delete(project=project_name,
-                                region=self._region_name,
+                                region=self.region_name,
                                 address=self._ip['name'])
                                 address=self._ip['name'])
                         .execute())
                         .execute())
-        self._provider.wait_for_operation(response, region=self._region_name)
+        self._provider.wait_for_operation(response, region=self.region_name)
 
 
     def refresh(self):
     def refresh(self):
         self._ip = self._provider.get_resource('addresses', self.id)
         self._ip = self._provider.get_resource('addresses', self.id)
         if not self._ip:
         if not self._ip:
             self._ip = {'status': 'UNKNOWN'}
             self._ip = {'status': 'UNKNOWN'}
+        else:
+            self._process_ip_users()
+
+    def _process_ip_users(self):
+        self._rule = None
+        self._target_instance = None
+
+        if 'users' in self._ip and len(self._ip['users']) > 0:
+            provider = self._provider
+            if len(self._ip['users']) > 1:
+                cb.log.warning('Address "%s" in use by more than one resource',
+                               self._ip['address'])
+            resource_parsed_url = provider.parse_url(self._ip['users'][0])
+            resource = resource_parsed_url.get_resource()
+            if resource['kind'] == 'compute#forwardingRule':
+                self._rule = resource
+                target = provider.parse_url(resource['target']).get_resource()
+                if target['kind'] == 'compute#targetInstance':
+                    url = provider.parse_url(target['instance'])
+                    try:
+                        self._target_instance = url.get_resource()
+                    except googleapiclient.errors.HttpError:
+                        self._target_instance = GCEFloatingIP._DEAD_INSTANCE
+                else:
+                    cb.log.warning('Address "%s" is forwarded to a %s',
+                                   self._ip['address'], target['kind'])
+            else:
+                cb.log.warning('Address "%s" in use by a %s',
+                               self._ip['address'], resource['kind'])
 
 
 
 
 class GCERouter(BaseRouter):
 class GCERouter(BaseRouter):
@@ -1483,10 +1560,14 @@ class GCERouter(BaseRouter):
             subnet = self._provider.networking.subnets.get(subnet)
             subnet = self._provider.networking.subnets.get(subnet)
         if subnet.network_id == self.network_id:
         if subnet.network_id == self.network_id:
             return
             return
-        cb.log.warning('GCE routers should be attached at creation time')
+        cb.log.warning('Google Cloud Routers automatically learn new subnets '
+                       'in your VPC network and announces them to your '
+                       'on-premises network')
 
 
     def detach_subnet(self, network_id):
     def detach_subnet(self, network_id):
-        cb.log.warning('GCE routers are always attached')
+        cb.log.warning('Cannot detach from subnet. Google Cloud Routers '
+                       'automatically learn new subnets in your VPC network '
+                       'and announces them to your on-premises network')
 
 
     def attach_gateway(self, gateway):
     def attach_gateway(self, gateway):
         pass
         pass
@@ -1572,6 +1653,7 @@ class GCESubnet(BaseSubnet):
 
 
     @name.setter
     @name.setter
     def name(self, value):
     def name(self, value):
+        GCESubnet.assert_valid_resource_name(value)
         if value == self.name:
         if value == self.name:
             return
             return
         cb.log.warning('Cannot change the name of a GCE subnetwork')
         cb.log.warning('Cannot change the name of a GCE subnetwork')
@@ -1594,7 +1676,7 @@ class GCESubnet(BaseSubnet):
 
 
     @property
     @property
     def region_name(self):
     def region_name(self):
-        parsed_url = self.provider.parse_url(self.id)
+        parsed_url = self._provider.parse_url(self.id)
         return parsed_url.parameters['region']
         return parsed_url.parameters['region']
 
 
     @property
     @property
@@ -2008,7 +2090,21 @@ class GCSObject(BaseBucketObject):
              .execute())
              .execute())
 
 
     def generate_url(self, expires_in=0):
     def generate_url(self, expires_in=0):
-        return self._obj['mediaLink']
+        """
+        Generates a signed URL accessible to everyone.
+        """
+        expiration = calendar.timegm(time.gmtime()) + 2 * 24 * 60 * 60
+        signature = self._provider.sign_blob(
+                'GET\n\n\n%d\n/%s/%s' %
+                (expiration, self._obj['bucket'], self.name))
+        encoded_signature = base64.b64encode(signature)
+        url_encoded_signature = (encoded_signature.replace('+', '%2B')
+                                                  .replace('/', '%2F'))
+        return ('https://storage.googleapis.com/%s/%s?GoogleAccessId=%s'
+                '&Expires=%d&Signature=%s' % (self._obj['bucket'], self.name,
+                                              self._provider.client_id,
+                                              expiration,
+                                              url_encoded_signature))
 
 
 
 
 class GCSBucketContainer(BaseBucketContainer):
 class GCSBucketContainer(BaseBucketContainer):
@@ -2095,6 +2191,10 @@ class GCSBucket(BaseBucket):
              .buckets()
              .buckets()
              .delete(bucket=self.name)
              .delete(bucket=self.name)
              .execute())
              .execute())
+        # GCS has a rate limit of 1 operation per 2 seconds for bucket
+        # creation/deletion: https://cloud.google.com/storage/quotas.  Throttle
+        # here to avoid future failures.
+        time.sleep(2)
 
 
     def create_object(self, name):
     def create_object(self, name):
         """
         """

+ 80 - 49
cloudbridge/cloud/providers/gce/services.py

@@ -1,4 +1,5 @@
 import hashlib
 import hashlib
+import time
 import uuid
 import uuid
 from collections import namedtuple
 from collections import namedtuple
 
 
@@ -22,6 +23,8 @@ from cloudbridge.cloud.base.services import BaseSubnetService
 from cloudbridge.cloud.base.services import BaseVMFirewallService
 from cloudbridge.cloud.base.services import BaseVMFirewallService
 from cloudbridge.cloud.base.services import BaseVMTypeService
 from cloudbridge.cloud.base.services import BaseVMTypeService
 from cloudbridge.cloud.base.services import BaseVolumeService
 from cloudbridge.cloud.base.services import BaseVolumeService
+from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
+from cloudbridge.cloud.interfaces.resources import TrafficDirection
 from cloudbridge.cloud.interfaces.resources import VMFirewall
 from cloudbridge.cloud.interfaces.resources import VMFirewall
 from cloudbridge.cloud.providers.gce import helpers
 from cloudbridge.cloud.providers.gce import helpers
 
 
@@ -82,7 +85,7 @@ class GCEKeyPairService(BaseKeyPairService):
         """
         """
         Returns a KeyPair given its ID.
         Returns a KeyPair given its ID.
         """
         """
-        for kp in self.list():
+        for kp in self:
             if kp.id == key_pair_id:
             if kp.id == key_pair_id:
                 return kp
                 return kp
         else:
         else:
@@ -107,16 +110,16 @@ class GCEKeyPairService(BaseKeyPairService):
 
 
     def _get_or_add_sshkey_entry(self, metadata):
     def _get_or_add_sshkey_entry(self, metadata):
         """
         """
-        Get the sshKeys entry from commonInstanceMetadata/items.
+        Get the ssh-keys entry from commonInstanceMetadata/items.
         If an entry does not exist, adds a new empty entry
         If an entry does not exist, adds a new empty entry
         """
         """
         sshkey_entry = None
         sshkey_entry = None
         entries = [item for item in metadata.get('items', [])
         entries = [item for item in metadata.get('items', [])
-                   if item['key'] == 'sshKeys']
+                   if item['key'] == 'ssh-keys']
         if entries:
         if entries:
             sshkey_entry = entries[0]
             sshkey_entry = entries[0]
         else:  # add a new entry
         else:  # add a new entry
-            sshkey_entry = {'key': 'sshKeys', 'value': ''}
+            sshkey_entry = {'key': 'ssh-keys', 'value': ''}
             if 'items' not in metadata:
             if 'items' not in metadata:
                 metadata['items'] = [sshkey_entry]
                 metadata['items'] = [sshkey_entry]
             else:
             else:
@@ -180,31 +183,44 @@ class GCEKeyPairService(BaseKeyPairService):
         key_pairs = []
         key_pairs = []
         for gce_kp in self._iter_gce_key_pairs():
         for gce_kp in self._iter_gce_key_pairs():
             kp_id = self.gce_kp_to_id(gce_kp)
             kp_id = self.gce_kp_to_id(gce_kp)
-            kp_name = gce_kp.email
-            key_pairs.append(GCEKeyPair(self.provider, kp_id, kp_name))
+            key_pairs.append(GCEKeyPair(self.provider, kp_id, gce_kp.email))
         return ClientPagedResultList(self.provider, key_pairs,
         return ClientPagedResultList(self.provider, key_pairs,
                                      limit=limit, marker=marker)
                                      limit=limit, marker=marker)
 
 
-    def find(self, name, limit=None, marker=None):
+    def find(self, **kwargs):
         """
         """
         Searches for a key pair by a given list of attributes.
         Searches for a key pair by a given list of attributes.
         """
         """
-        found_kps = []
-        for kp in self.list():
-            if kp.name == name:
-                found_kps.append(kp)
-        return ClientPagedResultList(self.provider, found_kps,
-                                     limit=limit, marker=marker)
+        kp_name = kwargs.get('name', None)
+        kp_id = kwargs.get('id', None)
+        for parameter in kwargs:
+            if parameter not in ('id', 'name'):
+                cb.log.error('Unrecognised parameters for search: %s. '
+                             'Supported attributes: id, name', parameter)
+
+        out = []
+        for kp in self:
+            if kp_name is not None and kp.name != kp_name:
+                continue
+            if kp_id is not None and kp.id != kp_id:
+                continue
+            out.append(kp)
+        return out
 
 
-    def create(self, name):
+    def create(self, name, public_key_material=None):
         GCEKeyPair.assert_valid_resource_name(name)
         GCEKeyPair.assert_valid_resource_name(name)
-        kp = self.find(name=name)
-        if kp:
-            return kp
 
 
-        private_key, public_key = helpers.generate_key_pair()
-        kp_info = GCEKeyPairService.GCEKeyInfo(name + u":ssh-rsa",
-                                               public_key, name)
+        if self.find(name=name):
+            raise DuplicateResourceException(
+                'A KeyPair with the same name %s exists', name)
+        private_key = None
+        if not public_key_material:
+            private_key, public_key_material = helpers.generate_key_pair()
+        parts = public_key_material.split(' ')
+        if len(parts) == 2:
+            public_key_material = parts[1]
+        kp_info = GCEKeyPairService.GCEKeyInfo(
+            '%s:ssh-rsa' % name, public_key_material, name)
 
 
         def _add_kp(gce_kp_generator):
         def _add_kp(gce_kp_generator):
             kp_list = []
             kp_list = []
@@ -216,7 +232,7 @@ class GCEKeyPairService(BaseKeyPairService):
 
 
         self.gce_metadata_save_op(_add_kp)
         self.gce_metadata_save_op(_add_kp)
         return GCEKeyPair(self.provider, self.gce_kp_to_id(kp_info), name,
         return GCEKeyPair(self.provider, self.gce_kp_to_id(kp_info), name,
-                          kp_material=private_key)
+                          private_key)
 
 
 
 
 class GCEVMFirewallService(BaseVMFirewallService):
 class GCEVMFirewallService(BaseVMFirewallService):
@@ -245,7 +261,13 @@ class GCEVMFirewallService(BaseVMFirewallService):
     def create(self, name, description, network_id=None):
     def create(self, name, description, network_id=None):
         GCEVMFirewall.assert_valid_resource_name(name)
         GCEVMFirewall.assert_valid_resource_name(name)
         network = self.provider.networking.networks.get(network_id)
         network = self.provider.networking.networks.get(network_id)
-        return GCEVMFirewall(self._delegate, name, network, description)
+        fw = GCEVMFirewall(self._delegate, name, network, description)
+        # This rule exists implicitly. Add it explicitly so that the firewall
+        # is not empty and the rule is shown by list/get/find methods.
+        fw.rules.create_with_priority(
+                direction=TrafficDirection.OUTBOUND, protocol='tcp',
+                priority=65534, cidr='0.0.0.0/0')
+        return fw
 
 
     def find(self, name, limit=None, marker=None):
     def find(self, name, limit=None, marker=None):
         """
         """
@@ -665,7 +687,8 @@ class GCENetworkService(BaseNetworkService):
         GCE networks are global. There is at most one network with a given
         GCE networks are global. There is at most one network with a given
         name.
         name.
         """
         """
-        return [self.get(name)]
+        network = self.get(name)
+        return [network] if network else []
 
 
     def get_by_name(self, network_name):
     def get_by_name(self, network_name):
         if network_name is None:
         if network_name is None:
@@ -674,6 +697,7 @@ class GCENetworkService(BaseNetworkService):
         return None if len(networks) == 0 else networks[0]
         return None if len(networks) == 0 else networks[0]
 
 
     def list(self, limit=None, marker=None, filter=None):
     def list(self, limit=None, marker=None, filter=None):
+        networks = []
         try:
         try:
             response = (self.provider
             response = (self.provider
                             .gce_compute
                             .gce_compute
@@ -681,13 +705,12 @@ class GCENetworkService(BaseNetworkService):
                             .list(project=self.provider.project_name,
                             .list(project=self.provider.project_name,
                                   filter=filter)
                                   filter=filter)
                             .execute())
                             .execute())
-            networks = []
             for network in response.get('items', []):
             for network in response.get('items', []):
                 networks.append(GCENetwork(self.provider, network))
                 networks.append(GCENetwork(self.provider, network))
-            return networks
         except googleapiclient.errors.HttpError as http_error:
         except googleapiclient.errors.HttpError as http_error:
             cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
             cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
-            return []
+        return ClientPagedResultList(self.provider, networks,
+                                     limit=limit, marker=marker)
 
 
     def _create(self, name, cidr_block, create_subnetworks):
     def _create(self, name, cidr_block, create_subnetworks):
         """
         """
@@ -736,7 +759,7 @@ class GCENetworkService(BaseNetworkService):
         to add additional subnets later.
         to add additional subnets later.
         """
         """
         GCENetwork.assert_valid_resource_name(name)
         GCENetwork.assert_valid_resource_name(name)
-        return self._create(name, cidr_block, True)
+        return self._create(name, cidr_block, False)
 
 
     def get_or_create_default(self):
     def get_or_create_default(self):
         return self._create(GCEFirewallsDelegate.DEFAULT_NETWORK, None, True)
         return self._create(GCEFirewallsDelegate.DEFAULT_NETWORK, None, True)
@@ -780,7 +803,7 @@ class GCERouterService(BaseRouterService):
                                      response.get('nextPageToken'),
                                      response.get('nextPageToken'),
                                      False, data=routers)
                                      False, data=routers)
 
 
-    def create(self, network, name=None):
+    def create(self, name, network):
         name = name if name else 'router-{0}'.format(uuid.uuid4())
         name = name if name else 'router-{0}'.format(uuid.uuid4())
         GCERouter.assert_valid_resource_name(name)
         GCERouter.assert_valid_resource_name(name)
 
 
@@ -874,15 +897,22 @@ class GCESubnetService(BaseSubnetService):
         instead of creating a new subnet. In this case, other parameters, i.e.
         instead of creating a new subnet. In this case, other parameters, i.e.
         the name and the zone, are ignored.
         the name and the zone, are ignored.
         """
         """
+        if not name:
+            name = 'subnet-{0}'.format(uuid.uuid4())
         GCESubnet.assert_valid_resource_name(name)
         GCESubnet.assert_valid_resource_name(name)
-        subnets = self.list(network)
-        for subnet in subnets:
+        region_name = self._zone_to_region_name(zone)
+        for subnet in self.iter(network=network):
             if BaseNetwork.cidr_blocks_overlap(subnet.cidr_block, cidr_block):
             if BaseNetwork.cidr_blocks_overlap(subnet.cidr_block, cidr_block):
+                if subnet.region_name != region_name:
+                    cb.log.error('Failed to create subnetwork in region %s: '
+                                 'the given IP range %s overlaps with a '
+                                 'subnetwork in a different region %s',
+                                 region_name, cidr_block, subnet.region_name)
+                    return None
+                return subnet
+            if subnet.name == name and subnet.region_name == region_name:
                 return subnet
                 return subnet
 
 
-        if not name:
-            name = 'subnet-{0}'.format(uuid.uuid4())
-        region_name = self._zone_to_region_name(zone)
         body = {'ipCidrRange': cidr_block,
         body = {'ipCidrRange': cidr_block,
                 'name': name,
                 'name': name,
                 'network': network.resource_url,
                 'network': network.resource_url,
@@ -913,7 +943,7 @@ class GCESubnetService(BaseSubnetService):
         zone.
         zone.
         """
         """
         network = self.provider.networking.networks.get_or_create_default()
         network = self.provider.networking.networks.get_or_create_default()
-        subnets = self.list(network, zone)
+        subnets = list(self.iter(network=network, zone=zone))
         if len(subnets) > 1:
         if len(subnets) > 1:
             cb.log.warning('The default network has more than one subnetwork '
             cb.log.warning('The default network has more than one subnetwork '
                            'in a region')
                            'in a region')
@@ -923,21 +953,18 @@ class GCESubnetService(BaseSubnetService):
         return None
         return None
 
 
     def delete(self, subnet):
     def delete(self, subnet):
-        network_url = self.provider.parse_url(subnet.network_url)
-        if subnet.name == network_url.parameters['network']:
-            # This is an auto subnetwork of an auto mode network. We cannot
-            # delete it. It will be deleted automatically when the network is
-            # deleted.
-            return
-
-        response = (self.provider
-                        .gce_compute
-                        .subnetworks()
-                        .delete(project=self.provider.project_name,
-                                region=subnet.region_name,
-                                subnetwork=subnet.name)
-                        .execute())
-        self._provider.wait_for_operation(response, region=subnet.region_name)
+        try:
+            response = (self.provider
+                            .gce_compute
+                            .subnetworks()
+                            .delete(project=self.provider.project_name,
+                                    region=subnet.region_name,
+                                    subnetwork=subnet.name)
+                            .execute())
+            self._provider.wait_for_operation(
+                response, region=subnet.region_name)
+        except googleapiclient.errors.HttpError as http_error:
+            cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
 
 
     def _zone_to_region_name(self, zone):
     def _zone_to_region_name(self, zone):
         if zone:
         if zone:
@@ -1225,6 +1252,10 @@ class GCSBucketService(BaseBucketService):
                             .execute())
                             .execute())
             if 'error' in response:
             if 'error' in response:
                 return None
                 return None
+            # GCS has a rate limit of 1 operation per 2 seconds for bucket
+            # creation/deletion: https://cloud.google.com/storage/quotas.
+            # Throttle here to avoid future failures.
+            time.sleep(2)
             return GCSBucket(self.provider, response)
             return GCSBucket(self.provider, response)
         except googleapiclient.errors.HttpError as http_error:
         except googleapiclient.errors.HttpError as http_error:
             cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
             cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)

+ 1 - 1
setup.py

@@ -34,7 +34,7 @@ REQS_AZURE = ['msrest>=0.4.7',
               'azure-mgmt-storage>=1.0.0rc1',
               'azure-mgmt-storage>=1.0.0rc1',
               'azure-storage>=0.34.0',
               'azure-storage>=0.34.0',
               'pysftp>=0.2.9']
               'pysftp>=0.2.9']
-REQS_GCP = ['google-api-python-client']
+REQS_GCP = ['google-api-python-client', 'oauth2client']
 REQS_OPENSTACK = [
 REQS_OPENSTACK = [
     'openstacksdk',
     'openstacksdk',
     'python-novaclient>=7.0.0',
     'python-novaclient>=7.0.0',

+ 1 - 1
test/test_block_store_service.py

@@ -116,7 +116,7 @@ class CloudBlockStoreServiceTestCase(ProviderTestBase):
                 self.assertEqual(test_vol.attachments.instance_id,
                 self.assertEqual(test_vol.attachments.instance_id,
                                  test_instance.id)
                                  test_instance.id)
                 if (self.provider.PROVIDER_ID != 'azure' and
                 if (self.provider.PROVIDER_ID != 'azure' and
-                    self.provider.PROVIDER_ID != 'gce'):
+                        self.provider.PROVIDER_ID != 'gce'):
                     self.assertEqual(test_vol.attachments.device,
                     self.assertEqual(test_vol.attachments.device,
                                      "/dev/sda2")
                                      "/dev/sda2")
                 test_vol.detach()
                 test_vol.detach()

+ 7 - 5
test/test_compute_service.py

@@ -350,13 +350,15 @@ class CloudComputeServiceTestCase(ProviderTestBase):
                 router.attach_subnet(subnet)
                 router.attach_subnet(subnet)
                 gateway = net.gateways.get_or_create_inet_gateway(name)
                 gateway = net.gateways.get_or_create_inet_gateway(name)
                 router.attach_gateway(gateway)
                 router.attach_gateway(gateway)
-                # check whether adding an elastic ip works
-                fip = gateway.floating_ips.create()
-                self.assertFalse(
-                    fip.in_use,
-                    "Newly created floating IP address should not be in use.")
+                fip = None
 
 
                 with helpers.cleanup_action(lambda: fip.delete()):
                 with helpers.cleanup_action(lambda: fip.delete()):
+                    # check whether adding an elastic ip works
+                    fip = gateway.floating_ips.create()
+                    self.assertFalse(
+                        fip.in_use,
+                        "Newly created floating IP should not be in use.")
+
                     with helpers.cleanup_action(
                     with helpers.cleanup_action(
                             lambda: test_inst.remove_floating_ip(fip)):
                             lambda: test_inst.remove_floating_ip(fip)):
                         test_inst.add_floating_ip(fip)
                         test_inst.add_floating_ip(fip)

+ 2 - 0
test/test_interface.py

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

+ 14 - 9
test/test_network_service.py

@@ -72,6 +72,10 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
                     " id %s." % (net.id, sn.network_id))
                     " id %s." % (net.id, sn.network_id))
 
 
                 self.assertEqual(
                 self.assertEqual(
+                    cidr, sn.cidr_block,
+                    "Should be exact cidr block that was requested")
+
+                self.assertTrue(
                     BaseNetwork.cidr_blocks_overlap(cidr, sn.cidr_block),
                     BaseNetwork.cidr_blocks_overlap(cidr, sn.cidr_block),
                     "Subnet's CIDR %s should overlap the specified one %s." % (
                     "Subnet's CIDR %s should overlap the specified one %s." % (
                         sn.cidr_block, cidr))
                         sn.cidr_block, cidr))
@@ -170,15 +174,16 @@ class CloudNetworkServiceTestCase(ProviderTestBase):
             # Check basic router properties
             # Check basic router properties
             sit.check_standard_behaviour(
             sit.check_standard_behaviour(
                 self, self.provider.networking.routers, router)
                 self, self.provider.networking.routers, router)
-            self.assertEqual(
-                router.state, RouterState.DETACHED,
-                "Router {0} state {1} should be {2}.".format(
-                    router.id, router.state, RouterState.DETACHED))
-
-#             self.assertFalse(
-#                 router.network_id,
-#                 "Router {0} should not be assoc. with a network {1}".format(
-#                     router.id, router.network_id))
+            if (self.provider.PROVIDER_ID != 'gce'):
+                self.assertEqual(
+                    router.state, RouterState.DETACHED,
+                    "Router {0} state {1} should be {2}.".format(
+                        router.id, router.state, RouterState.DETACHED))
+
+                self.assertFalse(
+                    router.network_id,
+                    "Router {0} should not be assoc. with network {1}".format(
+                            router.id, router.network_id))
 
 
             router.attach_subnet(sn)
             router.attach_subnet(sn)
             gteway = net.gateways.get_or_create_inet_gateway(name)
             gteway = net.gateways.get_or_create_inet_gateway(name)

+ 1 - 1
test/test_object_store_service.py

@@ -37,7 +37,7 @@ class CloudObjectStoreServiceTestCase(ProviderTestBase):
 
 
         with self.assertRaises(InvalidNameException):
         with self.assertRaises(InvalidNameException):
             # underscores are not allowed in bucket names
             # underscores are not allowed in bucket names
-            create_bucket("cb-bucket")
+            create_bucket("cb_bucket")
 
 
         with self.assertRaises(InvalidNameException):
         with self.assertRaises(InvalidNameException):
             # names of length less than 3 should raise an exception
             # names of length less than 3 should raise an exception

+ 0 - 10
test/test_security_service.py

@@ -183,16 +183,6 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             net, _ = helpers.create_test_network(self.provider, name)
             net, _ = helpers.create_test_network(self.provider, name)
             fw = self.provider.security.vm_firewalls.create(
             fw = self.provider.security.vm_firewalls.create(
                 name=name, description=name, network_id=net.id)
                 name=name, description=name, network_id=net.id)
-            rules = list(fw.rules)
-            self.assertTrue(
-                # TODO: This should be made consistent across all providers.
-                # Currently, OpenStack creates two rules, one for IPV6 and
-                # another for IPV4
-                len(rules) >= 1, "Expected a single VM firewall rule allowing"
-                " all outbound traffic. Got {0}.".format(rules))
-            self.assertEqual(
-                rules[0].direction, TrafficDirection.OUTBOUND,
-                "Expected rule to be outbound. Got {0}.".format(rules))
             rule = fw.rules.create(
             rule = fw.rules.create(
                 direction=TrafficDirection.INBOUND, src_dest_fw=fw,
                 direction=TrafficDirection.INBOUND, src_dest_fw=fw,
                 protocol='tcp', from_port=1, to_port=65535)
                 protocol='tcp', from_port=1, to_port=65535)