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
This commit is contained in:
Yusuke Hayashi 2016-01-06 17:02:54 +09:00
parent 551cd6ae07
commit 0ea3394dec
4 changed files with 432 additions and 4 deletions

View 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)

View 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'])

View File

@ -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

View File

@ -0,0 +1,3 @@
---
features:
- Added backend driver for Fujitsu ETERNUS DX (FC).