[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:
@@ -43,6 +43,37 @@ class NetAppApiServerTests(test.TestCase):
|
|||||||
self.root = netapp_api.NaServer('127.0.0.1')
|
self.root = netapp_api.NaServer('127.0.0.1')
|
||||||
super(NetAppApiServerTests, self).setUp()
|
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')
|
@ddt.data(None, 'ftp')
|
||||||
def test_set_transport_type_value_error(self, transport_type):
|
def test_set_transport_type_value_error(self, transport_type):
|
||||||
"""Tests setting an invalid transport type"""
|
"""Tests setting an invalid transport type"""
|
||||||
@@ -191,6 +222,31 @@ class NetAppApiServerTests(test.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(mock_invoke.called)
|
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)
|
@ddt.data(None, zapi_fakes.FAKE_XML_STR)
|
||||||
def test_send_http_request_value_error(self, na_element):
|
def test_send_http_request_value_error(self, na_element):
|
||||||
"""Tests whether invalid NaElement parameter causes error"""
|
"""Tests whether invalid NaElement parameter causes error"""
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class NetAppCDOTDataMotionTestCase(test.TestCase):
|
|||||||
hostname='fake_hostname', password='fake_password',
|
hostname='fake_hostname', password='fake_password',
|
||||||
username='fake_user', transport_type='https', port=8866,
|
username='fake_user', transport_type='https', port=8866,
|
||||||
trace=mock.ANY, vserver=None, api_trace_pattern="fake_regex",
|
trace=mock.ANY, vserver=None, api_trace_pattern="fake_regex",
|
||||||
|
ssl_cert_path='fake_ca',
|
||||||
private_key_file='fake_private_key.pem',
|
private_key_file='fake_private_key.pem',
|
||||||
certificate_file='fake_cert.pem',
|
certificate_file='fake_cert.pem',
|
||||||
ca_certificate_file='fake_ca_cert.crt',
|
ca_certificate_file='fake_ca_cert.crt',
|
||||||
@@ -140,7 +141,7 @@ class NetAppCDOTDataMotionTestCase(test.TestCase):
|
|||||||
hostname='fake_hostname', password='fake_password',
|
hostname='fake_hostname', password='fake_password',
|
||||||
username='fake_user', transport_type='https', port=8866,
|
username='fake_user', transport_type='https', port=8866,
|
||||||
trace=mock.ANY, vserver='fake_vserver',
|
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',
|
private_key_file='fake_private_key.pem',
|
||||||
certificate_file='fake_cert.pem',
|
certificate_file='fake_cert.pem',
|
||||||
ca_certificate_file='fake_ca_cert.crt',
|
ca_certificate_file='fake_ca_cert.crt',
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class NetAppDriver(driver.ProxyVD):
|
|||||||
reason=_('Required configuration not found'))
|
reason=_('Required configuration not found'))
|
||||||
|
|
||||||
config.append_config_values(options.netapp_proxy_opts)
|
config.append_config_values(options.netapp_proxy_opts)
|
||||||
|
config.append_config_values(options.netapp_transport_opts)
|
||||||
na_utils.check_flags(NetAppDriver.REQUIRED_FLAGS, config)
|
na_utils.check_flags(NetAppDriver.REQUIRED_FLAGS, config)
|
||||||
|
|
||||||
app_version = na_utils.OpenStackInfo().info()
|
app_version = na_utils.OpenStackInfo().info()
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ class NaServer(object):
|
|||||||
|
|
||||||
def __init__(self, host, server_type=SERVER_TYPE_FILER,
|
def __init__(self, host, server_type=SERVER_TYPE_FILER,
|
||||||
transport_type=TRANSPORT_TYPE_HTTP,
|
transport_type=TRANSPORT_TYPE_HTTP,
|
||||||
username=None,
|
ssl_cert_path=None, username=None, password=None, port=None,
|
||||||
password=None, port=None, api_trace_pattern=None,
|
api_trace_pattern=None,
|
||||||
private_key_file=None, certificate_file=None,
|
private_key_file=None, certificate_file=None,
|
||||||
ca_certificate_file=None, certificate_host_validation=None):
|
ca_certificate_file=None, certificate_host_validation=None):
|
||||||
self._host = host
|
self._host = host
|
||||||
@@ -87,6 +87,7 @@ class NaServer(object):
|
|||||||
self._ca_certificate_file = ca_certificate_file
|
self._ca_certificate_file = ca_certificate_file
|
||||||
self._certificate_host_validation = certificate_host_validation
|
self._certificate_host_validation = certificate_host_validation
|
||||||
self._refresh_conn = True
|
self._refresh_conn = True
|
||||||
|
self._ssl_cert_path = ssl_cert_path
|
||||||
|
|
||||||
if api_trace_pattern is not None:
|
if api_trace_pattern is not None:
|
||||||
na_utils.setup_api_trace_pattern(api_trace_pattern)
|
na_utils.setup_api_trace_pattern(api_trace_pattern)
|
||||||
@@ -306,7 +307,16 @@ class NaServer(object):
|
|||||||
auth_handler = self._create_certificate_auth_handler()
|
auth_handler = self._create_certificate_auth_handler()
|
||||||
else:
|
else:
|
||||||
auth_handler = self._create_basic_auth_handler()
|
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
|
self._opener = opener
|
||||||
|
|
||||||
def _create_basic_auth_handler(self):
|
def _create_basic_auth_handler(self):
|
||||||
|
|||||||
@@ -42,11 +42,13 @@ class Client(object, metaclass=volume_utils.TraceWrapperMetaclass):
|
|||||||
certificate_file = kwargs['certificate_file']
|
certificate_file = kwargs['certificate_file']
|
||||||
ca_certificate_file = kwargs['ca_certificate_file']
|
ca_certificate_file = kwargs['ca_certificate_file']
|
||||||
certificate_host_validation = kwargs['certificate_host_validation']
|
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:
|
if private_key_file and certificate_file and ca_certificate_file:
|
||||||
self.connection = netapp_api.NaServer(
|
self.connection = netapp_api.NaServer(
|
||||||
host=host,
|
host=host,
|
||||||
transport_type='https',
|
transport_type='https',
|
||||||
port=kwargs['port'],
|
port=kwargs['port'],
|
||||||
|
ssl_cert_path=ssl_cert_path,
|
||||||
private_key_file=private_key_file,
|
private_key_file=private_key_file,
|
||||||
certificate_file=certificate_file,
|
certificate_file=certificate_file,
|
||||||
ca_certificate_file=ca_certificate_file,
|
ca_certificate_file=ca_certificate_file,
|
||||||
@@ -57,6 +59,7 @@ class Client(object, metaclass=volume_utils.TraceWrapperMetaclass):
|
|||||||
host=host,
|
host=host,
|
||||||
transport_type='https',
|
transport_type='https',
|
||||||
port=kwargs['port'],
|
port=kwargs['port'],
|
||||||
|
ssl_cert_path=ssl_cert_path,
|
||||||
private_key_file=private_key_file,
|
private_key_file=private_key_file,
|
||||||
certificate_file=certificate_file,
|
certificate_file=certificate_file,
|
||||||
certificate_host_validation=certificate_host_validation,
|
certificate_host_validation=certificate_host_validation,
|
||||||
@@ -66,6 +69,7 @@ class Client(object, metaclass=volume_utils.TraceWrapperMetaclass):
|
|||||||
host=host,
|
host=host,
|
||||||
transport_type=kwargs['transport_type'],
|
transport_type=kwargs['transport_type'],
|
||||||
port=kwargs['port'],
|
port=kwargs['port'],
|
||||||
|
ssl_cert_path=ssl_cert_path,
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
api_trace_pattern=api_trace_pattern)
|
api_trace_pattern=api_trace_pattern)
|
||||||
|
|||||||
@@ -76,10 +76,13 @@ class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass):
|
|||||||
ca_certificate_file = kwargs['ca_certificate_file']
|
ca_certificate_file = kwargs['ca_certificate_file']
|
||||||
certificate_host_validation = kwargs['certificate_host_validation']
|
certificate_host_validation = kwargs['certificate_host_validation']
|
||||||
is_disaggregated = kwargs.get('is_disaggregated', False)
|
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:
|
if private_key_file and certificate_file and ca_certificate_file:
|
||||||
self.connection = netapp_api.RestNaServer(
|
self.connection = netapp_api.RestNaServer(
|
||||||
host=host,
|
host=host,
|
||||||
transport_type='https',
|
transport_type='https',
|
||||||
|
ssl_cert_path=ssl_cert_path,
|
||||||
port=kwargs['port'],
|
port=kwargs['port'],
|
||||||
private_key_file=private_key_file,
|
private_key_file=private_key_file,
|
||||||
certificate_file=certificate_file,
|
certificate_file=certificate_file,
|
||||||
@@ -90,6 +93,7 @@ class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass):
|
|||||||
self.connection = netapp_api.RestNaServer(
|
self.connection = netapp_api.RestNaServer(
|
||||||
host=host,
|
host=host,
|
||||||
transport_type='https',
|
transport_type='https',
|
||||||
|
ssl_cert_path=ssl_cert_path,
|
||||||
port=kwargs['port'],
|
port=kwargs['port'],
|
||||||
private_key_file=private_key_file,
|
private_key_file=private_key_file,
|
||||||
certificate_file=certificate_file,
|
certificate_file=certificate_file,
|
||||||
@@ -99,7 +103,7 @@ class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass):
|
|||||||
self.connection = netapp_api.RestNaServer(
|
self.connection = netapp_api.RestNaServer(
|
||||||
host=host,
|
host=host,
|
||||||
transport_type=kwargs['transport_type'],
|
transport_type=kwargs['transport_type'],
|
||||||
ssl_cert_path=kwargs.pop('ssl_cert_path'),
|
ssl_cert_path=ssl_cert_path,
|
||||||
port=kwargs['port'],
|
port=kwargs['port'],
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
|
|||||||
@@ -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:
|
if config.netapp_use_legacy_client and not force_rest:
|
||||||
client = client_cmode.Client(
|
client = client_cmode.Client(
|
||||||
transport_type=config.netapp_transport_type,
|
transport_type=config.netapp_transport_type,
|
||||||
|
ssl_cert_path=config.netapp_ssl_cert_path,
|
||||||
username=config.netapp_login,
|
username=config.netapp_login,
|
||||||
password=config.netapp_password,
|
password=config.netapp_password,
|
||||||
hostname=config.netapp_server_hostname,
|
hostname=config.netapp_server_hostname,
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user