diff --git a/lib/ansible/modules/storage/netapp/na_ontap_flexcache.py b/lib/ansible/modules/storage/netapp/na_ontap_flexcache.py new file mode 100644 index 0000000000..35323ba29f --- /dev/null +++ b/lib/ansible/modules/storage/netapp/na_ontap_flexcache.py @@ -0,0 +1,475 @@ +#!/usr/bin/python + +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'certified'} + + +DOCUMENTATION = ''' +short_description: NetApp ONTAP FlexCache - create/delete relationship +author: NetApp Ansible Team (@carchi8py) +description: + - Create/Delete FlexCache volume relationships +extends_documentation_fragment: + - netapp.na_ontap +module: na_ontap_flexcache +options: + state: + choices: ['present', 'absent'] + description: + - Whether the specified relationship should exist or not. + default: present + origin_volume: + description: + - Name of the origin volume for the FlexCache. + - Required for creation. + origin_vserver: + description: + - Name of the origin vserver for the FlexCache. + - Required for creation. + origin_cluster: + description: + - Name of the origin cluster for the FlexCache. + - Defaults to cluster associated with target vserver if absent. + - Not used for creation. + volume: + description: + - Name of the target volume for the FlexCache. + required: true + junction_path: + description: + - Junction path of the cache volume. + auto_provision_as: + description: + - Use this parameter to automatically select existing aggregates for volume provisioning.Eg flexgroup + - Note that the fastest aggregate type with at least one aggregate on each node of the cluster will be selected. + size: + description: + - Size of cache volume. + size_unit: + description: + - The unit used to interpret the size parameter. + choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'] + default: gb + vserver: + description: + - Name of the target vserver for the FlexCache. + - Note that hostname, username, password are intended for the target vserver. + required: true + aggr_list: + description: + - List of aggregates to host target FlexCache volume. + aggr_list_multiplier: + description: + - Aggregate list repeat count. + force_unmount: + description: + - Unmount FlexCache volume. Delete the junction path at which the volume is mounted before deleting the FlexCache relationship. + type: bool + default: false + force_offline: + description: + - Offline FlexCache volume before deleting the FlexCache relationship. + - The volume will be destroyed and data can be lost. + type: bool + default: false + time_out: + description: + - time to wait for flexcache creation or deletion in seconds + - if 0, the request is asynchronous + - default is set to 3 minutes + default: 180 +version_added: "2.8" +''' + +EXAMPLES = """ + + - name: Create FlexCache + na_ontap_FlexCache: + state: present + origin_volume: test_src + volume: test_dest + origin_vserver: ansible_src + vserver: ansible_dest + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + + - name: Delete FlexCache + na_ontap_FlexCache: + state: absent + volume: test_dest + vserver: ansible_dest + hostname: "{{ netapp_hostname }}" + username: "{{ netapp_username }}" + password: "{{ netapp_password }}" + +""" + +RETURN = """ +""" + +import time +import traceback +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +import ansible.module_utils.netapp as netapp_utils +from ansible.module_utils.netapp_module import NetAppModule + +HAS_NETAPP_LIB = netapp_utils.has_netapp_lib() + + +class NetAppONTAPFlexCache(object): + """ + Class with FlexCache methods + """ + + def __init__(self): + + self.argument_spec = netapp_utils.na_ontap_host_argument_spec() + self.argument_spec.update(dict( + state=dict(required=False, type='str', choices=['present', 'absent'], + default='present'), + origin_volume=dict(required=False, type='str'), + origin_vserver=dict(required=False, type='str'), + origin_cluster=dict(required=False, type='str'), + auto_provision_as=dict(required=False, type='str'), + volume=dict(required=True, type='str'), + junction_path=dict(required=False, type='str'), + size=dict(required=False, type='int'), + size_unit=dict(default='gb', + choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb', + 'pb', 'eb', 'zb', 'yb'], type='str'), + vserver=dict(required=True, type='str'), + aggr_list=dict(required=False, type='list'), + aggr_list_multiplier=dict(required=False, type='int'), + force_offline=dict(required=False, type='bool', default=False), + force_unmount=dict(required=False, type='bool', default=False), + time_out=dict(required=False, type='int', default=180), + )) + + self.module = AnsibleModule( + argument_spec=self.argument_spec, + mutually_exclusive=[ + ('aggr_list', 'auto_provision_as'), + ], + supports_check_mode=True + ) + + self.na_helper = NetAppModule() + self.parameters = self.na_helper.set_parameters(self.module.params) + if self.parameters.get('size'): + self.parameters['size'] = self.parameters['size'] * \ + netapp_utils.POW2_BYTE_MAP[self.parameters['size_unit']] + # setup later if required + self.origin_server = None + if HAS_NETAPP_LIB is False: + self.module.fail_json(msg="the python NetApp-Lib module is required") + else: + self.server = netapp_utils.setup_ontap_zapi(module=self.module, + vserver=self.parameters['vserver']) + + def add_parameter_to_dict(self, adict, name, key=None, tostr=False): + ''' add defined parameter (not None) to adict using key ''' + if key is None: + key = name + if self.parameters.get(name) is not None: + if tostr: + adict[key] = str(self.parameters.get(name)) + else: + adict[key] = self.parameters.get(name) + + 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'] + 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' % 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 flexcache_get_iter(self): + """ + Compose NaElement object to query current FlexCache relation + """ + options = {'volume': self.parameters['volume']} + self.add_parameter_to_dict(options, 'origin_volume', 'origin-volume') + self.add_parameter_to_dict(options, 'origin_vserver', 'origin-vserver') + self.add_parameter_to_dict(options, 'origin_cluster', 'origin-cluster') + flexcache_info = netapp_utils.zapi.NaElement.create_node_with_children( + 'flexcache-info', **options) + query = netapp_utils.zapi.NaElement('query') + query.add_child_elem(flexcache_info) + flexcache_get_iter = netapp_utils.zapi.NaElement('flexcache-get-iter') + flexcache_get_iter.add_child_elem(query) + return flexcache_get_iter + + def flexcache_get(self): + """ + Get current FlexCache relations + :return: Dictionary of current FlexCache details if query successful, else None + """ + flexcache_get_iter = self.flexcache_get_iter() + flex_info = dict() + try: + result = self.server.invoke_successfully(flexcache_get_iter, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error fetching FlexCache info: %s' % to_native(error), + exception=traceback.format_exc()) + if result.get_child_by_name('num-records') and \ + int(result.get_child_content('num-records')) == 1: + flexcache_info = result.get_child_by_name('attributes-list') \ + .get_child_by_name('flexcache-info') + flex_info['origin_cluster'] = flexcache_info.get_child_content('origin-cluster') + flex_info['origin_volume'] = flexcache_info.get_child_content('origin-volume') + flex_info['origin_vserver'] = flexcache_info.get_child_content('origin-vserver') + flex_info['size'] = flexcache_info.get_child_content('size') + flex_info['volume'] = flexcache_info.get_child_content('volume') + flex_info['vserver'] = flexcache_info.get_child_content('vserver') + flex_info['auto_provision_as'] = flexcache_info.get_child_content('auto-provision-as') + + return flex_info + if result.get_child_by_name('num-records') and \ + int(result.get_child_content('num-records')) > 1: + msg = 'Multiple records found for %s:' % self.parameters['volume'] + self.module.fail_json(msg='Error fetching FlexCache info: %s' % msg) + return None + + def flexcache_create_async(self): + """ + Create a FlexCache relationship + """ + options = {'origin-volume': self.parameters['origin_volume'], + 'origin-vserver': self.parameters['origin_vserver'], + 'volume': self.parameters['volume']} + self.add_parameter_to_dict(options, 'junction_path', 'junction-path') + self.add_parameter_to_dict(options, 'auto_provision_as', 'auto-provision-as') + self.add_parameter_to_dict(options, 'size', 'size', tostr=True) + if self.parameters.get('aggr_list'): + if self.parameters.get('aggr_list_multiplier'): + self.tobytes_aggr_list_multiplier = bytes(self.parameters['aggr_list_multiplier']) + self.add_parameter_to_dict(options, 'tobytes_aggr_list_multiplier', 'aggr-list-multiplier') + flexcache_create = netapp_utils.zapi.NaElement.create_node_with_children( + 'flexcache-create-async', **options) + if self.parameters.get('aggr_list'): + aggregates = netapp_utils.zapi.NaElement('aggr-list') + for aggregate in self.parameters['aggr_list']: + aggregates.add_new_child('aggr-name', aggregate) + flexcache_create.add_child_elem(aggregates) + try: + result = self.server.invoke_successfully(flexcache_create, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error creating FlexCache %s' % to_native(error), + exception=traceback.format_exc()) + results = dict() + for key in ('result-status', 'result-jobid'): + if result.get_child_by_name(key): + results[key] = result[key] + return results + + def flexcache_create(self): + """ + Create a FlexCache relationship + Check job status + """ + results = self.flexcache_create_async() + status = results.get('result-status') + if status == 'in_progress' and 'result-jobid' in results: + if self.parameters['time_out'] == 0: + # asynchronous call, assuming success! + return + error = self.check_job_status(results['result-jobid']) + if error is None: + return + else: + self.module.fail_json(msg='Error when creating flexcache: %s' % error) + self.module.fail_json(msg='Unexpected error when creating flexcache: results is: %s' % repr(results)) + + def flexcache_delete_async(self): + """ + Delete FlexCache relationship at destination cluster + """ + options = {'volume': self.parameters['volume']} + flexcache_delete = netapp_utils.zapi.NaElement.create_node_with_children( + 'flexcache-destroy-async', **options) + try: + result = self.server.invoke_successfully(flexcache_delete, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error deleting FlexCache : %s' + % (to_native(error)), + exception=traceback.format_exc()) + results = dict() + for key in ('result-status', 'result-jobid'): + if result.get_child_by_name(key): + results[key] = result[key] + return results + + def volume_offline(self): + """ + Offline FlexCache volume at destination cluster + """ + options = {'name': self.parameters['volume']} + xml = netapp_utils.zapi.NaElement.create_node_with_children( + 'volume-offline', **options) + try: + self.server.invoke_successfully(xml, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error offlining FlexCache volume: %s' + % (to_native(error)), + exception=traceback.format_exc()) + + def volume_unmount(self): + """ + Unmount FlexCache volume at destination cluster + """ + options = {'volume-name': self.parameters['volume']} + xml = netapp_utils.zapi.NaElement.create_node_with_children( + 'volume-unmount', **options) + try: + self.server.invoke_successfully(xml, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error unmounting FlexCache volume: %s' + % (to_native(error)), + exception=traceback.format_exc()) + + def flexcache_delete_async(self): + """ + Delete FlexCache relationship at destination cluster + """ + options = {'volume': self.parameters['volume']} + flexcache_delete = netapp_utils.zapi.NaElement.create_node_with_children( + 'flexcache-destroy-async', **options) + try: + result = self.server.invoke_successfully(flexcache_delete, enable_tunneling=True) + except netapp_utils.zapi.NaApiError as error: + self.module.fail_json(msg='Error deleting FlexCache : %s' + % (to_native(error)), + exception=traceback.format_exc()) + results = dict() + for key in ('result-status', 'result-jobid'): + if result.get_child_by_name(key): + results[key] = result[key] + return results + + def flexcache_delete(self): + """ + Delete FlexCache relationship at destination cluster + Check job status + """ + if self.parameters['force_unmount']: + self.volume_unmount() + if self.parameters['force_offline']: + self.volume_offline() + results = self.flexcache_delete_async() + status = results.get('result-status') + if status == 'in_progress' and 'result-jobid' in results: + if self.parameters['time_out'] == 0: + # asynchronous call, assuming success! + return + error = self.check_job_status(results['result-jobid']) + if error is None: + return + else: + self.module.fail_json(msg='Error when deleting flexcache: %s' % error) + self.module.fail_json(msg='Unexpected error when deleting flexcache: results is: %s' % repr(results)) + + def check_parameters(self): + """ + Validate parameters and fail if one or more required params are missing + """ + missings = list() + expected = ('origin_volume', 'origin_vserver') + if self.parameters['state'] == 'present': + for param in expected: + if not self.parameters.get(param): + missings.append(param) + if missings: + plural = 's' if len(missings) > 1 else '' + msg = 'Missing parameter%s: %s' % (plural, ', '.join(missings)) + self.module.fail_json(msg=msg) + + def apply(self): + """ + Apply action to FlexCache + """ + netapp_utils.ems_log_event("na_ontap_flexcache", self.server) + current = self.flexcache_get() + cd_action = self.na_helper.get_cd_action(current, self.parameters) + if cd_action == 'create': + self.check_parameters() + self.flexcache_create() + elif cd_action == 'delete': + self.flexcache_delete() + self.module.exit_json(changed=self.na_helper.changed) + + +def main(): + """Execute action""" + community_obj = NetAppONTAPFlexCache() + community_obj.apply() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/storage/netapp/test_na_ontap_flexcache.py b/test/units/modules/storage/netapp/test_na_ontap_flexcache.py new file mode 100644 index 0000000000..a960825227 --- /dev/null +++ b/test/units/modules/storage/netapp/test_na_ontap_flexcache.py @@ -0,0 +1,528 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP FlexCache Ansible module ''' + +from __future__ import print_function +import json +import pytest + +from units.compat import unittest +from units.compat.mock import patch +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes +import ansible.module_utils.netapp as netapp_utils + +from ansible.modules.storage.netapp.na_ontap_flexcache \ + import NetAppONTAPFlexCache as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def set_module_args(args): + """prepare arguments so that they will be picked up during module creation""" + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught by the test case""" + pass + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + pass + + +def exit_json(*args, **kwargs): # pylint: disable=unused-argument + """function to patch over exit_json; package return data into an exception""" + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): # pylint: disable=unused-argument + """function to patch over fail_json; package return data into an exception""" + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, parm1=None, api_error=None, job_error=None): + ''' save arguments ''' + self.type = kind + self.parm1 = parm1 + self.api_error = api_error + self.job_error = job_error + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + tag = xml.get_name() + if tag == 'flexcache-get-iter' and self.type == 'vserver': + xml = self.build_flexcache_info(self.parm1) + elif tag == 'flexcache-create-async': + xml = self.build_flexcache_create_destroy_rsp() + elif tag == 'flexcache-destroy-async': + if self.api_error: + code, message = self.api_error.split(':', 2) + raise netapp_utils.zapi.NaApiError(code, message) + xml = self.build_flexcache_create_destroy_rsp() + elif tag == 'job-get': + xml = self.build_job_info(self.job_error) + self.xml_out = xml + return xml + + @staticmethod + def build_flexcache_info(vserver): + ''' build xml data for vserser-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = netapp_utils.zapi.NaElement('attributes-list') + count = 2 if vserver == 'repeats' else 1 + for dummy in range(count): + attributes.add_node_with_children('flexcache-info', **{ + 'vserver': vserver, + 'origin-vserver': 'ovserver', + 'origin-volume': 'ovolume', + 'origin-cluster': 'ocluster', + 'volume': 'volume', + }) + xml.add_child_elem(attributes) + xml.add_new_child('num-records', str(count)) + return xml + + @staticmethod + def build_flexcache_create_destroy_rsp(): + ''' build xml data for a create or destroy response ''' + xml = netapp_utils.zapi.NaElement('xml') + xml.add_new_child('result-status', 'in_progress') + xml.add_new_child('result-jobid', '1234') + 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' + else: + state = 'failure' + 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 + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_module_helper = patch.multiple(basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json) + self.mock_module_helper.start() + self.addCleanup(self.mock_module_helper.stop) + # make sure to change this to False before submitting + self.onbox = False + self.dummy_args = dict() + for arg in ('hostname', 'username', 'password'): + self.dummy_args[arg] = arg + if self.onbox: + self.args = { + 'hostname': '10.193.78.219', + 'username': 'admin', + 'password': 'netapp1!', + } + else: + self.args = self.dummy_args + self.server = MockONTAPConnection() + + def create_flexcache(self, vserver, volume, junction_path): + ''' create flexcache ''' + if not self.onbox: + return + args = { + 'state': 'present', + 'volume': volume, + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'vserver': vserver, + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'junction_path': junction_path, + } + args.update(self.args) + set_module_args(args) + my_obj = my_module() + try: + my_obj.apply() + except AnsibleExitJson as exc: + print('Create util: ' + repr(exc)) + except AnsibleFailJson as exc: + print('Create util: ' + repr(exc)) + + def delete_flexcache(self, vserver, volume): + ''' delete flexcache ''' + if not self.onbox: + return + args = {'volume': volume, 'vserver': vserver, 'state': 'absent', 'force_offline': 'true'} + args.update(self.args) + set_module_args(args) + my_obj = my_module() + try: + my_obj.apply() + except AnsibleExitJson as exc: + print('Delete util: ' + repr(exc)) + except AnsibleFailJson as exc: + print('Delete util: ' + repr(exc)) + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_missing_parameters(self): + ''' fail if origin volume and origin verser are missing ''' + args = { + 'vserver': 'vserver', + 'volume': 'volume' + } + args.update(self.dummy_args) + set_module_args(args) + my_obj = my_module() + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + # It may not be a good idea to start with apply + # More atomic methods can be easier to mock + # Hint: start with get methods, as they are called first + my_obj.apply() + msg = 'Missing parameters: origin_volume, origin_vserver' + assert exc.value.args[0]['msg'] == msg + + def test_missing_parameter(self): + ''' fail if origin verser parameter is missing ''' + args = { + 'vserver': 'vserver', + 'origin_volume': 'origin_volume', + 'volume': 'volume' + } + args.update(self.dummy_args) + set_module_args(args) + my_obj = my_module() + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = 'Missing parameter: origin_vserver' + assert exc.value.args[0]['msg'] == msg + + def test_get_flexcache(self): + ''' get flexcache info ''' + args = { + 'vserver': 'ansibleSVM', + 'origin_volume': 'origin_volume', + 'volume': 'volume' + } + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('vserver') + info = my_obj.flexcache_get() + print('info: ' + repr(info)) + + def test_get_flexcache_double(self): + ''' get flexcache info returns 2 entries! ''' + args = { + 'vserver': 'ansibleSVM', + 'origin_volume': 'origin_volume', + 'volume': 'volume' + } + args.update(self.dummy_args) + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('vserver', 'repeats') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.flexcache_get() + msg = 'Error fetching FlexCache info: Multiple records found for %s:' % args['volume'] + assert exc.value.args[0]['msg'] == msg + + def test_create_flexcache(self): + ''' create flexcache ''' + args = { + 'volume': 'volume', + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'vserver': 'ansibleSVM', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + self.delete_flexcache(args['vserver'], args['volume']) + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection() + with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create: + # with patch('__main__.my_module.flexcache_create', wraps=my_obj.flexcache_create) as mock_create: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_create.assert_called_with() + + def test_create_flexcache_idempotent(self): + ''' create flexcache - already exists ''' + args = { + 'volume': 'volume', + 'vserver': 'ansibleSVM', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('vserver') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] is False + + def test_create_flexcache_autoprovision(self): + ''' create flexcache with autoprovision''' + args = { + 'volume': 'volume', + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'vserver': 'ansibleSVM', + 'auto_provision_as': 'flexgroup', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + self.delete_flexcache(args['vserver'], args['volume']) + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection() + with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_create.assert_called_with() + + def test_create_flexcache_autoprovision_idempotent(self): + ''' create flexcache with autoprovision - already exists ''' + args = { + 'volume': 'volume', + 'vserver': 'ansibleSVM', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'auto_provision_as': 'flexgroup', + } + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('vserver') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] is False + + def test_create_flexcache_multiplier(self): + ''' create flexcache with aggregate multiplier''' + args = { + 'volume': 'volume', + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'vserver': 'ansibleSVM', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'aggr_list_multiplier': '2', + } + self.delete_flexcache(args['vserver'], args['volume']) + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection() + with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_create.assert_called_with() + + def test_create_flexcache_multiplier_idempotent(self): + ''' create flexcache with aggregate multiplier - already exists ''' + args = { + 'volume': 'volume', + 'vserver': 'ansibleSVM', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'aggr_list_multiplier': '2', + } + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('vserver') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] is False + + def test_delete_flexcache_exists_no_force(self): + ''' delete flexcache ''' + args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'state': 'absent'} + args.update(self.args) + set_module_args(args) + my_obj = my_module() + error = '13001:Volume volume in Vserver ansibleSVM must be offline to be deleted. ' \ + 'Use "volume offline -vserver ansibleSVM -volume volume" command to offline ' \ + 'the volume' + if not self.onbox: + my_obj.server = MockONTAPConnection('vserver', 'flex', api_error=error) + with patch.object(my_module, 'flexcache_delete', wraps=my_obj.flexcache_delete) as mock_delete: + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + msg = 'Error deleting FlexCache : NetApp API failed. Reason - %s' % error + assert exc.value.args[0]['msg'] == msg + mock_delete.assert_called_with() + + def test_delete_flexcache_exists_with_force(self): + ''' delete flexcache ''' + args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'state': 'absent', 'force_offline': 'true'} + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('vserver', 'flex') + with patch.object(my_module, 'flexcache_delete', wraps=my_obj.flexcache_delete) as mock_delete: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_delete.assert_called_with() + + def test_delete_flexcache_exists_junctionpath_no_force(self): + ''' delete flexcache ''' + args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'junction_path': 'jpath', 'state': 'absent', 'force_offline': 'true'} + self.create_flexcache(args['vserver'], args['volume'], args['junction_path']) + args.update(self.args) + set_module_args(args) + my_obj = my_module() + error = '160:Volume volume on Vserver ansibleSVM must be unmounted before being taken offline or restricted.' + if not self.onbox: + my_obj.server = MockONTAPConnection('vserver', 'flex', api_error=error) + with patch.object(my_module, 'flexcache_delete', wraps=my_obj.flexcache_delete) as mock_delete: + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + msg = 'Error deleting FlexCache : NetApp API failed. Reason - %s' % error + assert exc.value.args[0]['msg'] == msg + mock_delete.assert_called_with() + + def test_delete_flexcache_exists_junctionpath_with_force(self): + ''' delete flexcache ''' + args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'junction_path': 'jpath', 'state': 'absent', 'force_offline': 'true', 'force_unmount': 'true'} + self.create_flexcache(args['vserver'], args['volume'], args['junction_path']) + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('vserver', 'flex') + with patch.object(my_module, 'flexcache_delete', wraps=my_obj.flexcache_delete) as mock_delete: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_delete.assert_called_with() + + def test_delete_flexcache_not_exist(self): + ''' delete flexcache ''' + args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'state': 'absent'} + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] is False + + def test_create_flexcache_size_error(self): + ''' create flexcache ''' + args = { + 'volume': 'volume_err', + 'size': '50', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'vserver': 'ansibleSVM', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + args.update(self.args) + set_module_args(args) + my_obj = my_module() + error = 'Size "50MB" ("52428800B") is too small. Minimum size is "80MB" ("83886080B"). ' + if not self.onbox: + my_obj.server = MockONTAPConnection(job_error=error) + with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create: + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + msg = 'Error when creating flexcache: %s' % error + assert exc.value.args[0]['msg'] == msg + mock_create.assert_called_with() + + def test_create_flexcache_time_out(self): + ''' create flexcache ''' + args = { + 'volume': 'volume_err', + 'size': '50', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'vserver': 'ansibleSVM', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'time_out': '2' + } + args.update(self.args) + set_module_args(args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection(job_error='time_out') + with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create: + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + msg = 'Error when creating flexcache: job completion exceeded expected timer of: %s seconds' \ + % args['time_out'] + assert exc.value.args[0]['msg'] == msg + mock_create.assert_called_with()