Add Cinder support for GCE

Supported operations:
1. Create/delete a volume
2. Snapshot a volume
3. Create a volume from snapshot
4. Attach/detach volume to an instance

Change-Id: I5b2a9ee964fc239153d6de9886cc5e4a8e743c17
Signed-off-by: Sanket <sanket@infracloud.io>
This commit is contained in:
Sanket Sudake 2017-05-04 05:16:54 +00:00 committed by Sanket
parent e4fd7a04f5
commit 1de5c3778c
7 changed files with 463 additions and 27 deletions

View File

@ -0,0 +1,2 @@
google-api-python-client
oslo.service>=1.19.0

View File

View File

@ -0,0 +1,135 @@
# Copyright 2017 Platform9 Systems Inc.(http://www.platform9.com)
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from cinder.i18n import _LI
from cinder.volume.driver import BaseVD
from cinder.volume.drivers.gce import gceconf
from cinder.volume.drivers.gce import gceutils
LOG = logging.getLogger(__name__)
class GceDriver(BaseVD):
def __init__(self, *args, **kwargs):
super(GceDriver, self).__init__(*args, **kwargs)
self.VERSION = '0.0.1'
self.gce_zone = gceconf.zone
self.gce_region = gceconf.region
self.gce_project = gceconf.project_id
self.gce_svc_key = gceconf.service_key_path
def do_setup(self, context):
self.gce_svc = gceutils.get_gce_service(self.gce_svc_key)
self.set_initialized()
LOG.info(
_LI("Gce volume driver init with %s project, %s zone") %
(self.gce_project, self.gce_zone))
def _gce_volume_name(self, volume):
return 'vol-' + volume.id
def _gce_snapshot_name(self, snapshot):
return 'snap-' + snapshot.id
def create_volume(self, volume):
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
name = self._gce_volume_name(volume)
size = volume['size']
operation = gceutils.create_disk(compute, project, zone, name, size)
gceutils.wait_for_operation(compute, project, operation)
def delete_volume(self, volume):
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
name = self._gce_volume_name(volume)
operation = gceutils.delete_disk(compute, project, zone, name)
gceutils.wait_for_operation(compute, project, operation)
def check_for_setup_error(self):
pass
def create_export(self, context, volume, connector):
pass
def ensure_export(self, context, volume):
pass
def remove_export(self, context, volume):
pass
def initialize_connection(self, volume, connector, initiator_data=None):
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
name = self._gce_volume_name(volume)
gce_volume = gceutils.get_disk(compute, project, zone, name)
return dict(data=gce_volume)
def terminate_connection(self, volume, connector, **kwargs):
pass
def _update_volume_stats(self):
data = dict()
data['volume_backend_name'] = 'gce'
data['vendor_name'] = 'Google, Inc.'
data['driver_version'] = '0.0.1'
data['storage_protocol'] = 'iscsi'
pool = dict(pool_name=gceconf.gce_pool_name,
free_capacity_gb=gceconf.gce_free_capacity_gb,
total_capacity_gb=gceconf.gce_free_capacity_gb,
provisioned_capacity_gb=0, reserved_percentage=0,
location_info=dict(), QoS_support=False,
max_over_subscription_ratio=1.0,
thin_provisioning_support=False,
thick_provisioning_support=True, total_volumes=0)
data['pools'] = [pool]
self._stats = data
def get_volume_stats(self, refresh=False):
if refresh:
self._update_volume_stats()
return self._stats
def create_snapshot(self, snapshot):
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
volume_name = self._gce_volume_name(snapshot.volume)
name = self._gce_snapshot_name(snapshot)
operation = gceutils.snapshot_disk(compute, project, zone, volume_name,
name)
gceutils.wait_for_operation(compute, project, operation)
def delete_snapshot(self, snapshot):
compute, project = self.gce_svc, self.gce_project
name = self._gce_snapshot_name(snapshot)
operation = gceutils.delete_snapshot(compute, project, name)
gceutils.wait_for_operation(compute, project, operation)
def create_volume_from_snapshot(self, volume, snapshot):
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
name = self._gce_volume_name(volume)
snapshot_name = self._gce_snapshot_name(snapshot)
operation = gceutils.create_disk_from_snapshot(compute, project, zone,
name, snapshot_name)
gceutils.wait_for_operation(compute, project, operation)
def copy_image_to_volume(self, context, volume, image_service, image_id):
raise NotImplementedError()
def copy_volume_to_image(self, context, volume, image_service, image_meta):
raise NotImplementedError()
def migrate_volume(self, context, volume, host):
raise NotImplementedError()
def copy_volume_data(self, context, src_vol, dest_vol, remote=None):
raise NotImplementedError()

View File

@ -0,0 +1,43 @@
# Copyright 2017 Platform9 Systems Inc.(http://www.platform9.com)
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
gce_group = cfg.OptGroup(name='GCE',
title='Options to connect to Google cloud')
gce_opts = [
cfg.StrOpt('service_key_path', help='Service key of GCE account',
secret=True),
cfg.StrOpt('zone', help='GCE zone'),
cfg.StrOpt('region', help='GCE region'),
cfg.StrOpt('project_id', help='GCE project id'),
cfg.StrOpt('gce_pool_name', help='Storage pool name'),
cfg.IntOpt('gce_free_capacity_gb',
help='Free space available on GCE storage pool', default=1024),
cfg.IntOpt('gce_total_capacity_gb',
help='Total space available on GCE storage pool', default=1024)
] # yapf:disable
cfg.CONF.register_group(gce_group)
cfg.CONF.register_opts(gce_opts, group=gce_group)
service_key_path = cfg.CONF.GCE.service_key_path
zone = cfg.CONF.GCE.zone
region = cfg.CONF.GCE.region
project_id = cfg.CONF.GCE.project_id
gce_pool_name = cfg.CONF.GCE.gce_pool_name
gce_free_capacity_gb = cfg.CONF.GCE.gce_free_capacity_gb
gce_total_capacity_gb = cfg.CONF.GCE.gce_total_capacity_gb

View File

@ -0,0 +1,180 @@
# Copyright (c) 2017 Platform9 Systems Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from googleapiclient.discovery import build
from oauth2client.client import GoogleCredentials
from oslo_log import log as logging
from cinder.i18n import _LI
from oslo_service import loopingcall
LOG = logging.getLogger(__name__)
class GceOperationError(Exception):
pass
def wait_for_operation(compute, project, operation, interval=1, timeout=60):
"""Wait for GCE operation to complete, raise error if operation failure
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param operation: object, Operation resource obtained by calling GCE API
:param interval: int, Time period(seconds) between two GCE operation checks
:param timeout: int, Absoulte time period(seconds) to monitor GCE operation
"""
def watch_operation(name, request):
result = request.execute()
if result['status'] == 'DONE':
LOG.info(
_LI("Operation %s status is %s") % (name, result['status']))
if 'error' in result:
raise GceOperationError(result['error'])
raise loopingcall.LoopingCallDone()
operation_name = operation['name']
if 'zone' in operation:
zone = operation['zone'].split('/')[-1]
monitor_request = compute.zoneOperations().get(
project=project, zone=zone, operation=operation_name)
elif 'region' in operation:
region = operation['region'].split('/')[-1]
monitor_request = compute.regionOperations().get(
project=project, region=region, operation=operation_name)
else:
monitor_request = compute.globalOperations().get(
project=project, operation=operation_name)
timer = loopingcall.FixedIntervalWithTimeoutLoopingCall(
watch_operation, operation_name, monitor_request)
timer.start(interval=interval, timeout=timeout).wait()
def get_gce_service(service_key):
"""Returns GCE compute resource object for interacting with GCE API
:param service_key: string, Path of service key obtained from
https://console.cloud.google.com/apis/credentials
:return: :class:`Resource <Resource>` object
:rtype: googleapiclient.discovery.Resource
"""
credentials = GoogleCredentials.from_stream(service_key)
service = build('compute', 'v1', credentials=credentials)
return service
def create_disk(compute, project, zone, name, size):
"""Create disk in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:param size: int, size of disk inn Gb
:return: Operation information
:rtype: dict
"""
body = {
"name": name,
"zone": "projects/%s/zones/%s" % (project, zone),
"type": "projects/%s/zones/%s/diskTypes/pd-standard" % (project, zone),
"sizeGb": size
}
return compute.disks().insert(project=project, zone=zone, body=body,
sourceImage=None).execute()
def delete_disk(compute, project, zone, name):
"""Delete disk in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:return: Operation information
:rtype: dict
"""
return compute.disks().delete(project=project, zone=zone,
disk=name).execute()
def get_disk(compute, project, zone, name):
"""Get info of disk in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:return: GCE disk information
:rtype: dict
"""
return compute.disks().get(project=project, zone=zone, disk=name).execute()
def snapshot_disk(compute, project, zone, name, snapshot_name):
"""Create snapshot of disk in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:param snapshot_name: string, GCE snapshot name
:return: Operation information
:rtype: dict
"""
body = {"name": snapshot_name}
return compute.disks().createSnapshot(project=project, zone=zone,
disk=name, body=body).execute()
def get_snapshot(compute, project, name):
"""Get info of snapshot in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param name: string, GCE snapshot name
:return: GCE snapshot information
:rtype: dict
"""
return compute.snapshots().get(project=project, snapshot=name).execute()
def delete_snapshot(compute, project, name):
"""Delete snapshot in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param name: string, GCE snapshot name
:return: Operation information
:rtype: dict
"""
return compute.snapshots().delete(project=project, snapshot=name).execute()
def create_disk_from_snapshot(compute, project, zone, name, snapshot_name):
"""Create disk from snapshot in GCE
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param name: string, GCE disk name
:param snapshot_name: string, GCE snapshot name
:return: Operation information
:rtype: dict
"""
gce_snapshot = get_snapshot(compute, project, snapshot_name)
body = {
"name": name,
"zone": "projects/%s/zones/%s" % (project, zone),
"type": "projects/%s/zones/%s/diskTypes/pd-standard" % (project, zone),
"sourceSnapshot": gce_snapshot["selfLink"],
"sizeGb": gce_snapshot["diskSizeGb"]
}
return compute.disks().insert(project=project, zone=zone, body=body,
sourceImage=None).execute()

View File

@ -18,6 +18,7 @@ import uuid
import nova.conf
from nova import exception
from nova.i18n import _LI
from nova.virt import driver, hardware
from oslo_config import cfg
from oslo_log import log as logging
@ -115,8 +116,9 @@ class GCEDriver(driver.ComputeDriver):
self.gce_svc = gceutils.get_gce_service(self.gce_svc_key)
self.gce_flavor_info = gceutils.get_machines_info(
self.gce_svc, self.gce_project, self.gce_zone)
LOG.info("GCE driver init with %s project, %s region" %
(self.gce_project, self.gce_zone))
LOG.info(
_LI("GCE driver init with %s project, %s region") %
(self.gce_project, self.gce_zone))
if '_GCE_NODES' not in globals():
set_nodes([CONF.host])
@ -192,6 +194,15 @@ class GCEDriver(driver.ComputeDriver):
return network_interfaces
def _process_ssh_keys(self, instance):
key_name, key_data = instance.key_name, instance.key_data
if key_name is None or key_data is None:
return {}
return {
'key': 'ssh-keys',
'value': '{0}:{1}'.format(key_name, key_data)
}
def spawn(self, context, instance, image_meta, injected_files,
admin_password, network_info=None, block_device_info=None):
"""Create a new instance/VM/domain on the virtualization platform.
@ -218,7 +229,8 @@ class GCEDriver(driver.ComputeDriver):
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
# TODO: Use instance id as instance name
instance_name = instance.display_name
LOG.info("Creating instance %s as %s on GCE." % (instance.display_name,
LOG.info(
_LI("Creating instance %s as %s on GCE.") % (instance.display_name,
instance_name))
# Image Info
image_link = instance.system_metadata['image_gce_link']
@ -236,13 +248,18 @@ class GCEDriver(driver.ComputeDriver):
instance_name)
# Update GCE info in openstack instance metadata
instance.metadata.update({'gce_id': gce_instance['name']})
gce_metadata = [
{
'key': 'openstack_id',
'value': instance.uuid
},
]
ssh_keys = self._process_ssh_keys(instance)
if ssh_keys:
gce_metadata.append(ssh_keys)
operation = gceutils.set_instance_metadata(
compute, project, zone, gce_instance['name'], [
{
'key': 'openstack_id',
'value': instance.uuid
},
], operation='add')
compute, project, zone, gce_instance['name'], gce_metadata,
operation='add')
gceutils.wait_for_operation(compute, project, zone, operation)
self._uuid_to_gce_instance[instance.uuid] = gceutils.get_instance(
compute, project, zone, instance_name)
@ -284,22 +301,22 @@ class GCEDriver(driver.ComputeDriver):
block_device_info=None):
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
gce_id = self._get_gce_id_from_instance(instance)
LOG.info('Stopping instance %s' % instance.uuid)
LOG.info(_LI('Stopping instance %s') % instance.uuid)
operation = gceutils.stop_instance(compute, project, zone, gce_id)
gceutils.wait_for_operation(compute, project, zone, operation)
LOG.info('Starting instance %s' % instance.uuid)
LOG.info(_LI('Starting instance %s') % instance.uuid)
operation = gceutils.start_instance(compute, project, zone, gce_id)
gceutils.wait_for_operation(compute, project, zone, operation)
LOG.info('Soft Reboot Complete for instance %s' % instance.uuid)
LOG.info(_LI('Soft Reboot Complete for instance %s') % instance.uuid)
def _hard_reboot(self, context, instance, network_info,
block_device_info=None):
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
gce_id = self._get_gce_id_from_instance(instance)
LOG.info('Resetting instance %s' % instance.uuid)
LOG.info(_LI('Resetting instance %s') % instance.uuid)
operation = gceutils.reset_instance(compute, project, zone, gce_id)
gceutils.wait_for_operation(compute, project, zone, operation)
LOG.info('Hard Reboot Complete %s' % instance.uuid)
LOG.info(_LI('Hard Reboot Complete %s') % instance.uuid)
@staticmethod
def get_host_ip_addr():
@ -351,23 +368,23 @@ class GCEDriver(driver.ComputeDriver):
"""
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
gce_id = self._get_gce_id_from_instance(instance)
LOG.info('Stopping instance %s' % instance.uuid)
LOG.info(_LI('Stopping instance %s') % instance.uuid)
operation = gceutils.stop_instance(compute, project, zone, gce_id)
gceutils.wait_for_operation(compute, project, zone, operation)
LOG.info('Power off complete %s' % instance.uuid)
LOG.info(_LI('Power off complete %s') % instance.uuid)
def power_on(self, context, instance, network_info, block_device_info):
"""Power on the specified instance."""
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
gce_id = self._get_gce_id_from_instance(instance)
LOG.info('Starting instance %s' % instance.uuid)
LOG.info(_LI('Starting instance %s') % instance.uuid)
operation = gceutils.start_instance(compute, project, zone, gce_id)
gceutils.wait_for_operation(compute, project, zone, operation)
LOG.info("Power on Complete %s" % instance.uuid)
LOG.info(_LI("Power on Complete %s") % instance.uuid)
def soft_delete(self, instance):
"""Deleting the specified instance"""
LOG.info("Soft delete instance %s" % instance.uuid)
LOG.info(_LI("Soft delete instance %s") % instance.uuid)
self.destroy(instance)
def restore(self, instance):
@ -380,7 +397,7 @@ class GCEDriver(driver.ComputeDriver):
instance.
:param instance: nova.objects.instance.Instance
"""
LOG.info("Pause instance %s" % instance.uuid)
LOG.info(_LI("Pause instance %s") % instance.uuid)
self.power_off(instance)
def unpause(self, instance):
@ -390,7 +407,7 @@ class GCEDriver(driver.ComputeDriver):
instance. and powering on such an instance in this method.
:param instance: nova.objects.instance.Instance
"""
LOG.info("Unpause instance %s" % instance.uuid)
LOG.info(_LI("Unpause instance %s") % instance.uuid)
self.power_on(context=None, instance=instance, network_info=None,
block_device_info=None)
@ -401,7 +418,7 @@ class GCEDriver(driver.ComputeDriver):
instance.
:param instance: nova.objects.instance.Instance
"""
LOG.info("Suspending instance %s" % instance.uuid)
LOG.info(_LI("Suspending instance %s") % instance.uuid)
self.power_off(instance)
def resume(self, context, instance, network_info, block_device_info=None):
@ -411,7 +428,7 @@ class GCEDriver(driver.ComputeDriver):
instance.
:param instance: nova.objects.instance.Instance
"""
LOG.info("Resuming instance %s" % instance.uuid)
LOG.info(_LI("Resuming instance %s") % instance.uuid)
self.power_on(context, instance, network_info, block_device_info)
def destroy(self, context, instance, network_info, block_device_info=None,
@ -433,22 +450,41 @@ class GCEDriver(driver.ComputeDriver):
"""
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
gce_id = self._get_gce_id_from_instance(instance)
LOG.info('Deleting instance %s' % instance.uuid)
LOG.info(_LI('Deleting instance %s') % instance.uuid)
operation = gceutils.delete_instance(compute, project, zone, gce_id)
gceutils.wait_for_operation(compute, project, zone, operation)
LOG.info("Destroy Complete %s" % instance.uuid)
LOG.info(_LI("Destroy Complete %s") % instance.uuid)
def attach_volume(self, context, connection_info, instance, mountpoint,
disk_bus=None, device_type=None, encryption=None):
"""Attach the disk to the instance at mountpoint using info.
"""
raise NotImplementedError()
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
gce_id = self._get_gce_id_from_instance(instance)
gce_volume = connection_info['data']
disk_name = gce_volume['name'],
disk_link = gce_volume['selfLink']
operation = gceutils.attach_disk(compute, project, zone, gce_id,
disk_name, disk_link)
gceutils.wait_for_operation(compute, project, zone, operation)
LOG.info(
_LI("Volume %s attached to instace %s") % (disk_name,
instance.uuid))
def detach_volume(self, connection_info, instance, mountpoint,
encryption=None):
"""Detach the disk attached to the instance.
"""
raise NotImplementedError()
compute, project, zone = self.gce_svc, self.gce_project, self.gce_zone
gce_id = self._get_gce_id_from_instance(instance)
gce_volume = connection_info['data']
disk_name = gce_volume['name']
operation = gceutils.detach_disk(compute, project, zone, gce_id,
disk_name)
gceutils.wait_for_operation(compute, project, zone, operation)
LOG.info(
_LI("Volume %s detached from instace %s") % (disk_name,
instance.uuid))
def swap_volume(self, old_connection_info, new_connection_info, instance,
mountpoint, resize_to):

View File

@ -297,3 +297,43 @@ def get_network(compute, project, name):
"""
result = compute.networks().get(project=project, network=name).execute()
return result
def attach_disk(compute, project, zone, instance_name, disk_name, disk_link):
"""Attach disk to instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance_name: string, GCE instance name
:param disk_name: string, GCE disk name
:param disk_link: url, GCE disk link
:return: Operation information
:rtype: dict
"""
body = {
"type": "PERSISTENT",
"mode": "READ_WRITE",
"source": disk_link,
"deviceName": disk_name,
"boot": False,
"autoDelete": False,
"interface": "SCSI"
}
return compute.instances().attachDisk(project=project, zone=zone,
instance=instance_name,
body=body).execute()
def detach_disk(compute, project, zone, instance_name, disk_name):
"""Detach disk from instance
:param compute: GCE compute resource object using googleapiclient.discovery
:param project: string, GCE Project Id
:param zone: string, GCE Name of zone
:param instance_name: string, GCE instance name
:param disk_name: string, GCE disk name
:return: Operation information
:rtype: dict
"""
return compute.instances().detachDisk(project=project, zone=zone,
instance=instance_name,
deviceName=disk_name).execute()