]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add volume driver for Tegile IntelliFlash array
authorAbhilash Divakaran <abhilashd@tegile.com>
Wed, 18 Nov 2015 07:54:52 +0000 (23:54 -0800)
committerAbhilash Divakaran <abhilashd@tegile.com>
Mon, 28 Dec 2015 19:36:37 +0000 (11:36 -0800)
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

cinder/exception.py
cinder/opts.py
cinder/tests/unit/test_tegile.py [new file with mode: 0644]
cinder/volume/drivers/tegile.py [new file with mode: 0644]
releasenotes/notes/add-tegile-driver-b7919c5f30911998.yaml [new file with mode: 0644]
tests-py3.txt

index 9f412f0501ef0d5c41b2dbab37a62968eb21623a..ebcee0f9b166bdef56c7c40d9ec94d074086e280 100644 (file)
@@ -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")
index ad20f46e745474b3d72101e7f7dd1b2229aa2927..456e00aa3b9af36eaaeba15f7a6a8529429ad38d 100644 (file)
@@ -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 (file)
index 0000000..1f1a198
--- /dev/null
@@ -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 (file)
index 0000000..08a352a
--- /dev/null
@@ -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 (file)
index 0000000..5111e55
--- /dev/null
@@ -0,0 +1,4 @@
+---
+features:
+  - Added driver for Tegile IntelliFlash arrays.
+
index 3563646ad14c75fb37ab57f0bd8340cc6e564267..77c84fb8bbd092a5c12694a73b42205007f30769 100644 (file)
@@ -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