Merge "[netapp-cinder] Added support for san active active capability"
This commit is contained in:
commit
68e204a457
7 files changed
+495
-1
No files matched your search
@@ -4536,3 +4536,154 @@ class NetAppCmodeClientTestCase(test.TestCase):
|
||||
self.client.check_api_permissions)
|
||||
|
||||
self.assertEqual(0, mock_log.call_count)
|
||||
|
||||
def test_get_rest_connection_creates_once(self):
|
||||
mock_rest = mock.Mock()
|
||||
self.mock_object(netapp_api, 'RestNaServer',
|
||||
return_value=mock_rest)
|
||||
self.connection._host = 'fake_host'
|
||||
self.connection._protocol = 'https'
|
||||
self.connection._ssl_cert_path = '/fake/ca.pem'
|
||||
self.connection._username = 'admin'
|
||||
self.connection._password = 'pass'
|
||||
self.connection._port = '443'
|
||||
self.connection._private_key_file = None
|
||||
self.connection._certificate_file = None
|
||||
self.connection._ca_certificate_file = None
|
||||
self.connection._certificate_host_validation = False
|
||||
|
||||
conn1 = self.client._get_rest_connection()
|
||||
conn2 = self.client._get_rest_connection()
|
||||
|
||||
self.assertIs(conn1, conn2)
|
||||
netapp_api.RestNaServer.assert_called_once()
|
||||
|
||||
def test_get_rest_connection_ssl_cert_path_string(self):
|
||||
self.mock_object(netapp_api, 'RestNaServer',
|
||||
return_value=mock.Mock())
|
||||
self.connection._host = 'fake_host'
|
||||
self.connection._protocol = 'https'
|
||||
self.connection._ssl_cert_path = '/fake/ca.pem'
|
||||
self.connection._username = 'admin'
|
||||
self.connection._password = 'pass'
|
||||
self.connection._port = '443'
|
||||
self.connection._private_key_file = None
|
||||
self.connection._certificate_file = None
|
||||
self.connection._ca_certificate_file = None
|
||||
self.connection._certificate_host_validation = False
|
||||
|
||||
self.client._get_rest_connection()
|
||||
|
||||
call_kwargs = netapp_api.RestNaServer.call_args[1]
|
||||
self.assertEqual('/fake/ca.pem', call_kwargs['ssl_cert_path'])
|
||||
|
||||
def test_get_rest_connection_ssl_cert_path_none(self):
|
||||
self.mock_object(netapp_api, 'RestNaServer',
|
||||
return_value=mock.Mock())
|
||||
self.connection._host = 'fake_host'
|
||||
self.connection._protocol = 'https'
|
||||
self.connection._ssl_cert_path = None
|
||||
self.connection._username = 'admin'
|
||||
self.connection._password = 'pass'
|
||||
self.connection._port = '443'
|
||||
self.connection._private_key_file = None
|
||||
self.connection._certificate_file = None
|
||||
self.connection._ca_certificate_file = None
|
||||
self.connection._certificate_host_validation = False
|
||||
|
||||
self.client._get_rest_connection()
|
||||
|
||||
call_kwargs = netapp_api.RestNaServer.call_args[1]
|
||||
self.assertFalse(call_kwargs['ssl_cert_path'])
|
||||
|
||||
def test_get_rest_connection_ssl_cert_path_missing_attr(self):
|
||||
self.mock_object(netapp_api, 'RestNaServer',
|
||||
return_value=mock.Mock())
|
||||
self.connection._host = 'fake_host'
|
||||
self.connection._protocol = 'https'
|
||||
self.connection._username = 'admin'
|
||||
self.connection._password = 'pass'
|
||||
self.connection._port = '443'
|
||||
self.connection._private_key_file = None
|
||||
self.connection._certificate_file = None
|
||||
self.connection._ca_certificate_file = None
|
||||
self.connection._certificate_host_validation = False
|
||||
if hasattr(self.connection, '_ssl_cert_path'):
|
||||
delattr(self.connection, '_ssl_cert_path')
|
||||
|
||||
self.client._get_rest_connection()
|
||||
|
||||
call_kwargs = netapp_api.RestNaServer.call_args[1]
|
||||
self.assertFalse(call_kwargs['ssl_cert_path'])
|
||||
|
||||
def test_get_svm_san_multipathing_active_active(self):
|
||||
mock_rest_conn = mock.Mock()
|
||||
mock_rest_conn.invoke_successfully.return_value = (
|
||||
200,
|
||||
{'records': [{'san_multipathing': 'active_active'}]})
|
||||
self.mock_object(self.client, '_get_rest_connection',
|
||||
return_value=mock_rest_conn)
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertEqual('active_active', result)
|
||||
mock_rest_conn.invoke_successfully.assert_called_once_with(
|
||||
'svm/svms', 'get',
|
||||
query={'name': self.client.vserver,
|
||||
'fields': 'san_multipathing'})
|
||||
|
||||
def test_get_svm_san_multipathing_local_active(self):
|
||||
mock_rest_conn = mock.Mock()
|
||||
mock_rest_conn.invoke_successfully.return_value = (
|
||||
200,
|
||||
{'records': [{'san_multipathing': 'local_active'}]})
|
||||
self.mock_object(self.client, '_get_rest_connection',
|
||||
return_value=mock_rest_conn)
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertEqual('local_active', result)
|
||||
|
||||
def test_get_svm_san_multipathing_empty_records(self):
|
||||
mock_rest_conn = mock.Mock()
|
||||
mock_rest_conn.invoke_successfully.return_value = (
|
||||
200, {'records': []})
|
||||
self.mock_object(self.client, '_get_rest_connection',
|
||||
return_value=mock_rest_conn)
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_svm_san_multipathing_no_records_key(self):
|
||||
mock_rest_conn = mock.Mock()
|
||||
mock_rest_conn.invoke_successfully.return_value = (200, {})
|
||||
self.mock_object(self.client, '_get_rest_connection',
|
||||
return_value=mock_rest_conn)
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_svm_san_multipathing_exception(self):
|
||||
mock_rest_conn = mock.Mock()
|
||||
mock_rest_conn.invoke_successfully.side_effect = (
|
||||
netapp_api.NaApiError(code=0, message='connection failed'))
|
||||
self.mock_object(self.client, '_get_rest_connection',
|
||||
return_value=mock_rest_conn)
|
||||
mock_log = self.mock_object(client_cmode.LOG, 'exception')
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertIsNone(result)
|
||||
mock_log.assert_called_once()
|
||||
|
||||
def test_get_svm_san_multipathing_unexpected_exception(self):
|
||||
mock_rest_conn = mock.Mock()
|
||||
mock_rest_conn.invoke_successfully.side_effect = ValueError(
|
||||
'connection failed')
|
||||
self.mock_object(self.client, '_get_rest_connection',
|
||||
return_value=mock_rest_conn)
|
||||
|
||||
self.assertRaises(
|
||||
ValueError, self.client.get_svm_san_multipathing)
|
||||
@@ -5887,3 +5887,74 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
|
||||
result = self.client.get_svm_uuid_by_name()
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_svm_san_multipathing_active_active(self):
|
||||
self.mock_object(
|
||||
self.client, 'send_request',
|
||||
return_value={'records': [
|
||||
{'san_multipathing': 'active_active'}]})
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertEqual('active_active', result)
|
||||
self.client.send_request.assert_called_once_with(
|
||||
'/svm/svms', 'get',
|
||||
query={'name': self.client.vserver,
|
||||
'fields': 'san_multipathing'})
|
||||
|
||||
def test_get_svm_san_multipathing_local_active(self):
|
||||
self.mock_object(
|
||||
self.client, 'send_request',
|
||||
return_value={'records': [
|
||||
{'san_multipathing': 'local_active'}]})
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertEqual('local_active', result)
|
||||
|
||||
def test_get_svm_san_multipathing_empty_records(self):
|
||||
self.mock_object(
|
||||
self.client, 'send_request',
|
||||
return_value={'records': []})
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_svm_san_multipathing_no_records_key(self):
|
||||
self.mock_object(
|
||||
self.client, 'send_request',
|
||||
return_value={})
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_get_svm_san_multipathing_exception(self):
|
||||
self.mock_object(
|
||||
self.client, 'send_request',
|
||||
side_effect=netapp_api.NaApiError(
|
||||
code=0, message='connection failed'))
|
||||
mock_log = self.mock_object(client_cmode_rest.LOG, 'exception')
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertIsNone(result)
|
||||
mock_log.assert_called_once()
|
||||
|
||||
def test_get_svm_san_multipathing_unexpected_exception(self):
|
||||
self.mock_object(
|
||||
self.client, 'send_request',
|
||||
side_effect=ValueError('connection failed'))
|
||||
|
||||
self.assertRaises(
|
||||
ValueError, self.client.get_svm_san_multipathing)
|
||||
|
||||
def test_get_svm_san_multipathing_missing_field(self):
|
||||
self.mock_object(
|
||||
self.client, 'send_request',
|
||||
return_value={'records': [{'name': 'vs0'}]})
|
||||
|
||||
result = self.client.get_svm_san_multipathing()
|
||||
|
||||
self.assertIsNone(result)
|
||||
@@ -192,6 +192,9 @@ class CapabilitiesLibraryTestCase(test.TestCase):
|
||||
self.ssc_library, '_get_ssc_qos_min_info',
|
||||
side_effect=[SSC_QOS_MIN_INFO['volume1'],
|
||||
SSC_QOS_MIN_INFO['volume2']])
|
||||
mock_get_ssc_san_mp_info = self.mock_object(
|
||||
self.ssc_library, '_get_ssc_san_multipathing_info',
|
||||
return_value={})
|
||||
if protocol != 'nfs':
|
||||
mock_get_ssc_volume_count_info = self.mock_object(
|
||||
self.ssc_library, '_get_ssc_volume_count_info',
|
||||
@@ -230,6 +233,75 @@ class CapabilitiesLibraryTestCase(test.TestCase):
|
||||
mock.call('volume1'), mock.call('volume2')])
|
||||
mock_get_ssc_qos_min_info.assert_has_calls([
|
||||
mock.call('node1'), mock.call('node2')])
|
||||
mock_get_ssc_san_mp_info.assert_called_once_with()
|
||||
|
||||
@ddt.data('nfs', 'iscsi', 'fc', 'nvme')
|
||||
def test_update_ssc_san_multipathing_protocol_gating(self, protocol):
|
||||
self.ssc_library.protocol = protocol
|
||||
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_flexvol_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_dedupe_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_mirror_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_encryption_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_aggregate_info',
|
||||
return_value={'netapp_node_name': 'node1'})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_qos_min_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_volume_count_info', return_value={})
|
||||
mock_san_mp_info = self.mock_object(
|
||||
self.ssc_library, '_get_ssc_san_multipathing_info',
|
||||
return_value={'netapp_san_active_active': 'true'})
|
||||
|
||||
flexvol_map = {'volume1': {'pool_name': 'volume1'}}
|
||||
|
||||
self.ssc_library.update_ssc(flexvol_map)
|
||||
|
||||
if protocol == 'nfs':
|
||||
mock_san_mp_info.assert_not_called()
|
||||
self.assertNotIn(
|
||||
'netapp_san_active_active',
|
||||
self.ssc_library.ssc['volume1'])
|
||||
else:
|
||||
mock_san_mp_info.assert_called_once_with()
|
||||
self.assertEqual(
|
||||
'true',
|
||||
self.ssc_library.ssc['volume1']['netapp_san_active_active'])
|
||||
|
||||
def test_update_ssc_san_multipathing_info_empty_not_applied(self):
|
||||
self.ssc_library.protocol = 'iscsi'
|
||||
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_flexvol_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_dedupe_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_mirror_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_encryption_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_aggregate_info',
|
||||
return_value={'netapp_node_name': 'node1'})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_qos_min_info', return_value={})
|
||||
self.mock_object(
|
||||
self.ssc_library, '_get_ssc_volume_count_info', return_value={})
|
||||
mock_san_mp_info = self.mock_object(
|
||||
self.ssc_library, '_get_ssc_san_multipathing_info',
|
||||
return_value={})
|
||||
|
||||
flexvol_map = {'volume1': {'pool_name': 'volume1'}}
|
||||
|
||||
self.ssc_library.update_ssc(flexvol_map)
|
||||
|
||||
mock_san_mp_info.assert_called_once_with()
|
||||
self.assertNotIn(
|
||||
'netapp_san_active_active', self.ssc_library.ssc['volume1'])
|
||||
|
||||
def test__update_for_failover(self):
|
||||
self.mock_object(self.ssc_library, 'update_ssc')
|
||||
@@ -725,3 +797,76 @@ class CapabilitiesLibraryTestCase(test.TestCase):
|
||||
contains_fg_returned = self.ssc_library.contains_flexgroup_pool()
|
||||
|
||||
self.assertEqual(contains_fg_returned, contains_fg)
|
||||
|
||||
def test_get_ssc_san_multipathing_info_version_none(self):
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_ontap_version', return_value=None)
|
||||
|
||||
result = self.ssc_library._get_ssc_san_multipathing_info()
|
||||
|
||||
self.assertEqual({}, result)
|
||||
self.zapi_client.get_ontap_version.assert_called_once_with(
|
||||
cached=True)
|
||||
|
||||
@ddt.data((9, 17, 1), (9, 14, 0), (9, 18, 0))
|
||||
def test_get_ssc_san_multipathing_info_old_ontap(self, ontap_version):
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_ontap_version',
|
||||
return_value=ontap_version)
|
||||
|
||||
result = self.ssc_library._get_ssc_san_multipathing_info()
|
||||
|
||||
self.assertEqual({}, result)
|
||||
self.zapi_client.get_svm_san_multipathing.assert_not_called()
|
||||
|
||||
def test_get_ssc_san_multipathing_info_active_active(self):
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_ontap_version',
|
||||
return_value=(9, 19, 1))
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_svm_san_multipathing',
|
||||
return_value='active_active')
|
||||
|
||||
result = self.ssc_library._get_ssc_san_multipathing_info()
|
||||
|
||||
self.assertEqual({'netapp_san_active_active': 'true'}, result)
|
||||
self.zapi_client.get_svm_san_multipathing.assert_called_once_with()
|
||||
|
||||
def test_get_ssc_san_multipathing_info_local_active(self):
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_ontap_version',
|
||||
return_value=(9, 19, 1))
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_svm_san_multipathing',
|
||||
return_value='local_active')
|
||||
|
||||
result = self.ssc_library._get_ssc_san_multipathing_info()
|
||||
|
||||
self.assertEqual({'netapp_san_active_active': 'false'}, result)
|
||||
|
||||
@ddt.data((9, 19, 1), (9, 20, 0), (10, 0, 0))
|
||||
def test_get_ssc_san_multipathing_info_new_ontap(self, ontap_version):
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_ontap_version',
|
||||
return_value=ontap_version)
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_svm_san_multipathing',
|
||||
return_value='local_active')
|
||||
|
||||
result = self.ssc_library._get_ssc_san_multipathing_info()
|
||||
|
||||
self.assertEqual({'netapp_san_active_active': 'false'}, result)
|
||||
self.zapi_client.get_svm_san_multipathing.assert_called_once_with()
|
||||
|
||||
def test_get_ssc_san_multipathing_info_none_response(self):
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_ontap_version',
|
||||
return_value=(9, 19, 1))
|
||||
self.mock_object(
|
||||
self.zapi_client, 'get_svm_san_multipathing',
|
||||
return_value=None)
|
||||
|
||||
result = self.ssc_library._get_ssc_san_multipathing_info()
|
||||
|
||||
self.assertEqual({}, result)
|
||||
self.zapi_client.get_svm_san_multipathing.assert_called_once_with()
|
||||
@@ -2819,3 +2819,54 @@ class Client(client_base.Client, metaclass=volume_utils.TraceWrapperMetaclass):
|
||||
}
|
||||
|
||||
return provisioning_opts
|
||||
|
||||
def _get_rest_connection(self):
|
||||
"""Lazily create a minimal REST connection for REST-only queries."""
|
||||
if not hasattr(self, '_rest_connection'):
|
||||
zapi_cert_path = getattr(
|
||||
self.connection, '_ssl_cert_path', None)
|
||||
# When the ZAPI connection has no explicit CA cert path it
|
||||
# silently disables SSL verification (unverified context).
|
||||
# Mirror that behaviour for the REST session by passing
|
||||
# False so that requests also skips certificate checks.
|
||||
ssl_cert_path = zapi_cert_path if (
|
||||
isinstance(zapi_cert_path, str)) else False
|
||||
self._rest_connection = netapp_api.RestNaServer(
|
||||
host=self.connection._host,
|
||||
transport_type=self.connection._protocol,
|
||||
ssl_cert_path=ssl_cert_path,
|
||||
username=self.connection._username,
|
||||
password=self.connection._password,
|
||||
port=self.connection._port,
|
||||
private_key_file=self.connection._private_key_file,
|
||||
certificate_file=self.connection._certificate_file,
|
||||
ca_certificate_file=(
|
||||
self.connection._ca_certificate_file),
|
||||
ssl_cert_verify=self.connection._ssl_cert_verify,
|
||||
certificate_host_validation=(
|
||||
self.connection._certificate_host_validation),
|
||||
)
|
||||
return self._rest_connection
|
||||
|
||||
def get_svm_san_multipathing(self):
|
||||
"""Query SVM san-multipathing via REST (no ZAPI equivalent).
|
||||
|
||||
Returns the san_multipathing value ('active_active',
|
||||
'local_active') or None if the query fails or the field
|
||||
is not available (e.g. ONTAP < 9.19.1).
|
||||
"""
|
||||
try:
|
||||
rest_conn = self._get_rest_connection()
|
||||
query = {
|
||||
'name': self.vserver,
|
||||
'fields': 'san_multipathing',
|
||||
}
|
||||
_, response = rest_conn.invoke_successfully(
|
||||
'svm/svms', 'get', query=query)
|
||||
records = response.get('records', [])
|
||||
if records:
|
||||
return records[0].get('san_multipathing')
|
||||
except netapp_api.NaApiError:
|
||||
LOG.exception('Failed to query san_multipathing via REST. '
|
||||
'Defaulting to None.')
|
||||
return None
|
||||
@@ -3834,3 +3834,24 @@ class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass):
|
||||
if r.get('name') == self.vserver:
|
||||
return r.get('uuid')
|
||||
return None
|
||||
|
||||
def get_svm_san_multipathing(self):
|
||||
"""Query SVM san-multipathing property via REST.
|
||||
|
||||
Returns the san_multipathing value ('active_active',
|
||||
'local_active') or None if the query fails or the field
|
||||
is not available (e.g. ONTAP < 9.19.1).
|
||||
"""
|
||||
try:
|
||||
query = {
|
||||
'name': self.vserver,
|
||||
'fields': 'san_multipathing',
|
||||
}
|
||||
response = self.send_request('/svm/svms', 'get', query=query)
|
||||
records = response.get('records', [])
|
||||
if records:
|
||||
return records[0].get('san_multipathing')
|
||||
except netapp_api.NaApiError:
|
||||
LOG.exception('Failed to query san_multipathing via REST. '
|
||||
'Defaulting to None.')
|
||||
return None
|
||||
@@ -20,10 +20,12 @@ import copy
|
||||
import re
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from packaging import version
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
MINIMUM_SAN_AA_VERSION = '9.19.1'
|
||||
|
||||
|
||||
class CapabilitiesLibrary(object):
|
||||
|
||||
@@ -92,6 +94,11 @@ class CapabilitiesLibrary(object):
|
||||
|
||||
ssc = {}
|
||||
|
||||
san_mp_info = {}
|
||||
# SVM-level property -- query once, apply to all pools
|
||||
if self.protocol.casefold() != 'nfs':
|
||||
san_mp_info = self._get_ssc_san_multipathing_info()
|
||||
|
||||
for flexvol_name, flexvol_info in flexvol_map.items():
|
||||
|
||||
ssc_volume = {}
|
||||
@@ -115,6 +122,10 @@ class CapabilitiesLibrary(object):
|
||||
|
||||
ssc_volume.update(self._get_ssc_qos_min_info(node_name))
|
||||
|
||||
if san_mp_info:
|
||||
# SVM-level SAN multipathing capability
|
||||
ssc_volume.update(san_mp_info)
|
||||
|
||||
if self.protocol.casefold() != 'nfs':
|
||||
ssc_volume.update
|
||||
(self._get_ssc_volume_count_info(flexvol_name))
|
||||
@@ -319,6 +330,33 @@ class CapabilitiesLibrary(object):
|
||||
'total_volumes': volume_count,
|
||||
}
|
||||
|
||||
def _get_ssc_san_multipathing_info(self):
|
||||
"""Gather SVM san-multipathing info for the SSC.
|
||||
|
||||
This is an SVM-level property indicating whether the SVM has
|
||||
active-active SAN multipathing enabled (ONTAP 9.19.1+).
|
||||
The capability is queried via REST; when using the legacy ZAPI
|
||||
client the ZAPI Client performs a lightweight REST fallback.
|
||||
Returns an empty dict on ONTAP < 9.19.1 so that the key is
|
||||
never advertised on clusters that do not support it.
|
||||
"""
|
||||
ontap_version = self.zapi_client.get_ontap_version(cached=True)
|
||||
if ontap_version is None:
|
||||
return {}
|
||||
|
||||
ontap_version_str = ".".join(map(str, ontap_version))
|
||||
if (version.parse(ontap_version_str) <
|
||||
version.parse(MINIMUM_SAN_AA_VERSION)):
|
||||
return {}
|
||||
|
||||
san_multipathing = self.zapi_client.get_svm_san_multipathing()
|
||||
if san_multipathing is None:
|
||||
return {}
|
||||
is_active_active = (san_multipathing == 'active_active')
|
||||
return {
|
||||
'netapp_san_active_active': str(is_active_active).lower(),
|
||||
}
|
||||
|
||||
def get_matching_flexvols_for_extra_specs(self, extra_specs):
|
||||
"""Return a list of flexvol names that match a set of extra specs."""
|
||||
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
NetApp ONTAP driver: Added pool-level capability
|
||||
``netapp_san_active_active`` that exposes the ONTAP 9.19.1 SVM-level
|
||||
``san_multipathing`` property (``active_active`` or ``local_active``)
|
||||
as a schedulable extra spec. Operators can steer volume placement
|
||||
toward Active-Active or Local-Active backends via volume types using
|
||||
``netapp_san_active_active=<is> True`` or
|
||||
``netapp_san_active_active=<is> False``. The ZAPI client uses a lazy,
|
||||
cached ``RestNaServer`` to query this REST-only property without
|
||||
requiring migration to the full REST client. The REST client queries
|
||||
it natively. The capability is injected into each pool's SSC data
|
||||
during ``update_ssc()`` and excluded from ASAr2 backends where
|
||||
Active-Active is inherent. A version gate ensures the capability is
|
||||
only advertised on clusters running ONTAP 9.19.1 or later; older
|
||||
releases omit the key entirely.
|
||||
Reference in new issue
Block a user