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