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 tests for Ansible module: na_ontap_aggregate """ 5 6from __future__ import (absolute_import, division, print_function) 7__metaclass__ = type 8import json 9import pytest 10 11from ansible.module_utils import basic 12from ansible.module_utils._text import to_bytes 13from ansible_collections.netapp.ontap.tests.unit.compat import unittest 14from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock 15import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils 16 17from ansible_collections.netapp.ontap.plugins.modules.na_ontap_aggregate \ 18 import NetAppOntapAggregate as my_module # module under test 19 20if not netapp_utils.has_netapp_lib(): 21 pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') 22 23 24def set_module_args(args): 25 """prepare arguments so that they will be picked up during module creation""" 26 args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) 27 basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access 28 29 30class AnsibleExitJson(Exception): 31 """Exception class to be raised by module.exit_json and caught by the test case""" 32 33 34class AnsibleFailJson(Exception): 35 """Exception class to be raised by module.fail_json and caught by the test case""" 36 37 38def exit_json(*args, **kwargs): # pylint: disable=unused-argument 39 """function to patch over exit_json; package return data into an exception""" 40 if 'changed' not in kwargs: 41 kwargs['changed'] = False 42 raise AnsibleExitJson(kwargs) 43 44 45def fail_json(*args, **kwargs): # pylint: disable=unused-argument 46 """function to patch over fail_json; package return data into an exception""" 47 kwargs['failed'] = True 48 raise AnsibleFailJson(kwargs) 49 50 51AGGR_NAME = 'aggr_name' 52OS_NAME = 'abc' 53 54 55class MockONTAPConnection(object): 56 ''' mock server connection to ONTAP host ''' 57 58 def __init__(self, kind=None, parm1=None, parm2=None): 59 ''' save arguments ''' 60 self.type = kind 61 self.parm1 = parm1 62 self.parm2 = parm2 63 self.xml_in = None 64 self.xml_out = None 65 self.zapis = list() 66 67 def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument 68 ''' mock invoke_successfully returning xml data ''' 69 self.xml_in = xml 70 print('request:', xml.to_string()) 71 zapi = xml.get_name() 72 self.zapis.append(zapi) 73 if zapi == 'aggr-object-store-get-iter': 74 if self.type in ('aggregate_no_object_store',): 75 xml = None 76 else: 77 xml = self.build_object_store_info() 78 elif self.type in ('aggregate', 'aggr_disks', 'aggr_mirrors', 'aggregate_no_object_store'): 79 with_os = self.type != 'aggregate_no_object_store' 80 xml = self.build_aggregate_info(self.parm1, self.parm2, with_object_store=with_os) 81 if self.type in ('aggr_disks', 'aggr_mirrors'): 82 self.type = 'disks' 83 elif self.type == 'no_aggregate': 84 xml = None 85 elif self.type == 'no_aggregate_then_aggregate': 86 xml = None 87 self.type = 'aggregate' 88 elif self.type == 'disks': 89 xml = self.build_disk_info() 90 elif self.type == 'aggregate_fail': 91 raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") 92 self.xml_out = xml 93 return xml 94 95 @staticmethod 96 def build_aggregate_info(vserver, aggregate, with_object_store): 97 ''' build xml data for aggregate and vserser-info ''' 98 xml = netapp_utils.zapi.NaElement('xml') 99 data = {'num-records': 3, 100 'attributes-list': 101 {'aggr-attributes': 102 {'aggregate-name': aggregate, 103 'aggr-raid-attributes': {'state': 'offline'} 104 }, 105 'object-store-information': {'object-store-name': 'abc'} 106 }, 107 'vserver-info': 108 {'vserver-name': vserver 109 } 110 } 111 if not with_object_store: 112 del data['attributes-list']['object-store-information'] 113 xml.translate_struct(data) 114 print(xml.to_string()) 115 return xml 116 117 @staticmethod 118 def build_object_store_info(): 119 ''' build xml data for object_store ''' 120 xml = netapp_utils.zapi.NaElement('xml') 121 data = {'num-records': 3, 122 'attributes-list': 123 {'object-store-information': {'object-store-name': 'abc'} 124 } 125 } 126 xml.translate_struct(data) 127 print(xml.to_string()) 128 return xml 129 130 @staticmethod 131 def build_disk_info(): 132 ''' build xml data for disk ''' 133 xml = netapp_utils.zapi.NaElement('xml') 134 data = {'num-records': 1, 135 'attributes-list': [ 136 {'disk-info': 137 {'disk-name': '1', 138 'disk-raid-info': 139 {'disk-aggregate-info': 140 {'plex-name': 'plex0'} 141 }}}, 142 {'disk-info': 143 {'disk-name': '2', 144 'disk-raid-info': 145 {'disk-aggregate-info': 146 {'plex-name': 'plex0'} 147 }}}, 148 {'disk-info': 149 {'disk-name': '3', 150 'disk-raid-info': 151 {'disk-aggregate-info': 152 {'plex-name': 'plexM'} 153 }}}, 154 {'disk-info': 155 {'disk-name': '4', 156 'disk-raid-info': 157 {'disk-aggregate-info': 158 {'plex-name': 'plexM'} 159 }}}, 160 ]} 161 xml.translate_struct(data) 162 print(xml.to_string()) 163 return xml 164 165 166class TestMyModule(unittest.TestCase): 167 ''' a group of related Unit Tests ''' 168 169 def setUp(self): 170 self.mock_module_helper = patch.multiple(basic.AnsibleModule, 171 exit_json=exit_json, 172 fail_json=fail_json) 173 self.mock_module_helper.start() 174 self.addCleanup(self.mock_module_helper.stop) 175 self.server = MockONTAPConnection('aggregate', '12', 'name') 176 # whether to use a mock or a simulator 177 self.onbox = False 178 self.zapis = list() 179 180 def set_default_args(self): 181 if self.onbox: 182 hostname = '10.193.74.78' 183 username = 'admin' 184 password = 'netapp1!' 185 name = 'name' 186 else: 187 hostname = 'hostname' 188 username = 'username' 189 password = 'password' 190 name = AGGR_NAME 191 return dict({ 192 'hostname': hostname, 193 'username': username, 194 'password': password, 195 'name': name 196 }) 197 198 def call_command(self, module_args, what=None): 199 ''' utility function to call apply ''' 200 args = dict(self.set_default_args()) 201 args.update(module_args) 202 set_module_args(args) 203 my_obj = my_module() 204 my_obj.asup_log_for_cserver = Mock(return_value=None) 205 aggregate = 'aggregate' 206 if what == 'disks': 207 aggregate = 'aggr_disks' 208 elif what == 'mirrors': 209 aggregate = 'aggr_mirrors' 210 elif what is not None: 211 aggregate = what 212 213 if not self.onbox: 214 # mock the connection 215 my_obj.server = MockONTAPConnection(aggregate, '12', AGGR_NAME) 216 self.zapis = my_obj.server.zapis 217 with pytest.raises(AnsibleExitJson) as exc: 218 my_obj.apply() 219 return exc.value.args[0]['changed'] 220 221 def test_module_fail_when_required_args_missing(self): 222 ''' required arguments are reported as errors ''' 223 with pytest.raises(AnsibleFailJson) as exc: 224 set_module_args({}) 225 my_module() 226 print('Info: %s' % exc.value.args[0]['msg']) 227 228 def test_create(self): 229 module_args = { 230 'disk_count': '2', 231 'is_mirrored': 'true', 232 } 233 changed = self.call_command(module_args, what='no_aggregate') 234 assert changed 235 assert 'aggr-object-store-attach' not in self.zapis 236 237 def test_create_with_object_store(self): 238 module_args = { 239 'disk_count': '2', 240 'is_mirrored': 'true', 241 'object_store_name': 'abc' 242 } 243 changed = self.call_command(module_args, what='no_aggregate') 244 assert changed 245 assert 'aggr-object-store-attach' in self.zapis 246 247 def test_is_mirrored(self): 248 module_args = { 249 'disk_count': '2', 250 'is_mirrored': 'true', 251 } 252 changed = self.call_command(module_args) 253 assert not changed 254 255 def test_disks_list(self): 256 module_args = { 257 'disks': ['1', '2'], 258 } 259 changed = self.call_command(module_args, 'disks') 260 assert not changed 261 262 def test_mirror_disks(self): 263 module_args = { 264 'disks': ['1', '2'], 265 'mirror_disks': ['3', '4'] 266 } 267 changed = self.call_command(module_args, 'mirrors') 268 assert not changed 269 270 def test_spare_pool(self): 271 module_args = { 272 'disk_count': '2', 273 'spare_pool': 'Pool1' 274 } 275 changed = self.call_command(module_args) 276 assert not changed 277 278 def test_rename(self): 279 module_args = { 280 'from_name': 'test_name2' 281 } 282 changed = self.call_command(module_args, 'no_aggregate_then_aggregate') 283 assert changed 284 assert 'aggr-rename' in self.zapis 285 286 def test_rename_error_no_from(self): 287 module_args = { 288 'from_name': 'test_name2' 289 } 290 with pytest.raises(AnsibleFailJson) as exc: 291 self.call_command(module_args, 'no_aggregate') 292 msg = 'Error renaming: aggregate %s does not exist' % module_args['from_name'] 293 assert msg in exc.value.args[0]['msg'] 294 295 def test_rename_with_add_object_store(self): 296 module_args = { 297 'from_name': 'test_name2' 298 } 299 changed = self.call_command(module_args, 'aggregate_no_object_store') 300 assert not changed 301 302 def test_object_store_present(self): 303 module_args = { 304 'object_store_name': 'abc' 305 } 306 changed = self.call_command(module_args) 307 assert not changed 308 309 def test_object_store_create(self): 310 module_args = { 311 'object_store_name': 'abc' 312 } 313 changed = self.call_command(module_args, 'aggregate_no_object_store') 314 assert changed 315 316 def test_object_store_modify(self): 317 ''' not supported ''' 318 module_args = { 319 'object_store_name': 'def' 320 } 321 with pytest.raises(AnsibleFailJson) as exc: 322 self.call_command(module_args) 323 msg = 'Error: object store %s is already associated with aggregate %s.' % (OS_NAME, AGGR_NAME) 324 assert msg in exc.value.args[0]['msg'] 325 326 def test_if_all_methods_catch_exception(self): 327 module_args = {} 328 module_args.update(self.set_default_args()) 329 module_args.update({'service_state': 'online'}) 330 module_args.update({'unmount_volumes': 'True'}) 331 module_args.update({'from_name': 'test_name2'}) 332 set_module_args(module_args) 333 my_obj = my_module() 334 if not self.onbox: 335 my_obj.server = MockONTAPConnection('aggregate_fail') 336 with pytest.raises(AnsibleFailJson) as exc: 337 my_obj.aggr_get_iter(module_args.get('name')) 338 assert '' in exc.value.args[0]['msg'] 339 with pytest.raises(AnsibleFailJson) as exc: 340 my_obj.aggregate_online() 341 assert 'Error changing the state of aggregate' in exc.value.args[0]['msg'] 342 with pytest.raises(AnsibleFailJson) as exc: 343 my_obj.aggregate_offline() 344 assert 'Error changing the state of aggregate' in exc.value.args[0]['msg'] 345 with pytest.raises(AnsibleFailJson) as exc: 346 my_obj.create_aggr() 347 assert 'Error provisioning aggregate' in exc.value.args[0]['msg'] 348 with pytest.raises(AnsibleFailJson) as exc: 349 my_obj.delete_aggr() 350 assert 'Error removing aggregate' in exc.value.args[0]['msg'] 351 with pytest.raises(AnsibleFailJson) as exc: 352 my_obj.rename_aggregate() 353 assert 'Error renaming aggregate' in exc.value.args[0]['msg'] 354 with pytest.raises(AnsibleFailJson) as exc: 355 my_obj.asup_log_for_cserver = Mock(return_value=None) 356 my_obj.apply() 357 assert 'TEST:This exception is from the unit test' in exc.value.args[0]['msg'] 358 359 def test_disks_bad_mapping(self): 360 module_args = { 361 'disks': ['0'], 362 } 363 with pytest.raises(AnsibleFailJson) as exc: 364 self.call_command(module_args, 'mirrors') 365 msg = "Error mapping disks for aggregate %s: cannot not match disks with current aggregate disks." % AGGR_NAME 366 assert exc.value.args[0]['msg'].startswith(msg) 367 368 def test_disks_overlapping_mirror(self): 369 module_args = { 370 'disks': ['1', '2', '3'], 371 } 372 with pytest.raises(AnsibleFailJson) as exc: 373 self.call_command(module_args, 'mirrors') 374 msg = "Error mapping disks for aggregate %s: found overlapping plexes:" % AGGR_NAME 375 assert exc.value.args[0]['msg'].startswith(msg) 376 377 def test_disks_removing_disk(self): 378 module_args = { 379 'disks': ['1'], 380 } 381 with pytest.raises(AnsibleFailJson) as exc: 382 self.call_command(module_args, 'mirrors') 383 msg = "Error removing disks is not supported. Aggregate %s: these disks cannot be removed: ['2']." % AGGR_NAME 384 assert exc.value.args[0]['msg'].startswith(msg) 385 386 def test_disks_removing_mirror_disk(self): 387 module_args = { 388 'disks': ['1', '2'], 389 'mirror_disks': ['4', '6'] 390 } 391 with pytest.raises(AnsibleFailJson) as exc: 392 self.call_command(module_args, 'mirrors') 393 msg = "Error removing disks is not supported. Aggregate %s: these disks cannot be removed: ['3']." % AGGR_NAME 394 assert exc.value.args[0]['msg'].startswith(msg) 395 396 def test_disks_add(self): 397 module_args = { 398 'disks': ['1', '2', '5'], 399 } 400 changed = self.call_command(module_args, 'disks') 401 assert changed 402 403 def test_mirror_disks_add(self): 404 module_args = { 405 'disks': ['1', '2', '5'], 406 'mirror_disks': ['3', '4', '6'] 407 } 408 changed = self.call_command(module_args, 'mirrors') 409 assert changed 410 411 def test_mirror_disks_add_unbalanced(self): 412 module_args = { 413 'disks': ['1', '2'], 414 'mirror_disks': ['3', '4', '6'] 415 } 416 with pytest.raises(AnsibleFailJson) as exc: 417 self.call_command(module_args, 'mirrors') 418 msg = "Error cannot add mirror disks ['6'] without adding disks for aggregate %s." % AGGR_NAME 419 assert exc.value.args[0]['msg'].startswith(msg) 420