]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add Fujitsu ETERNUS DX Volume Driver (FC part)
authorYusuke Hayashi <hayashi-yusuke@jp.fujitsu.com>
Wed, 6 Jan 2016 08:02:54 +0000 (17:02 +0900)
committerYusuke Hayashi <hayashi-yusuke@jp.fujitsu.com>
Mon, 18 Jan 2016 06:32:51 +0000 (06:32 +0000)
As I explained in my blueprint,
this patch completes Fujitsu ETERNUS DX Volume Driver.
Fujitsu ETERNUS DX Volume Driver consists of two parts, iSCSI and FC.
The iSCSI part [1] had been reviewed and
thanks to the nice reviews, it's merged.
The iSCSI and FC parts have a lot of common codes,
and all the common codes are included in the iSCSI part.

[1] https://review.openstack.org/201500/

DocImpact
Implements: blueprint fujitsu-eternus-dx-driver
Change-Id: If61145ee999bffd82223a99a7d59de315a5ecd3b

cinder/tests/unit/volume/drivers/test_fujitsu.py
cinder/volume/drivers/fujitsu/eternus_dx_common.py
cinder/volume/drivers/fujitsu/eternus_dx_fc.py [new file with mode: 0644]
releasenotes/notes/fujitsu-eternus-dx-fc-741319960195215c.yaml [new file with mode: 0644]

index 19df0a6edfe46cd79a9ab9b0fe9b01b7d5c8b465..d72416cacfb649177113cf032fe281adbfdece7c 100644 (file)
@@ -26,6 +26,7 @@ from cinder.volume import configuration as conf
 
 with mock.patch.dict('sys.modules', pywbem=mock.Mock()):
     from cinder.volume.drivers.fujitsu import eternus_dx_common as dx_common
+    from cinder.volume.drivers.fujitsu import eternus_dx_fc as dx_fc
     from cinder.volume.drivers.fujitsu import eternus_dx_iscsi as dx_iscsi
 
 CONFIG_FILE_NAME = 'cinder_fujitsu_eternus_dx.xml'
@@ -693,6 +694,119 @@ class FakeEternusConnection(object):
         return instance
 
 
+class FJFCDriverTestCase(test.TestCase):
+    def __init__(self, *args, **kwargs):
+        super(FJFCDriverTestCase, self).__init__(*args, **kwargs)
+
+    def setUp(self):
+        super(FJFCDriverTestCase, self).setUp()
+
+        # Make fake xml-configuration file.
+        self.config_file = tempfile.NamedTemporaryFile("w+", suffix='.xml')
+        self.addCleanup(self.config_file.close)
+        self.config_file.write(CONF)
+        self.config_file.flush()
+
+        # Make fake Object by using mock as configuration object.
+        self.configuration = mock.Mock(spec=conf.Configuration)
+        self.configuration.cinder_eternus_config_file = self.config_file.name
+
+        self.stubs.Set(dx_common.FJDXCommon, '_get_eternus_connection',
+                       self.fake_eternus_connection)
+
+        instancename = FakeCIMInstanceName()
+        self.stubs.Set(dx_common.FJDXCommon, '_create_eternus_instance_name',
+                       instancename.fake_create_eternus_instance_name)
+
+        # Set iscsi driver to self.driver.
+        driver = dx_fc.FJDXFCDriver(configuration=self.configuration)
+        self.driver = driver
+
+    def fake_eternus_connection(self):
+        conn = FakeEternusConnection()
+        return conn
+
+    def test_get_volume_stats(self):
+        ret = self.driver.get_volume_stats(True)
+        stats = {'vendor_name': ret['vendor_name'],
+                 'total_capacity_gb': ret['total_capacity_gb'],
+                 'free_capacity_gb': ret['free_capacity_gb']}
+        self.assertEqual(FAKE_STATS, stats)
+
+    def test_create_and_delete_volume(self):
+        model_info = self.driver.create_volume(TEST_VOLUME)
+        self.assertEqual(FAKE_MODEL_INFO1, model_info)
+
+        self.driver.delete_volume(TEST_VOLUME)
+
+    @mock.patch.object(dx_common.FJDXCommon, '_get_mapdata')
+    def test_map_unmap(self, mock_mapdata):
+        fake_data = {'target_wwn': FC_TARGET_WWN,
+                     'target_lun': 0}
+
+        mock_mapdata.return_value = fake_data
+        fake_mapdata = dict(fake_data)
+        fake_mapdata['initiator_target_map'] = {
+            initiator: FC_TARGET_WWN for initiator in TEST_WWPN
+        }
+
+        fake_mapdata['volume_id'] = TEST_VOLUME['id']
+        fake_mapdata['target_discovered'] = True
+        fake_info = {'driver_volume_type': 'fibre_channel',
+                     'data': fake_mapdata}
+
+        model_info = self.driver.create_volume(TEST_VOLUME)
+        self.assertEqual(FAKE_MODEL_INFO1, model_info)
+
+        info = self.driver.initialize_connection(TEST_VOLUME,
+                                                 TEST_CONNECTOR)
+        self.assertEqual(fake_info, info)
+        self.driver.terminate_connection(TEST_VOLUME,
+                                         TEST_CONNECTOR)
+        self.driver.delete_volume(TEST_VOLUME)
+
+    def test_create_and_delete_snapshot(self):
+        model_info = self.driver.create_volume(TEST_VOLUME)
+        self.assertEqual(FAKE_MODEL_INFO1, model_info)
+
+        snap_info = self.driver.create_snapshot(TEST_SNAP)
+        self.assertEqual(FAKE_SNAP_INFO, snap_info)
+
+        self.driver.delete_snapshot(TEST_SNAP)
+        self.driver.delete_volume(TEST_VOLUME)
+
+    def test_create_volume_from_snapshot(self):
+        model_info = self.driver.create_volume(TEST_VOLUME)
+        self.assertEqual(FAKE_MODEL_INFO1, model_info)
+
+        snap_info = self.driver.create_snapshot(TEST_SNAP)
+        self.assertEqual(FAKE_SNAP_INFO, snap_info)
+
+        model_info = self.driver.create_volume_from_snapshot(TEST_CLONE,
+                                                             TEST_SNAP)
+        self.assertEqual(FAKE_MODEL_INFO2, model_info)
+
+        self.driver.delete_snapshot(TEST_SNAP)
+        self.driver.delete_volume(TEST_CLONE)
+        self.driver.delete_volume(TEST_VOLUME)
+
+    def test_create_cloned_volume(self):
+        model_info = self.driver.create_volume(TEST_VOLUME)
+        self.assertEqual(FAKE_MODEL_INFO1, model_info)
+
+        model_info = self.driver.create_cloned_volume(TEST_CLONE, TEST_VOLUME)
+        self.assertEqual(FAKE_MODEL_INFO2, model_info)
+
+        self.driver.delete_volume(TEST_CLONE)
+        self.driver.delete_volume(TEST_VOLUME)
+
+    def test_extend_volume(self):
+        model_info = self.driver.create_volume(TEST_VOLUME)
+        self.assertEqual(FAKE_MODEL_INFO1, model_info)
+
+        self.driver.extend_volume(TEST_VOLUME, 10)
+
+
 class FJISCSIDriverTestCase(test.TestCase):
     def __init__(self, *args, **kwargs):
         super(FJISCSIDriverTestCase, self).__init__(*args, **kwargs)
index 5a4d25f9d4be09c90be7f20f2665ecc51afe6a9d..f25aba681f0aed39d6a10302dc71dc4729953d5d 100644 (file)
@@ -705,7 +705,10 @@ class FJDXCommon(object):
         mapdata['target_discovered'] = True
         mapdata['volume_id'] = volume['id']
 
-        if self.protocol == 'iSCSI':
+        if self.protocol == 'fc':
+            device_info = {'driver_volume_type': 'fibre_channel',
+                           'data': mapdata}
+        elif self.protocol == 'iSCSI':
             device_info = {'driver_volume_type': 'iscsi',
                            'data': mapdata}
 
@@ -726,6 +729,37 @@ class FJDXCommon(object):
         LOG.debug('terminate_connection, map_exist: %s.', map_exist)
         return map_exist
 
+    def build_fc_init_tgt_map(self, connector, target_wwn=None):
+        """Build parameter for Zone Manager"""
+        LOG.debug('build_fc_init_tgt_map, target_wwn: %s.', target_wwn)
+
+        initiatorlist = self._find_initiator_names(connector)
+
+        if target_wwn is None:
+            target_wwn = []
+            target_portlist = self._get_target_port()
+            for target_port in target_portlist:
+                target_wwn.append(target_port['Name'])
+
+        init_tgt_map = {initiator: target_wwn for initiator in initiatorlist}
+
+        LOG.debug('build_fc_init_tgt_map, '
+                  'initiator target mapping: %s.', init_tgt_map)
+        return init_tgt_map
+
+    def check_attached_volume_in_zone(self, connector):
+        """Check Attached Volume in Same FC Zone or not"""
+        LOG.debug('check_attached_volume_in_zone, connector: %s.', connector)
+
+        aglist = self._find_affinity_group(connector)
+        if not aglist:
+            attached = False
+        else:
+            attached = True
+
+        LOG.debug('check_attached_volume_in_zone, attached: %s.', attached)
+        return attached
+
     @lockutils.synchronized('ETERNUS-vol', 'cinder-', True)
     def extend_volume(self, volume, new_size):
         """Extend volume on ETERNUS."""
@@ -836,6 +870,8 @@ class FJDXCommon(object):
                    'eternus_pool': eternus_pool,
                    'pooltype': POOL_TYPE_dic[pooltype]})
 
+        return eternus_pool
+
     @lockutils.synchronized('ETERNUS-update', 'cinder-', True)
     def update_volume_stats(self):
         """get pool capacity."""
@@ -890,13 +926,67 @@ class FJDXCommon(object):
         if not aglist:
             LOG.debug('_get_mapdata, ag_list:%s.', aglist)
         else:
-            if self.protocol == 'iSCSI':
+            if self.protocol == 'fc':
+                mapdata = self._get_mapdata_fc(aglist, vol_instance,
+                                               target_portlist)
+            elif self.protocol == 'iSCSI':
                 mapdata = self._get_mapdata_iscsi(aglist, vol_instance,
                                                   multipath)
 
         LOG.debug('_get_mapdata, mapdata: %s.', mapdata)
         return mapdata
 
+    def _get_mapdata_fc(self, aglist, vol_instance, target_portlist):
+        """_get_mapdata for FibreChannel."""
+        target_wwn = []
+
+        try:
+            ag_volmaplist = self._reference_eternus_names(
+                aglist[0],
+                ResultClass='CIM_ProtocolControllerForUnit')
+            vo_volmaplist = self._reference_eternus_names(
+                vol_instance.path,
+                ResultClass='CIM_ProtocolControllerForUnit')
+        except pywbem.CIM_Error:
+            msg = (_('_get_mapdata_fc, '
+                     'getting host-affinity from aglist/vol_instance failed, '
+                     'affinitygroup: %(ag)s, '
+                     'ReferenceNames, '
+                     'cannot connect to ETERNUS.')
+                   % {'ag': aglist[0]})
+            LOG.exception(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        volmap = None
+        for vo_volmap in vo_volmaplist:
+            if vo_volmap in ag_volmaplist:
+                volmap = vo_volmap
+                break
+
+        try:
+            volmapinstance = self._get_eternus_instance(
+                volmap,
+                LocalOnly=False)
+        except pywbem.CIM_Error:
+            msg = (_('_get_mapdata_fc, '
+                     'getting host-affinity instance failed, '
+                     'volmap: %(volmap)s, '
+                     'GetInstance, '
+                     'cannot connect to ETERNUS.')
+                   % {'volmap': volmap})
+            LOG.exception(msg)
+            raise exception.VolumeBackendAPIException(data=msg)
+
+        target_lun = int(volmapinstance['DeviceNumber'], 16)
+
+        for target_port in target_portlist:
+            target_wwn.append(target_port['Name'])
+
+        mapdata = {'target_wwn': target_wwn,
+                   'target_lun': target_lun}
+        LOG.debug('_get_mapdata_fc, mapdata: %s.', mapdata)
+        return mapdata
+
     def _get_mapdata_iscsi(self, aglist, vol_instance, multipath):
         """_get_mapdata for iSCSI."""
         target_portals = []
@@ -1502,7 +1592,10 @@ class FJDXCommon(object):
         LOG.debug('_get_target_port, protocol: %s.', self.protocol)
 
         target_portlist = []
-        if self.protocol == 'iSCSI':
+        if self.protocol == 'fc':
+            prtcl_endpoint = 'FUJITSU_SCSIProtocolEndpoint'
+            connection_type = 2
+        elif self.protocol == 'iSCSI':
             prtcl_endpoint = 'FUJITSU_iSCSIProtocolEndpoint'
             connection_type = 7
 
@@ -1673,7 +1766,11 @@ class FJDXCommon(object):
 
         initiatornamelist = []
 
-        if self.protocol == 'iSCSI' and connector['initiator']:
+        if self.protocol == 'fc' and connector['wwpns']:
+            LOG.debug('_find_initiator_names, wwpns: %s.',
+                      connector['wwpns'])
+            initiatornamelist = connector['wwpns']
+        elif self.protocol == 'iSCSI' and connector['initiator']:
             LOG.debug('_find_initiator_names, initiator: %s.',
                       connector['initiator'])
             initiatornamelist.append(connector['initiator'])
diff --git a/cinder/volume/drivers/fujitsu/eternus_dx_fc.py b/cinder/volume/drivers/fujitsu/eternus_dx_fc.py
new file mode 100644 (file)
index 0000000..1c377ea
--- /dev/null
@@ -0,0 +1,214 @@
+# Copyright (c) 2015 FUJITSU LIMITED
+# Copyright (c) 2012 EMC Corporation.
+# Copyright (c) 2012 OpenStack Foundation
+# 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.
+#
+
+"""
+FibreChannel Cinder Volume driver for Fujitsu ETERNUS DX S3 series.
+"""
+from oslo_log import log as logging
+import six
+
+from cinder.volume import driver
+from cinder.volume.drivers.fujitsu import eternus_dx_common
+from cinder.zonemanager import utils as fczm_utils
+
+LOG = logging.getLogger(__name__)
+
+
+class FJDXFCDriver(driver.FibreChannelDriver):
+    """FC Cinder Volume Driver for Fujitsu ETERNUS DX S3 series."""
+
+    def __init__(self, *args, **kwargs):
+
+        super(FJDXFCDriver, self).__init__(*args, **kwargs)
+        self.common = eternus_dx_common.FJDXCommon(
+            'fc',
+            configuration=self.configuration)
+        self.VERSION = self.common.VERSION
+
+    def check_for_setup_error(self):
+        pass
+
+    def create_volume(self, volume):
+        """Create volume."""
+        LOG.debug('create_volume, '
+                  'volume id: %s, enter method.', volume['id'])
+
+        location, metadata = self.common.create_volume(volume)
+
+        v_metadata = self._get_metadata(volume)
+        metadata.update(v_metadata)
+
+        LOG.debug('create_volume, info: %s, exit method.', metadata)
+        return {'provider_location': six.text_type(location),
+                'metadata': metadata}
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """Creates a volume from a snapshot."""
+        LOG.debug('create_volume_from_snapshot, '
+                  'volume id: %(vid)s, snap id: %(sid)s, enter method.',
+                  {'vid': volume['id'], 'sid': snapshot['id']})
+
+        location, metadata = (
+            self.common.create_volume_from_snapshot(volume, snapshot))
+
+        v_metadata = self._get_metadata(volume)
+        metadata.update(v_metadata)
+
+        LOG.debug('create_volume_from_snapshot, '
+                  'info: %s, exit method.', metadata)
+        return {'provider_location': six.text_type(location),
+                'metadata': metadata}
+
+    def create_cloned_volume(self, volume, src_vref):
+        """Create cloned volume."""
+        LOG.debug('create_cloned_volume, '
+                  'target volume id: %(tid)s, '
+                  'source volume id: %(sid)s, enter method.',
+                  {'tid': volume['id'], 'sid': src_vref['id']})
+
+        location, metadata = (
+            self.common.create_cloned_volume(volume, src_vref))
+
+        v_metadata = self._get_metadata(volume)
+        metadata.update(v_metadata)
+
+        LOG.debug('create_cloned_volume, '
+                  'info: %s, exit method.', metadata)
+        return {'provider_location': six.text_type(location),
+                'metadata': metadata}
+
+    def delete_volume(self, volume):
+        """Delete volume on ETERNUS."""
+        LOG.debug('delete_volume, '
+                  'volume id: %s, enter method.', volume['id'])
+
+        vol_exist = self.common.delete_volume(volume)
+
+        LOG.debug('delete_volume, '
+                  'delete: %s, exit method.', vol_exist)
+
+    def create_snapshot(self, snapshot):
+        """Creates a snapshot."""
+        LOG.debug('create_snapshot, '
+                  'snap id: %(sid)s, volume id: %(vid)s, enter method.',
+                  {'sid': snapshot['id'], 'vid': snapshot['volume_id']})
+
+        location, metadata = self.common.create_snapshot(snapshot)
+
+        LOG.debug('create_snapshot, info: %s, exit method.', metadata)
+        return {'provider_location': six.text_type(location)}
+
+    def delete_snapshot(self, snapshot):
+        """Deletes a snapshot."""
+        LOG.debug('delete_snapshot, '
+                  'snap id: %(sid)s, volume id: %(vid)s, enter method.',
+                  {'sid': snapshot['id'], 'vid': snapshot['volume_id']})
+
+        vol_exist = self.common.delete_snapshot(snapshot)
+
+        LOG.debug('delete_snapshot, '
+                  'delete: %s, exit method.', vol_exist)
+
+    def ensure_export(self, context, volume):
+        """Driver entry point to get the export info for an existing volume."""
+        return
+
+    def create_export(self, context, volume, connector):
+        """Driver entry point to get the export info for a new volume."""
+        return
+
+    def remove_export(self, context, volume):
+        """Driver entry point to remove an export for a volume."""
+        return
+
+    @fczm_utils.AddFCZone
+    def initialize_connection(self, volume, connector):
+        """Allow connection to connector and return connection info."""
+        LOG.debug('initialize_connection, volume id: %(vid)s, '
+                  'wwpns: %(wwpns)s, enter method.',
+                  {'vid': volume['id'], 'wwpns': connector['wwpns']})
+
+        info = self.common.initialize_connection(volume, connector)
+
+        data = info['data']
+        init_tgt_map = (
+            self.common.build_fc_init_tgt_map(connector, data['target_wwn']))
+        data['initiator_target_map'] = init_tgt_map
+
+        info['data'] = data
+        LOG.debug('initialize_connection, '
+                  'info: %s, exit method.', info)
+        return info
+
+    @fczm_utils.RemoveFCZone
+    def terminate_connection(self, volume, connector, **kwargs):
+        """Disallow connection from connector."""
+        LOG.debug('terminate_connection, volume id: %(vid)s, '
+                  'wwpns: %(wwpns)s, enter method.',
+                  {'vid': volume['id'], 'wwpns': connector['wwpns']})
+
+        map_exist = self.common.terminate_connection(volume, connector)
+        attached = self.common.check_attached_volume_in_zone(connector)
+
+        info = {'driver_volume_type': 'fibre_channel',
+                'data': {}}
+
+        if not attached:
+            # No more volumes attached to the host
+            init_tgt_map = self.common.build_fc_init_tgt_map(connector)
+            info['data'] = {'initiator_target_map': init_tgt_map}
+
+        LOG.debug('terminate_connection, unmap: %(unmap)s, '
+                  'connection info: %(info)s, exit method',
+                  {'unmap': map_exist, 'info': info})
+        return info
+
+    def get_volume_stats(self, refresh=False):
+        """Get volume stats."""
+        LOG.debug('get_volume_stats, refresh: %s, enter method.', refresh)
+
+        pool_name = None
+        if refresh is True:
+            data, pool_name = self.common.update_volume_stats()
+            backend_name = self.configuration.safe_get('volume_backend_name')
+            data['volume_backend_name'] = backend_name or 'FJDXFCDriver'
+            data['storage_protocol'] = 'FC'
+            self._stats = data
+
+        LOG.debug('get_volume_stats, '
+                  'pool name: %s, exit method.', pool_name)
+        return self._stats
+
+    def extend_volume(self, volume, new_size):
+        """Extend volume."""
+        LOG.debug('extend_volume, '
+                  'volume id: %s, enter method.', volume['id'])
+
+        used_pool_name = self.common.extend_volume(volume, new_size)
+
+        LOG.debug('extend_volume, '
+                  'used pool name: %s, exit method.', used_pool_name)
+
+    def _get_metadata(self, volume):
+        v_metadata = volume.get('volume_metadata')
+        if v_metadata:
+            ret = {data['key']: data['value'] for data in v_metadata}
+        else:
+            ret = volume.get('metadata', {})
+
+        return ret
diff --git a/releasenotes/notes/fujitsu-eternus-dx-fc-741319960195215c.yaml b/releasenotes/notes/fujitsu-eternus-dx-fc-741319960195215c.yaml
new file mode 100644 (file)
index 0000000..9b612ec
--- /dev/null
@@ -0,0 +1,3 @@
+---
+features:
+ - Added backend driver for Fujitsu ETERNUS DX (FC).