Added Azure support for Cinder
Files modified: - omni-requirements.txt: Added Azure dependencies Testing Done: - Create empty volume - Create volume from snapshot - Create volume from another volume - Create volume from image - Create snapshot from volume Implements: blueprint azure-support Change-Id: I095aeab7668233fe230ccd6dbc4f452c557437cd
This commit is contained in:
parent
c9e0396752
commit
814542b626
|
@ -0,0 +1,302 @@
|
|||
"""
|
||||
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 azure.common.credentials import ServicePrincipalCredentials
|
||||
from azure.mgmt.compute import ComputeManagementClient
|
||||
from azure.mgmt.compute.models import DiskCreateOption
|
||||
from azure.mgmt.resource import ResourceManagementClient
|
||||
from cinder import exception
|
||||
from functools import partial
|
||||
from oslo_log import log as logging
|
||||
|
||||
import six
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_credentials(tenant_id, client_id, client_secret):
|
||||
credentials = ServicePrincipalCredentials(
|
||||
client_id=client_id, secret=client_secret, tenant=tenant_id)
|
||||
return credentials
|
||||
|
||||
|
||||
def get_azure_client(tenant_id, client_id, client_secret, subscription_id,
|
||||
cls=None):
|
||||
"""Returns Azure compute resource object for interacting with Azure API
|
||||
|
||||
:param tenant_id: string, tenant_id from azure account
|
||||
:param client_id: string, client_id (application id)
|
||||
:param client_secret: string, secret key of application
|
||||
:param subscription_id: string, unique identification id of account
|
||||
:return: :class:`Resource <Resource>` object
|
||||
"""
|
||||
credentials = _get_credentials(tenant_id, client_id, client_secret)
|
||||
client = cls(credentials, subscription_id)
|
||||
return client
|
||||
|
||||
get_management_client = partial(get_azure_client, cls=ComputeManagementClient)
|
||||
get_resource_client = partial(get_azure_client, cls=ResourceManagementClient)
|
||||
|
||||
|
||||
def check_resource_existence(client, resource_group):
|
||||
"""Create if resource group exists in Azure or not
|
||||
|
||||
:param client: Azure object using ResourceManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:return: True if exists, otherwise False
|
||||
:rtype: boolean
|
||||
"""
|
||||
response = client.resource_groups.check_existence(resource_group)
|
||||
return response
|
||||
|
||||
|
||||
def get_disk(client, resource_group, disk_name):
|
||||
"""Get disk info from Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param disk_name: string, name of disk
|
||||
:return: class:`Resource <Resource>` object
|
||||
:rtype:
|
||||
class 'azure.mgmt.compute.compute.v2016_04_30_preview.models.disk.Disk'
|
||||
"""
|
||||
return client.disks.get(resource_group, disk_name)
|
||||
|
||||
|
||||
def get_snapshot(client, resource_group, snapshot_name):
|
||||
"""Get snapshot info from Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param snapshot_name: string, name of snapshot
|
||||
:return: class:`Resource <Resource>` object
|
||||
:rtype: class
|
||||
`azure.mgmt.compute.compute.v2016_04_30_preview.models.snapshot.Snapshot`
|
||||
"""
|
||||
return client.snapshots.get(resource_group, snapshot_name)
|
||||
|
||||
|
||||
def get_image(client, resource_group, image_name):
|
||||
return client.images.get(resource_group, image_name)
|
||||
|
||||
|
||||
def create_resource_group(client, resource_group, region):
|
||||
"""Create resource group in Azure
|
||||
|
||||
:param client: Azure object using ResourceManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param region: string, name of Azure region
|
||||
"""
|
||||
response = client.resource_groups.create_or_update(
|
||||
resource_group, {'location': region})
|
||||
LOG.debug("resource_group response: {0}".format(response))
|
||||
LOG.debug("Created Resource Group '{0}' in Azure".format(resource_group))
|
||||
|
||||
|
||||
def create_disk(client, resource_group, region, disk_name, size):
|
||||
"""Create disk in Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param region: string, name of Azure region
|
||||
:param disk_name: string, name of disk
|
||||
:param size: int, size of disk in Gb
|
||||
"""
|
||||
data = {
|
||||
'location': region,
|
||||
'disk_size_gb': size,
|
||||
'creation_data': {
|
||||
'create_option': DiskCreateOption.empty
|
||||
}
|
||||
}
|
||||
try:
|
||||
async_action = client.disks.create_or_update(
|
||||
resource_group, disk_name, data)
|
||||
LOG.debug("create_disk response: {0}".format(async_action.result()))
|
||||
LOG.debug('Created Disk: %s in Azure.' % disk_name)
|
||||
except Exception as e:
|
||||
message = "Create disk {0} in Azure failed. reason: {1}".format(
|
||||
disk_name, six.text_type(e))
|
||||
LOG.exception(message)
|
||||
raise exception.VolumeBackendAPIException(data=message)
|
||||
|
||||
|
||||
def create_disk_from_snapshot(client, resource_group, region, disk_name,
|
||||
snapshot_name):
|
||||
"""Create disk from snapshot in Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param region: string, name of Azure region
|
||||
:param disk_name: string, name of disk
|
||||
:param snapshot_name: string, name of snapshot
|
||||
"""
|
||||
try:
|
||||
snapshot_info = get_snapshot(client, resource_group, snapshot_name)
|
||||
data = {
|
||||
'location': region,
|
||||
'creation_data': {
|
||||
'create_option': DiskCreateOption.copy,
|
||||
'source_resource_id': snapshot_info.id
|
||||
}
|
||||
}
|
||||
async_action = client.disks.create_or_update(
|
||||
resource_group, disk_name, data)
|
||||
LOG.debug("create_disk_from_snapshot response: {0}".format(
|
||||
async_action.result()))
|
||||
LOG.debug("Created %s volume from %s snapshot" % (disk_name,
|
||||
snapshot_name))
|
||||
except Exception as e:
|
||||
message = "Create Volume from Snapshot {0} failed. reason: {1}"
|
||||
message = message.format(snapshot_name, six.text_type(e))
|
||||
LOG.exception(message)
|
||||
raise exception.VolumeBackendAPIException(data=message)
|
||||
|
||||
|
||||
def create_disk_from_disk(client, resource_group, region,
|
||||
src_vol, dest_vol):
|
||||
"""Create disk from disk in Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param region: string, name of Azure region
|
||||
:param src_vol: class:`cinder.objects.volume.Volume`, Source volume data
|
||||
:param dest_vol: class:`cinder.objects.volume.Volume`, data for volume to
|
||||
be created
|
||||
"""
|
||||
src_disk_details = get_disk(client, resource_group, 'vol-' + src_vol.id)
|
||||
data = {
|
||||
'location': region,
|
||||
'creation_data': {
|
||||
'create_option': DiskCreateOption.copy,
|
||||
'source_resource_id': src_disk_details.id
|
||||
}
|
||||
}
|
||||
try:
|
||||
async_action = client.disks.create_or_update(
|
||||
resource_group, 'vol-' + dest_vol.id, data)
|
||||
LOG.debug('create_disk_from_disk response: {0}'.format(
|
||||
async_action.result()))
|
||||
LOG.debug('Created Disk: {0} in Azure.'.format('vol-' + dest_vol.id))
|
||||
except Exception as e:
|
||||
message = "Create disk {0} in Azure failed. reason: {1}".format(
|
||||
'vol-' + dest_vol['id'], six.text_type(e))
|
||||
LOG.exception(message)
|
||||
raise exception.VolumeBackendAPIException(data=message)
|
||||
|
||||
|
||||
def create_disk_from_image(client, resource_group, region, image_meta, volume):
|
||||
"""Create disk from image in Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param region: string, name of Azure region
|
||||
:param image_meta: dict, image metadata
|
||||
:param volume: class:`cinder.objects.volume.Volume`, volume data
|
||||
"""
|
||||
resp = get_image(client, resource_group, image_meta['name'])
|
||||
volume_name = 'vol-' + volume.id
|
||||
data = {
|
||||
'location': region,
|
||||
'disk_size_gb': volume.size,
|
||||
'creation_data': {
|
||||
'create_option': DiskCreateOption.copy,
|
||||
'source_resource_id': resp.storage_profile.os_disk.managed_disk.id
|
||||
}
|
||||
}
|
||||
try:
|
||||
async_action = client.disks.create_or_update(
|
||||
resource_group, volume_name, data)
|
||||
result = async_action.result()
|
||||
LOG.debug('create_disk_from_image response: {0}'.format(result))
|
||||
LOG.debug('Created Disk: {0} in Azure.'.format(volume_name))
|
||||
return result
|
||||
except Exception as e:
|
||||
message = "Create disk {0} in Azure failed. reason: {1}".format(
|
||||
volume_name, six.text_type(e))
|
||||
LOG.exception(message)
|
||||
raise exception.VolumeBackendAPIException(data=message)
|
||||
|
||||
|
||||
def delete_disk(client, resource_group, disk_name):
|
||||
"""Delete disk in Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param disk_name: string, name of disk
|
||||
"""
|
||||
try:
|
||||
# checking if disk is available or not. If not available, then
|
||||
# get_disk() raises Exception
|
||||
_ = get_disk(client, resource_group, disk_name) # noqa
|
||||
async_action = client.disks.delete(resource_group, disk_name)
|
||||
LOG.debug("delete_disk response: {0}".format(async_action.result()))
|
||||
LOG.debug('Deleted Disk: %s from Azure.' % disk_name)
|
||||
except Exception as e:
|
||||
message = "Delete Disk {0} in Azure failed. reason: {1}".format(
|
||||
disk_name, six.text_type(e))
|
||||
LOG.exception(message)
|
||||
raise exception.VolumeBackendAPIException(data=message)
|
||||
|
||||
|
||||
def snapshot_disk(client, resource_group, region, disk_name,
|
||||
snapshot_name):
|
||||
"""Create snapshot of disk in Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param region: string, name of Azure region
|
||||
:param disk_name: string, name of disk
|
||||
:param snapshot_name: string, name of snapshot
|
||||
"""
|
||||
try:
|
||||
disk_info = get_disk(client, resource_group, disk_name)
|
||||
data = {
|
||||
'location': region,
|
||||
'creation_data': {
|
||||
'create_option': DiskCreateOption.copy,
|
||||
'source_uri': disk_info.id
|
||||
}
|
||||
}
|
||||
async_action = client.snapshots.create_or_update(
|
||||
resource_group, snapshot_name, data)
|
||||
LOG.debug("snapshot_disk response: {0}".format(async_action.result()))
|
||||
LOG.debug('Created Snapshot: %s in Azure.' % snapshot_name)
|
||||
except Exception as e:
|
||||
message = "Create Snapshot {0} in Azure failed. reason: {1}".format(
|
||||
snapshot_name, six.text_type(e))
|
||||
LOG.exception(message)
|
||||
raise exception.VolumeBackendAPIException(data=message)
|
||||
|
||||
|
||||
def delete_snapshot(client, resource_group, snapshot_name):
|
||||
"""Delete snapshot in Azure
|
||||
|
||||
:param client: Azure object using ComputeManagementClient
|
||||
:param resource_group: string, name of Azure resource group
|
||||
:param snapshot_name: string, name of snapshot
|
||||
"""
|
||||
try:
|
||||
# checking if snapshot is available or not. If not available, then
|
||||
# get_snapshot() raises Exception
|
||||
_ = get_snapshot(client, resource_group, snapshot_name) # noqa
|
||||
async_action = client.snapshots.delete(resource_group,
|
||||
snapshot_name)
|
||||
LOG.debug("delete_snapshot response: {0}".format(
|
||||
async_action.result()))
|
||||
LOG.debug('Deleted Snapshot: %s from Azure.' % snapshot_name)
|
||||
except Exception as e:
|
||||
message = "Delete Snapshot {0} from Azure failed. reason: {1}"
|
||||
message = message.format(snapshot_name, six.text_type(e))
|
||||
LOG.exception(message)
|
||||
raise exception.VolumeBackendAPIException(data=message)
|
|
@ -0,0 +1,46 @@
|
|||
"""
|
||||
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
|
||||
|
||||
azure_group = cfg.OptGroup(name='azure',
|
||||
title='Options to connect to Azure cloud')
|
||||
|
||||
azure_opts = [
|
||||
cfg.StrOpt('tenant_id', help='Tenant id of Azure account'),
|
||||
cfg.StrOpt('client_id', help='Azure client id'),
|
||||
cfg.StrOpt('client_secret', help='Azure client secret', secret=True),
|
||||
cfg.StrOpt('subscription_id', help='Azure subscription id'),
|
||||
cfg.StrOpt('region', help='Azure region'),
|
||||
cfg.StrOpt('resource_group', help="Azure resource group"),
|
||||
cfg.StrOpt('azure_pool_name', help='Azure pool name', default='azure'),
|
||||
cfg.IntOpt('azure_free_capacity_gb',
|
||||
help='Free space available on Azure storage pool',
|
||||
default=1024),
|
||||
cfg.IntOpt('azure_total_capacity_gb',
|
||||
help='Total space available on Azure storage pool',
|
||||
default=1024)
|
||||
]
|
||||
|
||||
cfg.CONF.register_group(azure_group)
|
||||
cfg.CONF.register_opts(azure_opts, group=azure_group)
|
||||
|
||||
tenant_id = cfg.CONF.azure.tenant_id
|
||||
client_id = cfg.CONF.azure.client_id
|
||||
client_secret = cfg.CONF.azure.client_secret
|
||||
subscription_id = cfg.CONF.azure.subscription_id
|
||||
region = cfg.CONF.azure.region
|
||||
resource_group = cfg.CONF.azure.resource_group
|
||||
azure_pool_name = cfg.CONF.azure.azure_pool_name
|
||||
azure_free_capacity_gb = cfg.CONF.azure.azure_free_capacity_gb
|
||||
azure_total_capacity_gb = cfg.CONF.azure.azure_total_capacity_gb
|
|
@ -0,0 +1,150 @@
|
|||
"""
|
||||
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 cinder.volume.driver import BaseVD
|
||||
from cinder.volume.drivers.azure import azureutils
|
||||
from cinder.volume.drivers.azure import config
|
||||
from oslo_log import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AzureDriver(BaseVD):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AzureDriver, self).__init__(*args, **kwargs)
|
||||
self.tenant_id = config.tenant_id
|
||||
self.client_id = config.client_id
|
||||
self.client_secret = config.client_secret
|
||||
self.subscription_id = config.subscription_id
|
||||
self.region = config.region
|
||||
self.resource_group = config.resource_group
|
||||
|
||||
def do_setup(self, context):
|
||||
args = (self.tenant_id, self.client_id, self.client_secret,
|
||||
self.subscription_id)
|
||||
self.management_client = azureutils.get_management_client(*args)
|
||||
self.resource_client = azureutils.get_resource_client(*args)
|
||||
is_resource_created = azureutils.check_resource_existence(
|
||||
self.resource_client, self.resource_group)
|
||||
if not is_resource_created:
|
||||
azureutils.create_resource_group(
|
||||
self.resource_client, self.resource_group, self.region)
|
||||
self.set_initialized()
|
||||
LOG.info("Azure volume driver init with %s tenant_id" %
|
||||
self.tenant_id)
|
||||
|
||||
def _azure_volume_name(self, volume):
|
||||
return 'vol-' + volume.id
|
||||
|
||||
def _azure_snapshot_name(self, snapshot):
|
||||
return 'snap-' + snapshot.id
|
||||
|
||||
def create_volume(self, volume):
|
||||
volume_name = self._azure_volume_name(volume)
|
||||
azureutils.create_disk(self.management_client, self.resource_group,
|
||||
self.region, volume_name, volume['size'])
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
volume_name = self._azure_volume_name(volume)
|
||||
snapshot_name = self._azure_snapshot_name(snapshot)
|
||||
azureutils.create_disk_from_snapshot(
|
||||
self.management_client, self.resource_group, self.region,
|
||||
volume_name, snapshot_name)
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
azureutils.create_disk_from_disk(
|
||||
self.management_client, self.resource_group,
|
||||
self.region, src_vref, volume)
|
||||
|
||||
def clone_image(self, context, volume, image_location, image_meta,
|
||||
image_service):
|
||||
response = azureutils.create_disk_from_image(
|
||||
self.management_client, self.resource_group, self.region,
|
||||
image_meta, volume)
|
||||
metadata = volume['metadata']
|
||||
metadata['new_volume_id'] = response.id
|
||||
return dict(metadata=metadata), True
|
||||
|
||||
def delete_volume(self, volume):
|
||||
volume_name = self._azure_volume_name(volume)
|
||||
azureutils.delete_disk(self.management_client, self.resource_group,
|
||||
volume_name)
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
volume_name = self._azure_volume_name(snapshot.volume)
|
||||
snapshot_name = self._azure_snapshot_name(snapshot)
|
||||
azureutils.snapshot_disk(self.management_client, self.resource_group,
|
||||
self.region, volume_name, snapshot_name)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
snapshot_name = self._azure_snapshot_name(snapshot)
|
||||
azureutils.delete_snapshot(
|
||||
self.management_client, self.resource_group, snapshot_name)
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
if refresh:
|
||||
data = dict()
|
||||
data['volume_backend_name'] = 'azure',
|
||||
data['vendor_name'] = 'Azure',
|
||||
data['driver_version'] = '0.0.1',
|
||||
data['storage_protocol'] = 'iscsi',
|
||||
pool = dict(pool_name=config.azure_pool_name,
|
||||
free_capacity_gb=config.azure_free_capacity_gb,
|
||||
total_capacity_gb=config.azure_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
|
||||
return self._stats
|
||||
|
||||
def check_for_setup_error(self):
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def create_export(self, context, volume, connector):
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def initialize_connection(self, volume, connector, **kwargs):
|
||||
volume_name = self._azure_volume_name(volume)
|
||||
azure_volume = azureutils.get_disk(
|
||||
self.management_client, self.resource_group, volume_name)
|
||||
volume_data = {'id': azure_volume.id,
|
||||
'name': azure_volume.name,
|
||||
'size': azure_volume.disk_size_gb}
|
||||
return dict(data=volume_data)
|
||||
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
pass
|
||||
|
||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
||||
raise NotImplementedError("Azure does not support this operation")
|
||||
|
||||
def copy_volume_to_image(self, context, volume, image_service, image_meta):
|
||||
raise NotImplementedError("Azure does not support this operation")
|
||||
|
||||
def migrate_volume(self, context, volume, host):
|
||||
raise NotImplementedError("Azure does not support this operation")
|
||||
|
||||
def copy_volume_data(self, context, src_vol, dest_vol, remote=None):
|
||||
"""Nothing need to do here since we create volume from volume in
|
||||
create_cloned_volume.
|
||||
"""
|
||||
pass
|
|
@ -3,3 +3,6 @@ moto>=1.0.1
|
|||
boto>=2.32.1 # MIT
|
||||
ipaddr
|
||||
google_compute_engine
|
||||
azure-mgmt-resource==1.1.0
|
||||
azure-mgmt-compute==1.0.0
|
||||
azure-mgmt-network==1.0.0
|
||||
|
|
Loading…
Reference in New Issue