diff --git a/cinder/volume/drivers/azure/__init__.py b/cinder/volume/drivers/azure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cinder/volume/drivers/azure/azureutils.py b/cinder/volume/drivers/azure/azureutils.py new file mode 100644 index 0000000..5c32278 --- /dev/null +++ b/cinder/volume/drivers/azure/azureutils.py @@ -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 ` 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 ` 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 ` 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) diff --git a/cinder/volume/drivers/azure/config.py b/cinder/volume/drivers/azure/config.py new file mode 100644 index 0000000..a788358 --- /dev/null +++ b/cinder/volume/drivers/azure/config.py @@ -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 diff --git a/cinder/volume/drivers/azure/driver.py b/cinder/volume/drivers/azure/driver.py new file mode 100644 index 0000000..b059ea1 --- /dev/null +++ b/cinder/volume/drivers/azure/driver.py @@ -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 diff --git a/omni-requirements.txt b/omni-requirements.txt index 91bc26e..3abd60d 100644 --- a/omni-requirements.txt +++ b/omni-requirements.txt @@ -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