Merge "Add multiattach in Nimble driver"

This commit is contained in:
Zuul 2020-09-04 03:27:36 +00:00 committed by Gerrit Code Review
commit 0ca496fc48
5 changed files with 279 additions and 40 deletions

View File

@ -17,12 +17,14 @@
import sys
from unittest import mock
from oslo_utils import uuidutils
from six.moves import http_client
from cinder import context
from cinder import exception
from cinder.objects import volume as obj_volume
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import fake_volume
from cinder.tests.unit import test
from cinder.volume.drivers import nimble
from cinder.volume import volume_types
@ -83,6 +85,9 @@ FAKE_CREATE_VOLUME_POSITIVE_RESPONSE_QOS = {
'clone': False,
'name': "testvolume-qos"}
FAKE_EXTRA_SPECS = {'multiattach': '<is> True',
'nimble:iops-limit': '1024'}
FAKE_GET_VOL_INFO_RESPONSE = {'name': 'testvolume',
'clone': False,
'target_name': 'iqn.test',
@ -99,6 +104,13 @@ FAKE_GET_VOL_INFO_ONLINE = {'name': 'testvolume',
'online': True,
'agent_type': 'none'}
FAKE_GET_VOL_INFO_RETYPE = {'name': 'testvolume',
'size': 2048,
'online': True,
'agent_type': 'none',
'pool_id': 'none',
'pool_name': 'none'}
FAKE_GET_VOL_INFO_BACKUP_RESPONSE = {'name': 'testvolume',
'clone': True,
'target_name': 'iqn.test',
@ -250,6 +262,26 @@ class NimbleDriverBaseTestCase(test.TestCase):
return inner_client_mock
return client_mock_wrapper
@staticmethod
def client_mock_decorator_nimble_api(username, password, ip, verify):
def client_mock_wrapper(func):
def inner_client_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.return_value = (
self.mock_client_service)
self.driver = nimble.NimbleRestAPIExecutor(
username=username, password=password, ip=ip, verify=verify)
mock_login_response = mock_urllib2.post.return_value
mock_login_response = mock.MagicMock()
mock_login_response.status_code.return_value = http_client.OK
mock_login_response.json.return_value = (
FAKE_LOGIN_POST_RESPONSE)
func(self, *args, **kwargs)
return inner_client_mock
return client_mock_wrapper
class NimbleDriverLoginTestCase(NimbleDriverBaseTestCase):
@ -371,7 +403,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
mock.Mock(type_id=FAKE_TYPE_ID, return_value={
'nimble:perfpol-name': 'default',
'nimble:encryption': 'yes',
'nimble:multi-initiator': 'false'}))
'multiattach': 'false'}))
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_create_volume_encryption_positive(self):
@ -412,7 +444,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
mock.Mock(type_id=FAKE_TYPE_ID, return_value={
'nimble:perfpol-name': 'VMware ESX',
'nimble:encryption': 'no',
'nimble:multi-initiator': 'false'}))
'multiattach': 'false'}))
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_create_volume_perfpolicy_positive(self):
@ -452,7 +484,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
mock.Mock(type_id=FAKE_TYPE_ID, return_value={
'nimble:perfpol-name': 'default',
'nimble:encryption': 'no',
'nimble:multi-initiator': 'true'}))
'multiattach': 'true'}))
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_create_volume_multi_initiator_positive(self):
@ -573,7 +605,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
mock.Mock(type_id=FAKE_TYPE_ID, return_value={
'nimble:perfpol-name': 'default',
'nimble:encryption': 'no',
'nimble:multi-initiator': 'true'}))
'multiattach': 'false'}))
def test_create_volume_negative(self):
self.mock_client_service.get_vol_info.side_effect = (
FAKE_CREATE_VOLUME_NEGATIVE_RESPONSE)
@ -761,7 +793,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
return_value={
'nimble:perfpol-name': 'default',
'nimble:encryption': 'yes',
'nimble:multi-initiator': 'false',
'multiattach': False,
'nimble:iops-limit': '1024'}))
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*', False))
@ -979,7 +1011,7 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
'extra_specs':
{'nimble:perfpol-name': 'default',
'nimble:encryption': 'yes',
'nimble:multi-initiator': 'false',
'multiattach': False,
'nimble:iops-limit': '1024'}}))
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
@ -994,6 +1026,27 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
self.assertTrue(retype)
self.assertIsNone(update)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_ISCSI_DRIVER)
@mock.patch.object(nimble.NimbleRestAPIExecutor, 'login')
@mock.patch.object(nimble.NimbleRestAPIExecutor,
'get_performance_policy_id')
@mock.patch.object(nimble.NimbleRestAPIExecutor, 'get_pool_info')
@mock.patch.object(nimble.NimbleRestAPIExecutor, 'get_folder_id')
@NimbleDriverBaseTestCase.client_mock_decorator_nimble_api(
'nimble', 'nimble_pass', '10.18.108.55', 'False')
def test_nimble_extraspecs_retype(self, mock_folder,
mock_pool, mock_perf_id,
mock_login):
mock_folder.return_value = None
mock_pool.return_value = None
mock_perf_id.return_value = None
mock_login.return_value = None
data = self.driver.get_valid_nimble_extraspecs(
FAKE_EXTRA_SPECS,
FAKE_GET_VOL_INFO_RETYPE)
self.assertTrue(data['multi_initiator'])
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
@ -1011,7 +1064,8 @@ class NimbleDriverVolumeTestCase(NimbleDriverBaseTestCase):
'total_capacity_gb': 7466.30419921875,
'free_capacity_gb': 7463.567649364471,
'reserved_percentage': 0,
'QoS_support': False}]}
'QoS_support': False,
'multiattach': True}]}
self.assertEqual(
expected_res,
self.driver.get_volume_stats(refresh=True))
@ -1095,7 +1149,7 @@ class NimbleDriverSnapshotTestCase(NimbleDriverBaseTestCase):
mock.Mock(type_id=FAKE_TYPE_ID, return_value={
'nimble:perfpol-name': 'default',
'nimble:encryption': 'yes',
'nimble:multi-initiator': 'false'}))
'multiattach': False}))
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_create_volume_from_snapshot(self):
@ -1361,10 +1415,15 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
def test_terminate_connection_positive(self):
self.mock_client_service.get_initiator_grp_list.return_value = (
FAKE_IGROUP_LIST_RESPONSE)
ctx = context.get_admin_context()
volume = fake_volume.fake_volume_obj(
ctx, name='test-volume',
host='fakehost@nimble#Openstack',
provider_location='12 13',
id=12, multiattach=False)
self.driver.terminate_connection(
{'name': 'test-volume',
'provider_location': '12 13',
'id': 12},
volume,
{'initiator': 'test-initiator1'})
expected_calls = [mock.call._get_igroupname_for_initiator(
'test-initiator1'),
@ -1406,10 +1465,15 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
mock_wwpns.return_value = ["1111111111111101"]
self.mock_client_service.get_initiator_grp_list.return_value = (
FAKE_IGROUP_LIST_RESPONSE_FC)
ctx = context.get_admin_context()
volume = fake_volume.fake_volume_obj(
ctx, name='test-volume',
host='fakehost@nimble#Openstack',
provider_location='12 13',
id=14, multiattach=False)
self.driver.terminate_connection(
{'name': 'test-volume',
'provider_location': 'array1',
'id': 12},
volume,
{'initiator': 'test-initiator1',
'wwpns': ['1000000000000000']})
expected_calls = [
@ -1430,9 +1494,159 @@ class NimbleDriverConnectionTestCase(NimbleDriverBaseTestCase):
def test_terminate_connection_negative(self):
self.mock_client_service.get_initiator_grp_list.return_value = (
FAKE_IGROUP_LIST_RESPONSE)
ctx = context.get_admin_context()
volume = fake_volume.fake_volume_obj(
ctx, name='test-volume',
host='fakehost@nimble#Openstack',
provider_location='12 13',
id=12, multiattach=False)
self.assertRaises(
exception.VolumeDriverException,
self.driver.terminate_connection,
{'name': 'test-volume',
'provider_location': '12 13', 'id': 12},
volume,
{'initiator': 'test-initiator3'})
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
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_negative_fc(self, mock_wwpns):
mock_wwpns.return_value = ["1111111111111101"]
self.mock_client_service.get_initiator_grp_list.return_value = (
FAKE_IGROUP_LIST_RESPONSE_FC)
ctx = context.get_admin_context()
volume = fake_volume.fake_volume_obj(
ctx, name='test-volume',
host='fakehost@nimble#Openstack',
provider_location='12 13',
id=12, multiattach=False)
self.assertRaises(
exception.VolumeDriverException,
self.driver.terminate_connection,
volume,
{'initiator': 'test-initiator3',
'wwpns': ['1000000000000010']})
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
mock.Mock(return_value=[]))
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_terminate_connection_multiattach(self):
self.mock_client_service.get_initiator_grp_list.return_value = (
FAKE_IGROUP_LIST_RESPONSE)
ctx = context.get_admin_context()
att_1 = fake_volume.volume_attachment_ovo(
ctx, id=uuidutils.generate_uuid())
att_2 = fake_volume.volume_attachment_ovo(
ctx, id=uuidutils.generate_uuid())
volume = fake_volume.fake_volume_obj(
ctx, name='test-volume',
host='fakehost@nimble#Openstack',
provider_location='12 13',
id=12, multiattach=True)
volume.volume_attachment.objects = [att_1, att_2]
self.driver.terminate_connection(
volume,
{'initiator': 'test-initiator1'})
self.mock_client_service.remove_acl.assert_not_called()
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
mock.Mock(return_value=[]))
@NimbleDriverBaseTestCase.client_mock_decorator(create_configuration(
'nimble', 'nimble_pass', '10.18.108.55', 'default', '*'))
def test_terminate_connection_multiattach_complete(self):
self.mock_client_service.get_initiator_grp_list.return_value = (
FAKE_IGROUP_LIST_RESPONSE)
ctx = context.get_admin_context()
att_1 = fake_volume.volume_attachment_ovo(
ctx, id=uuidutils.generate_uuid())
volume = fake_volume.fake_volume_obj(
ctx, name='test-volume',
host='fakehost@nimble#Openstack',
provider_location='12 13',
id=12, multiattach=True)
volume.volume_attachment.objects = [att_1]
self.driver.terminate_connection(
volume,
{'initiator': 'test-initiator1'})
expected_calls = [mock.call._get_igroupname_for_initiator(
'test-initiator1'),
mock.call.remove_acl({'name': 'test-volume'},
'test-igrp1')]
self.mock_client_service.assert_has_calls(
self.mock_client_service.method_calls,
expected_calls)
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
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_multiattach_fc(self, mock_wwpns):
mock_wwpns.return_value = ["1111111111111101"]
self.mock_client_service.get_initiator_grp_list.return_value = (
FAKE_IGROUP_LIST_RESPONSE_FC)
ctx = context.get_admin_context()
att_1 = fake_volume.volume_attachment_ovo(
ctx, id=uuidutils.generate_uuid())
att_2 = fake_volume.volume_attachment_ovo(
ctx, id=uuidutils.generate_uuid())
volume = fake_volume.fake_volume_obj(
ctx, name='test-volume',
host='fakehost@nimble#Openstack',
provider_location='12 13',
id=12, multiattach=True)
volume.volume_attachment.objects = [att_1, att_2]
self.driver.terminate_connection(
volume,
{'initiator': 'test-initiator1',
'wwpns': ['1000000000000000']})
self.mock_client_service.remove_acl.assert_not_called()
@mock.patch(NIMBLE_URLLIB2)
@mock.patch(NIMBLE_CLIENT)
@mock.patch.object(obj_volume.VolumeList, 'get_all_by_host',
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_multiattach_complete_fc(self, mock_wwpns):
mock_wwpns.return_value = ["1111111111111101"]
self.mock_client_service.get_initiator_grp_list.return_value = (
FAKE_IGROUP_LIST_RESPONSE_FC)
ctx = context.get_admin_context()
att_1 = fake_volume.volume_attachment_ovo(
ctx, id=uuidutils.generate_uuid())
volume = fake_volume.fake_volume_obj(
ctx, name='test-volume',
host='fakehost@nimble#Openstack',
provider_location='12 13',
id=12, multiattach=True)
volume.volume_attachment.objects = [att_1]
self.driver.terminate_connection(
volume,
{'initiator': 'test-initiator1',
'wwpns': ['1000000000000000']})
expected_calls = [
mock.call.get_igroupname_for_initiator_fc(
"10:00:00:00:00:00:00:00"),
mock.call.remove_acl({'name': 'test-volume'},
'test-igrp1')]
self.mock_client_service.assert_has_calls(
self.mock_client_service.method_calls,
expected_calls)

View File

@ -50,7 +50,6 @@ AES_256_XTS_CIPHER = 'aes_256_xts'
DEFAULT_CIPHER = 'none'
EXTRA_SPEC_ENCRYPTION = 'nimble:encryption'
EXTRA_SPEC_PERF_POLICY = 'nimble:perfpol-name'
EXTRA_SPEC_MULTI_INITIATOR = 'nimble:multi-initiator'
EXTRA_SPEC_DEDUPE = 'nimble:dedupe'
EXTRA_SPEC_IOPS_LIMIT = 'nimble:iops-limit'
EXTRA_SPEC_FOLDER = 'nimble:folder'
@ -58,7 +57,6 @@ DEFAULT_PERF_POLICY_SETTING = 'default'
DEFAULT_ENCRYPTION_SETTING = 'no'
DEFAULT_DEDUPE_SETTING = 'false'
DEFAULT_IOPS_LIMIT_SETTING = None
DEFAULT_MULTI_INITIATOR_SETTING = 'false'
DEFAULT_FOLDER_SETTING = None
DEFAULT_SNAP_QUOTA = sys.maxsize
BACKUP_VOL_PREFIX = 'backup-vol-'
@ -393,7 +391,8 @@ class NimbleBaseVolumeDriver(san.SanDriver):
total_capacity_gb=total_capacity,
free_capacity_gb=free_space,
reserved_percentage=0,
QoS_support=False)
QoS_support=False,
multiattach=True)
self.group_stats['pools'] = [single_pool]
return self.group_stats
@ -721,6 +720,24 @@ class NimbleBaseVolumeDriver(san.SanDriver):
{'vol': volume['name'],
'igroup': initiator_group_name})
def _is_multiattach(self, volume):
if volume.multiattach:
attachment_list = volume.volume_attachment
try:
attachment_list = attachment_list.objects
except AttributeError:
pass
if attachment_list is not None and len(attachment_list) > 1:
LOG.info("Volume %(volume)s is attached to multiple "
"instances on host %(host_name)s, "
"skip terminate volume connection",
{'volume': volume.name,
'host_name': volume.host.split('@')[0]})
return True
else:
return False
@interface.volumedriver
class NimbleISCSIDriver(NimbleBaseVolumeDriver, san.SanISCSIDriver):
@ -799,6 +816,8 @@ class NimbleISCSIDriver(NimbleBaseVolumeDriver, san.SanISCSIDriver):
volume)
self.APIExecutor.remove_all_acls(volume)
return
if self._is_multiattach(volume):
return
initiator_name = connector['initiator']
initiator_group_name = self._get_igroupname_for_initiator(
@ -996,6 +1015,8 @@ class NimbleFCDriver(NimbleBaseVolumeDriver, driver.FibreChannelDriver):
volume)
self.APIExecutor.remove_all_acls(volume)
return
if self._is_multiattach(volume):
return
initiator_name = connector['initiator']
for wwpn in connector['wwpns']:
@ -1161,8 +1182,6 @@ class NimbleRestAPIExecutor(object):
DEFAULT_PERF_POLICY_SETTING)
encryption = extra_specs.get(EXTRA_SPEC_ENCRYPTION,
DEFAULT_ENCRYPTION_SETTING)
multi_initiator = extra_specs.get(EXTRA_SPEC_MULTI_INITIATOR,
DEFAULT_MULTI_INITIATOR_SETTING)
iops_limit = extra_specs.get(EXTRA_SPEC_IOPS_LIMIT,
DEFAULT_IOPS_LIMIT_SETTING)
folder_name = extra_specs.get(EXTRA_SPEC_FOLDER,
@ -1172,11 +1191,9 @@ class NimbleRestAPIExecutor(object):
extra_specs_map = {}
extra_specs_map[EXTRA_SPEC_PERF_POLICY] = perf_policy_name
extra_specs_map[EXTRA_SPEC_ENCRYPTION] = encryption
extra_specs_map[EXTRA_SPEC_MULTI_INITIATOR] = multi_initiator
extra_specs_map[EXTRA_SPEC_IOPS_LIMIT] = iops_limit
extra_specs_map[EXTRA_SPEC_DEDUPE] = dedupe
extra_specs_map[EXTRA_SPEC_FOLDER] = folder_name
return extra_specs_map
def get_valid_nimble_extraspecs(self, extra_specs_map, vol_info):
@ -1192,10 +1209,10 @@ class NimbleRestAPIExecutor(object):
if encrypt.lower() == 'yes':
cipher = AES_256_XTS_CIPHER
data['cipher'] = cipher
multi_initiator = extra_specs_map_updated[EXTRA_SPEC_MULTI_INITIATOR]
data['multi_initiator'] = multi_initiator
if extra_specs_map.get('multiattach') == "<is> True":
data['multi_initiator'] = True
else:
data['multi_initiator'] = False
folder_name = extra_specs_map_updated[EXTRA_SPEC_FOLDER]
folder_id = None
pool_id = vol_info['pool_id']
@ -1290,7 +1307,7 @@ class NimbleRestAPIExecutor(object):
perf_policy_name = extra_specs_map[EXTRA_SPEC_PERF_POLICY]
perf_policy_id = self.get_performance_policy_id(perf_policy_name)
encrypt = extra_specs_map[EXTRA_SPEC_ENCRYPTION]
multi_initiator = extra_specs_map[EXTRA_SPEC_MULTI_INITIATOR]
multi_initiator = volume.get('multiattach', False)
folder_name = extra_specs_map[EXTRA_SPEC_FOLDER]
iops_limit = extra_specs_map[EXTRA_SPEC_IOPS_LIMIT]
dedupe = extra_specs_map[EXTRA_SPEC_DEDUPE]
@ -1521,11 +1538,11 @@ class NimbleRestAPIExecutor(object):
"initiator_group_id": initiator_group_id}
api = "access_control_records"
r = self.get_query(api, filter)
LOG.info("ACL record is %result", {'result': r.json()})
if not r.json()['data']:
raise NimbleAPIException(_("Unable to retrieve ACL for volume: "
"%(vol)s %(igroup)s ") %
{'vol': volume_id,
'igroup': initiator_group_id})
LOG.warning('ACL is not available for this volume %vol_id', {
'vol_id': volume_id})
return
return r.json()['data'][0]
def get_volume_acl_records(self, volume_id):
@ -1560,9 +1577,10 @@ class NimbleRestAPIExecutor(object):
try:
acl_record = self.get_acl_record(volume_id, initiator_group_id)
LOG.debug("ACL Record %(acl)s", {"acl": acl_record})
acl_id = acl_record['id']
api = 'access_control_records/' + six.text_type(acl_id)
self.delete(api)
if acl_record is not None:
acl_id = acl_record['id']
api = 'access_control_records/' + six.text_type(acl_id)
self.delete(api)
except NimbleAPIException as ex:
LOG.debug("remove_acl_exception: %s", ex)
if SM_OBJ_ENOENT_MSG in six.text_type(ex):
@ -1700,7 +1718,7 @@ class NimbleRestAPIExecutor(object):
perf_policy_name = extra_specs_map.get(EXTRA_SPEC_PERF_POLICY)
perf_policy_id = self.get_performance_policy_id(perf_policy_name)
encrypt = extra_specs_map.get(EXTRA_SPEC_ENCRYPTION)
multi_initiator = extra_specs_map.get(EXTRA_SPEC_MULTI_INITIATOR)
multi_initiator = volume.get('multiattach', False)
iops_limit = extra_specs_map[EXTRA_SPEC_IOPS_LIMIT]
folder_name = extra_specs_map[EXTRA_SPEC_FOLDER]
pool_id = self.get_pool_id(pool_name)

View File

@ -37,6 +37,8 @@ Supported operations
* Force backup of an in-use volume
* Retype a volume
* Create a Thinly Provisioned Volume
* Attach a volume to multiple servers simultaneously (multiattach)
Nimble Storage driver configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -126,9 +128,6 @@ The Nimble volume driver also supports the following extra spec options:
PERF_POL_NAME is the name of a performance policy which exists on the
Nimble array and should be enabled for every volume in a volume type.
'nimble:multi-initiator'='true'
Used to enable multi-initiator access for a volume-type.
nimble:dedupe'='true'
Used to enable dedupe support for a volume-type.

View File

@ -768,7 +768,7 @@ driver.netapp_ontap=complete
driver.netapp_solidfire=complete
driver.nexenta=missing
driver.nfs=missing
driver.nimble=missing
driver.nimble=complete
driver.prophetstor=missing
driver.pure=complete
driver.qnap=missing

View File

@ -0,0 +1,8 @@
---
features:
- |
Add Multi-attach feature in Nimble driver.
upgrade:
- |
Nimble specific extra-spec nimble:multi-initiator is removed.
Common extra-spec multiattach is added.