From 0b04d8d671fb119d7cb1d5b508a148edc23890b3 Mon Sep 17 00:00:00 2001 From: Douglas Viroel Date: Tue, 9 Feb 2021 18:51:45 -0300 Subject: [PATCH] [NetApp] Add support for FPolicy native mode This patch adds support for automated creation of FPolicy policies and association to a share. The FPolicy configuration can be added using the extra-specs 'netapp:fpolicy_extensions_to_include', 'netapp:fpolicy_extensions_to_exclude' and 'netapp:fpolicy_file_operations'. Change-Id: I661de95bfb6f8e68b3a8c58663bb6055e9b809f6 Implements: bp netapp-fpolicy-support Signed-off-by: Douglas Viroel --- .../drivers/netapp/dataontap/client/api.py | 4 + .../netapp/dataontap/client/client_cmode.py | 400 +++++++++++++ .../netapp/dataontap/cluster_mode/lib_base.py | 411 +++++++++++++- .../dataontap/cluster_mode/lib_multi_svm.py | 52 +- manila/share/drivers/netapp/options.py | 13 +- manila/share/drivers/netapp/utils.py | 4 + .../drivers/netapp/dataontap/client/fakes.py | 100 ++++ .../dataontap/client/test_client_cmode.py | 358 ++++++++++++ .../dataontap/cluster_mode/test_lib_base.py | 536 ++++++++++++++++-- .../cluster_mode/test_lib_multi_svm.py | 66 ++- .../share/drivers/netapp/dataontap/fakes.py | 41 ++ ...-add-fpolicy-support-dd31628a1c8e64d6.yaml | 25 + 12 files changed, 1938 insertions(+), 72 deletions(-) create mode 100644 releasenotes/notes/netapp-add-fpolicy-support-dd31628a1c8e64d6.yaml diff --git a/manila/share/drivers/netapp/dataontap/client/api.py b/manila/share/drivers/netapp/dataontap/client/api.py index 1bfd397a03..ecc2373dca 100644 --- a/manila/share/drivers/netapp/dataontap/client/api.py +++ b/manila/share/drivers/netapp/dataontap/client/api.py @@ -40,6 +40,7 @@ EAPINOTFOUND = '13005' ESNAPSHOTNOTALLOWED = '13023' EVOLUMEOFFLINE = '13042' EINTERNALERROR = '13114' +EINVALIDINPUTERROR = '13115' EDUPLICATEENTRY = '13130' EVOLNOTCLONE = '13170' EVOLMOVE_CANNOT_MOVE_TO_CFO = '13633' @@ -57,6 +58,9 @@ EANOTHER_OP_ACTIVE = '17131' ERELATION_NOT_QUIESCED = '17127' ESOURCE_IS_DIFFERENT = '17105' EVOL_CLONE_BEING_SPLIT = '17151' +EPOLICYNOTFOUND = '18251' +EEVENTNOTFOUND = '18253' +ESCOPENOTFOUND = '18259' ESVMDR_CANNOT_PERFORM_OP_FOR_STATUS = '18815' diff --git a/manila/share/drivers/netapp/dataontap/client/client_cmode.py b/manila/share/drivers/netapp/dataontap/client/client_cmode.py index a750e88bf0..2799a4427f 100644 --- a/manila/share/drivers/netapp/dataontap/client/client_cmode.py +++ b/manila/share/drivers/netapp/dataontap/client/client_cmode.py @@ -4870,3 +4870,403 @@ class NetAppCmodeClient(client_base.NetAppBaseClient): def is_svm_dr_supported(self): return self.features.SVM_DR + + def create_fpolicy_event(self, event_name, protocol, file_operations): + """Creates a new fpolicy policy event. + + :param event_name: name of the new fpolicy event + :param protocol: name of protocol for which event is created. Possible + values are: 'nfsv3', 'nfsv4' or 'cifs'. + :param file_operations: name of file operations to be monitored. Values + should be provided as list of strings. + """ + api_args = { + 'event-name': event_name, + 'protocol': protocol, + 'file-operations': [], + } + for file_op in file_operations: + api_args['file-operations'].append({'fpolicy-operation': file_op}) + + self.send_request('fpolicy-policy-event-create', api_args) + + def delete_fpolicy_event(self, event_name): + """Deletes a fpolicy policy event. + + :param event_name: name of the event to be deleted + """ + try: + self.send_request('fpolicy-policy-event-delete', + {'event-name': event_name}) + except netapp_api.NaApiError as e: + if e.code in [netapp_api.EEVENTNOTFOUND, + netapp_api.EOBJECTNOTFOUND]: + msg = _("FPolicy event %s not found.") + LOG.debug(msg, event_name) + else: + raise exception.NetAppException(message=e.message) + + def get_fpolicy_events(self, event_name=None, protocol=None, + file_operations=None): + """Retrives a list of fpolicy events. + + :param event_name: name of the fpolicy event + :param protocol: name of protocol. Possible values are: 'nfsv3', + 'nfsv4' or 'cifs'. + :param file_operations: name of file operations to be monitored. Values + should be provided as list of strings. + :returns List of policy events or empty list + """ + event_options_config = {} + if event_name: + event_options_config['event-name'] = event_name + if protocol: + event_options_config['protocol'] = protocol + if file_operations: + event_options_config['file-operations'] = [] + for file_op in file_operations: + event_options_config['file-operations'].append( + {'fpolicy-operation': file_op}) + + api_args = { + 'query': { + 'fpolicy-event-options-config': event_options_config, + }, + } + result = self.send_iter_request('fpolicy-policy-event-get-iter', + api_args) + + fpolicy_events = [] + if self._has_records(result): + try: + fpolicy_events = [] + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + for event_info in attributes_list.get_children(): + name = event_info.get_child_content('event-name') + proto = event_info.get_child_content('protocol') + file_operations_child = event_info.get_child_by_name( + 'file-operations') or netapp_api.NaElement('none') + operations = [operation.get_content() + for operation in + file_operations_child.get_children()] + + fpolicy_events.append({ + 'event-name': name, + 'protocol': proto, + 'file-operations': operations + }) + except AttributeError: + msg = _('Could not retrieve fpolicy policy event information.') + raise exception.NetAppException(msg) + + return fpolicy_events + + def create_fpolicy_policy(self, fpolicy_name, events, engine='native'): + """Creates a fpolicy policy resource. + + :param fpolicy_name: name of the fpolicy policy to be created. + :param events: list of event names for file access monitoring. + :param engine: name of the engine to be used. + """ + api_args = { + 'policy-name': fpolicy_name, + 'events': [], + 'engine-name': engine + } + for event in events: + api_args['events'].append({'event-name': event}) + + self.send_request('fpolicy-policy-create', api_args) + + def delete_fpolicy_policy(self, policy_name): + """Deletes a fpolicy policy event. + + :param policy_name: name of the policy to be deleted. + """ + try: + self.send_request('fpolicy-policy-delete', + {'policy-name': policy_name}) + except netapp_api.NaApiError as e: + if e.code in [netapp_api.EPOLICYNOTFOUND, + netapp_api.EOBJECTNOTFOUND]: + msg = _("FPolicy policy %s not found.") + LOG.debug(msg, policy_name) + else: + raise exception.NetAppException(message=e.message) + + def get_fpolicy_policies(self, policy_name=None, engine_name='native', + event_names=[]): + """Retrieve one or more fpolicy policies. + + :param policy_name: name of the policy to be retrieved + :param engine_name: name of the engine + :param event_names: list of event names that must be associated to the + fpolicy policy + :return: list of fpolicy policies or empty list + """ + policy_info = {} + if policy_name: + policy_info['policy-name'] = policy_name + if engine_name: + policy_info['engine-name'] = engine_name + if event_names: + policy_info['events'] = [] + for event_name in event_names: + policy_info['events'].append({'event-name': event_name}) + + api_args = { + 'query': { + 'fpolicy-policy-info': policy_info, + }, + } + result = self.send_iter_request('fpolicy-policy-get-iter', api_args) + + fpolicy_policies = [] + if self._has_records(result): + try: + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + for policy_info in attributes_list.get_children(): + name = policy_info.get_child_content('policy-name') + engine = policy_info.get_child_content('engine-name') + events_child = policy_info.get_child_by_name( + 'events') or netapp_api.NaElement('none') + events = [event.get_content() + for event in events_child.get_children()] + + fpolicy_policies.append({ + 'policy-name': name, + 'engine-name': engine, + 'events': events + }) + except AttributeError: + msg = _('Could not retrieve fpolicy policy information.') + raise exception.NetAppException(message=msg) + + return fpolicy_policies + + def create_fpolicy_scope(self, policy_name, share_name, + extensions_to_include=None, + extensions_to_exclude=None): + """Assings a file scope to an existing fpolicy policy. + + :param policy_name: name of the policy to associate with the new scope. + :param share_name: name of the share to be associated with the new + scope. + :param extensions_to_include: file extensions included for screening. + Values should be provided as comma separated list + :param extensions_to_exclude: file extensions excluded for screening. + Values should be provided as comma separated list + """ + api_args = { + 'policy-name': policy_name, + 'shares-to-include': { + 'string': share_name, + }, + 'file-extensions-to-include': [], + 'file-extensions-to-exclude': [], + } + if extensions_to_include: + for file_ext in extensions_to_include.split(','): + api_args['file-extensions-to-include'].append( + {'string': file_ext.strip()}) + + if extensions_to_exclude: + for file_ext in extensions_to_exclude.split(','): + api_args['file-extensions-to-exclude'].append( + {'string': file_ext.strip()}) + + self.send_request('fpolicy-policy-scope-create', api_args) + + def modify_fpolicy_scope(self, policy_name, shares_to_include=[], + extensions_to_include=None, + extensions_to_exclude=None): + """Modify an existing fpolicy scope. + + :param policy_name: name of the policy associated to the scope. + :param shares_to_include: list of shares to include for file access + monitoring. + :param extensions_to_include: file extensions included for screening. + Values should be provided as comma separated list + :param extensions_to_exclude: file extensions excluded for screening. + Values should be provided as comma separated list + """ + api_args = { + 'policy-name': policy_name, + } + if extensions_to_include: + api_args['file-extensions-to-include'] = [] + for file_ext in extensions_to_include.split(','): + api_args['file-extensions-to-include'].append( + {'string': file_ext.strip()}) + + if extensions_to_exclude: + api_args['file-extensions-to-exclude'] = [] + for file_ext in extensions_to_exclude.split(','): + api_args['file-extensions-to-exclude'].append( + {'string': file_ext.strip()}) + + if shares_to_include: + api_args['shares-to-include'] = [ + {'string': share} for share in shares_to_include + ] + + self.send_request('fpolicy-policy-scope-modify', api_args) + + def delete_fpolicy_scope(self, policy_name): + """Deletes a fpolicy policy scope. + + :param policy_name: name of the policy associated to the scope to be + deleted. + """ + try: + self.send_request('fpolicy-policy-scope-delete', + {'policy-name': policy_name}) + except netapp_api.NaApiError as e: + if e.code in [netapp_api.ESCOPENOTFOUND, + netapp_api.EOBJECTNOTFOUND]: + msg = _("FPolicy scope %s not found.") + LOG.debug(msg, policy_name) + else: + raise exception.NetAppException(message=e.message) + + def get_fpolicy_scopes(self, policy_name=None, extensions_to_include=None, + extensions_to_exclude=None, shares_to_include=None): + """Retrieve fpolicy scopes. + + :param policy_name: name of the policy associated with a scope. + :param extensions_to_include: file extensions included for screening. + Values should be provided as comma separated list + :param extensions_to_exclude: file extensions excluded for screening. + Values should be provided as comma separated list + :param shares_to_include: list of shares to include for file access + monitoring. + :return: list of fpolicy scopes or empty list + """ + policy_scope_info = {} + if policy_name: + policy_scope_info['policy-name'] = policy_name + + if shares_to_include: + policy_scope_info['shares-to-include'] = [ + {'string': share} for share in shares_to_include + ] + if extensions_to_include: + policy_scope_info['file-extensions-to-include'] = [] + for file_op in extensions_to_include.split(','): + policy_scope_info['file-extensions-to-include'].append( + {'string': file_op.strip()}) + if extensions_to_exclude: + policy_scope_info['file-extensions-to-exclude'] = [] + for file_op in extensions_to_exclude.split(','): + policy_scope_info['file-extensions-to-exclude'].append( + {'string': file_op.strip()}) + + api_args = { + 'query': { + 'fpolicy-scope-config': policy_scope_info, + }, + } + result = self.send_iter_request('fpolicy-policy-scope-get-iter', + api_args) + + fpolicy_scopes = [] + if self._has_records(result): + try: + fpolicy_scopes = [] + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + for policy_scope in attributes_list.get_children(): + name = policy_scope.get_child_content('policy-name') + ext_include_child = policy_scope.get_child_by_name( + 'file-extensions-to-include') or netapp_api.NaElement( + 'none') + ext_include = [ext.get_content() + for ext in ext_include_child.get_children()] + ext_exclude_child = policy_scope.get_child_by_name( + 'file-extensions-to-exclude') or netapp_api.NaElement( + 'none') + ext_exclude = [ext.get_content() + for ext in ext_exclude_child.get_children()] + shares_child = policy_scope.get_child_by_name( + 'shares-to-include') or netapp_api.NaElement('none') + shares_include = [ext.get_content() + for ext in shares_child.get_children()] + fpolicy_scopes.append({ + 'policy-name': name, + 'file-extensions-to-include': ext_include, + 'file-extensions-to-exclude': ext_exclude, + 'shares-to-include': shares_include, + }) + except AttributeError: + msg = _('Could not retrieve fpolicy policy information.') + raise exception.NetAppException(msg) + + return fpolicy_scopes + + def enable_fpolicy_policy(self, policy_name, sequence_number): + """Enables a specific named policy. + + :param policy_name: name of the policy to be enabled + :param sequence_number: policy sequence number + """ + api_args = { + 'policy-name': policy_name, + 'sequence-number': sequence_number, + } + + self.send_request('fpolicy-enable-policy', api_args) + + def disable_fpolicy_policy(self, policy_name): + """Disables a specific policy. + + :param policy_name: name of the policy to be disabled + """ + try: + self.send_request('fpolicy-disable-policy', + {'policy-name': policy_name}) + except netapp_api.NaApiError as e: + disabled = "policy is already disabled" + if (e.code in [netapp_api.EPOLICYNOTFOUND, + netapp_api.EOBJECTNOTFOUND] or + (e.code == netapp_api.EINVALIDINPUTERROR and + disabled in e.message)): + msg = _("FPolicy policy %s not found or already disabled.") + LOG.debug(msg, policy_name) + else: + raise exception.NetAppException(message=e.message) + + def get_fpolicy_policies_status(self, policy_name=None, status='true'): + policy_status_info = {} + if policy_name: + policy_status_info['policy-name'] = policy_name + policy_status_info['status'] = status + api_args = { + 'query': { + 'fpolicy-policy-status-info': policy_status_info, + }, + } + result = self.send_iter_request('fpolicy-policy-status-get-iter', + api_args) + + fpolicy_status = [] + if self._has_records(result): + try: + fpolicy_status = [] + attributes_list = result.get_child_by_name( + 'attributes-list') or netapp_api.NaElement('none') + for policy_status in attributes_list.get_children(): + name = policy_status.get_child_content('policy-name') + status = policy_status.get_child_content('status') + seq = policy_status.get_child_content('sequence-number') + fpolicy_status.append({ + 'policy-name': name, + 'status': strutils.bool_from_string(status), + 'sequence-number': seq + }) + except AttributeError: + msg = _('Could not retrieve fpolicy status information.') + raise exception.NetAppException(msg) + + return fpolicy_status diff --git a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py index 9e89851e52..3e674ddee1 100644 --- a/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py +++ b/manila/share/drivers/netapp/dataontap/cluster_mode/lib_base.py @@ -34,6 +34,7 @@ from oslo_utils import uuidutils import six from manila.common import constants +from manila import coordination from manila import exception from manila.i18n import _ from manila.share.drivers.netapp.dataontap.client import api as netapp_api @@ -68,6 +69,9 @@ class NetAppCmodeFileStorageLibrary(object): STATE_MOVING_VOLUME = 'moving_volume' STATE_SNAPMIRROR_DATA_COPYING = 'snapmirror_data_copying' + # Maximum number of FPolicis per vServer + FPOLICY_MAX_VSERVER_POLICIES = 10 + # Maps NetApp qualified extra specs keys to corresponding backend API # client library argument keywords. When we expose more backend # capabilities here, we will add them to this map. @@ -80,11 +84,15 @@ class NetAppCmodeFileStorageLibrary(object): } STRING_QUALIFIED_EXTRA_SPECS_MAP = { - 'netapp:snapshot_policy': 'snapshot_policy', 'netapp:language': 'language', 'netapp:max_files': 'max_files', 'netapp:adaptive_qos_policy_group': 'adaptive_qos_policy_group', + 'netapp:fpolicy_extensions_to_include': + 'fpolicy_extensions_to_include', + 'netapp:fpolicy_extensions_to_exclude': + 'fpolicy_extensions_to_exclude', + 'netapp:fpolicy_file_operations': 'fpolicy_file_operations', } # Maps standard extra spec keys to legacy NetApp keys @@ -111,11 +119,15 @@ class NetAppCmodeFileStorageLibrary(object): # Maps the NFS config used by share-servers NFS_CONFIG_EXTRA_SPECS_MAP = { - 'netapp:tcp_max_xfer_size': 'tcp-max-xfer-size', 'netapp:udp_max_xfer_size': 'udp-max-xfer-size', } + FPOLICY_FILE_OPERATIONS_LIST = [ + 'close', 'create', 'create_dir', 'delete', 'delete_dir', 'getattr', + 'link', 'lookup', 'open', 'read', 'write', 'rename', 'rename_dir', + 'setattr', 'symlink'] + def __init__(self, driver_name, **kwargs): na_utils.validate_driver_instantiation(**kwargs) @@ -279,6 +291,17 @@ class NetAppCmodeFileStorageLibrary(object): return (self.configuration.netapp_snapmirror_policy_name_svm_template % {'share_server_id': share_server_id.replace('-', '_')}) + def _get_backend_fpolicy_policy_name(self, share_id): + """Get FPolicy policy name according with the configured template.""" + return (self.configuration.netapp_fpolicy_policy_name_template + % {'share_id': share_id.replace('-', '_')}) + + def _get_backend_fpolicy_event_name(self, share_id, protocol): + """Get FPolicy event name according with the configured template.""" + return (self.configuration.netapp_fpolicy_event_name_template + % {'protocol': protocol.lower(), + 'share_id': share_id.replace('-', '_')}) + @na_utils.trace def _get_aggregate_space(self): aggregates = self._find_matching_aggregates() @@ -588,10 +611,11 @@ class NetAppCmodeFileStorageLibrary(object): if (src_cluster_name != dest_cluster_name or not self._have_cluster_creds): # 1. Create a clone on source. We don't need to split from - # clone in order to replicate data + # clone in order to replicate data. We don't need to create + # fpolicies since this copy will be deleted. self._allocate_container_from_snapshot( dest_share, snapshot, src_vserver, src_vserver_client, - split=False) + split=False, create_fpolicy=False) # 2. Create a replica in destination host self._allocate_container( dest_share, dest_vserver, dest_vserver_client, @@ -617,8 +641,8 @@ class NetAppCmodeFileStorageLibrary(object): # If the share exists on the source vserser, we need to # delete it since it's a temporary share, not managed by the system dm_session.delete_snapmirror(src_share_instance, dest_share) - self._delete_share(src_share_instance, src_vserver_client, - remove_export=False) + self._delete_share(src_share_instance, src_vserver, + src_vserver_client, remove_export=False) msg = _('Could not create share %(share_id)s from snapshot ' '%(snapshot_id)s in the destination host %(dest_host)s.') msg_args = {'share_id': dest_share['id'], @@ -665,7 +689,7 @@ class NetAppCmodeFileStorageLibrary(object): src_vserver_client = data_motion.get_client_for_backend( src_backend, vserver_name=src_vserver) - self._delete_share(source_share, src_vserver_client, + self._delete_share(source_share, src_vserver, src_vserver_client, remove_export=False) # Delete private storage info self.private_storage.delete(share['id']) @@ -766,7 +790,7 @@ class NetAppCmodeFileStorageLibrary(object): dm_session.break_snapmirror(src_share, share) dm_session.delete_snapmirror(src_share, share) # 3. Delete the source volume - self._delete_share(src_share, src_vserver_client, + self._delete_share(src_share, src_vserver, src_vserver_client, remove_export=False) share_name = self._get_backend_share_name(src_share['id']) # 4. Set File system size fixed to false @@ -817,7 +841,7 @@ class NetAppCmodeFileStorageLibrary(object): @na_utils.trace def _allocate_container(self, share, vserver, vserver_client, - replica=False): + replica=False, create_fpolicy=True): """Create new share on aggregate.""" share_name = self._get_backend_share_name(share['id']) @@ -850,6 +874,15 @@ class NetAppCmodeFileStorageLibrary(object): self._apply_snapdir_visibility( hide_snapdir, share_name, vserver_client) + if create_fpolicy: + fpolicy_ext_to_include = provisioning_options.get( + 'fpolicy_extensions_to_include') + fpolicy_ext_to_exclude = provisioning_options.get( + 'fpolicy_extensions_to_exclude') + if fpolicy_ext_to_include or fpolicy_ext_to_exclude: + self._create_fpolicy_for_share(share, vserver, vserver_client, + **provisioning_options) + def _apply_snapdir_visibility( self, hide_snapdir, share_name, vserver_client): @@ -884,6 +917,9 @@ class NetAppCmodeFileStorageLibrary(object): if 'netapp:max_files' in extra_specs: self._check_if_max_files_is_valid(share, extra_specs['netapp:max_files']) + if 'netapp:fpolicy_file_operations' in extra_specs: + self._check_fpolicy_file_operations( + share, extra_specs['netapp:fpolicy_file_operations']) @na_utils.trace def _check_if_max_files_is_valid(self, share, value): @@ -895,6 +931,20 @@ class NetAppCmodeFileStorageLibrary(object): 'in share_type %(type_id)s for share %(share_id)s.') raise exception.NetAppException(msg % args) + @na_utils.trace + def _check_fpolicy_file_operations(self, share, value): + """Check if the provided fpolicy file operations are valid.""" + for file_op in value.split(','): + if file_op.strip() not in self.FPOLICY_FILE_OPERATIONS_LIST: + args = {'file_op': file_op, + 'extra_spec': 'netapp:fpolicy_file_operations', + 'type_id': share['share_type_id'], + 'share_id': share['id']} + msg = _('Invalid value "%(file_op)s" for extra_spec ' + '"%(extra_spec)s" in share_type %(type_id)s for share ' + '%(share_id)s.') + raise exception.NetAppException(msg % args) + @na_utils.trace def _check_boolean_extra_specs_validity(self, share, specs, keys_of_interest): @@ -1100,6 +1150,25 @@ class NetAppCmodeFileStorageLibrary(object): 'cluster credentials.') raise exception.NetAppException(msg) + fpolicy_ext_to_include = provisioning_options.get( + 'fpolicy_extensions_to_include') + fpolicy_ext_to_exclude = provisioning_options.get( + 'fpolicy_extensions_to_exclude') + if provisioning_options.get('fpolicy_file_operations') and not ( + fpolicy_ext_to_include or fpolicy_ext_to_exclude): + msg = _('The extra spec "fpolicy_file_operations" can only ' + 'be configured together with ' + '"fpolicy_extensions_to_include" or ' + '"fpolicy_extensions_to_exclude".') + raise exception.NetAppException(msg) + + if replication_type and ( + fpolicy_ext_to_include or fpolicy_ext_to_exclude): + msg = _("The extra specs 'fpolicy_extensions_to_include' and " + "'fpolicy_extensions_to_exclude' are not " + "supported by share replication feature.") + raise exception.NetAppException(msg) + def _get_nve_option(self, specs): if 'netapp_flexvol_encryption' in specs: nve = specs['netapp_flexvol_encryption'].lower() == 'true' @@ -1128,7 +1197,8 @@ class NetAppCmodeFileStorageLibrary(object): @na_utils.trace def _allocate_container_from_snapshot( self, share, snapshot, vserver, vserver_client, - snapshot_name_func=_get_backend_snapshot_name, split=None): + snapshot_name_func=_get_backend_snapshot_name, split=None, + create_fpolicy=True): """Clones existing share.""" share_name = self._get_backend_share_name(share['id']) parent_share_name = self._get_backend_share_name(snapshot['share_id']) @@ -1156,13 +1226,26 @@ class NetAppCmodeFileStorageLibrary(object): self._apply_snapdir_visibility( hide_snapdir, share_name, vserver_client) + if create_fpolicy: + fpolicy_ext_to_include = provisioning_options.get( + 'fpolicy_extensions_to_include') + fpolicy_ext_to_exclude = provisioning_options.get( + 'fpolicy_extensions_to_exclude') + if fpolicy_ext_to_include or fpolicy_ext_to_exclude: + self._create_fpolicy_for_share(share, vserver, vserver_client, + **provisioning_options) + @na_utils.trace def _share_exists(self, share_name, vserver_client): return vserver_client.volume_exists(share_name) @na_utils.trace - def _delete_share(self, share, vserver_client, remove_export=True): + def _delete_share(self, share, vserver, vserver_client, + remove_export=True): share_name = self._get_backend_share_name(share['id']) + # Share doesn't need to exist to be assigned to a fpolicy scope + self._delete_fpolicy_for_share(share, vserver, vserver_client) + if self._share_exists(share_name, vserver_client): if remove_export: self._remove_export(share, vserver_client) @@ -1188,7 +1271,7 @@ class NetAppCmodeFileStorageLibrary(object): "will proceed anyway. Error: %(error)s", {'share': share['id'], 'error': error}) return - self._delete_share(share, vserver_client) + self._delete_share(share, vserver, vserver_client) @na_utils.trace def _deallocate_container(self, share_name, vserver_client): @@ -1436,6 +1519,29 @@ class NetAppCmodeFileStorageLibrary(object): self.validate_provisioning_options_for_share(provisioning_options, extra_specs=extra_specs, qos_specs=qos_specs) + # Check fpolicy extra-specs + fpolicy_ext_include = provisioning_options.get( + 'fpolicy_extensions_to_include') + fpolicy_ext_exclude = provisioning_options.get( + 'fpolicy_extensions_to_exclude') + fpolicy_file_operations = provisioning_options.get( + 'fpolicy_file_operations') + + fpolicy_scope = None + if fpolicy_ext_include or fpolicy_ext_include: + fpolicy_scope = self._find_reusable_fpolicy_scope( + share, vserver_client, + fpolicy_extensions_to_include=fpolicy_ext_include, + fpolicy_extensions_to_exclude=fpolicy_ext_exclude, + fpolicy_file_operations=fpolicy_file_operations, + shares_to_include=[volume_name] + ) + if fpolicy_scope is None: + msg = _('Volume %(volume)s does not contains the expected ' + 'fpolicy configuration.') + msg_args = {'volume': volume_name} + raise exception.ManageExistingShareTypeMismatch( + reason=msg % msg_args) debug_args = { 'share': share_name, @@ -1459,6 +1565,17 @@ class NetAppCmodeFileStorageLibrary(object): vserver_client.modify_volume(aggregate_name, share_name, **provisioning_options) + # Update fpolicy to include the new share name and remove the old one + if fpolicy_scope is not None: + shares_to_include = copy.deepcopy( + fpolicy_scope.get('shares-to-include', [])) + shares_to_include.remove(volume_name) + shares_to_include.append(share_name) + policy_name = fpolicy_scope.get('policy-name') + # Update + vserver_client.modify_fpolicy_scope( + policy_name, shares_to_include=shares_to_include) + # Save original volume info to private storage original_data = { 'original_name': volume['name'], @@ -1883,7 +2000,7 @@ class NetAppCmodeFileStorageLibrary(object): dest_backend, vserver_name=vserver) self._allocate_container(new_replica, vserver, vserver_client, - replica=True) + replica=True, create_fpolicy=False) # 2. Setup SnapMirror dm_session.create_snapmirror(active_replica, new_replica) @@ -2431,7 +2548,30 @@ class NetAppCmodeFileStorageLibrary(object): self.validate_provisioning_options_for_share( provisioning_options, extra_specs=extra_specs, qos_specs=qos_specs) - + # Validate destination against fpolicy extra specs + fpolicy_ext_include = provisioning_options.get( + 'fpolicy_extensions_to_include') + fpolicy_ext_exclude = provisioning_options.get( + 'fpolicy_extensions_to_exclude') + fpolicy_file_operations = provisioning_options.get( + 'fpolicy_file_operations') + if fpolicy_ext_include or fpolicy_ext_include: + __, dest_client = self._get_vserver( + share_server=destination_share_server) + fpolicies = dest_client.get_fpolicy_policies_status() + if len(fpolicies) >= self.FPOLICY_MAX_VSERVER_POLICIES: + # If we can't create a new policy for the new share, + # we need to reuse an existing one. + reusable_scopes = self._find_reusable_fpolicy_scope( + destination_share, dest_client, + fpolicy_extensions_to_include=fpolicy_ext_include, + fpolicy_extensions_to_exclude=fpolicy_ext_exclude, + fpolicy_file_operations=fpolicy_file_operations) + if not reusable_scopes: + msg = _( + "Cannot migrate share because the destination " + "reached its maximum number of policies.") + raise exception.NetAppException(msg) # NOTE (felipe_rodrigues): NetApp only can migrate within the # same server, so it does not need to check that the # destination share has the same NFS config as the destination @@ -2448,7 +2588,6 @@ class NetAppCmodeFileStorageLibrary(object): share_server=share_server) share_volume = self._get_backend_share_name( source_share['id']) - # NOTE(dviroel): If source and destination vservers are # compatible for volume move, the provisioning option # 'adaptive_qos_policy_group' will also be supported since the @@ -2758,6 +2897,18 @@ class NetAppCmodeFileStorageLibrary(object): new_share_volume_name, **provisioning_options) + # Create or reuse fpolicy + fpolicy_ext_to_include = provisioning_options.get( + 'fpolicy_extensions_to_include') + fpolicy_ext_to_exclude = provisioning_options.get( + 'fpolicy_extensions_to_exclude') + if fpolicy_ext_to_include or fpolicy_ext_to_exclude: + self._create_fpolicy_for_share(destination_share, vserver, + vserver_client, + **provisioning_options) + # Delete old fpolicies if needed + self._delete_fpolicy_for_share(source_share, vserver, vserver_client) + msg = ("Volume move operation for share %(shr)s has completed " "successfully. Share has been moved from %(src)s to " "%(dest)s.") @@ -2956,3 +3107,233 @@ class NetAppCmodeFileStorageLibrary(object): backend_free_capacity += total_pool_free return size <= backend_free_capacity + + def _find_reusable_fpolicy_scope( + self, share, vserver_client, fpolicy_extensions_to_include=None, + fpolicy_extensions_to_exclude=None, fpolicy_file_operations=None, + shares_to_include=None): + """Searches a fpolicy scope that can be reused for a share.""" + protocols = ( + ['nfsv3', 'nfsv4'] if share['share_proto'].lower() == 'nfs' + else ['cifs']) + protocols.sort() + + requested_ext_to_include = [] + if fpolicy_extensions_to_include: + requested_ext_to_include = na_utils.convert_string_to_list( + fpolicy_extensions_to_include) + requested_ext_to_include.sort() + + requested_ext_to_exclude = [] + if fpolicy_extensions_to_exclude: + requested_ext_to_exclude = na_utils.convert_string_to_list( + fpolicy_extensions_to_exclude) + requested_ext_to_exclude.sort() + + if fpolicy_file_operations: + requested_file_operations = na_utils.convert_string_to_list( + fpolicy_file_operations) + else: + requested_file_operations = ( + self.configuration.netapp_fpolicy_default_file_operations) + requested_file_operations.sort() + + reusable_scopes = vserver_client.get_fpolicy_scopes( + extensions_to_exclude=fpolicy_extensions_to_exclude, + extensions_to_include=fpolicy_extensions_to_include, + shares_to_include=shares_to_include) + # NOTE(dviroel): get_fpolicy_scopes can return scopes that don't match + # the exact requirements. + for scope in reusable_scopes[:]: + scope_ext_include = copy.deepcopy( + scope.get('file-extensions-to-include', [])) + scope_ext_include.sort() + scope_ext_exclude = copy.deepcopy( + scope.get('file-extensions-to-exclude', [])) + scope_ext_exclude.sort() + + if scope_ext_include != requested_ext_to_include: + LOG.debug( + "Excluding scope for policy %(policy_name)s because " + "it doesn't match 'file-extensions-to-include' " + "configuration.", {'policy_name': scope['policy-name']}) + reusable_scopes.remove(scope) + elif scope_ext_exclude != requested_ext_to_exclude: + LOG.debug( + "Excluding scope for policy %(policy_name)s because " + "it doesn't match 'file-extensions-to-exclude' " + "configuration.", {'policy_name': scope['policy-name']}) + reusable_scopes.remove(scope) + + for scope in reusable_scopes[:]: + fpolicy_policy = vserver_client.get_fpolicy_policies( + policy_name=scope['policy-name']) + for policy in fpolicy_policy: + event_names = copy.deepcopy(policy.get('events', [])) + match_event_protocols = [] + for event_name in event_names: + events = vserver_client.get_fpolicy_events( + event_name=event_name) + for event in events: + event_file_ops = copy.deepcopy( + event.get('file-operations', [])) + event_file_ops.sort() + if event_file_ops == requested_file_operations: + # Event has same file operations + match_event_protocols.append(event.get('protocol')) + match_event_protocols.sort() + + if match_event_protocols != protocols: + LOG.debug( + "Excluding scope for policy %(policy_name)s because " + "it doesn't match 'events' configuration of file " + "operations per protocol.", + {'policy_name': scope['policy-name']}) + reusable_scopes.remove(scope) + + return reusable_scopes[0] if reusable_scopes else None + + def _create_fpolicy_for_share( + self, share, vserver, vserver_client, + fpolicy_extensions_to_include=None, + fpolicy_extensions_to_exclude=None, fpolicy_file_operations=None, + **options): + """Creates or reuses a fpolicy for a new share.""" + share_name = self._get_backend_share_name(share['id']) + + @manila_utils.synchronized('netapp-fpolicy-%s' % vserver, + external=True) + def _create_fpolicy_with_lock(): + + # 1. Try to reuse an existing FPolicy if matches the same + # requirements + reusable_scope = self._find_reusable_fpolicy_scope( + share, vserver_client, + fpolicy_extensions_to_include=fpolicy_extensions_to_include, + fpolicy_extensions_to_exclude=fpolicy_extensions_to_exclude, + fpolicy_file_operations=fpolicy_file_operations) + + if reusable_scope: + shares_to_include = copy.deepcopy( + reusable_scope.get('shares-to-include')) + shares_to_include.append(share_name) + # Add the new share to the existing policy scope + vserver_client.modify_fpolicy_scope( + reusable_scope.get('policy-name'), + shares_to_include=shares_to_include) + + LOG.debug("Share %(share_id)s was added to an existing " + "fpolicy scope.", {'share_id': share['id']}) + return + + # 2. Since we can't reuse any scope, start creating a new fpolicy + protocols = ( + ['nfsv3', 'nfsv4'] if share['share_proto'].lower() == 'nfs' + else ['cifs']) + + if fpolicy_file_operations: + file_operations = na_utils.convert_string_to_list( + fpolicy_file_operations) + else: + file_operations = ( + self.configuration.netapp_fpolicy_default_file_operations) + + # NOTE(dviroel): ONTAP limit of fpolicies for a vserser is 10. + # DHSS==True backends can create new share servers or fail earlier + # in choose_share_server_for_share. + vserver_policies = vserver_client.get_fpolicy_policies_status() + if len(vserver_policies) >= self.FPOLICY_MAX_VSERVER_POLICIES: + msg_args = {'share_id': share['id']} + msg = _("Cannot configure a new FPolicy for share " + "%(share_id)s. The maximum number of fpolicies was " + "already reached.") % msg_args + LOG.exception(msg) + raise exception.NetAppException(message=msg) + + seq_number_list = [int(policy['sequence-number']) + for policy in vserver_policies] + available_seq_number = None + for number in range(1, self.FPOLICY_MAX_VSERVER_POLICIES + 1): + if number not in seq_number_list: + available_seq_number = number + break + + events = [] + policy_name = self._get_backend_fpolicy_policy_name(share['id']) + try: + for protocol in protocols: + event_name = self._get_backend_fpolicy_event_name( + share['id'], protocol) + vserver_client.create_fpolicy_event(event_name, + protocol, + file_operations) + events.append(event_name) + + # 2. Create a fpolicy policy + vserver_client.create_fpolicy_policy(policy_name, events) + + # 3. Assign a scope to the fpolicy policy + vserver_client.create_fpolicy_scope( + policy_name, share_name, + extensions_to_include=fpolicy_extensions_to_include, + extensions_to_exclude=fpolicy_extensions_to_exclude) + except Exception: + # NOTE(dviroel): Rollback fpolicy policy and events creation + # since they won't be linked to the share, which is made by + # the scope creation. + + # Delete fpolicy policy + vserver_client.delete_fpolicy_policy(policy_name) + # Delete fpolicy events + for event in events: + vserver_client.delete_fpolicy_event(event) + + msg = _("Failed to configure a FPolicy resources for share " + "%(share_id)s. ") % {'share_id': share['id']} + LOG.exception(msg) + raise exception.NetAppException(message=msg) + + # 4. Enable fpolicy policy + vserver_client.enable_fpolicy_policy(policy_name, + available_seq_number) + + _create_fpolicy_with_lock() + LOG.debug('A new fpolicy was successfully created and associated to ' + 'share %(share_id)s', {'share_id': share['id']}) + + def _delete_fpolicy_for_share(self, share, vserver, vserver_client): + """Delete all associated fpolicy resources from a share.""" + share_name = self._get_backend_share_name(share['id']) + + @coordination.synchronized('netapp-fpolicy-%s' % vserver) + def _delete_fpolicy_with_lock(): + fpolicy_scopes = vserver_client.get_fpolicy_scopes( + shares_to_include=[share_name]) + + if fpolicy_scopes: + shares_to_include = copy.copy( + fpolicy_scopes[0].get('shares-to-include')) + shares_to_include.remove(share_name) + + policy_name = fpolicy_scopes[0].get('policy-name') + if shares_to_include: + vserver_client.modify_fpolicy_scope( + policy_name, shares_to_include=shares_to_include) + else: + # Delete an empty fpolicy + # 1. Disable fpolicy policy + vserver_client.disable_fpolicy_policy(policy_name) + # 2. Retrieve fpoliocy info + fpolicy_policies = vserver_client.get_fpolicy_policies( + policy_name=policy_name) + # 3. Delete fpolicy scope + vserver_client.delete_fpolicy_scope(policy_name) + # 4. Delete fpolicy policy + vserver_client.delete_fpolicy_policy(policy_name) + # 5. Delete fpolicy events + for policy in fpolicy_policies: + events = policy.get('events', []) + for event in events: + vserver_client.delete_fpolicy_event(event) + + _delete_fpolicy_with_lock() 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 4f241a4718..b18547243a 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 @@ -758,22 +758,37 @@ class NetAppCmodeMultiSVMFileStorageLibrary( return None nfs_config = None + extra_specs = share_types.get_extra_specs_from_share(share) if self.is_nfs_config_supported: - extra_specs = share_types.get_extra_specs_from_share(share) nfs_config = self._get_nfs_config_provisioning_options(extra_specs) + provisioning_options = self._get_provisioning_options(extra_specs) + # Get FPolicy extra specs to avoid incompatible share servers + fpolicy_ext_to_include = provisioning_options.get( + 'fpolicy_extensions_to_include') + fpolicy_ext_to_exclude = provisioning_options.get( + 'fpolicy_extensions_to_exclude') + fpolicy_file_operations = provisioning_options.get( + 'fpolicy_file_operations') + # Avoid the reuse of 'dp_protection' vservers: for share_server in share_servers: - if self._check_reuse_share_server(share_server, nfs_config, - share_group=share_group): + if self._check_reuse_share_server( + share_server, nfs_config, share=share, + share_group=share_group, + fpolicy_ext_include=fpolicy_ext_to_include, + fpolicy_ext_exclude=fpolicy_ext_to_exclude, + fpolicy_file_operations=fpolicy_file_operations): return share_server # There is no compatible share server to be reused return None @na_utils.trace - def _check_reuse_share_server(self, share_server, nfs_config, - share_group=None): + def _check_reuse_share_server(self, share_server, nfs_config, share=None, + share_group=None, fpolicy_ext_include=None, + fpolicy_ext_exclude=None, + fpolicy_file_operations=None): """Check whether the share_server can be reused or not.""" if (share_group and share_group.get('share_server_id') != share_server['id']): @@ -795,6 +810,20 @@ class NetAppCmodeMultiSVMFileStorageLibrary( # that the share type is an element of the group types. return self._is_share_server_compatible(share_server, nfs_config) + if fpolicy_ext_include or fpolicy_ext_exclude: + fpolicies = client.get_fpolicy_policies_status() + if len(fpolicies) >= self.FPOLICY_MAX_VSERVER_POLICIES: + # This share server already reached it maximum number of + # policies, we need to check if we can reuse one, otherwise, + # it is not suitable for this share. + reusable_scope = self._find_reusable_fpolicy_scope( + share, client, + fpolicy_extensions_to_include=fpolicy_ext_include, + fpolicy_extensions_to_exclude=fpolicy_ext_exclude, + fpolicy_file_operations=fpolicy_file_operations) + if not reusable_scope: + return False + return True @na_utils.trace @@ -814,6 +843,10 @@ class NetAppCmodeMultiSVMFileStorageLibrary( if self.is_nfs_config_supported: nfs_config = self._get_nfs_config_share_group(share_group_ref) + # NOTE(dviroel): FPolicy extra-specs won't be conflicting, since + # multiple policies can be created. The maximum number of policies or + # the reusability of existing ones, can only be analyzed at share + # instance creation. for share_server in share_servers: if self._check_reuse_share_server(share_server, nfs_config): return share_server @@ -1188,7 +1221,8 @@ class NetAppCmodeMultiSVMFileStorageLibrary( dest_share_server) # Rollback resources transferred to the destination for instance in share_instances: - self._delete_share(instance, dest_client, remove_export=False) + self._delete_share(instance, dest_vserver, dest_client, + remove_export=False) msg_args = { 'src': source_share_server['id'], @@ -1243,7 +1277,8 @@ class NetAppCmodeMultiSVMFileStorageLibrary( # 8. Release source share resources for instance in share_instances: - self._delete_share(instance, src_client, remove_export=True) + self._delete_share(instance, src_vserver, src_client, + remove_export=True) # NOTE(dviroel): source share server deletion must be triggered by # the manager after finishing the migration @@ -1270,7 +1305,8 @@ class NetAppCmodeMultiSVMFileStorageLibrary( dest_share_server) # Do a simple volume cleanup in the destination vserver for instance in shares: - self._delete_share(instance, dest_client, remove_export=False) + self._delete_share(instance, dest_vserver, dest_client, + remove_export=False) except Exception: msg_args = { diff --git a/manila/share/drivers/netapp/options.py b/manila/share/drivers/netapp/options.py index faab89a21c..83e0e448cd 100644 --- a/manila/share/drivers/netapp/options.py +++ b/manila/share/drivers/netapp/options.py @@ -123,7 +123,18 @@ netapp_provisioning_opts = [ cfg.StrOpt('netapp_snapmirror_policy_name_svm_template', help='NetApp SnapMirror policy name template for Storage ' 'Virtual Machines (Vservers).', - default='snapmirror_policy_%(share_server_id)s'), ] + default='snapmirror_policy_%(share_server_id)s'), + cfg.ListOpt('netapp_fpolicy_default_file_operations', + help='NetApp FPolicy file operations to apply to a FPolicy ' + 'event, when not provided by the user using ' + '"netapp:fpolicy_file_operations" extra-spec.', + default=['create', 'write', 'rename']), + cfg.StrOpt('netapp_fpolicy_policy_name_template', + help='NetApp FPolicy policy name template.', + default='fpolicy_policy_%(share_id)s'), + cfg.StrOpt('netapp_fpolicy_event_name_template', + help='NetApp FPolicy policy name template.', + default='fpolicy_event_%(protocol)s_%(share_id)s'), ] netapp_cluster_opts = [ cfg.StrOpt('netapp_vserver', diff --git a/manila/share/drivers/netapp/utils.py b/manila/share/drivers/netapp/utils.py index 784363e4e9..d1a74fdf1a 100644 --- a/manila/share/drivers/netapp/utils.py +++ b/manila/share/drivers/netapp/utils.py @@ -112,6 +112,10 @@ def convert_to_list(value): return [value] +def convert_string_to_list(string, separator=','): + return [elem.strip() for elem in string.split(separator)] + + class OpenStackInfo(object): """OS/distribution, release, and version. diff --git a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py index 15f3da1728..13f6677a14 100644 --- a/manila/tests/share/drivers/netapp/dataontap/client/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/client/fakes.py @@ -108,6 +108,18 @@ SM_SOURCE_VOLUME = 'fake_source_volume' SM_DEST_VSERVER = 'fake_destination_vserver' SM_DEST_VOLUME = 'fake_destination_volume' +FPOLICY_POLICY_NAME = 'fake_fpolicy_name' +FPOLICY_EVENT_NAME = 'fake_fpolicy_event_name' +FPOLICY_PROTOCOL = 'cifs' +FPOLICY_FILE_OPERATIONS = 'create,write,rename' +FPOLICY_FILE_OPERATIONS_LIST = ['create', 'write', 'rename'] +FPOLICY_ENGINE = 'native' +FPOLICY_EXT_TO_INCLUDE = 'avi' +FPOLICY_EXT_TO_INCLUDE_LIST = ['avi'] +FPOLICY_EXT_TO_EXCLUDE = 'jpg,mp3' +FPOLICY_EXT_TO_EXCLUDE_LIST = ['jpg', 'mp3'] + + NETWORK_INTERFACES = [{ 'interface_name': 'fake_interface', 'address': IP_ADDRESS, @@ -2766,6 +2778,94 @@ DNS_CONFIG_GET_RESPONSE = etree.XML(""" 'vserver_name': VSERVER_NAME, }) +FPOLICY_EVENT_GET_ITER_RESPONSE = etree.XML(""" + + + + %(event_name)s + + create + write + rename + + %(protocol)s + false + %(vserver_name)s + + + 1 + """ % { + 'event_name': FPOLICY_EVENT_NAME, + 'protocol': FPOLICY_PROTOCOL, + 'vserver_name': VSERVER_NAME, +}) + +FPOLICY_POLICY_GET_ITER_RESPONSE = etree.XML(""" + + + + false + %(engine)s + + %(event_name)s + + true + false + %(policy_name)s + %(vserver_name)s + + + 1 + """ % { + 'engine': FPOLICY_ENGINE, + 'event_name': FPOLICY_EVENT_NAME, + 'policy_name': FPOLICY_POLICY_NAME, + 'vserver_name': VSERVER_NAME, +}) + +FPOLICY_SCOPE_GET_ITER_RESPONSE = etree.XML(""" + + + + true + + jpg + mp3 + + + avi + + false + %(policy_name)s + + %(share_name)s + + %(vserver_name)s + + + 1 + """ % { + 'policy_name': FPOLICY_POLICY_NAME, + 'share_name': SHARE_NAME, + 'vserver_name': VSERVER_NAME, +}) + +FPOLICY_POLICY_STATUS_GET_ITER_RESPONSE = etree.XML(""" + + + + %(policy_name)s + 1 + true + %(vserver_name)s + + + 1 + """ % { + 'policy_name': FPOLICY_POLICY_NAME, + 'vserver_name': VSERVER_NAME, +}) + FAKE_VOL_XML = """ open123 online 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 cb9cab3f41..e910361136 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 @@ -7621,3 +7621,361 @@ class NetAppClientCmodeTestCase(test.TestCase): self.assertEqual(expected_result, result) self.client.send_request.assert_called_once_with( 'volume-autosize-get', {'volume': fake.SHARE_NAME}) + + def test_create_fpolicy_event(self): + self.mock_object(self.client, 'send_request') + + self.client.create_fpolicy_event(fake.FPOLICY_EVENT_NAME, + fake.FPOLICY_PROTOCOL, + fake.FPOLICY_FILE_OPERATIONS_LIST) + + expected_args = { + 'event-name': fake.FPOLICY_EVENT_NAME, + 'protocol': fake.FPOLICY_PROTOCOL, + 'file-operations': [], + } + for file_op in fake.FPOLICY_FILE_OPERATIONS_LIST: + expected_args['file-operations'].append( + {'fpolicy-operation': file_op}) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-event-create', expected_args) + + @ddt.data(None, netapp_api.EEVENTNOTFOUND) + def test_delete_fpolicy_event(self, send_request_error): + if send_request_error: + send_request_mock = mock.Mock( + side_effect=self._mock_api_error(code=send_request_error)) + else: + send_request_mock = mock.Mock() + self.mock_object(self.client, 'send_request', send_request_mock) + + self.client.delete_fpolicy_event(fake.FPOLICY_EVENT_NAME) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-event-delete', + {'event-name': fake.FPOLICY_EVENT_NAME}) + + def test_delete_fpolicy_event_error(self): + eapi_error = self._mock_api_error(code=netapp_api.EAPIERROR) + self.mock_object( + self.client, 'send_request', mock.Mock(side_effect=eapi_error)) + + self.assertRaises(exception.NetAppException, + self.client.delete_fpolicy_event, + fake.FPOLICY_EVENT_NAME) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-event-delete', + {'event-name': fake.FPOLICY_EVENT_NAME}) + + def test_get_fpolicy_events(self): + api_response = netapp_api.NaElement( + fake.FPOLICY_EVENT_GET_ITER_RESPONSE) + self.mock_object(self.client, 'send_iter_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_fpolicy_events( + event_name=fake.FPOLICY_EVENT_NAME, + protocol=fake.FPOLICY_PROTOCOL, + file_operations=fake.FPOLICY_FILE_OPERATIONS_LIST) + + expected_options = { + 'event-name': fake.FPOLICY_EVENT_NAME, + 'protocol': fake.FPOLICY_PROTOCOL, + 'file-operations': [] + } + for file_op in fake.FPOLICY_FILE_OPERATIONS_LIST: + expected_options['file-operations'].append( + {'fpolicy-operation': file_op}) + + expected_args = { + 'query': { + 'fpolicy-event-options-config': expected_options, + }, + } + expected = [{ + 'event-name': fake.FPOLICY_EVENT_NAME, + 'protocol': fake.FPOLICY_PROTOCOL, + 'file-operations': fake.FPOLICY_FILE_OPERATIONS_LIST + }] + + self.assertEqual(expected, result) + self.client.send_iter_request.assert_called_once_with( + 'fpolicy-policy-event-get-iter', expected_args) + + def test_create_fpolicy_policy(self): + self.mock_object(self.client, 'send_request') + + self.client.create_fpolicy_policy(fake.FPOLICY_POLICY_NAME, + [fake.FPOLICY_EVENT_NAME], + engine=fake.FPOLICY_ENGINE) + + expected_args = { + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'events': [], + 'engine-name': fake.FPOLICY_ENGINE + } + for event in [fake.FPOLICY_EVENT_NAME]: + expected_args['events'].append( + {'event-name': event}) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-create', expected_args) + + @ddt.data(None, netapp_api.EPOLICYNOTFOUND) + def test_delete_fpolicy_policy(self, send_request_error): + if send_request_error: + send_request_mock = mock.Mock( + side_effect=self._mock_api_error(code=send_request_error)) + else: + send_request_mock = mock.Mock() + self.mock_object(self.client, 'send_request', send_request_mock) + + self.client.delete_fpolicy_policy(fake.FPOLICY_POLICY_NAME) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-delete', + {'policy-name': fake.FPOLICY_POLICY_NAME}) + + def test_delete_fpolicy_policy_error(self): + eapi_error = self._mock_api_error(code=netapp_api.EAPIERROR) + self.mock_object( + self.client, 'send_request', mock.Mock(side_effect=eapi_error)) + + self.assertRaises(exception.NetAppException, + self.client.delete_fpolicy_policy, + fake.FPOLICY_POLICY_NAME) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-delete', + {'policy-name': fake.FPOLICY_POLICY_NAME}) + + def test_get_fpolicy_policies(self): + api_response = netapp_api.NaElement( + fake.FPOLICY_POLICY_GET_ITER_RESPONSE) + self.mock_object(self.client, 'send_iter_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_fpolicy_policies( + policy_name=fake.FPOLICY_POLICY_NAME, + engine_name=fake.FPOLICY_ENGINE, + event_names=[fake.FPOLICY_EVENT_NAME]) + + expected_options = { + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'engine-name': fake.FPOLICY_ENGINE, + 'events': [] + } + for policy in [fake.FPOLICY_EVENT_NAME]: + expected_options['events'].append( + {'event-name': policy}) + + expected_args = { + 'query': { + 'fpolicy-policy-info': expected_options, + }, + } + expected = [{ + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'engine-name': fake.FPOLICY_ENGINE, + 'events': [fake.FPOLICY_EVENT_NAME] + }] + + self.assertEqual(expected, result) + self.client.send_iter_request.assert_called_once_with( + 'fpolicy-policy-get-iter', expected_args) + + def test_create_fpolicy_scope(self): + self.mock_object(self.client, 'send_request') + + self.client.create_fpolicy_scope( + fake.FPOLICY_POLICY_NAME, + fake.SHARE_NAME, + extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE) + + expected_args = { + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'shares-to-include': { + 'string': fake.SHARE_NAME, + }, + 'file-extensions-to-include': [], + 'file-extensions-to-exclude': [], + } + for file_ext in fake.FPOLICY_EXT_TO_INCLUDE_LIST: + expected_args['file-extensions-to-include'].append( + {'string': file_ext}) + for file_ext in fake.FPOLICY_EXT_TO_EXCLUDE_LIST: + expected_args['file-extensions-to-exclude'].append( + {'string': file_ext}) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-scope-create', expected_args) + + def test_modify_fpolicy_scope(self): + self.mock_object(self.client, 'send_request') + + self.client.modify_fpolicy_scope( + fake.FPOLICY_POLICY_NAME, + shares_to_include=[fake.SHARE_NAME], + extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE) + + expected_args = { + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'file-extensions-to-include': [], + 'file-extensions-to-exclude': [], + 'shares-to-include': [{ + 'string': fake.SHARE_NAME, + }], + } + for file_ext in fake.FPOLICY_EXT_TO_INCLUDE_LIST: + expected_args['file-extensions-to-include'].append( + {'string': file_ext}) + for file_ext in fake.FPOLICY_EXT_TO_EXCLUDE_LIST: + expected_args['file-extensions-to-exclude'].append( + {'string': file_ext}) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-scope-modify', expected_args) + + @ddt.data(None, netapp_api.ESCOPENOTFOUND) + def test_delete_fpolicy_scope(self, send_request_error): + if send_request_error: + send_request_mock = mock.Mock( + side_effect=self._mock_api_error(code=send_request_error)) + else: + send_request_mock = mock.Mock() + self.mock_object(self.client, 'send_request', send_request_mock) + + self.client.delete_fpolicy_scope(fake.FPOLICY_POLICY_NAME) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-scope-delete', + {'policy-name': fake.FPOLICY_POLICY_NAME}) + + def test_delete_fpolicy_scope_error(self): + eapi_error = self._mock_api_error(code=netapp_api.EAPIERROR) + self.mock_object( + self.client, 'send_request', mock.Mock(side_effect=eapi_error)) + + self.assertRaises(exception.NetAppException, + self.client.delete_fpolicy_scope, + fake.FPOLICY_POLICY_NAME) + + self.client.send_request.assert_called_once_with( + 'fpolicy-policy-scope-delete', + {'policy-name': fake.FPOLICY_POLICY_NAME}) + + def test_get_fpolicy_scopes(self): + api_response = netapp_api.NaElement( + fake.FPOLICY_SCOPE_GET_ITER_RESPONSE) + self.mock_object(self.client, 'send_iter_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_fpolicy_scopes( + policy_name=fake.FPOLICY_POLICY_NAME, + extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + shares_to_include=[fake.SHARE_NAME]) + + expected_options = { + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'shares-to-include': [{ + 'string': fake.SHARE_NAME, + }], + 'file-extensions-to-include': [], + 'file-extensions-to-exclude': [], + } + for file_ext in fake.FPOLICY_EXT_TO_INCLUDE_LIST: + expected_options['file-extensions-to-include'].append( + {'string': file_ext}) + for file_ext in fake.FPOLICY_EXT_TO_EXCLUDE_LIST: + expected_options['file-extensions-to-exclude'].append( + {'string': file_ext}) + + expected_args = { + 'query': { + 'fpolicy-scope-config': expected_options, + }, + } + expected = [{ + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'file-extensions-to-include': fake.FPOLICY_EXT_TO_INCLUDE_LIST, + 'file-extensions-to-exclude': fake.FPOLICY_EXT_TO_EXCLUDE_LIST, + 'shares-to-include': [fake.SHARE_NAME], + }] + + self.assertEqual(expected, result) + self.client.send_iter_request.assert_called_once_with( + 'fpolicy-policy-scope-get-iter', expected_args) + + def test_enable_fpolicy_policy(self): + self.mock_object(self.client, 'send_request') + + self.client.enable_fpolicy_policy(fake.FPOLICY_POLICY_NAME, 10) + + expected_args = { + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'sequence-number': 10, + } + self.client.send_request.assert_called_once_with( + 'fpolicy-enable-policy', expected_args) + + @ddt.data(None, netapp_api.EPOLICYNOTFOUND) + def test_disable_fpolicy_policy(self, send_request_error): + if send_request_error: + send_request_mock = mock.Mock( + side_effect=self._mock_api_error(code=send_request_error)) + else: + send_request_mock = mock.Mock() + self.mock_object(self.client, 'send_request', send_request_mock) + + self.client.disable_fpolicy_policy(fake.FPOLICY_POLICY_NAME) + + expected_args = { + 'policy-name': fake.FPOLICY_POLICY_NAME, + } + self.client.send_request.assert_called_once_with( + 'fpolicy-disable-policy', expected_args) + + def test_disable_fpolicy_policy_error(self): + eapi_error = self._mock_api_error(code=netapp_api.EAPIERROR) + self.mock_object( + self.client, 'send_request', mock.Mock(side_effect=eapi_error)) + + self.assertRaises(exception.NetAppException, + self.client.disable_fpolicy_policy, + fake.FPOLICY_POLICY_NAME) + + self.client.send_request.assert_called_once_with( + 'fpolicy-disable-policy', + {'policy-name': fake.FPOLICY_POLICY_NAME}) + + def test_get_fpolicy_status(self): + api_response = netapp_api.NaElement( + fake.FPOLICY_POLICY_STATUS_GET_ITER_RESPONSE) + self.mock_object(self.client, 'send_iter_request', + mock.Mock(return_value=api_response)) + + result = self.client.get_fpolicy_policies_status( + policy_name=fake.FPOLICY_POLICY_NAME) + + expected_args = { + 'query': { + 'fpolicy-policy-status-info': { + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'status': 'true' + }, + }, + } + expected = [{ + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'status': True, + 'sequence-number': '1' + }] + + self.assertEqual(expected, result) + self.client.send_iter_request.assert_called_once_with( + 'fpolicy-policy-status-get-iter', expected_args) diff --git a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py index 707eb06d32..89cd15d0fc 100644 --- a/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py +++ b/manila/tests/share/drivers/netapp/dataontap/cluster_mode/test_lib_base.py @@ -826,7 +826,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): if dest_cluster != fake.CLUSTER_NAME: self.mock_allocate_container_from_snapshot.assert_called_once_with( self.fake_share, fake.SNAPSHOT, fake.VSERVER1, - self.src_vserver_client, split=False) + self.src_vserver_client, split=False, create_fpolicy=False) self.mock_allocate_container.assert_called_once_with( self.fake_share, fake.VSERVER2, self.dest_vserver_client, replica=True) @@ -863,6 +863,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(return_value=True)) mock_deallocate_container = self.mock_object( self.library, '_deallocate_container') + mock_delete_policy = self.mock_object(self.library, + '_delete_fpolicy_for_share') self.assertRaises(exception.NetAppException, self.library.create_share_from_snapshot, @@ -892,6 +894,9 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.src_vserver_client) mock_deallocate_container.assert_called_once_with( fake.SHARE_NAME, self.src_vserver_client) + mock_delete_policy.assert_called_once_with(self.temp_src_share, + fake.VSERVER1, + self.src_vserver_client) def test__update_create_from_snapshot_status(self): fake_result = mock.Mock() @@ -959,6 +964,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.library, '_deallocate_container') mock_pvt_storage_delete = self.mock_object( self.library.private_storage, 'delete') + mock_delete_policy = self.mock_object(self.library, + '_delete_fpolicy_for_share') result = self.library._update_create_from_snapshot_status( fake.SHARE, fake.SHARE_SERVER) @@ -980,6 +987,9 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_deallocate_container.assert_called_once_with(fake.SHARE_NAME, src_vserver_client) mock_pvt_storage_delete.assert_called_once_with(fake.SHARE['id']) + mock_delete_policy.assert_called_once_with(fake_src_share, + fake.VSERVER1, + src_vserver_client) self.assertEqual(expected_result, result) def _setup_mocks_for_create_from_snapshot_continue( @@ -1213,7 +1223,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): [mock.call(self.fake_src_share['id']), mock.call(fake.SHARE_ID)]) self.mock__delete_share.assert_called_once_with( - self.fake_src_share, self.src_vserver_client, + self.fake_src_share, fake.VSERVER1, self.src_vserver_client, remove_export=False) self.mock_set_vol_size_fixes.assert_called_once_with( fake.SHARE_NAME, filesys_size_fixed=False) @@ -1252,10 +1262,13 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.mock_pvt_storage_delete.assert_called_once_with(fake.SHARE_ID) - @ddt.data(False, True) - def test_allocate_container(self, hide_snapdir): + @ddt.data({'hide_snapdir': False, 'create_fpolicy': True}, + {'hide_snapdir': True, 'create_fpolicy': False}) + @ddt.unpack + def test_allocate_container(self, hide_snapdir, create_fpolicy): - provisioning_options = copy.deepcopy(fake.PROVISIONING_OPTIONS) + provisioning_options = copy.deepcopy( + fake.PROVISIONING_OPTIONS_WITH_FPOLICY) provisioning_options['hide_snapdir'] = hide_snapdir self.mock_object(self.library, '_get_backend_share_name', mock.Mock( return_value=fake.SHARE_NAME)) @@ -1264,11 +1277,14 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_get_provisioning_opts = self.mock_object( self.library, '_get_provisioning_options_for_share', mock.Mock(return_value=provisioning_options)) + mock_create_fpolicy = self.mock_object( + self.library, '_create_fpolicy_for_share') vserver_client = mock.Mock() self.library._allocate_container(fake.SHARE_INSTANCE, fake.VSERVER1, - vserver_client) + vserver_client, + create_fpolicy=create_fpolicy) mock_get_provisioning_opts.assert_called_once_with( fake.SHARE_INSTANCE, fake.VSERVER1, vserver_client=vserver_client, @@ -1276,10 +1292,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): vserver_client.create_volume.assert_called_once_with( fake.POOL_NAME, fake.SHARE_NAME, fake.SHARE['size'], - thin_provisioned=True, snapshot_policy='default', - language='en-US', dedup_enabled=True, split=True, encrypt=False, - compression_enabled=False, max_files=5000, snapshot_reserve=8, - adaptive_qos_policy_group=None) + snapshot_reserve=8, **provisioning_options) if hide_snapdir: vserver_client.set_volume_snapdir_access.assert_called_once_with( @@ -1287,6 +1300,13 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): else: vserver_client.set_volume_snapdir_access.assert_not_called() + if create_fpolicy: + mock_create_fpolicy.assert_called_once_with( + fake.SHARE_INSTANCE, fake.VSERVER1, vserver_client, + **provisioning_options) + else: + mock_create_fpolicy.assert_not_called() + def test_remap_standard_boolean_extra_specs(self): extra_specs = copy.deepcopy(fake.OVERLAPPING_EXTRA_SPEC) @@ -1370,7 +1390,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): def test_check_string_extra_specs_validity(self): result = self.library._check_string_extra_specs_validity( - fake.SHARE_INSTANCE, fake.EXTRA_SPEC) + fake.SHARE_INSTANCE, fake.EXTRA_SPEC_WITH_FPOLICY) self.assertIsNone(result) @@ -1499,6 +1519,9 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): 'split': False, 'encrypt': False, 'hide_snapdir': False, + 'fpolicy_extensions_to_exclude': None, + 'fpolicy_extensions_to_include': None, + 'fpolicy_file_operations': None, } self.assertEqual(expected, result) @@ -1624,6 +1647,21 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.library._check_if_max_files_is_valid, fake.SHARE, 'abc') + def test__check_fpolicy_file_operations(self): + result = self.library._check_fpolicy_file_operations( + fake.SHARE, fake.FPOLICY_FILE_OPERATIONS) + + self.assertIsNone(result) + + def test__check_fpolicy_file_operations_invalid_operation(self): + invalid_ops = copy.deepcopy(fake.FPOLICY_FILE_OPERATIONS) + invalid_ops += ',fake_op' + + self.assertRaises(exception.NetAppException, + self.library._check_fpolicy_file_operations, + fake.SHARE, + invalid_ops) + def test_allocate_container_no_pool(self): vserver_client = mock.Mock() @@ -1657,24 +1695,26 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.EXTRA_SPEC) @ddt.data({'provider_location': None, 'size': 50, 'hide_snapdir': True, - 'split': None}, + 'split': None, 'create_fpolicy': False}, {'provider_location': 'fake_location', 'size': 30, - 'hide_snapdir': False, 'split': True}, + 'hide_snapdir': False, 'split': True, 'create_fpolicy': True}, {'provider_location': 'fake_location', 'size': 20, - 'hide_snapdir': True, 'split': False}) + 'hide_snapdir': True, 'split': False, 'create_fpolicy': True}) @ddt.unpack def test_allocate_container_from_snapshot( - self, provider_location, size, hide_snapdir, split): - - provisioning_options = copy.deepcopy(fake.PROVISIONING_OPTIONS) + self, provider_location, size, hide_snapdir, split, + create_fpolicy): + provisioning_options = copy.deepcopy( + fake.PROVISIONING_OPTIONS_WITH_FPOLICY) provisioning_options['hide_snapdir'] = hide_snapdir mock_get_provisioning_opts = self.mock_object( self.library, '_get_provisioning_options_for_share', mock.Mock(return_value=provisioning_options)) + mock_create_fpolicy = self.mock_object( + self.library, '_create_fpolicy_for_share') vserver = fake.VSERVER1 vserver_client = mock.Mock() original_snapshot_size = 20 - expected_split_op = split or fake.PROVISIONING_OPTIONS['split'] fake_share_inst = copy.deepcopy(fake.SHARE_INSTANCE) fake_share_inst['size'] = size @@ -1682,10 +1722,12 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake_snapshot['provider_location'] = provider_location fake_snapshot['size'] = original_snapshot_size - self.library._allocate_container_from_snapshot(fake_share_inst, - fake_snapshot, - vserver, - vserver_client) + self.library._allocate_container_from_snapshot( + fake_share_inst, + fake_snapshot, + vserver, + vserver_client, + create_fpolicy=create_fpolicy) share_name = self.library._get_backend_share_name( fake_share_inst['id']) @@ -1697,10 +1739,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake_share_inst, fake.VSERVER1, vserver_client=vserver_client) vserver_client.create_volume_clone.assert_called_once_with( share_name, parent_share_name, parent_snapshot_name, - thin_provisioned=True, snapshot_policy='default', - language='en-US', dedup_enabled=True, split=expected_split_op, - encrypt=False, compression_enabled=False, max_files=5000, - adaptive_qos_policy_group=None) + **provisioning_options) if size > original_snapshot_size: vserver_client.set_volume_size.assert_called_once_with( share_name, size) @@ -1713,6 +1752,13 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): else: vserver_client.set_volume_snapdir_access.assert_not_called() + if create_fpolicy: + mock_create_fpolicy.assert_called_once_with( + fake_share_inst, vserver, vserver_client, + **provisioning_options) + else: + mock_create_fpolicy.assert_not_called() + def test_share_exists(self): vserver_client = mock.Mock() @@ -1744,6 +1790,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_remove_export = self.mock_object(self.library, '_remove_export') mock_deallocate_container = self.mock_object(self.library, '_deallocate_container') + mock_delete_policy = self.mock_object(self.library, + '_delete_fpolicy_for_share') self.library.delete_share(self.context, fake.SHARE, @@ -1756,6 +1804,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_remove_export.assert_called_once_with(fake.SHARE, vserver_client) mock_deallocate_container.assert_called_once_with(share_name, vserver_client) + mock_delete_policy.assert_called_once_with(fake.SHARE, fake.VSERVER1, + vserver_client) (vserver_client.mark_qos_policy_group_for_deletion .assert_called_once_with(qos_policy_name)) self.assertEqual(0, lib_base.LOG.info.call_count) @@ -1799,6 +1849,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_remove_export = self.mock_object(self.library, '_remove_export') mock_deallocate_container = self.mock_object(self.library, '_deallocate_container') + mock_delete_fpolicy = self.mock_object(self.library, + '_delete_fpolicy_for_share') self.library.delete_share(self.context, fake.SHARE, @@ -1806,6 +1858,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): share_name = self.library._get_backend_share_name(fake.SHARE['id']) mock_share_exists.assert_called_once_with(share_name, vserver_client) + mock_delete_fpolicy.assert_called_once_with(fake.SHARE, fake.VSERVER1, + vserver_client) self.assertFalse(mock_remove_export.called) self.assertFalse(mock_deallocate_container.called) self.assertFalse( @@ -2205,13 +2259,19 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.assertIsNone(result) - @ddt.data(True, False) - def test_manage_container_with_qos(self, qos): + @ddt.data({'qos': True, 'fpolicy': False}, {'qos': False, 'fpolicy': True}) + @ddt.unpack + def test_manage_container(self, qos, fpolicy): vserver_client = mock.Mock() self.library._have_cluster_creds = True qos_policy_group_name = fake.QOS_POLICY_GROUP_NAME if qos else None - extra_specs = fake.EXTRA_SPEC_WITH_QOS if qos else fake.EXTRA_SPEC + if qos: + extra_specs = copy.deepcopy(fake.EXTRA_SPEC_WITH_QOS) + elif fpolicy: + extra_specs = copy.deepcopy(fake.EXTRA_SPEC_WITH_FPOLICY) + else: + extra_specs = copy.deepcopy(fake.EXTRA_SPEC) provisioning_opts = self.library._get_provisioning_options(extra_specs) if qos: provisioning_opts['qos_policy_group'] = fake.QOS_POLICY_GROUP_NAME @@ -2244,6 +2304,15 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_modify_or_create_qos_policy = self.mock_object( self.library, '_modify_or_create_qos_for_existing_share', mock.Mock(return_value=qos_policy_group_name)) + fake_fpolicy_scope = { + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'shares-to-include': [fake.FLEXVOL_NAME] + } + mock_find_scope = self.mock_object( + self.library, '_find_reusable_fpolicy_scope', + mock.Mock(return_value=fake_fpolicy_scope)) + mock_modify_fpolicy = self.mock_object( + vserver_client, 'modify_fpolicy_scope') result = self.library._manage_container(share_to_manage, fake.VSERVER1, @@ -2266,6 +2335,18 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_modify_or_create_qos_policy.assert_called_once_with( share_to_manage, extra_specs, fake.VSERVER1, vserver_client) mock_validate_volume_for_manage.assert_called() + if fpolicy: + mock_find_scope.assert_called_once_with( + share_to_manage, vserver_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS, + shares_to_include=[fake.FLEXVOL_NAME]) + mock_modify_fpolicy.assert_called_once_with( + fake.FPOLICY_POLICY_NAME, shares_to_include=[fake.SHARE_NAME]) + else: + mock_find_scope.assert_not_called() + mock_modify_fpolicy.assert_not_called() original_data = { 'original_name': fake.FLEXVOL_TO_MANAGE['name'], @@ -2350,6 +2431,35 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.VSERVER1, vserver_client) + def test_manage_container_invalid_fpolicy(self): + vserver_client = mock.Mock() + extra_spec = copy.deepcopy(fake.EXTRA_SPEC_WITH_FPOLICY) + share_to_manage = copy.deepcopy(fake.SHARE) + share_to_manage['export_location'] = fake.EXPORT_LOCATION + + mock_helper = mock.Mock() + mock_helper.get_share_name_for_share.return_value = fake.FLEXVOL_NAME + self.mock_object(self.library, + '_get_helper', + mock.Mock(return_value=mock_helper)) + + self.mock_object(vserver_client, + 'get_volume_to_manage', + mock.Mock(return_value=fake.FLEXVOL_TO_MANAGE)) + self.mock_object(self.library, '_validate_volume_for_manage') + self.mock_object(share_types, + 'get_extra_specs_from_share', + mock.Mock(return_value=extra_spec)) + self.mock_object(self.library, '_check_extra_specs_validity') + self.mock_object(self.library, '_find_reusable_fpolicy_scope', + mock.Mock(return_value=None)) + + self.assertRaises(exception.ManageExistingShareTypeMismatch, + self.library._manage_container, + share_to_manage, + fake.VSERVER1, + vserver_client) + def test_validate_volume_for_manage(self): vserver_client = mock.Mock() @@ -5004,7 +5114,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.mock_object(share_types, 'get_extra_specs_from_share') self.mock_object(self.library, '_check_extra_specs_validity') self.mock_object(self.library, '_check_aggregate_extra_specs_validity') - self.mock_object(self.library, '_get_provisioning_options') + self.mock_object(self.library, '_get_provisioning_options', + mock.Mock(return_value={})) self.mock_object(self.library, '_get_normalized_qos_specs') self.mock_object(self.library, 'validate_provisioning_options_for_share') @@ -5046,7 +5157,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.mock_object(self.library, '_check_aggregate_extra_specs_validity') self.mock_object(self.library, '_get_backend_share_name', mock.Mock(return_value=fake.SHARE_NAME)) - self.mock_object(self.library, '_get_provisioning_options') + self.mock_object(self.library, '_get_provisioning_options', + mock.Mock(return_value={})) self.mock_object(self.library, '_get_normalized_qos_specs') self.mock_object(self.library, 'validate_provisioning_options_for_share') @@ -5085,7 +5197,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.mock_object(self.library, '_get_backend_share_name', mock.Mock(return_value=fake.SHARE_NAME)) self.mock_object(data_motion, 'get_backend_configuration') - self.mock_object(self.library, '_get_provisioning_options') + self.mock_object(self.library, '_get_provisioning_options', + mock.Mock(return_value={})) self.mock_object(self.library, '_get_normalized_qos_specs') self.mock_object(self.library, 'validate_provisioning_options_for_share') @@ -5126,7 +5239,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.mock_object(share_types, 'get_extra_specs_from_share') self.mock_object(self.library, '_check_extra_specs_validity') self.mock_object(self.library, '_check_aggregate_extra_specs_validity') - self.mock_object(self.library, '_get_provisioning_options') + self.mock_object(self.library, '_get_provisioning_options', + mock.Mock(return_value={})) self.mock_object(self.library, '_get_normalized_qos_specs') self.mock_object(self.library, 'validate_provisioning_options_for_share') @@ -5167,8 +5281,18 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): [mock.call(share_server=fake.SHARE_SERVER), mock.call(share_server='dst_srv')]) - def test_migration_check_compatibility(self): + @ddt.data(False, True) + def test_migration_check_compatibility(self, fpolicy): self.library._have_cluster_creds = True + mock_dest_client = mock.Mock() + if fpolicy: + provisioning_options = copy.deepcopy( + fake.PROVISIONING_OPTIONS_WITH_FPOLICY) + get_vserver_side_effect = [(mock.Mock(), mock_dest_client), + (fake.VSERVER1, mock.Mock())] + else: + get_vserver_side_effect = [(fake.VSERVER1, mock.Mock())] + provisioning_options = {} self.mock_object(share_types, 'get_extra_specs_from_share') self.mock_object(self.library, '_check_extra_specs_validity') self.mock_object(self.library, '_check_aggregate_extra_specs_validity') @@ -5176,21 +5300,33 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(return_value=fake.SHARE_NAME)) self.mock_object(data_motion, 'get_backend_configuration') self.mock_object(self.library, '_get_vserver', - mock.Mock(return_value=(fake.VSERVER1, mock.Mock()))) + mock.Mock(side_effect=get_vserver_side_effect)) self.mock_object(share_utils, 'extract_host', mock.Mock( side_effect=['destination_backend', 'destination_pool'])) mock_move_check = self.mock_object(self.client, 'check_volume_move') self.mock_object(self.library, '_get_dest_flexvol_encryption_value', mock.Mock(return_value=False)) - self.mock_object(self.library, '_get_provisioning_options') + self.mock_object(self.library, '_get_provisioning_options', + mock.Mock(return_value=provisioning_options)) self.mock_object(self.library, '_get_normalized_qos_specs') self.mock_object(self.library, 'validate_provisioning_options_for_share') + self.mock_object(self.library, + '_check_destination_vserver_for_vol_move') + fpolicies = [ + x for x in range(1, self.library.FPOLICY_MAX_VSERVER_POLICIES + 1)] + mock_fpolicy_status = self.mock_object( + mock_dest_client, 'get_fpolicy_policies_status', + mock.Mock(return_value=fpolicies)) + mock_reusable_fpolicy = self.mock_object( + self.library, '_find_reusable_fpolicy_scope', + mock.Mock(return_value={'fake'})) + src_instance = fake_share.fake_share_instance() + dst_instance = fake_share.fake_share_instance() migration_compatibility = self.library.migration_check_compatibility( - self.context, fake_share.fake_share_instance(), - fake_share.fake_share_instance(), share_server=fake.SHARE_SERVER, - destination_share_server='dst_srv') + self.context, src_instance, dst_instance, + share_server=fake.SHARE_SERVER, destination_share_server='dst_srv') expected_compatibility = { 'compatible': True, @@ -5205,9 +5341,20 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_move_check.assert_called_once_with( fake.SHARE_NAME, fake.VSERVER1, 'destination_pool', encrypt_destination=False) - self.library._get_vserver.assert_has_calls( - [mock.call(share_server=fake.SHARE_SERVER), - mock.call(share_server='dst_srv')]) + if fpolicy: + self.library._get_vserver.assert_has_calls( + [mock.call(share_server='dst_srv'), + mock.call(share_server=fake.SHARE_SERVER)]) + mock_fpolicy_status.assert_called_once() + mock_reusable_fpolicy.assert_called_once_with( + dst_instance, mock_dest_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS + ) + else: + self.library._get_vserver.assert_called_once_with( + share_server=fake.SHARE_SERVER) def test_migration_check_compatibility_destination_type_is_encrypted(self): self.library._have_cluster_creds = True @@ -5227,7 +5374,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): '_check_extra_specs_validity') self.mock_object(self.library, '_check_aggregate_extra_specs_validity') - self.mock_object(self.library, '_get_provisioning_options') + self.mock_object(self.library, '_get_provisioning_options', + mock.Mock(return_value={})) self.mock_object(self.library, '_get_normalized_qos_specs') self.mock_object(self.library, 'validate_provisioning_options_for_share') @@ -5251,7 +5399,6 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock_move_check.assert_called_once_with( fake.SHARE_NAME, fake.VSERVER1, 'destination_pool', encrypt_destination=True) - self.library._get_vserver.assert_has_calls( [mock.call(share_server=fake.SHARE_SERVER), mock.call(share_server='dst_srv')]) @@ -5529,6 +5676,9 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): 'policy_group_name': fake.QOS_POLICY_GROUP_NAME}, {'phase': 'completed', 'provisioning_options': fake.PROVISIONING_OPTIONS, + 'policy_group_name': False}, + {'phase': 'completed', + 'provisioning_options': fake.PROVISIONING_OPTIONS_WITH_FPOLICY, 'policy_group_name': False}) @ddt.unpack def test_migration_complete(self, phase, provisioning_options, @@ -5564,6 +5714,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): mock.Mock(side_effect=vol_move_side_effects)) self.mock_object(share_types, 'get_extra_specs_from_share', mock.Mock(return_value=fake.EXTRA_SPEC)) + self.mock_object(self.library, '_check_fpolicy_file_operations') self.mock_object( self.library, '_get_provisioning_options', mock.Mock(return_value=provisioning_options)) @@ -5571,6 +5722,11 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.library, '_modify_or_create_qos_for_existing_share', mock.Mock(return_value=policy_group_name)) self.mock_object(vserver_client, 'modify_volume') + mock_create_new_fpolicy = self.mock_object( + self.library, '_create_fpolicy_for_share') + + mock_delete_policy = self.mock_object(self.library, + '_delete_fpolicy_for_share') src_share = fake_share.fake_share_instance(id='source-share-instance') dest_share = fake_share.fake_share_instance(id='dest-share-instance') @@ -5596,6 +5752,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): vserver_client.modify_volume.assert_called_once_with( dest_aggr, 'new_share_name', **provisioning_options) mock_info_log.assert_called_once() + mock_delete_policy.assert_called_once_with(src_share, fake.VSERVER1, + vserver_client) if phase != 'completed': self.assertEqual(2, mock_warning_log.call_count) self.assertFalse(mock_debug_log.called) @@ -5604,6 +5762,13 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.assertFalse(mock_warning_log.called) mock_debug_log.assert_called_once() mock_move_status_check.assert_called_once() + if provisioning_options.get( + 'fpolicy_extensions_to_include') is not None: + mock_create_new_fpolicy.assert_called_once_with( + dest_share, fake.VSERVER1, vserver_client, + **provisioning_options) + else: + mock_create_new_fpolicy.assert_not_called() def test_modify_or_create_qos_for_existing_share_no_qos_extra_specs(self): vserver_client = mock.Mock() @@ -6011,7 +6176,16 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): {'provisioning_opts': fake.PROVISIONING_OPTS_WITH_ADAPT_QOS, 'qos_specs': None, 'extra_specs': None, - 'cluster_credentials': False},) + 'cluster_credentials': False}, + {'provisioning_opts': fake.PROVISIONING_OPTIONS_INVALID_FPOLICY, + 'qos_specs': None, + 'extra_specs': None, + 'cluster_credentials': False}, + {'provisioning_opts': fake.PROVISIONING_OPTIONS_WITH_FPOLICY, + 'qos_specs': None, + 'extra_specs': {'replication_type': 'dr'}, + 'cluster_credentials': False} + ) @ddt.unpack def test_validate_provisioning_options_for_share_invalid_params( self, provisioning_opts, qos_specs, extra_specs, @@ -6022,3 +6196,275 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.library.validate_provisioning_options_for_share, provisioning_opts, extra_specs=extra_specs, qos_specs=qos_specs) + + def test__get_backend_fpolicy_policy_name(self): + result = self.library._get_backend_fpolicy_policy_name( + fake.SHARE_ID) + expected = 'fpolicy_policy_' + fake.SHARE_ID.replace('-', '_') + + self.assertEqual(expected, result) + + def test__get_backend_fpolicy_event_name(self): + result = self.library._get_backend_fpolicy_event_name( + fake.SHARE_ID, 'NFS') + expected = 'fpolicy_event_nfs_' + fake.SHARE_ID.replace('-', '_') + + self.assertEqual(expected, result) + + @ddt.data({}, + {'policy-name': fake.FPOLICY_POLICY_NAME, + 'shares-to-include': [fake.SHARE_NAME]}) + def test__create_fpolicy_for_share(self, reusable_scope): + vserver_client = mock.Mock() + vserver_name = fake.VSERVER1 + new_fake_share = copy.deepcopy(fake.SHARE) + new_fake_share['id'] = 'new_fake_id' + new_fake_share['share_proto'] = 'CIFS' + event_name = 'fpolicy_event_cifs_new_fake_id' + events = [event_name] + policy_name = 'fpolicy_policy_new_fake_id' + shares_to_include = [] + if reusable_scope: + shares_to_include = copy.deepcopy( + reusable_scope.get('shares-to-include')) + shares_to_include.append('share_new_fake_id') + + mock_reusable_scope = self.mock_object( + self.library, '_find_reusable_fpolicy_scope', + mock.Mock(return_value=reusable_scope)) + mock_modify_policy = self.mock_object( + vserver_client, 'modify_fpolicy_scope') + mock_get_policies = self.mock_object( + vserver_client, 'get_fpolicy_policies_status', + mock.Mock(return_value=[])) + mock_create_event = self.mock_object( + vserver_client, 'create_fpolicy_event') + mock_create_fpolicy = self.mock_object( + vserver_client, 'create_fpolicy_policy') + mock_create_scope = self.mock_object( + vserver_client, 'create_fpolicy_scope') + mock_enable_fpolicy = self.mock_object( + vserver_client, 'enable_fpolicy_policy') + + self.library._create_fpolicy_for_share( + new_fake_share, vserver_name, vserver_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS) + + mock_reusable_scope.assert_called_once_with( + new_fake_share, vserver_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS) + + if reusable_scope: + mock_modify_policy.assert_called_once_with( + fake.FPOLICY_POLICY_NAME, shares_to_include=shares_to_include) + mock_get_policies.assert_not_called() + mock_create_event.assert_not_called() + mock_create_fpolicy.assert_not_called() + mock_create_scope.assert_not_called() + mock_enable_fpolicy.assert_not_called() + else: + mock_modify_policy.assert_not_called() + + mock_get_policies.assert_called_once() + mock_create_event.assert_called_once_with( + event_name, new_fake_share['share_proto'].lower(), + fake.FPOLICY_FILE_OPERATIONS_LIST) + mock_create_fpolicy.assert_called_once_with(policy_name, events) + mock_create_scope.assert_called_once_with( + policy_name, 'share_new_fake_id', + extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE) + mock_enable_fpolicy.assert_called_once_with(policy_name, 1) + + def test__create_fpolicy_for_share_max_policies_error(self): + fake_client = mock.Mock() + vserver_name = fake.VSERVER1 + mock_reusable_scope = self.mock_object( + self.library, '_find_reusable_fpolicy_scope', + mock.Mock(return_value=None)) + policies = [ + x for x in range(1, self.library.FPOLICY_MAX_VSERVER_POLICIES + 1)] + mock_get_policies = self.mock_object( + fake_client, 'get_fpolicy_policies_status', + mock.Mock(return_value=policies)) + + self.assertRaises( + exception.NetAppException, + self.library._create_fpolicy_for_share, + fake.SHARE, vserver_name, fake_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS) + + mock_reusable_scope.assert_called_once_with( + fake.SHARE, fake_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS) + mock_get_policies.assert_called_once() + + def test__create_fpolicy_for_share_client_error(self): + fake_client = mock.Mock() + vserver_name = fake.VSERVER1 + new_fake_share = copy.deepcopy(fake.SHARE) + new_fake_share['id'] = 'new_fake_id' + new_fake_share['share_proto'] = 'CIFS' + event_name = 'fpolicy_event_cifs_new_fake_id' + events = [event_name] + policy_name = 'fpolicy_policy_new_fake_id' + + mock_reusable_scope = self.mock_object( + self.library, '_find_reusable_fpolicy_scope', + mock.Mock(return_value=None)) + mock_get_policies = self.mock_object( + fake_client, 'get_fpolicy_policies_status', + mock.Mock(return_value=[])) + mock_create_event = self.mock_object( + fake_client, 'create_fpolicy_event') + mock_create_fpolicy = self.mock_object( + fake_client, 'create_fpolicy_policy') + mock_create_scope = self.mock_object( + fake_client, 'create_fpolicy_scope', + mock.Mock(side_effect=self._mock_api_error())) + mock_delete_fpolicy = self.mock_object( + fake_client, 'delete_fpolicy_policy') + mock_delete_event = self.mock_object( + fake_client, 'delete_fpolicy_event') + + self.assertRaises( + exception.NetAppException, + self.library._create_fpolicy_for_share, + new_fake_share, vserver_name, fake_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS) + + mock_reusable_scope.assert_called_once_with( + new_fake_share, fake_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS) + mock_get_policies.assert_called_once() + mock_create_event.assert_called_once_with( + event_name, new_fake_share['share_proto'].lower(), + fake.FPOLICY_FILE_OPERATIONS_LIST) + mock_create_fpolicy.assert_called_once_with(policy_name, events) + mock_create_scope.assert_called_once_with( + policy_name, 'share_new_fake_id', + extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE) + mock_delete_fpolicy.assert_called_once_with(policy_name) + mock_delete_event.assert_called_once_with(event_name) + + def test__find_reusable_fpolicy_scope(self): + vserver_client = mock.Mock() + new_fake_share = copy.deepcopy(fake.SHARE) + new_fake_share['share_proto'] = 'CIFS' + reusable_scopes = [{ + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'file-extensions-to-include': fake.FPOLICY_EXT_TO_INCLUDE_LIST, + 'file-extensions-to-exclude': fake.FPOLICY_EXT_TO_EXCLUDE_LIST, + 'shares-to-include': ['any_other_fake_share'], + }] + reusable_policies = [{ + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'engine-name': fake.FPOLICY_ENGINE, + 'events': [fake.FPOLICY_EVENT_NAME] + }] + reusable_events = [{ + 'event-name': fake.FPOLICY_EVENT_NAME, + 'protocol': new_fake_share['share_proto'].lower(), + 'file-operations': fake.FPOLICY_FILE_OPERATIONS_LIST + }] + mock_get_scopes = self.mock_object( + vserver_client, 'get_fpolicy_scopes', + mock.Mock(return_value=reusable_scopes)) + mock_get_policies = self.mock_object( + vserver_client, 'get_fpolicy_policies', + mock.Mock(return_value=reusable_policies)) + mocke_get_events = self.mock_object( + vserver_client, 'get_fpolicy_events', + mock.Mock(return_value=reusable_events) + ) + + result = self.library._find_reusable_fpolicy_scope( + new_fake_share, vserver_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS) + + self.assertEqual(reusable_scopes[0], result) + + mock_get_scopes.assert_called_once_with( + extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + shares_to_include=None) + mock_get_policies.assert_called_once_with( + policy_name=fake.FPOLICY_POLICY_NAME) + mocke_get_events.assert_called_once_with( + event_name=fake.FPOLICY_EVENT_NAME) + + @ddt.data(False, True) + def test__delete_fpolicy_for_share(self, last_share): + fake_vserver_client = mock.Mock() + fake_vserver_name = fake.VSERVER1 + fake_share = copy.deepcopy(fake.SHARE) + share_name = self.library._get_backend_share_name(fake.SHARE_ID) + existing_shares = [share_name] + if not last_share: + existing_shares.append('any_other_share') + scopes = [{ + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'file-extensions-to-include': fake.FPOLICY_EXT_TO_INCLUDE_LIST, + 'file-extensions-to-exclude': fake.FPOLICY_EXT_TO_EXCLUDE_LIST, + 'shares-to-include': existing_shares, + }] + shares_to_include = copy.copy(scopes[0].get('shares-to-include')) + shares_to_include.remove(share_name) + policies = [{ + 'policy-name': fake.FPOLICY_POLICY_NAME, + 'engine-name': fake.FPOLICY_ENGINE, + 'events': [fake.FPOLICY_EVENT_NAME] + }] + + mock_get_scopes = self.mock_object( + fake_vserver_client, 'get_fpolicy_scopes', + mock.Mock(return_value=scopes)) + mock_modify_scope = self.mock_object( + fake_vserver_client, 'modify_fpolicy_scope') + + mock_disable_policy = self.mock_object( + fake_vserver_client, 'disable_fpolicy_policy') + mock_get_policies = self.mock_object( + fake_vserver_client, 'get_fpolicy_policies', + mock.Mock(return_value=policies)) + mock_delete_scope = self.mock_object( + fake_vserver_client, 'delete_fpolicy_scope') + mock_delete_policy = self.mock_object( + fake_vserver_client, 'delete_fpolicy_policy') + mock_delete_event = self.mock_object( + fake_vserver_client, 'delete_fpolicy_event') + + self.library._delete_fpolicy_for_share(fake_share, fake_vserver_name, + fake_vserver_client) + + mock_get_scopes.assert_called_once_with( + shares_to_include=[share_name]) + if shares_to_include: + mock_modify_scope.assert_called_once_with( + fake.FPOLICY_POLICY_NAME, shares_to_include=shares_to_include) + else: + mock_disable_policy.assert_called_once_with( + fake.FPOLICY_POLICY_NAME) + mock_get_policies.assert_called_once_with( + policy_name=fake.FPOLICY_POLICY_NAME) + mock_delete_scope.assert_called_once_with( + fake.FPOLICY_POLICY_NAME) + mock_delete_policy.assert_called_once_with( + fake.FPOLICY_POLICY_NAME) + mock_delete_event.assert_called_once_with( + fake.FPOLICY_EVENT_NAME) 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 2e03ddb822..e481056df1 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 @@ -2240,7 +2240,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.SHARE_INSTANCE, self.mock_src_client, fake_volume['aggregate'], self.mock_dest_client) self.library._delete_share.assert_called_once_with( - fake.SHARE_INSTANCE, self.mock_src_client, remove_export=True) + fake.SHARE_INSTANCE, self.fake_src_vserver, + self.mock_src_client, remove_export=True) def test_share_server_migration_complete_failure_breaking(self): dm_session_mock = mock.Mock() @@ -2281,7 +2282,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.fake_src_share_server, self.fake_dest_share_server ) self.library._delete_share.assert_called_once_with( - fake.SHARE_INSTANCE, self.mock_dest_client, remove_export=False) + fake.SHARE_INSTANCE, self.fake_dest_vserver, self.mock_dest_client, + remove_export=False) def test_share_server_migration_complete_failure_get_new_volume(self): dm_session_mock = mock.Mock() @@ -2375,7 +2377,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): self.fake_src_share_server, self.fake_dest_share_server ) self.library._delete_share.assert_called_once_with( - fake.SHARE_INSTANCE, self.mock_dest_client, remove_export=False) + fake.SHARE_INSTANCE, self.fake_dest_vserver, self.mock_dest_client, + remove_export=False) def test_share_server_migration_cancel_snapmirror_failure(self): dm_session_mock = mock.Mock() @@ -2442,6 +2445,12 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): } self.mock_object(mock_client, 'get_vserver_info', mock.Mock(return_value=fake_vserver_info)) + mock_get_extra_spec = self.mock_object( + share_types, 'get_extra_specs_from_share', + mock.Mock(return_value='fake_extra_specs')) + mock_get_provisioning_opts = self.mock_object( + self.library, '_get_provisioning_options', + mock.Mock(return_value={})) result = self.library.choose_share_server_compatible_with_share( None, [fake.SHARE_SERVER], fake.SHARE_INSTANCE, @@ -2449,6 +2458,8 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): ) expected_result = fake.SHARE_SERVER if compatible else None self.assertEqual(expected_result, result) + mock_get_extra_spec.assert_called_once_with(fake.SHARE_INSTANCE) + mock_get_provisioning_opts.assert_called_once_with('fake_extra_specs') if (share_group and share_group['share_server_id'] != fake.SHARE_SERVER['id']): mock_client.get_vserver_info.assert_not_called() @@ -2461,6 +2472,55 @@ class NetAppFileStorageLibraryTestCase(test.TestCase): fake.SHARE_SERVER, backend_name=fake.BACKEND_NAME ) + @ddt.data( + {'policies': [], 'reusable_scope': None, 'compatible': True}, + {'policies': "0123456789", 'reusable_scope': {'scope'}, + 'compatible': True}, + {'policies': "0123456789", 'reusable_scope': None, + 'compatible': False}) + @ddt.unpack + def test_choose_share_server_compatible_with_share_fpolicy( + self, policies, reusable_scope, compatible): + self.library.is_nfs_config_supported = False + mock_client = mock.Mock() + fake_extra_spec = copy.deepcopy(fake.EXTRA_SPEC_WITH_FPOLICY) + mock_get_extra_spec = self.mock_object( + share_types, 'get_extra_specs_from_share', + mock.Mock(return_value=fake_extra_spec)) + self.mock_object(self.library, '_get_vserver', + mock.Mock(return_value=(fake.VSERVER1, + mock_client))) + self.mock_object(mock_client, 'get_vserver_info', + mock.Mock(return_value=fake.VSERVER_INFO)) + mock_get_policies = self.mock_object( + mock_client, 'get_fpolicy_policies_status', + mock.Mock(return_value=policies)) + mock_reusable_scope = self.mock_object( + self.library, '_find_reusable_fpolicy_scope', + mock.Mock(return_value=reusable_scope)) + + result = self.library.choose_share_server_compatible_with_share( + None, [fake.SHARE_SERVER], fake.SHARE_INSTANCE, + None, None + ) + + expected_result = fake.SHARE_SERVER if compatible else None + self.assertEqual(expected_result, result) + mock_get_extra_spec.assert_called_once_with(fake.SHARE_INSTANCE) + mock_client.get_vserver_info.assert_called_once_with( + fake.VSERVER1, + ) + self.library._get_vserver.assert_called_once_with( + fake.SHARE_SERVER, backend_name=fake.BACKEND_NAME + ) + mock_get_policies.assert_called_once() + if len(policies) >= self.library.FPOLICY_MAX_VSERVER_POLICIES: + mock_reusable_scope.assert_called_once_with( + fake.SHARE_INSTANCE, mock_client, + fpolicy_extensions_to_include=fake.FPOLICY_EXT_TO_INCLUDE, + fpolicy_extensions_to_exclude=fake.FPOLICY_EXT_TO_EXCLUDE, + fpolicy_file_operations=fake.FPOLICY_FILE_OPERATIONS) + @ddt.data({'subtype': 'default', 'compatible': True}, {'subtype': 'dp_destination', 'compatible': False}) @ddt.unpack diff --git a/manila/tests/share/drivers/netapp/dataontap/fakes.py b/manila/tests/share/drivers/netapp/dataontap/fakes.py index bbf7bc9d8e..03481b5e53 100644 --- a/manila/tests/share/drivers/netapp/dataontap/fakes.py +++ b/manila/tests/share/drivers/netapp/dataontap/fakes.py @@ -92,6 +92,16 @@ QOS_EXTRA_SPEC = 'netapp:maxiops' QOS_SIZE_DEPENDENT_EXTRA_SPEC = 'netapp:maxbpspergib' QOS_NORMALIZED_SPEC = 'maxiops' QOS_POLICY_GROUP_NAME = 'fake_qos_policy_group_name' +FPOLICY_POLICY_NAME = 'fake_fpolicy_name' +FPOLICY_EVENT_NAME = 'fake_fpolicy_event_name' +FPOLICY_PROTOCOL = 'cifs' +FPOLICY_FILE_OPERATIONS = 'create,write,rename' +FPOLICY_FILE_OPERATIONS_LIST = ['create', 'write', 'rename'] +FPOLICY_ENGINE = 'native' +FPOLICY_EXT_TO_INCLUDE = 'avi' +FPOLICY_EXT_TO_INCLUDE_LIST = ['avi'] +FPOLICY_EXT_TO_EXCLUDE = 'jpg,mp3' +FPOLICY_EXT_TO_EXCLUDE_LIST = ['jpg', 'mp3'] CLIENT_KWARGS = { 'username': 'admin', @@ -200,6 +210,12 @@ EXTRA_SPEC_WITH_REPLICATION.update({ 'replication_type': 'dr' }) +EXTRA_SPEC_WITH_FPOLICY = copy.copy(EXTRA_SPEC) +EXTRA_SPEC_WITH_FPOLICY.update( + {'fpolicy_extensions_to_include': FPOLICY_EXT_TO_INCLUDE, + 'fpolicy_extensions_to_exclude': FPOLICY_EXT_TO_EXCLUDE, + 'fpolicy_file_operations': FPOLICY_FILE_OPERATIONS}) + NFS_CONFIG_DEFAULT = { 'tcp-max-xfer-size': 65536, 'udp-max-xfer-size': 32768, @@ -262,6 +278,12 @@ EXTRA_SPEC_WITH_QOS.update({ QOS_EXTRA_SPEC: '3000', }) +EXTRA_SPEC_WITH_FPOLICY = copy.deepcopy(EXTRA_SPEC) +EXTRA_SPEC_WITH_FPOLICY.update( + {'netapp:fpolicy_extensions_to_include': FPOLICY_EXT_TO_INCLUDE, + 'netapp:fpolicy_extensions_to_exclude': FPOLICY_EXT_TO_EXCLUDE, + 'netapp:fpolicy_file_operations': FPOLICY_FILE_OPERATIONS}) + EXTRA_SPEC_WITH_SIZE_DEPENDENT_QOS = copy.deepcopy(EXTRA_SPEC) EXTRA_SPEC_WITH_SIZE_DEPENDENT_QOS.update({ 'qos': True, @@ -289,6 +311,16 @@ PROVISIONING_OPTS_WITH_ADAPT_QOS = copy.deepcopy(PROVISIONING_OPTIONS) PROVISIONING_OPTS_WITH_ADAPT_QOS.update( {'adaptive_qos_policy_group': QOS_POLICY_GROUP_NAME}) +PROVISIONING_OPTIONS_WITH_FPOLICY = copy.deepcopy(PROVISIONING_OPTIONS) +PROVISIONING_OPTIONS_WITH_FPOLICY.update( + {'fpolicy_extensions_to_include': FPOLICY_EXT_TO_INCLUDE, + 'fpolicy_extensions_to_exclude': FPOLICY_EXT_TO_EXCLUDE, + 'fpolicy_file_operations': FPOLICY_FILE_OPERATIONS}) + +PROVISIONING_OPTIONS_INVALID_FPOLICY = copy.deepcopy(PROVISIONING_OPTIONS) +PROVISIONING_OPTIONS_INVALID_FPOLICY.update( + {'fpolicy_file_operations': FPOLICY_FILE_OPERATIONS}) + PROVISIONING_OPTIONS_BOOLEAN = { 'thin_provisioned': True, 'dedup_enabled': False, @@ -313,6 +345,9 @@ PROVISIONING_OPTIONS_STRING = { 'language': 'en-US', 'max_files': 5000, 'adaptive_qos_policy_group': None, + 'fpolicy_extensions_to_exclude': None, + 'fpolicy_extensions_to_include': None, + 'fpolicy_file_operations': None, } PROVISIONING_OPTIONS_STRING_MISSING_SPECS = { @@ -320,6 +355,9 @@ PROVISIONING_OPTIONS_STRING_MISSING_SPECS = { 'language': 'en-US', 'max_files': None, 'adaptive_qos_policy_group': None, + 'fpolicy_extensions_to_exclude': None, + 'fpolicy_extensions_to_include': None, + 'fpolicy_file_operations': None, } PROVISIONING_OPTIONS_STRING_DEFAULT = { @@ -327,6 +365,9 @@ PROVISIONING_OPTIONS_STRING_DEFAULT = { 'language': None, 'max_files': None, 'adaptive_qos_policy_group': None, + 'fpolicy_extensions_to_exclude': None, + 'fpolicy_extensions_to_include': None, + 'fpolicy_file_operations': None, } SHORT_BOOLEAN_EXTRA_SPEC = { diff --git a/releasenotes/notes/netapp-add-fpolicy-support-dd31628a1c8e64d6.yaml b/releasenotes/notes/netapp-add-fpolicy-support-dd31628a1c8e64d6.yaml new file mode 100644 index 0000000000..f75940e0c2 --- /dev/null +++ b/releasenotes/notes/netapp-add-fpolicy-support-dd31628a1c8e64d6.yaml @@ -0,0 +1,25 @@ +--- +features: + - | + Added support for FPolicy on NetApp ONTAP driver. FPolicy allows creation + of file policies that specify file operation permissions according to + file type. This feature can be enabled using the following extra-specs: + + - ``netapp:fpolicy_extensions_to_include``: + specifies file extensions to be included for screening. Values should be + provided as comma separated list. + - ``netapp:fpolicy_extensions_to_exclude``: + specifies file extensions to be excluded for screening. Values should be + provided as comma separated list. + - ``netapp:fpolicy_file_operations``: + specifies all file operations to be monitored. Values should be provided + as comma separated list. + + FPolicy works for backends with and without share server management. When + using NetApp backends with SVM administrator accounts, make sure that the + assigned access-control role has access set to "all" for "vserver fpolicy" + directory. + + This feature does not work with share replicas to avoid failures on replica + promotion, due to lack of FPolicy resources in the destination SVM. +