1''' unit tests ONTAP Ansible module: na_ontap_storage_failover '''
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 import unittest
10from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock
11import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
12
13from ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_failover \
14    import NetAppOntapStorageFailover as storage_failover_module  # module under test
15
16
17if not netapp_utils.has_netapp_lib():
18    pytestmark = pytest.mark.skip('skipping as missing required netapp_lib')
19
20# REST API canned responses when mocking send_request
21SRR = {
22    # common responses
23    'is_rest': (200, {}, None),
24    'is_zapi': (400, {}, "Unreachable"),
25    'empty_good': (200, {}, None),
26    'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"),
27    'generic_error': (400, None, "Expected error"),
28    # module specific responses
29    'storage_failover_enabled_record': (200, {
30        'num_records': 1,
31        'records': [{
32            'uuid': '56ab5d21-312a-11e8-9166-9d4fc452db4e',
33            'ha': {
34                'enabled': True
35            }
36        }]
37    }, None),
38    'storage_failover_disabled_record': (200, {
39        'num_records': 1,
40        "records": [{
41            'uuid': '56ab5d21-312a-11e8-9166-9d4fc452db4e',
42            'ha': {
43                'enabled': False
44            }
45        }]
46    }, None)
47}
48
49
50def set_module_args(args):
51    """prepare arguments so that they will be picked up during module creation"""
52    args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
53    basic._ANSIBLE_ARGS = to_bytes(args)  # pylint: disable=protected-access
54
55
56class AnsibleExitJson(Exception):
57    """Exception class to be raised by module.exit_json and caught by the test case"""
58
59
60class AnsibleFailJson(Exception):
61    """Exception class to be raised by module.fail_json and caught by the test case"""
62
63
64def exit_json(*args, **kwargs):  # pylint: disable=unused-argument
65    """function to patch over exit_json; package return data into an exception"""
66    if 'changed' not in kwargs:
67        kwargs['changed'] = False
68    raise AnsibleExitJson(kwargs)
69
70
71def fail_json(*args, **kwargs):  # pylint: disable=unused-argument
72    """function to patch over fail_json; package return data into an exception"""
73    kwargs['failed'] = True
74    raise AnsibleFailJson(kwargs)
75
76
77class MockONTAPConnection(object):
78    ''' mock server connection to ONTAP host '''
79
80    def __init__(self, kind=None):
81        ''' save arguments '''
82        self.type = kind
83        self.xml_in = None
84        self.xml_out = None
85
86    def invoke_successfully(self, xml, enable_tunneling):  # pylint: disable=unused-argument
87        ''' mock invoke_successfully returning xml data '''
88        self.xml_in = xml
89        if self.type == 'storage_failover_enabled':
90            xml = self.build_storage_failover_enabled_info()
91        elif self.type == 'storage_failover_disabled':
92            xml = self.build_storage_failover_disabled_info()
93        elif self.type == 'storage_failover_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_storage_failover_enabled_info():
100        ''' build xml data for cf-status '''
101        xml = netapp_utils.zapi.NaElement('xml')
102        data = {
103            'is-enabled': 'true'
104        }
105
106        xml.translate_struct(data)
107        return xml
108
109    @staticmethod
110    def build_storage_failover_disabled_info():
111        ''' build xml data for cf-status '''
112        xml = netapp_utils.zapi.NaElement('xml')
113        data = {
114            'is-enabled': 'false'
115        }
116
117        xml.translate_struct(data)
118        return xml
119
120
121class TestMyModule(unittest.TestCase):
122    ''' a group of related Unit Tests '''
123
124    def setUp(self):
125        self.mock_module_helper = patch.multiple(basic.AnsibleModule,
126                                                 exit_json=exit_json,
127                                                 fail_json=fail_json)
128        self.mock_module_helper.start()
129        self.addCleanup(self.mock_module_helper.stop)
130        self.server = MockONTAPConnection()
131        self.onbox = False
132
133    def set_default_args(self, use_rest=None):
134        if self.onbox:
135            hostname = '10.10.10.10'
136            username = 'username'
137            password = 'password'
138            node_name = 'node1'
139        else:
140            hostname = '10.10.10.10'
141            username = 'username'
142            password = 'password'
143            node_name = 'node1'
144
145        args = dict({
146            'state': 'present',
147            'hostname': hostname,
148            'username': username,
149            'password': password,
150            'node_name': node_name
151        })
152
153        if use_rest is not None:
154            args['use_rest'] = use_rest
155
156        return args
157
158    @staticmethod
159    def get_storage_failover_mock_object(cx_type='zapi', kind=None):
160        storage_failover_obj = storage_failover_module()
161        if cx_type == 'zapi':
162            if kind is None:
163                storage_failover_obj.server = MockONTAPConnection()
164            else:
165                storage_failover_obj.server = MockONTAPConnection(kind=kind)
166        return storage_failover_obj
167
168    def test_module_fail_when_required_args_missing(self):
169        ''' required arguments are reported as errors '''
170        with pytest.raises(AnsibleFailJson) as exc:
171            set_module_args({})
172            storage_failover_module()
173        print('Info: %s' % exc.value.args[0]['msg'])
174
175    def test_ensure_get_called_existing(self):
176        ''' test get_storage_failover for existing config '''
177        set_module_args(self.set_default_args(use_rest='Never'))
178        my_obj = storage_failover_module()
179        my_obj.server = MockONTAPConnection(kind='storage_failover_enabled')
180        assert my_obj.get_storage_failover()
181
182    @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_failover.NetAppOntapStorageFailover.modify_storage_failover')
183    def test_successful_enable(self, modify_storage_failover):
184        ''' enable storage_failover and testing idempotency '''
185        set_module_args(self.set_default_args(use_rest='Never'))
186        my_obj = storage_failover_module()
187        my_obj.ems_log_event = Mock(return_value=None)
188        if not self.onbox:
189            my_obj.server = MockONTAPConnection('storage_failover_disabled')
190        with pytest.raises(AnsibleExitJson) as exc:
191            my_obj.apply()
192        assert exc.value.args[0]['changed']
193        modify_storage_failover.assert_called_with({'is_enabled': False})
194        # to reset na_helper from remembering the previous 'changed' value
195        set_module_args(self.set_default_args(use_rest='Never'))
196        my_obj = storage_failover_module()
197        my_obj.ems_log_event = Mock(return_value=None)
198        if not self.onbox:
199            my_obj.server = MockONTAPConnection('storage_failover_enabled')
200        with pytest.raises(AnsibleExitJson) as exc:
201            my_obj.apply()
202        assert not exc.value.args[0]['changed']
203
204    @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_failover.NetAppOntapStorageFailover.modify_storage_failover')
205    def test_successful_disable(self, modify_storage_failover):
206        ''' disable storage_failover and testing idempotency '''
207        data = self.set_default_args(use_rest='Never')
208        data['state'] = 'absent'
209        set_module_args(data)
210        my_obj = storage_failover_module()
211        my_obj.ems_log_event = Mock(return_value=None)
212        if not self.onbox:
213            my_obj.server = MockONTAPConnection('storage_failover_enabled')
214        with pytest.raises(AnsibleExitJson) as exc:
215            my_obj.apply()
216        assert exc.value.args[0]['changed']
217        modify_storage_failover.assert_called_with({'is_enabled': True})
218        # to reset na_helper from remembering the previous 'changed' value
219        my_obj = storage_failover_module()
220        my_obj.ems_log_event = Mock(return_value=None)
221        if not self.onbox:
222            my_obj.server = MockONTAPConnection('storage_failover_disabled')
223        with pytest.raises(AnsibleExitJson) as exc:
224            my_obj.apply()
225        assert not exc.value.args[0]['changed']
226
227    def test_if_all_methods_catch_exception(self):
228        data = self.set_default_args(use_rest='Never')
229        set_module_args(data)
230        my_obj = storage_failover_module()
231        if not self.onbox:
232            my_obj.server = MockONTAPConnection('storage_failover_fail')
233        with pytest.raises(AnsibleFailJson) as exc:
234            my_obj.modify_storage_failover(self.get_storage_failover_mock_object())
235        assert 'Error modifying storage failover' in exc.value.args[0]['msg']
236
237    @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
238    def test_rest_error(self, mock_request):
239        data = self.set_default_args()
240        set_module_args(data)
241        mock_request.side_effect = [
242            SRR['is_rest'],
243            SRR['generic_error'],
244            SRR['end_of_sequence']
245        ]
246        with pytest.raises(AnsibleFailJson) as exc:
247            self.get_storage_failover_mock_object(cx_type='rest').apply()
248        assert SRR['generic_error'][2] in exc.value.args[0]['msg']
249
250    @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
251    def test_successful_enabled_rest(self, mock_request):
252        data = self.set_default_args()
253        set_module_args(data)
254        mock_request.side_effect = [
255            SRR['is_rest'],
256            SRR['storage_failover_disabled_record'],  # get
257            SRR['empty_good'],  # patch
258            SRR['end_of_sequence']
259        ]
260        with pytest.raises(AnsibleExitJson) as exc:
261            self.get_storage_failover_mock_object(cx_type='rest').apply()
262        assert exc.value.args[0]['changed']
263
264    @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
265    def test_idempotent_enabled_rest(self, mock_request):
266        data = self.set_default_args()
267        set_module_args(data)
268        mock_request.side_effect = [
269            SRR['is_rest'],
270            SRR['storage_failover_enabled_record'],  # get
271            SRR['end_of_sequence']
272        ]
273        with pytest.raises(AnsibleExitJson) as exc:
274            self.get_storage_failover_mock_object(cx_type='rest').apply()
275        assert not exc.value.args[0]['changed']
276
277    @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
278    def test_successful_disabled_rest(self, mock_request):
279        data = self.set_default_args()
280        data['state'] = 'absent'
281        set_module_args(data)
282        mock_request.side_effect = [
283            SRR['is_rest'],
284            SRR['storage_failover_enabled_record'],  # get
285            SRR['empty_good'],  # patch
286            SRR['end_of_sequence']
287        ]
288        with pytest.raises(AnsibleExitJson) as exc:
289            self.get_storage_failover_mock_object(cx_type='rest').apply()
290        assert exc.value.args[0]['changed']
291
292    @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
293    def test_idempotent_disabled_rest(self, mock_request):
294        data = self.set_default_args()
295        data['state'] = 'absent'
296        set_module_args(data)
297        mock_request.side_effect = [
298            SRR['is_rest'],
299            SRR['storage_failover_disabled_record'],  # get
300            SRR['end_of_sequence']
301        ]
302        with pytest.raises(AnsibleExitJson) as exc:
303            self.get_storage_failover_mock_object(cx_type='rest').apply()
304        assert not exc.value.args[0]['changed']
305