diff --git a/cinder/opts.py b/cinder/opts.py index 02747fa70f6..bb90c0cf552 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -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, 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 ebae46f0f6c..6c9751dfc83 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 @@ -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)) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py index 9849e515e24..25764ca86ae 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_base.py @@ -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 diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py index edf023b6d53..12c20f82520 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode.py @@ -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 diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py index 24077eda94b..b3dda1decdb 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/client/test_client_cmode_rest.py @@ -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 diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py index 12739e13850..cd4caae7c90 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/fakes.py @@ -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) diff --git a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_data_motion.py b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_data_motion.py index b0a830ec8af..2ac74eee300 100644 --- a/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_data_motion.py +++ b/cinder/tests/unit/volume/drivers/netapp/dataontap/utils/test_data_motion.py @@ -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) 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 80ec2e99400..eed57da2a83 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 @@ -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() diff --git a/cinder/tests/unit/volume/drivers/netapp/fakes.py b/cinder/tests/unit/volume/drivers/netapp/fakes.py index 2b560371c2f..e3fdfde7a9f 100644 --- a/cinder/tests/unit/volume/drivers/netapp/fakes.py +++ b/cinder/tests/unit/volume/drivers/netapp/fakes.py @@ -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 diff --git a/cinder/volume/drivers/netapp/dataontap/block_base.py b/cinder/volume/drivers/netapp/dataontap/block_base.py index 762486ecbd1..0efdabeb6b3 100644 --- a/cinder/volume/drivers/netapp/dataontap/block_base.py +++ b/cinder/volume/drivers/netapp/dataontap/block_base.py @@ -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. diff --git a/cinder/volume/drivers/netapp/dataontap/client/api.py b/cinder/volume/drivers/netapp/dataontap/client/api.py index 72d5d68aeea..20121c3c5ae 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/api.py +++ b/cinder/volume/drivers/netapp/dataontap/client/api.py @@ -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): diff --git a/cinder/volume/drivers/netapp/dataontap/client/client_base.py b/cinder/volume/drivers/netapp/dataontap/client/client_base.py index 65aec49b31a..de68ff4f21a 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_base.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_base.py @@ -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) 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 85e6516b418..edd6300308d 100644 --- a/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py +++ b/cinder/volume/drivers/netapp/dataontap/client/client_cmode_rest.py @@ -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) diff --git a/cinder/volume/drivers/netapp/dataontap/nfs_base.py b/cinder/volume/drivers/netapp/dataontap/nfs_base.py index eace7b4c063..2d3d59f0efd 100644 --- a/cinder/volume/drivers/netapp/dataontap/nfs_base.py +++ b/cinder/volume/drivers/netapp/dataontap/nfs_base.py @@ -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): diff --git a/cinder/volume/drivers/netapp/dataontap/nvme_library.py b/cinder/volume/drivers/netapp/dataontap/nvme_library.py index bec06518172..e8852696cbd 100644 --- a/cinder/volume/drivers/netapp/dataontap/nvme_library.py +++ b/cinder/volume/drivers/netapp/dataontap/nvme_library.py @@ -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 diff --git a/cinder/volume/drivers/netapp/dataontap/utils/utils.py b/cinder/volume/drivers/netapp/dataontap/utils/utils.py index 481b3f20121..01ab72bc7fc 100644 --- a/cinder/volume/drivers/netapp/dataontap/utils/utils.py +++ b/cinder/volume/drivers/netapp/dataontap/utils/utils.py @@ -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 diff --git a/cinder/volume/drivers/netapp/options.py b/cinder/volume/drivers/netapp/options.py index 1cd16931690..7a365f73a15 100644 --- a/cinder/volume/drivers/netapp/options.py +++ b/cinder/volume/drivers/netapp/options.py @@ -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) diff --git a/releasenotes/notes/certificate-based-authentication-for-netapp-drivers-b06a62df620aebc3.yaml b/releasenotes/notes/certificate-based-authentication-for-netapp-drivers-b06a62df620aebc3.yaml new file mode 100644 index 00000000000..0338d486386 --- /dev/null +++ b/releasenotes/notes/certificate-based-authentication-for-netapp-drivers-b06a62df620aebc3.yaml @@ -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.