From: Abhilash Divakaran Date: Wed, 18 Nov 2015 07:54:52 +0000 (-0800) Subject: Add volume driver for Tegile IntelliFlash array X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=951a757ec0ace2148367c0e137396286ad3ca7b2;p=openstack-build%2Fcinder-build.git Add volume driver for Tegile IntelliFlash array The Tegile driver will support the iSCSI and FC protocols and it will include the minimum set of features. [Supported Protocol] - iSCSI, FC [Supported Feature] - Volume Create/Delete - Volume Attach/Detach - Snapshot Create/Delete - Create Volume from Snapshot - Get Volume Stats - Copy Image to Volume, - Copy Volume to Image - Clone Volume - Extend Volume Tegile has setup a CI. It will report as "Tegile Storage CI". DocImpact Implements: blueprint tegile-volume-driver Change-Id: Ia0e6c320964f3955d6c7d4dcff4a0241a3960495 --- diff --git a/cinder/exception.py b/cinder/exception.py index 9f412f050..ebcee0f9b 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -1035,3 +1035,8 @@ class HNASConnError(CinderException): # Coho drivers class CohoException(VolumeDriverException): message = _("Coho Data Cinder driver failure: %(message)s") + + +# Tegile Storage drivers +class TegileAPIException(VolumeBackendAPIException): + message = _("Unexpected response from Tegile IntelliFlash API") diff --git a/cinder/opts.py b/cinder/opts.py index ad20f46e7..456e00aa3 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -145,6 +145,7 @@ from cinder.volume.drivers import scality as cinder_volume_drivers_scality from cinder.volume.drivers import sheepdog as cinder_volume_drivers_sheepdog from cinder.volume.drivers import smbfs as cinder_volume_drivers_smbfs from cinder.volume.drivers import solidfire as cinder_volume_drivers_solidfire +from cinder.volume.drivers import tegile as cinder_volume_drivers_tegile from cinder.volume.drivers import tintri as cinder_volume_drivers_tintri from cinder.volume.drivers.violin import v6000_common as \ cinder_volume_drivers_violin_v6000common @@ -315,6 +316,7 @@ def list_opts(): nexenta_edge_opts, cinder_volume_drivers_ibm_flashsystemiscsi. flashsystem_iscsi_opts, + cinder_volume_drivers_tegile.tegile_opts, cinder_volume_drivers_ibm_flashsystemcommon.flashsystem_opts, [cinder_volume_api.allow_force_upload_opt], [cinder_volume_api.volume_host_opt], diff --git a/cinder/tests/unit/test_tegile.py b/cinder/tests/unit/test_tegile.py new file mode 100644 index 000000000..1f1a198dd --- /dev/null +++ b/cinder/tests/unit/test_tegile.py @@ -0,0 +1,410 @@ +# Copyright (c) 2015 by Tegile Systems, Inc. +# All Rights Reserved. +# +# 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. +""" +Volume driver Test for Tegile storage. +""" + +import mock + +from cinder import context +from cinder.exception import TegileAPIException +from cinder import test +from cinder.volume.drivers import tegile + +BASE_DRIVER = tegile.TegileIntelliFlashVolumeDriver +ISCSI_DRIVER = tegile.TegileISCSIDriver +FC_DRIVER = tegile.TegileFCDriver + +test_config = mock.Mock() +test_config.san_ip = 'some-ip' +test_config.san_login = 'some-user' +test_config.san_password = 'some-password' +test_config.san_is_local = True +test_config.tegile_default_pool = 'random-pool' +test_config.tegile_default_project = 'random-project' +test_config.volume_backend_name = "unittest" + +test_volume = {'host': 'node#testPool', + 'name': 'testvol', + 'id': 'a24c2ee8-525a-4406-8ccd-8d38688f8e9e', + '_name_id': 'testvol', + 'metadata': {'project': 'testProj'}, + 'provider_location': None, + 'size': 10} + +test_snapshot = {'name': 'testSnap', + 'id': '07ae9978-5445-405e-8881-28f2adfee732', + 'volume': {'host': 'node#testPool', + 'size': '1', + '_name_id': 'testvol' + } + } + +array_stats = {'total_capacity_gb': 4569.199686084874, + 'free_capacity_gb': 4565.381390112452, + 'pools': [{'total_capacity_gb': 913.5, + 'QoS_support': False, + 'free_capacity_gb': 911.812650680542, + 'reserved_percentage': 0, + 'pool_name': 'pyramid' + }, + {'total_capacity_gb': 2742.1996604874, + 'QoS_support': False, + 'free_capacity_gb': 2740.148867149747, + 'reserved_percentage': 0, + 'pool_name': 'cobalt' + }, + {'total_capacity_gb': 913.5, + 'QoS_support': False, + 'free_capacity_gb': 913.4198722839355, + 'reserved_percentage': 0, + 'pool_name': 'test' + }] + } + + +class FakeTegileService(object): + @staticmethod + def send_api_request(method, params=None, + request_type='post', + api_service='v2', + fine_logging=False): + if method is 'createVolume': + return '' + elif method is 'deleteVolume': + return '' + elif method is 'createVolumeSnapshot': + return '' + elif method is 'deleteVolumeSnapshot': + return '' + elif method is 'cloneVolumeSnapshot': + return '' + elif method is 'listPools': + return '' + elif method is 'resizeVolume': + return '' + elif method is 'getVolumeSizeinGB': + return 25 + elif method is 'getISCSIMappingForVolume': + return {'target_lun': '27', + 'target_iqn': 'iqn.2012-02.com.tegile:openstack-cobalt', + 'target_portal': '10.68.103.106:3260' + } + elif method is 'getFCPortsForVolume': + return {'target_lun': '12', + 'initiator_target_map': + '{"21000024ff59bb6e":["21000024ff578701",],' + '"21000024ff59bb6f":["21000024ff578700",],}', + 'target_wwn': '["21000024ff578700","21000024ff578701",]'} + elif method is 'getArrayStats': + return array_stats + + +fake_tegile_backend = FakeTegileService() + + +class FakeTegileServiceFail(object): + @staticmethod + def send_api_request(method, params=None, + request_type='post', + api_service='v2', + fine_logging=False): + raise TegileAPIException + + +fake_tegile_backend_fail = FakeTegileServiceFail() + + +class TegileIntelliFlashVolumeDriverTestCase(test.TestCase): + def setUp(self): + self.ctxt = context.get_admin_context() + self.configuration = test_config + super(TegileIntelliFlashVolumeDriverTestCase, self).setUp() + + def test_create_volume(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual({ + 'metadata': {'pool': 'testPool', + 'project': test_config.tegile_default_project + } + }, tegile_driver.create_volume(test_volume)) + + def test_create_volume_fail(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend_fail): + self.assertRaises(TegileAPIException, + tegile_driver.create_volume, + test_volume) + + def test_delete_volume(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + tegile_driver.delete_volume(test_volume) + + def test_delete_volume_fail(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend_fail): + self.assertRaises(TegileAPIException, + tegile_driver.delete_volume, + test_volume) + + def test_create_snapshot(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + tegile_driver.create_snapshot(test_snapshot) + + def test_create_snapshot_fail(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend_fail): + self.assertRaises(TegileAPIException, + tegile_driver.create_snapshot, + test_snapshot) + + def test_delete_snapshot(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + tegile_driver.delete_snapshot(test_snapshot) + + def test_delete_snapshot_fail(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend_fail): + self.assertRaises(TegileAPIException, + tegile_driver.delete_snapshot, + test_snapshot) + + def test_create_volume_from_snapshot(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual({ + 'metadata': {'pool': 'testPool', + 'project': test_config.tegile_default_project + } + }, tegile_driver.create_volume_from_snapshot(test_volume, + test_snapshot)) + + def test_create_volume_from_snapshot_fail(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend_fail): + self.assertRaises(TegileAPIException, + tegile_driver.create_volume_from_snapshot, + test_volume, test_snapshot) + + def test_create_cloned_volume(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual({'metadata': {'project': 'testProj', + 'pool': 'testPool'}}, + tegile_driver.create_cloned_volume(test_volume, + test_volume)) + + def test_create_cloned_volume_fail(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend_fail): + self.assertRaises(TegileAPIException, + tegile_driver.create_cloned_volume, + test_volume, test_volume) + + def test_get_volume_stats(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual({'driver_version': '1.0.0', + 'free_capacity_gb': 4565.381390112452, + 'pools': [{'QoS_support': False, + 'allocated_capacity_gb': 0.0, + 'free_capacity_gb': 911.812650680542, + 'pool_name': 'pyramid', + 'reserved_percentage': 0, + 'total_capacity_gb': 913.5}, + {'QoS_support': False, + 'allocated_capacity_gb': 0.0, + 'free_capacity_gb': 2740.148867149747, + 'pool_name': 'cobalt', + 'reserved_percentage': 0, + 'total_capacity_gb': 2742.1996604874}, + {'QoS_support': False, + 'allocated_capacity_gb': 0.0, + 'free_capacity_gb': 913.4198722839355, + 'pool_name': 'test', + 'reserved_percentage': 0, + 'total_capacity_gb': 913.5}], + 'storage_protocol': 'iSCSI', + 'total_capacity_gb': 4569.199686084874, + 'vendor_name': 'Tegile Systems Inc.', + 'volume_backend_name': 'unittest'}, + tegile_driver.get_volume_stats(True)) + + def test_get_pool(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual('testPool', tegile_driver.get_pool(test_volume)) + + def test_extend_volume(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + tegile_driver.extend_volume(test_volume, 12) + + def test_extend_volume_fail(self): + tegile_driver = self.get_object(self.configuration) + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend_fail): + self.assertRaises(TegileAPIException, + tegile_driver.extend_volume, + test_volume, 30) + + def test_manage_existing(self): + tegile_driver = self.get_object(self.configuration) + existing_ref = {'name': 'existingvol'} + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual({'metadata': {'pool': 'testPool', + 'project': 'testProj' + }, + '_name_id': ('existingvol',) + }, tegile_driver.manage_existing(test_volume, + existing_ref)) + + def test_manage_existing_get_size(self): + tegile_driver = self.get_object(self.configuration) + existing_ref = {'name': 'existingvol'} + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual(25, + tegile_driver.manage_existing_get_size( + test_volume, + existing_ref)) + + def test_manage_existing_get_size_fail(self): + tegile_driver = self.get_object(self.configuration) + existing_ref = {'name': 'existingvol'} + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend_fail): + self.assertRaises(TegileAPIException, + tegile_driver.manage_existing_get_size, + test_volume, existing_ref) + + def get_object(self, configuration): + class TegileBaseDriver(BASE_DRIVER): + def initialize_connection(self, volume, connector, **kwargs): + pass + + def terminate_connection(self, volume, connector, + force=False, **kwargs): + pass + + return TegileBaseDriver(configuration=self.configuration) + + +class TegileISCSIDriverTestCase(test.TestCase): + def setUp(self): + super(TegileISCSIDriverTestCase, self).setUp() + self.ctxt = context.get_admin_context() + self.configuration = test_config + self.configuration.chap_username = 'fake' + self.configuration.chap_password = "test" + + def test_initialize_connection(self): + tegile_driver = self.get_object(self.configuration) + connector = {'initiator': 'iqn.1993-08.org.debian:01:d0bb9a834f8'} + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual( + {'data': {'access_mode': 'rw', + 'auth_method': 'CHAP', + 'discard': False, + 'target_discovered': (False,), + 'auth_password': 'test', + 'auth_username': 'fake', + 'target_iqn': 'iqn.2012-02.' + 'com.tegile:openstack-cobalt', + 'target_lun': '27', + 'target_portal': '10.68.103.106:3260', + 'volume_id': ( + 'a24c2ee8-525a-4406-8ccd-8d38688f8e9e',)}, + 'driver_volume_type': 'iscsi'}, + tegile_driver.initialize_connection(test_volume, + connector)) + + def get_object(self, configuration): + return ISCSI_DRIVER(configuration=configuration) + + +class TegileFCDriverTestCase(test.TestCase): + def setUp(self): + super(TegileFCDriverTestCase, self).setUp() + self.ctxt = context.get_admin_context() + self.configuration = test_config + + def test_initialize_connection(self): + tegile_driver = self.get_object(self.configuration) + connector = {'wwpns': ['500110a0001a3990']} + with mock.patch.object(tegile_driver, + '_api_executor', + fake_tegile_backend): + self.assertEqual({'data': {'access_mode': 'rw', + 'encrypted': False, + 'initiator_target_map': { + '21000024ff59bb6e': + ['21000024ff578701'], + '21000024ff59bb6f': + ['21000024ff578700'] + }, + 'target_discovered': False, + 'target_lun': '12', + 'target_wwn': + ['21000024ff578700', + '21000024ff578701']}, + 'driver_volume_type': 'fibre_channel'}, + tegile_driver.initialize_connection( + test_volume, + connector)) + + def get_object(self, configuration): + return FC_DRIVER(configuration=configuration) diff --git a/cinder/volume/drivers/tegile.py b/cinder/volume/drivers/tegile.py new file mode 100644 index 000000000..08a352a23 --- /dev/null +++ b/cinder/volume/drivers/tegile.py @@ -0,0 +1,661 @@ +# Copyright (c) 2015 by Tegile Systems, Inc. +# All Rights Reserved. +# +# 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. +""" +Volume driver for Tegile storage. +""" + +import ast +import json +import requests + +from oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import units +import six + +from cinder import exception +from cinder import utils +from cinder.i18n import _, _LI, _LW +from cinder.volume import driver +from cinder.volume.drivers.san import san +from cinder.volume import utils as volume_utils +from cinder.zonemanager import utils as fczm_utils + +LOG = logging.getLogger(__name__) +default_api_service = 'openstack' +TEGILE_API_PATH = 'zebi/api' +TEGILE_DEFAULT_BLOCK_SIZE = '32KB' +TEGILE_LOCAL_CONTAINER_NAME = 'Local' +DEBUG_LOGGING = False + +tegile_opts = [ + cfg.StrOpt('tegile_default_pool', + help='Create volumes in this pool'), + cfg.StrOpt('tegile_default_project', + help='Create volumes in this project')] + +CONF = cfg.CONF +CONF.register_opts(tegile_opts) + + +def debugger(func): + """Returns a wrapper that wraps func. + + The wrapper will log the entry and exit points of the function + """ + + def wrapper(*args, **kwds): + if DEBUG_LOGGING: + LOG.debug('Entering %(classname)s.%(funcname)s', + {'classname': args[0].__class__.__name__, + 'funcname': func.__name__}) + LOG.debug('Arguments: %(args)s, %(kwds)s', + {'args': args[1:], + 'kwds': kwds}) + f_result = func(*args, **kwds) + if DEBUG_LOGGING: + LOG.debug('Exiting %(classname)s.%(funcname)s', + {'classname': args[0].__class__.__name__, + 'funcname': func.__name__}) + LOG.debug('Results: %(result)s', + {'result': f_result}) + return f_result + + return wrapper + + +class TegileAPIExecutor(object): + def __init__(self, classname, hostname, username, password): + self._classname = classname + self._hostname = hostname + self._username = username + self._password = password + + @debugger + @utils.retry(exceptions=(requests.ConnectionError, requests.Timeout)) + def send_api_request(self, method, params=None, + request_type='post', + api_service=default_api_service, + fine_logging=DEBUG_LOGGING): + if params is not None: + params = json.dumps(params) + + url = 'https://%s/%s/%s/%s' % (self._hostname, + TEGILE_API_PATH, + api_service, + method) + if fine_logging: + LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, ' + 'url: %(url)s', {'classname': self._classname, + 'method': method, + 'url': url}) + if request_type == 'post': + if fine_logging: + LOG.debug('TegileAPIExecutor(%(classname)s) ' + 'method: %(method)s, payload: %(payload)s', + {'classname': self._classname, + 'method': method, + 'payload': params}) + req = requests.post(url, + data=params, + auth=(self._username, self._password), + verify=False) + else: + req = requests.get(url, + auth=(self._username, self._password), + verify=False) + + if fine_logging: + LOG.debug('TegileAPIExecutor(%(classname)s) method: %(method)s, ' + 'return code: %(retcode)s', + {'classname': self._classname, + 'method': method, + 'retcode': req}) + try: + response = req.json() + if fine_logging: + LOG.debug('TegileAPIExecutor(%(classname)s) ' + 'method: %(method)s, response: %(response)s', + {'classname': self._classname, + 'method': method, + 'response': response}) + except ValueError: + response = '' + req.close() + + if req.status_code != 200: + msg = _('API response: %(response)s') % {'response': response} + raise exception.TegileAPIException(msg) + + return response + + +class TegileIntelliFlashVolumeDriver(san.SanDriver): + """Tegile IntelliFlash Volume Driver.""" + + VENDOR = 'Tegile Systems Inc.' + VERSION = '1.0.0' + REQUIRED_OPTIONS = ['san_ip', 'san_login', + 'san_password', 'tegile_default_pool'] + SNAPSHOT_PREFIX = 'Manual-V-' + + _api_executor = None + + def __init__(self, *args, **kwargs): + self._context = None + super(TegileIntelliFlashVolumeDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(tegile_opts) + self._protocol = 'iSCSI' # defaults to iscsi + hostname = getattr(self.configuration, 'san_ip') + username = getattr(self.configuration, 'san_login') + password = getattr(self.configuration, 'san_password') + self._default_pool = getattr(self.configuration, 'tegile_default_pool') + self._default_project = ( + getattr(self.configuration, 'tegile_default_project') or + 'openstack') + self._api_executor = TegileAPIExecutor(self.__class__.__name__, + hostname, + username, + password) + + @debugger + def do_setup(self, context): + super(TegileIntelliFlashVolumeDriver, self).do_setup(context) + self._context = context + self._check_ops(self.REQUIRED_OPTIONS, self.configuration) + + @debugger + def create_volume(self, volume): + pool = volume_utils.extract_host(volume['host'], level='pool', + default_pool_name=self._default_pool) + tegile_volume = {'blockSize': TEGILE_DEFAULT_BLOCK_SIZE, + 'datasetPath': '%s/%s/%s' % + (pool, + TEGILE_LOCAL_CONTAINER_NAME, + self._default_project), + 'local': 'true', + 'name': volume['name'], + 'poolName': '%s' % pool, + 'projectName': '%s' % self._default_project, + 'protocol': self._protocol, + 'thinProvision': 'true', + 'volSize': volume['size'] * units.Gi} + params = list() + params.append(tegile_volume) + params.append(True) + + self._api_executor.send_api_request(method='createVolume', + params=params) + + LOG.info(_LI("Created volume %(volname)s, volume id %(volid)s."), + {'volname': volume['name'], 'volid': volume['id']}) + + return self.get_additional_info(volume, pool, self._default_project) + + @debugger + def delete_volume(self, volume): + """Deletes a snapshot.""" + params = list() + pool, project, volume_name = self._get_pool_project_volume_name(volume) + params.append('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + volume_name)) + params.append(True) + params.append(False) + + self._api_executor.send_api_request('deleteVolume', params) + + @debugger + def create_snapshot(self, snapshot): + """Creates a snapshot.""" + snap_name = snapshot['name'] + display_list = [getattr(snapshot, 'display_name', ''), + getattr(snapshot, 'display_description', '')] + snap_description = ':'.join(filter(None, display_list)) + # Limit to 254 characters + snap_description = snap_description[:254] + + pool, project, volume_name = self._get_pool_project_volume_name( + snapshot['volume']) + + volume = {'blockSize': TEGILE_DEFAULT_BLOCK_SIZE, + 'datasetPath': '%s/%s/%s' % + (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project), + 'local': 'true', + 'name': volume_name, + 'poolName': '%s' % pool, + 'projectName': '%s' % project, + 'protocol': self._protocol, + 'thinProvision': 'true', + 'volSize': snapshot['volume']['size'] * units.Gi} + params = list() + params.append(volume) + params.append(snap_name) + params.append(False) + + LOG.info(_LI('Creating snapshot for volume_name=%(vol)s' + ' snap_name=%(name)s snap_description=%(desc)s'), + {'vol': volume_name, + 'name': snap_name, + 'desc': snap_description}) + + self._api_executor.send_api_request('createVolumeSnapshot', params) + + @debugger + def delete_snapshot(self, snapshot): + """Deletes a snapshot.""" + params = list() + pool, project, volume_name = self._get_pool_project_volume_name( + snapshot['volume']) + params.append('%s/%s/%s/%s@%s%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + volume_name, + self.SNAPSHOT_PREFIX, + snapshot['name'])) + params.append(False) + + self._api_executor.send_api_request('deleteVolumeSnapshot', params) + + @debugger + def create_volume_from_snapshot(self, volume, snapshot): + """Creates a volume from snapshot.""" + params = list() + pool, project, volume_name = self._get_pool_project_volume_name( + snapshot['volume']) + + params.append('%s/%s/%s/%s@%s%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + volume_name, + self.SNAPSHOT_PREFIX, + snapshot['name'])) + params.append(volume['name']) + params.append(True) + params.append(True) + + self._api_executor.send_api_request('cloneVolumeSnapshot', params) + return self.get_additional_info(volume, pool, project) + + @debugger + def create_cloned_volume(self, volume, src_vref): + """Creates a clone of the specified volume.""" + pool, project, volume_name = self._get_pool_project_volume_name( + src_vref) + data_set_path = '%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project) + source_volume = {'blockSize': TEGILE_DEFAULT_BLOCK_SIZE, + 'datasetPath': data_set_path, + 'local': 'true', + 'name': volume_name, + 'poolName': '%s' % pool, + 'projectName': '%s' % project, + 'protocol': self._protocol, + 'thinProvision': 'true', + 'volSize': src_vref['size'] * units.Gi} + + dest_volume = {'blockSize': TEGILE_DEFAULT_BLOCK_SIZE, + 'datasetPath': data_set_path, + # clone can reside only in the source project + 'local': 'true', + 'name': volume['name'], + 'poolName': '%s' % pool, + 'projectName': '%s' % project, + 'protocol': self._protocol, + 'thinProvision': 'true', + 'volSize': volume['size'] * units.Gi} + + params = list() + params.append(source_volume) + params.append(dest_volume) + + self._api_executor.send_api_request(method='createClonedVolume', + params=params) + return self.get_additional_info(volume, pool, project) + + @debugger + def get_volume_stats(self, refresh=False): + """Get volume status. + + If 'refresh' is True, run update first. + The name is a bit misleading as + the majority of the data here is cluster + data + """ + if refresh: + try: + self._update_volume_stats() + except Exception: + pass + + return self._stats + + @debugger + def _update_volume_stats(self): + """Retrieves stats info from volume group.""" + + try: + data = self._api_executor.send_api_request(method='getArrayStats', + request_type='get', + fine_logging=False) + # fixing values coming back here as String to float + data['total_capacity_gb'] = float(data.get('total_capacity_gb', 0)) + data['free_capacity_gb'] = float(data.get('free_capacity_gb', 0)) + for pool in data.get('pools', []): + pool['total_capacity_gb'] = float( + pool.get('total_capacity_gb', 0)) + pool['free_capacity_gb'] = float( + pool.get('free_capacity_gb', 0)) + pool['allocated_capacity_gb'] = float( + pool.get('allocated_capacity_gb', 0)) + + data['volume_backend_name'] = getattr(self.configuration, + 'volume_backend_name') + data['vendor_name'] = self.VENDOR + data['driver_version'] = self.VERSION + data['storage_protocol'] = self._protocol + + self._stats = data + except Exception as e: + LOG.warning(_LW('TegileIntelliFlashVolumeDriver(%(clsname)s) ' + '_update_volume_stats failed: %(error)s'), + {'clsname': self.__class__.__name__, + 'error': e}) + + @debugger + def get_pool(self, volume): + """Returns pool name where volume resides. + + :param volume: The volume hosted by the driver. + :return: Name of the pool where given volume is hosted. + """ + pool = volume_utils.extract_host(volume['host'], level='pool', + default_pool_name=self._default_pool) + return pool + + @debugger + def extend_volume(self, volume, new_size): + params = list() + pool, project, volume_name = self._get_pool_project_volume_name(volume) + params.append('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + volume_name)) + vol_size = six.text_type(new_size) + params.append(vol_size) + params.append('GB') + self._api_executor.send_api_request(method='resizeVolume', + params=params) + + @debugger + def manage_existing(self, volume, existing_ref): + volume['name_id'] = existing_ref['name'] + pool, project, volume_name = self._get_pool_project_volume_name(volume) + additional_info = self.get_additional_info(volume, pool, project) + additional_info['_name_id'] = existing_ref['name'], + return additional_info + + @debugger + def manage_existing_get_size(self, volume, existing_ref): + params = list() + pool, project, volume_name = self._get_pool_project_volume_name(volume) + params.append('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + existing_ref['name'])) + volume_size = self._api_executor.send_api_request( + method='getVolumeSizeinGB', + params=params) + + return volume_size + + @debugger + def _get_pool_project_volume_name(self, volume): + pool = volume_utils.extract_host(volume['host'], level='pool', + default_pool_name=self._default_pool) + try: + project = volume['metadata']['project'] + except (AttributeError, TypeError, KeyError): + project = self._default_project + + if volume['_name_id'] is not None: + volume_name = volume['_name_id'] + else: + volume_name = volume['name'] + + return pool, project, volume_name + + @debugger + def get_additional_info(self, volume, pool, project): + try: + metadata = self._get_volume_metadata(volume) + except Exception: + metadata = dict() + metadata['pool'] = pool + metadata['project'] = project + return {'metadata': metadata} + + @debugger + def _get_volume_metadata(self, volume): + volume_metadata = {} + if 'volume_metadata' in volume: + for metadata in volume['volume_metadata']: + volume_metadata[metadata['key']] = metadata['value'] + if 'metadata' in volume: + metadata = volume['metadata'] + for key in metadata: + volume_metadata[key] = metadata[key] + return volume_metadata + + @debugger + def _check_ops(self, required_ops, configuration): + """Ensures that the options we care about are set.""" + for attr in required_ops: + if not getattr(configuration, attr, None): + raise exception.InvalidInput(reason=_('%(attr)s is not ' + 'set.') % {'attr': attr}) + + +class TegileISCSIDriver(TegileIntelliFlashVolumeDriver, san.SanISCSIDriver): + """Tegile ISCSI Driver.""" + + def __init__(self, *args, **kwargs): + super(TegileISCSIDriver, self).__init__(*args, **kwargs) + self._protocol = 'iSCSI' + + @debugger + def do_setup(self, context): + super(TegileISCSIDriver, self).do_setup(context) + + @debugger + def initialize_connection(self, volume, connector): + """Driver entry point to attach a volume to an instance.""" + + if getattr(self.configuration, 'use_chap_auth', False): + chap_username = getattr(self.configuration, + 'chap_username', + '') + chap_password = getattr(self.configuration, + 'chap_password', + '') + else: + chap_username = '' + chap_password = '' + + if volume['provider_location'] is None: + params = list() + pool, project, volume_name = ( + self._get_pool_project_volume_name(volume)) + params.append('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + volume_name)) + initiator_info = { + 'initiatorName': connector['initiator'], + 'chapUserName': chap_username, + 'chapSecret': chap_password + } + params.append(initiator_info) + mapping_info = self._api_executor.send_api_request( + method='getISCSIMappingForVolume', + params=params) + target_portal = mapping_info['target_portal'] + target_iqn = mapping_info['target_iqn'] + target_lun = mapping_info['target_lun'] + else: + (target_portal, target_iqn, target_lun) = ( + volume['provider_location'].split()) + + connection_data = dict() + connection_data['target_portal'] = target_portal + connection_data['target_iqn'] = target_iqn + connection_data['target_lun'] = target_lun + connection_data['target_discovered'] = False, + connection_data['volume_id'] = volume['id'], + connection_data['access_mode'] = 'rw' + connection_data['discard'] = False + if getattr(self.configuration, 'use_chap_auth', False): + connection_data['auth_method'] = 'CHAP' + connection_data['auth_username'] = chap_username + connection_data['auth_password'] = chap_password + return { + 'driver_volume_type': 'iscsi', + 'data': connection_data + } + + @debugger + def terminate_connection(self, volume, connector, **kwargs): + pass + + @debugger + def create_export(self, context, volume, connector): + """Driver entry point to get the export info for a new volume.""" + params = list() + pool, project, volume_name = self._get_pool_project_volume_name(volume) + params.append('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + volume_name)) + if getattr(self.configuration, 'use_chap_auth', False): + chap_username = getattr(self.configuration, 'chap_username', '') + chap_password = getattr(self.configuration, 'chap_password', '') + else: + chap_username = '' + chap_password = '' + + initiator_info = { + 'initiatorName': connector['initiator'], + 'chapUserName': chap_username, + 'chapSecret': chap_password + } + params.append(initiator_info) + mapping_info = self._api_executor.send_api_request( + method='getISCSIMappingForVolume', + params=params) + target_portal = mapping_info['target_portal'] + target_iqn = mapping_info['target_iqn'] + target_lun = mapping_info['target_lun'] + + provider_location = '%s %s %s' % (target_portal, + target_iqn, + target_lun) + if getattr(self.configuration, 'use_chap_auth', False): + provider_auth = ('CHAP %s %s' % (chap_username, + chap_password)) + else: + provider_auth = None + return ( + {'provider_location': provider_location, + 'provider_auth': provider_auth}) + + +class TegileFCDriver(TegileIntelliFlashVolumeDriver, + driver.FibreChannelDriver): + """Tegile FC driver.""" + + def __init__(self, *args, **kwargs): + super(TegileFCDriver, self).__init__(*args, **kwargs) + self._protocol = 'FC' + + @debugger + def do_setup(self, context): + super(TegileFCDriver, self).do_setup(context) + + @fczm_utils.AddFCZone + @debugger + def initialize_connection(self, volume, connector): + """Initializes the connection and returns connection info.""" + + params = list() + pool, project, volume_name = self._get_pool_project_volume_name(volume) + params.append('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + volume_name)) + wwpns = connector['wwpns'] + + connectors = ','.join(wwpns) + + params.append(connectors) + target_info = self._api_executor.send_api_request( + method='getFCPortsForVolume', + params=params) + initiator_target_map = target_info['initiator_target_map'] + connection_data = { + 'driver_volume_type': 'fibre_channel', + 'data': { + 'encrypted': False, + 'target_discovered': False, + 'access_mode': 'rw', + 'target_lun': target_info['target_lun'], + 'target_wwn': ast.literal_eval(target_info['target_wwn']), + 'initiator_target_map': ast.literal_eval(initiator_target_map) + } + } + + return connection_data + + @fczm_utils.RemoveFCZone + @debugger + def terminate_connection(self, volume, connector, force=False, **kwargs): + + params = list() + pool, project, volume_name = self._get_pool_project_volume_name(volume) + params.append('%s/%s/%s/%s' % (pool, + TEGILE_LOCAL_CONTAINER_NAME, + project, + volume_name)) + wwpns = connector['wwpns'] + + connectors = ','.join(wwpns) + + params.append(connectors) + target_info = self._api_executor.send_api_request( + method='getFCPortsForVolume', + params=params) + initiator_target_map = target_info['initiator_target_map'] + + connection_data = { + 'data': { + 'target_wwn': ast.literal_eval(target_info['target_wwn']), + 'initiator_target_map': ast.literal_eval(initiator_target_map) + } + } + + return connection_data diff --git a/releasenotes/notes/add-tegile-driver-b7919c5f30911998.yaml b/releasenotes/notes/add-tegile-driver-b7919c5f30911998.yaml new file mode 100644 index 000000000..5111e55a2 --- /dev/null +++ b/releasenotes/notes/add-tegile-driver-b7919c5f30911998.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added driver for Tegile IntelliFlash arrays. + diff --git a/tests-py3.txt b/tests-py3.txt index 3563646ad..77c84fb8b 100644 --- a/tests-py3.txt +++ b/tests-py3.txt @@ -124,6 +124,7 @@ cinder.tests.unit.test_smbfs cinder.tests.unit.test_solidfire cinder.tests.unit.test_ssh_utils cinder.tests.unit.test_storwize_svc +cinder.tests.unit.test_tegile cinder.tests.unit.test_test cinder.tests.unit.test_test_utils cinder.tests.unit.test_tintri