From 0553eb78bea25bff078d50bd42e7391b03b138e5 Mon Sep 17 00:00:00 2001 From: Kiran Pawar Date: Thu, 16 Mar 2023 10:22:35 +0000 Subject: [PATCH] [NetApp] Recreate security cert during vserver create. The certificate is automatically created on NetApp with 1 year i.e. 365 days of expiration time, and admin needs to manually extend it. It would be nice Manila can take care to create certs with admin configuable expiration time. Manila should first create the new cert with given expiration time and if successful, delete the old cert. Closes-bug: #2011693 Change-Id: I37e52b94dc492e91fe9e673b3619e6716737d39a --- .../netapp/dataontap/client/client_cmode.py | 117 +++++++++++++++++- .../dataontap/client/client_cmode_rest.py | 76 +++++++++++- .../dataontap/cluster_mode/lib_multi_svm.py | 3 +- manila/share/drivers/netapp/options.py | 8 ++ .../drivers/netapp/dataontap/client/fakes.py | 54 ++++++++ .../dataontap/client/test_client_cmode.py | 70 ++++++++++- .../client/test_client_cmode_rest.py | 58 ++++++++- .../cluster_mode/test_lib_multi_svm.py | 3 +- .../share/drivers/netapp/dataontap/fakes.py | 2 + ...tificate-for-vserver-aba543211ae6b811.yaml | 8 ++ 10 files changed, 391 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/netapp-add-new-security-certificate-for-vserver-aba543211ae6b811.yaml diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index d7affdbf65..3bd450b815 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -175,7 +175,8 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): @na_utils.trace def create_vserver(self, vserver_name, root_volume_aggregate_name, - root_volume_name, aggregate_names, ipspace_name): + root_volume_name, aggregate_names, ipspace_name, + security_cert_expire_days): """Creates new vserver and assigns aggregates.""" self._create_vserver( vserver_name, aggregate_names, ipspace_name, @@ -183,6 +184,7 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): root_volume_aggregate_name=root_volume_aggregate_name, root_volume_security_style='unix', name_server_switch='file') + self._modify_security_cert(vserver_name, security_cert_expire_days) @na_utils.trace def create_vserver_dp_destination(self, vserver_name, aggregate_names, @@ -230,6 +232,119 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): } self.send_request('vserver-modify', modify_args) + @na_utils.trace + def _modify_security_cert(self, vserver_name, security_cert_expire_days): + """Create new security certificate with given expire days.""" + + # Do not modify security certificate if specified expire days are + # equal to default security certificate expire days i.e. 365. + if security_cert_expire_days == 365: + return + + api_args = { + 'query': { + 'certificate-info': { + 'vserver': vserver_name, + 'common-name': vserver_name, + 'certificate-authority': vserver_name, + 'type': 'server', + }, + }, + 'desired-attributes': { + 'certificate-info': { + 'serial-number': None, + }, + }, + } + result = self.send_iter_request('security-certificate-get-iter', + api_args) + try: + old_certificate_info_list = result.get_child_by_name( + 'attributes-list') + except AttributeError: + LOG.warning('Could not retrieve certificate-info for vserver ' + '%(server)s.', {'server': vserver_name}) + return + + old_serial_nums = [] + for certificate_info in old_certificate_info_list.get_children(): + serial_num = certificate_info.get_child_content('serial-number') + old_serial_nums.append(serial_num) + + try: + create_args = { + 'vserver': vserver_name, + 'common-name': vserver_name, + 'type': 'server', + 'expire-days': security_cert_expire_days, + } + self.send_request('security-certificate-create', create_args) + except netapp_api.NaApiError as e: + LOG.warning("Failed to create new security certificate: %s - %s", + e.code, e.message) + return + + api_args = { + 'query': { + 'certificate-info': { + 'vserver': vserver_name, + 'common-name': vserver_name, + 'certificate-authority': vserver_name, + 'type': 'server', + }, + }, + 'desired-attributes': { + 'certificate-info': { + 'serial-number': None, + }, + }, + } + + result = self.send_iter_request('security-certificate-get-iter', + api_args) + try: + new_certificate_info_list = result.get_child_by_name( + 'attributes-list') + except AttributeError: + LOG.warning('Could not retrieve certificate-info for vserver ' + '%(server)s.', {'server': vserver_name}) + return + + for certificate_info in new_certificate_info_list.get_children(): + serial_num = certificate_info.get_child_content('serial-number') + if serial_num not in old_serial_nums: + try: + ssl_modify_args = { + 'certificate-authority': vserver_name, + 'common-name': vserver_name, + 'certificate-serial-number': serial_num, + 'vserver': vserver_name, + 'client-authentication-enabled': 'false', + 'server-authentication-enabled': 'true', + } + self.send_request('security-ssl-modify', ssl_modify_args) + except netapp_api.NaApiError as e: + LOG.debug('Failed to modify SSL for security certificate ' + 'with serial number %s: %s - %s', serial_num, + e.code, e.message) + + # Delete all old security certificates + for certificate_info in old_certificate_info_list.get_children(): + serial_num = certificate_info.get_child_content('serial-number') + delete_args = { + 'certificate-authority': vserver_name, + 'common-name': vserver_name, + 'serial-number': serial_num, + 'type': 'server', + 'vserver': vserver_name, + } + try: + self.send_request('security-certificate-delete', delete_args) + except netapp_api.NaApiError as e: + LOG.warning('Failed to delete security certificate with ' + 'serial number %s: %s - %s', serial_num, e.code, + e.message) + @na_utils.trace def get_vserver_info(self, vserver_name): """Retrieves Vserver info.""" diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py b/manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py index b09f10e4fa..5fa6b13cb1 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py @@ -46,6 +46,7 @@ CUTOVER_ACTION_MAP = { DEFAULT_TIMEOUT = 15 DEFAULT_TCP_MAX_XFER_SIZE = 65536 DEFAULT_UDP_MAX_XFER_SIZE = 32768 +DEFAULT_SECURITY_CERT_EXPIRE_DAYS = 365 class NetAppRestClient(object): @@ -4120,7 +4121,8 @@ class NetAppRestClient(object): @na_utils.trace def create_vserver(self, vserver_name, root_volume_aggregate_name, - root_volume_name, aggregate_names, ipspace_name): + root_volume_name, aggregate_names, ipspace_name, + security_cert_expire_days): """Creates new vserver and assigns aggregates.""" # NOTE(nahimsouza): root_volume_aggregate_name and root_volume_name @@ -4129,6 +4131,7 @@ class NetAppRestClient(object): self._create_vserver( vserver_name, aggregate_names, ipspace_name, name_server_switch=['files']) + self._modify_security_cert(vserver_name, security_cert_expire_days) @na_utils.trace def create_vserver_dp_destination(self, vserver_name, aggregate_names, @@ -4161,6 +4164,77 @@ class NetAppRestClient(object): self.send_request('/svm/svms', 'post', body=body) + @na_utils.trace + def _modify_security_cert(self, vserver_name, security_cert_expire_days): + """Create new security certificate with given expire days.""" + + # Do not modify security certificate if specified expire days are + # equal to default security certificate expire days i.e. 365. + if security_cert_expire_days == DEFAULT_SECURITY_CERT_EXPIRE_DAYS: + return + + query = { + 'common-name': vserver_name, + 'ca': vserver_name, + 'type': 'server', + 'svm.name': vserver_name, + } + result = self.send_request('/security/certificates', + 'get', query=query) + old_certificate_info_list = result.get('records', []) + if not old_certificate_info_list: + LOG.warning("Unable to retrieve certificate-info for vserver " + "%(server)s'. Cannot set the certificate expiry to " + "%s(conf)s. ", {'server': vserver_name, + 'conf': security_cert_expire_days}) + return + + body = { + 'common-name': vserver_name, + 'type': 'server', + 'svm.name': vserver_name, + 'expiry_time': f'P{security_cert_expire_days}DT', + } + query = { + 'return_records': 'true' + } + result = self.send_request('/security/certificates', + 'post', body=body, query=query) + new_certificate_info_list = result.get('records', []) + if not new_certificate_info_list: + LOG.warning('Failed to create new security certificate for ' + 'vserver %(server)s.', {'server': vserver_name}) + return + + for certificate_info in new_certificate_info_list: + cert_uuid = certificate_info.get('uuid', None) + svm = certificate_info.get('svm', []) + svm_uuid = svm.get('uuid', None) + if not svm_uuid or not cert_uuid: + continue + + try: + body = { + 'certificate': { + 'uuid': cert_uuid, + }, + 'client_enabled': 'false', + } + self.send_request(f'/svm/svms/{svm_uuid}', 'patch', + body=body) + except netapp_api.api.NaApiError: + LOG.debug('Failed to modify SSL for vserver ' + '%(server)s.', {'server': vserver_name}) + + # Delete all old security certificates + for certificate_info in old_certificate_info_list: + uuid = certificate_info.get('uuid', None) + try: + self.send_request(f'/security/certificates/{uuid}', 'delete') + except netapp_api.api.NaApiError: + LOG.error("Failed to delete security certificate for vserver " + "%s.", vserver_name) + @na_utils.trace def list_node_data_ports(self, node): """List data ports from node.""" diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py index cc6266adea..26f4875e22 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_multi_svm.py @@ -343,7 +343,8 @@ class NetAppCmodeMultiSVMFileStorageLibrary( self.configuration.netapp_root_volume_aggregate, self.configuration.netapp_root_volume, aggr_set, - ipspace_name) + ipspace_name, + self.configuration.netapp_security_cert_expire_days) vserver_client = self._get_api_client(vserver=vserver_name) diff --git a/manila/share/drivers/netapp/options.py b/manila/share/drivers/netapp/options.py index b90e9e296a..6eeecc1fbb 100644 --- a/manila/share/drivers/netapp/options.py +++ b/manila/share/drivers/netapp/options.py @@ -190,6 +190,14 @@ netapp_provisioning_opts = [ default=60, # Default to one minutes help='Sets maximum amount of time in seconds to wait for a ' 'synchronous ONTAP REST API operation to be completed.'), + cfg.IntOpt('netapp_security_cert_expire_days', + min=1, + max=3652, + default=365, + help='Defines the expiration time (in days) for the ' + 'certificate created during the vserver creation. This ' + 'option only applies when the option ' + 'driver_handles_share_servers is set to True.'), ] netapp_cluster_opts = [ diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py index c22bfe4ab8..674e478036 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -50,6 +50,8 @@ VSERVER_PEER_STATE = 'peered' ADMIN_VSERVER_NAME = 'fake_admin_vserver' NODE_VSERVER_NAME = 'fake_node_vserver' NFS_VERSIONS = ['nfs3', 'nfs4.0'] +SECURITY_CERT_DEFAULT_EXPIRE_DAYS = 365 +SECURITY_CERT_LARGE_EXPIRE_DAYS = 3652 ROOT_AGGREGATE_NAMES = ('root_aggr1', 'root_aggr2') ROOT_VOLUME_AGGREGATE_NAME = 'fake_root_aggr' ROOT_VOLUME_NAME = 'fake_root_volume' @@ -315,6 +317,18 @@ VSERVER_GET_RESPONSE = etree.XML(""" 'aggr2': SHARE_AGGREGATE_NAMES[1], }) +SECURITY_CERT_GET_RESPONSE = etree.XML(""" + + + + %(vserver)s + 12345 + + + 1 + +""" % {'vserver': VSERVER_NAME}) + VSERVER_DATA_LIST_RESPONSE = etree.XML(""" @@ -4451,6 +4465,46 @@ SERVICE_POLICIES_REST = { 'num_records': 1, } +SECURITY_CERT_GET_RESPONSE_REST = { + 'records': [ + { + 'uuid': 'fake_cert_uuid', + 'serial_number': 'fake_serial_number', + 'key_size': 0, + 'hash_function': "sha256", + 'common_name': "fake_common_name", + 'name': "cert1", + 'ca': 'fake_ca', + 'expiry_time': 'fake_expiry_time', + 'svm': { + 'name': VSERVER_NAME, + 'uuid': 'fake_uuid', + }, + }, + ], + 'num_records': 1, +} + +SECURITY_CERT_POST_RESPONSE_REST = { + 'records': [ + { + 'uuid': 'fake_cert_uuid', + 'serial_number': 'fake_serial_number', + 'key_size': 0, + 'hash_function': "sha256", + 'common_name': "fake_common_name", + 'name': "cert1", + 'ca': 'fake_ca', + 'expiry_time': 'fake_expiry_time', + 'svm': { + 'name': VSERVER_NAME, + 'uuid': 'fake_uuid', + }, + }, + ], + 'num_records': 1, +} + GET_SNAPMIRROR_POLICIES_REST = { "records": [ diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py index 1ebf0235df..0e6b703b6d 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode.py @@ -420,6 +420,9 @@ class NetAppClientCmodeTestCase(test.TestCase): def test_create_vserver_no_ipspace(self): self.mock_object(self.client, 'send_request') + self.mock_object(self.client, + '_modify_security_cert', + mock.Mock()) vserver_create_args = { 'vserver-name': fake.VSERVER_NAME, @@ -438,16 +441,22 @@ class NetAppClientCmodeTestCase(test.TestCase): fake.ROOT_VOLUME_AGGREGATE_NAME, fake.ROOT_VOLUME_NAME, fake.SHARE_AGGREGATE_NAMES, - None) + None, + fake.SECURITY_CERT_LARGE_EXPIRE_DAYS) self.client.send_request.assert_has_calls([ mock.call('vserver-create', vserver_create_args), mock.call('vserver-modify', vserver_modify_args)]) + self.client._modify_security_cert.assert_called_with( + fake.VSERVER_NAME, fake.SECURITY_CERT_LARGE_EXPIRE_DAYS) def test_create_vserver_with_ipspace(self): self.client.features.add_feature('IPSPACES') self.mock_object(self.client, 'send_request') + self.mock_object(self.client, + '_modify_security_cert', + mock.Mock()) vserver_create_args = { 'vserver-name': fake.VSERVER_NAME, @@ -467,11 +476,65 @@ class NetAppClientCmodeTestCase(test.TestCase): fake.ROOT_VOLUME_AGGREGATE_NAME, fake.ROOT_VOLUME_NAME, fake.SHARE_AGGREGATE_NAMES, - fake.IPSPACE_NAME) + fake.IPSPACE_NAME, + fake.SECURITY_CERT_LARGE_EXPIRE_DAYS) self.client.send_request.assert_has_calls([ mock.call('vserver-create', vserver_create_args), mock.call('vserver-modify', vserver_modify_args)]) + self.client._modify_security_cert.assert_called_with( + fake.VSERVER_NAME, fake.SECURITY_CERT_LARGE_EXPIRE_DAYS) + + def test__modify_security_cert(self): + + certificate_create_args = { + 'vserver': fake.VSERVER_NAME, + 'common-name': fake.VSERVER_NAME, + 'type': 'server', + 'expire-days': fake.SECURITY_CERT_LARGE_EXPIRE_DAYS, + } + + self.mock_object(self.client, 'send_request') + api_response = netapp_api.NaElement(fake.SECURITY_CERT_GET_RESPONSE) + self.mock_object(self.client, + 'send_iter_request', + mock.Mock(return_value=api_response)) + certificate_get_args = { + 'query': { + 'certificate-info': { + 'vserver': fake.VSERVER_NAME, + 'common-name': fake.VSERVER_NAME, + 'certificate-authority': fake.VSERVER_NAME, + 'type': 'server', + }, + }, + 'desired-attributes': { + 'certificate-info': { + 'serial-number': None, + }, + }, + } + + certificate_delete_args = { + 'certificate-authority': fake.VSERVER_NAME, + 'common-name': fake.VSERVER_NAME, + 'serial-number': '12345', + 'type': 'server', + 'vserver': fake.VSERVER_NAME, + } + + self.client._modify_security_cert( + fake.VSERVER_NAME, + fake.SECURITY_CERT_LARGE_EXPIRE_DAYS) + + self.client.send_request.assert_has_calls([ + mock.call( + 'security-certificate-create', certificate_create_args), + mock.call( + 'security-certificate-delete', certificate_delete_args)]) + + self.client.send_iter_request.assert_has_calls([ + mock.call('security-certificate-get-iter', certificate_get_args)]) def test_create_vserver_dp_destination(self): @@ -506,7 +569,8 @@ class NetAppClientCmodeTestCase(test.TestCase): fake.ROOT_VOLUME_AGGREGATE_NAME, fake.ROOT_VOLUME_NAME, fake.SHARE_AGGREGATE_NAMES, - fake.IPSPACE_NAME) + fake.IPSPACE_NAME, + fake.SECURITY_CERT_LARGE_EXPIRE_DAYS) def test_get_vserver_root_volume_name(self): diff --git a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode_rest.py b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode_rest.py index 2d4ccf8953..83ea0b7737 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode_rest.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/test_client_cmode_rest.py @@ -4808,13 +4808,69 @@ class NetAppRestCmodeClientTestCase(test.TestCase): def test_create_vserver(self): mock = self.mock_object(self.client, '_create_vserver') + self.mock_object(self.client, '_modify_security_cert', + mock.Mock(return_value=[])) self.client.create_vserver(fake.VSERVER_NAME, None, None, [fake.SHARE_AGGREGATE_NAME], - fake.IPSPACE_NAME) + fake.IPSPACE_NAME, + fake.SECURITY_CERT_DEFAULT_EXPIRE_DAYS) mock.assert_called_once_with(fake.VSERVER_NAME, [fake.SHARE_AGGREGATE_NAME], fake.IPSPACE_NAME, name_server_switch=['files']) + self.client._modify_security_cert.assert_called_once_with( + fake.VSERVER_NAME, + fake.SECURITY_CERT_DEFAULT_EXPIRE_DAYS) + + def test__modify_security_cert(self): + api_response = copy.deepcopy(fake.SECURITY_CERT_GET_RESPONSE_REST) + api_response2 = copy.deepcopy(fake.SECURITY_CERT_POST_RESPONSE_REST) + self.mock_object( + self.client, 'send_request', + mock.Mock(side_effect=[api_response, api_response2, None, None])) + + query = { + 'common-name': fake.VSERVER_NAME, + 'ca': fake.VSERVER_NAME, + 'type': 'server', + 'svm.name': fake.VSERVER_NAME, + } + old_cert_info = copy.deepcopy( + fake.SECURITY_CERT_GET_RESPONSE_REST['records'][0]) + old_cert_uuid = old_cert_info['uuid'] + + body1 = { + 'common-name': fake.VSERVER_NAME, + 'type': 'server', + 'svm.name': fake.VSERVER_NAME, + 'expiry_time': 'P' + str( + fake.SECURITY_CERT_LARGE_EXPIRE_DAYS) + 'DT', + } + query1 = { + 'return_records': 'true' + } + new_cert_info = copy.deepcopy( + fake.SECURITY_CERT_POST_RESPONSE_REST['records'][0]) + new_cert_uuid = new_cert_info['uuid'] + new_svm_uuid = new_cert_info['svm']['uuid'] + body2 = { + 'certificate': { + 'uuid': new_cert_uuid, + }, + 'client_enabled': 'false', + } + + self.client._modify_security_cert( + fake.VSERVER_NAME, + fake.SECURITY_CERT_LARGE_EXPIRE_DAYS) + + self.client.send_request.assert_has_calls([ + mock.call('/security/certificates', 'get', query=query), + mock.call('/security/certificates', 'post', body=body1, + query=query1), + mock.call(f'/svm/svms/{new_svm_uuid}', 'patch', body=body2), + mock.call(f'/security/certificates/{old_cert_uuid}', 'delete'), + ]) def test__broadcast_domain_exists(self): response = fake.FAKE_GET_BROADCAST_DOMAIN diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py index 6868007952..b525443bf9 100644 --- a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py +++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_multi_svm.py @@ -731,7 +731,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.NETWORK_INFO) self.library._client.create_vserver.assert_called_once_with( vserver_name, fake.ROOT_VOLUME_AGGREGATE, fake.ROOT_VOLUME, - set(fake.AGGREGATES), fake.IPSPACE) + set(fake.AGGREGATES), fake.IPSPACE, + fake.SECURITY_CERT_DEFAULT_EXPIRE_DAYS) self.library._get_api_client.assert_called_once_with( vserver=vserver_name) self.library._create_vserver_lifs.assert_called_once_with( diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py index cfc2c8fc21..2832fdf697 100644 --- a/manila/tests/share/drivers/netapp/dataontap/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py @@ -68,6 +68,8 @@ AGGR_POOL_NAME = 'manila_aggr_1' FLEXGROUP_POOL_NAME = 'flexgroup_pool' ROOT_AGGREGATES = ('root_aggr_1', 'root_aggr_2') ROOT_VOLUME_AGGREGATE = 'manila1' +SECURITY_CERT_DEFAULT_EXPIRE_DAYS = 365 +SECURITY_CERT_LARGE_EXPIRE_DAYS = 3652 ROOT_VOLUME = 'root' CLUSTER_NODE = 'cluster1_01' CLUSTER_NODES = ('cluster1_01', 'cluster1_02') diff --git a/releasenotes/notes/netapp-add-new-security-certificate-for-vserver-aba543211ae6b811.yaml b/releasenotes/notes/netapp-add-new-security-certificate-for-vserver-aba543211ae6b811.yaml new file mode 100644 index 0000000000..e66d9d1d9d --- /dev/null +++ b/releasenotes/notes/netapp-add-new-security-certificate-for-vserver-aba543211ae6b811.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + NetApp ONTAP driver now allows cloud operator to define security + certificate expire days for vserver. So instead of using vserver's default + security certificate with 365 expire days, cloud operator can ask backend + to create new security certificate with given expire days using config + option 'netapp_security_cert_expire_days'.