Add Fibre Channel support for Nimble Storage
Support all the basic volume driver features which are also supported by the NimbleISCSIDriver DocImpact Implements: blueprint nimble-add-fc-support Change-Id: I83b62ab42c6224509b7fef5e72a692a559b2e56d
This commit is contained in:
parent
ac179a3a43
commit
c3d1dd1048
@ -33,7 +33,9 @@ NIMBLE_CLIENT = 'cinder.volume.drivers.nimble.client'
|
|||||||
NIMBLE_URLLIB2 = 'six.moves.urllib.request'
|
NIMBLE_URLLIB2 = 'six.moves.urllib.request'
|
||||||
NIMBLE_RANDOM = 'cinder.volume.drivers.nimble.random'
|
NIMBLE_RANDOM = 'cinder.volume.drivers.nimble.random'
|
||||||
NIMBLE_ISCSI_DRIVER = 'cinder.volume.drivers.nimble.NimbleISCSIDriver'
|
NIMBLE_ISCSI_DRIVER = 'cinder.volume.drivers.nimble.NimbleISCSIDriver'
|
||||||
DRIVER_VERSION = '3.0.0'
|
NIMBLE_BASE_DRIVER = 'cinder.volume.drivers.nimble.NimbleBaseVolumeDriver'
|
||||||
|
NIMBLE_FC_DRIVER = 'cinder.volume.drivers.nimble.NimbleFCDriver'
|
||||||
|
DRIVER_VERSION = '3.1.0'
|
||||||
|
|
||||||
FAKE_ENUM_STRING = """
|
FAKE_ENUM_STRING = """
|
||||||
<simpleType name="SmErrorType">
|
<simpleType name="SmErrorType">
|
||||||
@ -115,6 +117,15 @@ FAKE_IGROUP_LIST_RESPONSE = {
|
|||||||
{'initiator-list': [{'name': 'test-initiator1'}],
|
{'initiator-list': [{'name': 'test-initiator1'}],
|
||||||
'name': 'test-igrp2'}]}
|
'name': 'test-igrp2'}]}
|
||||||
|
|
||||||
|
FAKE_IGROUP_LIST_RESPONSE_FC = {
|
||||||
|
'err-list': {'err-list': [{'code': 0}]},
|
||||||
|
'initiatorgrp-list': [
|
||||||
|
{'initiator-list': [{'wwpn': '10:00:00:00:00:00:00:00'}],
|
||||||
|
'name': 'test-igrp2'},
|
||||||
|
{'initiator-list': [{'wwpn': '10:00:00:00:00:00:00:00'},
|
||||||
|
{'wwpn': '10:00:00:00:00:00:00:01'}],
|
||||||
|
'name': 'test-igrp1'}]}
|
||||||
|
|
||||||
FAKE_GET_VOL_INFO_RESPONSE = {
|
FAKE_GET_VOL_INFO_RESPONSE = {
|
||||||
'err-list': {'err-list': [{'code': 0}]},
|
'err-list': {'err-list': [{'code': 0}]},
|
||||||
'vol': {'target-name': 'iqn.test',
|
'vol': {'target-name': 'iqn.test',
|
||||||
@ -205,6 +216,27 @@ class NimbleDriverBaseTestCase(test.TestCase):
|
|||||||
return inner_client_mock
|
return inner_client_mock
|
||||||
return client_mock_wrapper
|
return client_mock_wrapper
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def client_mock_decorator_fc(configuration):
|
||||||
|
def client_mock_wrapper(func):
|
||||||
|
def inner_clent_mock(
|
||||||
|
self, mock_client_class, mock_urllib2, *args, **kwargs):
|
||||||
|
self.mock_client_class = mock_client_class
|
||||||
|
self.mock_client_service = mock.MagicMock(name='Client')
|
||||||
|
self.mock_client_class.Client.return_value = (
|
||||||
|
self.mock_client_service)
|
||||||
|
mock_wsdl = mock_urllib2.urlopen.return_value
|
||||||
|
mock_wsdl.read = mock.MagicMock()
|
||||||
|
mock_wsdl.read.return_value = FAKE_ENUM_STRING
|
||||||
|
self.driver = nimble.NimbleFCDriver(
|
||||||
|
configuration=configuration)
|
||||||
|
self.mock_client_service.service.login.return_value = (
|
||||||
|
FAKE_POSITIVE_LOGIN_RESPONSE_1)
|
||||||
|
self.driver.do_setup(context.get_admin_context())
|
||||||
|
func(self, *args, **kwargs)
|
||||||
|
return inner_clent_mock
|
||||||
|
return client_mock_wrapper
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(NimbleDriverBaseTestCase, self).tearDown()
|
super(NimbleDriverBaseTestCase, self).tearDown()
|
||||||
|
|
||||||
@ -1009,6 +1041,53 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
|
|||||||
self.mock_client_service.method_calls,
|
self.mock_client_service.method_calls,
|
||||||
expected_call_list)
|
expected_call_list)
|
||||||
|
|
||||||
|
@mock.patch(NIMBLE_URLLIB2)
|
||||||
|
@mock.patch(NIMBLE_CLIENT)
|
||||||
|
@mock.patch.object(obj_volume.VolumeList, 'get_all',
|
||||||
|
mock.Mock(return_value=[]))
|
||||||
|
@NimbleDriverBaseTestCase.client_mock_decorator_fc(create_configuration(
|
||||||
|
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
|
||||||
|
@mock.patch(NIMBLE_FC_DRIVER + ".get_lun_number")
|
||||||
|
@mock.patch(NIMBLE_FC_DRIVER + ".get_wwpns_from_array")
|
||||||
|
def test_initialize_connection_fc_igroup_exist(self, mock_wwpns,
|
||||||
|
mock_lun_number):
|
||||||
|
mock_lun_number.return_value = 13
|
||||||
|
mock_wwpns.return_value = ["1111111111111101"]
|
||||||
|
self.mock_client_service.service.getInitiatorGrpList.return_value = (
|
||||||
|
FAKE_IGROUP_LIST_RESPONSE_FC)
|
||||||
|
expected_res = {
|
||||||
|
'driver_volume_type': 'fibre_channel',
|
||||||
|
'data': {
|
||||||
|
'target_lun': 13,
|
||||||
|
'target_discovered': True,
|
||||||
|
'target_wwn': ["1111111111111101"],
|
||||||
|
'initiator_target_map': {'1000000000000000':
|
||||||
|
['1111111111111101']}}}
|
||||||
|
self.assertEqual(
|
||||||
|
expected_res,
|
||||||
|
self.driver.initialize_connection(
|
||||||
|
{'name': 'test-volume',
|
||||||
|
'provider_location': 'array1',
|
||||||
|
'id': 12},
|
||||||
|
{'initiator': 'test-initiator1',
|
||||||
|
'wwpns': ['1000000000000000']}))
|
||||||
|
expected_call_list = [mock.call.set_options(
|
||||||
|
location='https://10.18.108.55:5391/soap'),
|
||||||
|
mock.call.service.login(
|
||||||
|
req={
|
||||||
|
'username': 'nimble', 'password': 'nimble_pass'}),
|
||||||
|
mock.call.service.getInitiatorGrpList(
|
||||||
|
request={'sid': 'a9b9aba7'}),
|
||||||
|
mock.call.service.addVolAcl(
|
||||||
|
request={'volname': 'test-volume',
|
||||||
|
'apply-to': 3,
|
||||||
|
'chapuser': '*',
|
||||||
|
'initiatorgrp': 'test-igrp2',
|
||||||
|
'sid': 'a9b9aba7'})]
|
||||||
|
self.assertEqual(
|
||||||
|
self.mock_client_service.method_calls,
|
||||||
|
expected_call_list)
|
||||||
|
|
||||||
@mock.patch(NIMBLE_URLLIB2)
|
@mock.patch(NIMBLE_URLLIB2)
|
||||||
@mock.patch(NIMBLE_CLIENT)
|
@mock.patch(NIMBLE_CLIENT)
|
||||||
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
||||||
@ -1053,6 +1132,57 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
|
|||||||
self.mock_client_service.method_calls,
|
self.mock_client_service.method_calls,
|
||||||
expected_calls)
|
expected_calls)
|
||||||
|
|
||||||
|
@mock.patch(NIMBLE_URLLIB2)
|
||||||
|
@mock.patch(NIMBLE_CLIENT)
|
||||||
|
@mock.patch.object(obj_volume.VolumeList, 'get_all',
|
||||||
|
mock.Mock(return_value=[]))
|
||||||
|
@NimbleDriverBaseTestCase.client_mock_decorator_fc(create_configuration(
|
||||||
|
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
|
||||||
|
@mock.patch(NIMBLE_FC_DRIVER + ".get_wwpns_from_array")
|
||||||
|
@mock.patch(NIMBLE_FC_DRIVER + ".get_lun_number")
|
||||||
|
@mock.patch(NIMBLE_RANDOM)
|
||||||
|
def test_initialize_connection_fc_igroup_not_exist(self, mock_random,
|
||||||
|
mock_lun_number,
|
||||||
|
mock_wwpns):
|
||||||
|
mock_random.sample.return_value = 'abcdefghijkl'
|
||||||
|
mock_lun_number.return_value = 13
|
||||||
|
mock_wwpns.return_value = ["1111111111111101"]
|
||||||
|
self.mock_client_service.service.getInitiatorGrpList.return_value = (
|
||||||
|
FAKE_IGROUP_LIST_RESPONSE_FC)
|
||||||
|
expected_res = {
|
||||||
|
'driver_volume_type': 'fibre_channel',
|
||||||
|
'data': {
|
||||||
|
'target_lun': 13,
|
||||||
|
'target_discovered': True,
|
||||||
|
'target_wwn': ["1111111111111101"],
|
||||||
|
'initiator_target_map': {'1000000000000000':
|
||||||
|
['1111111111111101']}}}
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
expected_res,
|
||||||
|
self.driver.initialize_connection(
|
||||||
|
{'name': 'test-volume',
|
||||||
|
'provider_location': 'array1',
|
||||||
|
'id': 12},
|
||||||
|
{'initiator': 'test-initiator3',
|
||||||
|
'wwpns': ['1000000000000000']}))
|
||||||
|
expected_calls = [
|
||||||
|
mock.call.service.getInitiatorGrpList(
|
||||||
|
request={'sid': 'a9b9aba7'}),
|
||||||
|
mock.call.service.createInitiatorGrp(
|
||||||
|
request={
|
||||||
|
'attr': {'initiator-list': [{'wwn': '1000000000000000'}],
|
||||||
|
'name': 'openstack-abcdefghijkl'},
|
||||||
|
'sid': 'a9b9aba7'}),
|
||||||
|
mock.call.service.addVolAcl(
|
||||||
|
request={'volname': 'test-volume', 'apply-to': 3,
|
||||||
|
'chapuser': '*',
|
||||||
|
'initiatorgrp': 'openstack-abcdefghijkl',
|
||||||
|
'sid': 'a9b9aba7'})]
|
||||||
|
self.mock_client_service.assert_has_calls(
|
||||||
|
self.mock_client_service.method_calls,
|
||||||
|
expected_calls)
|
||||||
|
|
||||||
@mock.patch(NIMBLE_URLLIB2)
|
@mock.patch(NIMBLE_URLLIB2)
|
||||||
@mock.patch(NIMBLE_CLIENT)
|
@mock.patch(NIMBLE_CLIENT)
|
||||||
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
||||||
@ -1080,6 +1210,36 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
|
|||||||
self.mock_client_service.method_calls,
|
self.mock_client_service.method_calls,
|
||||||
expected_calls)
|
expected_calls)
|
||||||
|
|
||||||
|
@mock.patch(NIMBLE_URLLIB2)
|
||||||
|
@mock.patch(NIMBLE_CLIENT)
|
||||||
|
@mock.patch.object(obj_volume.VolumeList, 'get_all',
|
||||||
|
mock.Mock(return_value=[]))
|
||||||
|
@NimbleDriverBaseTestCase.client_mock_decorator_fc(create_configuration(
|
||||||
|
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
|
||||||
|
@mock.patch(NIMBLE_FC_DRIVER + ".get_wwpns_from_array")
|
||||||
|
def test_terminate_connection_positive_fc(self, mock_wwpns):
|
||||||
|
mock_wwpns.return_value = ["1111111111111101"]
|
||||||
|
self.mock_client_service.service.getInitiatorGrpList.return_value = (
|
||||||
|
FAKE_IGROUP_LIST_RESPONSE_FC)
|
||||||
|
self.driver.terminate_connection(
|
||||||
|
{'name': 'test-volume',
|
||||||
|
'provider_location': 'array1',
|
||||||
|
'id': 12},
|
||||||
|
{'initiator': 'test-initiator1',
|
||||||
|
'wwpns': ['1000000000000000']})
|
||||||
|
expected_calls = [mock.call.service.getInitiatorGrpList(
|
||||||
|
request={'sid': 'a9b9aba7'}),
|
||||||
|
mock.call.service.removeVolAcl(
|
||||||
|
request={'volname': 'test-volume',
|
||||||
|
'apply-to': 3,
|
||||||
|
'chapuser': '*',
|
||||||
|
'initiatorgrp': {'initiator-list':
|
||||||
|
[{'wwn': '1000000000000000'}]},
|
||||||
|
'sid': 'a9b9aba7'})]
|
||||||
|
self.mock_client_service.assert_has_calls(
|
||||||
|
self.mock_client_service.method_calls,
|
||||||
|
expected_calls)
|
||||||
|
|
||||||
@mock.patch(NIMBLE_URLLIB2)
|
@mock.patch(NIMBLE_URLLIB2)
|
||||||
@mock.patch(NIMBLE_CLIENT)
|
@mock.patch(NIMBLE_CLIENT)
|
||||||
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
|
||||||
|
@ -18,6 +18,7 @@ Volume driver for Nimble Storage.
|
|||||||
This driver supports Nimble Storage controller CS-Series.
|
This driver supports Nimble Storage controller CS-Series.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import abc
|
||||||
import functools
|
import functools
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
@ -37,11 +38,13 @@ from cinder import exception
|
|||||||
from cinder.i18n import _, _LE, _LI, _LW
|
from cinder.i18n import _, _LE, _LI, _LW
|
||||||
from cinder import interface
|
from cinder import interface
|
||||||
from cinder.objects import volume
|
from cinder.objects import volume
|
||||||
|
from cinder.volume import driver
|
||||||
from cinder.volume.drivers.san import san
|
from cinder.volume.drivers.san import san
|
||||||
from cinder.volume import volume_types
|
from cinder.volume import volume_types
|
||||||
|
from cinder.zonemanager import utils as fczm_utils
|
||||||
|
|
||||||
|
|
||||||
DRIVER_VERSION = '3.0.0'
|
DRIVER_VERSION = '3.1.0'
|
||||||
AES_256_XTS_CIPHER = 2
|
AES_256_XTS_CIPHER = 2
|
||||||
DEFAULT_CIPHER = 3
|
DEFAULT_CIPHER = 3
|
||||||
EXTRA_SPEC_ENCRYPTION = 'nimble:encryption'
|
EXTRA_SPEC_ENCRYPTION = 'nimble:encryption'
|
||||||
@ -63,6 +66,8 @@ SM_SUBNET_DATA = 3
|
|||||||
SM_SUBNET_MGMT_PLUS_DATA = 4
|
SM_SUBNET_MGMT_PLUS_DATA = 4
|
||||||
LUN_ID = '0'
|
LUN_ID = '0'
|
||||||
WARN_LEVEL = 0.8
|
WARN_LEVEL = 0.8
|
||||||
|
ISCSI_TARGET_PORT = ':3260'
|
||||||
|
FIBRE_CHANNEL_PROTOCOL = 2
|
||||||
|
|
||||||
# Work around for ubuntu_openssl_bug_965371. Python soap client suds
|
# Work around for ubuntu_openssl_bug_965371. Python soap client suds
|
||||||
# throws the error ssl-certificate-verify-failed-error, workaround to disable
|
# throws the error ssl-certificate-verify-failed-error, workaround to disable
|
||||||
@ -93,9 +98,7 @@ class NimbleAPIException(exception.VolumeBackendAPIException):
|
|||||||
message = _("Unexpected response from Nimble API")
|
message = _("Unexpected response from Nimble API")
|
||||||
|
|
||||||
|
|
||||||
@interface.volumedriver
|
class NimbleBaseVolumeDriver(san.SanDriver):
|
||||||
class NimbleISCSIDriver(san.SanISCSIDriver):
|
|
||||||
|
|
||||||
"""OpenStack driver to enable Nimble Controller.
|
"""OpenStack driver to enable Nimble Controller.
|
||||||
|
|
||||||
Version history:
|
Version history:
|
||||||
@ -111,6 +114,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
2.0.1 - Added multi-initiator support through extra-specs
|
2.0.1 - Added multi-initiator support through extra-specs
|
||||||
2.0.2 - Fixed supporting extra specs while cloning vols
|
2.0.2 - Fixed supporting extra specs while cloning vols
|
||||||
3.0.0 - Newton Support for Force Backup
|
3.0.0 - Newton Support for Force Backup
|
||||||
|
3.1.0 - Fibre Channel Support
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = DRIVER_VERSION
|
VERSION = DRIVER_VERSION
|
||||||
@ -119,9 +123,10 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
CI_WIKI_NAME = "Nimble_Storage_CI"
|
CI_WIKI_NAME = "Nimble_Storage_CI"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(NimbleISCSIDriver, self).__init__(*args, **kwargs)
|
super(NimbleBaseVolumeDriver, self).__init__(*args, **kwargs)
|
||||||
self.APIExecutor = None
|
self.APIExecutor = None
|
||||||
self.group_stats = {}
|
self.group_stats = {}
|
||||||
|
self._storage_protocol = None
|
||||||
self.configuration.append_config_values(nimble_opts)
|
self.configuration.append_config_values(nimble_opts)
|
||||||
|
|
||||||
def _check_config(self):
|
def _check_config(self):
|
||||||
@ -132,43 +137,6 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
raise exception.InvalidInput(reason=_('%s is not set.') %
|
raise exception.InvalidInput(reason=_('%s is not set.') %
|
||||||
attr)
|
attr)
|
||||||
|
|
||||||
def _get_discovery_ip(self, netconfig):
|
|
||||||
"""Get discovery ip."""
|
|
||||||
subnet_label = self.configuration.nimble_subnet_label
|
|
||||||
LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s',
|
|
||||||
{'netlabel': subnet_label, 'netconf': netconfig})
|
|
||||||
ret_discovery_ip = ''
|
|
||||||
for subnet in netconfig['subnet-list']:
|
|
||||||
LOG.info(_LI('Exploring array subnet label %s'), subnet['label'])
|
|
||||||
if subnet_label == '*':
|
|
||||||
# Use the first data subnet, save mgmt+data for later
|
|
||||||
if subnet['subnet-id']['type'] == SM_SUBNET_DATA:
|
|
||||||
LOG.info(_LI('Discovery ip %(disc_ip)s is used '
|
|
||||||
'on data subnet %(net_label)s'),
|
|
||||||
{'disc_ip': subnet['discovery-ip'],
|
|
||||||
'net_label': subnet['label']})
|
|
||||||
return subnet['discovery-ip']
|
|
||||||
elif (subnet['subnet-id']['type'] ==
|
|
||||||
SM_SUBNET_MGMT_PLUS_DATA):
|
|
||||||
LOG.info(_LI('Discovery ip %(disc_ip)s is found'
|
|
||||||
' on mgmt+data subnet %(net_label)s'),
|
|
||||||
{'disc_ip': subnet['discovery-ip'],
|
|
||||||
'net_label': subnet['label']})
|
|
||||||
ret_discovery_ip = subnet['discovery-ip']
|
|
||||||
# If subnet is specified and found, use the subnet
|
|
||||||
elif subnet_label == subnet['label']:
|
|
||||||
LOG.info(_LI('Discovery ip %(disc_ip)s is used'
|
|
||||||
' on subnet %(net_label)s'),
|
|
||||||
{'disc_ip': subnet['discovery-ip'],
|
|
||||||
'net_label': subnet['label']})
|
|
||||||
return subnet['discovery-ip']
|
|
||||||
if ret_discovery_ip:
|
|
||||||
LOG.info(_LI('Discovery ip %s is used on mgmt+data subnet'),
|
|
||||||
ret_discovery_ip)
|
|
||||||
return ret_discovery_ip
|
|
||||||
else:
|
|
||||||
raise NimbleDriverException(_('No suitable discovery ip found'))
|
|
||||||
|
|
||||||
def _update_existing_vols_agent_type(self, context):
|
def _update_existing_vols_agent_type(self, context):
|
||||||
LOG.debug("Updating existing volumes to have "
|
LOG.debug("Updating existing volumes to have "
|
||||||
"agent_type = 'OPENSTACK'")
|
"agent_type = 'OPENSTACK'")
|
||||||
@ -200,24 +168,18 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
raise
|
raise
|
||||||
self._update_existing_vols_agent_type(context)
|
self._update_existing_vols_agent_type(context)
|
||||||
|
|
||||||
def _get_provider_location(self, volume_name):
|
|
||||||
"""Get volume iqn for initiator access."""
|
|
||||||
vol_info = self.APIExecutor.get_vol_info(volume_name)
|
|
||||||
iqn = vol_info['target-name']
|
|
||||||
netconfig = self.APIExecutor.get_netconfig('active')
|
|
||||||
target_ipaddr = self._get_discovery_ip(netconfig)
|
|
||||||
iscsi_portal = target_ipaddr + ':3260'
|
|
||||||
provider_location = '%s %s %s' % (iscsi_portal, iqn, LUN_ID)
|
|
||||||
LOG.info(_LI('vol_name=%(name)s provider_location=%(loc)s'),
|
|
||||||
{'name': volume_name, 'loc': provider_location})
|
|
||||||
return provider_location
|
|
||||||
|
|
||||||
def _get_model_info(self, volume_name):
|
def _get_model_info(self, volume_name):
|
||||||
"""Get model info for the volume."""
|
"""Get model info for the volume."""
|
||||||
return (
|
return (
|
||||||
{'provider_location': self._get_provider_location(volume_name),
|
{'provider_location': self._get_provider_location(volume_name),
|
||||||
'provider_auth': None})
|
'provider_auth': None})
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _get_provider_location(self, volume_name):
|
||||||
|
"""Volume info for iSCSI and FC"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
def create_volume(self, volume):
|
def create_volume(self, volume):
|
||||||
"""Create a new volume."""
|
"""Create a new volume."""
|
||||||
reserve = not self.configuration.san_thin_provision
|
reserve = not self.configuration.san_thin_provision
|
||||||
@ -368,7 +330,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
self.group_stats = {'volume_backend_name': backend_name,
|
self.group_stats = {'volume_backend_name': backend_name,
|
||||||
'vendor_name': 'Nimble',
|
'vendor_name': 'Nimble',
|
||||||
'driver_version': DRIVER_VERSION,
|
'driver_version': DRIVER_VERSION,
|
||||||
'storage_protocol': 'iSCSI'}
|
'storage_protocol': self._storage_protocol}
|
||||||
# Just use a single pool for now, FIXME to support multiple
|
# Just use a single pool for now, FIXME to support multiple
|
||||||
# pools
|
# pools
|
||||||
single_pool = dict(
|
single_pool = dict(
|
||||||
@ -491,15 +453,6 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
self.APIExecutor.online_vol(vol_name, False, ignore_list=[
|
self.APIExecutor.online_vol(vol_name, False, ignore_list=[
|
||||||
'SM-enoent'])
|
'SM-enoent'])
|
||||||
|
|
||||||
def _create_igroup_for_initiator(self, initiator_name):
|
|
||||||
"""Creates igroup for an initiator and returns the igroup name."""
|
|
||||||
igrp_name = 'openstack-' + self._generate_random_string(12)
|
|
||||||
LOG.info(_LI('Creating initiator group %(grp)s '
|
|
||||||
'with initiator %(iname)s'),
|
|
||||||
{'grp': igrp_name, 'iname': initiator_name})
|
|
||||||
self.APIExecutor.create_initiator_group(igrp_name, initiator_name)
|
|
||||||
return igrp_name
|
|
||||||
|
|
||||||
def _get_igroupname_for_initiator(self, initiator_name):
|
def _get_igroupname_for_initiator(self, initiator_name):
|
||||||
initiator_groups = self.APIExecutor.get_initiator_grp_list()
|
initiator_groups = self.APIExecutor.get_initiator_grp_list()
|
||||||
for initiator_group in initiator_groups:
|
for initiator_group in initiator_groups:
|
||||||
@ -515,6 +468,51 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
LOG.info(_LI('No igroup found for initiator %s'), initiator_name)
|
LOG.info(_LI('No igroup found for initiator %s'), initiator_name)
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
@interface.volumedriver
|
||||||
|
class NimbleISCSIDriver(NimbleBaseVolumeDriver, san.SanISCSIDriver):
|
||||||
|
"""OpenStack driver to enable Nimble ISCSI Controller."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(NimbleISCSIDriver, self).__init__(*args, **kwargs)
|
||||||
|
self._storage_protocol = "iSCSI"
|
||||||
|
|
||||||
|
def _get_discovery_ip(self, netconfig):
|
||||||
|
"""Get discovery ip."""
|
||||||
|
subnet_label = self.configuration.nimble_subnet_label
|
||||||
|
LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s',
|
||||||
|
{'netlabel': subnet_label, 'netconf': netconfig})
|
||||||
|
ret_discovery_ip = ''
|
||||||
|
for subnet in netconfig['subnet-list']:
|
||||||
|
LOG.info(_LI('Exploring array subnet label %s'), subnet['label'])
|
||||||
|
if subnet_label == '*':
|
||||||
|
# Use the first data subnet, save mgmt+data for later
|
||||||
|
if subnet['subnet-id']['type'] == SM_SUBNET_DATA:
|
||||||
|
LOG.info(_LI('Discovery ip %(disc_ip)s is used '
|
||||||
|
'on data subnet %(net_label)s'),
|
||||||
|
{'disc_ip': subnet['discovery-ip'],
|
||||||
|
'net_label': subnet['label']})
|
||||||
|
return subnet['discovery-ip']
|
||||||
|
elif (subnet['subnet-id']['type'] ==
|
||||||
|
SM_SUBNET_MGMT_PLUS_DATA):
|
||||||
|
LOG.info(_LI('Discovery ip %(disc_ip)s is found'
|
||||||
|
' on mgmt+data subnet %(net_label)s'),
|
||||||
|
{'disc_ip': subnet['discovery-ip'],
|
||||||
|
'net_label': subnet['label']})
|
||||||
|
ret_discovery_ip = subnet['discovery-ip']
|
||||||
|
# If subnet is specified and found, use the subnet
|
||||||
|
elif subnet_label == subnet['label']:
|
||||||
|
LOG.info(_LI('Discovery ip %(disc_ip)s is used'
|
||||||
|
' on subnet %(net_label)s'),
|
||||||
|
{'disc_ip': subnet['discovery-ip'],
|
||||||
|
'net_label': subnet['label']})
|
||||||
|
return subnet['discovery-ip']
|
||||||
|
if ret_discovery_ip:
|
||||||
|
LOG.info(_LI('Discovery ip %s is used on mgmt+data subnet'),
|
||||||
|
ret_discovery_ip)
|
||||||
|
return ret_discovery_ip
|
||||||
|
raise NimbleDriverException(_('No suitable discovery ip found'))
|
||||||
|
|
||||||
def initialize_connection(self, volume, connector):
|
def initialize_connection(self, volume, connector):
|
||||||
"""Driver entry point to attach a volume to an instance."""
|
"""Driver entry point to attach a volume to an instance."""
|
||||||
LOG.info(_LI('Entering initialize_connection volume=%(vol)s'
|
LOG.info(_LI('Entering initialize_connection volume=%(vol)s'
|
||||||
@ -526,7 +524,7 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
initiator_group_name = self._get_igroupname_for_initiator(
|
initiator_group_name = self._get_igroupname_for_initiator(
|
||||||
initiator_name)
|
initiator_name)
|
||||||
if not initiator_group_name:
|
if not initiator_group_name:
|
||||||
initiator_group_name = self._create_igroup_for_initiator(
|
initiator_group_name = self._create_igroup_for_initiator_iscsi(
|
||||||
initiator_name)
|
initiator_name)
|
||||||
LOG.info(_LI('Initiator group name is %(grp)s for initiator '
|
LOG.info(_LI('Initiator group name is %(grp)s for initiator '
|
||||||
'%(iname)s'),
|
'%(iname)s'),
|
||||||
@ -560,6 +558,203 @@ class NimbleISCSIDriver(san.SanISCSIDriver):
|
|||||||
initiator_name)
|
initiator_name)
|
||||||
self.APIExecutor.remove_acl(volume, initiator_group_name)
|
self.APIExecutor.remove_acl(volume, initiator_group_name)
|
||||||
|
|
||||||
|
def _create_igroup_for_initiator_iscsi(self, initiator_name):
|
||||||
|
"""Creates igroup for an initiator and returns the igroup name."""
|
||||||
|
igrp_name = 'openstack-' + self._generate_random_string(12)
|
||||||
|
LOG.info(_LI('Creating initiator group %(grp)s '
|
||||||
|
'with initiator %(iname)s'),
|
||||||
|
{'grp': igrp_name, 'iname': initiator_name})
|
||||||
|
self.APIExecutor.create_initiator_group(igrp_name, initiator_name)
|
||||||
|
return igrp_name
|
||||||
|
|
||||||
|
def _get_provider_location(self, volume_name):
|
||||||
|
"""Get volume iqn for initiator access."""
|
||||||
|
vol_info = self.APIExecutor.get_vol_info(volume_name)
|
||||||
|
iqn = vol_info['target-name']
|
||||||
|
netconfig = self.APIExecutor.get_netconfig('active')
|
||||||
|
target_ipaddr = self._get_discovery_ip(netconfig)
|
||||||
|
iscsi_portal = target_ipaddr + ISCSI_TARGET_PORT
|
||||||
|
provider_location = '%s %s %s' % (iscsi_portal, iqn, LUN_ID)
|
||||||
|
LOG.info(_LI('vol_name=%(name)s provider_location=%(loc)s'),
|
||||||
|
{'name': volume_name, 'loc': provider_location})
|
||||||
|
return provider_location
|
||||||
|
|
||||||
|
|
||||||
|
@interface.volumedriver
|
||||||
|
class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver):
|
||||||
|
"""OpenStack driver to enable Nimble FC Driver Controller."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(NimbleFCDriver, self).__init__(*args, **kwargs)
|
||||||
|
self._storage_protocol = "FC"
|
||||||
|
self._lookup_service = fczm_utils.create_lookup_service()
|
||||||
|
|
||||||
|
def _get_provider_location(self, volume_name):
|
||||||
|
"""Get array info wwn details."""
|
||||||
|
netconfig = self.APIExecutor.get_netconfig('active')
|
||||||
|
array_name = netconfig['array-list'][0]['array-name']
|
||||||
|
provider_location = '%s' % (array_name)
|
||||||
|
LOG.info(_LI('vol_name=%(name)s provider_location=%(loc)s'),
|
||||||
|
{'name': volume_name, 'loc': provider_location})
|
||||||
|
return provider_location
|
||||||
|
|
||||||
|
def _build_initiator_target_map(self, target_wwns, connector):
|
||||||
|
"""Build the target_wwns and the initiator target map."""
|
||||||
|
init_targ_map = {}
|
||||||
|
|
||||||
|
if self._lookup_service:
|
||||||
|
# use FC san lookup to determine which wwpns to use
|
||||||
|
# for the new VLUN.
|
||||||
|
dev_map = self._lookup_service.get_device_mapping_from_network(
|
||||||
|
connector['wwpns'],
|
||||||
|
target_wwns)
|
||||||
|
map_fabric = dev_map
|
||||||
|
LOG.info(_LI("dev_map =%(fabric)s"), {'fabric': map_fabric})
|
||||||
|
|
||||||
|
for fabric_name in dev_map:
|
||||||
|
fabric = dev_map[fabric_name]
|
||||||
|
for initiator in fabric['initiator_port_wwn_list']:
|
||||||
|
if initiator not in init_targ_map:
|
||||||
|
init_targ_map[initiator] = []
|
||||||
|
init_targ_map[initiator] += fabric['target_port_wwn_list']
|
||||||
|
init_targ_map[initiator] = list(set(
|
||||||
|
init_targ_map[initiator]))
|
||||||
|
else:
|
||||||
|
init_targ_map = dict.fromkeys(connector["wwpns"], target_wwns)
|
||||||
|
|
||||||
|
return init_targ_map
|
||||||
|
|
||||||
|
@fczm_utils.AddFCZone
|
||||||
|
def initialize_connection(self, volume, connector):
|
||||||
|
"""Driver entry point to attach a volume to an instance."""
|
||||||
|
LOG.info(_LI('Entering initialize_connection volume=%(vol)s'
|
||||||
|
' connector=%(conn)s location=%(loc)s'),
|
||||||
|
{'vol': volume,
|
||||||
|
'conn': connector,
|
||||||
|
'loc': volume['provider_location']})
|
||||||
|
wwpns = []
|
||||||
|
initiator_name = connector['initiator']
|
||||||
|
for wwpn in connector['wwpns']:
|
||||||
|
wwpns.append(wwpn)
|
||||||
|
initiator_group_name = self._get_igroupname_for_initiator_fc(wwpns)
|
||||||
|
|
||||||
|
if not initiator_group_name:
|
||||||
|
initiator_group_name = self._create_igroup_for_initiator_fc(
|
||||||
|
initiator_name, wwpns)
|
||||||
|
|
||||||
|
LOG.info(_LI('Initiator group name is %(grp)s for initiator '
|
||||||
|
'%(iname)s'),
|
||||||
|
{'grp': initiator_group_name, 'iname': initiator_name})
|
||||||
|
self.APIExecutor.add_acl(volume, initiator_group_name)
|
||||||
|
|
||||||
|
lun = self.get_lun_number(volume, initiator_group_name)
|
||||||
|
init_targ_map = {}
|
||||||
|
array_name = volume['provider_location'].split()
|
||||||
|
|
||||||
|
target_wwns = self.get_wwpns_from_array(array_name)
|
||||||
|
|
||||||
|
init_targ_map = self._build_initiator_target_map(target_wwns,
|
||||||
|
connector)
|
||||||
|
|
||||||
|
data = {'driver_volume_type': 'fibre_channel',
|
||||||
|
'data': {'target_lun': lun,
|
||||||
|
'target_discovered': True,
|
||||||
|
'target_wwn': target_wwns,
|
||||||
|
'initiator_target_map': init_targ_map}}
|
||||||
|
|
||||||
|
LOG.info(_LI("Return FC data for zone addition: %(data)s."),
|
||||||
|
{'data': data})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@fczm_utils.RemoveFCZone
|
||||||
|
def terminate_connection(self, volume, connector, **kwargs):
|
||||||
|
"""Driver entry point to unattach a volume from an instance."""
|
||||||
|
LOG.info(_LI('Entering terminate_connection volume=%(vol)s'
|
||||||
|
' connector=%(conn)s location=%(loc)s.'),
|
||||||
|
{'vol': volume,
|
||||||
|
'conn': connector,
|
||||||
|
'loc': volume['provider_location']})
|
||||||
|
|
||||||
|
wwpns = []
|
||||||
|
initiator_name = connector['initiator']
|
||||||
|
for wwpn in connector['wwpns']:
|
||||||
|
wwpns.append(wwpn)
|
||||||
|
init_targ_map = {}
|
||||||
|
array_name = volume['provider_location'].split()
|
||||||
|
target_wwns = self.get_wwpns_from_array(array_name)
|
||||||
|
init_targ_map = self._build_initiator_target_map(target_wwns,
|
||||||
|
connector)
|
||||||
|
initiator_group_name = self._get_igroupname_for_initiator_fc(wwpns)
|
||||||
|
if not initiator_group_name:
|
||||||
|
raise NimbleDriverException(
|
||||||
|
_('No initiator group found for initiator %s') %
|
||||||
|
initiator_name)
|
||||||
|
LOG.debug("initiator_target_map".format(init_targ_map))
|
||||||
|
self.APIExecutor.remove_acl(volume, initiator_group_name)
|
||||||
|
# FIXME to check for other volumes attached to the host and then
|
||||||
|
# return the data. Bug https://bugs.launchpad.net/cinder/+bug/1617472
|
||||||
|
|
||||||
|
data = {'driver_volume_type': 'fibre_channel',
|
||||||
|
'data': {}}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _get_igroupname_for_initiator_fc(self, initiator_wwpns):
|
||||||
|
initiator_groups = self.APIExecutor.get_initiator_grp_list()
|
||||||
|
for initiator_group in initiator_groups:
|
||||||
|
if 'initiator-list' in initiator_group:
|
||||||
|
wwpns_list = []
|
||||||
|
for initiator in initiator_group['initiator-list']:
|
||||||
|
wwpn = str(initiator['wwpn']).replace(":", "")
|
||||||
|
wwpns_list.append(wwpn)
|
||||||
|
LOG.debug("initiator_wwpns= %s wwpns_list_from_array=%s",
|
||||||
|
initiator_wwpns, wwpns_list)
|
||||||
|
if set(initiator_wwpns) == set(wwpns_list):
|
||||||
|
LOG.info(_LI('igroup %(grp)s found for '
|
||||||
|
'initiator %(wwpns_list)s'),
|
||||||
|
{'grp': initiator_group['name'],
|
||||||
|
'wwpns_list': wwpns_list})
|
||||||
|
return initiator_group['name']
|
||||||
|
LOG.info(_LI('No igroup found for initiator %s'), initiator_wwpns)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_lun_number(self, volume, initiator_group_name):
|
||||||
|
vol_info = self.APIExecutor.get_vol_info(volume['name'])
|
||||||
|
for acl in vol_info['aclList']:
|
||||||
|
if (initiator_group_name == acl['initiatorgrp']):
|
||||||
|
LOG.info(_LI("acllist =%(aclList)s"),
|
||||||
|
{'aclList': vol_info['aclList']})
|
||||||
|
lun = vol_info['aclList'][0]['lun']
|
||||||
|
return lun
|
||||||
|
|
||||||
|
def _create_igroup_for_initiator_fc(self, initiator_name, wwpn_list):
|
||||||
|
"""Creates igroup for an initiator and returns the igroup name."""
|
||||||
|
igrp_name = 'openstack-' + self._generate_random_string(12)
|
||||||
|
LOG.info(_LI('Creating initiator group %(grp)s '
|
||||||
|
'with initiator %(iname)s'),
|
||||||
|
{'grp': igrp_name, 'iname': initiator_name})
|
||||||
|
self.APIExecutor.create_initiator_group_fc(igrp_name, initiator_name,
|
||||||
|
wwpn_list)
|
||||||
|
return igrp_name
|
||||||
|
|
||||||
|
def get_wwpns_from_array(self, array_name):
|
||||||
|
"""Retrieve the wwpns from the array"""
|
||||||
|
target_wwpns = []
|
||||||
|
interface_info = self.APIExecutor.get_fc_interface_list(array_name)
|
||||||
|
|
||||||
|
for wwpn_list in interface_info['ctrlr-a-interface-list']:
|
||||||
|
wwpn = wwpn_list['wwpn']
|
||||||
|
wwpn = wwpn.replace(":", "")
|
||||||
|
target_wwpns.append(wwpn)
|
||||||
|
|
||||||
|
for wwpn_list in interface_info['ctrlr-b-interface-list']:
|
||||||
|
wwpn = wwpn_list['wwpn']
|
||||||
|
wwpn = wwpn.replace(":", "")
|
||||||
|
target_wwpns.append(wwpn)
|
||||||
|
|
||||||
|
return target_wwpns
|
||||||
|
|
||||||
|
|
||||||
def _response_checker(func):
|
def _response_checker(func):
|
||||||
"""Decorator function to check if the response of an API is positive."""
|
"""Decorator function to check if the response of an API is positive."""
|
||||||
@ -607,6 +802,7 @@ class NimbleAPIExecutor(object):
|
|||||||
self.sid = None
|
self.sid = None
|
||||||
self.username = kwargs['username']
|
self.username = kwargs['username']
|
||||||
self.password = kwargs['password']
|
self.password = kwargs['password']
|
||||||
|
self.ip = kwargs['ip']
|
||||||
|
|
||||||
wsdl_url = 'https://%s/wsdl/NsGroupManagement.wsdl' % (kwargs['ip'])
|
wsdl_url = 'https://%s/wsdl/NsGroupManagement.wsdl' % (kwargs['ip'])
|
||||||
LOG.debug('Using Nimble wsdl_url: %s', wsdl_url)
|
LOG.debug('Using Nimble wsdl_url: %s', wsdl_url)
|
||||||
@ -982,6 +1178,29 @@ class NimbleAPIExecutor(object):
|
|||||||
return (response['initiatorgrp-list']
|
return (response['initiatorgrp-list']
|
||||||
if 'initiatorgrp-list' in response else [])
|
if 'initiatorgrp-list' in response else [])
|
||||||
|
|
||||||
|
@_connection_checker
|
||||||
|
@_response_checker
|
||||||
|
def create_initiator_group_fc(self, initiator_group_name, initiator_name,
|
||||||
|
wwpn_list):
|
||||||
|
"""Execute createInitiatorGrp API for Fibre Channel"""
|
||||||
|
LOG.info(_LI('Creating initiator group %(igrp)s'
|
||||||
|
' with wwpn %(wwpn)s'),
|
||||||
|
{'igrp': initiator_group_name, 'wwpn': wwpn_list})
|
||||||
|
request = {}
|
||||||
|
request['sid'] = self.sid
|
||||||
|
request['attr'] = {}
|
||||||
|
request['attr']['name'] = initiator_group_name
|
||||||
|
request['attr']['access-protocol'] = FIBRE_CHANNEL_PROTOCOL
|
||||||
|
request['attr']['initiator-list'] = []
|
||||||
|
for wwpn in wwpn_list:
|
||||||
|
initiator = {}
|
||||||
|
initiator['access-protocol'] = FIBRE_CHANNEL_PROTOCOL
|
||||||
|
initiator['wwpn'] = wwpn
|
||||||
|
request['attr']['initiator-list'].append(initiator)
|
||||||
|
LOG.debug("createInitiatorGrp request %s", request)
|
||||||
|
|
||||||
|
return self.client.service.createInitiatorGrp(request=request)
|
||||||
|
|
||||||
@_connection_checker
|
@_connection_checker
|
||||||
@_response_checker
|
@_response_checker
|
||||||
def create_initiator_group(self, initiator_group_name, initiator_name):
|
def create_initiator_group(self, initiator_group_name, initiator_name):
|
||||||
@ -1003,3 +1222,11 @@ class NimbleAPIExecutor(object):
|
|||||||
return self.client.service.deleteInitiatorGrp(
|
return self.client.service.deleteInitiatorGrp(
|
||||||
request={'sid': self.sid,
|
request={'sid': self.sid,
|
||||||
'name': initiator_group_name})
|
'name': initiator_group_name})
|
||||||
|
|
||||||
|
@_connection_checker
|
||||||
|
@_response_checker
|
||||||
|
def get_fc_interface_list(self, array_name):
|
||||||
|
"""getFibreChannelInterfaceList API to get FC interfaces on array"""
|
||||||
|
return self.client.service.getFibreChannelInterfaceList(
|
||||||
|
request={'sid': self.sid,
|
||||||
|
'name-or-serial': array_name})
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added Nimble Storage Fibre Channel backend driver.
|
Loading…
Reference in New Issue
Block a user