diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_api.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_api.py index 6c9751dfc83..80771066abf 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_api.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_api.py @@ -43,6 +43,37 @@ class NetAppApiServerTests(test.TestCase): self.root = netapp_api.NaServer('127.0.0.1') super(NetAppApiServerTests, self).setUp() + @ddt.data( + {'host': '127.0.0.1', 'ssl_cert_path': None, + 'port': 8080, 'api_trace_pattern': None + }, + {'host': '127.0.0.1', 'ssl_cert_path': '/test/fake_cert.pem', + 'port': 8080, 'api_trace_pattern': 'pattern' + }, + ) + @ddt.unpack + def test__init__ssl_verify(self, host, ssl_cert_path, port, + api_trace_pattern): + + with mock.patch( + 'cinder.volume.drivers.netapp.utils.setup_api_trace_pattern' + ) as mock_trace: + server = netapp_api.NaServer( + host=host, + ssl_cert_path=ssl_cert_path, + port=port, + api_trace_pattern=api_trace_pattern + ) + + self.assertEqual(server._host, host) + self.assertEqual(server._port, str(port) if port else None) + self.assertEqual(server._refresh_conn, True) + + if api_trace_pattern: + mock_trace.assert_called_once_with(api_trace_pattern) + else: + mock_trace.assert_not_called() + @ddt.data(None, 'ftp') def test_set_transport_type_value_error(self, transport_type): """Tests setting an invalid transport type""" @@ -191,6 +222,31 @@ class NetAppApiServerTests(test.TestCase): self.assertTrue(mock_invoke.called) + @mock.patch('ssl._create_unverified_context') + @mock.patch('urllib.request.build_opener') + def test_build_opener_with_ssl_verification_disabled( + self, mock_build_opener, mock_unverified_context): + self.root._ssl_verify = False + mock_unverified_context.return_value = 'mock_unverified_context' + + self.root._build_opener() + + mock_unverified_context.assert_called_once() + mock_build_opener.assert_called_once() + + @mock.patch('urllib.request.HTTPPasswordMgrWithDefaultRealm') + @mock.patch('urllib.request.build_opener') + def test_build_opener_with_basic_auth(self, mock_build_opener, + mock_password_mgr): + self.root._username = 'user' + self.root._password = 'pass' + mock_password_mgr.return_value = mock.Mock() + + self.root._build_opener() + + mock_password_mgr.assert_called_once() + mock_build_opener.assert_called_once() + @ddt.data(None, zapi_fakes.FAKE_XML_STR) def test_send_http_request_value_error(self, na_element): """Tests whether invalid NaElement parameter causes error""" diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_utils.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_utils.py index a99e383a5d6..7aba894a2c4 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_utils.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_utils.py @@ -107,6 +107,7 @@ class NetAppCDOTDataMotionTestCase(test.TestCase): hostname='fake_hostname', password='fake_password', username='fake_user', transport_type='https', port=8866, trace=mock.ANY, vserver=None, api_trace_pattern="fake_regex", + ssl_cert_path='fake_ca', private_key_file='fake_private_key.pem', certificate_file='fake_cert.pem', ca_certificate_file='fake_ca_cert.crt', @@ -140,7 +141,7 @@ class NetAppCDOTDataMotionTestCase(test.TestCase): hostname='fake_hostname', password='fake_password', username='fake_user', transport_type='https', port=8866, trace=mock.ANY, vserver='fake_vserver', - api_trace_pattern="fake_regex", + api_trace_pattern="fake_regex", ssl_cert_path='fake_ca', private_key_file='fake_private_key.pem', certificate_file='fake_cert.pem', ca_certificate_file='fake_ca_cert.crt', diff --git a/cinder/volume/drivers/netapp/common.py b/cinder/volume/drivers/netapp/common.py index a22ae9734d4..b0be9d23f4d 100644 --- a/cinder/volume/drivers/netapp/common.py +++ b/cinder/volume/drivers/netapp/common.py @@ -61,6 +61,7 @@ class NetAppDriver(driver.ProxyVD): reason=_('Required configuration not found')) config.append_config_values(options.netapp_proxy_opts) + config.append_config_values(options.netapp_transport_opts) na_utils.check_flags(NetAppDriver.REQUIRED_FLAGS, config) app_version = na_utils.OpenStackInfo().info() diff --git a/cinder/volume/drivers/netapp/dataontap/client/api.py b/cinder/volume/drivers/netapp/dataontap/client/api.py index 20121c3c5ae..b7646eca942 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/api.py +++ b/cinder/volume/drivers/netapp/dataontap/client/api.py @@ -71,8 +71,8 @@ class NaServer(object): def __init__(self, host, server_type=SERVER_TYPE_FILER, transport_type=TRANSPORT_TYPE_HTTP, - username=None, - password=None, port=None, api_trace_pattern=None, + ssl_cert_path=None, username=None, password=None, port=None, + api_trace_pattern=None, private_key_file=None, certificate_file=None, ca_certificate_file=None, certificate_host_validation=None): self._host = host @@ -87,6 +87,7 @@ class NaServer(object): self._ca_certificate_file = ca_certificate_file self._certificate_host_validation = certificate_host_validation self._refresh_conn = True + self._ssl_cert_path = ssl_cert_path if api_trace_pattern is not None: na_utils.setup_api_trace_pattern(api_trace_pattern) @@ -306,7 +307,16 @@ class NaServer(object): auth_handler = self._create_certificate_auth_handler() else: auth_handler = self._create_basic_auth_handler() - opener = urllib.request.build_opener(auth_handler) + + # Create an SSL context based on _ssl_cert_path + if isinstance(self._ssl_cert_path, str): # with cert path + ssl_context = ( + ssl.create_default_context(cafile=self._ssl_cert_path)) + else: # Disable SSL verification + ssl_context = ssl._create_unverified_context() + + https_handler = urllib.request.HTTPSHandler(context=ssl_context) + opener = urllib.request.build_opener(auth_handler, https_handler) self._opener = opener def _create_basic_auth_handler(self): diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_base.py b/cinder/volume/drivers/netapp/dataontap/client/client_base.py index de68ff4f21a..d094d866199 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_base.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_base.py @@ -42,11 +42,13 @@ class Client(object, metaclass=volume_utils.TraceWrapperMetaclass): certificate_file = kwargs['certificate_file'] ca_certificate_file = kwargs['ca_certificate_file'] certificate_host_validation = kwargs['certificate_host_validation'] + ssl_cert_path = kwargs.get('ssl_cert_path') if private_key_file and certificate_file and ca_certificate_file: self.connection = netapp_api.NaServer( host=host, transport_type='https', port=kwargs['port'], + ssl_cert_path=ssl_cert_path, private_key_file=private_key_file, certificate_file=certificate_file, ca_certificate_file=ca_certificate_file, @@ -57,6 +59,7 @@ class Client(object, metaclass=volume_utils.TraceWrapperMetaclass): host=host, transport_type='https', port=kwargs['port'], + ssl_cert_path=ssl_cert_path, private_key_file=private_key_file, certificate_file=certificate_file, certificate_host_validation=certificate_host_validation, @@ -66,6 +69,7 @@ class Client(object, metaclass=volume_utils.TraceWrapperMetaclass): host=host, transport_type=kwargs['transport_type'], port=kwargs['port'], + ssl_cert_path=ssl_cert_path, username=username, password=password, api_trace_pattern=api_trace_pattern) diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py b/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py index 7b19e43b4e5..847898c43ed 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py @@ -76,10 +76,13 @@ class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass): ca_certificate_file = kwargs['ca_certificate_file'] certificate_host_validation = kwargs['certificate_host_validation'] is_disaggregated = kwargs.get('is_disaggregated', False) + ssl_cert_path = kwargs['ssl_cert_path'] + if private_key_file and certificate_file and ca_certificate_file: self.connection = netapp_api.RestNaServer( host=host, transport_type='https', + ssl_cert_path=ssl_cert_path, port=kwargs['port'], private_key_file=private_key_file, certificate_file=certificate_file, @@ -90,6 +93,7 @@ class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass): self.connection = netapp_api.RestNaServer( host=host, transport_type='https', + ssl_cert_path=ssl_cert_path, port=kwargs['port'], private_key_file=private_key_file, certificate_file=certificate_file, @@ -99,7 +103,7 @@ class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass): self.connection = netapp_api.RestNaServer( host=host, transport_type=kwargs['transport_type'], - ssl_cert_path=kwargs.pop('ssl_cert_path'), + ssl_cert_path=ssl_cert_path, port=kwargs['port'], username=username, password=password, diff --git a/cinder/volume/drivers/netapp/dataontap/utils/utils.py b/cinder/volume/drivers/netapp/dataontap/utils/utils.py index 1204cfbe782..fb99fd7587d 100644 --- a/cinder/volume/drivers/netapp/dataontap/utils/utils.py +++ b/cinder/volume/drivers/netapp/dataontap/utils/utils.py @@ -84,6 +84,7 @@ def get_client_for_backend(backend_name, vserver_name=None, force_rest=False): if config.netapp_use_legacy_client and not force_rest: client = client_cmode.Client( transport_type=config.netapp_transport_type, + ssl_cert_path=config.netapp_ssl_cert_path, username=config.netapp_login, password=config.netapp_password, hostname=config.netapp_server_hostname, diff --git a/releasenotes/notes/bp-netapp-self-signed-https-support-cb30081d4465acd1.yaml b/releasenotes/notes/bp-netapp-self-signed-https-support-cb30081d4465acd1.yaml new file mode 100644 index 00000000000..f727cc7dd06 --- /dev/null +++ b/releasenotes/notes/bp-netapp-self-signed-https-support-cb30081d4465acd1.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + NetApp ONTAP driver: Added support for self-signed certificate + support for HTTPS transport for management communication between + Cinder and NetApp ONTAP. + + ONTAP systems utilize self-signed certificates for HTTPS management + access by default. These certificates are generated automatically + during the initial setup or deployment of ONTAP. When ssl_cert_path + is configured with the extracted certificate file (.PEM format), + Cinder establishes HTTPS communication with full certificate validation. + When ssl_cert_path is not provided, Cinder automatically uses HTTPS + with an unverified SSL context, which provides encrypted communication + but skips certificate validation. This allows secure transport while + maintaining ease of configuration with ONTAP's default self-signed + certificates. Administrators can extract the certificate using tools + such as openssl or curl for full certificate validation if desired.