1# (c) 2018, NetApp, Inc
2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
4''' unit test for ONTAP FlexCache Ansible module '''
5
6from __future__ import print_function
7import json
8import pytest
9
10from units.compat import unittest
11from units.compat.mock import patch
12from ansible.module_utils import basic
13from ansible.module_utils._text import to_bytes
14import ansible.module_utils.netapp as netapp_utils
15
16from ansible.modules.storage.netapp.na_ontap_flexcache \
17    import NetAppONTAPFlexCache as my_module  # module under test
18
19if not netapp_utils.has_netapp_lib():
20    pytestmark = pytest.mark.skip('skipping as missing required netapp_lib')
21
22
23def set_module_args(args):
24    """prepare arguments so that they will be picked up during module creation"""
25    args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
26    basic._ANSIBLE_ARGS = to_bytes(args)  # pylint: disable=protected-access
27
28
29class AnsibleExitJson(Exception):
30    """Exception class to be raised by module.exit_json and caught by the test case"""
31    pass
32
33
34class AnsibleFailJson(Exception):
35    """Exception class to be raised by module.fail_json and caught by the test case"""
36    pass
37
38
39def exit_json(*args, **kwargs):  # pylint: disable=unused-argument
40    """function to patch over exit_json; package return data into an exception"""
41    if 'changed' not in kwargs:
42        kwargs['changed'] = False
43    raise AnsibleExitJson(kwargs)
44
45
46def fail_json(*args, **kwargs):  # pylint: disable=unused-argument
47    """function to patch over fail_json; package return data into an exception"""
48    kwargs['failed'] = True
49    raise AnsibleFailJson(kwargs)
50
51
52class MockONTAPConnection(object):
53    ''' mock server connection to ONTAP host '''
54
55    def __init__(self, kind=None, parm1=None, api_error=None, job_error=None):
56        ''' save arguments '''
57        self.type = kind
58        self.parm1 = parm1
59        self.api_error = api_error
60        self.job_error = job_error
61        self.xml_in = None
62        self.xml_out = None
63
64    def invoke_successfully(self, xml, enable_tunneling):  # pylint: disable=unused-argument
65        ''' mock invoke_successfully returning xml data '''
66        self.xml_in = xml
67        tag = xml.get_name()
68        if tag == 'flexcache-get-iter' and self.type == 'vserver':
69            xml = self.build_flexcache_info(self.parm1)
70        elif tag == 'flexcache-create-async':
71            xml = self.build_flexcache_create_destroy_rsp()
72        elif tag == 'flexcache-destroy-async':
73            if self.api_error:
74                code, message = self.api_error.split(':', 2)
75                raise netapp_utils.zapi.NaApiError(code, message)
76            xml = self.build_flexcache_create_destroy_rsp()
77        elif tag == 'job-get':
78            xml = self.build_job_info(self.job_error)
79        self.xml_out = xml
80        return xml
81
82    @staticmethod
83    def build_flexcache_info(vserver):
84        ''' build xml data for vserser-info '''
85        xml = netapp_utils.zapi.NaElement('xml')
86        attributes = netapp_utils.zapi.NaElement('attributes-list')
87        count = 2 if vserver == 'repeats' else 1
88        for dummy in range(count):
89            attributes.add_node_with_children('flexcache-info', **{
90                'vserver': vserver,
91                'origin-vserver': 'ovserver',
92                'origin-volume': 'ovolume',
93                'origin-cluster': 'ocluster',
94                'volume': 'volume',
95            })
96        xml.add_child_elem(attributes)
97        xml.add_new_child('num-records', str(count))
98        return xml
99
100    @staticmethod
101    def build_flexcache_create_destroy_rsp():
102        ''' build xml data for a create or destroy response '''
103        xml = netapp_utils.zapi.NaElement('xml')
104        xml.add_new_child('result-status', 'in_progress')
105        xml.add_new_child('result-jobid', '1234')
106        return xml
107
108    @staticmethod
109    def build_job_info(error):
110        ''' build xml data for a job '''
111        xml = netapp_utils.zapi.NaElement('xml')
112        attributes = netapp_utils.zapi.NaElement('attributes')
113        if error is None:
114            state = 'success'
115        elif error == 'time_out':
116            state = 'running'
117        else:
118            state = 'failure'
119        attributes.add_node_with_children('job-info', **{
120            'job-state': state,
121            'job-progress': 'dummy',
122            'job-completion': error,
123        })
124        xml.add_child_elem(attributes)
125        xml.add_new_child('result-status', 'in_progress')
126        xml.add_new_child('result-jobid', '1234')
127        return xml
128
129
130class TestMyModule(unittest.TestCase):
131    ''' a group of related Unit Tests '''
132
133    def setUp(self):
134        self.mock_module_helper = patch.multiple(basic.AnsibleModule,
135                                                 exit_json=exit_json,
136                                                 fail_json=fail_json)
137        self.mock_module_helper.start()
138        self.addCleanup(self.mock_module_helper.stop)
139        # make sure to change this to False before submitting
140        self.onbox = False
141        self.dummy_args = dict()
142        for arg in ('hostname', 'username', 'password'):
143            self.dummy_args[arg] = arg
144        if self.onbox:
145            self.args = {
146                'hostname': '10.193.78.219',
147                'username': 'admin',
148                'password': 'netapp1!',
149            }
150        else:
151            self.args = self.dummy_args
152        self.server = MockONTAPConnection()
153
154    def create_flexcache(self, vserver, volume, junction_path):
155        ''' create flexcache '''
156        if not self.onbox:
157            return
158        args = {
159            'state': 'present',
160            'volume': volume,
161            'size': '90',       # 80MB minimum
162            'size_unit': 'mb',  # 80MB minimum
163            'vserver': vserver,
164            'aggr_list': 'aggr1',
165            'origin_volume': 'fc_vol_origin',
166            'origin_vserver': 'ansibleSVM',
167            'junction_path': junction_path,
168        }
169        args.update(self.args)
170        set_module_args(args)
171        my_obj = my_module()
172        try:
173            my_obj.apply()
174        except AnsibleExitJson as exc:
175            print('Create util: ' + repr(exc))
176        except AnsibleFailJson as exc:
177            print('Create util: ' + repr(exc))
178
179    def delete_flexcache(self, vserver, volume):
180        ''' delete flexcache '''
181        if not self.onbox:
182            return
183        args = {'volume': volume, 'vserver': vserver, 'state': 'absent', 'force_offline': 'true'}
184        args.update(self.args)
185        set_module_args(args)
186        my_obj = my_module()
187        try:
188            my_obj.apply()
189        except AnsibleExitJson as exc:
190            print('Delete util: ' + repr(exc))
191        except AnsibleFailJson as exc:
192            print('Delete util: ' + repr(exc))
193
194    def test_module_fail_when_required_args_missing(self):
195        ''' required arguments are reported as errors '''
196        with pytest.raises(AnsibleFailJson) as exc:
197            set_module_args({})
198            my_module()
199        print('Info: %s' % exc.value.args[0]['msg'])
200
201    def test_missing_parameters(self):
202        ''' fail if origin volume and origin verser are missing '''
203        args = {
204            'vserver': 'vserver',
205            'volume': 'volume'
206        }
207        args.update(self.dummy_args)
208        set_module_args(args)
209        my_obj = my_module()
210        my_obj.server = self.server
211        with pytest.raises(AnsibleFailJson) as exc:
212            # It may not be a good idea to start with apply
213            # More atomic methods can be easier to mock
214            # Hint: start with get methods, as they are called first
215            my_obj.apply()
216        msg = 'Missing parameters: origin_volume, origin_vserver'
217        assert exc.value.args[0]['msg'] == msg
218
219    def test_missing_parameter(self):
220        ''' fail if origin verser parameter is missing '''
221        args = {
222            'vserver': 'vserver',
223            'origin_volume': 'origin_volume',
224            'volume': 'volume'
225        }
226        args.update(self.dummy_args)
227        set_module_args(args)
228        my_obj = my_module()
229        my_obj.server = self.server
230        with pytest.raises(AnsibleFailJson) as exc:
231            my_obj.apply()
232        msg = 'Missing parameter: origin_vserver'
233        assert exc.value.args[0]['msg'] == msg
234
235    def test_get_flexcache(self):
236        ''' get flexcache info '''
237        args = {
238            'vserver': 'ansibleSVM',
239            'origin_volume': 'origin_volume',
240            'volume': 'volume'
241        }
242        args.update(self.args)
243        set_module_args(args)
244        my_obj = my_module()
245        if not self.onbox:
246            my_obj.server = MockONTAPConnection('vserver')
247        info = my_obj.flexcache_get()
248        print('info: ' + repr(info))
249
250    def test_get_flexcache_double(self):
251        ''' get flexcache info returns 2 entries! '''
252        args = {
253            'vserver': 'ansibleSVM',
254            'origin_volume': 'origin_volume',
255            'volume': 'volume'
256        }
257        args.update(self.dummy_args)
258        set_module_args(args)
259        my_obj = my_module()
260        my_obj.server = MockONTAPConnection('vserver', 'repeats')
261        with pytest.raises(AnsibleFailJson) as exc:
262            my_obj.flexcache_get()
263        msg = 'Error fetching FlexCache info: Multiple records found for %s:' % args['volume']
264        assert exc.value.args[0]['msg'] == msg
265
266    def test_create_flexcache(self):
267        ''' create flexcache '''
268        args = {
269            'volume': 'volume',
270            'size': '90',       # 80MB minimum
271            'size_unit': 'mb',  # 80MB minimum
272            'vserver': 'ansibleSVM',
273            'aggr_list': 'aggr1',
274            'origin_volume': 'fc_vol_origin',
275            'origin_vserver': 'ansibleSVM',
276        }
277        self.delete_flexcache(args['vserver'], args['volume'])
278        args.update(self.args)
279        set_module_args(args)
280        my_obj = my_module()
281        if not self.onbox:
282            my_obj.server = MockONTAPConnection()
283        with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create:
284            # with patch('__main__.my_module.flexcache_create', wraps=my_obj.flexcache_create) as mock_create:
285            with pytest.raises(AnsibleExitJson) as exc:
286                my_obj.apply()
287            print('Create: ' + repr(exc.value))
288            assert exc.value.args[0]['changed']
289            mock_create.assert_called_with()
290
291    def test_create_flexcache_idempotent(self):
292        ''' create flexcache - already exists '''
293        args = {
294            'volume': 'volume',
295            'vserver': 'ansibleSVM',
296            'aggr_list': 'aggr1',
297            'origin_volume': 'fc_vol_origin',
298            'origin_vserver': 'ansibleSVM',
299        }
300        args.update(self.args)
301        set_module_args(args)
302        my_obj = my_module()
303        if not self.onbox:
304            my_obj.server = MockONTAPConnection('vserver')
305        with pytest.raises(AnsibleExitJson) as exc:
306            my_obj.apply()
307        print('Create: ' + repr(exc.value))
308        assert exc.value.args[0]['changed'] is False
309
310    def test_create_flexcache_autoprovision(self):
311        ''' create flexcache with autoprovision'''
312        args = {
313            'volume': 'volume',
314            'size': '90',       # 80MB minimum
315            'size_unit': 'mb',  # 80MB minimum
316            'vserver': 'ansibleSVM',
317            'auto_provision_as': 'flexgroup',
318            'origin_volume': 'fc_vol_origin',
319            'origin_vserver': 'ansibleSVM',
320        }
321        self.delete_flexcache(args['vserver'], args['volume'])
322        args.update(self.args)
323        set_module_args(args)
324        my_obj = my_module()
325        if not self.onbox:
326            my_obj.server = MockONTAPConnection()
327        with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create:
328            with pytest.raises(AnsibleExitJson) as exc:
329                my_obj.apply()
330            print('Create: ' + repr(exc.value))
331            assert exc.value.args[0]['changed']
332            mock_create.assert_called_with()
333
334    def test_create_flexcache_autoprovision_idempotent(self):
335        ''' create flexcache with autoprovision - already exists '''
336        args = {
337            'volume': 'volume',
338            'vserver': 'ansibleSVM',
339            'origin_volume': 'fc_vol_origin',
340            'origin_vserver': 'ansibleSVM',
341            'auto_provision_as': 'flexgroup',
342        }
343        args.update(self.args)
344        set_module_args(args)
345        my_obj = my_module()
346        if not self.onbox:
347            my_obj.server = MockONTAPConnection('vserver')
348        with pytest.raises(AnsibleExitJson) as exc:
349            my_obj.apply()
350        print('Create: ' + repr(exc.value))
351        assert exc.value.args[0]['changed'] is False
352
353    def test_create_flexcache_multiplier(self):
354        ''' create flexcache with aggregate multiplier'''
355        args = {
356            'volume': 'volume',
357            'size': '90',       # 80MB minimum
358            'size_unit': 'mb',  # 80MB minimum
359            'vserver': 'ansibleSVM',
360            'aggr_list': 'aggr1',
361            'origin_volume': 'fc_vol_origin',
362            'origin_vserver': 'ansibleSVM',
363            'aggr_list_multiplier': '2',
364        }
365        self.delete_flexcache(args['vserver'], args['volume'])
366        args.update(self.args)
367        set_module_args(args)
368        my_obj = my_module()
369        if not self.onbox:
370            my_obj.server = MockONTAPConnection()
371        with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create:
372            with pytest.raises(AnsibleExitJson) as exc:
373                my_obj.apply()
374            print('Create: ' + repr(exc.value))
375            assert exc.value.args[0]['changed']
376            mock_create.assert_called_with()
377
378    def test_create_flexcache_multiplier_idempotent(self):
379        ''' create flexcache with aggregate multiplier - already exists '''
380        args = {
381            'volume': 'volume',
382            'vserver': 'ansibleSVM',
383            'aggr_list': 'aggr1',
384            'origin_volume': 'fc_vol_origin',
385            'origin_vserver': 'ansibleSVM',
386            'aggr_list_multiplier': '2',
387        }
388        args.update(self.args)
389        set_module_args(args)
390        my_obj = my_module()
391        if not self.onbox:
392            my_obj.server = MockONTAPConnection('vserver')
393        with pytest.raises(AnsibleExitJson) as exc:
394            my_obj.apply()
395        print('Create: ' + repr(exc.value))
396        assert exc.value.args[0]['changed'] is False
397
398    def test_delete_flexcache_exists_no_force(self):
399        ''' delete flexcache '''
400        args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'state': 'absent'}
401        args.update(self.args)
402        set_module_args(args)
403        my_obj = my_module()
404        error = '13001:Volume volume in Vserver ansibleSVM must be offline to be deleted. ' \
405                'Use "volume offline -vserver ansibleSVM -volume volume" command to offline ' \
406                'the volume'
407        if not self.onbox:
408            my_obj.server = MockONTAPConnection('vserver', 'flex', api_error=error)
409        with patch.object(my_module, 'flexcache_delete', wraps=my_obj.flexcache_delete) as mock_delete:
410            with pytest.raises(AnsibleFailJson) as exc:
411                my_obj.apply()
412            print('Delete: ' + repr(exc.value))
413            msg = 'Error deleting FlexCache : NetApp API failed. Reason - %s' % error
414            assert exc.value.args[0]['msg'] == msg
415            mock_delete.assert_called_with()
416
417    def test_delete_flexcache_exists_with_force(self):
418        ''' delete flexcache '''
419        args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'state': 'absent', 'force_offline': 'true'}
420        args.update(self.args)
421        set_module_args(args)
422        my_obj = my_module()
423        if not self.onbox:
424            my_obj.server = MockONTAPConnection('vserver', 'flex')
425        with patch.object(my_module, 'flexcache_delete', wraps=my_obj.flexcache_delete) as mock_delete:
426            with pytest.raises(AnsibleExitJson) as exc:
427                my_obj.apply()
428            print('Delete: ' + repr(exc.value))
429            assert exc.value.args[0]['changed']
430            mock_delete.assert_called_with()
431
432    def test_delete_flexcache_exists_junctionpath_no_force(self):
433        ''' delete flexcache '''
434        args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'junction_path': 'jpath', 'state': 'absent', 'force_offline': 'true'}
435        self.create_flexcache(args['vserver'], args['volume'], args['junction_path'])
436        args.update(self.args)
437        set_module_args(args)
438        my_obj = my_module()
439        error = '160:Volume volume on Vserver ansibleSVM must be unmounted before being taken offline or restricted.'
440        if not self.onbox:
441            my_obj.server = MockONTAPConnection('vserver', 'flex', api_error=error)
442        with patch.object(my_module, 'flexcache_delete', wraps=my_obj.flexcache_delete) as mock_delete:
443            with pytest.raises(AnsibleFailJson) as exc:
444                my_obj.apply()
445            print('Delete: ' + repr(exc.value))
446            msg = 'Error deleting FlexCache : NetApp API failed. Reason - %s' % error
447            assert exc.value.args[0]['msg'] == msg
448            mock_delete.assert_called_with()
449
450    def test_delete_flexcache_exists_junctionpath_with_force(self):
451        ''' delete flexcache '''
452        args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'junction_path': 'jpath', 'state': 'absent', 'force_offline': 'true', 'force_unmount': 'true'}
453        self.create_flexcache(args['vserver'], args['volume'], args['junction_path'])
454        args.update(self.args)
455        set_module_args(args)
456        my_obj = my_module()
457        if not self.onbox:
458            my_obj.server = MockONTAPConnection('vserver', 'flex')
459        with patch.object(my_module, 'flexcache_delete', wraps=my_obj.flexcache_delete) as mock_delete:
460            with pytest.raises(AnsibleExitJson) as exc:
461                my_obj.apply()
462            print('Delete: ' + repr(exc.value))
463            assert exc.value.args[0]['changed']
464            mock_delete.assert_called_with()
465
466    def test_delete_flexcache_not_exist(self):
467        ''' delete flexcache '''
468        args = {'volume': 'volume', 'vserver': 'ansibleSVM', 'state': 'absent'}
469        args.update(self.args)
470        set_module_args(args)
471        my_obj = my_module()
472        if not self.onbox:
473            my_obj.server = MockONTAPConnection()
474        with pytest.raises(AnsibleExitJson) as exc:
475            my_obj.apply()
476        print('Delete: ' + repr(exc.value))
477        assert exc.value.args[0]['changed'] is False
478
479    def test_create_flexcache_size_error(self):
480        ''' create flexcache '''
481        args = {
482            'volume': 'volume_err',
483            'size': '50',       # 80MB minimum
484            'size_unit': 'mb',  # 80MB minimum
485            'vserver': 'ansibleSVM',
486            'aggr_list': 'aggr1',
487            'origin_volume': 'fc_vol_origin',
488            'origin_vserver': 'ansibleSVM',
489        }
490        args.update(self.args)
491        set_module_args(args)
492        my_obj = my_module()
493        error = 'Size "50MB" ("52428800B") is too small.  Minimum size is "80MB" ("83886080B"). '
494        if not self.onbox:
495            my_obj.server = MockONTAPConnection(job_error=error)
496        with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create:
497            with pytest.raises(AnsibleFailJson) as exc:
498                my_obj.apply()
499            print('Create: ' + repr(exc.value))
500            msg = 'Error when creating flexcache: %s' % error
501            assert exc.value.args[0]['msg'] == msg
502            mock_create.assert_called_with()
503
504    @patch('time.sleep')
505    def test_create_flexcache_time_out(self, mock_sleep):
506        ''' create flexcache '''
507        args = {
508            'volume': 'volume_err',
509            'size': '50',       # 80MB minimum
510            'size_unit': 'mb',  # 80MB minimum
511            'vserver': 'ansibleSVM',
512            'aggr_list': 'aggr1',
513            'origin_volume': 'fc_vol_origin',
514            'origin_vserver': 'ansibleSVM',
515            'time_out': '2'
516        }
517        args.update(self.args)
518        set_module_args(args)
519        my_obj = my_module()
520        if not self.onbox:
521            my_obj.server = MockONTAPConnection(job_error='time_out')
522        with patch.object(my_module, 'flexcache_create', wraps=my_obj.flexcache_create) as mock_create:
523            with pytest.raises(AnsibleFailJson) as exc:
524                my_obj.apply()
525            print('Create: ' + repr(exc.value))
526            msg = 'Error when creating flexcache: job completion exceeded expected timer of: %s seconds' \
527                % args['time_out']
528            assert exc.value.args[0]['msg'] == msg
529            mock_create.assert_called_with()
530