Răsfoiți Sursa

Merge; removing httpretty from explicit setup reqs to bring it inline with the master branch

Enis Afgan 10 ani în urmă
părinte
comite
d7e625c8bb

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

@@ -1,15 +1,6 @@
 """
 Base implementation for data objects exposed through a provider or service
 """
-import inspect
-import itertools
-import json
-import logging
-import shutil
-import time
-
-import six
-
 from cloudbridge.cloud.interfaces.resources \
     import InvalidConfigurationException
 from cloudbridge.cloud.interfaces.resources import AttachmentInfo
@@ -38,6 +29,14 @@ from cloudbridge.cloud.interfaces.resources import Subnet
 from cloudbridge.cloud.interfaces.resources import Volume
 from cloudbridge.cloud.interfaces.resources import VolumeState
 from cloudbridge.cloud.interfaces.resources import WaitStateException
+import inspect
+import itertools
+import json
+import logging
+import shutil
+import time
+
+import six
 
 
 log = logging.getLogger(__name__)
@@ -465,7 +464,7 @@ class BaseKeyPair(KeyPair, BaseCloudResource):
         self._key_pair.delete()
 
     def __repr__(self):
-        return "<CBKeyPair: {0}>".format(self.name)
+        return "<CBKeyPair: {0} ({1})>".format(self.name, self.id)
 
 
 class BaseSecurityGroup(SecurityGroup, BaseCloudResource):

+ 4 - 4
cloudbridge/cloud/factory.py

@@ -1,13 +1,12 @@
+from cloudbridge.cloud import providers
+from cloudbridge.cloud.interfaces import CloudProvider
+from cloudbridge.cloud.interfaces import TestMockHelperMixin
 from collections import defaultdict
 import importlib
 import inspect
 import logging
 import pkgutil
 
-from cloudbridge.cloud import providers
-from cloudbridge.cloud.interfaces import CloudProvider
-from cloudbridge.cloud.interfaces import TestMockHelperMixin
-
 
 log = logging.getLogger(__name__)
 
@@ -15,6 +14,7 @@ log = logging.getLogger(__name__)
 class ProviderList(object):
     AWS = 'aws'
     OPENSTACK = 'openstack'
+    GCE = 'gce'
 
 
 class CloudProviderFactory(object):

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

@@ -960,10 +960,10 @@ class InstanceTypesService(PageableObjectMixin, CloudService):
     @abstractmethod
     def find(self, **kwargs):
         """
-        Searches for an instance by a given list of attributes.
+        Searches for instances by a given list of attributes.
 
-        :rtype: ``object`` of :class:`.InstanceType`
-        :return: an Instance object
+        :rtype: ``list`` of :class:`.InstanceType`
+        :return: list of InstanceType objects
         """
         pass
 

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

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

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

@@ -0,0 +1,9 @@
+import os
+from Crypto.PublicKey import RSA
+
+
+def generate_key_pair():
+    kp = RSA.generate(2048, os.urandom)
+    public_key = kp.publickey().exportKey("OpenSSH").split(" ")[1]
+    private_key = kp.exportKey("PEM")
+    return private_key, public_key

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

@@ -0,0 +1,92 @@
+"""
+Provider implementation based on google-api-python-client library
+for GCE.
+"""
+
+
+from cloudbridge.cloud.base import BaseCloudProvider
+import json
+import os
+import time
+
+from googleapiclient import discovery
+from oauth2client.client import GoogleCredentials
+from oauth2client.service_account import ServiceAccountCredentials
+
+from .services import GCEComputeService
+from .services import GCESecurityService
+
+
+class GCECloudProvider(BaseCloudProvider):
+
+    PROVIDER_ID = 'gce'
+
+    def __init__(self, config):
+        super(GCECloudProvider, self).__init__(config)
+
+        # Initialize cloud connection fields
+        self.client_email = self._get_config_value(
+            'gce_client_email', os.environ.get('GCE_CLIENT_EMAIL'))
+        self.project_name = self._get_config_value(
+            'gce_project_name', os.environ.get('GCE_PROJECT_NAME'))
+        self.credentials_file = self._get_config_value(
+            'gce_service_creds_file', os.environ.get('GCE_SERVICE_CREDS_FILE'))
+        self.default_zone = self._get_config_value(
+            'gce_default_zone', os.environ.get('GCE_DEFAULT_ZONE'))
+
+        # service connections, lazily initialized
+        self._gce_compute = None
+
+        # Initialize provider services
+        self._compute = GCEComputeService(self)
+        self._security = GCESecurityService(self)
+
+    @property
+    def compute(self):
+        return self._compute
+
+    @property
+    def network(self):
+        raise NotImplementedError(
+            "GCECloudProvider does not implement this service")
+
+    @property
+    def security(self):
+        return self._security
+
+    @property
+    def block_store(self):
+        raise NotImplementedError(
+            "GCECloudProvider does not implement this service")
+
+    @property
+    def object_store(self):
+        raise NotImplementedError(
+            "GCECloudProvider does not implement this service")
+
+    @property
+    def gce_compute(self):
+        if not self._gce_compute:
+            self._gce_compute = self._connect_gce_compute()
+        return self._gce_compute
+
+    def _connect_gce_compute(self):
+        if self.credentials_file:
+            credentials = ServiceAccountCredentials.from_json_keyfile_name(
+                self.credentials_file)
+        else:
+            credentials = GoogleCredentials.get_application_default()
+        return discovery.build('compute', 'v1', credentials=credentials)
+
+    def wait_for_global_operation(self, operation):
+        while True:
+            result = self.gce_compute.globalOperations().get(
+                project=self.project_name,
+                operation=operation['name']).execute()
+
+            if result['status'] == 'DONE':
+                if 'error' in result:
+                    raise Exception(result['error'])
+                return result
+
+            time.sleep(0.5)

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

@@ -0,0 +1,90 @@
+"""
+DataTypes used by this provider
+"""
+from cloudbridge.cloud.base.resources import BaseInstanceType
+from cloudbridge.cloud.base.resources import BaseKeyPair
+
+
+class GCEKeyPair(BaseKeyPair):
+
+    def __init__(self, provider, kp_id, kp_name, kp_material=None):
+        super(GCEKeyPair, self).__init__(provider, None)
+        self._kp_id = kp_id
+        self._kp_name = kp_name
+        self._kp_material = kp_material
+
+    @property
+    def id(self):
+        return self._kp_id
+
+    @property
+    def name(self):
+        # use e-mail as keyname if possible, or ID if not
+        return self._kp_name or self.id
+
+    def delete(self):
+        svc = self._provider.security.key_pairs
+
+        def _delete_key(gce_kp_generator):
+            kp_list = []
+            for gce_kp in gce_kp_generator:
+                if svc.gce_kp_to_id(gce_kp) == self.id:
+                    continue
+                else:
+                    kp_list.append(gce_kp)
+            return kp_list
+
+        svc.gce_metadata_save_op(_delete_key)
+
+    @property
+    def material(self):
+        return self._kp_material
+
+    @material.setter
+    def material(self, value):
+        self._kp_material = value
+
+
+class GCEInstanceType(BaseInstanceType):
+    def __init__(self, provider, instance_dict):
+        super(GCEInstanceType, self).__init__(provider)
+        self._inst_dict = instance_dict
+
+    @property
+    def id(self):
+        return str(self._inst_dict.get('id'))
+
+    @property
+    def name(self):
+        return self._inst_dict.get('name')
+
+    @property
+    def family(self):
+        return self._inst_dict.get('kind')
+
+    @property
+    def vcpus(self):
+        return self._inst_dict.get('guestCpus')
+
+    @property
+    def ram(self):
+        return self._inst_dict.get('memoryMb')
+
+    @property
+    def size_root_disk(self):
+        return 0
+
+    @property
+    def size_ephemeral_disks(self):
+        return int(self._inst_dict.get('maximumPersistentDisksSizeGb'))
+
+    @property
+    def num_ephemeral_disks(self):
+        return self._inst_dict.get('maximumPersistentDisks')
+
+    @property
+    def extra_data(self):
+        return {key: val for key, val in self._inst_dict.items()
+                if key not in ['id', 'name', 'kind', 'guestCpus', 'memoryMb',
+                               'maximumPersistentDisksSizeGb',
+                               'maximumPersistentDisks']}

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

@@ -0,0 +1,255 @@
+from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.services import BaseComputeService
+from cloudbridge.cloud.base.services import BaseInstanceTypesService
+from cloudbridge.cloud.base.services import BaseKeyPairService
+from cloudbridge.cloud.base.services import BaseSecurityGroupService
+from cloudbridge.cloud.base.services import BaseSecurityService
+from cloudbridge.cloud.providers.gce import helpers
+from collections import namedtuple
+import hashlib
+
+
+from retrying import retry
+
+from .resources import GCEInstanceType
+from .resources import GCEKeyPair
+
+
+class GCESecurityService(BaseSecurityService):
+
+    def __init__(self, provider):
+        super(GCESecurityService, self).__init__(provider)
+
+        # Initialize provider services
+        self._key_pairs = GCEKeyPairService(provider)
+
+    @property
+    def key_pairs(self):
+        return self._key_pairs
+
+    @property
+    def security_groups(self):
+        raise NotImplementedError(
+            "GCECloudProvider does not implement this service")
+
+
+class GCEKeyPairService(BaseKeyPairService):
+
+    GCEKeyInfo = namedtuple('GCEKeyInfo', 'format public_key email')
+
+    def __init__(self, provider):
+        super(GCEKeyPairService, self).__init__(provider)
+        self._gce_projects = None
+
+    @property
+    def gce_projects(self):
+        if not self._gce_projects:
+            self._gce_projects = self.provider.gce_compute.projects()
+        return self._gce_projects
+
+    def get(self, key_pair_id):
+        """
+        Returns a KeyPair given its ID.
+        """
+        for kp in self.list():
+            if kp.id == key_pair_id:
+                return kp
+        else:
+            return None
+
+    def _iter_gce_key_pairs(self):
+        """
+        Iterates through the project's metadata, yielding a GCEKeyInfo object
+        for each entry in commonInstanceMetaData/items
+        """
+        metadata = self._get_common_metadata()
+        for kpinfo in self._iter_gce_ssh_keys(metadata):
+            yield kpinfo
+
+    def _get_common_metadata(self):
+        """
+        Get a project's commonInstanceMetadata entry
+        """
+        metadata = self.gce_projects.get(
+            project=self.provider.project_name).execute()
+        return metadata["commonInstanceMetadata"]
+
+    def _get_or_add_sshkey_entry(self, metadata):
+        """
+        Get the sshKeys entry from commonInstanceMetadata/items.
+        If an entry does not exist, adds a new empty entry
+        """
+        sshkey_entry = None
+        entries = [item for item in metadata["items"]
+                   if item["key"] == "sshKeys"]
+        if entries:
+            sshkey_entry = entries[0]
+        else:  # add a new entry
+            sshkey_entry = {"key": "sshKeys", "value": ""}
+            metadata["items"].append(sshkey_entry)
+        return sshkey_entry
+
+    def _iter_gce_ssh_keys(self, metadata):
+        """
+        Iterates through the ssh keys given a commonInstanceMetadata dict,
+        yielding a GCEKeyInfo object for each entry in
+        commonInstanceMetaData/items
+        """
+        sshkeys = self._get_or_add_sshkey_entry(metadata)["value"]
+        for key in sshkeys.split("\n"):
+            # elems should be "ssh-rsa <public_key> <email>"
+            elems = key.split(" ")
+            if elems and elems[0]:  # ignore blank lines
+                yield GCEKeyPairService.GCEKeyInfo(elems[0], elems[1],
+                                                   elems[2])
+
+    def gce_metadata_save_op(self, callback):
+        """
+        Carries out a metadata save operation. In GCE, a fingerprint based
+        locking mechanism is used to prevent lost updates. A new fingerprint
+        is returned each time metadata is retrieved. Therefore, this method
+        retrieves the metadata, invokes the provided callback with that
+        metadata, and saves the metadata using the original fingerprint
+        immediately afterwards, ensuring that update conflicts can be detected.
+        """
+        def _save_common_metadata():
+            metadata = self._get_common_metadata()
+            # add a new entry if one doesn'te xist
+            sshkey_entry = self._get_or_add_sshkey_entry(metadata)
+            gce_kp_list = callback(self._iter_gce_ssh_keys(metadata))
+
+            entry = ""
+            for gce_kp in gce_kp_list:
+                entry = entry + u"{0} {1} {2}\n".format(gce_kp.format,
+                                                        gce_kp.public_key,
+                                                        gce_kp.email)
+            sshkey_entry["value"] = entry.rstrip()
+            # common_metadata will have the current fingerprint at this point
+            operation = self.gce_projects.setCommonInstanceMetadata(
+                project=self.provider.project_name, body=metadata).execute()
+            self.provider.wait_for_global_operation(operation)
+
+        # Retry a few times if the fingerprints conflict
+        retry_decorator = retry(stop_max_attempt_number=5)
+        retry_decorator(_save_common_metadata)()
+
+    def gce_kp_to_id(self, gce_kp):
+        """
+        Accept a GCEKeyInfo object and return a unique
+        ID for it
+        """
+        md5 = hashlib.md5()
+        md5.update(gce_kp.public_key)
+        return md5.hexdigest()
+
+    def list(self, limit=None, marker=None):
+        key_pairs = []
+        for gce_kp in self._iter_gce_key_pairs():
+            kp_id = self.gce_kp_to_id(gce_kp)
+            kp_name = gce_kp.email
+            key_pairs.append(GCEKeyPair(self.provider, kp_id, kp_name))
+        return ClientPagedResultList(self.provider, key_pairs,
+                                     limit=limit, marker=marker)
+
+    def find(self, name, limit=None, marker=None):
+        """
+        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)
+
+    def create(self, 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)
+
+        def _add_kp(gce_kp_generator):
+            kp_list = []
+            # Add the new key pair
+            kp_list.append(kp_info)
+            for gce_kp in gce_kp_generator:
+                kp_list.append(gce_kp)
+            return kp_list
+
+        self.gce_metadata_save_op(_add_kp)
+        return GCEKeyPair(self.provider, self.gce_kp_to_id(kp_info), name,
+                          kp_material=private_key)
+
+
+class GCESecurityGroupService(BaseSecurityGroupService):
+
+    def __init__(self, provider):
+        super(GCESecurityGroupService, self).__init__(provider)
+
+
+class GCEInstanceTypesService(BaseInstanceTypesService):
+
+    def __init__(self, provider):
+        super(GCEInstanceTypesService, self).__init__(provider)
+
+    @property
+    def instance_data(self):
+        response = self.provider.gce_compute \
+                                .machineTypes() \
+                                .list(project=self.provider.project_name,
+                                      zone=self.provider.default_zone) \
+                                .execute()
+        return response['items']
+
+    def get(self, instance_type_id):
+        for inst_type in self.instance_data:
+            if inst_type.get('id') == instance_type_id:
+                return GCEInstanceType(self.provider, inst_type)
+        return None
+
+    def find(self, **kwargs):
+        matched_inst_types = []
+        for inst_type in self.instance_data:
+            is_match = True
+            for key, value in kwargs.iteritems():
+                if key not in inst_type:
+                    raise TypeError("The attribute key is not valid.")
+                if inst_type.get(key) != value:
+                    is_match = False
+                    break
+            if is_match:
+                matched_inst_types.append(
+                    GCEInstanceType(self.provider, inst_type))
+        return matched_inst_types
+
+    def list(self, limit=None, marker=None):
+        inst_types = [GCEInstanceType(self.provider, inst_type)
+                      for inst_type in self.instance_data]
+        return ClientPagedResultList(self.provider, inst_types,
+                                     limit=limit, marker=marker)
+
+
+class GCEComputeService(BaseComputeService):
+    # TODO: implement GCEComputeService
+    def __init__(self, provider):
+        super(GCEComputeService, self).__init__(provider)
+        self._instance_type_svc = GCEInstanceTypesService(self.provider)
+
+    @property
+    def images(self):
+        raise NotImplementedError("To be implemented")
+
+    @property
+    def instance_types(self):
+        return self._instance_type_svc
+
+    @property
+    def instances(self):
+        raise NotImplementedError("To be implemented")
+
+    @property
+    def regions(self):
+        raise NotImplementedError("To be implemented")

+ 3 - 2
setup.py

@@ -21,8 +21,9 @@ openstack_reqs = ['python-novaclient>=2.33.0',
                   'python-neutronclient>=3.1.0',
                   'python-keystoneclient>=2.0.0']
 aws_reqs = ['boto>=2.38.0']
-full_reqs = base_reqs + aws_reqs + openstack_reqs
-dev_reqs = (['tox>=2.1.1', 'moto>=0.4.20', 'sphinx>=1.3.1'] + full_reqs)
+gce_reqs = ['google-api-python-client>=1.4.2', "pycrypto"]
+full_reqs = base_reqs + aws_reqs + openstack_reqs + gce_reqs
+dev_reqs = (['tox>=2.1.1', 'moto>=0.4.18', 'sphinx>=1.3.1'] + full_reqs)
 
 setup(name='cloudbridge',
       version=version,

+ 5 - 0
test/helpers.py

@@ -58,6 +58,9 @@ TEST_DATA_CONFIG = {
                                 'a471339a-bd0e-41e2-9406-4f308267ed0f'),
         "instance_type": os.environ.get('CB_INSTANCE_TYPE_OS', 'm1.tiny'),
         "placement": os.environ.get('CB_PLACEMENT_OS', 'nova'),
+    },
+    "GCECloudProvider": {
+        "instance_type": os.environ.get('CB_INSTANCE_TYPE_OS', 'f1-micro'),
     }
 }
 
@@ -67,6 +70,8 @@ def get_provider_test_data(provider, key):
         return TEST_DATA_CONFIG.get("AWSCloudProvider").get(key)
     elif "OpenStackCloudProvider" in provider.name:
         return TEST_DATA_CONFIG.get("OpenStackCloudProvider").get(key)
+    elif "GCECloudProvider" in provider.name:
+        return TEST_DATA_CONFIG.get("GCECloudProvider").get(key)
     return None
 
 

+ 19 - 20
test/test_security_service.py

@@ -1,7 +1,7 @@
 import json
+from test.helpers import ProviderTestBase
 import uuid
 
-from test.helpers import ProviderTestBase
 import test.helpers as helpers
 
 
@@ -20,7 +20,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         ):
             # test list method
             kpl = self.provider.security.key_pairs.list()
-            list_kpl = [i for i in kpl if i.name == name]
+            list_kpl = [i for i in kpl if i.id == kp.id]
             self.assertTrue(
                 len(list_kpl) == 1,
                 "List key pairs does not return the expected key pair %s" %
@@ -28,33 +28,35 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
 
             # check iteration
             iter_kpl = [i for i in self.provider.security.key_pairs
-                        if i.name == name]
+                        if i.id == kp.id]
             self.assertTrue(
                 len(iter_kpl) == 1,
                 "Iter key pairs does not return the expected key pair %s" %
                 name)
 
             # check find
-            find_kp = self.provider.security.key_pairs.find(name=name)[0]
+            find_kp = self.provider.security.key_pairs.find(name=kp.name)[0]
             self.assertTrue(
                 find_kp == kp,
                 "Find key pair did not return the expected key {0}."
                 .format(name))
 
             # check get
-            get_kp = self.provider.security.key_pairs.get(name)
+            get_kp = self.provider.security.key_pairs.get(kp.id)
             self.assertTrue(
                 get_kp == kp,
                 "Get key pair did not return the expected key {0}."
                 .format(name))
 
-            recreated_kp = self.provider.security.key_pairs.create(name=name)
-            self.assertTrue(
-                recreated_kp == kp,
-                "Recreating key pair did not return the expected key {0}."
-                .format(name))
+            # FIXME: This test doesn't work if the server generates the id
+            # and does not care about name uniqueness (e.g. azure)
+#             recreated_kp = self.provider.security.key_pairs.create(name=name)
+#             self.assertTrue(
+#                 recreated_kp == kp,
+#                 "Recreating key pair did not return the expected key {0}."
+#                 .format(name))
         kpl = self.provider.security.key_pairs.list()
-        found_kp = [k for k in kpl if k.name == name]
+        found_kp = [k for k in kpl if k.id == kp.id]
         self.assertTrue(
             len(found_kp) == 0,
             "Key pair {0} should have been deleted but still exists."
@@ -69,7 +71,7 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
         kp = self.provider.security.key_pairs.create(name=name)
         with helpers.cleanup_action(lambda: kp.delete()):
             kpl = self.provider.security.key_pairs.list()
-            found_kp = [k for k in kpl if k.name == name]
+            found_kp = [k for k in kpl if k.id == kp.id]
             self.assertTrue(
                 len(found_kp) == 1,
                 "List key pairs did not return the expected key {0}."
@@ -84,15 +86,12 @@ class CloudSecurityServiceTestCase(ProviderTestBase):
             self.assertTrue(
                 kp == kp,
                 "The same key pair should be equal to self.")
-            json_repr = json.dumps(
-                {"material": kp.material, "id": name, "name": name},
-                sort_keys=True)
-            self.assertEqual(
-                kp.to_json(), json_repr,
-                "JSON key pair representation {0} does not match expected {1}"
-                .format(kp.to_json(), json_repr))
+            # check json deserialization
+            self.assertTrue(json.loads(kp.to_json()),
+                            "to_json must yield a valid json string: {0}"
+                            .format(kp.to_json()))
         kpl = self.provider.security.key_pairs.list()
-        found_kp = [k for k in kpl if k.name == name]
+        found_kp = [k for k in kpl if k.id == kp.id]
         self.assertTrue(
             len(found_kp) == 0,
             "Key pair {0} should have been deleted but still exists."

+ 1 - 1
tox.ini

@@ -8,7 +8,7 @@ envlist = py27, py35, pypy
 
 [testenv]
 commands = {envpython} -m coverage run --branch --source=cloudbridge --omit=cloudbridge/cloud/interfaces/* setup.py test
-passenv = AWS_ACCESS_KEY AWS_SECRET_KEY OS_AUTH_URL OS_PASSWORD OS_TENANT_NAME OS_USERNAME OS_REGION_NAME NOVA_SERVICE_NAME CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS CB_IMAGE_OS CB_INSTANCE_TYPE_OS CB_PLACEMENT_OS CB_TEST_PROVIDER CB_USE_MOCK_PROVIDERS
+passenv = AWS_ACCESS_KEY AWS_SECRET_KEY GCE_CLIENT_EMAIL GCE_PROJECT_NAME GCE_DEFAULT_ZONE GCE_SERVICE_CREDS_FILE OS_AUTH_URL OS_PASSWORD OS_TENANT_NAME OS_USERNAME OS_REGION_NAME NOVA_SERVICE_NAME CB_IMAGE_AWS CB_INSTANCE_TYPE_AWS CB_PLACEMENT_AWS CB_IMAGE_OS CB_INSTANCE_TYPE_OS CB_PLACEMENT_OS CB_TEST_PROVIDER CB_USE_MOCK_PROVIDERS
 deps =
     -rrequirements.txt
     coverage