Bladeren bron

Merge pull request #27 from baizhang/gce

Add GCEInstanceService.
Nuwan Goonasekera 9 jaren geleden
bovenliggende
commit
09142c78be

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

@@ -5,6 +5,7 @@ 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 re
 import re
@@ -12,6 +13,7 @@ from string import Template
 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
 
 
@@ -205,6 +207,30 @@ class GCECloudProvider(BaseCloudProvider):
         else:
         else:
             return GoogleCredentials.get_application_default()
             return GoogleCredentials.get_application_default()
 
 
+    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):
+        while True:
+            result = self.gce_compute.globalOperations().get(
+                project=self.project_name,
+                operation=operation['name']).execute()
+
     def _connect_gcp_storage(self):
     def _connect_gcp_storage(self):
         return discovery.build('storage', 'v1', credentials=self._credentials)
         return discovery.build('storage', 'v1', credentials=self._credentials)
 
 

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

@@ -2,6 +2,7 @@
 DataTypes used by this provider
 DataTypes used by this provider
 """
 """
 from cloudbridge.cloud.base.resources import BaseFloatingIP
 from cloudbridge.cloud.base.resources import BaseFloatingIP
+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
@@ -10,6 +11,7 @@ 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
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 
 
 import cloudbridge as cb
 import cloudbridge as cb
@@ -72,6 +74,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'))
@@ -620,6 +626,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):
         """
         """
@@ -689,12 +699,256 @@ class GCEMachineImage(BaseMachineImage):
             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.STOPPED,
+        '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.
+
+        A GCE instance is uniquely identified by its selfLink, which is used
+        as its id.
+        """
+        return self._gce_instance.get('selfLink')
+
+    @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.
+        """
+        # In GCE, the name of the instance is provided by the client when
+        # initially creating the resource. The name cannot be changed after
+        # the instance is created.
+        cb.log.warning("Setting instance name after it is created is not "
+                       "supported by this provider.")
+
+    @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.STOPPED:
+            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() \
+                      .delete(project=self._provider.project_name,
+                              zone=self._provider.default_zone,
+                              instance=self.name) \
+                      .execute()
+
+    def stop(self):
+        """
+        Stop this instance.
+        """
+        self._provider.gce_compute \
+                      .instances() \
+                      .stop(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.
+        """
+        network_url = self._gce_instance.get('networkInterfaces')[0].get(
+            'network')
+        url = self._provider.parse_url(network_url)
+        network_name = url.parameters['network']
+        if 'items' not in self._gce_instance['tags']:
+            return []
+        tags = self._gce_instance['tags']['items']
+        # Tags are mapped to non-empty security groups under the instance
+        # network. Unmatched tags are ignored.
+        sgs = (self._provider.security
+               .security_groups.find_by_network_and_tags(
+                   network_name, tags))
+        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'))
+
 class GCENetwork(BaseNetwork):
 class GCENetwork(BaseNetwork):
 
 
     def __init__(self, provider, network):
     def __init__(self, provider, network):
         super(GCENetwork, self).__init__(provider)
         super(GCENetwork, self).__init__(provider)
         self._network = network
         self._network = network
 
 
+    @property
+    def resource_url(self):
+        return self._network['selfLink']
+
     @property
     @property
     def id(self):
     def id(self):
         return self._network['id']
         return self._network['id']

+ 126 - 3
cloudbridge/cloud/providers/gce/services.py

@@ -1,12 +1,15 @@
 from cloudbridge.cloud.base.resources import ClientPagedResultList
 from cloudbridge.cloud.base.resources import ClientPagedResultList
+from cloudbridge.cloud.base.resources import ServerPagedResultList
 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 BaseNetworkService
 from cloudbridge.cloud.base.services import BaseNetworkService
 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
 
 
@@ -21,10 +24,11 @@ import uuid
 
 
 from .resources import GCEFirewallsDelegate
 from .resources import GCEFirewallsDelegate
 from .resources import GCEFloatingIP
 from .resources import GCEFloatingIP
-from .resources import GCEMachineImage
-from .resources import GCENetwork
+from .resources import GCEInstance
 from .resources import GCEInstanceType
 from .resources import GCEInstanceType
 from .resources import GCEKeyPair
 from .resources import GCEKeyPair
+from .resources import GCEMachineImage
+from .resources import GCENetwork
 from .resources import GCERegion
 from .resources import GCERegion
 from .resources import GCESecurityGroup
 from .resources import GCESecurityGroup
 from .resources import GCESecurityGroupRule
 from .resources import GCESecurityGroupRule
@@ -241,6 +245,23 @@ class GCESecurityGroupService(BaseSecurityGroupService):
     def delete(self, group_id):
     def delete(self, group_id):
         return self._delegate.delete_tag_network_with_id(group_id)
         return self._delegate.delete_tag_network_with_id(group_id)
 
 
+    def find_by_network_and_tags(self, network_name, tags):
+        """
+        Finds non-empty security groups by network name and security group
+        names (tags). If no matching security group is found, an empty list
+        is returned.
+        """
+        security_groups = []
+        for tag, net_name in self._delegate.tag_networks:
+            if network_name != net_name:
+                continue
+            if tag not in tags:
+                continue
+            network = self.provider.network.get_by_name(net_name)
+            security_groups.append(
+                GCESecurityGroup(self._delegate, tag, network))
+        return security_groups
+
 
 
 class GCEInstanceTypesService(BaseInstanceTypesService):
 class GCEInstanceTypesService(BaseInstanceTypesService):
 
 
@@ -406,10 +427,112 @@ 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, network=None, zone=None,
+               key_pair=None, security_groups=None, user_data=None,
+               launch_config=None, **kwargs):
+        """
+        Creates a new virtual machine instance.
+        """
+        if not zone:
+            zone = self.provider.default_zone
+        if not launch_config:
+            if network:
+                network_url = (network.resource_url
+                               if isinstance(network, Network) else network)
+            else:
+                network_url = 'global/networks/default'
+            config = {
+                'name': name,
+                'machineType': instance_type.resource_url,
+                'disks': [{'boot': True,
+                           'autoDelete': True,
+                           'initializeParams': {
+                               'sourceImage': image.resource_url,
+                           }
+                       }],
+                'networkInterfaces': [
+                    {'network': network_url,
+                     'accessConfigs': [{'type': 'ONE_TO_ONE_NAT',
+                                        'name': 'External NAT'}]
+                 }],
+            }
+            if security_groups and isinstance(security_groups, list):
+                sg_names = []
+                if isinstance(security_groups[0], SecurityGroup):
+                    sg_names = [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_id):
+        """
+        Returns an instance given its name. Returns None
+        if the object does not exist.
+
+        A GCE instance is uniquely identified by its selfLink, which is used
+        as its id.
+        """
+        try:
+            response = self.provider.get_gce_resource_data(instance_id)
+            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 = [instance for instance in self.list()
+                     if instance.name == name]
+        if limit and len(instances) > limit:
+            instances = instances[:limit]
+        return instances
+
+    def list(self, limit=None, marker=None):
+        """
+        List all instances.
+        """
+        # For GCE API, Acceptable values are 0 to 500, inclusive.
+        # (Default: 500).
+        max_result = limit if limit is not None and limit < 500 else 500
+        response = self.provider.gce_compute.instances().list(
+            project=self.provider.project_name,
+            zone=self.provider.default_zone,
+            maxResults=max_result,
+            pageToken=marker).execute()
+        instances = [GCEInstance(self.provider, inst)
+                     for inst in response['items']]
+        return ServerPagedResultList(len(instances) > max_result,
+                                     response.get('nextPageToken'),
+                                     False, data=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)
@@ -424,7 +547,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):