[NetApp]:Cinder support for self-signed transport

The NetApp ONTAP driver now supports HTTPS transport
for management communication with certificates.
ONTAP systems use self-signed certificates by default
for HTTPS management access.

This patch enables two modes of HTTPS communication:
- When ssl_cert_path is configured: certificate validation
- When ssl_cert_path is not provided: Unverified SSL context
  (encrypted transport but skips certificate validation)

This allows secure communication while maintaining ease of
configuration with ONTAP's default self-signed certificates.

Implements: blueprint netapp-self-signed-https
Change-Id: Ia486448b85feba636effef609d79ef8e57a6d39a
Signed-off-by: jayaanan <jayaanand.borra@netapp.com>
This commit is contained in:
jayaanan
2025-04-18 08:21:02 -04:00
parent 55d27f77a6
commit d3d91d9a13
8 changed files with 100 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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