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