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