Browse Source

Add GCEInstanceService.

baizhang 9 years ago
parent
commit
107f2e83d5

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

@@ -5,11 +5,13 @@ for GCE.
 
 
 
 
 from cloudbridge.cloud.base import BaseCloudProvider
 from cloudbridge.cloud.base import BaseCloudProvider
+import httplib2
 import json
 import json
 import os
 import os
 import time
 import time
 
 
 from googleapiclient import discovery
 from googleapiclient import discovery
+import googleapiclient.http
 from oauth2client.client import GoogleCredentials
 from oauth2client.client import GoogleCredentials
 from oauth2client.service_account import ServiceAccountCredentials
 from oauth2client.service_account import ServiceAccountCredentials
 
 
@@ -44,6 +46,8 @@ class GCECloudProvider(BaseCloudProvider):
         self.region_name = self._get_config_value(
         self.region_name = self._get_config_value(
             'gce_region_name', 'us-central1')
             'gce_region_name', 'us-central1')
 
 
+        # oauth2client.Credentials to be used for authentication
+        self._credentials = None
         # service connections, lazily initialized
         # service connections, lazily initialized
         self._gce_compute = None
         self._gce_compute = None
 
 
@@ -86,8 +90,27 @@ class GCECloudProvider(BaseCloudProvider):
                 self.credentials_dict)
                 self.credentials_dict)
         else:
         else:
             credentials = GoogleCredentials.get_application_default()
             credentials = GoogleCredentials.get_application_default()
+        self._credentials = credentials
         return discovery.build('compute', 'v1', credentials=credentials)
         return discovery.build('compute', 'v1', credentials=credentials)
 
 
+    def get_gce_resource_data(self, uri):
+        """
+        Retrieves GCE resoure data given its resource URI.
+        """
+        http = httplib2.Http()
+        http = self._credentials.authorize(http)
+        def _postproc(*kwargs):
+            if len(kwargs) >= 2:
+                # The first argument is request, and the second is response.
+                resource_dict = json.loads(kwargs[1])
+                return resource_dict
+        request = googleapiclient.http.HttpRequest(http=http,
+                                                   postproc=_postproc,
+                                                   uri=uri)
+        # The response is a dict representing the GCE resource data.
+        response = request.execute()
+        return response
+
     def wait_for_global_operation(self, operation):
     def wait_for_global_operation(self, operation):
         while True:
         while True:
             result = self.gce_compute.globalOperations().get(
             result = self.gce_compute.globalOperations().get(

+ 251 - 1
cloudbridge/cloud/providers/gce/resources.py

@@ -1,6 +1,7 @@
 """
 """
 DataTypes used by this provider
 DataTypes used by this provider
 """
 """
+from cloudbridge.cloud.base.resources import BaseInstance
 from cloudbridge.cloud.base.resources import BaseInstanceType
 from cloudbridge.cloud.base.resources import BaseInstanceType
 from cloudbridge.cloud.base.resources import BaseKeyPair
 from cloudbridge.cloud.base.resources import BaseKeyPair
 from cloudbridge.cloud.base.resources import BaseMachineImage
 from cloudbridge.cloud.base.resources import BaseMachineImage
@@ -8,6 +9,8 @@ from cloudbridge.cloud.base.resources import BasePlacementZone
 from cloudbridge.cloud.base.resources import BaseRegion
 from cloudbridge.cloud.base.resources import BaseRegion
 from cloudbridge.cloud.base.resources import BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
+from cloudbridge.cloud.interfaces.resources import InstanceState
+from cloudbridge.cloud.interfaces.resources import MachineImageState
 
 
 # Older versions of Python do not have a built-in set data-structure.
 # Older versions of Python do not have a built-in set data-structure.
 try:
 try:
@@ -65,6 +68,10 @@ class GCEInstanceType(BaseInstanceType):
         super(GCEInstanceType, self).__init__(provider)
         super(GCEInstanceType, self).__init__(provider)
         self._inst_dict = instance_dict
         self._inst_dict = instance_dict
 
 
+    @property
+    def resource_url(self):
+        return self._inst_dict.get('selfLink')
+
     @property
     @property
     def id(self):
     def id(self):
         return str(self._inst_dict.get('id'))
         return str(self._inst_dict.get('id'))
@@ -614,6 +621,10 @@ class GCEMachineImage(BaseMachineImage):
         else:
         else:
             self._gce_image = image
             self._gce_image = image
 
 
+    @property
+    def resource_url(self):
+        return self._gce_image.get('selfLink')
+
     @property
     @property
     def id(self):
     def id(self):
         """
         """
@@ -680,4 +691,243 @@ class GCEMachineImage(BaseMachineImage):
             # image no longer exists
             # image no longer exists
             cb.log.warning(
             cb.log.warning(
                 "googleapiclient.errors.HttpError: {0}".format(http_error))
                 "googleapiclient.errors.HttpError: {0}".format(http_error))
-            self._gce_image['status'] = "unknown"
+            self._gce_image['status'] = "unknown"
+
+
+class GCEInstance(BaseInstance):
+    # https://cloud.google.com/compute/docs/reference/latest/instances
+    # The status of the instance. One of the following values:
+    # PROVISIONING, STAGING, RUNNING, STOPPING, SUSPENDING, SUSPENDED,
+    # and TERMINATED.
+    INSTANCE_STATE_MAP = {
+        'PROVISIONING': InstanceState.PENDING,
+        'STAGING': InstanceState.PENDING,
+        'RUNNING': InstanceState.RUNNING,
+        'STOPPING': InstanceState.CONFIGURING,
+        'TERMINATED': InstanceState.TERMINATED,
+        'SUSPENDING': InstanceState.CONFIGURING,
+        'SUSPENDED': InstanceState.STOPPED
+    }
+
+    def __init__(self, provider, gce_instance):
+        super(GCEInstance, self).__init__(provider)
+        self._gce_instance = gce_instance
+
+    @property
+    def resource_url(self):
+        return self._gce_instance.get('selfLink')
+
+    @property
+    def id(self):
+        """
+        Get the instance identifier.
+        """
+        return self._gce_instance['id']
+
+    @property
+    def name(self):
+        """
+        Get the instance name.
+        """
+        return self._gce_instance['name']
+
+    @name.setter
+    # pylint:disable=arguments-differ
+    def name(self, value):
+        """
+        Set the instance name.
+        """
+        response = self._provider.gce_compute \
+                                 .instances() \
+                                 .setTags(project=self._provider.project_name,
+                                          zone=self._provider.default_zone,
+                                          instance=self.name,
+                                          body={"items": [value]}) \
+                                 .execute()
+
+    @property
+    def public_ips(self):
+        """
+        Get all the public IP addresses for this instance.
+        """
+        network_interfaces = self._gce_instance.get('networkInterfaces')
+        if network_interfaces is None or len(network_interfaces) == 0:
+            return []
+        access_configs = network_interfaces[0].get('accessConfigs')
+        if access_configs is None or len(access_configs) == 0:
+            return []
+        # https://cloud.google.com/compute/docs/reference/beta/instances
+        # An array of configurations for this interface. Currently, only one
+        # access config, ONE_TO_ONE_NAT, is supported. If there are no
+        # accessConfigs specified, then this instance will have no external
+        # internet access.
+        access_config = access_configs[0]
+        if 'natIP' in access_config:
+            return [access_config['natIP']]
+        else:
+            return []
+
+    @property
+    def private_ips(self):
+        """
+        Get all the private IP addresses for this instance.
+        """
+        network_interfaces = self._gce_instance.get('networkInterfaces')
+        if network_interfaces is None or len(network_interfaces) == 0:
+            return []
+        if 'networkIP' in network_interfaces[0]:
+            return [network_interfaces[0]['networkIP']]
+        else:
+            return []
+
+    @property
+    def instance_type_id(self):
+        """
+        Get the instance type name.
+        """
+        machine_type_uri = self._gce_instance.get('machineType')
+        if machine_type_uri is None:
+            return None
+        instance_type = self._provider.get_gce_resource_data(machine_type_uri)
+        return instance_type.get('name', None)
+
+    @property
+    def instance_type(self):
+        """
+        Get the instance type.
+        """
+        machine_type_uri = self._gce_instance.get('machineType')
+        if machine_type_uri is None:
+            return None
+        instance_type = self._provider.get_gce_resource_data(machine_type_uri)
+        return GCEInstanceType(self._provider, instance_type)
+
+    def reboot(self):
+        """
+        Reboot this instance.
+        """
+        if self.state == InstanceState.TERMINATED:
+            self._provider.gce_compute \
+                          .instances() \
+                          .start(project=self._provider.project_name,
+                                 zone=self._provider.default_zone,
+                                 instance=self.name) \
+                          .execute()
+        else:
+            self._provider.gce_compute \
+                          .instances() \
+                          .reset(project=self._provider.project_name,
+                                 zone=self._provider.default_zone,
+                                 instance=self.name) \
+                          .execute()
+
+    def terminate(self):
+        """
+        Permanently terminate this instance.
+        """
+        self._provider.gce_compute \
+                      .instances() \
+                      .stop(project=self._provider.project_name,
+                            zone=self._provider.default_zone,
+                            instance=self.name) \
+                      .execute()
+
+    def delete(self):
+        """
+        Permanently delete this instance.
+        """
+        self._provider.gce_compute \
+                      .instances() \
+                      .delete(project=self._provider.project_name,
+                              zone=self._provider.default_zone,
+                              instance=self.name) \
+                      .execute()
+
+    @property
+    def image_id(self):
+        """
+        Get the image ID for this insance.
+        """
+        raise NotImplementedError(
+            'To be implemented after GCEVolumeService.')
+        return None
+
+    @property
+    def zone_id(self):
+        """
+        Get the placement zone id where this instance is running.
+        """
+        zone_uri = self._gce_instance.get('zone')
+        if zone_uri is None:
+            return None
+        zone = self._provider.get_gce_resource_data(zone_uri)
+        return zone.get('name', None)
+
+    @property
+    def security_groups(self):
+        """
+        Get the security groups associated with this instance.
+        """
+        sg_service = self._provider.security.security_groups
+        sg_names = [sg.name for sg in sg_service.list()]
+        tags = self._gce_instance['tags']['items']
+        if len(tags) == 0 or len(sg_names) == 0:
+            return []
+        common_tags = set(sg_names) & set(tags)
+        sgs = []
+        for tag in common_tags:
+            result = sg_service.find(tag)
+            if len(result) > 0:
+                sgs.append(result[0])
+        return sgs
+
+    @property
+    def security_group_ids(self):
+        """
+        Get the security groups IDs associated with this instance.
+        """
+        sg_ids = []
+        for sg in self.security_groups:
+            sg_ids.append(sg.id)
+        return sg_ids
+
+    @property
+    def key_pair_name(self):
+        """
+        Get the name of the key pair associated with this instance.
+        """
+        return self._provider.security.key_pairs.name
+
+    def create_image(self, name):
+        """
+        Create a new image based on this instance.
+        """
+        raise NotImplementedError(
+            'To be implemented after GCEVolumeService.')
+
+    def add_floating_ip(self, ip_address):
+        """
+        Add an elastic IP address to this instance.
+        """
+        raise NotImplementedError(
+            'To be implemented after GCENetworkService.')
+
+    def remove_floating_ip(self, ip_address):
+        """
+        Remove a elastic IP address from this instance.
+        """
+        raise NotImplementedError(
+            'To be implemented after GCENetworkService.')
+
+    @property
+    def state(self):
+        return GCEInstance.INSTANCE_STATE_MAP.get(
+            self._gce_instance['status'], InstanceState.UNKNOWN)
+
+    def refresh(self):
+        """
+        Refreshes the state of this instance by re-querying the cloud provider
+        for its latest state.
+        """
+        self._gce_instance = self._provider.get_gce_resource_data(
+            self._gce_instance.get('selfLink'))

+ 127 - 1
cloudbridge/cloud/providers/gce/services.py

@@ -1,11 +1,13 @@
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.services import BaseComputeService
 from cloudbridge.cloud.base.services import BaseComputeService
 from cloudbridge.cloud.base.services import BaseImageService
 from cloudbridge.cloud.base.services import BaseImageService
+from cloudbridge.cloud.base.services import BaseInstanceService
 from cloudbridge.cloud.base.services import BaseInstanceTypesService
 from cloudbridge.cloud.base.services import BaseInstanceTypesService
 from cloudbridge.cloud.base.services import BaseKeyPairService
 from cloudbridge.cloud.base.services import BaseKeyPairService
 from cloudbridge.cloud.base.services import BaseRegionService
 from cloudbridge.cloud.base.services import BaseRegionService
 from cloudbridge.cloud.base.services import BaseSecurityGroupService
 from cloudbridge.cloud.base.services import BaseSecurityGroupService
 from cloudbridge.cloud.base.services import BaseSecurityService
 from cloudbridge.cloud.base.services import BaseSecurityService
+from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.providers.gce import helpers
 from cloudbridge.cloud.providers.gce import helpers
 import cloudbridge as cb
 import cloudbridge as cb
 
 
@@ -17,6 +19,7 @@ from retrying import retry
 import sys
 import sys
 
 
 from .resources import GCEMachineImage
 from .resources import GCEMachineImage
+from .resources import GCEInstance
 from .resources import GCEInstanceType
 from .resources import GCEInstanceType
 from .resources import GCEKeyPair
 from .resources import GCEKeyPair
 from .resources import GCERegion
 from .resources import GCERegion
@@ -394,10 +397,133 @@ class GCEImageService(BaseImageService):
                                      limit=limit, marker=marker)
                                      limit=limit, marker=marker)
 
 
 
 
+class GCEInstanceService(BaseInstanceService):
+
+    def __init__(self, provider):
+        super(GCEInstanceService, self).__init__(provider)
+
+    def create(self, name, image, instance_type, zone=None,
+               key_pair=None, security_groups=None, user_data=None,
+               launch_config=None,
+               **kwargs):
+        """
+        Creates a new virtual machine instance.
+        """
+        if zone is None:
+            zone = self.provider.default_zone
+        if launch_config is None:
+            config = {
+                'name': name,
+                'machineType': instance_type.resource_url,
+                'disks': [{'boot': True,
+                           'autoDelete': False,
+                           'initializeParams': {
+                               'sourceImage': image.resource_url,
+                           }
+                       }],
+                'networkInterfaces': [
+                    {'network': 'global/networks/default',
+                     'accessConfigs': [{'type': 'ONE_TO_ONE_NAT',
+                                        'name': 'External NAT'}]
+                 }],
+            }
+            if (security_groups is not None and
+                isinstance(security_groups, list) and
+                len(security_groups) > 0):
+                sg_names = []
+                if isinstance(security_groups[0], SecurityGroup):
+                    sg_namess = [sg.name for sg in security_groups]
+                elif isinstance(security_groups[0], str):
+                    sg_names = security_groups
+                if len(sg_names) > 0:
+                    config['tags'] = {}
+                    config['tags']['items'] = sg_names
+        else:
+            config = launch_config
+        response = self.provider.gce_compute \
+                                .instances() \
+                                .insert(
+                                    project=self.provider.project_name,
+                                    zone=self.provider.default_zone,
+                                    body=config) \
+                                .execute()
+
+    def get(self, instance_name):
+        """
+        Returns an instance given its name. Returns None
+        if the object does not exist.
+
+        GCE client API only supports returning the specified Instance resource
+        by its name (not by its id).
+        """
+        try:
+            response = self.provider.gce_compute\
+                               .instances() \
+                               .get(project=self.provider.project_name,
+                                    zone=self.provider.default_zone,
+                                    instance=instance_name) \
+                               .execute()
+            if response:
+                return GCEInstance(self.provider, response)
+        except googleapiclient.errors.HttpError as http_error:
+            # If the instance is not found, the API will raise
+            # googleapiclient.errors.HttpError.
+            cb.log.warning(
+                "googleapiclient.errors.HttpError: {0}".format(http_error))
+        return None
+
+    def find(self, name, limit=None, marker=None):
+        """
+        Searches for instances by instance name.
+        :return: a list of Instance objects
+        """
+        instances = []
+        for instance in self.list():
+            if instance.name == name:
+                instances.append(instance)
+        if limit and len(instances) > limit:
+            instances = instances[:limit]
+        return instances
+
+    def list(self, limit=None, marker=None):
+        """
+        List all instances.
+        """
+        if limit is None or limit > 500:
+            # For GCE API, Acceptable values are 0 to 500, inclusive.
+            # (Default: 500). If the number of available results is larger
+            # than maxResults, Compute Engine returns a nextPageToken that
+            # can be used to get the next page of results in subsequent
+            # list requests.
+            max_result = 500
+        else:
+            max_result = limit
+        request = self.provider.gce_compute \
+                               .instances() \
+                               .list(project=self.provider.project_name,
+                                     zone=self.provider.default_zone,
+                                     maxResults=max_result)
+        instances = []
+        while request is not None:
+            response = request.execute()
+            instances.extend([GCEInstance(self.provider, inst)
+                              for inst in response['items']])
+            if limit and len(instances) >= limit:
+                break
+            request = self.provider.gce_compute \
+                                   .instances() \
+                                   .list_next(previous_request=request,
+                                              previous_response=response)
+        if limit and len(instances) > limit:
+            instances = instances[:limit]
+        return instances
+
+
 class GCEComputeService(BaseComputeService):
 class GCEComputeService(BaseComputeService):
     # TODO: implement GCEComputeService
     # TODO: implement GCEComputeService
     def __init__(self, provider):
     def __init__(self, provider):
         super(GCEComputeService, self).__init__(provider)
         super(GCEComputeService, self).__init__(provider)
+        self._instance_svc = GCEInstanceService(self.provider)
         self._instance_type_svc = GCEInstanceTypesService(self.provider)
         self._instance_type_svc = GCEInstanceTypesService(self.provider)
         self._region_svc = GCERegionService(self.provider)
         self._region_svc = GCERegionService(self.provider)
         self._images_svc = GCEImageService(self.provider)
         self._images_svc = GCEImageService(self.provider)
@@ -412,7 +538,7 @@ class GCEComputeService(BaseComputeService):
 
 
     @property
     @property
     def instances(self):
     def instances(self):
-        raise NotImplementedError("To be implemented")
+        return self._instance_svc
 
 
     @property
     @property
     def regions(self):
     def regions(self):