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