From 1e6aa9533c8802cd11c4e9447845a46345dde090 Mon Sep 17 00:00:00 2001 From: Chris Archibald Date: Fri, 22 Mar 2019 07:12:18 -0700 Subject: [PATCH] New na_ontap_volume option (#52587) * Revert "changes to clusteR" This reverts commit 33ee1b71e4bc8435fb315762a871f8c4cb6c5f80. * Revert "Revert "changes to clusteR"" This reverts commit f1104a37b42886aebb4d2b2ab27c91c96d97858a. * Update to volume * fix documentation * Fix doc --- .../modules/storage/netapp/na_ontap_volume.py | 572 +++++++++++++++--- .../storage/netapp/test_na_ontap_volume.py | 516 +++++++++++++++- 2 files changed, 995 insertions(+), 93 deletions(-) diff --git a/lib/ansible/modules/storage/netapp/na_ontap_volume.py b/lib/ansible/modules/storage/netapp/na_ontap_volume.py index b6879995c62..752c4f4a7dc 100644 --- a/lib/ansible/modules/storage/netapp/na_ontap_volume.py +++ b/lib/ansible/modules/storage/netapp/na_ontap_volume.py @@ -1,6 +1,6 @@ #!/usr/bin/python -# (c) 2018, NetApp, Inc +# (c) 2018-2019, NetApp, Inc # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -21,6 +21,7 @@ extends_documentation_fragment: - netapp.na_ontap version_added: '2.6' author: NetApp Ansible Team (@carchi8py) + description: - Create or destroy or modify volumes on NetApp ONTAP. @@ -90,7 +91,7 @@ options: space_guarantee: description: - Space guarantee style for the volume. - choices: ['none', 'volume'] + choices: ['none', 'file', 'volume'] percent_snapshot_space: description: @@ -127,6 +128,100 @@ options: - the default policy name is 'default'. version_added: '2.8' + aggr_list: + description: + - an array of names of aggregates to be used for FlexGroup constituents. + version_added: '2.8' + + aggr_list_multiplier: + description: + - The number of times to iterate over the aggregates listed with the aggr_list parameter when creating a FlexGroup. + version_added: '2.8' + + auto_provision_as: + description: + - Automatically provision a FlexGroup volume. + version_added: '2.8' + choices: ['flexgroup'] + + snapdir_access: + description: + - This is an advanced option, the default is False. + - Enable the visible '.snapshot' directory that is normally present at system internal mount points. + - This value also turns on access to all other '.snapshot' directories in the volume. + type: bool + version_added: '2.8' + + atime_update: + description: + - This is an advanced option, the default is True. + - If false, prevent the update of inode access times when a file is read. + - This value is useful for volumes with extremely high read traffic, + since it prevents writes to the inode file for the volume from contending with reads from other files. + - This field should be used carefully. + - That is, use this field when you know in advance that the correct access time for inodes will not be needed for files on that volume. + type: bool + version_added: '2.8' + + wait_for_completion: + description: + - Set this parameter to 'true' for synchronous execution during create (wait until volume status is online) + - Set this parameter to 'false' for asynchronous execution + - For asynchronous, execution exits as soon as the request is sent, without checking volume status + type: bool + default: false + version_added: '2.8' + + time_out: + description: + - time to wait for flexGroup creation, modification, or deletion in seconds. + - Error out if task is not completed in defined time. + - if 0, the request is asynchronous. + - default is set to 3 minutes. + default: 180 + version_added: '2.8' + + language: + description: + - Language to use for Volume + - Default uses SVM language + - Possible values Language + - c POSIX + - ar Arabic + - cs Czech + - da Danish + - de German + - en English + - en_us English (US) + - es Spanish + - fi Finnish + - fr French + - he Hebrew + - hr Croatian + - hu Hungarian + - it Italian + - ja Japanese euc-j + - ja_v1 Japanese euc-j + - ja_jp.pck Japanese PCK (sjis) + - ja_jp.932 Japanese cp932 + - ja_jp.pck_v2 Japanese PCK (sjis) + - ko Korean + - no Norwegian + - nl Dutch + - pl Polish + - pt Portuguese + - ro Romanian + - ru Russian + - sk Slovak + - sl Slovenian + - sv Swedish + - tr Turkish + - zh Simplified Chinese + - zh.gbk Simplified Chinese (GBK) + - zh_tw Traditional Chinese euc-tw + - zh_tw.big5 Traditional Chinese Big 5 + - To use UTF-8 as the NFS character set, append '.UTF-8' to the language code + version_added: '2.8' ''' EXAMPLES = """ @@ -134,12 +229,25 @@ EXAMPLES = """ - name: Create FlexVol na_ontap_volume: state: present - name: ansibleVolume + name: ansibleVolume12 is_infinite: False - aggregate_name: aggr1 - size: 20 + aggregate_name: ansible_aggr + size: 100 size_unit: mb - junction_path: /ansibleVolume11 + space_guarantee: none + policy: default + percent_snapshot_space: 60 + vserver: ansibleVServer + wait_for_completion: True + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + + - name: Volume Delete + na_ontap_volume: + state: absent + name: ansibleVolume12 + aggregate_name: ansible_aggr vserver: ansibleVServer hostname: "{{ netapp_hostname }}" username: "{{ netapp_username }}" @@ -156,13 +264,52 @@ EXAMPLES = """ username: "{{ netapp_username }}" password: "{{ netapp_password }}" + - name: Create flexGroup volume manually + na_ontap_volume: + state: present + name: ansibleVolume + is_infinite: False + aggr_list: "{{ aggr_list }}" + aggr_list_multiplier: 2 + size: 200 + size_unit: mb + space_guarantee: none + policy: default + vserver: "{{ vserver }}" + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: False + unix_permissions: 777 + snapshot_policy: default + time_out: 0 + + - name: Create flexGroup volume auto provsion as flex group + na_ontap_volume: + state: present + name: ansibleVolume + is_infinite: False + auto_provision_as: flexgroup + size: 200 + size_unit: mb + space_guarantee: none + policy: default + vserver: "{{ vserver }}" + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + https: False + unix_permissions: 777 + snapshot_policy: default + time_out: 0 + """ RETURN = """ """ +import time import traceback - import ansible.module_utils.netapp as netapp_utils from ansible.module_utils.netapp_module import NetAppModule from ansible.module_utils.basic import AnsibleModule @@ -208,15 +355,23 @@ class NetAppOntapVolume(object): type=dict(type='str', default=None), policy=dict(type='str', default=None), junction_path=dict(type='str', default=None), - space_guarantee=dict(choices=['none', 'volume'], default=None), - percent_snapshot_space=dict(type='str', default=None), + space_guarantee=dict(choices=['none', 'file', 'volume'], default=None), + percent_snapshot_space=dict(type='int', default=None), volume_security_style=dict(choices=['mixed', 'ntfs', 'unified', 'unix'], default='mixed'), encrypt=dict(required=False, type='bool', default=False), efficiency_policy=dict(required=False, type='str'), unix_permissions=dict(required=False, type='str'), - snapshot_policy=dict(required=False, type='str') + snapshot_policy=dict(required=False, type='str'), + aggr_list=dict(required=False, type='list'), + aggr_list_multiplier=dict(required=False, type='int'), + snapdir_access=dict(required=False, type='bool'), + atime_update=dict(required=False, type='bool'), + auto_provision_as=dict(choices=['flexgroup'], required=False, type='str'), + wait_for_completion=dict(required=False, type='bool', default=False), + time_out=dict(required=False, type='int', default=180), + language=dict(type='str', required=False) )) self.module = AnsibleModule( argument_spec=self.argument_spec, @@ -224,10 +379,16 @@ class NetAppOntapVolume(object): ) self.na_helper = NetAppModule() self.parameters = self.na_helper.set_parameters(self.module.params) + self.volume_style = None if self.parameters.get('size'): self.parameters['size'] = self.parameters['size'] * \ self._size_unit_map[self.parameters['size_unit']] + # ONTAP will return True and False as the string true and false. + if 'snapdir_access' in self.parameters: + self.parameters['snapdir_access'] = str(self.parameters['snapdir_access']).lower() + if 'atime_update' in self.parameters: + self.parameters['atime_update'] = str(self.parameters['atime_update']).lower() if HAS_NETAPP_LIB is False: self.module.fail_json( msg="the python NetApp-Lib module is required") @@ -281,6 +442,7 @@ class NetAppOntapVolume(object): volume_export_attributes = volume_attributes['volume-export-attributes'] volume_security_unix_attributes = volume_attributes['volume-security-attributes']['volume-security-unix-attributes'] volume_snapshot_attributes = volume_attributes['volume-snapshot-attributes'] + volume_performance_attributes = volume_attributes['volume-performance-attributes'] # Get volume's state (online/offline) current_state = volume_state_attributes['state'] is_online = (current_state == "online") @@ -290,11 +452,12 @@ class NetAppOntapVolume(object): 'size': int(volume_space_attributes['size']), 'is_online': is_online, 'policy': volume_export_attributes['policy'], - 'space_guarantee': volume_space_attributes['space-guarantee'], 'unix_permissions': volume_security_unix_attributes['permissions'], 'snapshot_policy': volume_snapshot_attributes['snapshot-policy'] } + if volume_space_attributes.get_child_by_name('percentage-snapshot-reserve'): + return_value['percent_snapshot_space'] = volume_space_attributes['percentage-snapshot-reserve'] if volume_id_attributes.get_child_by_name('containing-aggregate-name'): return_value['aggregate_name'] = volume_id_attributes['containing-aggregate-name'] else: @@ -303,58 +466,132 @@ class NetAppOntapVolume(object): return_value['junction_path'] = volume_id_attributes['junction-path'] else: return_value['junction_path'] = '' + if volume_id_attributes.get_child_by_name('style-extended'): + return_value['style_extended'] = volume_id_attributes['style-extended'] + else: + return_value['style_extended'] = None + if volume_space_attributes.get_child_by_name('space-guarantee'): + return_value['space_guarantee'] = volume_space_attributes['space-guarantee'] + else: + return_value['space_guarantee'] = None + if volume_snapshot_attributes.get_child_by_name('snapdir-access-enabled'): + return_value['snapdir_access'] = volume_snapshot_attributes['snapdir-access-enabled'] + else: + return_value['snapdir_access'] = None + if volume_performance_attributes.get_child_by_name('is-atime-update-enabled'): + return_value['atime_update'] = volume_performance_attributes['is-atime-update-enabled'] + else: + return_value['atime_update'] = None return return_value def create_volume(self): '''Create ONTAP volume''' - if self.parameters.get('aggregate_name') is None: - self.module.fail_json(msg='Error provisioning volume %s: aggregate_name is required' - % self.parameters['name']) - options = {'volume': self.parameters['name'], - 'containing-aggr-name': self.parameters['aggregate_name'], - 'size': str(self.parameters['size'])} - if self.parameters.get('percent_snapshot_space'): - options['percentage-snapshot-reserve'] = self.parameters['percent_snapshot_space'] - if self.parameters.get('type'): - options['volume-type'] = self.parameters['type'] - if self.parameters.get('policy'): - options['export-policy'] = self.parameters['policy'] - if self.parameters.get('junction_path'): - options['junction-path'] = self.parameters['junction_path'] - if self.parameters.get('space_guarantee'): - options['space-reserve'] = self.parameters['space_guarantee'] - if self.parameters.get('volume_security_style'): - options['volume-security-style'] = self.parameters['volume_security_style'] - if self.parameters.get('unix_permissions'): - options['unix-permissions'] = self.parameters['unix_permissions'] - if self.parameters.get('snapshot_policy'): - options['snapshot-policy'] = self.parameters['snapshot_policy'] - volume_create = netapp_utils.zapi.NaElement.create_node_with_children('volume-create', **options) + if self.volume_style == 'flexGroup': + self.create_volume_async() + else: + options = self.create_volume_options() + volume_create = netapp_utils.zapi.NaElement.create_node_with_children('volume-create', **options) + try: + self.server.invoke_successfully(volume_create, enable_tunneling=True) + if self.parameters.get('wait_for_completion'): + # round off time_out + retries = (self.parameters['time_out'] + 5) // 10 + current = self.get_volume() + is_online = None if current is None else current['is_online'] + while not is_online and retries > 0: + time.sleep(10) + retries = retries - 1 + current = self.get_volume() + is_online = None if current is None else current['is_online'] + self.ems_log_event("volume-create") + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error provisioning volume %s of size %s: %s' + % (self.parameters['name'], self.parameters['size'], to_native(error)), + exception=traceback.format_exc()) + + if self.parameters.get('efficiency_policy'): + self.assign_efficiency_policy() + + def create_volume_async(self): + ''' + create volume async. + ''' + options = self.create_volume_options() + volume_create = netapp_utils.zapi.NaElement.create_node_with_children('volume-create-async', **options) + if self.parameters.get('aggr_list'): + aggr_list_obj = netapp_utils.zapi.NaElement('aggr-list') + volume_create.add_child_elem(aggr_list_obj) + for aggr in self.parameters['aggr_list']: + aggr_list_obj.add_new_child('aggr-name', aggr) try: - self.server.invoke_successfully(volume_create, - enable_tunneling=True) + result = self.server.invoke_successfully(volume_create, enable_tunneling=True) self.ems_log_event("volume-create") except netapp_utils.zapi.NaApiError as error: - self.module.fail_json(msg='Error provisioning volume %s \ - of size %s: %s' + self.module.fail_json(msg='Error provisioning volume %s of size %s: %s' % (self.parameters['name'], self.parameters['size'], to_native(error)), exception=traceback.format_exc()) + self.check_invoke_result(result, 'create') + + if self.parameters.get('efficiency_policy'): + self.assign_efficiency_policy_async() + + def create_volume_options(self): + options = {} + if self.volume_style == 'flexGroup': + options['volume-name'] = self.parameters['name'] + if self.parameters.get('aggr_list_multiplier'): + options['aggr-list-multiplier'] = str(self.parameters['aggr_list_multiplier']) + if self.parameters.get('auto_provision_as'): + options['auto-provision-as'] = self.parameters['auto_provision_as'] + if self.parameters.get('space_guarantee'): + options['space-guarantee'] = self.parameters['space_guarantee'] + if self.parameters.get('size'): + options['size'] = str(self.parameters['size']) + else: + options['volume'] = self.parameters['name'] + options['size'] = str(self.parameters['size']) + if self.parameters.get('aggregate_name') is None: + self.module.fail_json(msg='Error provisioning volume %s: aggregate_name is required' + % self.parameters['name']) + options['containing-aggr-name'] = self.parameters['aggregate_name'] + if self.parameters.get('space_guarantee'): + options['space-reserve'] = self.parameters['space_guarantee'] + + if self.parameters.get('snapshot_policy'): + options['snapshot-policy'] = self.parameters['snapshot_policy'] + if self.parameters.get('unix_permissions'): + options['unix-permissions'] = self.parameters['unix_permissions'] + if self.parameters.get('volume_security_style'): + options['volume-security-style'] = self.parameters['volume_security_style'] + if self.parameters.get('policy'): + options['export-policy'] = self.parameters['policy'] + if self.parameters.get('junction_path'): + options['junction-path'] = self.parameters['junction_path'] + if self.parameters.get('type'): + options['volume-type'] = self.parameters['type'] + if self.parameters.get('percent_snapshot_space'): + options['percentage-snapshot-reserve'] = self.parameters['percent_snapshot_space'] + if self.parameters.get('language'): + options['language-code'] = self.parameters['language'] + return options def delete_volume(self): '''Delete ONTAP volume''' - if self.parameters.get('is_infinite'): + if self.parameters.get('is_infinite') or self.volume_style == 'flexGroup': volume_delete = netapp_utils.zapi\ .NaElement.create_node_with_children( - 'volume-destroy-async', **{'volume-name': self.parameters['name']}) + 'volume-destroy-async', **{'volume-name': self.parameters['name'], 'unmount-and-offline': 'true'}) else: volume_delete = netapp_utils.zapi\ .NaElement.create_node_with_children( 'volume-destroy', **{'name': self.parameters['name'], 'unmount-and-offline': 'true'}) try: - self.server.invoke_successfully(volume_delete, enable_tunneling=True) - self.ems_log_event("delete") + result = self.server.invoke_successfully(volume_delete, enable_tunneling=True) + if self.parameters.get('is_infinite') or self.volume_style == 'flexGroup': + self.check_invoke_result(result, 'delete') + self.ems_log_event("volume-delete") except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error deleting volume %s: %s' % (self.parameters['name'], to_native(error)), @@ -380,7 +617,7 @@ class NetAppOntapVolume(object): Rename the volume. Note: 'is_infinite' needs to be set to True in order to rename an - Infinite Volume. + Infinite Volume. Use time_out parameter to set wait time for rename completion. """ vol_rename_zapi, vol_name_zapi = ['volume-rename-async', 'volume-name'] if self.parameters['is_infinite']\ else ['volume-rename', 'volume'] @@ -388,8 +625,9 @@ class NetAppOntapVolume(object): vol_rename_zapi, **{vol_name_zapi: self.parameters['from_name'], 'new-volume-name': str(self.parameters['name'])}) try: - self.server.invoke_successfully(volume_rename, - enable_tunneling=True) + result = self.server.invoke_successfully(volume_rename, enable_tunneling=True) + if vol_rename_zapi == 'volume-rename-async': + self.check_invoke_result(result, 'rename') self.ems_log_event("volume-rename") except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error renaming volume %s: %s' @@ -403,13 +641,16 @@ class NetAppOntapVolume(object): Note: 'is_infinite' needs to be set to True in order to rename an Infinite Volume. """ - vol_size_zapi, vol_name_zapi = ['volume-size-async', 'volume-name'] if self.parameters['is_infinite']\ + vol_size_zapi, vol_name_zapi = ['volume-size-async', 'volume-name']\ + if (self.parameters['is_infinite'] or self.volume_style == 'flexGroup')\ else ['volume-size', 'volume'] volume_resize = netapp_utils.zapi.NaElement.create_node_with_children( vol_size_zapi, **{vol_name_zapi: self.parameters['name'], 'new-size': str(self.parameters['size'])}) try: - self.server.invoke_successfully(volume_resize, enable_tunneling=True) + result = self.server.invoke_successfully(volume_resize, enable_tunneling=True) + if vol_size_zapi == 'volume-size-async': + self.check_invoke_result(result, 'resize') self.ems_log_event("volume-resize") except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error re-sizing volume %s: %s' @@ -421,11 +662,13 @@ class NetAppOntapVolume(object): Change volume's state (offline/online). """ if self.parameters['is_online']: # Desired state is online, setup zapi APIs respectively - vol_state_zapi, vol_name_zapi = ['volume-online-async', 'volume-name'] if self.parameters['is_infinite']\ - else ['volume-online', 'name'] + vol_state_zapi, vol_name_zapi, action = ['volume-online-async', 'volume-name', 'online']\ + if (self.parameters['is_infinite'] or self.volume_style == 'flexGroup')\ + else ['volume-online', 'name', 'online'] else: # Desired state is offline, setup zapi APIs respectively - vol_state_zapi, vol_name_zapi = ['volume-offline-async', 'volume-name'] if self.parameters['is_infinite']\ - else ['volume-offline', 'name'] + vol_state_zapi, vol_name_zapi, action = ['volume-offline-async', 'volume-name', 'offline']\ + if (self.parameters['is_infinite'] or self.volume_style == 'flexGroup')\ + else ['volume-offline', 'name', 'offline'] volume_unmount = netapp_utils.zapi.NaElement.create_node_with_children( 'volume-unmount', **{'volume-name': self.parameters['name']}) volume_change_state = netapp_utils.zapi.NaElement.create_node_with_children( @@ -433,7 +676,9 @@ class NetAppOntapVolume(object): try: if not self.parameters['is_online']: # Unmount before offline self.server.invoke_successfully(volume_unmount, enable_tunneling=True) - self.server.invoke_successfully(volume_change_state, enable_tunneling=True) + result = self.server.invoke_successfully(volume_change_state, enable_tunneling=True) + if self.volume_style == 'flexGroup' or self.parameters['is_infinite']: + self.check_invoke_result(result, action) self.ems_log_event("change-state") except netapp_utils.zapi.NaApiError as error: state = "online" if self.parameters['is_online'] else "offline" @@ -443,23 +688,30 @@ class NetAppOntapVolume(object): def volume_modify_attributes(self): """ - modify volume parameter 'policy' or 'space_guarantee' + modify volume parameter 'policy','unix_permissions','snapshot_policy','space_guarantee','percent_snapshot_space' """ # TODO: refactor this method - vol_mod_iter = netapp_utils.zapi.NaElement('volume-modify-iter') + vol_mod_iter = None + if self.volume_style == 'flexGroup' or self.parameters['is_infinite']: + vol_mod_iter = netapp_utils.zapi.NaElement('volume-modify-iter-async') + else: + vol_mod_iter = netapp_utils.zapi.NaElement('volume-modify-iter') attributes = netapp_utils.zapi.NaElement('attributes') vol_mod_attributes = netapp_utils.zapi.NaElement('volume-attributes') + vol_space_attributes = netapp_utils.zapi.NaElement('volume-space-attributes') if self.parameters.get('policy'): vol_export_attributes = netapp_utils.zapi.NaElement( 'volume-export-attributes') vol_export_attributes.add_new_child('policy', self.parameters['policy']) vol_mod_attributes.add_child_elem(vol_export_attributes) if self.parameters.get('space_guarantee'): - vol_space_attributes = netapp_utils.zapi.NaElement( - 'volume-space-attributes') vol_space_attributes.add_new_child( 'space-guarantee', self.parameters['space_guarantee']) vol_mod_attributes.add_child_elem(vol_space_attributes) + if self.parameters.get('percent_snapshot_space'): + vol_space_attributes.add_new_child( + 'percentage-snapshot-reserve', str(self.parameters['percent_snapshot_space'])) + vol_mod_attributes.add_child_elem(vol_space_attributes) if self.parameters.get('unix_permissions'): vol_unix_permissions_attributes = netapp_utils.zapi.NaElement( 'volume-security-unix-attributes') @@ -468,11 +720,17 @@ class NetAppOntapVolume(object): 'volume-security-attributes') vol_security_attributes.add_child_elem(vol_unix_permissions_attributes) vol_mod_attributes.add_child_elem(vol_security_attributes) - if self.parameters.get('snapshot_policy'): - vol_snapshot_policy_attributes = netapp_utils.zapi.NaElement( - 'volume-snapshot-attributes') - vol_snapshot_policy_attributes.add_new_child('snapshot-policy', self.parameters['snapshot_policy']) + if self.parameters.get('snapshot_policy') or self.parameters.get('snapdir_access'): + vol_snapshot_policy_attributes = netapp_utils.zapi.NaElement('volume-snapshot-attributes') + if self.parameters.get('snapshot_policy'): + vol_snapshot_policy_attributes.add_new_child('snapshot-policy', self.parameters['snapshot_policy']) + if self.parameters.get('snapdir_access'): + vol_snapshot_policy_attributes.add_new_child('snapdir-access-enabled', self.parameters.get('snapdir_access')) vol_mod_attributes.add_child_elem(vol_snapshot_policy_attributes) + if self.parameters.get('atime_update'): + vol_performance_attributes = netapp_utils.zapi.NaElement('volume-performance-attributes') + vol_performance_attributes.add_new_child('is-atime-update-enabled', self.parameters.get('atime_update')) + vol_mod_attributes.add_child_elem(vol_performance_attributes) attributes.add_child_elem(vol_mod_attributes) query = netapp_utils.zapi.NaElement('query') vol_query_attributes = netapp_utils.zapi.NaElement('volume-attributes') @@ -485,12 +743,37 @@ class NetAppOntapVolume(object): try: result = self.server.invoke_successfully(vol_mod_iter, enable_tunneling=True) failures = result.get_child_by_name('failure-list') + if self.volume_style == 'flexGroup' or self.parameters['is_infinite']: + success = result.get_child_by_name('success-list') + success = success.get_child_by_name('volume-modify-iter-async-info') + results = dict() + for key in ('status', 'jobid'): + if success.get_child_by_name(key): + results[key] = success[key] + status = results.get('status') + if status == 'in_progress' and 'jobid' in results: + if self.parameters['time_out'] == 0: + return + error = self.check_job_status(results['jobid']) + if error is None: + return + else: + self.module.fail_json(msg='Error when modify volume: %s' % error) + self.module.fail_json(msg='Unexpected error when modify volume: results is: %s' % repr(results)) # handle error if modify space, policy, or unix-permissions parameter fails - if failures is not None and failures.get_child_by_name('volume-modify-iter-info') is not None: - error_msg = failures.get_child_by_name('volume-modify-iter-info').get_child_content('error-message') - self.module.fail_json(msg="Error modifying volume %s: %s" - % (self.parameters['name'], error_msg), - exception=traceback.format_exc()) + if failures is not None: + if failures.get_child_by_name('volume-modify-iter-info') is not None: + return_info = 'volume-modify-iter-info' + error_msg = failures.get_child_by_name(return_info).get_child_content('error-message') + self.module.fail_json(msg="Error modifying volume %s: %s" + % (self.parameters['name'], error_msg), + exception=traceback.format_exc()) + elif failures.get_child_by_name('volume-modify-iter-async-info') is not None: + return_info = 'volume-modify-iter-async-info' + error_msg = failures.get_child_by_name(return_info).get_child_content('error-message') + self.module.fail_json(msg="Error modifying volume %s: %s" + % (self.parameters['name'], error_msg), + exception=traceback.format_exc()) self.ems_log_event("volume-modify") except netapp_utils.zapi.NaApiError as error: self.module.fail_json(msg='Error modifying volume %s: %s' @@ -533,7 +816,8 @@ class NetAppOntapVolume(object): self.change_volume_state() if attribute == 'aggregate_name': self.move_volume() - if attribute in ['space_guarantee', 'policy', 'unix_permissions', 'snapshot_policy']: + if attribute in ['space_guarantee', 'policy', 'unix_permissions', + 'snapshot_policy', 'percent_snapshot_space', 'snapdir_access', 'atime_update']: self.volume_modify_attributes() if attribute == 'junction_path': if modify.get('junction_path') == '': @@ -580,9 +864,150 @@ class NetAppOntapVolume(object): total += 1 return total + def get_volume_style(self, current): + if current is None: + if self.parameters.get('aggr_list') or self.parameters.get('aggr_list_multiplier') or self.parameters.get('auto_provision_as'): + return 'flexGroup' + else: + if current.get('style_extended'): + if current['style_extended'] == 'flexgroup': + return 'flexGroup' + else: + return current['style_extended'] + return None + + def get_job(self, jobid, server): + """ + Get job details by id + """ + job_get = netapp_utils.zapi.NaElement('job-get') + job_get.add_new_child('job-id', jobid) + try: + result = server.invoke_successfully(job_get, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + if to_native(error.code) == "15661": + # Not found + return None + self.module.fail_json(msg='Error fetching job info: %s' % to_native(error), + exception=traceback.format_exc()) + results = dict() + job_info = result.get_child_by_name('attributes').get_child_by_name('job-info') + results = { + 'job-progress': job_info['job-progress'], + 'job-state': job_info['job-state'] + } + if job_info.get_child_by_name('job-completion') is not None: + results['job-completion'] = job_info['job-completion'] + else: + results['job-completion'] = None + return results + + def check_job_status(self, jobid): + """ + Loop until job is complete + """ + server = self.server + sleep_time = 5 + time_out = self.parameters['time_out'] + results = self.get_job(jobid, server) + + while time_out > 0: + results = self.get_job(jobid, server) + # If running as cluster admin, the job is owned by cluster vserver + # rather than the target vserver. + if results is None and server == self.server: + results = netapp_utils.get_cserver(self.server) + server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=results) + continue + if results is None: + error = 'cannot locate job with id: %d' % int(jobid) + break + if results['job-state'] in ('queued', 'running'): + time.sleep(sleep_time) + time_out -= sleep_time + continue + if results['job-state'] in ('success', 'failure'): + break + else: + self.module.fail_json(msg='Unexpected job status in: %s' % repr(results)) + + if results is not None: + if results['job-state'] == 'success': + error = None + elif results['job-state'] in ('queued', 'running'): + error = 'job completion exceeded expected timer of: %s seconds' % \ + self.parameters['time_out'] + else: + if results['job-completion'] is not None: + error = results['job-completion'] + else: + error = results['job-progress'] + return error + + def check_invoke_result(self, result, action): + ''' + check invoked api call back result. + ''' + results = dict() + for key in ('result-status', 'result-jobid'): + if result.get_child_by_name(key): + results[key] = result[key] + status = results.get('result-status') + if status == 'in_progress' and 'result-jobid' in results: + if self.parameters['time_out'] == 0: + return + error = self.check_job_status(results['result-jobid']) + if error is None: + return + else: + self.module.fail_json(msg='Error when %s volume: %s' % (action, error)) + if status == 'failed': + self.module.fail_json(msg='Operation failed when %s volume.' % action) + + def assign_efficiency_policy(self): + options = {'path': '/vol/' + self.parameters['name']} + efficiency_enable = netapp_utils.zapi.NaElement.create_node_with_children('sis-enable', **options) + try: + self.server.invoke_successfully(efficiency_enable, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error enable efficiency on volume %s: %s' + % (self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) + + options['policy-name'] = self.parameters['efficiency_policy'] + efficiency_start = netapp_utils.zapi.NaElement.create_node_with_children('sis-set-config', **options) + try: + self.server.invoke_successfully(efficiency_start, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error setting up an efficiency policy %s on volume %s: %s' + % (self.parameters['efficiency_policy'], self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) + + def assign_efficiency_policy_async(self): + options = {'volume-name': self.parameters['name']} + efficiency_enable = netapp_utils.zapi.NaElement.create_node_with_children('sis-enable-async', **options) + try: + result = self.server.invoke_successfully(efficiency_enable, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error enable efficiency on volume %s: %s' + % (self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) + self.check_invoke_result(result, 'enable efficiency on') + + options['policy-name'] = self.parameters['efficiency_policy'] + efficiency_start = netapp_utils.zapi.NaElement.create_node_with_children('sis-set-config-async', **options) + try: + result = self.server.invoke_successfully(efficiency_start, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error setting up an efficiency policy on volume %s: %s' + % (self.parameters['name'], to_native(error)), + exception=traceback.format_exc()) + self.check_invoke_result(result, 'set efficiency policy on') + def apply(self): '''Call create/modify/delete operations''' current = self.get_volume() + self.volume_style = self.get_volume_style(current) # rename and create are mutually exclusive rename, cd_action = None, None if self.parameters.get('from_name'): @@ -594,6 +1019,8 @@ class NetAppOntapVolume(object): # unix_permission in self.parameter can be either numeric or character. if self.compare_chmod_value(current): del self.parameters['unix_permissions'] + if self.parameters.get('percent_snapshot_space'): + self.parameters['percent_snapshot_space'] = str(self.parameters['percent_snapshot_space']) modify = self.na_helper.get_modified_attributes(current, self.parameters) if self.na_helper.changed: if self.module.check_mode: @@ -603,6 +1030,9 @@ class NetAppOntapVolume(object): self.rename_volume() if cd_action == 'create': self.create_volume() + # if we create, and modify only variable are set (snapdir_access or atime_update) we need to run a modify + if 'snapdir_access' in self.parameters or 'atime_update' in self.parameters: + self.volume_modify_attributes() elif cd_action == 'delete': self.delete_volume() elif modify: @@ -614,16 +1044,16 @@ class NetAppOntapVolume(object): if state == 'create': message = "A Volume has been created, size: " + \ str(self.parameters['size']) + str(self.parameters['size_unit']) - elif state == 'delete': + elif state == 'volume-delete': message = "A Volume has been deleted" - elif state == 'move': + elif state == 'volume-move': message = "A Volume has been moved" - elif state == 'rename': + elif state == 'volume-rename': message = "A Volume has been renamed" - elif state == 'resize': + elif state == 'volume-resize': message = "A Volume has been resized to: " + \ str(self.parameters['size']) + str(self.parameters['size_unit']) - elif state == 'change': + elif state == 'volume-change': message = "A Volume state has been changed" else: message = "na_ontap_volume has been called" diff --git a/test/units/modules/storage/netapp/test_na_ontap_volume.py b/test/units/modules/storage/netapp/test_na_ontap_volume.py index 03356b17e4c..ea1e00b2f20 100644 --- a/test/units/modules/storage/netapp/test_na_ontap_volume.py +++ b/test/units/modules/storage/netapp/test_na_ontap_volume.py @@ -52,18 +52,30 @@ def fail_json(*args, **kwargs): # pylint: disable=unused-argument class MockONTAPConnection(object): ''' mock server connection to ONTAP host ''' - def __init__(self, kind=None, data=None): + def __init__(self, kind=None, data=None, job_error=None): ''' save arguments ''' self.kind = kind self.params = data self.xml_in = None self.xml_out = None + self.job_error = job_error def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument ''' mock invoke_successfully returning xml data ''' self.xml_in = xml if self.kind == 'volume': xml = self.build_volume_info(self.params) + elif self.kind == 'job_info': + xml = self.build_job_info(self.job_error) + elif self.kind == 'error_modify': + xml = self.build_modify_error() + elif self.kind == 'failure_modify_async': + xml = self.build_failure_modify_async() + elif self.kind == 'success_modify_async': + xml = self.build_success_modify_async() + elif self.kind == 'zapi_error': + error = netapp_utils.zapi.NaApiError('test', 'error') + raise error self.xml_out = xml return xml @@ -77,11 +89,62 @@ class MockONTAPConnection(object): 'volume-attributes': { 'volume-id-attributes': { 'containing-aggregate-name': vol_details['aggregate'], - 'junction-path': vol_details['junction_path'] + 'junction-path': vol_details['junction_path'], + 'style-extended': 'flexvol' + }, + 'volume-language-attributes': { + 'language-code': 'en' + }, + 'volume-export-attributes': { + 'policy': 'default' + }, + 'volume-performance-attributes': { + 'is-atime-update-enabled': 'true' + }, + 'volume-state-attributes': { + 'state': "online" + }, + 'volume-space-attributes': { + 'space-guarantee': 'none', + 'size': vol_details['size'], + 'percentage-snapshot-reserve': vol_details['percent_snapshot_space'] + }, + 'volume-snapshot-attributes': { + 'snapshot-policy': vol_details['snapshot_policy'] + }, + 'volume-security-attributes': { + 'volume-security-unix-attributes': { + 'permissions': vol_details['unix_permissions'] + } + } + } + } + } + xml.translate_struct(attributes) + return xml + + @staticmethod + def build_flex_group_info(vol_details): + ''' build xml data for flexGroup volume-attributes ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'volume-attributes': { + 'volume-id-attributes': { + 'aggr-list': vol_details['aggregate'], + 'junction-path': vol_details['junction_path'], + 'style-extended': 'flexgroup' + }, + 'volume-language-attributes': { + 'language-code': 'en' }, 'volume-export-attributes': { 'policy': 'default' }, + 'volume-performance-attributes': { + 'is-atime-update-enabled': 'true' + }, 'volume-state-attributes': { 'state': "online" }, @@ -103,6 +166,65 @@ class MockONTAPConnection(object): xml.translate_struct(attributes) return xml + @staticmethod + def build_job_info(error): + ''' build xml data for a job ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = netapp_utils.zapi.NaElement('attributes') + if error is None: + state = 'success' + elif error == 'time_out': + state = 'running' + elif error == 'failure': + state = 'failure' + else: + state = 'other' + attributes.add_node_with_children('job-info', **{ + 'job-state': state, + 'job-progress': 'dummy', + 'job-completion': error, + }) + xml.add_child_elem(attributes) + xml.add_new_child('result-status', 'in_progress') + xml.add_new_child('result-jobid', '1234') + return xml + + @staticmethod + def build_modify_error(): + ''' build xml data for modify error ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = netapp_utils.zapi.NaElement('failure-list') + info_list_obj = netapp_utils.zapi.NaElement('volume-modify-iter-info') + info_list_obj.add_new_child('error-message', 'modify error message') + attributes.add_child_elem(info_list_obj) + xml.add_child_elem(attributes) + return xml + + @staticmethod + def build_success_modify_async(): + ''' build xml data for success modify async ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = netapp_utils.zapi.NaElement('success-list') + info_list_obj = netapp_utils.zapi.NaElement('volume-modify-iter-async-info') + info_list_obj.add_new_child('status', 'in_progress') + info_list_obj.add_new_child('jobid', '1234') + attributes.add_child_elem(info_list_obj) + xml.add_child_elem(attributes) + return xml + + @staticmethod + def build_failure_modify_async(): + ''' build xml data for failure modify async ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = netapp_utils.zapi.NaElement('failure-list') + info_list_obj = netapp_utils.zapi.NaElement('volume-modify-iter-async-info') + info_list_obj.add_new_child('status', 'failed') + info_list_obj.add_new_child('jobid', '1234') + info_list_obj.add_new_child('error-message', 'modify error message') + attributes.add_child_elem(info_list_obj) + xml.add_child_elem(attributes) + return xml + class TestMyModule(unittest.TestCase): ''' a group of related Unit Tests ''' @@ -118,40 +240,71 @@ class TestMyModule(unittest.TestCase): 'aggregate': 'test_aggr', 'junction_path': '/test', 'vserver': 'test_vserver', - 'size': 20, + 'size': 20971520, 'unix_permissions': '755', - 'snapshot_policy': 'default' + 'snapshot_policy': 'default', + 'percent_snapshot_space': 60, + 'language': 'en' } - def mock_args(self): - return { + def mock_args(self, tag=None): + args = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', 'name': self.mock_vol['name'], 'vserver': self.mock_vol['vserver'], - 'aggregate_name': self.mock_vol['aggregate'], 'space_guarantee': 'none', 'policy': 'default', + 'language': self.mock_vol['language'], 'is_online': True, - 'hostname': 'test', - 'username': 'test_user', - 'password': 'test_pass!', 'unix_permissions': '---rwxr-xr-x', - 'snapshot_policy': 'default' + 'snapshot_policy': 'default', + 'size': 20, + 'size_unit': 'mb', + 'junction_path': '/test', + 'percent_snapshot_space': 60, + 'type': 'type' } + if tag is None: + args['aggregate_name'] = self.mock_vol['aggregate'] + return args + + elif tag == 'flexGroup_manual': + args['aggr_list'] = 'aggr_0,aggr_1' + args['aggr_list_multiplier'] = 2 + return args + + elif tag == 'flexGroup_auto': + args['auto_provision_as'] = 'flexgroup' + return args - def get_volume_mock_object(self, kind=None): + def get_volume_mock_object(self, kind=None, job_error=None): """ Helper method to return an na_ontap_volume object - :param kind: passes this param to MockONTAPConnection() + :param kind: passes this param to MockONTAPConnection(). + :param job_error: error message when getting job status. :return: na_ontap_volume object """ vol_obj = vol_module() vol_obj.ems_log_event = Mock(return_value=None) vol_obj.cluster = Mock() vol_obj.cluster.invoke_successfully = Mock() + vol_obj.volume_style = None if kind is None: vol_obj.server = MockONTAPConnection() - else: + elif kind == 'volume': vol_obj.server = MockONTAPConnection(kind='volume', data=self.mock_vol) + elif kind == 'job_info': + vol_obj.server = MockONTAPConnection(kind='job_info', data=self.mock_vol, job_error=job_error) + elif kind == 'error_modify': + vol_obj.server = MockONTAPConnection(kind='error_modify', data=self.mock_vol) + elif kind == 'failure_modify_async': + vol_obj.server = MockONTAPConnection(kind='failure_modify_async', data=self.mock_vol) + elif kind == 'success_modify_async': + vol_obj.server = MockONTAPConnection(kind='success_modify_async', data=self.mock_vol) + elif kind == 'zapi_error': + vol_obj.server = MockONTAPConnection(kind='zapi_error', data=self.mock_vol) return vol_obj def test_module_fail_when_required_args_missing(self): @@ -236,6 +389,14 @@ class TestMyModule(unittest.TestCase): self.get_volume_mock_object('volume').apply() assert not exc.value.args[0]['changed'] + def test_modify_error(self): + ''' Test modify idempotency ''' + data = self.mock_args() + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_volume_mock_object('error_modify').volume_modify_attributes() + assert exc.value.args[0]['msg'] == 'Error modifying volume test_vol: modify error message' + def test_mount_volume(self): ''' Test mount volume ''' data = self.mock_args() @@ -263,28 +424,28 @@ class TestMyModule(unittest.TestCase): self.get_volume_mock_object('volume').apply() assert exc.value.args[0]['changed'] - def test_successful_modify_state(self): - ''' Test successful modify state ''' + def test_successful_modify_unix_permissions(self): + ''' Test successful modify unix_permissions ''' data = self.mock_args() - data['is_online'] = False + data['unix_permissions'] = '---rw-r-xr-x' set_module_args(data) with pytest.raises(AnsibleExitJson) as exc: self.get_volume_mock_object('volume').apply() assert exc.value.args[0]['changed'] - def test_successful_modify_unix_permissions(self): - ''' Test successful modify unix_permissions ''' + def test_successful_modify_snapshot_policy(self): + ''' Test successful modify snapshot_policy ''' data = self.mock_args() - data['unix_permissions'] = '---rw-r-xr-x' + data['snapshot_policy'] = 'default-1weekly' set_module_args(data) with pytest.raises(AnsibleExitJson) as exc: self.get_volume_mock_object('volume').apply() assert exc.value.args[0]['changed'] - def test_successful_modify_snapshot_policy(self): + def test_successful_modify_percent_snapshot_space(self): ''' Test successful modify snapshot_policy ''' data = self.mock_args() - data['snapshot_policy'] = 'default-1weekly' + data['percent_snapshot_space'] = '90' set_module_args(data) with pytest.raises(AnsibleExitJson) as exc: self.get_volume_mock_object('volume').apply() @@ -299,6 +460,52 @@ class TestMyModule(unittest.TestCase): self.get_volume_mock_object('volume').apply() assert exc.value.args[0]['changed'] + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_successful_rename(self, get_volume): + ''' Test successful rename volume ''' + data = self.mock_args() + data['from_name'] = self.mock_vol['name'] + data['name'] = 'new_name' + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + } + get_volume.side_effect = [ + None, + current + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_volume_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_successful_rename_async(self, get_volume): + ''' Test successful rename volume ''' + data = self.mock_args() + data['from_name'] = self.mock_vol['name'] + data['name'] = 'new_name' + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'is_infinite': True + } + get_volume.side_effect = [ + None, + current + ] + obj = self.get_volume_mock_object('job_info') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.change_volume_state') @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.volume_mount') def test_modify_helper(self, mount_volume, change_state): @@ -436,3 +643,268 @@ class TestMyModule(unittest.TestCase): ] obj = self.get_volume_mock_object() assert not obj.compare_chmod_value(current) + + def test_successful_create_flex_group_manually(self): + ''' Test successful create flexGroup manually ''' + data = self.mock_args('flexGroup_manual') + data['time_out'] = 20 + set_module_args(data) + obj = self.get_volume_mock_object('job_info') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + def test_successful_create_flex_group_auto_provision(self): + ''' Test successful create flexGroup auto provision ''' + data = self.mock_args('flexGroup_auto') + data['time_out'] = 20 + set_module_args(data) + obj = self.get_volume_mock_object('job_info') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_successful_delete_flex_group(self, get_volume): + ''' Test successful delete felxGroup ''' + data = self.mock_args('flexGroup_manual') + data['state'] = 'absent' + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '755' + } + get_volume.side_effect = [ + current + ] + obj = self.get_volume_mock_object('job_info') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_successful_resize_flex_group(self, get_volume): + ''' Test successful reszie flexGroup ''' + data = self.mock_args('flexGroup_manual') + data['size'] = 400 + data['size_unit'] = 'mb' + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'style_extended': 'flexgroup', + 'size': 20971520, + 'unix_permissions': '755' + } + get_volume.side_effect = [ + current + ] + obj = self.get_volume_mock_object('job_info') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.check_job_status') + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_successful_modify_unix_permissions_flex_group(self, get_volume, check_job_status): + ''' Test successful modify unix permissions flexGroup ''' + data = self.mock_args('flexGroup_manual') + data['time_out'] = 20 + data['unix_permissions'] = '---rw-r-xr-x' + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '777' + } + get_volume.side_effect = [ + current + ] + check_job_status.side_effect = [ + None + ] + obj = self.get_volume_mock_object('success_modify_async') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_successful_modify_unix_permissions_flex_group_0_time_out(self, get_volume): + ''' Test successful modify unix permissions flexGroup ''' + data = self.mock_args('flexGroup_manual') + data['time_out'] = 0 + data['unix_permissions'] = '---rw-r-xr-x' + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '777' + } + get_volume.side_effect = [ + current + ] + obj = self.get_volume_mock_object('success_modify_async') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.check_job_status') + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_error_modify_unix_permissions_flex_group(self, get_volume, check_job_status): + ''' Test error modify unix permissions flexGroup ''' + data = self.mock_args('flexGroup_manual') + data['time_out'] = 20 + data['unix_permissions'] = '---rw-r-xr-x' + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '777' + } + get_volume.side_effect = [ + current + ] + check_job_status.side_effect = ['error'] + obj = self.get_volume_mock_object('success_modify_async') + with pytest.raises(AnsibleFailJson) as exc: + obj.apply() + assert exc.value.args[0]['msg'] == 'Error when modify volume: error' + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_failure_modify_unix_permissions_flex_group(self, get_volume): + ''' Test failure modify unix permissions flexGroup ''' + data = self.mock_args('flexGroup_manual') + data['unix_permissions'] = '---rw-r-xr-x' + data['time_out'] = 20 + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'style_extended': 'flexvol', + 'unix_permissions': '777' + } + get_volume.side_effect = [ + current + ] + obj = self.get_volume_mock_object('failure_modify_async') + with pytest.raises(AnsibleFailJson) as exc: + obj.apply() + assert exc.value.args[0]['msg'] == 'Error modifying volume test_vol: modify error message' + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_successful_offline_state_flex_group(self, get_volume): + ''' Test successful offline flexGroup state ''' + data = self.mock_args('flexGroup_manual') + data['is_online'] = False + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'style_extended': 'flexgroup', + 'is_online': True, + 'junction_path': 'anything', + 'unix_permissions': '755' + } + get_volume.side_effect = [ + current + ] + obj = self.get_volume_mock_object('job_info') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible.modules.storage.netapp.na_ontap_volume.NetAppOntapVolume.get_volume') + def test_successful_online_state_flex_group(self, get_volume): + ''' Test successful online flexGroup state ''' + data = self.mock_args('flexGroup_manual') + set_module_args(data) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': self.mock_vol['name'], + 'vserver': self.mock_vol['vserver'], + 'style_extended': 'flexgroup', + 'is_online': False, + 'junction_path': 'anything', + 'unix_permissions': '755' + } + get_volume.side_effect = [ + current + ] + obj = self.get_volume_mock_object('job_info') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + def test_check_job_status_error(self): + ''' Test check job status error ''' + data = self.mock_args('flexGroup_manual') + data['time_out'] = 0 + set_module_args(data) + obj = self.get_volume_mock_object('job_info', job_error='failure') + result = obj.check_job_status('123') + assert result == 'failure' + + def test_check_job_status_time_out_is_0(self): + ''' Test check job status time out is 0''' + data = self.mock_args('flexGroup_manual') + data['time_out'] = 0 + set_module_args(data) + obj = self.get_volume_mock_object('job_info', job_error='time_out') + result = obj.check_job_status('123') + assert result == 'job completion exceeded expected timer of: 0 seconds' + + def test_check_job_status_unexpected(self): + ''' Test check job status unexpected state ''' + data = self.mock_args('flexGroup_manual') + data['time_out'] = 20 + set_module_args(data) + obj = self.get_volume_mock_object('job_info', job_error='other') + with pytest.raises(AnsibleFailJson) as exc: + obj.check_job_status('123') + assert exc.value.args[0]['failed'] + + def test_error_assign_efficiency_policy(self): + data = self.mock_args() + data['efficiency_policy'] = 'test_policy' + set_module_args(data) + obj = self.get_volume_mock_object('zapi_error') + with pytest.raises(AnsibleFailJson) as exc: + obj.assign_efficiency_policy() + assert exc.value.args[0]['msg'] == 'Error enable efficiency on volume test_vol: NetApp API failed. Reason - test:error' + + def test_error_assign_efficiency_policy_async(self): + data = self.mock_args() + data['efficiency_policy'] = 'test_policy' + set_module_args(data) + obj = self.get_volume_mock_object('zapi_error') + with pytest.raises(AnsibleFailJson) as exc: + obj.assign_efficiency_policy_async() + assert exc.value.args[0]['msg'] == 'Error enable efficiency on volume test_vol: NetApp API failed. Reason - test:error'