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:
Pratik Shah 2017-08-01 15:21:47 +05:30
parent c9e0396752
commit 814542b626
5 changed files with 501 additions and 0 deletions

View File

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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