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