[NetApp] Certificate based authentication for NetApp drivers

The NetApp ONTAP driver now supports Certificate-Based-Authentication (CBA)
for operators that desire certificate based authentication instead of user
and password.
Note: The options for cert-auth take precedence, if all the auth options
are defined in the config (both cert and legacy), the legacy ones are
ignored.

Change-Id: Idad916a541fc1f355469912da38bd30cf366e3b0
This commit is contained in:
Saikumar Pulluri 2025-02-20 03:25:24 -05:00
parent e0c9a012b0
commit 9c26885e9b
18 changed files with 387 additions and 78 deletions

View File

@ -411,6 +411,8 @@ def list_opts():
cinder_volume_drivers_netapp_options.netapp_connection_opts,
cinder_volume_drivers_netapp_options.netapp_transport_opts,
cinder_volume_drivers_netapp_options.netapp_basicauth_opts,
cinder_volume_drivers_netapp_options.
netapp_certificateauth_opts,
cinder_volume_drivers_netapp_options.netapp_cluster_opts,
cinder_volume_drivers_netapp_options.netapp_provisioning_opts,
cinder_volume_drivers_netapp_options.netapp_img_cache_opts,

View File

@ -168,15 +168,23 @@ class NetAppApiServerTests(test.TestCase):
mock_invoke.assert_called_with(zapi_fakes.FAKE_XML_STR)
def test__build_opener_not_implemented_error(self):
"""Tests whether certificate style authorization raises Exception"""
self.root._auth_style = 'not_basic_auth'
def test_build_opener_with_certificate_auth(self):
"""Tests whether build opener works with """
"""valid certificate parameters"""
self.root._private_key_file = 'fake_key.pem'
self.root._certificate_file = 'fake_cert.pem'
auth_handler = self.mock_object(self.root,
'_create_certificate_auth_handler',
mock.Mock(return_value='fake_auth'))
expected_opener = 'fake_auth'
self.mock_object(urllib.request, 'build_opener', auth_handler)
self.root._build_opener()
self.assertEqual(self.root._opener, expected_opener)
self.root._create_certificate_auth_handler.assert_called()
self.assertRaises(NotImplementedError, self.root._build_opener)
def test__build_opener_valid(self):
"""Tests whether build opener works with valid parameters"""
self.root._auth_style = 'basic_auth'
def test__build_opener_default(self):
"""Tests whether build opener works with """
"""default(basic auth) parameters"""
mock_invoke = self.mock_object(urllib.request, 'build_opener')
self.root._build_opener()
@ -837,7 +845,9 @@ class NetAppRestApiServerTests(test.TestCase):
self.assertEqual(expected_vserver, res)
def test__build_session(self):
def test__build_session_with_basic_auth(self):
"""Tests whether build session works with """
"""default(basic auth) parameters"""
fake_session = mock.Mock()
mock_requests_session = self.mock_object(
requests, 'Session', mock.Mock(return_value=fake_session))
@ -856,6 +866,28 @@ class NetAppRestApiServerTests(test.TestCase):
mock_requests_session.assert_called_once_with()
mock_auth.assert_called_once_with()
def test__build_session_with_certificate_auth(self):
"""Tests whether build session works with """
"""valid certificate parameters"""
self.rest_client._private_key_file = 'fake_key.pem'
self.rest_client._certificate_file = 'fake_cert.pem'
self.rest_client._certificate_host_validation = False
fake_session = mock.Mock()
mock_requests_session = self.mock_object(
requests, 'Session', mock.Mock(return_value=fake_session))
mock_auth = self.mock_object(
self.rest_client, '_create_certificate_auth_handler',
mock.Mock(return_value=('fake_cert', 'fake_verify')))
self.rest_client._build_session(zapi_fakes.FAKE_HEADERS)
self.assertEqual(fake_session, self.rest_client._session)
self.assertEqual(('fake_cert', 'fake_verify'),
(self.rest_client._session.cert,
self.rest_client._session.verify))
self.assertEqual(zapi_fakes.FAKE_HEADERS,
self.rest_client._session.headers)
mock_requests_session.assert_called_once_with()
mock_auth.assert_called_once_with()
@ddt.data(True, False)
def test__build_headers(self, enable_tunneling):
self.rest_client._vserver = zapi_fakes.VSERVER_NAME
@ -880,3 +912,33 @@ class NetAppRestApiServerTests(test.TestCase):
expected = auth.HTTPBasicAuth(username, password)
self.assertEqual(expected.__dict__, res.__dict__)
def test__create_certificate_auth_handler_default(self):
"""Test whether create certificate auth handler """
"""works with default params"""
self.rest_client._private_key_file = 'fake_key.pem'
self.rest_client._certificate_file = 'fake_cert.pem'
self.rest_client._certificate_host_validation = False
cert = self.rest_client._certificate_file, \
self.rest_client._private_key_file
self.rest_client._session = mock.Mock()
if not self.rest_client._certificate_host_validation:
self.assertFalse(self.rest_client._certificate_host_validation)
res = self.rest_client._create_certificate_auth_handler()
self.assertEqual(res,
(cert, self.rest_client._certificate_host_validation))
def test__create_certificate_auth_handler_with_host_validation(self):
"""Test whether create certificate auth handler """
"""works with host validation enabled"""
self.rest_client._private_key_file = 'fake_key.pem'
self.rest_client._certificate_file = 'fake_cert.pem'
self.rest_client._ca_certificate_file = 'fake_ca_cert.crt'
self.rest_client._certificate_host_validation = True
cert = self.rest_client._certificate_file, \
self.rest_client._private_key_file
self.rest_client._session = mock.Mock()
if self.rest_client._certificate_host_validation:
self.assertTrue(self.rest_client._certificate_host_validation)
res = self.rest_client._create_certificate_auth_handler()
self.assertEqual(res, (cert, self.rest_client._ca_certificate_file))

View File

@ -35,7 +35,12 @@ CONNECTION_INFO = {'hostname': 'hostname',
'port': 443,
'username': 'admin',
'password': 'passw0rd',
'api_trace_pattern': 'fake_regex'}
'api_trace_pattern': 'fake_regex',
'private_key_file': 'fake_private_key.pem',
'certificate_file': 'fake_cert.pem',
'ca_certificate_file': 'fake_ca_cert.crt',
'certificate_host_validation': 'False'
}
@ddt.ddt

View File

@ -43,7 +43,11 @@ CONNECTION_INFO = {'hostname': 'hostname',
'username': 'admin',
'password': 'passw0rd',
'vserver': 'fake_vserver',
'api_trace_pattern': 'fake_regex'}
'api_trace_pattern': 'fake_regex',
'private_key_file': 'fake_private_key.pem',
'certificate_file': 'fake_cert.pem',
'ca_certificate_file': 'fake_ca_cert.crt',
'certificate_host_validation': 'False'}
@ddt.ddt

View File

@ -41,7 +41,12 @@ CONNECTION_INFO = {'hostname': 'hostname',
'password': 'passw0rd',
'vserver': 'fake_vserver',
'ssl_cert_path': 'fake_ca',
'api_trace_pattern': 'fake_regex'}
'api_trace_pattern': 'fake_regex',
'private_key_file': 'fake_private_key.pem',
'certificate_file': 'fake_cert.pem',
'ca_certificate_file': 'fake_ca_cert.crt',
'certificate_host_validation': 'False'
}
@ddt.ddt

View File

@ -183,6 +183,7 @@ def get_fake_cmode_config(backend_name):
config.append_config_values(na_opts.netapp_connection_opts)
config.append_config_values(na_opts.netapp_transport_opts)
config.append_config_values(na_opts.netapp_basicauth_opts)
config.append_config_values(na_opts.netapp_certificateauth_opts)
config.append_config_values(na_opts.netapp_provisioning_opts)
config.append_config_values(na_opts.netapp_cluster_opts)
config.append_config_values(na_opts.netapp_san_opts)

View File

@ -76,6 +76,7 @@ class NetAppCDOTDataMotionMixinTestCase(test.TestCase):
config.append_config_values(na_opts.netapp_connection_opts)
config.append_config_values(na_opts.netapp_transport_opts)
config.append_config_values(na_opts.netapp_basicauth_opts)
config.append_config_values(na_opts.netapp_certificateauth_opts)
config.append_config_values(na_opts.netapp_provisioning_opts)
config.append_config_values(na_opts.netapp_cluster_opts)
config.append_config_values(na_opts.netapp_san_opts)

View File

@ -53,6 +53,14 @@ class NetAppCDOTDataMotionTestCase(test.TestCase):
group=self.backend)
CONF.set_override('netapp_ssl_cert_path', 'fake_ca',
group=self.backend)
CONF.set_override('netapp_private_key_file', 'fake_private_key.pem',
group=self.backend)
CONF.set_override('netapp_certificate_file', 'fake_cert.pem',
group=self.backend)
CONF.set_override('netapp_ca_certificate_file', 'fake_ca_cert.crt',
group=self.backend)
CONF.set_override('netapp_certificate_host_validation', False,
group=self.backend)
def test_get_backend_configuration(self):
self.mock_object(utils, 'CONF')
@ -98,14 +106,22 @@ class NetAppCDOTDataMotionTestCase(test.TestCase):
self.mock_cmode_client.assert_called_once_with(
hostname='fake_hostname', password='fake_password',
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",
private_key_file='fake_private_key.pem',
certificate_file='fake_cert.pem',
ca_certificate_file='fake_ca_cert.crt',
certificate_host_validation=False)
self.mock_cmode_rest_client.assert_not_called()
else:
self.mock_cmode_rest_client.assert_called_once_with(
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', async_rest_timeout=60)
ssl_cert_path='fake_ca', async_rest_timeout=60,
private_key_file='fake_private_key.pem',
certificate_file='fake_cert.pem',
ca_certificate_file='fake_ca_cert.crt',
certificate_host_validation=False)
self.mock_cmode_client.assert_not_called()
@ddt.data(True, False)
@ -124,7 +140,11 @@ 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",
private_key_file='fake_private_key.pem',
certificate_file='fake_cert.pem',
ca_certificate_file='fake_ca_cert.crt',
certificate_host_validation=False)
self.mock_cmode_rest_client.assert_not_called()
else:
self.mock_cmode_rest_client.assert_called_once_with(
@ -132,7 +152,11 @@ class NetAppCDOTDataMotionTestCase(test.TestCase):
username='fake_user', transport_type='https', port=8866,
trace=mock.ANY, vserver='fake_vserver',
api_trace_pattern="fake_regex", ssl_cert_path='fake_ca',
async_rest_timeout = 60)
async_rest_timeout = 60,
private_key_file='fake_private_key.pem',
certificate_file='fake_cert.pem',
ca_certificate_file='fake_ca_cert.crt',
certificate_host_validation=False)
self.mock_cmode_client.assert_not_called()

View File

@ -176,6 +176,7 @@ def create_configuration():
config.append_config_values(na_opts.netapp_connection_opts)
config.append_config_values(na_opts.netapp_transport_opts)
config.append_config_values(na_opts.netapp_basicauth_opts)
config.append_config_values(na_opts.netapp_certificateauth_opts)
config.append_config_values(na_opts.netapp_provisioning_opts)
return config

View File

@ -65,18 +65,19 @@ class NetAppLun(object):
def __str__(self, *args, **kwargs):
return 'NetApp LUN [handle:%s, name:%s, size:%s, metadata:%s]' % (
self.handle, self.name, self.size, self.metadata)
self.handle, self.name, self.size, self.metadata)
class NetAppBlockStorageLibrary(
object,
metaclass=volume_utils.TraceWrapperMetaclass):
"""NetApp block storage library for Data ONTAP."""
# do not increment this as it may be used in volume type definitions
VERSION = "1.0.0"
REQUIRED_FLAGS = ['netapp_login', 'netapp_password',
'netapp_server_hostname']
REQUIRED_FLAGS_BASIC = ['netapp_login', 'netapp_password',
'netapp_server_hostname']
REQUIRED_FLAGS_CERT = ['netapp_private_key_file',
'netapp_certificate_file']
ALLOWED_LUN_OS_TYPES = ['linux', 'aix', 'hpux', 'image', 'windows',
'windows_2008', 'windows_gpt', 'solaris',
'solaris_efi', 'netware', 'openvms', 'hyper_v']
@ -109,6 +110,8 @@ class NetAppBlockStorageLibrary(
self.configuration = kwargs['configuration']
self.configuration.append_config_values(na_opts.netapp_connection_opts)
self.configuration.append_config_values(na_opts.netapp_basicauth_opts)
self.configuration.append_config_values(
na_opts.netapp_certificateauth_opts)
self.configuration.append_config_values(na_opts.netapp_transport_opts)
self.configuration.append_config_values(
na_opts.netapp_provisioning_opts)
@ -137,13 +140,18 @@ class NetAppBlockStorageLibrary(
reserved_percentage = 100 * int(reserved_ratio)
msg = ('The "netapp_size_multiplier" configuration option is '
'deprecated and will be removed in the Mitaka release. '
'Please set "reserved_percentage = %d" instead.') % (
reserved_percentage)
'Please set "reserved_percentage = %d" instead.') \
% reserved_percentage
versionutils.report_deprecated_feature(LOG, msg)
return reserved_percentage
def do_setup(self, context):
na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration)
if self.configuration.netapp_private_key_file or\
self.configuration.netapp_certificate_file:
na_utils.check_flags(self.REQUIRED_FLAGS_CERT,
self.configuration)
else:
na_utils.check_flags(self.REQUIRED_FLAGS_BASIC, self.configuration)
self.lun_ostype = (self.configuration.netapp_lun_ostype
or self.DEFAULT_LUN_OS)
self.host_type = (self.configuration.netapp_host_type
@ -242,9 +250,9 @@ class NetAppBlockStorageLibrary(
na_utils.get_qos_policy_group_name_from_info(
qos_policy_group_info))
qos_policy_group_is_adaptive = (volume_utils.is_boolean_str(
extra_specs.get('netapp:qos_policy_group_is_adaptive')) or
na_utils.is_qos_policy_group_spec_adaptive(
qos_policy_group_info))
extra_specs.get('netapp:qos_policy_group_is_adaptive'))
or na_utils.is_qos_policy_group_spec_adaptive
(qos_policy_group_info))
try:
self._create_lun(pool_name, lun_name, size, metadata,
@ -367,9 +375,9 @@ class NetAppBlockStorageLibrary(
na_utils.get_qos_policy_group_name_from_info(
qos_policy_group_info))
qos_policy_group_is_adaptive = (volume_utils.is_boolean_str(
extra_specs.get('netapp:qos_policy_group_is_adaptive')) or
na_utils.is_qos_policy_group_spec_adaptive(
qos_policy_group_info))
extra_specs.get('netapp:qos_policy_group_is_adaptive'))
or na_utils.is_qos_policy_group_spec_adaptive
(qos_policy_group_info))
try:
self._clone_lun(
@ -882,8 +890,8 @@ class NetAppBlockStorageLibrary(
LOG.info("Unmanaged LUN with current path %(path)s and uuid "
"%(uuid)s.",
{'path': managed_lun.get_metadata_property('Path'),
'uuid': managed_lun.get_metadata_property('UUID')
or 'unknown'})
'uuid': managed_lun.get_metadata_property('UUID') or
'unknown'})
def initialize_connection_iscsi(self, volume, connector):
"""Driver entry point to attach a volume to an instance.

View File

@ -20,8 +20,10 @@
Contains classes required to issue API calls to Data ONTAP and OnCommand DFM.
"""
import random
import ssl
import urllib
from eventlet import greenthread
from eventlet import semaphore
from lxml import etree
@ -66,21 +68,24 @@ class NaServer(object):
URL_FILER = 'servlets/netapp.servlets.admin.XMLrequest_filer'
URL_DFM = 'apis/XMLrequest'
NETAPP_NS = 'http://www.netapp.com/filer/admin'
STYLE_LOGIN_PASSWORD = 'basic_auth'
STYLE_CERTIFICATE = 'certificate_auth'
def __init__(self, host, server_type=SERVER_TYPE_FILER,
transport_type=TRANSPORT_TYPE_HTTP,
style=STYLE_LOGIN_PASSWORD, username=None,
password=None, port=None, api_trace_pattern=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
self.set_server_type(server_type)
self.set_transport_type(transport_type)
self.set_style(style)
if port:
self.set_port(port)
self._username = username
self._password = password
self._private_key_file = private_key_file
self._certificate_file = certificate_file
self._ca_certificate_file = ca_certificate_file
self._certificate_host_validation = certificate_host_validation
self._refresh_conn = True
if api_trace_pattern is not None:
@ -189,10 +194,8 @@ class NaServer(object):
"""Invoke the API on the server."""
if not na_element or not isinstance(na_element, NaElement):
raise ValueError('NaElement must be supplied to invoke API')
request, request_element = self._create_request(na_element,
enable_tunneling)
if not hasattr(self, '_opener') or not self._opener \
or self._refresh_conn:
self._build_opener()
@ -223,14 +226,14 @@ class NaServer(object):
result = self.send_http_request(na_element, enable_tunneling)
if result.has_attr('status') and result.get_attr('status') == 'passed':
return result
code = result.get_attr('errno')\
or result.get_child_content('errorno')\
code = result.get_attr('errno') \
or result.get_child_content('errorno') \
or 'ESTATUSFAILED'
if code == ESIS_CLONE_NOT_LICENSED:
msg = 'Clone operation failed: FlexClone not licensed.'
else:
msg = result.get_attr('reason')\
or result.get_child_content('reason')\
msg = result.get_attr('reason') \
or result.get_child_content('reason') \
or 'Execution status is failed due to unknown reason'
raise NaApiError(code, msg)
@ -299,10 +302,10 @@ class NaServer(object):
self._url)
def _build_opener(self):
if self._auth_style == NaServer.STYLE_LOGIN_PASSWORD:
auth_handler = self._create_basic_auth_handler()
else:
if self._private_key_file and self._certificate_file:
auth_handler = self._create_certificate_auth_handler()
else:
auth_handler = self._create_basic_auth_handler()
opener = urllib.request.build_opener(auth_handler)
self._opener = opener
@ -314,7 +317,17 @@ class NaServer(object):
return auth_handler
def _create_certificate_auth_handler(self):
raise NotImplementedError()
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
if not self._certificate_host_validation:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
if self._certificate_file and self._private_key_file:
context.load_cert_chain(certfile=self._certificate_file,
keyfile=self._private_key_file)
if self._ca_certificate_file:
context.load_verify_locations(cafile=self._ca_certificate_file)
auth_handler = urllib.request.HTTPSHandler(context=context)
return auth_handler
def __str__(self):
return "server: %s" % self._host
@ -606,10 +619,9 @@ class SSHUtil(object):
response = stdout.channel.recv(999)
if expected_prompt_text not in response.strip().decode():
msg = _("Unexpected output. Expected [%(expected)s] but "
"received [%(output)s]") % {
'expected': expected_prompt_text,
'output': response.strip(),
}
"received [%(output)s]")\
% {'expected': expected_prompt_text,
'output': response.strip(), }
LOG.error(msg)
stdin.close()
stdout.close()
@ -651,7 +663,6 @@ REST_NAMESPACE_EOBJECTNOTFOUND = ('72090006', '72090006')
class RestNaServer(object):
TRANSPORT_TYPE_HTTP = 'http'
TRANSPORT_TYPE_HTTPS = 'https'
HTTP_PORT = '80'
@ -664,12 +675,18 @@ class RestNaServer(object):
def __init__(self, host, transport_type=TRANSPORT_TYPE_HTTP,
ssl_cert_path=None, username=None, password=None, port=None,
api_trace_pattern=None):
api_trace_pattern=None,
private_key_file=None, certificate_file=None,
ca_certificate_file=None, certificate_host_validation=None):
self._host = host
self.set_transport_type(transport_type)
self.set_port(port=port)
self._username = username
self._password = password
self._private_key_file = private_key_file
self._certificate_file = certificate_file
self._ca_certificate_file = ca_certificate_file
self._certificate_host_validation = certificate_host_validation
if api_trace_pattern is not None:
na_utils.setup_api_trace_pattern(api_trace_pattern)
@ -799,9 +816,12 @@ class RestNaServer(object):
max_retries = Retry(total=5, connect=5, read=2, backoff_factor=1)
adapter = HTTPAdapter(max_retries=max_retries)
self._session.mount('%s://' % self._protocol, adapter)
self._session.auth = self._create_basic_auth_handler()
self._session.verify = self._ssl_verify
if self._private_key_file and self._certificate_file:
self._session.cert, self._session.verify\
= self._create_certificate_auth_handler()
else:
self._session.auth = self._create_basic_auth_handler()
self._session.verify = self._ssl_verify
self._session.headers = headers
def _build_headers(self, enable_tunneling):
@ -819,6 +839,20 @@ class RestNaServer(object):
"""Creates and returns a basic HTTP auth handler."""
return auth.HTTPBasicAuth(self._username, self._password)
def _create_certificate_auth_handler(self):
"""Creates and returns a certificate auth handler."""
self._certificate_host_validation = self._session.verify
if self._certificate_file and self._private_key_file \
and self._ca_certificate_file:
self._session.cert = (self._certificate_file,
self._private_key_file)
if self._certificate_host_validation:
self._session.verify = self._ca_certificate_file
elif self._certificate_file and self._private_key_file:
self._session.cert = (self._certificate_file,
self._private_key_file)
return self._session.cert, self._session.verify
@volume_utils.trace_api(
filter_function=na_utils.trace_filter_func_rest_api)
def send_http_request(self, method, url, body, headers):

View File

@ -38,13 +38,37 @@ class Client(object, metaclass=volume_utils.TraceWrapperMetaclass):
username = kwargs['username']
password = kwargs['password']
api_trace_pattern = kwargs['api_trace_pattern']
self.connection = netapp_api.NaServer(
host=host,
transport_type=kwargs['transport_type'],
port=kwargs['port'],
username=username,
password=password,
api_trace_pattern=api_trace_pattern)
private_key_file = kwargs['private_key_file']
certificate_file = kwargs['certificate_file']
ca_certificate_file = kwargs['ca_certificate_file']
certificate_host_validation = kwargs['certificate_host_validation']
if private_key_file and certificate_file and ca_certificate_file:
self.connection = netapp_api.NaServer(
host=host,
transport_type='https',
port=kwargs['port'],
private_key_file=private_key_file,
certificate_file=certificate_file,
ca_certificate_file=ca_certificate_file,
certificate_host_validation=certificate_host_validation,
api_trace_pattern=api_trace_pattern)
elif private_key_file and certificate_file:
self.connection = netapp_api.NaServer(
host=host,
transport_type='https',
port=kwargs['port'],
private_key_file=private_key_file,
certificate_file=certificate_file,
certificate_host_validation=certificate_host_validation,
api_trace_pattern=api_trace_pattern)
else:
self.connection = netapp_api.NaServer(
host=host,
transport_type=kwargs['transport_type'],
port=kwargs['port'],
username=username,
password=password,
api_trace_pattern=api_trace_pattern)
self.ssh_client = self._init_ssh_client(host, username, password)

View File

@ -71,14 +71,38 @@ class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass):
username = kwargs['username']
password = kwargs['password']
api_trace_pattern = kwargs['api_trace_pattern']
self.connection = netapp_api.RestNaServer(
host=host,
transport_type=kwargs['transport_type'],
ssl_cert_path=kwargs.pop('ssl_cert_path'),
port=kwargs['port'],
username=username,
password=password,
api_trace_pattern=api_trace_pattern)
private_key_file = kwargs['private_key_file']
certificate_file = kwargs['certificate_file']
ca_certificate_file = kwargs['ca_certificate_file']
certificate_host_validation = kwargs['certificate_host_validation']
if private_key_file and certificate_file and ca_certificate_file:
self.connection = netapp_api.RestNaServer(
host=host,
transport_type='https',
port=kwargs['port'],
private_key_file=private_key_file,
certificate_file=certificate_file,
ca_certificate_file=ca_certificate_file,
certificate_host_validation=certificate_host_validation,
api_trace_pattern=api_trace_pattern)
elif private_key_file and certificate_file:
self.connection = netapp_api.RestNaServer(
host=host,
transport_type='https',
port=kwargs['port'],
private_key_file=private_key_file,
certificate_file=certificate_file,
certificate_host_validation=certificate_host_validation,
api_trace_pattern=api_trace_pattern)
else:
self.connection = netapp_api.RestNaServer(
host=host,
transport_type=kwargs['transport_type'],
ssl_cert_path=kwargs.pop('ssl_cert_path'),
port=kwargs['port'],
username=username,
password=password,
api_trace_pattern=api_trace_pattern)
self.async_rest_timeout = kwargs.get('async_rest_timeout', 60)

View File

@ -49,7 +49,6 @@ from cinder.volume.drivers.netapp import utils as na_utils
from cinder.volume.drivers import nfs
from cinder.volume import volume_utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
HOUSEKEEPING_INTERVAL_SECONDS = 600 # ten minutes
@ -67,8 +66,10 @@ class NetAppNfsDriver(driver.ManageableVD,
# ThirdPartySystems wiki page
CI_WIKI_NAME = "NetApp_CI"
REQUIRED_FLAGS = ['netapp_login', 'netapp_password',
'netapp_server_hostname']
REQUIRED_FLAGS_BASIC = ['netapp_login', 'netapp_password',
'netapp_server_hostname']
REQUIRED_FLAGS_CERT = ['netapp_private_key_file',
'netapp_certificate_file']
DEFAULT_FILTER_FUNCTION = 'capabilities.utilization < 70'
DEFAULT_GOODNESS_FUNCTION = '100 - capabilities.utilization'
@ -81,6 +82,8 @@ class NetAppNfsDriver(driver.ManageableVD,
super(NetAppNfsDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(na_opts.netapp_connection_opts)
self.configuration.append_config_values(na_opts.netapp_basicauth_opts)
self.configuration.append_config_values(
na_opts.netapp_certificateauth_opts)
self.configuration.append_config_values(na_opts.netapp_transport_opts)
self.configuration.append_config_values(na_opts.netapp_img_cache_opts)
self.configuration.append_config_values(na_opts.netapp_nfs_extra_opts)
@ -90,7 +93,12 @@ class NetAppNfsDriver(driver.ManageableVD,
def do_setup(self, context):
super(NetAppNfsDriver, self).do_setup(context)
self._context = context
na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration)
if self.configuration.netapp_private_key_file or\
self.configuration.netapp_certificate_file:
na_utils.check_flags(self.REQUIRED_FLAGS_CERT,
self.configuration)
else:
na_utils.check_flags(self.REQUIRED_FLAGS_BASIC, self.configuration)
self.zapi_client = None
def check_for_setup_error(self):
@ -542,6 +550,7 @@ class NetAppNfsDriver(driver.ManageableVD,
def _do_clone_rel_img_cache(self, src, dst, share, cache_file):
"""Do clone operation w.r.t image cache file."""
@utils.synchronized(cache_file, external=True)
def _do_clone():
dir = self._get_mount_point_for_share(share)
@ -552,6 +561,7 @@ class NetAppNfsDriver(driver.ManageableVD,
share=share)
src_path = '%s/%s' % (dir, src)
os.utime(src_path, None)
_do_clone()
def _clean_image_cache(self):

View File

@ -63,8 +63,10 @@ class NetAppNVMeStorageLibrary(
# do not increment this as it may be used in volume type definitions.
VERSION = "1.0.0"
REQUIRED_FLAGS = ['netapp_login', 'netapp_password',
'netapp_server_hostname']
REQUIRED_FLAGS_BASIC = ['netapp_login', 'netapp_password',
'netapp_server_hostname']
REQUIRED_FLAGS_CERT = ['netapp_private_key_file',
'netapp_certificate_file']
ALLOWED_NAMESPACE_OS_TYPES = ['aix', 'linux', 'vmware', 'windows']
ALLOWED_SUBSYSTEM_HOST_TYPES = ['aix', 'linux', 'vmware', 'windows']
DEFAULT_NAMESPACE_OS = 'linux'
@ -93,6 +95,8 @@ class NetAppNVMeStorageLibrary(
self.configuration = kwargs['configuration']
self.configuration.append_config_values(na_opts.netapp_connection_opts)
self.configuration.append_config_values(na_opts.netapp_basicauth_opts)
self.configuration.append_config_values(
na_opts.netapp_certificateauth_opts)
self.configuration.append_config_values(na_opts.netapp_transport_opts)
self.configuration.append_config_values(
na_opts.netapp_provisioning_opts)
@ -107,7 +111,13 @@ class NetAppNVMeStorageLibrary(
self.loopingcalls = loopingcalls.LoopingCalls()
def do_setup(self, context):
na_utils.check_flags(self.REQUIRED_FLAGS, self.configuration)
if self.configuration.netapp_private_key_file or\
self.configuration.netapp_certificate_file:
na_utils.check_flags(self.REQUIRED_FLAGS_CERT,
self.configuration)
else:
na_utils.check_flags(self.REQUIRED_FLAGS_BASIC,
self.configuration)
self.namespace_ostype = (self.configuration.netapp_namespace_ostype
or self.DEFAULT_NAMESPACE_OS)
self.host_type = (self.configuration.netapp_host_type

View File

@ -52,6 +52,7 @@ def get_backend_configuration(backend_name):
config.append_config_values(na_opts.netapp_connection_opts)
config.append_config_values(na_opts.netapp_transport_opts)
config.append_config_values(na_opts.netapp_basicauth_opts)
config.append_config_values(na_opts.netapp_certificateauth_opts)
config.append_config_values(na_opts.netapp_provisioning_opts)
config.append_config_values(na_opts.netapp_cluster_opts)
config.append_config_values(na_opts.netapp_san_opts)
@ -72,6 +73,11 @@ def get_client_for_backend(backend_name, vserver_name=None, force_rest=False):
username=config.netapp_login,
password=config.netapp_password,
hostname=config.netapp_server_hostname,
private_key_file=config.netapp_private_key_file,
certificate_file=config.netapp_certificate_file,
ca_certificate_file=config.netapp_ca_certificate_file,
certificate_host_validation=
config.netapp_certificate_host_validation,
port=config.netapp_server_port,
vserver=vserver_name or config.netapp_vserver,
trace=volume_utils.TRACE_API,
@ -83,12 +89,16 @@ def get_client_for_backend(backend_name, vserver_name=None, force_rest=False):
username=config.netapp_login,
password=config.netapp_password,
hostname=config.netapp_server_hostname,
private_key_file=config.netapp_private_key_file,
certificate_file=config.netapp_certificate_file,
ca_certificate_file=config.netapp_ca_certificate_file,
certificate_host_validation=
config.netapp_certificate_host_validation,
port=config.netapp_server_port,
vserver=vserver_name or config.netapp_vserver,
trace=volume_utils.TRACE_API,
api_trace_pattern=config.netapp_api_trace_pattern,
async_rest_timeout=config.netapp_async_rest_timeout)
return client

View File

@ -88,6 +88,80 @@ netapp_basicauth_opts = [
'specified in the netapp_login option.'),
secret=True), ]
netapp_certificateauth_opts = [
cfg.StrOpt('netapp_private_key_file',
sample_default='/path/to/private_key.key',
help=("""
This option is applicable for both self signed and ca
verified certificates.
For self signed certificate: Absolute path to the file
containing the private key associated with the self
signed certificate. It is a sensitive file that should
be kept secure and protected. The private key is used
to sign the certificate and establish the authenticity
and integrity of the certificate during the
authentication process.
For ca verified certificate: Absolute path to the file
containing the private key associated with the
certificate. It is generated when creating the
certificate signingrequest (CSR) and should be kept
secure and protected. The private key is used to sign
the CSR and later used to establish secure connections
and authenticate the entity.
"""),
secret=True),
cfg.StrOpt('netapp_certificate_file',
sample_default='/path/to/certificate.pem',
help=("""
This option is applicable for both self signed and ca
verified certificates.
For self signed certificate: Absolute path to the file
containing the self-signed digital certificate itself.
It includes information about the entity such as the
common name (e.g., domain name), organization details,
validity period, and public key. The certificate file
is generated based on the private key and is used by
clients or systems to verify the entity identity during
the authentication process.
For ca verified certificate: Absolute path to the file
containing the digital certificate issued by the
trusted third-party certificate authority (CA). It
includes information about the entity identity, public
key, and the CA that issued the certificate. The
certificate file is used by clients or systems to verify
the authenticity and integrity of the entity during the
authentication process.
"""),
secret=True),
cfg.StrOpt('netapp_ca_certificate_file',
sample_default='/path/to/ca_certificate.crt',
help=("""
This option is applicable only for a ca verified
certificate.
Ca verified file: Absolute path to the file containing
the public key certificate of the trusted third-party
certificate authority (CA) that issued the certificate.
It is used by clients or systems to validate the
authenticity of the certificate presented by the
entity. The CA certificate file is typically pre
configured in the trust store of clients or systems to
establish trust in certificates issued by that CA.
"""),
secret=True),
cfg.BoolOpt('netapp_certificate_host_validation',
default=False,
help=('This option is used only if netapp_private_key_file'
' and netapp_certificate_file files are passed in the'
' configuration.'
' By default certificate verification is disabled'
' and to verify the certificates please set the value'
' to True.')), ]
netapp_provisioning_opts = [
cfg.FloatOpt('netapp_size_multiplier',
default=NETAPP_SIZE_MULTIPLIER_DEFAULT,
@ -245,6 +319,7 @@ CONF.register_opts(netapp_proxy_opts, group=conf.SHARED_CONF_GROUP)
CONF.register_opts(netapp_connection_opts, group=conf.SHARED_CONF_GROUP)
CONF.register_opts(netapp_transport_opts, group=conf.SHARED_CONF_GROUP)
CONF.register_opts(netapp_basicauth_opts, group=conf.SHARED_CONF_GROUP)
CONF.register_opts(netapp_certificateauth_opts, group=conf.SHARED_CONF_GROUP)
CONF.register_opts(netapp_cluster_opts, group=conf.SHARED_CONF_GROUP)
CONF.register_opts(netapp_provisioning_opts, group=conf.SHARED_CONF_GROUP)
CONF.register_opts(netapp_img_cache_opts, group=conf.SHARED_CONF_GROUP)

View File

@ -0,0 +1,9 @@
---
features:
- |
The NetApp ONTAP driver now supports Certificate-Based-Authentication (CBA)
for operators that desire certificate based authentication instead of user
and password.
Note: The options for cert-auth take precedence, if all the auth options
are defined in the config (both cert and legacy), the legacy ones are
ignored.