1''' unit tests ONTAP Ansible module: na_ontap_quotas '''
2from __future__ import (absolute_import, division, print_function)
3__metaclass__ = type
4import json
5import pytest
6
7from ansible.module_utils import basic
8from ansible.module_utils._text import to_bytes
9from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch
10import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
11
12from ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree \
13    import NetAppOntapQTree as qtree_module  # module under test
14
15if not netapp_utils.has_netapp_lib():
16    pytestmark = pytest.mark.skip('skipping as missing required netapp_lib')
17
18# change this to True to run on a VSIM
19ONBOX = False
20
21# REST API canned responses when mocking send_request
22SRR = {
23    # common responses
24    'is_rest': (200, {}, None),
25    'is_zapi': (400, {}, "Unreachable"),
26    'empty_good': (200, {}, None),
27    'no_record': (200, dict(records=[], num_records=0), None),
28    'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"),
29    'generic_error': (400, None, "Expected error"),
30    # module specific responses
31    'qtree_record': (200,
32                     {"records": [{"svm": {"uuid": "09e9fd5e-8ebd-11e9-b162-005056b39fe7",
33                                           "name": "ansibleSVM"},
34                                   "id": 1,
35                                   "name": "string",
36                                   "security_style": "unix",
37                                   "unix_permissions": "abc",
38                                   "export_policy": {"name": "ansible"},
39                                   "volume": {"name": "volume1",
40                                              "uuid": "028baa66-41bd-11e9-81d5-00a0986138f7"}}]}, None)
41}
42
43
44def set_module_args(args):
45    """prepare arguments so that they will be picked up during module creation"""
46    args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
47    basic._ANSIBLE_ARGS = to_bytes(args)  # pylint: disable=protected-access
48
49
50class AnsibleExitJson(Exception):
51    """Exception class to be raised by module.exit_json and caught by the test case"""
52
53
54class AnsibleFailJson(Exception):
55    """Exception class to be raised by module.fail_json and caught by the test case"""
56
57
58def exit_json(*args, **kwargs):  # pylint: disable=unused-argument
59    """function to patch over exit_json; package return data into an exception"""
60    if 'changed' not in kwargs:
61        kwargs['changed'] = False
62    raise AnsibleExitJson(kwargs)
63
64
65def fail_json(*args, **kwargs):  # pylint: disable=unused-argument
66    """function to patch over fail_json; package return data into an exception"""
67    kwargs['failed'] = True
68    raise AnsibleFailJson(kwargs)
69
70
71@pytest.fixture(name='patch_ansible_mod')
72def fixture_patch_ansible():
73    with patch.multiple(basic.AnsibleModule,
74                        exit_json=exit_json,
75                        fail_json=fail_json) as mocks:
76        yield mocks
77
78
79class MockONTAPConnection(object):
80    ''' mock server connection to ONTAP host '''
81
82    def __init__(self, kind=None):
83        ''' save arguments '''
84        self.type = kind
85        self.xml_in = None
86        self.xml_out = None
87
88    def invoke_successfully(self, xml, enable_tunneling):  # pylint: disable=unused-argument
89        ''' mock invoke_successfully returning xml data '''
90        self.xml_in = xml
91        if self.type == 'qtree':
92            xml = self.build_qtree_info()
93        elif self.type == 'qtree_fail':
94            raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test")
95        self.xml_out = xml
96        return xml
97
98    @staticmethod
99    def build_qtree_info():
100        ''' build xml data for quota-entry '''
101        xml = netapp_utils.zapi.NaElement('xml')
102        data = {'num-records': 1,
103                'attributes-list': {'qtree-info': {'export-policy': 'ansible', 'vserver': 'ansible', 'qtree': 'ansible',
104                                                   'oplocks': 'enabled', 'security-style': 'unix', 'mode': 'abc',
105                                                   'volume': 'ansible'}}}
106        xml.translate_struct(data)
107        return xml
108
109
110def set_default_args(use_rest=None):
111    if ONBOX:
112        hostname = '10.10.10.10'
113        username = 'username'
114        password = 'password'
115        name = 'ansible'
116        vserver = 'ansible'
117        flexvol_name = 'ansible'
118        export_policy = 'ansible'
119        security_style = 'unix'
120        mode = 'abc'
121    else:
122        hostname = '10.10.10.10'
123        username = 'username'
124        password = 'password'
125        name = 'ansible'
126        vserver = 'ansible'
127        flexvol_name = 'ansible'
128        export_policy = 'ansible'
129        security_style = 'unix'
130        mode = 'abc'
131
132    args = dict({
133        'state': 'present',
134        'hostname': hostname,
135        'username': username,
136        'password': password,
137        'name': name,
138        'vserver': vserver,
139        'flexvol_name': flexvol_name,
140        'export_policy': export_policy,
141        'security_style': security_style,
142        'unix_permissions': mode
143    })
144
145    if use_rest is not None:
146        args['use_rest'] = use_rest
147
148    return args
149
150
151def get_qtree_mock_object(cx_type='zapi', kind=None):
152    qtree_obj = qtree_module()
153    if cx_type == 'zapi':
154        if kind is None:
155            qtree_obj.server = MockONTAPConnection()
156        else:
157            qtree_obj.server = MockONTAPConnection(kind=kind)
158    return qtree_obj
159
160
161def test_module_fail_when_required_args_missing(patch_ansible_mod):     # pylint: disable=unused-argument
162    ''' required arguments are reported as errors '''
163    with pytest.raises(AnsibleFailJson) as exc:
164        set_module_args({})
165        qtree_module()
166    print('Info: %s' % exc.value.args[0]['msg'])
167
168
169def test_ensure_get_called():
170    ''' test get_qtree for non-existent qtree'''
171    set_module_args(set_default_args(use_rest='Never'))
172    print('starting')
173    my_obj = qtree_module()
174    print('use_rest:', my_obj.use_rest)
175    my_obj.server = MockONTAPConnection()
176    assert my_obj.get_qtree is not None
177
178
179def test_ensure_get_called_existing():
180    ''' test get_qtree for existing qtree'''
181    set_module_args(set_default_args(use_rest='Never'))
182    my_obj = qtree_module()
183    my_obj.server = MockONTAPConnection(kind='qtree')
184    assert my_obj.get_qtree()
185
186
187@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree.NetAppOntapQTree.create_qtree')
188def test_successful_create(create_qtree, patch_ansible_mod):    # pylint: disable=unused-argument
189    ''' creating qtree and testing idempotency '''
190    set_module_args(set_default_args(use_rest='Never'))
191    my_obj = qtree_module()
192    if not ONBOX:
193        my_obj.server = MockONTAPConnection()
194    with pytest.raises(AnsibleExitJson) as exc:
195        my_obj.apply()
196    assert exc.value.args[0]['changed']
197    create_qtree.assert_called_with()
198    # to reset na_helper from remembering the previous 'changed' value
199    set_module_args(set_default_args(use_rest='Never'))
200    my_obj = qtree_module()
201    if not ONBOX:
202        my_obj.server = MockONTAPConnection('qtree')
203    with pytest.raises(AnsibleExitJson) as exc:
204        my_obj.apply()
205    assert not exc.value.args[0]['changed']
206
207
208@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree.NetAppOntapQTree.delete_qtree')
209def test_successful_delete(delete_qtree, patch_ansible_mod):    # pylint: disable=unused-argument
210    ''' deleting qtree and testing idempotency '''
211    data = set_default_args(use_rest='Never')
212    data['state'] = 'absent'
213    set_module_args(data)
214    my_obj = qtree_module()
215    if not ONBOX:
216        my_obj.server = MockONTAPConnection('qtree')
217    with pytest.raises(AnsibleExitJson) as exc:
218        my_obj.apply()
219    assert exc.value.args[0]['changed']
220    # delete_qtree.assert_called_with()
221    # to reset na_helper from remembering the previous 'changed' value
222    my_obj = qtree_module()
223    if not ONBOX:
224        my_obj.server = MockONTAPConnection()
225    with pytest.raises(AnsibleExitJson) as exc:
226        my_obj.apply()
227    assert not exc.value.args[0]['changed']
228
229
230@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree.NetAppOntapQTree.modify_qtree')
231def test_successful_modify(modify_qtree, patch_ansible_mod):    # pylint: disable=unused-argument
232    ''' modifying qtree and testing idempotency '''
233    data = set_default_args(use_rest='Never')
234    data['export_policy'] = 'test'
235    set_module_args(data)
236    my_obj = qtree_module()
237    if not ONBOX:
238        my_obj.server = MockONTAPConnection('qtree')
239    with pytest.raises(AnsibleExitJson) as exc:
240        my_obj.apply()
241    assert exc.value.args[0]['changed']
242    # modify_qtree.assert_called_with()
243    # to reset na_helper from remembering the previous 'changed' value
244    data['export_policy'] = 'ansible'
245    set_module_args(data)
246    my_obj = qtree_module()
247    if not ONBOX:
248        my_obj.server = MockONTAPConnection('qtree')
249    with pytest.raises(AnsibleExitJson) as exc:
250        my_obj.apply()
251    assert not exc.value.args[0]['changed']
252
253
254@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree.NetAppOntapQTree.get_qtree')
255@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree.NetAppOntapQTree.rename_qtree')
256def test_failed_rename(rename_qtree, get_qtree, patch_ansible_mod):     # pylint: disable=unused-argument
257    ''' creating qtree and testing idempotency '''
258    get_qtree.side_effect = [None, None]
259    data = set_default_args(use_rest='Never')
260    data['from_name'] = 'ansible_old'
261    set_module_args(data)
262    my_obj = qtree_module()
263    if not ONBOX:
264        my_obj.server = MockONTAPConnection()
265    with pytest.raises(AnsibleFailJson) as exc:
266        my_obj.apply()
267    msg = 'Error renaming: qtree %s does not exist' % data['from_name']
268    assert msg in exc.value.args[0]['msg']
269
270
271@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree.NetAppOntapQTree.get_qtree')
272@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree.NetAppOntapQTree.rename_qtree')
273def test_successful_rename(rename_qtree, get_qtree, patch_ansible_mod):     # pylint: disable=unused-argument
274    ''' creating qtree and testing idempotency '''
275    data = set_default_args(use_rest='Never')
276    data['from_name'] = 'ansible_old'
277    qtree = dict(
278        security_style=data['security_style'],
279        unix_permissions=data['unix_permissions'],
280        export_policy=data['export_policy']
281    )
282    get_qtree.side_effect = [None, qtree]
283    set_module_args(data)
284    my_obj = qtree_module()
285    if not ONBOX:
286        my_obj.server = MockONTAPConnection()
287    with pytest.raises(AnsibleExitJson) as exc:
288        my_obj.apply()
289    assert exc.value.args[0]['changed']
290    rename_qtree.assert_called_with()
291    # Idempotency
292    get_qtree.side_effect = [qtree, 'whatever']
293    my_obj = qtree_module()
294    if not ONBOX:
295        my_obj.server = MockONTAPConnection('qtree')
296    with pytest.raises(AnsibleExitJson) as exc:
297        my_obj.apply()
298    assert not exc.value.args[0]['changed']
299
300
301def test_if_all_methods_catch_exception(patch_ansible_mod):     # pylint: disable=unused-argument
302    data = set_default_args(use_rest='Never')
303    data['from_name'] = 'ansible'
304    set_module_args(data)
305    my_obj = qtree_module()
306    if not ONBOX:
307        my_obj.server = MockONTAPConnection('qtree_fail')
308    with pytest.raises(AnsibleFailJson) as exc:
309        my_obj.create_qtree()
310    assert 'Error provisioning qtree ' in exc.value.args[0]['msg']
311    with pytest.raises(AnsibleFailJson) as exc:
312        my_obj.delete_qtree(get_qtree_mock_object())
313    assert 'Error deleting qtree ' in exc.value.args[0]['msg']
314    with pytest.raises(AnsibleFailJson) as exc:
315        my_obj.modify_qtree(get_qtree_mock_object())
316    assert 'Error modifying qtree ' in exc.value.args[0]['msg']
317    with pytest.raises(AnsibleFailJson) as exc:
318        my_obj.rename_qtree()
319    assert 'Error renaming qtree ' in exc.value.args[0]['msg']
320
321
322@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
323def test_rest_error(mock_request, patch_ansible_mod):   # pylint: disable=unused-argument
324    data = set_default_args()
325    set_module_args(data)
326    mock_request.side_effect = [
327        SRR['is_rest'],
328        SRR['generic_error'],
329        SRR['end_of_sequence']
330    ]
331    with pytest.raises(AnsibleFailJson) as exc:
332        get_qtree_mock_object(cx_type='rest').apply()
333    assert exc.value.args[0]['msg'] == 'Error in get_qtree: calling: storage/qtrees: got %s.' % SRR['generic_error'][2]
334
335
336@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
337def test_successful_create_rest(mock_request, patch_ansible_mod):   # pylint: disable=unused-argument
338    data = set_default_args()
339    set_module_args(data)
340    mock_request.side_effect = [
341        SRR['is_rest'],
342        SRR['no_record'],   # get
343        SRR['empty_good'],  # post
344        SRR['end_of_sequence']
345    ]
346    with pytest.raises(AnsibleExitJson) as exc:
347        get_qtree_mock_object(cx_type='rest').apply()
348    assert exc.value.args[0]['changed']
349
350
351@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
352def test_idempotent_create_rest(mock_request, patch_ansible_mod):   # pylint: disable=unused-argument
353    data = set_default_args()
354    set_module_args(data)
355    mock_request.side_effect = [
356        SRR['is_rest'],
357        SRR['qtree_record'],  # get
358        SRR['end_of_sequence']
359    ]
360    with pytest.raises(AnsibleExitJson) as exc:
361        get_qtree_mock_object(cx_type='rest').apply()
362    assert not exc.value.args[0]['changed']
363
364
365@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
366def test_successful_delete_rest(mock_request, patch_ansible_mod):   # pylint: disable=unused-argument
367    data = set_default_args()
368    data['state'] = 'absent'
369    set_module_args(data)
370    mock_request.side_effect = [
371        SRR['is_rest'],
372        SRR['qtree_record'],  # get
373        SRR['empty_good'],    # delete
374        SRR['end_of_sequence']
375    ]
376    with pytest.raises(AnsibleExitJson) as exc:
377        get_qtree_mock_object(cx_type='rest').apply()
378    assert exc.value.args[0]['changed']
379
380
381@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
382def test_idempotent_delete_rest(mock_request, patch_ansible_mod):   # pylint: disable=unused-argument
383    data = set_default_args()
384    data['state'] = 'absent'
385    set_module_args(data)
386    mock_request.side_effect = [
387        SRR['is_rest'],
388        SRR['no_record'],  # get
389        SRR['end_of_sequence']
390    ]
391    with pytest.raises(AnsibleExitJson) as exc:
392        get_qtree_mock_object(cx_type='rest').apply()
393    assert not exc.value.args[0]['changed']
394
395
396@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
397def test_successful_modify_rest(mock_request, patch_ansible_mod):   # pylint: disable=unused-argument
398    data = set_default_args()
399    data['state'] = 'present'
400    data['unix_permissions'] = 'abcde'
401    set_module_args(data)
402    mock_request.side_effect = [
403        SRR['is_rest'],
404        SRR['qtree_record'],  # get
405        SRR['empty_good'],    # patch
406        SRR['end_of_sequence']
407    ]
408    with pytest.raises(AnsibleExitJson) as exc:
409        get_qtree_mock_object(cx_type='rest').apply()
410    assert exc.value.args[0]['changed']
411
412
413@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
414def test_idempotent_modify_rest(mock_request, patch_ansible_mod):   # pylint: disable=unused-argument
415    data = set_default_args()
416    data['state'] = 'present'
417    set_module_args(data)
418    mock_request.side_effect = [
419        SRR['is_rest'],
420        SRR['qtree_record'],  # get
421        SRR['end_of_sequence']
422    ]
423    with pytest.raises(AnsibleExitJson) as exc:
424        get_qtree_mock_object(cx_type='rest').apply()
425    assert not exc.value.args[0]['changed']
426
427
428@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
429def test_successful_rename_rest(mock_request, patch_ansible_mod):   # pylint: disable=unused-argument
430    data = set_default_args()
431    data['state'] = 'present'
432    data['from_name'] = 'abcde'
433    # data['unix_permissions'] = 'abcde'
434    set_module_args(data)
435    mock_request.side_effect = [
436        SRR['is_rest'],
437        SRR['no_record'],     # get (current)
438        SRR['qtree_record'],  # get (from)
439        SRR['empty_good'],    # patch
440        SRR['end_of_sequence']
441    ]
442    with pytest.raises(AnsibleExitJson) as exc:
443        get_qtree_mock_object(cx_type='rest').apply()
444    assert exc.value.args[0]['changed']
445
446
447@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
448def test_successful_rename_rest_idempotent(mock_request, patch_ansible_mod):    # pylint: disable=unused-argument
449    data = set_default_args()
450    data['state'] = 'present'
451    data['from_name'] = 'abcde'
452    # data['unix_permissions'] = 'abcde'
453    set_module_args(data)
454    mock_request.side_effect = [
455        SRR['is_rest'],
456        SRR['qtree_record'],  # get (current exists)
457        SRR['no_record'],    # patch
458        SRR['end_of_sequence']
459    ]
460    with pytest.raises(AnsibleExitJson) as exc:
461        get_qtree_mock_object(cx_type='rest').apply()
462    assert not exc.value.args[0]['changed']
463
464
465@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
466def test_successful_rename_and_modify_rest(mock_request, patch_ansible_mod):    # pylint: disable=unused-argument
467    data = set_default_args()
468    data['state'] = 'present'
469    data['from_name'] = 'abcde'
470    data['unix_permissions'] = 'abcde'
471    set_module_args(data)
472    mock_request.side_effect = [
473        SRR['is_rest'],
474        SRR['no_record'],     # get (current)
475        SRR['qtree_record'],  # get (from)
476        SRR['empty_good'],    # patch (modify, including name change)
477        SRR['end_of_sequence']
478    ]
479    with pytest.raises(AnsibleExitJson) as exc:
480        get_qtree_mock_object(cx_type='rest').apply()
481    assert exc.value.args[0]['changed']
482