Procházet zdrojové kódy

Merge pull request #27 from baizhang/gce

Add GCEInstanceService.
Nuwan Goonasekera před 9 roky
rodič
revize
09142c78be

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

@@ -5,6 +5,7 @@ for GCE.
 
 
 from cloudbridge.cloud.base import BaseCloudProvider
+import httplib2
 import json
 import os
 import re
@@ -12,6 +13,7 @@ from string import Template
 import time
 
 from googleapiclient import discovery
+import googleapiclient.http
 from oauth2client.client import GoogleCredentials
 from oauth2client.service_account import ServiceAccountCredentials
 
@@ -205,6 +207,30 @@ class GCECloudProvider(BaseCloudProvider):
         else:
             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):
         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
 """
 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 BaseKeyPair
 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 BaseSecurityGroup
 from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
+from cloudbridge.cloud.interfaces.resources import InstanceState
 from cloudbridge.cloud.interfaces.resources import MachineImageState
 
 import cloudbridge as cb
@@ -72,6 +74,10 @@ class GCEInstanceType(BaseInstanceType):
         super(GCEInstanceType, self).__init__(provider)
         self._inst_dict = instance_dict
 
+    @property
+    def resource_url(self):
+        return self._inst_dict.get('selfLink')
+
     @property
     def id(self):
         return str(self._inst_dict.get('id'))
@@ -620,6 +626,10 @@ class GCEMachineImage(BaseMachineImage):
         else:
             self._gce_image = image
 
+    @property
+    def resource_url(self):
+        return self._gce_image.get('selfLink')
+
     @property
     def id(self):
         """
@@ -689,12 +699,256 @@ class GCEMachineImage(BaseMachineImage):
             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):
 
     def __init__(self, provider, network):
         super(GCENetwork, self).__init__(provider)
         self._network = network
 
+    @property
+    def resource_url(self):
+        return self._network['selfLink']
+
     @property
     def id(self):
         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 ServerPagedResultList
 from cloudbridge.cloud.base.services import BaseComputeService
 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 BaseKeyPairService
 from cloudbridge.cloud.base.services import BaseNetworkService
 from cloudbridge.cloud.base.services import BaseRegionService
 from cloudbridge.cloud.base.services import BaseSecurityGroupService
 from cloudbridge.cloud.base.services import BaseSecurityService
+from cloudbridge.cloud.interfaces.resources import SecurityGroup
 from cloudbridge.cloud.providers.gce import helpers
 import cloudbridge as cb
 
@@ -21,10 +24,11 @@ import uuid
 
 from .resources import GCEFirewallsDelegate
 from .resources import GCEFloatingIP
-from .resources import GCEMachineImage
-from .resources import GCENetwork
+from .resources import GCEInstance
 from .resources import GCEInstanceType
 from .resources import GCEKeyPair
+from .resources import GCEMachineImage
+from .resources import GCENetwork
 from .resources import GCERegion
 from .resources import GCESecurityGroup
 from .resources import GCESecurityGroupRule
@@ -241,6 +245,23 @@ class GCESecurityGroupService(BaseSecurityGroupService):
     def delete(self, 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):
 
@@ -406,10 +427,112 @@ class GCEImageService(BaseImageService):
                                      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):
     # TODO: implement GCEComputeService
     def __init__(self, provider):
         super(GCEComputeService, self).__init__(provider)
+        self._instance_svc = GCEInstanceService(self.provider)
         self._instance_type_svc = GCEInstanceTypesService(self.provider)
         self._region_svc = GCERegionService(self.provider)
         self._images_svc = GCEImageService(self.provider)
@@ -424,7 +547,7 @@ class GCEComputeService(BaseComputeService):
 
     @property
     def instances(self):
-        raise NotImplementedError("To be implemented")
+        return self._instance_svc
 
     @property
     def regions(self):