From 0ea3394dec577d25cf2bcfd72e45f57752010c06 Mon Sep 17 00:00:00 2001 From: Yusuke Hayashi Date: Wed, 6 Jan 2016 17:02:54 +0900 Subject: [PATCH] Add Fujitsu ETERNUS DX Volume Driver (FC part) 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 --- .../tests/unit/volume/drivers/test_fujitsu.py | 114 ++++++++++ .../drivers/fujitsu/eternus_dx_common.py | 105 ++++++++- .../volume/drivers/fujitsu/eternus_dx_fc.py | 214 ++++++++++++++++++ ...ujitsu-eternus-dx-fc-741319960195215c.yaml | 3 + 4 files changed, 432 insertions(+), 4 deletions(-) create mode 100644 cinder/volume/drivers/fujitsu/eternus_dx_fc.py create mode 100644 releasenotes/notes/fujitsu-eternus-dx-fc-741319960195215c.yaml diff --git a/cinder/tests/unit/volume/drivers/test_fujitsu.py b/cinder/tests/unit/volume/drivers/test_fujitsu.py index 19df0a6ed..d72416cac 100644 --- a/cinder/tests/unit/volume/drivers/test_fujitsu.py +++ b/cinder/tests/unit/volume/drivers/test_fujitsu.py @@ -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) diff --git a/cinder/volume/drivers/fujitsu/eternus_dx_common.py b/cinder/volume/drivers/fujitsu/eternus_dx_common.py index 5a4d25f9d..f25aba681 100644 --- a/cinder/volume/drivers/fujitsu/eternus_dx_common.py +++ b/cinder/volume/drivers/fujitsu/eternus_dx_common.py @@ -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 index 000000000..1c377ea32 --- /dev/null +++ b/cinder/volume/drivers/fujitsu/eternus_dx_fc.py @@ -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 index 000000000..9b612ec4a --- /dev/null +++ b/releasenotes/notes/fujitsu-eternus-dx-fc-741319960195215c.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added backend driver for Fujitsu ETERNUS DX (FC). -- 2.45.2