diff options
Diffstat (limited to 'ansible_collections/netapp/ontap/tests')
206 files changed, 68090 insertions, 0 deletions
diff --git a/ansible_collections/netapp/ontap/tests/sanity/ignore-2.10.txt b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.10.txt new file mode 100644 index 000000000..f4539e2ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.10.txt @@ -0,0 +1,4 @@ +plugins/modules/na_ontap_autosupport_invoke.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_login_messages.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_motd.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_nfs.py validate-modules:parameter-invalid diff --git a/ansible_collections/netapp/ontap/tests/sanity/ignore-2.11.txt b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.11.txt new file mode 100644 index 000000000..f4539e2ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.11.txt @@ -0,0 +1,4 @@ +plugins/modules/na_ontap_autosupport_invoke.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_login_messages.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_motd.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_nfs.py validate-modules:parameter-invalid diff --git a/ansible_collections/netapp/ontap/tests/sanity/ignore-2.12.txt b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.12.txt new file mode 100644 index 000000000..f4539e2ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.12.txt @@ -0,0 +1,4 @@ +plugins/modules/na_ontap_autosupport_invoke.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_login_messages.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_motd.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_nfs.py validate-modules:parameter-invalid diff --git a/ansible_collections/netapp/ontap/tests/sanity/ignore-2.13.txt b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.13.txt new file mode 100644 index 000000000..f4539e2ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.13.txt @@ -0,0 +1,4 @@ +plugins/modules/na_ontap_autosupport_invoke.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_login_messages.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_motd.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_nfs.py validate-modules:parameter-invalid diff --git a/ansible_collections/netapp/ontap/tests/sanity/ignore-2.14.txt b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.14.txt new file mode 100644 index 000000000..f4539e2ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.14.txt @@ -0,0 +1,4 @@ +plugins/modules/na_ontap_autosupport_invoke.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_login_messages.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_motd.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_nfs.py validate-modules:parameter-invalid diff --git a/ansible_collections/netapp/ontap/tests/sanity/ignore-2.15.txt b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.15.txt new file mode 100644 index 000000000..f4539e2ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.15.txt @@ -0,0 +1,4 @@ +plugins/modules/na_ontap_autosupport_invoke.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_login_messages.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_motd.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_nfs.py validate-modules:parameter-invalid diff --git a/ansible_collections/netapp/ontap/tests/sanity/ignore-2.16.txt b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.16.txt new file mode 100644 index 000000000..f4539e2ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.16.txt @@ -0,0 +1,4 @@ +plugins/modules/na_ontap_autosupport_invoke.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_login_messages.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_motd.py validate-modules:invalid-argument-name +plugins/modules/na_ontap_nfs.py validate-modules:parameter-invalid diff --git a/ansible_collections/netapp/ontap/tests/sanity/ignore-2.9.txt b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.9.txt new file mode 100644 index 000000000..5c626a030 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/sanity/ignore-2.9.txt @@ -0,0 +1 @@ +plugins/modules/na_ontap_nfs.py validate-modules:parameter-invalid
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/tests/unit/compat/__init__.py b/ansible_collections/netapp/ontap/tests/unit/compat/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/compat/__init__.py diff --git a/ansible_collections/netapp/ontap/tests/unit/compat/builtins.py b/ansible_collections/netapp/ontap/tests/unit/compat/builtins.py new file mode 100644 index 000000000..feef5d758 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/compat/builtins.py @@ -0,0 +1,34 @@ +# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +# +# Compat for python2.7 +# + +# One unittest needs to import builtins via __import__() so we need to have +# the string that represents it +try: + # pylint: disable=unused-import + import __builtin__ +except ImportError: + BUILTINS = 'builtins' +else: + BUILTINS = '__builtin__' diff --git a/ansible_collections/netapp/ontap/tests/unit/compat/mock.py b/ansible_collections/netapp/ontap/tests/unit/compat/mock.py new file mode 100644 index 000000000..0972cd2e8 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/compat/mock.py @@ -0,0 +1,122 @@ +# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python3.x's unittest.mock module +''' +import sys + +# Python 2.7 + +# Note: Could use the pypi mock library on python3.x as well as python2.x. It +# is the same as the python3 stdlib mock library + +try: + # Allow wildcard import because we really do want to import all of mock's + # symbols into this compat shim + # pylint: disable=wildcard-import,unused-wildcard-import + from unittest.mock import * +except ImportError: + # Python 2 + # pylint: disable=wildcard-import,unused-wildcard-import + try: + from mock import * + except ImportError: + print('You need the mock library installed on python2.x to run tests') + + +# Prior to 3.4.4, mock_open cannot handle binary read_data +if sys.version_info >= (3,) and sys.version_info < (3, 4, 4): + file_spec = None + + def _iterate_read_data(read_data): + # Helper for mock_open: + # Retrieve lines from read_data via a generator so that separate calls to + # readline, read, and readlines are properly interleaved + sep = b'\n' if isinstance(read_data, bytes) else '\n' + data_as_list = [l + sep for l in read_data.split(sep)] + + if data_as_list[-1] == sep: + # If the last line ended in a newline, the list comprehension will have an + # extra entry that's just a newline. Remove this. + data_as_list = data_as_list[:-1] + else: + # If there wasn't an extra newline by itself, then the file being + # emulated doesn't have a newline to end the last line remove the + # newline that our naive format() added + data_as_list[-1] = data_as_list[-1][:-1] + + for line in data_as_list: + yield line + + def mock_open(mock=None, read_data=''): + """ + A helper function to create a mock to replace the use of `open`. It works + for `open` called directly or used as a context manager. + + The `mock` argument is the mock object to configure. If `None` (the + default) then a `MagicMock` will be created for you, with the API limited + to methods or attributes available on standard file handles. + + `read_data` is a string for the `read` methoddline`, and `readlines` of the + file handle to return. This is an empty string by default. + """ + def _readlines_side_effect(*args, **kwargs): + if handle.readlines.return_value is not None: + return handle.readlines.return_value + return list(_data) + + def _read_side_effect(*args, **kwargs): + if handle.read.return_value is not None: + return handle.read.return_value + return type(read_data)().join(_data) + + def _readline_side_effect(): + if handle.readline.return_value is not None: + while True: + yield handle.readline.return_value + for line in _data: + yield line + + global file_spec + if file_spec is None: + import _io + file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) + + if mock is None: + mock = MagicMock(name='open', spec=open) + + handle = MagicMock(spec=file_spec) + handle.__enter__.return_value = handle + + _data = _iterate_read_data(read_data) + + handle.write.return_value = None + handle.read.return_value = None + handle.readline.return_value = None + handle.readlines.return_value = None + + handle.read.side_effect = _read_side_effect + handle.readline.side_effect = _readline_side_effect() + handle.readlines.side_effect = _readlines_side_effect + + mock.return_value = handle + return mock diff --git a/ansible_collections/netapp/ontap/tests/unit/compat/unittest.py b/ansible_collections/netapp/ontap/tests/unit/compat/unittest.py new file mode 100644 index 000000000..73a20cf8c --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/compat/unittest.py @@ -0,0 +1,44 @@ +# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +''' +Compat module for Python2.7's unittest module +''' + +import sys + +import pytest + +# Allow wildcard import because we really do want to import all of +# unittests's symbols into this compat shim +# pylint: disable=wildcard-import,unused-wildcard-import +if sys.version_info < (2, 7): + try: + # Need unittest2 on python2.6 + from unittest2 import * + except ImportError: + print('You need unittest2 installed on python2.6.x to run tests') + + class TestCase: + """ skip everything """ + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as unittest2 may not be available') +else: + from unittest import * diff --git a/ansible_collections/netapp/ontap/tests/unit/framework/mock_rest_and_zapi_requests.py b/ansible_collections/netapp/ontap/tests/unit/framework/mock_rest_and_zapi_requests.py new file mode 100644 index 000000000..a920eeab6 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/framework/mock_rest_and_zapi_requests.py @@ -0,0 +1,288 @@ + +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Author: Laurent Nicolas, laurentn@netapp.com + +""" unit tests for Ansible modules for ONTAP: + fixture to mock REST send_request and ZAPI invoke_elem to trap all network calls + + Note: errors are reported as exception. Additional details are printed to the output. + pytest suppresses the output unless -s is used. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from copy import deepcopy +from functools import partial +import inspect +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +# set this to true to print messages about the fixture itself. +DEBUG = False +# if true, an error is raised if register_responses was not called. +FORCE_REGISTRATION = False + + +@pytest.fixture(autouse=True) +def patch_request_and_invoke(request): + if DEBUG: + print('entering patch_request_and_invoke fixture for', request.function) + function_name = request.function.__name__ + + with patch('time.sleep') as mock_time_sleep: + mock_time_sleep.side_effect = partial(_mock_time_sleep, function_name) + with patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') as mock_send_request: + mock_send_request.side_effect = partial(_mock_netapp_send_request, function_name) + if netapp_utils.has_netapp_lib(): + with patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapZAPICx.invoke_elem') as mock_invoke_elem: + mock_invoke_elem.side_effect = partial(_mock_netapp_invoke_elem, function_name) + yield mock_send_request, mock_invoke_elem + else: + yield mock_send_request + + # This part is executed after the test completes + _patch_request_and_invoke_exit_checks(function_name) + + +def register_responses(responses, function_name=None): + ''' When patching, the pytest request identifies the UT test function + if the registration is happening in a helper function, function_name needs to identify the calling test function + EG: + test_me(): + for x in range: + check_something() + if the registration happens in check_something, function_name needs to be set to test_me (as a string) + ''' + caller = inspect.currentframe().f_back.f_code.co_name + if DEBUG: + print('register_responses - caller:', caller, 'function_name:', function_name) + if function_name is not None and function_name != caller and (caller.startswith('test') or not function_name.startswith('test')): + raise KeyError('inspect reported a different name: %s, received: %s' % (caller, function_name)) + if function_name is None: + function_name = caller + fixed_records = [] + for record in responses: + try: + expected_method, expected_api, response = record + if expected_method not in ['ZAPI', 'GET', 'OPTIONS', 'POST', 'PATCH', 'DELETE']: + raise KeyError('Unexpected method %s in %s for function: %s' % (expected_method, record, function_name)) + except ValueError: + expected_method = 'ZAPI' + expected_api, response = record + if expected_method == 'ZAPI': + # sanity checks for netapp-lib are deferred until the test is actually run + response, valid = response + if valid != 'valid': + raise ImportError(response) + # some modules modify the record in place - keep the original intact + fixed_records.append((expected_method, expected_api, deepcopy(response))) + _RESPONSES[function_name] = fixed_records + + +def get_mock_record(function_name=None): + if function_name is None: + function_name = inspect.currentframe().f_back.f_code.co_name + return _REQUESTS.get(function_name) + + +def print_requests(function_name=None): + if function_name is None: + function_name = inspect.currentframe().f_back.f_code.co_name + if function_name not in _REQUESTS: + print('No request processed for %s' % function_name) + return + print('--- %s - processed requests ---' % function_name) + for record in _REQUESTS[function_name].get_requests(): + print(record) + print('--- %s - end of processed requests ---' % function_name) + + +def print_requests_and_responses(function_name=None): + if function_name is None: + function_name = inspect.currentframe().f_back.f_code.co_name + if function_name not in _REQUESTS: + print('No request processed for %s' % function_name) + return + print('--- %s - processed requests and responses---' % function_name) + for record in _REQUESTS[function_name].get_responses(): + print(record) + print('--- %s - end of processed requests and responses---' % function_name) + + +class MockCalls: + '''record calls''' + def __init__(self, function_name): + self.function_name = function_name + self.requests = [] + self.responses = [] + + def get_responses(self, method=None, api=None): + for record in self.responses: + if ((method is None or record.get('method') == method) + and (api is None or record.get('api') == api)): + yield record + + def get_requests(self, method=None, api=None, response=None): + for record in self.requests: + if ((method is None or record.get('method') == method) + and (api is None or record.get('api') == api) + and (response is None or record.get('response') == response)): + yield record + + def is_record_in_json(self, record, method, api, response=None): + for request in self.get_requests(method, api, response): + json = request.get('json') + if json and self._record_in_dict(record, json): + return True + return False + + def is_zapi_called(self, zapi): + return any(self.get_requests('ZAPI', zapi)) + + def get_request(self, sequence): + return self.requests[sequence] + + def is_text_in_zapi_request(self, text, sequence, present=True): + found = text in str(self.get_request(sequence)['zapi_request']) + if found != present: + not_expected = 'not ' if present else '' + print('Error: %s %sfound in %s' % (text, not_expected, self.get_request(sequence)['zapi_request'])) + return found + + # private methods + + def __del__(self): + if DEBUG: + print('Deleting MockCalls instance for', self.function_name) + + def _record_response(self, method, api, response): + print(method, api, response) + if method == 'ZAPI': + try: + response = response.to_string() + except AttributeError: + pass + self.responses.append((method, api, response)) + + @staticmethod + def _record_in_dict(record, adict): + for key, value in record.items(): + if key not in adict: + print('key: %s not found in %s' % (key, adict)) + return False + if value != adict[key]: + print('Values differ for key: %s: - %s vs %s' % (key, value, adict[key])) + return False + return True + + def _record_rest_request(self, method, api, params, json, headers, files): + record = { + 'params': params, + 'json': json, + 'headers': headers, + 'files': files, + } + self._record_request(method, api, record) + + def _record_zapi_request(self, zapi, na_element, enable_tunneling): + try: + zapi_request = na_element.to_string() + except AttributeError: + zapi_request = na_element + record = { + 'na_element': na_element, + 'zapi_request': zapi_request, + 'tunneling': enable_tunneling + } + self._record_request('ZAPI', zapi, record) + + def _record_request(self, method, api, record=None): + record = record or {} + record['function'] = self.function_name + record['method'] = method + record['api'] = api + self.requests.append(record) + + def _get_response(self, method, api): + response = _get_response(self.function_name, method, api) + self._record_response(method, api, response) + return response + + +# private variables and methods + +_REQUESTS = {} +_RESPONSES = {} + + +def _get_response(function, method, api): + if function not in _RESPONSES: + print('Error: make sure to add entries for %s in RESPONSES.' % function) + raise KeyError('function %s is not registered - %s %s' % (function, method, api)) + if not _RESPONSES[function]: + print('Error: exhausted all entries for %s in RESPONSES, received request for %s %s' % (function, method, api)) + print_requests(function) + raise KeyError('function %s received unhandled call %s %s' % (function, method, api)) + expected_method, expected_api, response = _RESPONSES[function][0] + if expected_method != method or expected_api not in ['*', api]: + print_requests(function) + raise KeyError('function %s received an unexpected call %s %s, expecting %s %s' % (function, method, api, expected_method, expected_api)) + _RESPONSES[function].pop(0) + if isinstance(response, Exception): + raise response + # some modules modify the record in place - keep the original intact + return deepcopy(response) + + +def _get_or_create_mock_record(function_name): + if function_name not in _REQUESTS: + _REQUESTS[function_name] = MockCalls(function_name) + return _REQUESTS[function_name] + + +def _mock_netapp_send_request(function_name, method, api, params, json=None, headers=None, files=None): + if DEBUG: + print('Inside _mock_netapp_send_request') + mock_calls = _get_or_create_mock_record(function_name) + mock_calls._record_rest_request(method, api, params, json, headers, files) + return mock_calls._get_response(method, api) + + +def _mock_netapp_invoke_elem(function_name, na_element, enable_tunneling=False): + if DEBUG: + print('Inside _mock_netapp_invoke_elem') + zapi = na_element.get_name() + mock_calls = _get_or_create_mock_record(function_name) + mock_calls._record_zapi_request(zapi, na_element, enable_tunneling) + return mock_calls._get_response('ZAPI', zapi) + + +def _mock_time_sleep(function_name, duration): + if DEBUG: + print('Inside _mock_time_sleep for %s' % function_name) + if duration > 0.1: + # the IDE or debug mode may add a small timer - only report for "large" value + raise KeyError("time.sleep(%s) was called - add: @patch('time.sleep')" % duration) + + +def _patch_request_and_invoke_exit_checks(function_name): + # action to be performed afther a test is complete + if DEBUG: + print('exiting patch_request_and_invoke fixture for', function_name) + if FORCE_REGISTRATION: + assert function_name in _RESPONSES, 'Error: responses for ZAPI invoke or REST send requests are not registered.' + # make sure all expected requests were consumed + if _RESPONSES.get(function_name): + print('Error: not all responses were processed. It is expected if the test failed.') + print('Error: remaining responses: %s' % _RESPONSES[function_name]) + msg = 'Error: not all responses were processed. Use -s to see detailed error. '\ + 'Ignore this error if there is an earlier error in the test.' + assert not _RESPONSES.get(function_name), msg + if function_name in _RESPONSES: + del _RESPONSES[function_name] + if function_name in _REQUESTS: + del _REQUESTS[function_name] diff --git a/ansible_collections/netapp/ontap/tests/unit/framework/rest_factory.py b/ansible_collections/netapp/ontap/tests/unit/framework/rest_factory.py new file mode 100644 index 000000000..dfda97c03 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/framework/rest_factory.py @@ -0,0 +1,107 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Author: Laurent Nicolas, laurentn@netapp.com + +""" unit tests for Ansible modules for ONTAP: + utility to build REST responses and errors, and register them to use them in testcases. + + 1) at the module level, define the REST responses: + SRR = rest_responses() if you're only interested in the default ones: 'empty', 'error', ... + SRR = rest_responses(dict) to use the default ones and augment them: + a key identifies a response name, and the value is a tuple. + + 3) in each test function, create a list of (event, response) using rest_response + def test_create_aggr(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'storage/aggregates', SRR['empty_good']) + ]) + + See ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_aggregate_rest.py + for an example. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +JOB_GET_API = ' cluster/jobs/94b6e6a7-d426-11eb-ac81-00505690980f' + + +def _build_job(state): + return (200, { + "uuid": "f03ccbb6-d8bb-11eb-ac81-00505690980f", + "description": "job results with state: %s" % state, + "state": state, + "message": "job reported %s" % state + }, None) + + +# name: (html_code, dict, None or error string) +# dict is translated into an xml structure, num_records is None or an integer >= 0 +_DEFAULT_RESPONSES = { + # common responses + 'is_rest': (200, {}, None), + 'is_rest_95': (200, dict(version=dict(generation=9, major=5, minor=0, full='dummy_9_5_0')), None), + 'is_rest_96': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy_9_6_0')), None), + 'is_rest_97': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy_9_7_0')), None), + 'is_rest_9_7_5': (200, dict(version=dict(generation=9, major=7, minor=5, full='dummy_9_7_5')), None), + 'is_rest_9_8_0': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy_9_8_0')), None), + 'is_rest_9_9_0': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy_9_9_0')), None), + 'is_rest_9_9_1': (200, dict(version=dict(generation=9, major=9, minor=1, full='dummy_9_9_1')), None), + 'is_rest_9_10_1': (200, dict(version=dict(generation=9, major=10, minor=1, full='dummy_9_10_1')), None), + 'is_rest_9_11_0': (200, dict(version=dict(generation=9, major=11, minor=0, full='dummy_9_11_0')), None), + 'is_rest_9_11_1': (200, dict(version=dict(generation=9, major=11, minor=1, full='dummy_9_11_1')), None), + 'is_rest_9_12_0': (200, dict(version=dict(generation=9, major=12, minor=0, full='dummy_9_12_0')), None), + 'is_rest_9_12_1': (200, dict(version=dict(generation=9, major=12, minor=1, full='dummy_9_12_1')), None), + 'is_rest_9_13_1': (200, dict(version=dict(generation=9, major=13, minor=1, full='dummy_9_13_1')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'success': (200, {}, None), + 'success_with_job_uuid': (200, {'job': {'_links': {'self': {'href': '/api/%s' % JOB_GET_API}}}}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'empty_records': (200, {'records': []}, None), + 'zero_records': (200, {'num_records': 0}, None), + 'one_record': (200, {'num_records': 1}, None), + 'one_vserver_record': (200, {'num_records': 1, 'records': [{'svm': {'name': 'svm_name', 'uuid': 'svm_uuid'}}]}, None), + 'generic_error': (400, None, "Expected error"), + 'error_record': (400, None, {'code': 6, 'message': 'Expected error'}), + 'job_generic_response_success': _build_job('success'), + 'job_generic_response_running': _build_job('running'), + 'job_generic_response_failure': _build_job('failure'), +} + + +def rest_error_message(error, api=None, extra='', got=None): + if got is None: + got = 'got Expected error.' + msg = ('%s: ' % error) if error else '' + msg += ('calling: %s: ' % api) if api else '' + msg += got + msg += extra + return msg + + +class rest_responses: + ''' return an object that behaves like a read-only dictionary + supports [key] to read an entry, and 'in' keyword to check key existence. + ''' + def __init__(self, adict=None, allow_override=True): + self.responses = dict(_DEFAULT_RESPONSES.items()) + if adict: + for key, value in adict.items(): + if not allow_override and key in self.responses: + raise KeyError('duplicated key: %s' % key) + self.responses[key] = value + + def _get_response(self, name): + try: + return self.responses[name] + except KeyError: + raise KeyError('%s not registered, list of valid keys: %s' % (name, self.responses.keys())) + + def __getitem__(self, name): + return self._get_response(name) + + def __contains__(self, name): + return name in self.responses diff --git a/ansible_collections/netapp/ontap/tests/unit/framework/test_mock_rest_and_zapi_requests.py b/ansible_collections/netapp/ontap/tests/unit/framework/test_mock_rest_and_zapi_requests.py new file mode 100644 index 000000000..bd94b027a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/framework/test_mock_rest_and_zapi_requests.py @@ -0,0 +1,189 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module unit test fixture amd helper mock_rest_and_zapi_requests """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +import ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests as uut +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke as patch_fixture +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import zapi_responses, build_zapi_response + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') # pragma: no cover + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses() +# ZAPI canned responses when mocking invoke_elem. +# The zapi_factory provides default responses shared across testcases. +ZRR = zapi_responses() + +uut.DEBUG = True + + +def test_register_responses(): + uut.register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('get-version', build_zapi_response({})), + ('get-bad-zapi', ('BAD_ZAPI', 'valid')) + ], 'test_register_responses') + assert uut._get_response('test_register_responses', 'GET', 'cluster') == SRR['is_rest'] + assert uut._get_response('test_register_responses', 'ZAPI', 'get-version').to_string() == build_zapi_response({})[0].to_string() + # If to_string() is not available, the ZAPI is registered as is. + assert uut._get_response('test_register_responses', 'ZAPI', 'get-bad-zapi') == 'BAD_ZAPI' + + +def test_negative_register_responses(): + with pytest.raises(KeyError) as exc: + uut.register_responses([ + ('MOVE', 'cluster', SRR['is_rest']), + ], 'not_me') + assert exc.value.args[0] == 'inspect reported a different name: test_negative_register_responses, received: not_me' + + with pytest.raises(KeyError) as exc: + uut.register_responses([ + ('MOVE', 'cluster', SRR['is_rest']), + ]) + assert 'Unexpected method MOVE' in exc.value.args[0] + + +def test_negative_get_response(): + with pytest.raises(KeyError) as exc: + uut._get_response('test_negative_get_response', 'POST', 'cluster') + assert exc.value.args[0] == 'function test_negative_get_response is not registered - POST cluster' + + uut.register_responses([ + ('GET', 'cluster', SRR['is_rest'])]) + with pytest.raises(KeyError) as exc: + uut._get_response('test_negative_get_response', 'POST', 'cluster') + assert exc.value.args[0] == 'function test_negative_get_response received an unexpected call POST cluster, expecting GET cluster' + + uut._get_response('test_negative_get_response', 'GET', 'cluster') + with pytest.raises(KeyError) as exc: + uut._get_response('test_negative_get_response', 'POST', 'cluster') + assert exc.value.args[0] == 'function test_negative_get_response received unhandled call POST cluster' + + +def test_record_rest_request(): + function_name = 'testme' + method = 'METHOD' + api = 'API' + params = 'PARAMS' + json = {'record': {'key': 'value'}} + headers = {} + files = {'data': 'value'} + calls = uut.MockCalls(function_name) + calls._record_rest_request(method, api, params, json, headers, files) + uut.print_requests(function_name) + assert len([calls.get_requests(method, api)]) == 1 + assert calls.is_record_in_json({'record': {'key': 'value'}}, 'METHOD', 'API') + assert not calls.is_record_in_json({'record': {'key': 'value1'}}, 'METHOD', 'API') + assert not calls.is_record_in_json({'key': 'value'}, 'METHOD', 'API') + + +def test_record_zapi_request(): + function_name = 'testme' + api = 'API' + zapi = build_zapi_response({}) + tunneling = False + calls = uut.MockCalls(function_name) + calls._record_zapi_request(api, zapi, tunneling) + uut.print_requests(function_name) + assert len([calls.get_requests('ZAPI', api)]) == 1 + assert calls.is_zapi_called('API') + assert not calls.is_zapi_called('version') + + +def test_negative_record_zapi_request(): + function_name = 'testme' + api = 'API' + zapi = 'STRING' # AttributeError is handled in the function + tunneling = False + calls = uut.MockCalls(function_name) + calls._record_zapi_request(api, zapi, tunneling) + uut.print_requests(function_name) + assert len([calls.get_requests('ZAPI', api)]) == 1 + + +def test_negative_record_zapi_response(): + function_name = 'testme' + api = 'API' + zapi = 'STRING' # AttributeError is handled in the function + calls = uut.MockCalls(function_name) + calls._record_response('ZAPI', api, zapi) + uut.print_requests_and_responses(function_name) + assert len([calls.get_responses('ZAPI', api)]) == 1 + + +def test_mock_netapp_send_request(): + function_name = 'test_mock_netapp_send_request' + method = 'GET' + api = 'cluster' + params = 'PARAMS' + uut.register_responses([ + ('GET', 'cluster', SRR['is_rest'])]) + response = uut._mock_netapp_send_request(function_name, method, api, params) + assert response == SRR['is_rest'] + + +def test_mock_netapp_invoke_elem(): + function_name = 'test_mock_netapp_invoke_elem' + method = 'ZAPI' + api = 'cluster' + params = 'PARAMS' + zapi = netapp_utils.zapi.NaElement.create_node_with_children('get-version') + uut.register_responses([ + ('get-version', build_zapi_response({}))]) + response = uut._mock_netapp_invoke_elem(function_name, zapi) + assert response.to_string() == build_zapi_response({})[0].to_string() + + +def test_print_requests_and_responses(): + uut.print_requests_and_responses() + + +def test_fixture(patch_fixture): + uut.register_responses([ + ('get-version', build_zapi_response({}))]) + mock_sr, mock_invoke = patch_fixture + cx = netapp_utils.OntapZAPICx() + cx.invoke_elem(netapp_utils.zapi.NaElement.create_node_with_children('get-version')) + assert 'test_fixture' in uut._RESPONSES + assert 'test_fixture' in uut._REQUESTS + uut.print_requests() + uut.print_requests_and_responses() + assert len(mock_sr.mock_calls) == 0 + assert len(mock_invoke.mock_calls) == 1 + calls = uut.get_mock_record() + assert len([calls.get_requests()]) == 1 + + +def test_fixture_exit_unregistered(patch_fixture): + uut.FORCE_REGISTRATION = True + with pytest.raises(AssertionError) as exc: + uut._patch_request_and_invoke_exit_checks('test_fixture_exit_unregistered') + msg = 'Error: responses for ZAPI invoke or REST send requests are not registered.' + assert msg in exc.value.args[0] + uut.FORCE_REGISTRATION = False + + +def test_fixture_exit_unused_response(patch_fixture): + uut.FORCE_REGISTRATION = True + uut.register_responses([ + ('get-version', build_zapi_response({}))]) + # report an error if any response is not consumed + with pytest.raises(AssertionError) as exc: + uut._patch_request_and_invoke_exit_checks('test_fixture_exit_unused_response') + msg = 'Error: not all responses were processed. Use -s to see detailed error. Ignore this error if there is an earlier error in the test.' + assert msg in exc.value.args[0] + # consume the response + cx = netapp_utils.OntapZAPICx() + cx.invoke_elem(netapp_utils.zapi.NaElement.create_node_with_children('get-version')) + uut.FORCE_REGISTRATION = False diff --git a/ansible_collections/netapp/ontap/tests/unit/framework/test_mock_rest_and_zapi_requests_no_netapp_lib.py b/ansible_collections/netapp/ontap/tests/unit/framework/test_mock_rest_and_zapi_requests_no_netapp_lib.py new file mode 100644 index 000000000..4d929c975 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/framework/test_mock_rest_and_zapi_requests_no_netapp_lib.py @@ -0,0 +1,94 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module unit test fixture amd helper mock_rest_and_zapi_requests """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +import ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests as uut +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke as patch_fixture +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import zapi_responses, build_zapi_response, build_zapi_error + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses() + +uut.DEBUG = True + + +@pytest.fixture(autouse=True, scope='module') +def patch_has_netapp_lib(): + with patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') as has_netapp_lib: + has_netapp_lib.return_value = False + yield has_netapp_lib + + +def test_import_error_zapi_responses(): + # ZAPI canned responses when mocking invoke_elem. + # The zapi_factory provides default responses shared across testcases. + ZRR = zapi_responses() + with pytest.raises(ImportError) as exc: + zapi = ZRR['empty'] + print("ZAPI", zapi) + msg = 'build_zapi_response: netapp-lib is missing' + assert msg == exc.value.args[0] + + +def test_register_responses(): + get_version = build_zapi_response({}) + with pytest.raises(ImportError) as exc: + uut.register_responses([ + ('get-version', get_version), + ('get-bad-zapi', 'BAD_ZAPI') + ], 'test_register_responses') + msg = 'build_zapi_response: netapp-lib is missing' + assert msg == exc.value.args[0] + + +def test_import_error_build_zapi_response(): + zapi = build_zapi_response({}) + expected = ('build_zapi_response: netapp-lib is missing', 'invalid') + assert expected == zapi + + +def test_import_error_build_zapi_error(): + zapi = build_zapi_error(12345, 'test') + expected = ('build_zapi_error: netapp-lib is missing', 'invalid') + assert expected == zapi + + +class Module: + def __init__(self): + self.params = { + 'username': 'user', + 'password': 'pwd', + 'hostname': 'host', + 'use_rest': 'never', + 'cert_filepath': None, + 'key_filepath': None, + 'validate_certs': False, + 'http_port': None, + 'feature_flags': None, + } + + +def test_fixture_no_netapp_lib(patch_fixture): + uut.register_responses([ + ('GET', 'cluster', (200, {}, None))]) + mock_sr = patch_fixture + cx = netapp_utils.OntapRestAPI(Module()) + cx.send_request('GET', 'cluster', None) + assert 'test_fixture_no_netapp_lib' in uut._RESPONSES + assert 'test_fixture_no_netapp_lib' in uut._REQUESTS + uut.print_requests() + uut.print_requests_and_responses() + assert len(mock_sr.mock_calls) == 1 + calls = uut.get_mock_record() + assert len([calls.get_requests()]) == 1 diff --git a/ansible_collections/netapp/ontap/tests/unit/framework/test_rest_factory.py b/ansible_collections/netapp/ontap/tests/unit/framework/test_rest_factory.py new file mode 100644 index 000000000..f04dc4518 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/framework/test_rest_factory.py @@ -0,0 +1,44 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module unit test helper rest_factory """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + + +def test_default_responses(): + srr = rest_responses() + assert srr + assert 'is_zapi' in srr + assert srr['is_zapi'] == (400, {}, "Unreachable") + + +def test_add_response(): + srr = rest_responses( + {'is_zapi': (444, {'k': 'v'}, "Unknown")} + ) + assert srr + assert 'is_zapi' in srr + assert srr['is_zapi'] == (444, {'k': 'v'}, "Unknown") + + +def test_negative_add_response(): + with pytest.raises(KeyError) as exc: + srr = rest_responses( + {'is_zapi': (444, {'k': 'v'}, "Unknown")}, allow_override=False + ) + print(exc.value) + assert 'duplicated key: is_zapi' == exc.value.args[0] + + +def test_negative_key_does_not_exist(): + srr = rest_responses() + with pytest.raises(KeyError) as exc: + srr['bad_key'] + print(exc.value) + msg = 'bad_key not registered, list of valid keys:' + assert msg in exc.value.args[0] diff --git a/ansible_collections/netapp/ontap/tests/unit/framework/test_zapi_factory.py b/ansible_collections/netapp/ontap/tests/unit/framework/test_zapi_factory.py new file mode 100644 index 000000000..c82fb6a01 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/framework/test_zapi_factory.py @@ -0,0 +1,108 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module unit test helper zapi_factory """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.tests.unit.framework import zapi_factory as uut + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') # pragma: no cover + +AGGR_INFO = {'num-records': 3, + 'attributes-list': + {'aggr-attributes': + {'aggregate-name': 'aggr_name', + 'aggr-raid-attributes': { + 'state': 'online', + 'disk-count': '4', + 'encrypt-with-aggr-key': 'true'}, + 'aggr-snaplock-attributes': {'snaplock-type': 'snap_t'}} + }, + } + + +def test_build_zapi_response_empty(): + empty, valid = uut.build_zapi_response({}) + assert valid == 'valid' + print(empty.to_string()) + assert empty.to_string() == b'<results status="passed"/>' + + +def test_build_zapi_response_dict(): + aggr_info, valid = uut.build_zapi_response(AGGR_INFO) + assert valid == 'valid' + print(aggr_info.to_string()) + aggr_str = aggr_info.to_string() + assert b'<aggregate-name>aggr_name</aggregate-name>' in aggr_str + assert b'<aggr-snaplock-attributes><snaplock-type>snap_t</snaplock-type></aggr-snaplock-attributes>' in aggr_str + assert b'<results status="passed">' in aggr_str + assert b'<num-records>3</num-records>' in aggr_str + + +def test_build_zapi_error(): + zapi1, valid = uut.build_zapi_error('54321', 'error_text') + assert valid == 'valid' + zapi2, valid = uut.build_zapi_error(54321, 'error_text') + assert valid == 'valid' + assert zapi1.to_string() == zapi2.to_string() + print(zapi1.to_string()) + assert zapi1.to_string() == b'<results errno="54321" reason="error_text"/>' + + +def test_default_responses(): + zrr = uut.zapi_responses() + assert zrr + assert 'empty' in zrr + print(zrr['empty'][0].to_string()) + assert zrr['empty'][0].to_string() == uut.build_zapi_response({})[0].to_string() + + +def test_add_response(): + zrr = uut.zapi_responses( + {'empty': uut.build_zapi_response({'k': 'v'}, 1)} + ) + assert zrr + assert 'empty' in zrr + print(zrr['empty'][0].to_string()) + assert zrr['empty'][0].to_string() == uut.build_zapi_response({'k': 'v'}, 1)[0].to_string() + + +def test_negative_add_response(): + with pytest.raises(KeyError) as exc: + zrr = uut.zapi_responses( + {'empty': uut.build_zapi_response({})}, allow_override=False + ) + print(exc.value) + assert 'duplicated key: empty' == exc.value.args[0] + + +def test_negative_add_default_error(): + uut._DEFAULT_ERRORS['empty'] = uut.build_zapi_error(12345, 'hello') + with pytest.raises(KeyError) as exc: + zrr = uut.zapi_responses(allow_override=False) + print(exc.value) + assert 'duplicated key: empty' == exc.value.args[0] + del uut._DEFAULT_ERRORS['empty'] + + +def test_negative_add_error(): + with pytest.raises(KeyError) as exc: + zrr = uut.zapi_responses( + {'empty': uut.build_zapi_error(12345, 'hello')}, allow_override=False + ) + print(exc.value) + assert 'duplicated key: empty' == exc.value.args[0] + + +def test_negative_key_does_not_exist(): + zrr = uut.zapi_responses() + with pytest.raises(KeyError) as exc: + zrr['bad_key'] + print(exc.value) + msg = 'bad_key not registered, list of valid keys:' + assert msg in exc.value.args[0] diff --git a/ansible_collections/netapp/ontap/tests/unit/framework/ut_utilities.py b/ansible_collections/netapp/ontap/tests/unit/framework/ut_utilities.py new file mode 100644 index 000000000..ac770604a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/framework/ut_utilities.py @@ -0,0 +1,31 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Author: Laurent Nicolas, laurentn@netapp.com + +""" unit tests for Ansible modules for ONTAP: + shared utility functions +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys + + +def is_indexerror_exception_formatted(): + """ some versions of python do not format IndexError exception properly + the error message is not reported in str() or repr() + We see this for older versions of Ansible, where the python version is frozen + - fails on 3.5.7 but works on 3.5.10 + - fails on 3.6.8 but works on 3.6.9 + - fails on 3.7.4 but works on 3.7.5 + - fails on 3.8.0 but works on 3.8.1 + """ + return ( + sys.version_info[:2] == (2, 7) + or (sys.version_info[:2] == (3, 5) and sys.version_info[:3] > (3, 5, 7)) + or (sys.version_info[:2] == (3, 6) and sys.version_info[:3] > (3, 6, 8)) + or (sys.version_info[:2] == (3, 7) and sys.version_info[:3] > (3, 7, 4)) + or (sys.version_info[:2] == (3, 8) and sys.version_info[:3] > (3, 8, 0)) + or sys.version_info[:2] >= (3, 9) + ) diff --git a/ansible_collections/netapp/ontap/tests/unit/framework/zapi_factory.py b/ansible_collections/netapp/ontap/tests/unit/framework/zapi_factory.py new file mode 100644 index 000000000..5f23fbad0 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/framework/zapi_factory.py @@ -0,0 +1,148 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# Author: Laurent Nicolas, laurentn@netapp.com + +""" unit tests for Ansible modules for ONTAP: + utility to build REST responses and errors, and register them to use them in testcases. + + 1) at the module level, define the ZAPI responses: + ZRR = zapi_responses() if you're only interested in the default ones: 'empty', 'error', ... + or + ZRR = zapi_responses(dict) to use the default ones and augment them: + a key identifies a response name, and the value is an XML structure. + + 2) create a ZAPI XML response or error using + build_zapi_response(contents, num_records=None) + build_zapi_error(errno, reason) + + Typically, these will be used with zapi_responses as + + ZRR = zapi_responses({ + 'aggr_info': build_zapi_response(aggr_info), + 'object_store_info': build_zapi_response(object_store_info), + 'disk_info': build_zapi_response(disk_info), + }) + + 3) in each test function, create a list of (event, response) using zapi_responses (and rest_responses) + def test_create(self): + register_responses([ + ('aggr-get-iter', ZRR['empty']), + ('aggr-create', ZRR['empty']), + ('aggr-get-iter', ZRR['empty']), + ]) + + See ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_aggregate.py + for an example. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +# name: (dict, num_records) +# dict is translated into an xml structure, num_records is None or an integer >= 0 +_DEFAULT_RESPONSES = { + 'empty': ({}, None), + 'success': ({}, None), + 'no_records': ({'num-records': '0'}, None), + 'one_record_no_data': ({'num-records': '1'}, None), + 'version': ({'version': 'zapi_version'}, None), + 'cserver': ({ + 'attributes-list': { + 'vserver-info': { + 'vserver-name': 'cserver' + } + }}, 1), +} +# name: (errno, reason) +# errno as int, reason as str +_DEFAULT_ERRORS = { + 'error': (12345, 'synthetic error for UT purpose'), + 'error_missing_api': (13005, 'Unable to find API: xxxx on data vserver') +} + + +def get_error_desc(error_code): + return next((err_desc for err_num, err_desc in _DEFAULT_ERRORS.values() if err_num == error_code), + 'no registered error for %d' % error_code) + + +def zapi_error_message(error, error_code=12345, reason=None, addal=None): + if reason is None: + reason = get_error_desc(error_code) + msg = "%s: NetApp API failed. Reason - %s:%s" % (error, error_code, reason) + if addal: + msg += addal + return msg + + +def build_raw_xml_response(contents, num_records=None, force_dummy=False): + top_contents = {'results': contents} + xml, valid = build_zapi_response(top_contents) + if valid == 'valid' and not force_dummy: + return xml.to_string() + return b'<xml><results status="netapp-lib is missing"/></xml>' + + +def build_zapi_response(contents, num_records=None): + ''' build an XML response + contents is translated into an xml structure + num_records is None or an integer >= 0 + ''' + if not netapp_utils.has_netapp_lib(): + # do not report an error at init, as it breaks ansible-test checks + return 'build_zapi_response: netapp-lib is missing', 'invalid' + if num_records is not None: + contents['num-records'] = str(num_records) + response = netapp_utils.zapi.NaElement('results') + response.translate_struct(contents) + response.add_attr('status', 'passed') + return (response, 'valid') + + +def build_zapi_error(errno, reason): + ''' build an XML response + errno as int + reason as str + ''' + if not netapp_utils.has_netapp_lib(): + return 'build_zapi_error: netapp-lib is missing', 'invalid' + response = netapp_utils.zapi.NaElement('results') + response.add_attr('errno', str(errno)) + response.add_attr('reason', reason) + return (response, 'valid') + + +class zapi_responses: + + def __init__(self, adict=None, allow_override=True): + self.responses = {} + for key, value in _DEFAULT_RESPONSES.items(): + self.responses[key] = build_zapi_response(*value) + for key, value in _DEFAULT_ERRORS.items(): + if key in self.responses: + raise KeyError('duplicated key: %s' % key) + self.responses[key] = build_zapi_error(*value) + if adict: + for key, value in adict.items(): + if not allow_override and key in self.responses: + raise KeyError('duplicated key: %s' % key) + self.responses[key] = value + + def _get_response(self, name): + try: + value, valid = self.responses[name] + # sanity checks for netapp-lib are deferred until the test is actually run + if valid != 'valid': + print("Error: Defer any runtime dereference, eg ZRR['key'], until runtime or protect dereference under has_netapp_lib().") + raise ImportError(value) + return value, valid + except KeyError: + raise KeyError('%s not registered, list of valid keys: %s' % (name, self.responses.keys())) + + def __getitem__(self, name): + return self._get_response(name) + + def __contains__(self, name): + return name in self.responses diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/filter/test_na_filter_iso8601.py b/ansible_collections/netapp/ontap/tests/unit/plugins/filter/test_na_filter_iso8601.py new file mode 100644 index 000000000..7bdb37191 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/filter/test_na_filter_iso8601.py @@ -0,0 +1,66 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for iso8601 filter """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible.errors import AnsibleFilterError +from ansible_collections.netapp.ontap.plugins.filter import na_filter_iso8601 +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +from ansible_collections.netapp.ontap.tests.unit.framework import ut_utilities + +if na_filter_iso8601.IMPORT_ERROR and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as isodate is not available') + +ISO_DURATION = 'P689DT13H57M44S' +ISO_DURATION_WEEKS = 'P98W' +SECONDS_DURATION = 59579864 + + +def test_class_filter(): + my_obj = na_filter_iso8601.FilterModule() + assert len(my_obj.filters()) == 2 + + +def test_iso8601_duration_to_seconds(): + my_obj = na_filter_iso8601.FilterModule() + assert my_obj.filters()['iso8601_duration_to_seconds'](ISO_DURATION) == SECONDS_DURATION + + +def test_negative_iso8601_duration_to_seconds(): + my_obj = na_filter_iso8601.FilterModule() + with pytest.raises(AnsibleFilterError) as exc: + my_obj.filters()['iso8601_duration_to_seconds']('BAD_DATE') + print('EXC', exc) + # exception is not properly formatted with older 3.x versions, assuming same issue as for IndexError + if ut_utilities.is_indexerror_exception_formatted(): + assert 'BAD_DATE' in str(exc) + + +def test_iso8601_duration_from_seconds(): + my_obj = na_filter_iso8601.FilterModule() + assert my_obj.filters()['iso8601_duration_from_seconds'](SECONDS_DURATION) == ISO_DURATION + + +def test_negative_iso8601_duration_from_seconds_str(): + my_obj = na_filter_iso8601.FilterModule() + with pytest.raises(AnsibleFilterError) as exc: + my_obj.filters()['iso8601_duration_from_seconds']('BAD_INT') + print('EXC', exc) + if ut_utilities.is_indexerror_exception_formatted(): + assert 'BAD_INT' in str(exc) + + +@patch('ansible_collections.netapp.ontap.plugins.filter.na_filter_iso8601.IMPORT_ERROR', 'import failed') +def test_negative_check_for_import(): + my_obj = na_filter_iso8601.FilterModule() + with pytest.raises(AnsibleFilterError) as exc: + my_obj.filters()['iso8601_duration_to_seconds'](ISO_DURATION) + print('EXC', exc) + if ut_utilities.is_indexerror_exception_formatted(): + assert 'import failed' in str(exc) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/ansible_mocks.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/ansible_mocks.py new file mode 100644 index 000000000..85c0bc1b2 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/ansible_mocks.py @@ -0,0 +1,181 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import copy +import json +import pytest +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +from ansible_collections.netapp.ontap.plugins.module_utils.netapp import ZAPI_DEPRECATION_MESSAGE + +VERBOSE = True + + +def set_module_args(args): + """prepare arguments so that they will be picked up during module creation""" + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught by the test case""" + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + + +def exit_json(*args, **kwargs): # pylint: disable=unused-argument + """function to patch over exit_json; package return data into an exception""" + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): # pylint: disable=unused-argument + """function to patch over fail_json; package return data into an exception""" + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +WARNINGS = [] + + +def warn(dummy, msg): + print('WARNING:', msg) + WARNINGS.append(msg) + + +def expect_and_capture_ansible_exception(function, exception, *args, **kwargs): + ''' wraps a call to a funtion in a pytest.raises context and return the exception data as a dict + + function: the function to call -- without () + exception: 'exit' or 'fail' to trap Ansible exceptions raised by exit_json or fail_json + can also take an exception to test some corner cases (eg KeyError) + *args, **kwargs to capture any function arguments + ''' + if exception in ('fail', 'exit'): + exception = AnsibleFailJson if exception == 'fail' else AnsibleExitJson + if not (isinstance(exception, type) and issubclass(exception, Exception)): + raise KeyError('Error: got: %s, expecting fail, exit, or some exception' % exception) + with pytest.raises(exception) as exc: + function(*args, **kwargs) + if VERBOSE: + print('EXC:', exception, exc.value) + if exception in (AnsibleExitJson, AnsibleFailJson, Exception, AttributeError, KeyError, TypeError, ValueError): + return exc.value.args[0] + return exc + + +def call_main(my_main, default_args=None, module_args=None, fail=False): + ''' utility function to call a module main() entry point + my_main: main function for a module + default_args: a dict for the Ansible options - in general, what is accepted by all tests + module_args: additional options - in general what is specific to a test + + call main and should raise AnsibleExitJson or AnsibleFailJson + ''' + args = copy.deepcopy(default_args) if default_args else {} + if module_args: + args.update(module_args) + set_module_args(args) + return expect_and_capture_ansible_exception(my_main, 'fail' if fail else 'exit') + + +def create_module(my_module, default_args=None, module_args=None, check_mode=None, fail=False): + ''' utility function to create a module object + my_module: a class that represent an ONTAP Ansible module + default_args: a dict for the Ansible options - in general, what is accepted by all tests + module_args: additional options - in general what is specific to a test + check_mode: True/False - if not None, check_mode is set accordingly + + returns an instance of the module + ''' + args = copy.deepcopy(default_args) if default_args else {} + if module_args: + args.update(module_args) + set_module_args(args) + if fail: + return expect_and_capture_ansible_exception(my_module, 'fail') + my_module_object = my_module() + if check_mode is not None: + my_module_object.module.check_mode = check_mode + return my_module_object + + +def create_and_apply(my_module, default_args=None, module_args=None, fail=False, check_mode=None): + ''' utility function to create a module and call apply + + calls create_module, then calls the apply function and checks for: + AnsibleExitJson exception if fail is False or not present. + AnsibleFailJson exception if fail is True. + + see create_module for a description of the other arguments. + ''' + try: + my_obj = create_module(my_module, default_args, module_args, check_mode) + except Exception as exc: + print('Unexpected exception returned in create_module: %s' % exc) + print('If expected, use create_module with fail=True.') + raise + return expect_and_capture_ansible_exception(my_obj.apply, 'fail' if fail else 'exit') + + +# using pytest natively, without unittest.TestCase +@pytest.fixture(autouse=True) +def patch_ansible(): + with patch.multiple(basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + warn=warn) as mocks: + clear_warnings() + # so that we get a SystemExit: 1 error (no able to read from stdin in ansible-test !) + # if set_module_args() was not called + basic._ANSIBLE_ARGS = None + yield mocks + + +def get_warnings(): + return WARNINGS + + +def print_warnings(framed=True): + if framed: + sep = '-' * 7 + title = ' WARNINGS ' + print(sep, title, sep) + for warning in WARNINGS: + print(warning) + if framed: + sep = '-' * (7 * 2 + len(title)) + print(sep) + + +def assert_no_warnings(): + assert not WARNINGS + + +def assert_no_warnings_except_zapi(): + # Deprecation message can appear more than once. Remove will only remove the first instance. + local_warning = list(set(WARNINGS)) + tmp_warnings = local_warning[:] + for warning in tmp_warnings: + if warning in ZAPI_DEPRECATION_MESSAGE: + local_warning.remove(ZAPI_DEPRECATION_MESSAGE) + assert not local_warning + + +def assert_warning_was_raised(warning, partial_match=False): + if partial_match: + assert any(warning in msg for msg in WARNINGS) + else: + assert warning in WARNINGS + + +def clear_warnings(): + global WARNINGS + WARNINGS = [] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp.py new file mode 100644 index 000000000..a96f08dea --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp.py @@ -0,0 +1,182 @@ +# Copyright (c) 2018-2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils netapp.py - general features ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils import basic +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + patch_ansible, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'cert_filepath': None, + 'key_filepath': None, +} + +CERT_ARGS = { + 'hostname': 'test', + 'cert_filepath': 'test_pem.pem', + 'key_filepath': 'test_key.key' +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_ontap_module(default_args=None, module_args=None): + return create_module(MockONTAPModule, default_args, module_args).module + + +def test_has_feature_success_default(): + ''' existing feature_flag with default ''' + flag = 'deprecation_warning' + module = create_ontap_module(DEFAULT_ARGS) + assert netapp_utils.has_feature(module, flag) + + +def test_has_feature_success_user_true(): + ''' existing feature_flag with value set to True ''' + flag = 'user_deprecation_warning' + module_args = {'feature_flags': {flag: True}} + module = create_ontap_module(DEFAULT_ARGS, module_args) + assert netapp_utils.has_feature(module, flag) + + +def test_has_feature_success_user_false(): + ''' existing feature_flag with value set to False ''' + flag = 'user_deprecation_warning' + module_args = {'feature_flags': {flag: False}} + module = create_ontap_module(DEFAULT_ARGS, module_args) + assert not netapp_utils.has_feature(module, flag) + + +def test_has_feature_invalid_key(): + ''' existing feature_flag with unknown key ''' + flag = 'deprecation_warning_bad_key' + module = create_ontap_module(DEFAULT_ARGS) + msg = 'Internal error: unexpected feature flag: %s' % flag + assert expect_and_capture_ansible_exception(netapp_utils.has_feature, 'fail', module, flag)['msg'] == msg + + +def test_has_feature_invalid_bool(): + ''' existing feature_flag with non boolean value ''' + flag = 'user_deprecation_warning' + module_args = {'feature_flags': {flag: 'non bool'}} + module = create_ontap_module(DEFAULT_ARGS, module_args) + msg = 'Error: expected bool type for feature flag: %s' % flag + assert expect_and_capture_ansible_exception(netapp_utils.has_feature, 'fail', module, flag)['msg'] == msg + + +def test_get_na_ontap_host_argument_spec_peer(): + ''' validate spec does not have default key and feature_flags option ''' + spec = netapp_utils.na_ontap_host_argument_spec_peer() + for key in ('username', 'https'): + assert key in spec + assert 'feature_flags' not in spec + for entry in spec.values(): + assert 'type' in entry + assert 'default' not in entry + + +def test_setup_host_options_from_module_params_from_empty(): + ''' make sure module.params options are reflected in host_options ''' + module = create_ontap_module(DEFAULT_ARGS) + host_options = {} + keys = ('hostname', 'username') + netapp_utils.setup_host_options_from_module_params(host_options, module, keys) + # we gave 2 keys + assert len(host_options) == 2 + for key in keys: + assert host_options[key] == DEFAULT_ARGS[key] + + +def test_setup_host_options_from_module_params_username_not_set_when_cert_present(): + ''' make sure module.params options are reflected in host_options ''' + module = create_ontap_module(DEFAULT_ARGS) + host_options = dict(cert_filepath='some_path') + unchanged_keys = tuple(host_options.keys()) + copied_over_keys = ('hostname',) + ignored_keys = ('username',) + keys = unchanged_keys + copied_over_keys + ignored_keys + netapp_utils.setup_host_options_from_module_params(host_options, module, keys) + # we gave 2 keys + assert len(host_options) == 2 + for key in ignored_keys: + assert key not in host_options + for key in copied_over_keys: + assert host_options[key] == DEFAULT_ARGS[key] + print(host_options) + for key in unchanged_keys: + assert host_options[key] != DEFAULT_ARGS[key] + + +def test_setup_host_options_from_module_params_not_none_fields_are_preserved(): + ''' make sure module.params options are reflected in host_options ''' + args = dict(DEFAULT_ARGS) + args['cert_filepath'] = 'some_path' + module = create_ontap_module(args) + host_options = dict(cert_filepath='some_other_path') + unchanged_keys = tuple(host_options.keys()) + copied_over_keys = ('hostname',) + ignored_keys = ('username',) + keys = unchanged_keys + copied_over_keys + ignored_keys + netapp_utils.setup_host_options_from_module_params(host_options, module, keys) + # we gave 2 keys + assert len(host_options) == 2 + for key in ignored_keys: + assert key not in host_options + for key in copied_over_keys: + assert host_options[key] == args[key] + print(host_options) + for key in unchanged_keys: + assert host_options[key] != args[key] + + +def test_setup_host_options_from_module_params_cert_not_set_when_username_present(): + ''' make sure module.params options are reflected in host_options ''' + args = dict(DEFAULT_ARGS) + args['cert_filepath'] = 'some_path' + module = create_ontap_module(args) + host_options = dict(username='some_name') + unchanged_keys = tuple(host_options.keys()) + copied_over_keys = ('hostname',) + ignored_keys = ('cert_filepath',) + keys = unchanged_keys + copied_over_keys + ignored_keys + netapp_utils.setup_host_options_from_module_params(host_options, module, keys) + # we gave 2 keys + assert len(host_options) == 2 + for key in ignored_keys: + assert key not in host_options + for key in copied_over_keys: + assert host_options[key] == args[key] + print(host_options) + for key in unchanged_keys: + assert host_options[key] != args[key] + + +def test_setup_host_options_from_module_params_conflict(): + ''' make sure module.params options are reflected in host_options ''' + module = create_ontap_module(DEFAULT_ARGS) + host_options = dict(username='some_name', key_filepath='not allowed') + msg = 'Error: host cannot have both basic authentication (username/password) and certificate authentication (cert/key files).' + assert expect_and_capture_ansible_exception(netapp_utils.setup_host_options_from_module_params, + 'fail', host_options, module, host_options.keys())['msg'] == msg + + +def test_set_auth_method(): + args = {'hostname': None} + # neither password nor cert + error = expect_and_capture_ansible_exception(netapp_utils.set_auth_method, 'fail', create_ontap_module(args), None, None, None, None)['msg'] + assert 'Error: ONTAP module requires username/password or SSL certificate file(s)' in error + # keyfile but no cert + error = expect_and_capture_ansible_exception(netapp_utils.set_auth_method, 'fail', create_ontap_module(args), None, None, None, 'keyfile')['msg'] + assert 'Error: cannot have a key file without a cert file' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_invoke_elem.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_invoke_elem.py new file mode 100644 index 000000000..a185fcb2d --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_invoke_elem.py @@ -0,0 +1,154 @@ +# Copyright (c) 2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""unit tests for module_utils netapp.py - ZAPI invoke_elem + + We cannot use the general UT framework as it patches invoke_elem +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + patch_ansible, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_raw_xml_response, zapi_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip("skipping as missing required netapp_lib") + +ZRR = zapi_responses({ +}) + + +class MockModule: + def __init__(self): + self._name = 'testme' + + +class MockOpener: + def __init__(self, response=None, exception=None): + self.response = response + self.exception = exception + self.timeout = -1 + + def open(self, request, timeout=None): + self.timeout = timeout + if self.exception: + raise self.exception + return self.response + + +class MockResponse: + def __init__(self, contents, force_dummy=False): + self.response = build_raw_xml_response(contents, force_dummy=force_dummy) + print('RESPONSE', self.response) + + def read(self): + return self.response + + +def create_ontapzapicx_object(): + return netapp_utils.OntapZAPICx(module=MockModule()) + + +def test_error_invalid_naelement(): + ''' should fail when NaElement is None, empty, or not of type NaElement ''' + zapi_cx = create_ontapzapicx_object() + assert str(expect_and_capture_ansible_exception(zapi_cx.invoke_elem, ValueError, {})) ==\ + 'NaElement must be supplied to invoke API' + assert str(expect_and_capture_ansible_exception(zapi_cx.invoke_elem, ValueError, {'x': 'yz'})) ==\ + 'NaElement must be supplied to invoke API' + + +def test_exception_with_opener_generic_exception(): + zapi_cx = create_ontapzapicx_object() + zapi_cx._refresh_conn = False + zapi_cx._opener = MockOpener(exception=KeyError('testing')) + exc = expect_and_capture_ansible_exception(zapi_cx.invoke_elem, netapp_utils.zapi.NaApiError, ZRR['success'][0]) + # KeyError('testing') in 3.x but KeyError('testing',) with 2.7 + assert str(exc.value).startswith("NetApp API failed. Reason - Unexpected error:KeyError('testing'") + + +def test_exception_with_opener_httperror(): + if not hasattr(netapp_utils.zapi.urllib.error.HTTPError, 'reason'): + # skip the test in 2.6 as netapp_lib is not fully supported + # HTTPError does not support reason, and it's not worth changing the code + # raise zapi.NaApiError(exc.code, exc.reason) + # AttributeError: 'HTTPError' object has no attribute 'reason' + pytest.skip('this test requires HTTPError.reason which is not available in python 2.6') + zapi_cx = create_ontapzapicx_object() + zapi_cx._refresh_conn = False + zapi_cx._opener = MockOpener(exception=netapp_utils.zapi.urllib.error.HTTPError('url', 400, 'testing', None, None)) + exc = expect_and_capture_ansible_exception(zapi_cx.invoke_elem, netapp_utils.zapi.NaApiError, ZRR['success'][0]) + assert str(exc.value) == 'NetApp API failed. Reason - 400:testing' + + +def test_exception_with_opener_urlerror(): + # ConnectionRefusedError is not defined in 2.7 + connection_error = ConnectionRefusedError('UT') if sys.version_info >= (3, 0) else 'connection_error' + zapi_cx = create_ontapzapicx_object() + zapi_cx._refresh_conn = False + zapi_cx._opener = MockOpener(exception=netapp_utils.zapi.urllib.error.URLError(connection_error)) + exc = expect_and_capture_ansible_exception(zapi_cx.invoke_elem, netapp_utils.zapi.NaApiError, ZRR['success'][0]) + # skip the assert for 2.7 + # ConnectionRefusedError('UT'), with 3.x but ConnectionRefusedError('UT',), with 3.5 + assert str(exc.value).startswith("NetApp API failed. Reason - Unable to connect:(ConnectionRefusedError('UT'") or sys.version_info < (3, 0) + + zapi_cx._opener = MockOpener(exception=netapp_utils.zapi.urllib.error.URLError('connection_error')) + exc = expect_and_capture_ansible_exception(zapi_cx.invoke_elem, netapp_utils.zapi.NaApiError, ZRR['success'][0]) + # URLError('connection_error') with 3.x but URL error:URLError('connection_error',) with 2.7 + assert str(exc.value).startswith("NetApp API failed. Reason - URL error:URLError('connection_error'") + + # force an exception when reading exc.reason + exc = netapp_utils.zapi.urllib.error.URLError('connection_error') + delattr(exc, 'reason') + zapi_cx._opener = MockOpener(exception=exc) + exc = expect_and_capture_ansible_exception(zapi_cx.invoke_elem, netapp_utils.zapi.NaApiError, ZRR['success'][0]) + # URLError('connection_error') with 3.x but URL error:URLError('connection_error',) with 2.7 + assert str(exc.value).startswith("NetApp API failed. Reason - URL error:URLError('connection_error'") + + +def test_response(): + zapi_cx = create_ontapzapicx_object() + zapi_cx._refresh_conn = False + zapi_cx._timeout = 10 + zapi_cx._trace = True + zapi_cx._opener = MockOpener(MockResponse({})) + response = zapi_cx.invoke_elem(ZRR['success'][0]) + print(response) + assert response.to_string() == b'<results/>' + assert zapi_cx._opener.timeout == 10 + + +def test_response_no_netapp_lib(): + zapi_cx = create_ontapzapicx_object() + zapi_cx._refresh_conn = False + zapi_cx._timeout = 10 + zapi_cx._trace = True + zapi_cx._opener = MockOpener(MockResponse({}, True)) + response = zapi_cx.invoke_elem(ZRR['success'][0]) + print(response) + assert response.to_string() == b'<results status="netapp-lib is missing"/>' + assert zapi_cx._opener.timeout == 10 + + +def mock_build_opener(zapi_cx, opener): + def build_opener(): + zapi_cx._opener = opener + return build_opener + + +def test_response_build_opener(): + zapi_cx = create_ontapzapicx_object() + zapi_cx._refresh_conn = False + zapi_cx._trace = True + zapi_cx._build_opener = mock_build_opener(zapi_cx, MockOpener(MockResponse({}))) + response = zapi_cx.invoke_elem(ZRR['success'][0]) + print(response) + assert response.to_string() == b'<results/>' + assert zapi_cx._opener.timeout is None diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_ipaddress.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_ipaddress.py new file mode 100644 index 000000000..5d381d852 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_ipaddress.py @@ -0,0 +1,95 @@ +# Copyright (c) 2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils netapp_ipaddress.py - REST features ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible.module_utils import basic +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import expect_and_capture_ansible_exception, patch_ansible, create_module +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.module_utils import netapp_ipaddress + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +class MockONTAPModule(object): + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_ontap_module(args=None): + if args is None: + args = {'hostname': 'xxx'} + return create_module(MockONTAPModule, args) + + +def test_check_ipaddress_is_present(): + assert netapp_ipaddress._check_ipaddress_is_present(None) is None + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp_ipaddress.HAS_IPADDRESS_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + error = 'Error: the python ipaddress package is required for this module. Import error: None' + assert error in expect_and_capture_ansible_exception(netapp_ipaddress._check_ipaddress_is_present, 'fail', create_ontap_module().module)['msg'] + + +def test_validate_and_compress_ip_address(): + module = create_ontap_module().module + valid_addresses = [ + # IPv4 + ['10.11.12.13', '10.11.12.13'], + # IPv6 + ['1111:0123:0012:0001:abcd:0abc:9891:abcd', '1111:123:12:1:abcd:abc:9891:abcd'], + ['1111:0000:0000:0000:abcd:0abc:9891:abcd', '1111::abcd:abc:9891:abcd'], + ['1111:0000:0000:0012:abcd:0000:0000:abcd', '1111::12:abcd:0:0:abcd'], + ['ffff:ffff:0000:0000:0000:0000:0000:0000', 'ffff:ffff::'], + ] + for before, after in valid_addresses: + assert after == netapp_ipaddress.validate_and_compress_ip_address(before, module) + + +def test_negative_validate_and_compress_ip_address(): + module = create_ontap_module().module + invalid_addresses = [ + # IPv4 + ['10.11.12.345', 'Invalid IP address value 10.11.12.345'], + # IPv6 + ['1111:0123:0012:0001:abcd:0abc:9891:abcg', 'Invalid IP address value'], + ['1111:0000:0000:0000:abcd:9891:abcd', 'Invalid IP address value'], + ['1111:::0012:abcd::abcd', 'Invalid IP address value'], + ] + for before, error in invalid_addresses: + assert error in expect_and_capture_ansible_exception(netapp_ipaddress.validate_and_compress_ip_address, 'fail', before, module)['msg'] + + +def test_netmask_to_len(): + module = create_ontap_module().module + assert netapp_ipaddress.netmask_to_netmask_length('10.10.10.10', '255.255.0.0', module) == 16 + assert netapp_ipaddress.netmask_to_netmask_length('1111::', 16, module) == 16 + assert netapp_ipaddress.netmask_to_netmask_length('1111::', '16', module) == 16 + error = 'Error: only prefix_len is supported for IPv6 addresses, got ffff::' + assert error in expect_and_capture_ansible_exception(netapp_ipaddress.netmask_to_netmask_length, 'fail', '1111::', 'ffff::', module)['msg'] + error = 'Error: Invalid IP network value 10.11.12.13/abc.' + assert error in expect_and_capture_ansible_exception(netapp_ipaddress.netmask_to_netmask_length, 'fail', '10.11.12.13', 'abc', module)['msg'] + + +def test_len_to_netmask(): + module = create_ontap_module().module + assert netapp_ipaddress.netmask_length_to_netmask('10.10.10.10', 16, module) == '255.255.0.0' + assert netapp_ipaddress.netmask_length_to_netmask('1111::', 16, module) == 'ffff::' + + +def test_validate_ip_address_is_network_address(): + module = create_ontap_module().module + assert netapp_ipaddress.validate_ip_address_is_network_address('10.11.12.0', module) is None + assert netapp_ipaddress.validate_ip_address_is_network_address('10.11.12.0/24', module) is None + error = 'Error: Invalid IP network value 10.11.12.0/21' + assert error in expect_and_capture_ansible_exception(netapp_ipaddress.validate_ip_address_is_network_address, 'fail', '10.11.12.0/21', module)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_module.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_module.py new file mode 100644 index 000000000..55729d7cd --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_module.py @@ -0,0 +1,885 @@ +# Copyright (c) 2018-2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for module_utils netapp_module.py """ +from __future__ import (absolute_import, division, print_function) +import copy +__metaclass__ = type + +import pytest +import sys + +from ansible.module_utils import basic +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.module_utils.netapp_module import NetAppModule as na_helper, cmp as na_cmp +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_no_warnings, assert_warning_was_raised, clear_warnings, patch_ansible, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response +from ansible_collections.netapp.ontap.tests.unit.framework import ut_utilities + + +class MockONTAPModule(object): + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + self.na_helper = na_helper(self.module) + self.na_helper.set_parameters(self.module.params) + + +class MockONTAPModuleV2(object): + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + self.na_helper = na_helper(self) + self.na_helper.set_parameters(self.module.params) + + +def create_ontap_module(args=None, version=1): + if version == 2: + return create_module(MockONTAPModuleV2, args) + return create_module(MockONTAPModule, args) + + +def test_get_cd_action_create(): + """ validate cd_action for create """ + current = None + desired = {'state': 'present'} + my_obj = na_helper() + result = my_obj.get_cd_action(current, desired) + assert result == 'create' + + +def test_get_cd_action_delete(): + """ validate cd_action for delete """ + current = {'state': 'absent'} + desired = {'state': 'absent'} + my_obj = na_helper() + result = my_obj.get_cd_action(current, desired) + assert result == 'delete' + + +def test_get_cd_action_already_exist(): + """ validate cd_action for returning None """ + current = {'state': 'whatever'} + desired = {'state': 'present'} + my_obj = na_helper() + result = my_obj.get_cd_action(current, desired) + assert result is None + + +def test_get_cd_action_already_absent(): + """ validate cd_action for returning None """ + current = None + desired = {'state': 'absent'} + my_obj = na_helper() + result = my_obj.get_cd_action(current, desired) + assert result is None + + +def test_get_modified_attributes_for_no_data(): + """ validate modified attributes when current is None """ + current = None + desired = {'name': 'test'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired) + assert result == {} + + +def test_get_modified_attributes(): + """ validate modified attributes """ + current = {'name': ['test', 'abcd', 'xyz', 'pqr'], 'state': 'present'} + desired = {'name': ['abcd', 'abc', 'xyz', 'pqr'], 'state': 'absent'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired) + assert result == desired + + +def test_get_modified_attributes_for_intersecting_mixed_list(): + """ validate modified attributes for list diff """ + current = {'name': [2, 'four', 'six', 8]} + desired = {'name': ['a', 8, 'ab', 'four', 'abcd']} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'name': ['a', 'ab', 'abcd']} + + +def test_get_modified_attributes_for_intersecting_list(): + """ validate modified attributes for list diff """ + current = {'name': ['two', 'four', 'six', 'eight']} + desired = {'name': ['a', 'six', 'ab', 'four', 'abc']} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'name': ['a', 'ab', 'abc']} + + +def test_get_modified_attributes_for_nonintersecting_list(): + """ validate modified attributes for list diff """ + current = {'name': ['two', 'four', 'six', 'eight']} + desired = {'name': ['a', 'ab', 'abd']} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'name': ['a', 'ab', 'abd']} + + +def test_get_modified_attributes_for_list_of_dicts_no_data(): + """ validate modified attributes for list diff """ + current = None + desired = {'address_blocks': [{'start': '10.20.10.40', 'size': 5}]} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {} + + +def test_get_modified_attributes_for_intersecting_list_of_dicts(): + """ validate modified attributes for list diff """ + current = {'address_blocks': [{'start': '10.10.10.23', 'size': 5}, {'start': '10.10.10.30', 'size': 5}]} + desired = {'address_blocks': [{'start': '10.10.10.23', 'size': 5}, {'start': '10.10.10.30', 'size': 5}, {'start': '10.20.10.40', 'size': 5}]} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'address_blocks': [{'start': '10.20.10.40', 'size': 5}]} + + +def test_get_modified_attributes_for_nonintersecting_list_of_dicts(): + """ validate modified attributes for list diff """ + current = {'address_blocks': [{'start': '10.10.10.23', 'size': 5}, {'start': '10.10.10.30', 'size': 5}]} + desired = {'address_blocks': [{'start': '10.20.10.23', 'size': 5}, {'start': '10.20.10.30', 'size': 5}, {'start': '10.20.10.40', 'size': 5}]} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'address_blocks': [{'start': '10.20.10.23', 'size': 5}, {'start': '10.20.10.30', 'size': 5}, {'start': '10.20.10.40', 'size': 5}]} + + +def test_get_modified_attributes_for_list_diff(): + """ validate modified attributes for list diff """ + current = {'name': ['test', 'abcd'], 'state': 'present'} + desired = {'name': ['abcd', 'abc'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'name': ['abc']} + + +def test_get_modified_attributes_for_no_change(): + """ validate modified attributes for same data in current and desired """ + current = {'name': 'test'} + desired = {'name': 'test'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired) + assert result == {} + + +def test_get_modified_attributes_for_an_empty_desired_list(): + """ validate modified attributes for an empty desired list """ + current = {'snapmirror_label': ['daily', 'weekly', 'monthly'], 'state': 'present'} + desired = {'snapmirror_label': [], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired) + assert result == {'snapmirror_label': []} + + +def test_get_modified_attributes_for_an_empty_desired_list_diff(): + """ validate modified attributes for an empty desired list with diff""" + current = {'snapmirror_label': ['daily', 'weekly', 'monthly'], 'state': 'present'} + desired = {'snapmirror_label': [], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'snapmirror_label': []} + + +def test_get_modified_attributes_for_an_empty_current_list(): + """ validate modified attributes for an empty current list """ + current = {'snapmirror_label': [], 'state': 'present'} + desired = {'snapmirror_label': ['daily', 'weekly', 'monthly'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired) + assert result == {'snapmirror_label': ['daily', 'weekly', 'monthly']} + + +def test_get_modified_attributes_for_an_empty_current_list_diff(): + """ validate modified attributes for an empty current list with diff""" + current = {'snapmirror_label': [], 'state': 'present'} + desired = {'snapmirror_label': ['daily', 'weekly', 'monthly'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'snapmirror_label': ['daily', 'weekly', 'monthly']} + + +def test_get_modified_attributes_for_empty_lists(): + """ validate modified attributes for empty lists """ + current = {'snapmirror_label': [], 'state': 'present'} + desired = {'snapmirror_label': [], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired) + assert result == {} + + +def test_get_modified_attributes_for_empty_lists_diff(): + """ validate modified attributes for empty lists with diff """ + current = {'snapmirror_label': [], 'state': 'present'} + desired = {'snapmirror_label': [], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {} + + +def test_get_modified_attributes_equal_lists_with_duplicates(): + """ validate modified attributes for equal lists with duplicates """ + current = {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily'], 'state': 'present'} + desired = {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, False) + assert result == {} + + +def test_get_modified_attributes_equal_lists_with_duplicates_diff(): + """ validate modified attributes for equal lists with duplicates with diff """ + current = {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily'], 'state': 'present'} + desired = {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {} + + +def test_get_modified_attributes_for_current_list_with_duplicates(): + """ validate modified attributes for current list with duplicates """ + current = {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily'], 'state': 'present'} + desired = {'schedule': ['daily', 'daily', 'weekly', 'monthly'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, False) + assert result == {'schedule': ['daily', 'daily', 'weekly', 'monthly']} + + +def test_get_modified_attributes_for_current_list_with_duplicates_diff(): + """ validate modified attributes for current list with duplicates with diff """ + current = {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily'], 'state': 'present'} + desired = {'schedule': ['daily', 'daily', 'weekly', 'monthly'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'schedule': []} + + +def test_get_modified_attributes_for_desired_list_with_duplicates(): + """ validate modified attributes for desired list with duplicates """ + current = {'schedule': ['daily', 'weekly', 'monthly'], 'state': 'present'} + desired = {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, False) + assert result == {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily']} + + +def test_get_modified_attributes_for_desired_list_with_duplicates_diff(): + """ validate modified attributes for desired list with duplicates with diff """ + current = {'schedule': ['daily', 'weekly', 'monthly'], 'state': 'present'} + desired = {'schedule': ['hourly', 'daily', 'daily', 'weekly', 'monthly', 'daily'], 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'schedule': ['hourly', 'daily', 'daily']} + + +def test_get_modified_attributes_exceptions(): + """ validate exceptions """ + current = {'schedule': {'name': 'weekly'}, 'state': 'present'} + desired = {'schedule': 'weekly', 'state': 'present'} + my_obj = create_ontap_module({'hostname': None}) + # mismatch in structure + error = expect_and_capture_ansible_exception(my_obj.na_helper.get_modified_attributes, TypeError, current, desired) + assert "Expecting dict, got: weekly with current: {'name': 'weekly'}" in error + # mismatch in types + if sys.version_info[:2] > (3, 0): + # our cmp function reports an exception. But python 2.x has it's own version. + desired = {'schedule': {'name': 12345}, 'state': 'present'} + error = expect_and_capture_ansible_exception(my_obj.na_helper.get_modified_attributes, TypeError, current, desired) + assert ("unorderable types:" in error # 3.5 + or "'>' not supported between instances of 'str' and 'int'" in error) # 3.9 + + +def test_get_modified_attributes_for_dicts(): + """ validate modified attributes for dict of dicts """ + current = {'schedule': {'name': 'weekly'}, 'state': 'present'} + desired = {'schedule': {'name': 'daily'}, 'state': 'present'} + my_obj = na_helper() + result = my_obj.get_modified_attributes(current, desired, True) + assert result == {'schedule': {'name': 'daily'}} + + +def test_is_rename_action_for_empty_input(): + """ validate rename action for input None """ + source = None + target = None + my_obj = na_helper() + result = my_obj.is_rename_action(source, target) + assert result == source + + +def test_is_rename_action_for_no_source(): + """ validate rename action when source is None """ + source = None + target = 'test2' + my_obj = na_helper() + result = my_obj.is_rename_action(source, target) + assert result is False + + +def test_is_rename_action_for_no_target(): + """ validate rename action when target is None """ + source = 'test2' + target = None + my_obj = na_helper() + result = my_obj.is_rename_action(source, target) + assert result is True + + +def test_is_rename_action(): + """ validate rename action """ + source = 'test' + target = 'test2' + my_obj = na_helper() + result = my_obj.is_rename_action(source, target) + assert result is False + + +def test_required_is_not_set_to_none(): + """ if a key is present, without a value, Ansible sets it to None """ + my_obj = create_ontap_module({'hostname': None}) + msg = 'hostname requires a value, got: None' + assert msg == expect_and_capture_ansible_exception(my_obj.na_helper.check_and_set_parameters, 'fail', my_obj.module)['msg'] + + # force a value different than None + my_obj.module.params['hostname'] = 1 + my_params = my_obj.na_helper.check_and_set_parameters(my_obj.module) + assert set(my_params.keys()) == set(['hostname', 'https', 'validate_certs', 'use_rest']) + + +def test_sanitize_wwn_no_action(): + """ no change """ + initiator = 'tEsT' + expected = initiator + my_obj = na_helper() + result = my_obj.sanitize_wwn(initiator) + assert result == expected + + +def test_sanitize_wwn_no_action_valid_iscsi(): + """ no change """ + initiator = 'iqn.1995-08.com.eXaMpLe:StRiNg' + expected = initiator + my_obj = na_helper() + result = my_obj.sanitize_wwn(initiator) + assert result == expected + + +def test_sanitize_wwn_no_action_valid_wwn(): + """ no change """ + initiator = '01:02:03:04:0A:0b:0C:0d' + expected = initiator.lower() + my_obj = na_helper() + result = my_obj.sanitize_wwn(initiator) + assert result == expected + + +def test_filter_empty_dict(): + """ empty dict return empty dict """ + my_obj = na_helper() + arg = {} + result = my_obj.filter_out_none_entries(arg) + assert arg == result + + +def test_filter_empty_list(): + """ empty list return empty list """ + my_obj = na_helper() + arg = [] + result = my_obj.filter_out_none_entries(arg) + assert arg == result + + +def test_filter_typeerror_on_none(): + """ empty list return empty list """ + my_obj = na_helper() + arg = None + with pytest.raises(TypeError) as exc: + my_obj.filter_out_none_entries(arg) + if sys.version_info[:2] < (3, 0): + # the assert fails on 2.x + return + msg = "unexpected type <class 'NoneType'>" + assert exc.value.args[0] == msg + + +def test_filter_typeerror_on_str(): + """ empty list return empty list """ + my_obj = na_helper() + arg = "" + with pytest.raises(TypeError) as exc: + my_obj.filter_out_none_entries(arg) + if sys.version_info[:2] < (3, 0): + # the assert fails on 2.x + return + msg = "unexpected type <class 'str'>" + assert exc.value.args[0] == msg + + +def test_filter_simple_dict(): + """ simple dict return simple dict """ + my_obj = na_helper() + arg = dict(a=None, b=1, c=None, d=2, e=3) + expected = dict(b=1, d=2, e=3) + result = my_obj.filter_out_none_entries(arg) + assert expected == result + + +def test_filter_simple_list(): + """ simple list return simple list """ + my_obj = na_helper() + arg = [None, 2, 3, None, 5] + expected = [2, 3, 5] + result = my_obj.filter_out_none_entries(arg) + assert expected == result + + +def test_filter_dict_dict(): + """ simple dict return simple dict """ + my_obj = na_helper() + arg = dict(a=None, b=dict(u=1, v=None, w=2), c={}, d=2, e=3) + expected = dict(b=dict(u=1, w=2), d=2, e=3) + result = my_obj.filter_out_none_entries(arg) + assert expected == result + + +def test_filter_list_list(): + """ simple list return simple list """ + my_obj = na_helper() + arg = [None, [1, None, 3], 3, None, 5] + expected = [[1, 3], 3, 5] + result = my_obj.filter_out_none_entries(arg) + assert expected == result + + +def test_filter_dict_list_dict(): + """ simple dict return simple dict """ + my_obj = na_helper() + arg = dict(a=None, b=[dict(u=1, v=None, w=2), 5, None, dict(x=6, y=None)], c={}, d=2, e=3) + expected = dict(b=[dict(u=1, w=2), 5, dict(x=6)], d=2, e=3) + result = my_obj.filter_out_none_entries(arg) + assert expected == result + + +def test_filter_list_dict_list(): + """ simple list return simple list """ + my_obj = na_helper() + arg = [None, [1, None, 3], dict(a=None, b=[7, None, 9], c=None, d=dict(u=None, v=10)), None, 5] + expected = [[1, 3], dict(b=[7, 9], d=dict(v=10)), 5] + result = my_obj.filter_out_none_entries(arg) + assert expected == result + + +def test_convert_value(): + """ positive tests """ + my_obj = na_helper() + for value, convert_to, expected in [ + ('any', None, 'any'), + (12345, None, 12345), + ('12345', int, 12345), + ('any', str, 'any'), + ('true', bool, True), + ('false', bool, False), + ('online', 'bool_online', True), + ('any', 'bool_online', False), + ]: + result, error = my_obj.convert_value(value, convert_to) + assert error is None + assert expected == result + + +def test_convert_value_with_error(): + """ negative tests """ + my_obj = na_helper() + for value, convert_to, expected in [ + (12345, 'any', "Unexpected type:"), + ('any', int, "Unexpected value for int: any"), + ('any', bool, "Unexpected value: any received from ZAPI for boolean attribute"), + ]: + result, error = my_obj.convert_value(value, convert_to) + print(value, convert_to, result, '"%s"' % expected, '"%s"' % error) + assert result is None + assert expected in error + + +def test_convert_value_with_exception(): + """ negative tests """ + my_obj = create_ontap_module({'hostname': None}) + expect_and_capture_ansible_exception(my_obj.na_helper.convert_value, 'fail', 'any', 'any') + + +def get_zapi_info(): + return { + 'a': {'b': '12345', 'bad_stuff': ['a', 'b'], 'none_stuff': None} + } + + +def get_zapi_na_element(zapi_info): + na_element, valid = build_zapi_response(zapi_info) + if valid != 'valid' and sys.version_info[:2] < (2, 7): + pytest.skip('Skipping Unit Tests on 2.6 as netapp-lib is not available') + assert valid == 'valid' + return na_element + + +def test_zapi_get_value(): + na_element = get_zapi_na_element(get_zapi_info()) + my_obj = na_helper() + assert my_obj.zapi_get_value(na_element, ['a', 'b'], convert_to=int) == 12345 + # missing key returns None if sparse dict is allowed (default) + assert my_obj.zapi_get_value(na_element, ['a', 'c'], convert_to=int) is None + # missing key returns 'default' - note, no conversion - if sparse dict is allowed (default) + assert my_obj.zapi_get_value(na_element, ['a', 'c'], convert_to=int, default='default') == 'default' + + +def test_zapi_get_value_with_exception(): + na_element = get_zapi_na_element(get_zapi_info()) + my_obj = create_ontap_module({'hostname': None}) + # KeyError + error = expect_and_capture_ansible_exception(my_obj.na_helper.zapi_get_value, 'fail', na_element, ['a', 'c'], required=True)['msg'] + assert 'No element by given name c.' in error + + +def test_safe_get(): + na_element = get_zapi_na_element(get_zapi_info()) + my_obj = na_helper() + assert my_obj.safe_get(na_element, ['a', 'b']) == '12345' + assert my_obj.safe_get(na_element, ['a', 'c']) is None + assert my_obj.safe_get(get_zapi_info(), ['a', 'b']) == '12345' + assert my_obj.safe_get(get_zapi_info(), ['a', 'c']) is None + assert my_obj.safe_get(get_zapi_info(), ['a', 'none_stuff', 'extra']) is None # TypeError on None + + +def test_safe_get_dict_of_list(): + my_obj = na_helper() + my_dict = {'a': ['b', 'c', {'d': ['e']}]} + assert my_obj.safe_get(my_dict, ['a', 0]) == 'b' + assert my_obj.safe_get(my_dict, ['a', 2, 'd', 0]) == 'e' + assert my_obj.safe_get(my_dict, ['a', 3]) is None + + +def test_safe_get_with_exception(): + na_element = get_zapi_na_element(get_zapi_info()) + my_obj = create_ontap_module({'hostname': None}) + # KeyError + error = expect_and_capture_ansible_exception(my_obj.na_helper.safe_get, KeyError, na_element, ['a', 'c'], allow_sparse_dict=False) + assert 'No element by given name c.' in error + error = expect_and_capture_ansible_exception(my_obj.na_helper.safe_get, KeyError, get_zapi_info(), ['a', 'c'], allow_sparse_dict=False) + assert 'c' == error + # IndexError + error = expect_and_capture_ansible_exception(my_obj.na_helper.safe_get, IndexError, get_zapi_info(), ['a', 'bad_stuff', 4], allow_sparse_dict=False) + print('EXC', error) + if ut_utilities.is_indexerror_exception_formatted(): + assert 'list index out of range' in str(error) + error = expect_and_capture_ansible_exception(my_obj.na_helper.safe_get, IndexError, get_zapi_info(), ['a', 'bad_stuff', -4], allow_sparse_dict=False) + print('EXC', error) + if ut_utilities.is_indexerror_exception_formatted(): + assert 'list index out of range' in str(error) + # TypeError - not sure I can build a valid ZAPI NaElement that can give a type error, but using a dict worked. + error = expect_and_capture_ansible_exception(my_obj.na_helper.safe_get, TypeError, get_zapi_info(), ['a', 'bad_stuff', 'extra'], allow_sparse_dict=False) + # 'list indices must be integers, not str' with 2.7 + # 'list indices must be integers or slices, not str' with 3.x + assert 'list indices must be integers' in error + error = expect_and_capture_ansible_exception(my_obj.na_helper.safe_get, TypeError, get_zapi_info(), ['a', 'none_stuff', 'extra'], allow_sparse_dict=False) + # 'NoneType' object has no attribute '__getitem__' with 2.7 + # 'NoneType' object is not subscriptable with 3.x + assert "'NoneType' object " in error + + +def test_get_value_for_bool(): + my_obj = na_helper() + for value, from_zapi, expected in [ + (None, 'any', None), + ('true', True, True), + ('false', True, False), + ('any', True, False), # no error checking if key is not present + (True, False, 'true'), + (False, False, 'false'), + ('any', False, 'true'), # no error checking if key is not present + ]: + result = my_obj.get_value_for_bool(from_zapi, value) + print(value, from_zapi, result) + assert result == expected + + +def test_get_value_for_bool_with_exception(): + na_element = get_zapi_na_element(get_zapi_info()) + my_obj = create_ontap_module({'hostname': None}) + # Error with from_zapi=True if key is present + error = expect_and_capture_ansible_exception(my_obj.na_helper.get_value_for_bool, TypeError, True, 1234, 'key') + assert "expecting 'str' type for 'key': 1234" in error + error = expect_and_capture_ansible_exception(my_obj.na_helper.get_value_for_bool, ValueError, True, 'any', 'key') + assert "Unexpected value: 'any' received from ZAPI for boolean attribute: 'key'" == error + # TypeError - expecting a bool + error = expect_and_capture_ansible_exception(my_obj.na_helper.get_value_for_bool, TypeError, False, 'any', 'key') + assert "expecting 'bool' type for 'key': 'any'" in error + + +def test_get_value_for_int(): + my_obj = na_helper() + for value, from_zapi, expected in [ + (None, 'any', None), + ('12345', True, 12345), + (12345, True, 12345), # no error checking if key is not present + (12345, False, '12345'), + ]: + result = my_obj.get_value_for_int(from_zapi, value) + print(value, from_zapi, result) + assert result == expected + + +def test_get_value_for_int_with_exception(): + na_element = get_zapi_na_element(get_zapi_info()) + my_obj = create_ontap_module({'hostname': None}) + # Error with from_zapi=True if key is present + error = expect_and_capture_ansible_exception(my_obj.na_helper.get_value_for_int, TypeError, True, 1234, 'key') + assert "expecting 'str' type for 'key': 1234" in error + error = expect_and_capture_ansible_exception(my_obj.na_helper.get_value_for_int, ValueError, True, 'any', 'key') + assert "invalid literal for int() with base 10: 'any'" == error + # TypeError - expecting a int + error = expect_and_capture_ansible_exception(my_obj.na_helper.get_value_for_int, TypeError, False, 'any', 'key') + assert "expecting 'int' type for 'key': 'any'" in error + + +def test_get_value_for_list(): + my_obj = na_helper() + zapi_info = { + 'a': [{'b': 'a1'}, {'b': 'a2'}, {'b': 'a3'}] + } + for from_zapi, zapi_parent, zapi_child, data, expected in [ + (True, None, None, None, []), + (True, get_zapi_na_element(zapi_info), None, None, [None]), + (True, get_zapi_na_element(get_zapi_info()).get_child_by_name('a'), None, None, ['12345', None, None]), + (True, get_zapi_na_element(zapi_info).get_child_by_name('a'), None, None, ['a1', 'a2', 'a3']), + (False, 'parent', 'child', [], b'<parent/>'), + (False, 'parent', 'child', ['1', '1'], b'<parent><child>1</child><child>1</child></parent>'), + ]: + result = my_obj.get_value_for_list(from_zapi, zapi_parent, zapi_child, data) + print(from_zapi, expected, result) + if from_zapi: + if zapi_parent: + print(zapi_parent.to_string()) + # ordering maybe different with 3.5 compared to 3.9 or 2.7 + assert set(result) == set(expected) + else: + print(result.to_string()) + assert result.to_string() == expected + + +def test_zapi_get_attrs(): + my_obj = na_helper() + zapi_info = { + 'a': {'b': 'a1', 'c': 'a2', 'd': 'a3', 'int': '123'} + } + naelement = get_zapi_na_element(zapi_info) + attr_dict = { + 'first': {'key_list': ['a', 'b']} + } + result = {} + my_obj.zapi_get_attrs(naelement, attr_dict, result) + assert result == {'first': 'a1'} + + # if element not found return None, unless omitnone is True + attr_dict = { + 'none': {'key_list': ['a', 'z'], 'omitnone': True} + } + my_obj.zapi_get_attrs(naelement, attr_dict, result) + assert result == {'first': 'a1'} + + # if element not found return None when required and omitnone are False + attr_dict = { + 'none': {'key_list': ['a', 'z']} + } + my_obj.zapi_get_attrs(naelement, attr_dict, result) + assert result == {'first': 'a1', 'none': None} + + # if element not found return default + result = {} + attr_dict = { + 'none': {'key_list': ['a', 'z'], 'default': 'some_default'} + } + my_obj.zapi_get_attrs(naelement, attr_dict, result) + assert result == {'none': 'some_default'} + + # convert to int + result = {} + attr_dict = { + 'int': {'key_list': ['a', 'int'], 'convert_to': int} + } + my_obj.zapi_get_attrs(naelement, attr_dict, result) + assert result == {'int': 123} + + # if element not found return None, unless required is True + my_obj = create_ontap_module({'hostname': 'abc'}) + attr_dict = { + 'none': {'key_list': ['a', 'z'], 'required': True} + } + # the contents of to_string() may be in a different sequence depending on the pytohn version + assert expect_and_capture_ansible_exception(my_obj.na_helper.zapi_get_attrs, 'fail', naelement, attr_dict, result)['msg'].startswith(( + "Error reading ['a', 'z'] from b'<results status=\"passed\"><a>", # python 3.x + "Error reading ['a', 'z'] from <results status=\"passed\"><a>" # python 2.7 + )) + + +def test_set_parameters(): + my_obj = na_helper() + adict = dict((x, x * x) for x in range(10)) + assert my_obj.set_parameters(adict) == adict + assert my_obj.parameters == adict + assert len(my_obj.parameters) == 10 + + # None values are copied + adict[3] = None + assert my_obj.set_parameters(adict) != adict + assert my_obj.parameters != adict + assert len(my_obj.parameters) == 9 + + +def test_get_caller(): + assert na_helper.get_caller(0) == 'get_caller' + assert na_helper.get_caller(1) == 'test_get_caller' + + def one(depth): + return na_helper.get_caller(depth) + assert one(1) == 'one' + + def two(): + return one(2) + assert two() == 'two' + + def three(): + return two(), one(3) + assert three() == ('two', 'test_get_caller') + + +@patch('traceback.extract_stack') +def test_get_caller_2_7(mock_frame): + frame = ('first', 'second', 'function_name') + mock_frame.return_value = [frame] + assert na_helper.get_caller(0) == 'function_name' + + +@patch('traceback.extract_stack') +def test_get_caller_bad_format(mock_frame): + frame = ('first', 'second') + mock_frame.return_value = [frame] + assert na_helper.get_caller(0) == "Error retrieving function name: tuple index out of range - [('first', 'second')]" + + +def test_fail_on_error(): + my_obj = create_ontap_module({'hostname': 'abc'}) + assert my_obj.na_helper.fail_on_error(None) is None + assert expect_and_capture_ansible_exception(my_obj.na_helper.fail_on_error, 'fail', 'error_msg')['msg'] ==\ + 'Error in expect_and_capture_ansible_exception: error_msg' + assert expect_and_capture_ansible_exception(my_obj.na_helper.fail_on_error, 'fail', 'error_msg', 'api')['msg'] ==\ + 'Error in expect_and_capture_ansible_exception: calling api: api: error_msg' + previous_errors = ['some_errror'] + exc = expect_and_capture_ansible_exception(my_obj.na_helper.fail_on_error, 'fail', 'error_msg', 'api', previous_errors=previous_errors) + assert exc['msg'] == 'Error in expect_and_capture_ansible_exception: calling api: api: error_msg' + assert exc['previous_errors'] == previous_errors[0] + exc = expect_and_capture_ansible_exception(my_obj.na_helper.fail_on_error, 'fail', 'error_msg', 'api', True) + assert exc['msg'] == 'Error in expect_and_capture_ansible_exception: calling api: api: error_msg' + assert exc['stack'] + delattr(my_obj.na_helper, 'ansible_module') + assert expect_and_capture_ansible_exception(my_obj.na_helper.fail_on_error, AttributeError, 'error_message') ==\ + "Expecting self.ansible_module to be set when reporting {'msg': 'Error in expect_and_capture_ansible_exception: error_message'}" + + +def test_cmp(): + assert na_cmp(None, 'any') == -1 + # string comparison ignores case + assert na_cmp('ABC', 'abc') == 0 + assert na_cmp('abcd', 'abc') == 1 + assert na_cmp('abd', 'abc') == 1 + assert na_cmp(['abd', 'abc'], ['abc', 'abd']) == 0 + # list comparison ignores case + assert na_cmp(['ABD', 'abc'], ['abc', 'abd']) == 0 + # but not duplicates + assert na_cmp(['ABD', 'ABD', 'abc'], ['abc', 'abd']) == 1 + + +def test_fall_back_to_zapi(): + my_obj = create_ontap_module({'hostname': 'abc'}, version=2) + parameters = {'use_rest': 'never'} + assert my_obj.na_helper.fall_back_to_zapi(my_obj.na_helper.ansible_module, 'some message', parameters) is None + assert_no_warnings() + + parameters = {'use_rest': 'auto'} + assert my_obj.na_helper.fall_back_to_zapi(my_obj.na_helper.ansible_module, 'some message', parameters) is False + assert_warning_was_raised('Falling back to ZAPI: some message') + + parameters = {'use_rest': 'always'} + clear_warnings() + assert 'Error: some message' in expect_and_capture_ansible_exception( + my_obj.na_helper.fall_back_to_zapi, 'fail', my_obj.na_helper.ansible_module, 'some message', parameters)['msg'] + assert_no_warnings() + + +def test_module_deprecated(): + my_obj = create_ontap_module({'hostname': 'abc'}) + assert my_obj.na_helper.module_deprecated(my_obj.na_helper.ansible_module) is None + assert_warning_was_raised('This module only supports ZAPI and is deprecated. It will no longer work with newer versions of ONTAP. ' + 'The final ONTAP version to support ZAPI is ONTAP 9.12.1.') + + +def test_module_replaces(): + my_obj = create_ontap_module({'hostname': 'abc'}) + new_module = 'na_ontap_new_modules' + assert my_obj.na_helper.module_replaces(new_module, my_obj.na_helper.ansible_module) is None + assert_warning_was_raised('netapp.ontap.%s should be used instead.' % new_module) + + +def test_compare_chmod_value(): + myobj = na_helper() + assert myobj.compare_chmod_value("0777", "---rwxrwxrwx") is True + assert myobj.compare_chmod_value("777", "---rwxrwxrwx") is True + assert myobj.compare_chmod_value("7777", "sstrwxrwxrwx") is True + assert myobj.compare_chmod_value("4555", "s--r-xr-xr-x") is True + assert myobj.compare_chmod_value(None, "---rwxrwxrwx") is False + assert myobj.compare_chmod_value("755", "rwxrwxrwxrwxr") is False + assert myobj.compare_chmod_value("777", "---ssxrwxrwx") is False + assert myobj.compare_chmod_value("7777", "rwxrwxrwxrwx") is False + assert myobj.compare_chmod_value("7777", "7777") is True + + +def test_ignore_missing_vserver_on_delete(): + my_obj = create_ontap_module({'hostname': 'abc'}) + assert not my_obj.na_helper.ignore_missing_vserver_on_delete('error') + my_obj.na_helper.parameters['state'] = 'absent' + error = 'Internal error, vserver name is required, when processing error: error_msg' + assert error in expect_and_capture_ansible_exception(my_obj.na_helper.ignore_missing_vserver_on_delete, 'fail', 'error_msg')['msg'] + my_obj.na_helper.parameters['vserver'] = 'svm' + error = 'Internal error, error should contain "message" key, found:' + assert error in expect_and_capture_ansible_exception(my_obj.na_helper.ignore_missing_vserver_on_delete, 'fail', {'error_msg': 'error'})['msg'] + error = 'Internal error, error should be str or dict, found:' + assert error in expect_and_capture_ansible_exception(my_obj.na_helper.ignore_missing_vserver_on_delete, 'fail', ['error_msg'])['msg'] + assert not my_obj.na_helper.ignore_missing_vserver_on_delete('error') + assert my_obj.na_helper.ignore_missing_vserver_on_delete({'message': 'SVM "svm" does not exist.'}) + + +def test_remove_hal_links(): + my_obj = create_ontap_module({'hostname': 'abc'}) + assert my_obj.na_helper.remove_hal_links(None) is None + assert my_obj.na_helper.remove_hal_links('string') is None + adict = { + '_links': 'whatever', + 'other': 'other' + } + # dict + test_object = copy.deepcopy(adict) + assert my_obj.na_helper.remove_hal_links(test_object) is None + assert '_links' not in test_object + # list of dicts + test_object = [copy.deepcopy(adict)] * 5 + assert my_obj.na_helper.remove_hal_links(test_object) is None + assert all('_links' not in elem for elem in test_object) + # dict of dicts + test_object = {'a': copy.deepcopy(adict), 'b': copy.deepcopy(adict)} + assert my_obj.na_helper.remove_hal_links(test_object) is None + assert all('_links' not in value for value in test_object.values()) + # list of list of dicts + items = [copy.deepcopy(adict)] * 5 + test_object = [items, items] + assert my_obj.na_helper.remove_hal_links(test_object) is None + assert all('_links' not in elem for elems in test_object for elem in elems) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_rest.py new file mode 100644 index 000000000..98cac5390 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_rest.py @@ -0,0 +1,586 @@ +# Copyright (c) 2018-2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils netapp.py - REST features ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os.path +import pytest +import sys +import tempfile + +from ansible.module_utils import basic +from ansible_collections.netapp.ontap.tests.unit.compat.mock import call, patch + +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + assert_no_warnings, assert_warning_was_raised, create_module, expect_and_capture_ansible_exception, patch_ansible, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +VERSION = {'version': { + 'full': '9.8.45', + 'generation': 9, + 'major': 8, + 'minor': 45 +}} + +SRR = rest_responses({ + 'vservers_with_admin': (200, { + 'records': [ + {'vserver': 'vserver1', 'type': 'data '}, + {'vserver': 'vserver2', 'type': 'data '}, + {'vserver': 'cserver', 'type': 'admin'} + ]}, None), + 'vservers_without_admin': (200, { + 'records': [ + {'vserver': 'vserver1', 'type': 'data '}, + {'vserver': 'vserver2', 'type': 'data '}, + ]}, None), + 'vservers_single': (200, { + 'records': [ + {'vserver': 'single', 'type': 'data '}, + ]}, None), + 'vservers_empty': (200, {}, None), + 'vservers_error': (200, { + 'records': [ + {'vserver': 'single', 'type': 'data '}, + ]}, 'some error'), + 'nodes': (200, { + 'records': [ + VERSION, + {'node': 'node2', 'version': 'version'}, + ]}, None), + 'precluster_error': (400, {}, {'message': 'are available in precluster.'}), +}) + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'cert_filepath': None, + 'key_filepath': None, +} + +CERT_ARGS = { + 'hostname': 'test', + 'cert_filepath': 'test_pem.pem', + 'key_filepath': 'test_key.key' +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_restapi_object(default_args, module_args=None): + module = create_module(MockONTAPModule, default_args, module_args) + return netapp_utils.OntapRestAPI(module.module) + + +def test_write_to_file(): + ''' check error and debug logs can be written to disk ''' + rest_api = create_restapi_object(DEFAULT_ARGS) + # logging an error also add a debug record + rest_api.log_error(404, '404 error') + print(rest_api.errors) + print(rest_api.debug_logs) + # logging a debug record only + rest_api.log_debug(501, '501 error') + print(rest_api.errors) + print(rest_api.debug_logs) + + try: + tempdir = tempfile.TemporaryDirectory() + filepath = os.path.join(tempdir.name, 'log.txt') + except AttributeError: + # python 2.7 does not support tempfile.TemporaryDirectory + # we're taking a small chance that there is a race condition + filepath = '/tmp/deleteme354.txt' + rest_api.write_debug_log_to_file(filepath=filepath, append=False) + with open(filepath, 'r') as log: + lines = log.readlines() + assert len(lines) == 4 + assert lines[0].strip() == 'Debug: 404' + assert lines[2].strip() == 'Debug: 501' + + # Idempotent, as append is False + rest_api.write_debug_log_to_file(filepath=filepath, append=False) + with open(filepath, 'r') as log: + lines = log.readlines() + assert len(lines) == 4 + assert lines[0].strip() == 'Debug: 404' + assert lines[2].strip() == 'Debug: 501' + + # Duplication, as append is True + rest_api.write_debug_log_to_file(filepath=filepath, append=True) + with open(filepath, 'r') as log: + lines = log.readlines() + assert len(lines) == 8 + assert lines[0].strip() == 'Debug: 404' + assert lines[2].strip() == 'Debug: 501' + assert lines[4].strip() == 'Debug: 404' + assert lines[6].strip() == 'Debug: 501' + + rest_api.write_errors_to_file(filepath=filepath, append=False) + with open(filepath, 'r') as log: + lines = log.readlines() + assert len(lines) == 1 + assert lines[0].strip() == 'Error: 404 error' + + # Idempotent, as append is False + rest_api.write_errors_to_file(filepath=filepath, append=False) + with open(filepath, 'r') as log: + lines = log.readlines() + assert len(lines) == 1 + assert lines[0].strip() == 'Error: 404 error' + + # Duplication, as append is True + rest_api.write_errors_to_file(filepath=filepath, append=True) + with open(filepath, 'r') as log: + lines = log.readlines() + assert len(lines) == 2 + assert lines[0].strip() == 'Error: 404 error' + assert lines[1].strip() == 'Error: 404 error' + + # Empty data + rest_api.write_to_file(tag='atag', filepath=filepath, append=False) + with open(filepath, 'r') as log: + lines = log.readlines() + assert len(lines) == 1 + assert lines[0].strip() == 'atag' + + builtins = 'builtins' if sys.version_info > (3, 0) else '__builtin__' + + with patch('%s.open' % builtins) as mock_open: + mock_open.side_effect = KeyError('Open error') + exc = expect_and_capture_ansible_exception(rest_api.write_to_file, KeyError, tag='atag') + assert str(exc) == 'Open error' + print(mock_open.mock_calls) + assert call('/tmp/ontap_log', 'a') in mock_open.mock_calls + + +def test_is_rest_true(): + ''' is_rest is expected to return True ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + is_rest = rest_api.is_rest() + print(rest_api.errors) + print(rest_api.debug_logs) + assert is_rest + + +def test_is_rest_false(): + ''' is_rest is expected to return False ''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + is_rest = rest_api.is_rest() + print(rest_api.errors) + print(rest_api.debug_logs) + assert not is_rest + assert rest_api.errors[0] == SRR['is_zapi'][2] + assert rest_api.debug_logs[0][0] == SRR['is_zapi'][0] # status_code + assert rest_api.debug_logs[0][1] == SRR['is_zapi'][2] # error + + +def test_is_rest_false_9_5(): + ''' is_rest is expected to return False ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_95']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + is_rest = rest_api.is_rest() + print(rest_api.errors) + print(rest_api.debug_logs) + assert not is_rest + assert not rest_api.errors + assert not rest_api.debug_logs + + +def test_is_rest_true_9_6(): + ''' is_rest is expected to return False ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + is_rest = rest_api.is_rest() + print(rest_api.errors) + print(rest_api.debug_logs) + assert is_rest + assert not rest_api.errors + assert not rest_api.debug_logs + + +def test_fail_has_username_password_and_cert(): + ''' failure case in auth_method ''' + module_args = dict(cert_filepath='dummy') + msg = 'Error: cannot have both basic authentication (username/password) and certificate authentication (cert/key files)' + assert expect_and_capture_ansible_exception(create_restapi_object, 'fail', DEFAULT_ARGS, module_args)['msg'] == msg + + +def test_fail_has_username_password_and_key(): + ''' failure case in auth_method ''' + module_args = dict(key_filepath='dummy') + msg = 'Error: cannot have both basic authentication (username/password) and certificate authentication (cert/key files)' + assert expect_and_capture_ansible_exception(create_restapi_object, 'fail', DEFAULT_ARGS, module_args)['msg'] == msg + + +def test_fail_has_username_and_cert(): + ''' failure case in auth_method ''' + args = dict(DEFAULT_ARGS) + module_args = dict(cert_filepath='dummy') + del args['password'] + msg = 'Error: username and password have to be provided together and cannot be used with cert or key files' + assert expect_and_capture_ansible_exception(create_restapi_object, 'fail', args, module_args)['msg'] == msg + + +def test_fail_has_password_and_cert(): + ''' failure case in auth_method ''' + args = dict(DEFAULT_ARGS) + module_args = dict(cert_filepath='dummy') + del args['username'] + msg = 'Error: username and password have to be provided together and cannot be used with cert or key files' + assert expect_and_capture_ansible_exception(create_restapi_object, 'fail', args, module_args)['msg'] == msg + + +def test_has_username_password(): + ''' auth_method reports expected value ''' + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_api.auth_method == 'speedy_basic_auth' + + +def test_has_cert_no_key(): + ''' auth_method reports expected value ''' + args = dict(CERT_ARGS) + del args['key_filepath'] + rest_api = create_restapi_object(args) + assert rest_api.auth_method == 'single_cert' + + +def test_has_cert_and_key(): + ''' auth_method reports expected value ''' + rest_api = create_restapi_object(CERT_ARGS) + assert rest_api.auth_method == 'cert_key' + + +def test_get_cserver(): + ''' using REST to get cserver - not sure if it's needed ''' + register_responses([ + ('GET', 'private/cli/vserver', SRR['vservers_with_admin']), + ('GET', 'private/cli/vserver', SRR['vservers_without_admin']), + ('GET', 'private/cli/vserver', SRR['vservers_single']), + ('GET', 'private/cli/vserver', SRR['vservers_empty']), + ('GET', 'private/cli/vserver', SRR['vservers_error']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + assert netapp_utils.get_cserver(rest_api, is_rest=True) == 'cserver' + assert netapp_utils.get_cserver(rest_api, is_rest=True) is None + assert netapp_utils.get_cserver(rest_api, is_rest=True) == 'single' + assert netapp_utils.get_cserver(rest_api, is_rest=True) is None + assert netapp_utils.get_cserver(rest_api, is_rest=True) is None + + +def test_ontaprestapi_init(): + module_args = {'http_port': 123} + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_api.url == 'https://%s/api/' % DEFAULT_ARGS['hostname'] + rest_api = create_restapi_object(DEFAULT_ARGS, module_args) + assert rest_api.url == 'https://%s:%d/api/' % (DEFAULT_ARGS['hostname'], module_args['http_port']) + + +@patch('logging.basicConfig') +def test_ontaprestapi_logging(mock_config): + create_restapi_object(DEFAULT_ARGS) + assert not mock_config.mock_calls + module_args = {'feature_flags': {'trace_apis': True}} + create_restapi_object(DEFAULT_ARGS, module_args) + assert len(mock_config.mock_calls) == 1 + + +def test_requires_ontap_9_6(): + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_api.requires_ontap_9_6('module_name') == 'module_name only supports REST, and requires ONTAP 9.6 or later.' + + +def test_requires_ontap_version(): + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_api.requires_ontap_version('module_name', '9.1.2') == 'module_name only supports REST, and requires ONTAP 9.1.2 or later.' + + +def test_options_require_ontap_version(): + rest_api = create_restapi_object(DEFAULT_ARGS) + base = 'using %s requires ONTAP 9.1.2 or later and REST must be enabled' + msg = base % 'option_name' + msg_m = base % "any of ['op1', 'op2', 'op3']" + assert rest_api.options_require_ontap_version('option_name', '9.1.2') == '%s.' % msg + assert rest_api.options_require_ontap_version('option_name', '9.1.2', use_rest=True) == '%s - using REST.' % msg + assert rest_api.options_require_ontap_version('option_name', '9.1.2', use_rest=False) == '%s - using ZAPI.' % msg + assert rest_api.options_require_ontap_version(['op1', 'op2', 'op3'], '9.1.2') == '%s.' % msg_m + rest_api.set_version(VERSION) + assert rest_api.options_require_ontap_version(['option_name'], '9.1.2') == '%s - ONTAP version: %s.' % (msg, VERSION['version']['full']) + assert rest_api.options_require_ontap_version(['op1', 'op2', 'op3'], '9.1.2', use_rest=True) ==\ + '%s - ONTAP version: %s - using REST.' % (msg_m, VERSION['version']['full']) + + +def test_meets_rest_minimum_version(): + rest_api = create_restapi_object(DEFAULT_ARGS) + rest_api.set_version(VERSION) + assert rest_api.meets_rest_minimum_version(True, VERSION['version']['generation'], VERSION['version']['major']) + assert rest_api.meets_rest_minimum_version(True, VERSION['version']['generation'], VERSION['version']['major'] - 1) + assert not rest_api.meets_rest_minimum_version(True, VERSION['version']['generation'], VERSION['version']['major'] + 1) + assert not rest_api.meets_rest_minimum_version(True, VERSION['version']['generation'], VERSION['version']['major'], VERSION['version']['minor'] + 1) + + +def test_fail_if_not_rest_minimum_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['generic_error']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_96']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + rest_api.use_rest = 'never' + # validate consistency bug in fail_if_not_rest_minimum_version + assert expect_and_capture_ansible_exception(rest_api.fail_if_not_rest_minimum_version, 'fail', 'module_name', 9, 6)['msg'] ==\ + 'Error: REST is required for this module, found: "use_rest: never".' + # never + rest_api = create_restapi_object(DEFAULT_ARGS, {'use_rest': 'never'}) + assert expect_and_capture_ansible_exception(rest_api.fail_if_not_rest_minimum_version, 'fail', 'module_name', 9, 6)['msg'] ==\ + 'Error: REST is required for this module, found: "use_rest: never".' + # REST error + rest_api = create_restapi_object(DEFAULT_ARGS, {'use_rest': 'auto'}) + assert expect_and_capture_ansible_exception(rest_api.fail_if_not_rest_minimum_version, 'fail', 'module_name', 9, 6)['msg'] ==\ + 'Error using REST for version, error: Expected error. Error using REST for version, status_code: 400.' + # version mismatch + assert expect_and_capture_ansible_exception(rest_api.fail_if_not_rest_minimum_version, 'fail', 'module_name', 9, 7)['msg'] ==\ + 'Error: module_name only supports REST, and requires ONTAP 9.7.0 or later. Found: 9.6.0.' + # version match + assert rest_api.fail_if_not_rest_minimum_version('module_name', 9, 6) is None + + +def test_check_required_library(): + rest_api = create_restapi_object(DEFAULT_ARGS) + msg = 'Failed to import the required Python library (requests)' + with patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_REQUESTS', False): + assert expect_and_capture_ansible_exception(rest_api.check_required_library, 'fail')['msg'].startswith(msg) + + +def test_build_headers(): + rest_api = create_restapi_object(DEFAULT_ARGS) + app_version = 'basic.py/%s' % netapp_utils.COLLECTION_VERSION + assert rest_api.build_headers() == {'X-Dot-Client-App': app_version} + assert rest_api.build_headers(accept='accept') == {'X-Dot-Client-App': app_version, 'accept': 'accept'} + assert rest_api.build_headers(vserver_name='vserver_name') == {'X-Dot-Client-App': app_version, 'X-Dot-SVM-Name': 'vserver_name'} + assert rest_api.build_headers(vserver_uuid='vserver_uuid') == {'X-Dot-Client-App': app_version, 'X-Dot-SVM-UUID': 'vserver_uuid'} + assert len(rest_api.build_headers(accept='accept', vserver_name='name', vserver_uuid='uuid')) == 4 + + +def test_get_method(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + assert create_restapi_object(DEFAULT_ARGS).get('cluster') == (SRR['is_rest_96'][1], None) + + +def test_post_method(): + register_responses([ + ('POST', 'cluster', SRR['is_rest_96']), + ]) + assert create_restapi_object(DEFAULT_ARGS).post('cluster', None) == (SRR['is_rest_96'][1], None) + + +def test_patch_method(): + register_responses([ + ('PATCH', 'cluster', SRR['is_rest_96']), + ]) + assert create_restapi_object(DEFAULT_ARGS).patch('cluster', None) == (SRR['is_rest_96'][1], None) + + +def test_delete_method(): + register_responses([ + ('DELETE', 'cluster', SRR['is_rest_96']), + ]) + assert create_restapi_object(DEFAULT_ARGS).delete('cluster', None) == (SRR['is_rest_96'][1], None) + + +def test_options_method(): + register_responses([ + ('OPTIONS', 'cluster', SRR['is_rest_96']), + ]) + assert create_restapi_object(DEFAULT_ARGS).options('cluster', None) == (SRR['is_rest_96'][1], None) + + +def test_get_node_version_using_rest(): + register_responses([ + ('GET', 'cluster/nodes', SRR['nodes']), + ]) + assert create_restapi_object(DEFAULT_ARGS).get_node_version_using_rest() == (200, SRR['nodes'][1]['records'][0], None) + + +def test_get_ontap_version_using_rest(): + register_responses([ + ('GET', 'cluster', SRR['precluster_error']), + ('GET', 'cluster/nodes', SRR['nodes']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_api.get_ontap_version_using_rest() == 200 + assert rest_api.ontap_version['major'] == VERSION['version']['major'] + assert rest_api.ontap_version['valid'] + + +def test__is_rest(): + if not sys.version_info > (3, 0): + return + rest_api = create_restapi_object(DEFAULT_ARGS) + rest_api.use_rest = 'invalid' + msg = "use_rest must be one of: never, always, auto. Got: 'invalid'" + assert rest_api._is_rest() == (False, msg) + # testing always with used_unsupported_rest_properties + rest_api.use_rest = 'always' + msg = "REST API currently does not support 'xyz'" + assert rest_api._is_rest(used_unsupported_rest_properties=['xyz']) == (True, msg) + # testing never + rest_api.use_rest = 'never' + assert rest_api._is_rest() == (False, None) + # we need the version + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + # testing always unconditionnally and with partially_supported_rest_properties + rest_api.use_rest = 'always' + msg = 'Error: Minimum version of ONTAP for xyz is (9, 7). Current version: (9, 6, 0).' + assert rest_api._is_rest(partially_supported_rest_properties=[('xyz', (9, 7))], parameters=['xyz']) == (True, msg) + # No error when version requirement is matched + assert rest_api._is_rest(partially_supported_rest_properties=[('xyz', (9, 6))], parameters=['xyz']) == (True, None) + # No error when parameter is not used + assert rest_api._is_rest(partially_supported_rest_properties=[('abc', (9, 6))], parameters=['xyz']) == (True, None) + # testing auto with used_unsupported_rest_properties + rest_api.use_rest = 'auto' + assert rest_api._is_rest(used_unsupported_rest_properties=['xyz']) == (False, None) + # TODO: check warning + + +def test_is_rest_supported_properties(): + rest_api = create_restapi_object(DEFAULT_ARGS) + rest_api.use_rest = 'always' + assert expect_and_capture_ansible_exception(rest_api.is_rest_supported_properties, 'fail', ['xyz'], ['xyz'])['msg'] ==\ + "REST API currently does not support 'xyz'" + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + assert rest_api.is_rest_supported_properties(['abc'], ['xyz']) + assert rest_api.is_rest_supported_properties(['abc'], ['xyz'], report_error=True) == (True, None) + + +def test_is_rest_partially_supported_properties(): + if not sys.version_info > (3, 0): + return + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + rest_api.use_rest = 'auto' + assert not rest_api.is_rest_supported_properties(['xyz'], None, [('xyz', (9, 8, 1))]) + assert_warning_was_raised('Falling back to ZAPI because of unsupported option(s) or option value(s) "xyz" in REST require (9, 8, 1)') + rest_api = create_restapi_object(DEFAULT_ARGS) + rest_api.use_rest = 'auto' + assert rest_api.is_rest_supported_properties(['xyz'], None, [('xyz', (9, 8, 1))]) + + +def test_is_rest(): + rest_api = create_restapi_object(DEFAULT_ARGS) + # testing always with used_unsupported_rest_properties + rest_api.use_rest = 'always' + msg = "REST API currently does not support 'xyz'" + assert rest_api.is_rest(used_unsupported_rest_properties=['xyz']) == (True, msg) + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + assert rest_api.is_rest() + + +def test_set_version(): + rest_api = create_restapi_object(DEFAULT_ARGS) + rest_api.set_version(VERSION) + print('VERSION', rest_api.ontap_version) + assert rest_api.ontap_version['generation'] == VERSION['version']['generation'] + assert rest_api.ontap_version['valid'] + rest_api.set_version({}) + assert not rest_api.ontap_version['valid'] + + +def test_force_ontap_version_local(): + """ test get_ontap_version_from_params in isolation """ + rest_api = create_restapi_object(DEFAULT_ARGS) + rest_api.set_version(VERSION) + print('VERSION', rest_api.ontap_version) + assert rest_api.ontap_version['generation'] == VERSION['version']['generation'] + # same version + rest_api.force_ontap_version = VERSION['version']['full'] + assert not rest_api.get_ontap_version_from_params() + # different versions + rest_api.force_ontap_version = '10.8.1' + warning = rest_api.get_ontap_version_from_params() + assert rest_api.ontap_version['generation'] != VERSION['version']['generation'] + assert rest_api.ontap_version['generation'] == 10 + assert 'Forcing ONTAP version to 10.8.1 but current version is 9.8.45' in warning + # version could not be read + rest_api.set_version({}) + rest_api.force_ontap_version = '10.8' + warning = rest_api.get_ontap_version_from_params() + assert rest_api.ontap_version['generation'] != VERSION['version']['generation'] + assert rest_api.ontap_version['generation'] == 10 + assert rest_api.ontap_version['minor'] == 0 + assert 'Forcing ONTAP version to 10.8, unable to read current version:' in warning + + +def test_negative_force_ontap_version_local(): + """ test get_ontap_version_from_params in isolation """ + rest_api = create_restapi_object(DEFAULT_ARGS) + # non numeric + rest_api.force_ontap_version = '9.8P4' + error = 'Error: unexpected format in force_ontap_version, expecting G.M.m or G.M, as in 9.10.1, got: 9.8P4,' + assert error in expect_and_capture_ansible_exception(rest_api.get_ontap_version_from_params, 'fail')['msg'] + # too short + rest_api.force_ontap_version = '9' + error = 'Error: unexpected format in force_ontap_version, expecting G.M.m or G.M, as in 9.10.1, got: 9,' + assert error in expect_and_capture_ansible_exception(rest_api.get_ontap_version_from_params, 'fail')['msg'] + # too long + rest_api.force_ontap_version = '9.1.2.3' + error = 'Error: unexpected format in force_ontap_version, expecting G.M.m or G.M, as in 9.10.1, got: 9.1.2.3,' + assert error in expect_and_capture_ansible_exception(rest_api.get_ontap_version_from_params, 'fail')['msg'] + + +def test_force_ontap_version_rest_call(): + """ test get_ontap_version_using_rest with force_ontap_version option """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['generic_error']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + # same version + rest_api.force_ontap_version = '9.7' + assert rest_api.get_ontap_version_using_rest() == 200 + assert_no_warnings() + # different versions + rest_api.force_ontap_version = '10.8.1' + assert rest_api.get_ontap_version_using_rest() == 200 + assert rest_api.ontap_version['generation'] == 10 + assert_warning_was_raised('Forcing ONTAP version to 10.8.1 but current version is dummy_9_9_0') + # version could not be read + assert rest_api.get_ontap_version_using_rest() == 200 + assert_warning_was_raised('Forcing ONTAP version to 10.8.1, unable to read current version: error: Expected error, status_code: 400') + assert rest_api.ontap_version['generation'] == 10 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_send_request.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_send_request.py new file mode 100644 index 000000000..f6ae38f21 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_send_request.py @@ -0,0 +1,271 @@ +# Copyright (c) 2018 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils netapp.py ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible.module_utils import basic +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + create_module, expect_and_capture_ansible_exception + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'cert_filepath': None, + 'key_filepath': None, +} + +SINGLE_CERT_ARGS = { + 'hostname': 'test', + 'username': None, + 'password': None, + 'cert_filepath': 'cert_file', + 'key_filepath': None, +} + +CERT_KEY_ARGS = { + 'hostname': 'test', + 'username': None, + 'password': None, + 'cert_filepath': 'cert_file', + 'key_filepath': 'key_file', +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_restapi_object(default_args): + module = create_module(MockONTAPModule, default_args) + return netapp_utils.OntapRestAPI(module.module) + + +class mockResponse: + def __init__(self, json_data, status_code, raise_action=None, headers=None, text=None): + self.json_data = json_data + self.status_code = status_code + self.content = json_data + self.raise_action = raise_action + self.headers = headers or {} + self.text = text + + def raise_for_status(self): + if self.status_code >= 400 and self.status_code < 600: + raise netapp_utils.requests.exceptions.HTTPError('status_code: %s' % self.status_code, response=self) + + def json(self): + if self.raise_action == 'bad_json': + raise ValueError(self.raise_action) + return self.json_data + + +@patch('requests.request') +def test_empty_get_sent_bad_json(mock_request): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data='anything', status_code=200, raise_action='bad_json') + rest_api = create_restapi_object(DEFAULT_ARGS) + message, error = rest_api.get('api', None) + assert error + assert 'Expecting json, got: anything' in error + print('errors:', rest_api.errors) + print('debug:', rest_api.debug_logs) + + +@patch('requests.request') +def test_empty_get_sent_bad_but_empty_json(mock_request): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data='', status_code=200, raise_action='bad_json') + rest_api = create_restapi_object(DEFAULT_ARGS) + message, error = rest_api.get('api', None) + assert not error + + +def test_wait_on_job_bad_url(): + ''' URL format error ''' + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'testme' + job = dict(_links=dict(self=dict(href=api))) + message, error = rest_api.wait_on_job(job) + msg = "URL Incorrect format: list index out of range - Job: {'_links': {'self': {'href': 'testme'}}}" + assert msg in error + + +@patch('time.sleep') +@patch('requests.request') +def test_wait_on_job_timeout(mock_request, sleep_mock): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data='', status_code=200, raise_action='bad_json') + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + job = dict(_links=dict(self=dict(href=api))) + message, error = rest_api.wait_on_job(job) + msg = 'Timeout error: Process still running' + assert msg in error + + +@patch('time.sleep') +@patch('requests.request') +def test_wait_on_job_job_error(mock_request, sleep_mock): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data=dict(error='Job error message'), status_code=200) + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + job = dict(_links=dict(self=dict(href=api))) + message, error = rest_api.wait_on_job(job) + msg = 'Job error message' + assert msg in error + + +@patch('time.sleep') +@patch('requests.request') +def test_wait_on_job_job_failure(mock_request, dont_sleep): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data=dict(error='Job error message', state='failure', message='failure message'), status_code=200) + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + job = dict(_links=dict(self=dict(href=api))) + message, error = rest_api.wait_on_job(job) + msg = 'failure message' + assert msg in error + assert not message + + +@patch('time.sleep') +@patch('requests.request') +def test_wait_on_job_timeout_running(mock_request, sleep_mock): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data=dict(error='Job error message', state='running', message='any message'), status_code=200) + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + job = dict(_links=dict(self=dict(href=api))) + message, error = rest_api.wait_on_job(job) + msg = 'Timeout error: Process still running' + assert msg in error + assert message == 'any message' + + +@patch('time.sleep') +@patch('requests.request') +def test_wait_on_job(mock_request, dont_sleep): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data=dict(error='Job error message', state='other', message='any message'), status_code=200) + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + job = dict(_links=dict(self=dict(href=api))) + message, error = rest_api.wait_on_job(job) + msg = 'Job error message' + assert msg in error + assert message == 'any message' + + +@patch('requests.request') +def test_get_auth_single_cert(mock_request): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data='', status_code=200) + rest_api = create_restapi_object(SINGLE_CERT_ARGS) + api = 'api/testme' + # rest_api.auth_method = 'single_cert' + message, error = rest_api.get(api, None) + print(mock_request.mock_calls) + assert rest_api.auth_method == 'single_cert' + assert "cert='cert_file'" in str(mock_request.mock_calls[0]) + + +@patch('requests.request') +def test_get_auth_cert_key(mock_request): + ''' get with no data ''' + mock_request.return_value = mockResponse(json_data='', status_code=200) + rest_api = create_restapi_object(CERT_KEY_ARGS) + api = 'api/testme' + # rest_api.auth_method = 'single_cert' + message, error = rest_api.get(api, None) + print(mock_request.mock_calls) + assert rest_api.auth_method == 'cert_key' + assert "cert=('cert_file', 'key_file')" in str(mock_request.mock_calls[0]) + + +def test_get_auth_method_keyerror(): + my_cx = create_restapi_object(CERT_KEY_ARGS) + my_cx.auth_method = 'invalid_method' + args = ('method', 'api', 'params') + msg = 'xxxx' + assert expect_and_capture_ansible_exception(my_cx.send_request, KeyError, *args) == 'invalid_method' + + +@patch('requests.request') +def test_http_error_no_json(mock_request): + ''' get raises HTTPError ''' + mock_request.return_value = mockResponse(json_data={}, status_code=400) + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + message, error = rest_api.get(api) + assert error == 'status_code: 400' + + +@patch('requests.request') +def test_http_error_with_json_error_field(mock_request): + ''' get raises HTTPError ''' + mock_request.return_value = mockResponse(json_data=dict(state='other', message='any message', error='error_message'), status_code=400) + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + message, error = rest_api.get(api) + assert error == 'error_message' + + +@patch('requests.request') +def test_http_error_attribute_error(mock_request): + ''' get raises HTTPError ''' + mock_request.return_value = mockResponse(json_data='bad_data', status_code=400) + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + message, error = rest_api.get(api) + assert error == 'status_code: 400' + + +@patch('requests.request') +def test_connection_error(mock_request): + ''' get raises HTTPError ''' + mock_request.side_effect = netapp_utils.requests.exceptions.ConnectionError('connection_error') + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + message, error = rest_api.get(api) + # print(rest_api.errors) + assert error == 'connection_error' + # assert False + + +@patch('requests.request') +def test_options_allow_in_header(mock_request): + ''' OPTIONS returns Allow key ''' + mock_request.return_value = mockResponse(json_data={}, headers={'Allow': 'allowed'}, status_code=200) + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + message, error = rest_api.options(api) + assert error is None + assert message == {'Allow': 'allowed'} + + +@patch('requests.request') +def test_formdata_in_response(mock_request): + ''' GET return formdata ''' + mock_request.return_value = mockResponse( + json_data={}, headers={'Content-Type': 'multipart/form-data'}, raise_action='bad_json', status_code=200, text='testme') + rest_api = create_restapi_object(DEFAULT_ARGS) + api = 'api/testme' + message, error = rest_api.get(api) + assert error is None + assert message == {'text': 'testme'} diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_sf.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_sf.py new file mode 100644 index 000000000..99c74242b --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_sf.py @@ -0,0 +1,85 @@ +# Copyright (c) 2018-2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils netapp.py - solidfire related methods ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible.module_utils import basic +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + patch_ansible, create_module, expect_and_capture_ansible_exception +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +if not netapp_utils.has_sf_sdk(): + pytestmark = pytest.mark.skip("skipping as missing required solidfire") + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_ontap_module(default_args=None): + return create_module(MockONTAPModule, default_args).module + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_SF_SDK', 'dummy') +def test_has_sf_sdk(): + assert netapp_utils.has_sf_sdk() == 'dummy' + + +@patch('solidfire.factory.ElementFactory.create') +def test_create_sf_connection(mock_sf_create): + module = create_ontap_module(DEFAULT_ARGS) + mock_sf_create.return_value = 'dummy' + assert netapp_utils.create_sf_connection(module) == 'dummy' + + +@patch('solidfire.factory.ElementFactory.create') +def test_negative_create_sf_connection_exception(mock_sf_create): + module = create_ontap_module(DEFAULT_ARGS) + mock_sf_create.side_effect = KeyError('dummy') + assert str(expect_and_capture_ansible_exception(netapp_utils.create_sf_connection, Exception, module)) == "Unable to create SF connection: 'dummy'" + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_SF_SDK', False) +def test_negative_create_sf_connection_no_sdk(): + module = create_ontap_module(DEFAULT_ARGS) + assert expect_and_capture_ansible_exception(netapp_utils.create_sf_connection, 'fail', module)['msg'] == 'the python SolidFire SDK module is required' + + +def test_negative_create_sf_connection_no_options(): + module = create_ontap_module(DEFAULT_ARGS) + peer_options = {} + assert expect_and_capture_ansible_exception(netapp_utils.create_sf_connection, 'fail', module, host_options=peer_options)['msg'] ==\ + 'hostname, username, password are required for ElementSW connection.' + + +def test_negative_create_sf_connection_missing_and_extra_options(): + module = create_ontap_module(DEFAULT_ARGS) + peer_options = {'hostname': 'host', 'username': 'user'} + assert expect_and_capture_ansible_exception(netapp_utils.create_sf_connection, 'fail', module, host_options=peer_options)['msg'] ==\ + 'password is required for ElementSW connection.' + peer_options = {'hostname': 'host', 'username': 'user', 'cert_filepath': 'cert'} + assert expect_and_capture_ansible_exception(netapp_utils.create_sf_connection, 'fail', module, host_options=peer_options)['msg'] ==\ + 'password is required for ElementSW connection. cert_filepath is not supported for ElementSW connection.' + + +def test_negative_create_sf_connection_extra_options(): + module = create_ontap_module(DEFAULT_ARGS) + peer_options = {'hostname': 'host', 'username': 'user'} + assert expect_and_capture_ansible_exception(netapp_utils.create_sf_connection, 'fail', module, host_options=peer_options)['msg'] ==\ + 'password is required for ElementSW connection.' + peer_options = {'hostname': 'host', 'username': 'user', 'password': 'pass', 'cert_filepath': 'cert', 'key_filepath': 'key'} + assert expect_and_capture_ansible_exception(netapp_utils.create_sf_connection, 'fail', module, host_options=peer_options)['msg'] ==\ + 'cert_filepath, key_filepath are not supported for ElementSW connection.' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_zapi.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_zapi.py new file mode 100644 index 000000000..b3a09f5cb --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_netapp_zapi.py @@ -0,0 +1,374 @@ +# Copyright (c) 2018-2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils netapp.py - ZAPI related features ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible.module_utils import basic +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + patch_ansible, create_module, expect_and_capture_ansible_exception, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_raw_xml_response, build_zapi_error, zapi_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip("skipping as missing required netapp_lib") + +if not hasattr(netapp_utils.ssl, 'create_default_context') or not hasattr(netapp_utils.ssl, 'SSLContext'): + pytestmark = pytest.mark.skip("skipping as missing required ssl package with SSLContext support") + +ZRR = zapi_responses({ + 'error_no_vserver': build_zapi_error(12345, 'Vserver API missing vserver parameter.'), + 'error_connection_error': build_zapi_error(12345, 'URLError'), + 'error_other_error': build_zapi_error(12345, 'Some other error message.'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'cert_filepath': None, + 'key_filepath': None, +} + +CERT_ARGS = { + 'hostname': 'test', + 'cert_filepath': 'test_pem.pem', + 'key_filepath': 'test_key.key' +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_ontap_module(default_args, module_args=None): + return create_module(MockONTAPModule, default_args, module_args).module + + +def create_ontapzapicx_object(default_args, module_args=None): + ontap_mock = create_module(MockONTAPModule, default_args, module_args) + my_args = {'module': ontap_mock.module} + for key in 'hostname', 'username', 'password', 'cert_filepath', 'key_filepath': + if key in ontap_mock.module.params: + my_args[key] = ontap_mock.module.params[key] + return netapp_utils.OntapZAPICx(**my_args) + + +def test_get_cserver(): + ''' validate cluster vserser name is correctly retrieved ''' + register_responses([ + ('vserver-get-iter', ZRR['cserver']), + ]) + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS)) + cserver = netapp_utils.get_cserver(server) + assert cserver == 'cserver' + + +def test_get_cserver_none(): + ''' validate cluster vserser name is correctly retrieved ''' + register_responses([ + ('vserver-get-iter', ZRR['empty']), + ]) + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS)) + cserver = netapp_utils.get_cserver(server) + assert cserver is None + + +def test_negative_get_cserver(): + ''' validate NaApiError is correctly reported ''' + register_responses([ + ('vserver-get-iter', ZRR['error']), + ]) + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS)) + assert expect_and_capture_ansible_exception(netapp_utils.get_cserver, netapp_utils.zapi.NaApiError, server) + + +def test_negative_get_cserver_connection_error(): + ''' validate NaApiError error is correctly ignore for connection or autorization issues ''' + register_responses([ + ('vserver-get-iter', ZRR['error_connection_error']), + ]) + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS)) + cserver = netapp_utils.get_cserver(server) + assert cserver is None + + +def test_setup_na_ontap_zapi_logging(): + module_args = {'feature_flags': {'trace_apis': False}} + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS, module_args)) + assert not server._trace + module_args = {'feature_flags': {'trace_apis': True}} + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS, module_args)) + assert server._trace + + +def test_setup_na_ontap_zapi_auth_method_and_https(): + module_args = {'feature_flags': {'trace_apis': False}} + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS, module_args)) + assert server._auth_style == server.STYLE_LOGIN_PASSWORD + assert server.get_port() == '80' + module_args = {'feature_flags': {'trace_apis': True}} + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(CERT_ARGS, module_args)) + assert server._auth_style == server.STYLE_CERTIFICATE + assert server.get_port() == '443' + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_negative_setup_na_ontap_zapi(): + error = 'Error: the python NetApp-Lib module is required. Import error: None' + assert expect_and_capture_ansible_exception(netapp_utils.setup_na_ontap_zapi, 'fail', create_ontap_module(DEFAULT_ARGS))['msg'] == error + + +def test_set_zapi_port_and_transport(): + server = netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS)) + netapp_utils.set_zapi_port_and_transport(server, True, None, False) + assert server.get_port() == '443' + assert server.get_transport_type() == 'https' + netapp_utils.set_zapi_port_and_transport(server, False, None, False) + assert server.get_port() == '80' + assert server.get_transport_type() == 'http' + + +@patch('ssl.SSLContext.load_cert_chain') +def test_certificate_method_zapi(mock_ssl): + ''' should fail when trying to read the certificate file ''' + zapi_cx = create_ontapzapicx_object(CERT_ARGS) + assert isinstance(zapi_cx._create_certificate_auth_handler(), netapp_utils.zapi.urllib.request.HTTPSHandler) + assert zapi_cx._get_url() == 'http://test:80/servlets/netapp.servlets.admin.XMLrequest_filer' + + +def test_certificate_method_zapi_missing_files(): + ''' should fail when trying to read the certificate file ''' + zapi_cx = create_ontapzapicx_object(CERT_ARGS) + msg1 = 'Cannot load SSL certificate, check files exist.' + # for python 2,6 :( + msg2 = 'SSL certificate authentication requires python 2.7 or later.' + assert expect_and_capture_ansible_exception(zapi_cx._create_certificate_auth_handler, 'fail')['msg'].startswith((msg1, msg2)) + assert zapi_cx._get_url() == 'http://test:80/servlets/netapp.servlets.admin.XMLrequest_filer' + + +@patch('ssl.create_default_context') +def test_negative_certificate_method_zapi(mock_ssl): + ''' should fail when trying to set context ''' + mock_ssl.side_effect = AttributeError('for test purpose') + zapi_cx = create_ontapzapicx_object(CERT_ARGS) + # AttributeError('for test purpose') with 3.x but AttributeError('for test purpose',) with 2.7 + error = "SSL certificate authentication requires python 2.7 or later. More info: AttributeError('for test purpose'" + assert expect_and_capture_ansible_exception(zapi_cx._create_certificate_auth_handler, 'fail')['msg'].startswith(error) + + +def test_classify_zapi_exception_cluster_only(): + ''' verify output matches expectations ''' + code = 13005 + message = 'Unable to find API: diagnosis-alert-get-iter on data vserver trident_svm' + zapi_exception = netapp_utils.zapi.NaApiError(code, message) + kind, new_message = netapp_utils.classify_zapi_exception(zapi_exception) + assert kind == 'missing_vserver_api_error' + assert new_message.endswith("%d:%s" % (code, message)) + + +def test_classify_zapi_exception_rpc_error(): + ''' verify output matches expectations ''' + code = 13001 + message = "RPC: Couldn't make connection [from mgwd on node \"laurentn-vsim1\" (VSID: -1) to mgwd at 172.32.78.223]" + error_message = 'NetApp API failed. Reason - %d:%s' % (code, message) + zapi_exception = netapp_utils.zapi.NaApiError(code, message) + kind, new_message = netapp_utils.classify_zapi_exception(zapi_exception) + assert kind == 'rpc_error' + assert new_message == error_message + + +def test_classify_zapi_exception_other_error(): + ''' verify output matches expectations ''' + code = 13008 + message = 'whatever' + error_message = 'NetApp API failed. Reason - %d:%s' % (code, message) + zapi_exception = netapp_utils.zapi.NaApiError(code, message) + kind, new_message = netapp_utils.classify_zapi_exception(zapi_exception) + assert kind == 'other_error' + assert new_message == error_message + + +def test_classify_zapi_exception_attributeerror(): + ''' verify output matches expectations ''' + zapi_exception = 'invalid' + kind, new_message = netapp_utils.classify_zapi_exception(zapi_exception) + assert kind == 'other_error' + assert new_message == zapi_exception + + +def test_zapi_parse_response_sanitized(): + ''' should not fail when trying to read invalid XML characters (\x08) ''' + zapi_cx = create_ontapzapicx_object(DEFAULT_ARGS) + response = b"<?xml version='1.0' encoding='UTF-8' ?>\n<!DOCTYPE netapp SYSTEM 'file:/etc/netapp_gx.dtd'>\n" + response += b"<netapp version='1.180' xmlns='http://www.netapp.com/filer/admin'>\n<results status=\"passed\">" + response += b"<cli-output> (cluster log-forwarding create)\n\n" + response += b"Testing network connectivity to the destination host 10.10.10.10. \x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\n\n" + response += b"Error: command failed: Cannot contact destination host (10.10.10.10) from node\n" + response += b" "laurentn-vsim1". Verify connectivity to desired host or skip the\n" + response += b" connectivity check with the "-force" parameter.</cli-output>" + response += b"<cli-result-value>0</cli-result-value></results></netapp>\n" + # Manually extract cli-output contents + cli_output = response.split(b'<cli-output>')[1] + cli_output = cli_output.split(b'</cli-output>')[0] + cli_output = cli_output.replace(b'"', b'"') + # the XML parser would chole on \x08, zapi_cx._parse_response replaces them with '.' + cli_output = cli_output.replace(b'\x08', b'.') + # Use xml parser to extract cli-output contents + xml = zapi_cx._parse_response(response) + results = xml.get_child_by_name('results') + new_cli_output = results.get_child_content('cli-output') + assert cli_output.decode() == new_cli_output + + +def test_zapi_parse_response_unsanitized(): + ''' should fail when trying to read invalid XML characters (\x08) ''' + # use feature_flags to disable sanitization + module_args = {'feature_flags': {'sanitize_xml': False}} + zapi_cx = create_ontapzapicx_object(DEFAULT_ARGS, module_args) + response = b"<?xml version='1.0' encoding='UTF-8' ?>\n<!DOCTYPE netapp SYSTEM 'file:/etc/netapp_gx.dtd'>\n" + response += b"<netapp version='1.180' xmlns='http://www.netapp.com/filer/admin'>\n<results status=\"passed\">" + response += b"<cli-output> (cluster log-forwarding create)\n\n" + response += b"Testing network connectivity to the destination host 10.10.10.10. \x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\n\n" + response += b"Error: command failed: Cannot contact destination host (10.10.10.10) from node\n" + response += b" "laurentn-vsim1". Verify connectivity to desired host or skip the\n" + response += b" connectivity check with the "-force" parameter.</cli-output>" + response += b"<cli-result-value>0</cli-result-value></results></netapp>\n" + with pytest.raises(netapp_utils.zapi.etree.XMLSyntaxError) as exc: + zapi_cx._parse_response(response) + msg = 'PCDATA invalid Char value 8' + assert exc.value.msg.startswith(msg) + + +def test_zapi_cx_add_auth_header(): + ''' should add header ''' + module = create_ontap_module(DEFAULT_ARGS) + zapi_cx = netapp_utils.setup_na_ontap_zapi(module) + assert isinstance(zapi_cx, netapp_utils.OntapZAPICx) + assert zapi_cx.base64_creds is not None + request, dummy = zapi_cx._create_request(netapp_utils.zapi.NaElement('dummy_tag')) + assert "Authorization" in [x[0] for x in request.header_items()] + + +def test_zapi_cx_add_auth_header_explicit(): + ''' should add header ''' + module_args = {'feature_flags': {'classic_basic_authorization': False}} + module = create_ontap_module(DEFAULT_ARGS, module_args) + zapi_cx = netapp_utils.setup_na_ontap_zapi(module) + assert isinstance(zapi_cx, netapp_utils.OntapZAPICx) + assert zapi_cx.base64_creds is not None + request, dummy = zapi_cx._create_request(netapp_utils.zapi.NaElement('dummy_tag')) + assert "Authorization" in [x[0] for x in request.header_items()] + + +def test_zapi_cx_no_auth_header(): + ''' should add header ''' + module_args = {'feature_flags': {'classic_basic_authorization': True, 'always_wrap_zapi': False}} + module = create_ontap_module(DEFAULT_ARGS, module_args) + zapi_cx = netapp_utils.setup_na_ontap_zapi(module) + assert not isinstance(zapi_cx, netapp_utils.OntapZAPICx) + request, dummy = zapi_cx._create_request(netapp_utils.zapi.NaElement('dummy_tag')) + assert "Authorization" not in [x[0] for x in request.header_items()] + + +def test_is_zapi_connection_error(): + message = 'URLError' + assert netapp_utils.is_zapi_connection_error(message) + if sys.version_info >= (3, 5, 0): + # not defined in python 2.7 + message = (ConnectionError(), '') + assert netapp_utils.is_zapi_connection_error(message) + message = [] + assert not netapp_utils.is_zapi_connection_error(message) + + +def test_is_zapi_write_access_error(): + message = 'Insufficient privileges: XXXXXXX does not have write access' + assert netapp_utils.is_zapi_write_access_error(message) + message = 'URLError' + assert not netapp_utils.is_zapi_write_access_error(message) + message = [] + assert not netapp_utils.is_zapi_write_access_error(message) + + +def test_is_zapi_missing_vserver_error(): + message = 'Vserver API missing vserver parameter.' + assert netapp_utils.is_zapi_missing_vserver_error(message) + message = 'URLError' + assert not netapp_utils.is_zapi_missing_vserver_error(message) + message = [] + assert not netapp_utils.is_zapi_missing_vserver_error(message) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.IMPORT_EXCEPTION', 'test_exc') +def test_netapp_lib_is_required(): + msg = 'Error: the python NetApp-Lib module is required. Import error: %s' % 'test_exc' + assert netapp_utils.netapp_lib_is_required() == msg + + +def test_warn_when_rest_is_not_supported_http(): + assert netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS, {'use_rest': 'always'})) + print_warnings() + assert_warning_was_raised("Using ZAPI for basic.py, ignoring 'use_rest: always'. Note: https is set to false.") + + +def test_warn_when_rest_is_not_supported_https(): + assert netapp_utils.setup_na_ontap_zapi(module=create_ontap_module(DEFAULT_ARGS, {'use_rest': 'always', 'https': True})) + print_warnings() + assert_warning_was_raised("Using ZAPI for basic.py, ignoring 'use_rest: always'.") + + +def test_sanitize_xml(): + zapi_cx = create_ontapzapicx_object(CERT_ARGS) + xml = build_raw_xml_response({'test_key': 'test_Value'}) + print('XML', xml) + assert zapi_cx.sanitize_xml(xml) == xml + + # these tests require that 'V' is not used, and 3.x because of bytes + if sys.version_info > (3, 0): + test_xml = zapi_cx.sanitize_xml(xml.replace(b'V', bytes([8]))) + sanitized_xml = xml.replace(b'V', b'.') + assert zapi_cx.sanitize_xml(test_xml) == sanitized_xml + + with patch('builtins.bytes') as mock_bytes: + # forcing bytes to return some unexpected value to force the older paths + mock_bytes.return_value = 0 + assert zapi_cx.sanitize_xml(test_xml) == sanitized_xml + with patch('builtins.chr') as mock_chr: + # forcing python 2.7 behavior + mock_chr.return_value = b'\x08' + assert zapi_cx.sanitize_xml(test_xml) == sanitized_xml + + +def test_parse_response_exceptions_single(): + zapi_cx = create_ontapzapicx_object(CERT_ARGS) + exc = expect_and_capture_ansible_exception(zapi_cx._parse_response, netapp_utils.zapi.etree.XMLSyntaxError, b'response') + print(exc.value) + assert str(exc.value).startswith('Start tag expected') + + +@patch('netapp_lib.api.zapi.zapi.NaServer._parse_response') +def test_parse_response_exceptions_double(mock_parse_response): + xml_exc = netapp_utils.zapi.etree.XMLSyntaxError('UT', 'code', 101, 22, 'filename') + mock_parse_response.side_effect = [xml_exc, KeyError('second exception')] + zapi_cx = create_ontapzapicx_object(CERT_ARGS) + exc = expect_and_capture_ansible_exception(zapi_cx._parse_response, netapp_utils.zapi.etree.XMLSyntaxError, 'response') + print(exc) + assert str(exc.value) == 'UT. Received: response (filename, line 101)' + + # force an exception while processing exception + delattr(xml_exc, 'msg') + mock_parse_response.side_effect = [xml_exc, KeyError('second exception')] + zapi_cx = create_ontapzapicx_object(CERT_ARGS) + exc = expect_and_capture_ansible_exception(zapi_cx._parse_response, netapp_utils.zapi.etree.XMLSyntaxError, 'response') + print(exc) + assert str(exc.value) == 'None (filename, line 101)' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_response_helper.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_response_helper.py new file mode 100644 index 000000000..21bb3c187 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_response_helper.py @@ -0,0 +1,156 @@ +# Copyright (c) 2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils rest_generic.py - REST features ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +from ansible_collections.netapp.ontap.plugins.module_utils import rest_response_helpers + +RECORD = {'key': 'value'} + +RESPONSES = { + 'empty': {}, + 'zero_record': {'num_records': 0}, + 'empty_records': {'records': []}, + 'one_record': {'records': [RECORD], 'num_records': 1}, + 'one_record_no_num_records': {'records': [RECORD]}, + 'one_record_no_num_records_no_records': RECORD, + 'two_records': {'records': [RECORD, RECORD], 'num_records': 2}, +} + + +def test_check_for_0_or_1_records(): + # no records --> None + response_in, error_in, response_out, error_out = RESPONSES['zero_record'], None, None, None + assert rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) == (response_out, error_out) + response_in, error_in, response_out, error_out = RESPONSES['empty_records'], None, None, None + assert rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) == (response_out, error_out) + + # one record + response_in, error_in, response_out, error_out = RESPONSES['one_record'], None, RECORD, None + assert rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) == (response_out, error_out) + response_in, error_in, response_out, error_out = RESPONSES['one_record_no_num_records'], None, RECORD, None + assert rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) == (response_out, error_out) + response_in, error_in, response_out, error_out = RESPONSES['one_record_no_num_records_no_records'], None, RECORD, None + assert rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) == (response_out, error_out) + + +def test_check_for_0_or_1_records_errors(): + # bad input + response_in, error_in, response_out, error_out = None, None, None, 'calling: cluster: no response None.' + assert rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) == (response_out, error_out) + response_in, error_in, response_out, error_out = RESPONSES['empty'], None, None, 'calling: cluster: no response {}.' + assert rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) == (response_out, error_out) + + # error in + response_in, error_in, response_out, error_out = None, 'some_error', None, 'calling: cluster: got some_error.' + assert rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) == (response_out, error_out) + + # more than 1 record + response_in, error_in, response_out, error_out = RESPONSES['two_records'], None, RESPONSES['two_records'], 'calling: cluster: unexpected response' + response, error = rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in) + assert response == response_out + assert error.startswith(error_out) + assert 'for query' not in error + response, error = rest_response_helpers.check_for_0_or_1_records('cluster', response_in, error_in, query=RECORD) + assert response == response_out + assert error.startswith(error_out) + expected = 'for query: %s' % RECORD + assert expected in error + + +def test_check_for_0_or_more_records(): + # no records --> None + response_in, error_in, response_out, error_out = RESPONSES['zero_record'], None, None, None + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + response_in, error_in, response_out, error_out = RESPONSES['empty_records'], None, None, None + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + + # one record + response_in, error_in, response_out, error_out = RESPONSES['one_record'], None, [RECORD], None + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + response_in, error_in, response_out, error_out = RESPONSES['one_record_no_num_records'], None, [RECORD], None + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + + # more than 1 record + response_in, error_in, response_out, error_out = RESPONSES['two_records'], None, [RECORD, RECORD], None + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + + +def test_check_for_0_or_more_records_errors(): + # bad input + response_in, error_in, response_out, error_out = None, None, None, 'calling: cluster: no response None.' + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + response_in, error_in, response_out, error_out = RESPONSES['empty'], None, None, 'calling: cluster: no response {}.' + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + error = "calling: cluster: got No \"records\" key in {'key': 'value'}." + response_in, error_in, response_out, error_out = RESPONSES['one_record_no_num_records_no_records'], None, None, error + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + + # error in + response_in, error_in, response_out, error_out = None, 'some_error', None, 'calling: cluster: got some_error.' + assert rest_response_helpers.check_for_0_or_more_records('cluster', response_in, error_in) == (response_out, error_out) + + +class MockOntapRestAPI: + def __init__(self, job_response=None, error=None, raise_if_called=False): + self.job_response, self.error, self.raise_if_called = job_response, error, raise_if_called + + def wait_on_job(self, job): + if self.raise_if_called: + raise AttributeError('wait_on_job should not be called in this test!') + return self.job_response, self.error + + +def test_check_for_error_and_job_results_no_job(): + rest_api = MockOntapRestAPI(raise_if_called=True) + response_in, error_in, response_out, error_out = None, None, None, None + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api) == (response_out, error_out) + + response_in, error_in, response_out, error_out = 'any', None, 'any', None + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api) == (response_out, error_out) + + response = {'no_job': 'entry'} + response_in, error_in, response_out, error_out = response, None, response, None + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api) == (response_out, error_out) + + +def test_check_for_error_and_job_results_with_job(): + rest_api = MockOntapRestAPI(job_response='job_response', error=None) + response = {'job': 'job_entry'} + expected_response = {'job': 'job_entry', 'job_response': 'job_response'} + response_in, error_in, response_out, error_out = response, None, expected_response, None + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api) == (response_out, error_out) + + response = {'jobs': ['job_entry'], 'num_records': 1} + expected_response = {'jobs': ['job_entry'], 'num_records': 1, 'job_response': 'job_response'} + response_in, error_in, response_out, error_out = response, None, expected_response, None + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api) == (response_out, error_out) + + +def test_negative_check_for_error_and_job_results_error_in(): + rest_api = MockOntapRestAPI(raise_if_called=True) + response_in, error_in, response_out, error_out = None, 'forced_error', None, 'calling: cluster: got forced_error.' + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api) == (response_out, error_out) + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api, raw_error=False) == (response_out, error_out) + error_out = 'forced_error' + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api, raw_error=True) == (response_out, error_out) + + +def test_negative_check_for_error_and_job_results_job_error(): + rest_api = MockOntapRestAPI(job_response='job_response', error='job_error') + response = {'job': 'job_entry'} + response_in, error_in, response_out, error_out = response, None, response, "job reported error: job_error, received {'job': 'job_entry'}." + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api) == (response_out, error_out) + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api, raw_error=False) == (response_out, error_out) + error_out = 'job_error' + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api, raw_error=True) == (response_out, error_out) + + +def test_negative_check_for_error_and_job_results_multiple_jobs_error(): + rest_api = MockOntapRestAPI(raise_if_called=True) + response = {'jobs': 'job_entry', 'num_records': 3} + response_in, error_in, response_out, error_out = response, None, response, "multiple jobs in progress, can't check status" + assert rest_response_helpers.check_for_error_and_job_results('cluster', response_in, error_in, rest_api) == (response_out, error_out) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_application.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_application.py new file mode 100644 index 000000000..346114ebb --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_application.py @@ -0,0 +1,346 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c) 2022, Laurent Nicolas <laurentn@netapp.com> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" unit tests for module_utils rest_vserver.py + + Provides wrappers for svm/svms REST APIs +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible.module_utils import basic +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import create_module, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.module_utils import rest_application + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'app_uuid': (200, {"records": [{"uuid": "test_uuid"}], "num_records": 1}, None), + 'app_details': (200, {"details": "test_details"}, None), + 'app_components': (200, {"records": [{"component": "test_component", "uuid": "component_uuid"}], "num_records": 1}, None), + 'app_component_details': (200, {"component": "test_component", "uuid": "component_uuid", 'backing_storage': 'backing_storage'}, None), + 'unexpected_argument': (200, None, 'Unexpected argument: exclude_aggregates'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'cert_filepath': None, + 'key_filepath': None, +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_restapi_object(default_args, module_args=None): + module = create_module(MockONTAPModule, default_args, module_args) + return netapp_utils.OntapRestAPI(module.module) + + +def create_app(svm_name='vserver_name', app_name='application_name'): + rest_api = create_restapi_object(DEFAULT_ARGS) + return rest_application.RestApplication(rest_api, svm_name, app_name) + + +def test_successfully_create_object(): + register_responses([ + # ('GET', 'svm/svms', SRR['svm_uuid']), + # ('GET', 'svm/svms', SRR['zero_records']), + ]) + assert create_app().svm_name == 'vserver_name' + + +def test_successfully_get_application_uuid(): + register_responses([ + ('GET', 'application/applications', SRR['zero_records']), + ('GET', 'application/applications', SRR['app_uuid']), + ]) + my_app = create_app() + assert my_app.get_application_uuid() == (None, None) + assert my_app.get_application_uuid() == ('test_uuid', None) + # UUID is cached if not None, so no API call + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_uuid() == ('test_uuid', None) + + +def test_negative_get_application_uuid(): + register_responses([ + ('GET', 'application/applications', SRR['generic_error']), + ]) + my_app = create_app() + assert my_app.get_application_uuid() == (None, rest_error_message('', 'application/applications')) + + +def test_successfully_get_application_details(): + register_responses([ + ('GET', 'application/applications', SRR['zero_records']), + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid', SRR['app_details']), + ('GET', 'application/applications/test_uuid', SRR['app_details']), + ('GET', 'application/applications/test_uuid', SRR['app_details']), + ]) + my_app = create_app() + assert my_app.get_application_details() == (None, None) + assert my_app.get_application_details() == (SRR['app_details'][1], None) + # UUID is cached if not None, so no API call + assert my_app.get_application_details(template='test') == (SRR['app_details'][1], None) + assert my_app.get_application_details() == (SRR['app_details'][1], None) + + +def test_negative_get_application_details(): + register_responses([ + ('GET', 'application/applications', SRR['generic_error']), + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid', SRR['generic_error']), + ]) + my_app = create_app() + assert my_app.get_application_details() == (None, rest_error_message('', 'application/applications')) + assert my_app.get_application_details() == (None, rest_error_message('', 'application/applications/test_uuid')) + + +def test_successfully_create_application(): + register_responses([ + ('POST', 'application/applications', SRR['success']), + ]) + my_app = create_app() + assert my_app.create_application({'option': 'option'}) == ({}, None) + + +def test_negative_create_application(): + register_responses([ + ('POST', 'application/applications', SRR['generic_error']), + ('POST', 'application/applications', SRR['unexpected_argument']), + # third call, create fails if app already exists + ('GET', 'application/applications', SRR['app_uuid']), + ]) + my_app = create_app() + assert my_app.create_application({'option': 'option'}) == (None, rest_error_message('', 'application/applications')) + assert my_app.create_application({'option': 'option'}) == ( + None, 'calling: application/applications: got Unexpected argument: exclude_aggregates. "exclude_aggregates" requires ONTAP 9.9.1 GA or later.') + + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.create_application({'option': 'option'}) ==\ + (None, 'function create_application should not be called when application uuid is set: test_uuid.') + + +def test_successfully_patch_application(): + register_responses([ + ('GET', 'application/applications', SRR['app_uuid']), + ('PATCH', 'application/applications/test_uuid', SRR['success']), + ]) + my_app = create_app() + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.patch_application({'option': 'option'}) == ({}, None) + + +def test_negative_patch_application(): + register_responses([ + # first call, patch fails if app does not exist + # second call + ('GET', 'application/applications', SRR['app_uuid']), + ('PATCH', 'application/applications/test_uuid', SRR['generic_error']), + ]) + my_app = create_app() + assert my_app.patch_application({'option': 'option'}) == (None, 'function should not be called before application uuid is set.') + + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.patch_application({'option': 'option'}) == (None, rest_error_message('', 'application/applications/test_uuid')) + + +def test_successfully_delete_application(): + register_responses([ + ('GET', 'application/applications', SRR['app_uuid']), + ('DELETE', 'application/applications/test_uuid', SRR['success']), + ]) + my_app = create_app() + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.delete_application() == ({}, None) + + +def test_negative_delete_application(): + register_responses([ + # first call, delete fails if app does not exist + # second call + ('GET', 'application/applications', SRR['app_uuid']), + ('DELETE', 'application/applications/test_uuid', SRR['generic_error']), + ]) + my_app = create_app() + assert my_app.delete_application() == (None, 'function should not be called before application uuid is set.') + + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.delete_application() == (None, rest_error_message('', 'application/applications/test_uuid')) + + +def test_successfully_get_application_components(): + register_responses([ + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid/components', SRR['zero_records']), + ('GET', 'application/applications/test_uuid/components', SRR['app_components']), + ('GET', 'application/applications/test_uuid/components', SRR['app_components']), + ]) + my_app = create_app() + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_components() == (None, None) + assert my_app.get_application_components() == (SRR['app_components'][1]['records'], None) + assert my_app.get_application_components() == (SRR['app_components'][1]['records'], None) + + +def test_negative_get_application_components(): + register_responses([ + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid/components', SRR['generic_error']), + ]) + my_app = create_app() + assert my_app.get_application_components() == (None, 'function should not be called before application uuid is set.') + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_components() == (None, rest_error_message('', 'application/applications/test_uuid/components')) + + +def test_successfully_get_application_component_uuid(): + register_responses([ + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid/components', SRR['zero_records']), + ('GET', 'application/applications/test_uuid/components', SRR['app_components']), + ('GET', 'application/applications/test_uuid/components', SRR['app_components']), + ]) + my_app = create_app() + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_component_uuid() == (None, None) + assert my_app.get_application_component_uuid() == ('component_uuid', None) + assert my_app.get_application_component_uuid() == ('component_uuid', None) + + +def test_negative_get_application_component_uuid(): + register_responses([ + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid/components', SRR['generic_error']), + ]) + my_app = create_app() + assert my_app.get_application_component_uuid() == (None, 'function should not be called before application uuid is set.') + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_component_uuid() == (None, rest_error_message('', 'application/applications/test_uuid/components')) + + +def test_successfully_get_application_component_details(): + register_responses([ + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid/components', SRR['app_components']), + ('GET', 'application/applications/test_uuid/components/component_uuid', SRR['app_components']), + ]) + my_app = create_app() + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_component_details() == (SRR['app_components'][1]['records'][0], None) + + +def test_negative_get_application_component_details(): + register_responses([ + # first call, fail as UUID not set + # second call, fail to retrieve UUID + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid/components', SRR['zero_records']), + # fail to retrieve UUID + ('GET', 'application/applications/test_uuid/components', SRR['generic_error']), + # fail to retrieve component_details + ('GET', 'application/applications/test_uuid/components', SRR['app_components']), + ('GET', 'application/applications/test_uuid/components/component_uuid', SRR['generic_error']), + ]) + my_app = create_app() + assert my_app.get_application_component_details() == (None, 'function should not be called before application uuid is set.') + # second call, set UUI first + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_component_details() == (None, 'no component for application application_name') + # third call + assert my_app.get_application_component_details() == (None, rest_error_message('', 'application/applications/test_uuid/components')) + # fourth call + assert my_app.get_application_component_details() == (None, rest_error_message('', 'application/applications/test_uuid/components/component_uuid')) + + +def test_successfully_get_application_component_backing_storage(): + register_responses([ + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid/components', SRR['app_components']), + ('GET', 'application/applications/test_uuid/components/component_uuid', SRR['app_component_details']), + ]) + my_app = create_app() + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_component_backing_storage() == ('backing_storage', None) + + +def test_negative_get_application_component_backing_storage(): + register_responses([ + # first call, fail as UUID not set + # second call, fail to retrieve UUID + ('GET', 'application/applications', SRR['app_uuid']), + ('GET', 'application/applications/test_uuid/components', SRR['zero_records']), + # fail to retrieve UUID + ('GET', 'application/applications/test_uuid/components', SRR['generic_error']), + # fail to retrieve component_backing_storage + ('GET', 'application/applications/test_uuid/components', SRR['app_components']), + ('GET', 'application/applications/test_uuid/components/component_uuid', SRR['generic_error']), + ]) + my_app = create_app() + assert my_app.get_application_component_backing_storage() == (None, 'function should not be called before application uuid is set.') + # second call, set UUI first + assert my_app.get_application_uuid() == ('test_uuid', None) + assert my_app.get_application_component_backing_storage() == (None, 'no component for application application_name') + # third call + assert my_app.get_application_component_backing_storage() == (None, rest_error_message('', 'application/applications/test_uuid/components')) + # fourth call + assert my_app.get_application_component_backing_storage() == (None, rest_error_message('', 'application/applications/test_uuid/components/component_uuid')) + + +def test_create_application_body(): + my_app = create_app() + body = { + 'name': my_app.app_name, + 'svm': {'name': my_app.svm_name}, + 'smart_container': True, + 'tname': 'tbody' + } + assert my_app.create_application_body('tname', 'tbody') == (body, None) + body['smart_container'] = False + assert my_app.create_application_body('tname', 'tbody', False) == (body, None) + assert my_app.create_application_body('tname', 'tbody', 'False') == (None, 'expecting bool value for smart_container, got: False') diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_generic.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_generic.py new file mode 100644 index 000000000..b2b42ed97 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_generic.py @@ -0,0 +1,492 @@ +# Copyright (c) 2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils rest_generic.py - REST features ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible.module_utils import basic +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, create_module +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'vservers_with_admin': (200, { + 'records': [ + {'vserver': 'vserver1', 'type': 'data '}, + {'vserver': 'vserver2', 'type': 'data '}, + {'vserver': 'cserver', 'type': 'admin'} + ]}, None), + 'vservers_single': (200, { + 'records': [ + {'vserver': 'single', 'type': 'data '}, + ]}, None), + 'accepted_response': (202, { + 'job': { + 'uuid': 'd0b3eefe-cd59-11eb-a170-005056b338cd', + '_links': {'self': {'href': '/api/cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd'}} + }}, None), + 'job_in_progress': (200, { + 'job': { + 'uuid': 'a1b2c3_job', + '_links': {'self': {'href': 'api/some_link'}} + }}, None), + 'job_success': (200, { + 'state': 'success', + 'message': 'success_message', + 'job': { + 'uuid': 'a1b2c3_job', + '_links': {'self': {'href': 'some_link'}} + }}, None), + 'job_failed': (200, { + 'state': 'error', + 'message': 'error_message', + 'job': { + 'uuid': 'a1b2c3_job', + '_links': {'self': {'href': 'some_link'}} + }}, None), +}) + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'cert_filepath': None, + 'key_filepath': None, +} + +CERT_ARGS = { + 'hostname': 'test', + 'cert_filepath': 'test_pem.pem', + 'key_filepath': 'test_key.key' +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_restapi_object(default_args, module_args=None): + module = create_module(MockONTAPModule, default_args, module_args) + return netapp_utils.OntapRestAPI(module.module) + + +def test_build_query_with_fields(): + assert rest_generic.build_query_with_fields(None, None) is None + assert rest_generic.build_query_with_fields(query=None, fields=None) is None + assert rest_generic.build_query_with_fields(query={'aaa': 'vvv'}, fields=None) == {'aaa': 'vvv'} + assert rest_generic.build_query_with_fields(query=None, fields='aaa,bbb') == {'fields': 'aaa,bbb'} + assert rest_generic.build_query_with_fields(query={'aaa': 'vvv'}, fields='aaa,bbb') == {'aaa': 'vvv', 'fields': 'aaa,bbb'} + + +def test_build_query_with_timeout(): + assert rest_generic.build_query_with_timeout(query=None, timeout=30) == {'return_timeout': 30} + + # when timeout is 0, return_timeout is not added + assert rest_generic.build_query_with_timeout(query=None, timeout=0) is None + assert rest_generic.build_query_with_timeout(query={'aaa': 'vvv'}, timeout=0) == {'aaa': 'vvv'} + + # when return_timeout is in the query, it has precedence + query = {'return_timeout': 55} + assert rest_generic.build_query_with_timeout(query, timeout=0) == query + assert rest_generic.build_query_with_timeout(query, timeout=20) == query + query = {'aaa': 'vvv', 'return_timeout': 55} + assert rest_generic.build_query_with_timeout(query, timeout=0) == query + assert rest_generic.build_query_with_timeout(query, timeout=20) == query + + +def test_successful_get_one_record_no_records_field(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster') + assert error is None + assert record == SRR['is_rest_9_10_1'][1] + + +def test_successful_get_one_record(): + register_responses([ + ('GET', 'cluster', SRR['vservers_single']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster') + assert error is None + assert record == SRR['vservers_single'][1]['records'][0] + + +def test_successful_get_one_record_no_record(): + register_responses([ + ('GET', 'cluster', SRR['zero_records']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster') + assert error is None + assert record is None + + +def test_successful_get_one_record_NN(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster', query=None, fields=None) + assert error is None + assert record == SRR['is_rest_9_10_1'][1] + + +def test_successful_get_one_record_NV(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster', query=None, fields='aaa,bbb') + assert error is None + assert record == SRR['is_rest_9_10_1'][1] + + +def test_successful_get_one_record_VN(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster', query={'aaa': 'value'}, fields=None) + assert error is None + assert record == SRR['is_rest_9_10_1'][1] + + +def test_successful_get_one_record_VV(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster', query={'aaa': 'value'}, fields='aaa,bbb') + assert error is None + assert record == SRR['is_rest_9_10_1'][1] + + +def test_error_get_one_record_empty(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster', query=None, fields=None) + assert error == 'calling: cluster: no response {}.' + assert record is None + + +def test_error_get_one_record_multiple(): + register_responses([ + ('GET', 'cluster', SRR['vservers_with_admin']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster', query={'aaa': 'vvv'}, fields=None) + assert "calling: cluster: unexpected response {'records':" in error + assert "for query: {'aaa': 'vvv'}" in error + assert record == SRR['vservers_with_admin'][1] + + +def test_error_get_one_record_rest_error(): + register_responses([ + ('GET', 'cluster', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record, error = rest_generic.get_one_record(rest_api, 'cluster', query=None, fields=None) + assert error == 'calling: cluster: got Expected error.' + assert record is None + + +def test_successful_get_0_or_more_records(): + register_responses([ + ('GET', 'cluster', SRR['vservers_with_admin']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster') + assert error is None + assert records == SRR['vservers_with_admin'][1]['records'] + + +def test_successful_get_0_or_more_records_NN(): + register_responses([ + ('GET', 'cluster', SRR['vservers_with_admin']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster', query=None, fields=None) + assert error is None + assert records == SRR['vservers_with_admin'][1]['records'] + + +def test_successful_get_0_or_more_records_NV(): + register_responses([ + ('GET', 'cluster', SRR['vservers_with_admin']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster', query=None, fields='aaa,bbb') + assert error is None + assert records == SRR['vservers_with_admin'][1]['records'] + + +def test_successful_get_0_or_more_records_VN(): + register_responses([ + ('GET', 'cluster', SRR['vservers_with_admin']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster', query={'aaa': 'value'}, fields=None) + assert error is None + assert records == SRR['vservers_with_admin'][1]['records'] + + +def test_successful_get_0_or_more_records_VV(): + register_responses([ + ('GET', 'cluster', SRR['vservers_with_admin']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster', query={'aaa': 'value'}, fields='aaa,bbb') + assert error is None + assert records == SRR['vservers_with_admin'][1]['records'] + + +def test_successful_get_0_or_more_records_VV_1_record(): + register_responses([ + ('GET', 'cluster', SRR['vservers_single']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster', query={'aaa': 'value'}, fields='aaa,bbb') + assert error is None + assert records == SRR['vservers_single'][1]['records'] + + +def test_successful_get_0_or_more_records_VV_0_record(): + register_responses([ + ('GET', 'cluster', SRR['zero_records']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster', query={'aaa': 'value'}, fields='aaa,bbb') + assert error is None + assert records is None + + +def test_error_get_0_or_more_records(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster', query=None, fields=None) + assert error == 'calling: cluster: no response {}.' + assert records is None + + +def test_error_get_0_or_more_records_rest_error(): + register_responses([ + ('GET', 'cluster', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + records, error = rest_generic.get_0_or_more_records(rest_api, 'cluster', query=None, fields=None) + assert error == 'calling: cluster: got Expected error.' + assert records is None + + +def test_successful_post_async(): + register_responses([ + ('POST', 'cluster', SRR['vservers_single']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.post_async(rest_api, 'cluster', {}) + assert error is None + assert response == SRR['vservers_single'][1] + + +def test_error_post_async(): + register_responses([ + ('POST', 'cluster', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.post_async(rest_api, 'cluster', {}) + assert error == 'calling: cluster: got Expected error.' + assert response is None + + +@patch('time.sleep') +def test_successful_post_async_with_job(dont_sleep): + register_responses([ + ('POST', 'cluster', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_success']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.post_async(rest_api, 'cluster', {}) + assert error is None + assert 'job_response' in response + assert response['job_response'] == 'success_message' + + +@patch('time.sleep') +def test_successful_post_async_with_job_failure(dont_sleep): + register_responses([ + ('POST', 'cluster', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_failed']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.post_async(rest_api, 'cluster', {}) + assert error is None + assert 'job_response' in response + assert response['job_response'] == 'error_message' + + +@patch('time.sleep') +def test_error_post_async_with_job(dont_sleep): + register_responses([ + ('POST', 'cluster', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.post_async(rest_api, 'cluster', {}) + assert 'job reported error: Expected error - Expected error - Expected error - Expected error, received' in error + assert response == SRR['accepted_response'][1] + + +def test_successful_patch_async(): + register_responses([ + ('PATCH', 'cluster/uuid', SRR['vservers_single']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.patch_async(rest_api, 'cluster', 'uuid', {}) + assert error is None + assert response == SRR['vservers_single'][1] + + +def test_error_patch_async(): + register_responses([ + ('PATCH', 'cluster/uuid', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.patch_async(rest_api, 'cluster', 'uuid', {}) + assert error == 'calling: cluster/uuid: got Expected error.' + assert response is None + + +@patch('time.sleep') +def test_successful_patch_async_with_job(dont_sleep): + register_responses([ + ('PATCH', 'cluster/uuid', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_success']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.patch_async(rest_api, 'cluster', 'uuid', {}) + assert error is None + assert 'job_response' in response + assert response['job_response'] == 'success_message' + + +@patch('time.sleep') +def test_successful_patch_async_with_job_failure(dont_sleep): + register_responses([ + ('PATCH', 'cluster/uuid', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_failed']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.patch_async(rest_api, 'cluster', 'uuid', {}) + assert error is None + assert 'job_response' in response + assert response['job_response'] == 'error_message' + + +@patch('time.sleep') +def test_error_patch_async_with_job(dont_sleep): + register_responses([ + ('PATCH', 'cluster/uuid', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.patch_async(rest_api, 'cluster', 'uuid', {}) + assert 'job reported error: Expected error - Expected error - Expected error - Expected error, received' in error + assert response == SRR['accepted_response'][1] + + +def test_successful_delete_async(): + register_responses([ + ('DELETE', 'cluster/uuid', SRR['vservers_single']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.delete_async(rest_api, 'cluster', 'uuid') + assert error is None + assert response == SRR['vservers_single'][1] + + +def test_error_delete_async(): + register_responses([ + ('DELETE', 'cluster/uuid', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.delete_async(rest_api, 'cluster', 'uuid') + assert error == 'calling: cluster/uuid: got Expected error.' + assert response is None + + +@patch('time.sleep') +def test_successful_delete_async_with_job(dont_sleep): + register_responses([ + ('DELETE', 'cluster/uuid', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_success']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.delete_async(rest_api, 'cluster', 'uuid') + assert error is None + assert 'job_response' in response + assert response['job_response'] == 'success_message' + + +@patch('time.sleep') +def test_successful_delete_async_with_job_failure(dont_sleep): + register_responses([ + ('DELETE', 'cluster/uuid', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_failed']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.delete_async(rest_api, 'cluster', 'uuid') + assert error is None + assert 'job_response' in response + assert response['job_response'] == 'error_message' + + +@patch('time.sleep') +def test_error_delete_async_with_job(dont_sleep): + register_responses([ + ('DELETE', 'cluster/uuid', SRR['accepted_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_in_progress']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + response, error = rest_generic.delete_async(rest_api, 'cluster', 'uuid') + assert 'job reported error: Expected error - Expected error - Expected error - Expected error, received' in error + assert response == SRR['accepted_response'][1] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_owning_resource.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_owning_resource.py new file mode 100644 index 000000000..a7465e8d2 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_owning_resource.py @@ -0,0 +1,98 @@ +# Copyright (c) 2022 NetApp +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for module_utils rest_generic.py - REST features ''' +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +from ansible.module_utils import basic +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.module_utils import rest_owning_resource + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'get_uuid_policy_id_export_policy': ( + 200, + { + "records": [{ + "svm": { + "uuid": "uuid", + "name": "svm"}, + "id": 123, + "name": "ansible" + }], + "num_records": 1}, None), + 'get_uuid_from_volume': ( + 200, + { + "records": [{ + "svm": { + "uuid": "uuid", + "name": "svm"}, + "uuid": "028baa66-41bd-11e9-81d5-00a0986138f7" + }] + }, None + ) +}) + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_restapi_object(default_args, module_args=None): + module = create_module(MockONTAPModule, default_args, module_args) + return netapp_utils.OntapRestAPI(module.module) + + +def test_get_policy_id(): + register_responses([ + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record = rest_owning_resource.get_export_policy_id(rest_api, 'ansible', 'svm', rest_api.module) + assert record == SRR['get_uuid_policy_id_export_policy'][1]['records'][0]['id'] + + +def test_error_get_policy_id(): + register_responses([ + ('GET', 'protocols/nfs/export-policies', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + error = 'Could not find export policy ansible on SVM svm' + assert error in expect_and_capture_ansible_exception(rest_owning_resource.get_export_policy_id, 'fail', rest_api, 'ansible', 'svm', rest_api.module)['msg'] + + +def test_get_volume_uuid(): + register_responses([ + ('GET', 'storage/volumes', SRR['get_uuid_from_volume']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + record = rest_owning_resource.get_volume_uuid(rest_api, 'ansible', 'svm', rest_api.module) + assert record == SRR['get_uuid_from_volume'][1]['records'][0]['uuid'] + + +def test_error_get_volume_uuid(): + register_responses([ + ('GET', 'storage/volumes', SRR['generic_error']) + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + error = 'Could not find volume ansible on SVM svm' + assert error in expect_and_capture_ansible_exception(rest_owning_resource.get_volume_uuid, 'fail', rest_api, 'ansible', 'svm', rest_api.module)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_volume.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_volume.py new file mode 100644 index 000000000..0c1a77e7f --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_volume.py @@ -0,0 +1,233 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c) 2021, Laurent Nicolas <laurentn@netapp.com> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" unit tests for module_utils netapp_module.py + + Provides wrappers for storage/volumes REST APIs +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import copy +import pytest +import sys + +# from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, call +from ansible_collections.netapp.ontap.plugins.module_utils import rest_volume +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'one_volume_record': (200, dict(records=[ + dict(uuid='a1b2c3', + name='test', + svm=dict(name='vserver'), + ) + ], num_records=1), None), + 'three_volume_records': (200, dict(records=[ + dict(uuid='a1b2c3_1', + name='test1', + svm=dict(name='vserver'), + ), + dict(uuid='a1b2c3_2', + name='test2', + svm=dict(name='vserver'), + ), + dict(uuid='a1b2c3_3', + name='test3', + svm=dict(name='vserver'), + ) + ], num_records=3), None), + 'job': (200, { + 'job': { + 'uuid': 'a1b2c3_job', + '_links': {'self': {'href': 'api/some_link'}} + }}, None), + 'job_bad_url': (200, { + 'job': { + 'uuid': 'a1b2c3_job', + '_links': {'self': {'href': 'some_link'}} + }}, None), + 'job_status_success': (200, { + 'state': 'success', + 'message': 'success_message', + 'job': { + 'uuid': 'a1b2c3_job', + '_links': {'self': {'href': 'some_link'}} + }}, None), +} + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + + +class MockModule(object): + ''' rough mock for an Ansible module class ''' + def __init__(self): + self.params = dict( + username='my_username', + password='my_password', + hostname='my_hostname', + use_rest='my_use_rest', + cert_filepath=None, + key_filepath=None, + validate_certs='my_validate_certs', + http_port=None, + feature_flags=None, + ) + + def fail_json(self, *args, **kwargs): # pylint: disable=unused-argument + """function to simulate fail_json: package return data into an exception""" + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_get_volumes_none(mock_request): + module = MockModule() + rest_api = netapp_utils.OntapRestAPI(module) + mock_request.side_effect = [ + SRR['zero_record'], + SRR['end_of_sequence']] + volumes, error = rest_volume.get_volumes(rest_api) + assert error is None + assert volumes is None + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_get_volumes_one(mock_request): + module = MockModule() + rest_api = netapp_utils.OntapRestAPI(module) + mock_request.side_effect = [ + SRR['one_volume_record'], + SRR['end_of_sequence']] + volumes, error = rest_volume.get_volumes(rest_api, 'vserver', 'name') + assert error is None + assert volumes == [SRR['one_volume_record'][1]['records'][0]] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_get_volumes_three(mock_request): + module = MockModule() + rest_api = netapp_utils.OntapRestAPI(module) + mock_request.side_effect = [ + SRR['three_volume_records'], + SRR['end_of_sequence']] + volumes, error = rest_volume.get_volumes(rest_api) + assert error is None + assert volumes == [SRR['three_volume_records'][1]['records'][x] for x in (0, 1, 2)] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_get_volume_not_found(mock_request): + module = MockModule() + rest_api = netapp_utils.OntapRestAPI(module) + mock_request.side_effect = [ + SRR['zero_record'], + SRR['end_of_sequence']] + volume, error = rest_volume.get_volume(rest_api, 'name', 'vserver') + assert error is None + assert volume is None + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_get_volume_found(mock_request): + module = MockModule() + rest_api = netapp_utils.OntapRestAPI(module) + mock_request.side_effect = [ + SRR['one_volume_record'], + SRR['end_of_sequence']] + volume, error = rest_volume.get_volume(rest_api, 'name', 'vserver') + assert error is None + assert volume == SRR['one_volume_record'][1]['records'][0] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_get_volume_too_many(mock_request): + module = MockModule() + rest_api = netapp_utils.OntapRestAPI(module) + mock_request.side_effect = [ + SRR['three_volume_records'], + SRR['end_of_sequence']] + dummy, error = rest_volume.get_volume(rest_api, 'name', 'vserver') + expected = "calling: storage/volumes: unexpected response" + assert expected in error + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_patch_volume_async(mock_request): + module = MockModule() + rest_api = netapp_utils.OntapRestAPI(module) + mock_request.side_effect = [ + copy.deepcopy(SRR['job']), # deepcopy as job is modified in place! + SRR['job_status_success'], + SRR['end_of_sequence']] + body = dict(a1=1, a2=True, a3='str') + response, error = rest_volume.patch_volume(rest_api, 'uuid', body) + job = dict(SRR['job'][1]) # deepcopy as job is modified in place! + job['job_response'] = SRR['job_status_success'][1]['message'] + assert error is None + assert response == job + expected = call('PATCH', 'storage/volumes/uuid', {'return_timeout': 30}, json=body, headers=None, files=None) + assert expected in mock_request.mock_calls + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_patch_volume_async_with_query(mock_request): + module = MockModule() + rest_api = netapp_utils.OntapRestAPI(module) + mock_request.side_effect = [ + copy.deepcopy(SRR['job']), # deepcopy as job is modified in place! + SRR['job_status_success'], + SRR['end_of_sequence']] + body = dict(a1=1, a2=True, a3='str') + query = dict(return_timeout=20) + response, error = rest_volume.patch_volume(rest_api, 'uuid', body, query) + job = dict(SRR['job'][1]) # deepcopy as job is modified in place! + job['job_response'] = SRR['job_status_success'][1]['message'] + assert error is None + assert response == job + expected = call('PATCH', 'storage/volumes/uuid', {'return_timeout': 20}, json=body, headers=None, files=None) + assert expected in mock_request.mock_calls diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_vserver.py b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_vserver.py new file mode 100644 index 000000000..c646abed2 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/module_utils/test_rest_vserver.py @@ -0,0 +1,120 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# Copyright (c) 2022, Laurent Nicolas <laurentn@netapp.com> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" unit tests for module_utils rest_vserver.py + + Provides wrappers for svm/svms REST APIs +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible.module_utils import basic +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.module_utils import rest_vserver + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'svm_uuid': (200, {"records": [{"uuid": "test_uuid"}], "num_records": 1}, None), +}) + + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'cert_filepath': None, + 'key_filepath': None, +} + + +class MockONTAPModule: + def __init__(self): + self.module = basic.AnsibleModule(netapp_utils.na_ontap_host_argument_spec()) + + +def create_restapi_object(default_args, module_args=None): + module = create_module(MockONTAPModule, default_args, module_args) + return netapp_utils.OntapRestAPI(module.module) + + +def test_successfully_get_vserver(): + register_responses([ + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'svm/svms', SRR['zero_records']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_vserver.get_vserver(rest_api, 'svm_name') == ({'uuid': 'test_uuid'}, None) + assert rest_vserver.get_vserver(rest_api, 'svm_name') == (None, None) + + +def test_negative_get_vserver(): + register_responses([ + ('GET', 'svm/svms', SRR['generic_error']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_vserver.get_vserver(rest_api, 'svm_name') == (None, rest_error_message('', 'svm/svms')) + + +def test_successfully_get_vserver_uuid(): + register_responses([ + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'svm/svms', SRR['zero_records']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_vserver.get_vserver_uuid(rest_api, 'svm_name') == ('test_uuid', None) + assert rest_vserver.get_vserver_uuid(rest_api, 'svm_name') == (None, None) + + +def test_negative_get_vserver_uuid(): + register_responses([ + ('GET', 'svm/svms', SRR['generic_error']), + ('GET', 'svm/svms', SRR['generic_error']), + ('GET', 'svm/svms', SRR['zero_records']), + ('GET', 'svm/svms', SRR['zero_records']), + ]) + rest_api = create_restapi_object(DEFAULT_ARGS) + assert rest_vserver.get_vserver_uuid(rest_api, 'svm_name') == (None, rest_error_message('', 'svm/svms')) + assert expect_and_capture_ansible_exception(rest_vserver.get_vserver_uuid, 'fail', rest_api, 'svm_name', rest_api.module)['msg'] ==\ + rest_error_message('Error fetching vserver svm_name', 'svm/svms') + assert rest_vserver.get_vserver_uuid(rest_api, 'svm_name', error_on_none=True) == (None, 'vserver svm_name does not exist or is not a data vserver.') + assert expect_and_capture_ansible_exception(rest_vserver.get_vserver_uuid, 'fail', rest_api, 'svm_name', rest_api.module, error_on_none=True)['msg'] ==\ + 'Error vserver svm_name does not exist or is not a data vserver.' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/.gitignore b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/.gitignore new file mode 100644 index 000000000..bc1a1f616 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/.gitignore @@ -0,0 +1,2 @@ +# Created by pytest automatically. +* diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/CACHEDIR.TAG b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/CACHEDIR.TAG new file mode 100644 index 000000000..fce15ad7e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/CACHEDIR.TAG @@ -0,0 +1,4 @@ +Signature: 8a477f597d28d172789f06886806bc55 +# This file is a cache directory tag created by pytest. +# For information about cache directory tags, see: +# https://bford.info/cachedir/spec.html diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/README.md b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/README.md new file mode 100644 index 000000000..b89018ced --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/README.md @@ -0,0 +1,8 @@ +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + +See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information. diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/lastfailed b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/lastfailed new file mode 100644 index 000000000..ba7b58d20 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/lastfailed @@ -0,0 +1,3 @@ +{ + "test_na_ontap_lun_rest.py": true +}
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/nodeids b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/nodeids new file mode 100644 index 000000000..ca22cf9ee --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/nodeids @@ -0,0 +1,6 @@ +[ + "test_na_ontap_lun.py::TestMyModule::test_create_error_missing_param", + "test_na_ontap_lun.py::TestMyModule::test_module_fail_when_required_args_missing", + "test_na_ontap_lun_rest.py::TestMyModule::test_create_error_missing_param", + "test_na_ontap_lun_rest.py::TestMyModule::test_successful_create_appli" +]
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/stepwise b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/stepwise new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/.pytest_cache/v/cache/stepwise @@ -0,0 +1 @@ +[]
\ No newline at end of file diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_active_directory.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_active_directory.py new file mode 100644 index 000000000..7e108b081 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_active_directory.py @@ -0,0 +1,311 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP Ansible module na_ontap_active_directory ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + set_module_args, AnsibleExitJson, AnsibleFailJson, patch_ansible, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import zapi_responses, build_zapi_response +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_active_directory \ + import NetAppOntapActiveDirectory as my_module, main as my_main # module under test +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +# not available on 2.6 anymore +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def default_args(use_rest='never'): + return { + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'account_name': 'account_name', + 'vserver': 'vserver', + 'admin_password': 'admin_password', + 'admin_username': 'admin_username', + 'use_rest': use_rest + } + + +ad_info = { + 'attributes-list': { + 'active-directory-account-config': { + 'account-name': 'account_name', + 'domain': 'current.domain', + 'organizational-unit': 'current.ou', + } + } +} + + +ZRR = zapi_responses( + {'ad': build_zapi_response(ad_info, 1)} +) + +SRR = rest_responses({ + 'ad_1': (200, {"records": [{ + "fqdn": "server1.com", + "name": "account_name", + "organizational_unit": "CN=Test", + "svm": {"name": "svm1", "uuid": "02c9e252"} + }], "num_records": 1}, None), + 'ad_2': (200, {"records": [{ + "fqdn": "server2.com", + "name": "account_name", + "organizational_unit": "CN=Test", + "svm": {"name": "svm1", "uuid": "02c9e252"} + }], "num_records": 1}, None) +}) + + +def test_success_create(): + ''' test get''' + args = dict(default_args()) + args['domain'] = 'some.domain' + args['force_account_overwrite'] = True + args['organizational_unit'] = 'some.OU' + set_module_args(args) + register_responses([ + # list of tuples: (expected ZAPI, response) + ('active-directory-account-get-iter', ZRR['success']), + ('active-directory-account-create', ZRR['success']), + ]) + + with pytest.raises(AnsibleExitJson) as exc: + my_main() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] + + +def test_fail_create_zapi_error(): + ''' test get''' + args = dict(default_args()) + set_module_args(args) + register_responses([ + # list of tuples: (expected ZAPI, response) + ('active-directory-account-get-iter', ZRR['success']), + ('active-directory-account-create', ZRR['error']), + ]) + + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error creating vserver Active Directory account_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg == exc.value.args[0]['msg'] + + +def test_success_delete(): + ''' test get''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + register_responses([ + # list of tuples: (expected ZAPI, response) + ('active-directory-account-get-iter', ZRR['ad']), + ('active-directory-account-delete', ZRR['success']), + ]) + + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] + + +def test_fail_delete_zapi_error(): + ''' test get''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + register_responses([ + # list of tuples: (expected ZAPI, response) + ('active-directory-account-get-iter', ZRR['ad']), + ('active-directory-account-delete', ZRR['error']), + ]) + + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error deleting vserver Active Directory account_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg == exc.value.args[0]['msg'] + + +def test_success_modify(): + ''' test get''' + args = dict(default_args()) + args['domain'] = 'some.other.domain' + args['force_account_overwrite'] = True + set_module_args(args) + register_responses([ + # list of tuples: (expected ZAPI, response) + ('active-directory-account-get-iter', ZRR['ad']), + ('active-directory-account-modify', ZRR['success']), + ]) + + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] + + +def test_fail_modify_zapi_error(): + ''' test get''' + args = dict(default_args()) + args['domain'] = 'some.other.domain' + set_module_args(args) + register_responses([ + # list of tuples: (expected ZAPI, response) + ('active-directory-account-get-iter', ZRR['ad']), + ('active-directory-account-modify', ZRR['error']), + ]) + + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error modifying vserver Active Directory account_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg == exc.value.args[0]['msg'] + + +def test_fail_modify_on_ou(): + ''' test get''' + args = dict(default_args()) + args['organizational_unit'] = 'some.other.OU' + set_module_args(args) + register_responses([ + # list of tuples: (expected ZAPI, response) + ('active-directory-account-get-iter', ZRR['ad']), + ]) + + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = "Error: organizational_unit cannot be modified; found {'organizational_unit': 'some.other.OU'}." + assert msg == exc.value.args[0]['msg'] + + +def test_fail_on_get_zapi_error(): + ''' test get''' + args = dict(default_args()) + set_module_args(args) + register_responses([ + # list of tuples: (expected ZAPI, response) + ('active-directory-account-get-iter', ZRR['error']), + ]) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error searching for Active Directory account_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + ''' test get''' + args = dict(default_args()) + set_module_args(args) + mock_has_netapp_lib.return_value = False + with pytest.raises(AnsibleFailJson) as exc: + my_module() + assert 'Error: the python NetApp-Lib module is required. Import error: None' == exc.value.args[0]['msg'] + + +def test_fail_on_rest(): + ''' test error with rest versions less than 9.12.1''' + args = dict(default_args('always')) + set_module_args(args) + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']) + ]) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + assert 'Error: REST requires ONTAP 9.12.1 or later' in exc.value.args[0]['msg'] + + +def test_success_create_rest(): + ''' test create''' + args = dict(default_args('always')) + args['domain'] = 'server1.com' + args['force_account_overwrite'] = True + args['organizational_unit'] = 'CN=Test' + set_module_args(args) + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/active-directory', SRR['empty_records']), + ('POST', 'protocols/active-directory', SRR['success']), + ]) + + with pytest.raises(AnsibleExitJson) as exc: + my_main() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] + + +def test_success_delete_rest(): + ''' test delete rest''' + args = dict(default_args('always')) + args['state'] = 'absent' + set_module_args(args) + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/active-directory', SRR['ad_1']), + ('DELETE', 'protocols/active-directory/02c9e252', SRR['success']), + ]) + + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] + + +def test_success_modify_rest(): + ''' test modify rest''' + args = dict(default_args('always')) + args['domain'] = 'some.other.domain' + args['force_account_overwrite'] = True + set_module_args(args) + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/active-directory', SRR['ad_1']), + ('PATCH', 'protocols/active-directory/02c9e252', SRR['success']), + ]) + + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/active-directory', SRR['generic_error']), + ('POST', 'protocols/active-directory', SRR['generic_error']), + ('PATCH', 'protocols/active-directory/02c9e252', SRR['generic_error']), + ('DELETE', 'protocols/active-directory/02c9e252', SRR['generic_error']) + ]) + ad_obj = create_module(my_module, default_args('always')) + ad_obj.svm_uuid = '02c9e252' + assert 'Error searching for Active Directory' in expect_and_capture_ansible_exception(ad_obj.get_active_directory_rest, 'fail')['msg'] + assert 'Error creating vserver Active Directory' in expect_and_capture_ansible_exception(ad_obj.create_active_directory_rest, 'fail')['msg'] + assert 'Error modifying vserver Active Directory' in expect_and_capture_ansible_exception(ad_obj.modify_active_directory_rest, 'fail')['msg'] + assert 'Error deleting vserver Active Directory' in expect_and_capture_ansible_exception(ad_obj.delete_active_directory_rest, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_active_directory_domain_controllers.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_active_directory_domain_controllers.py new file mode 100644 index 000000000..cedbe0519 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_active_directory_domain_controllers.py @@ -0,0 +1,177 @@ +# Copyright: NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_active_directory_preferred_domain_controllers """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import create_and_apply,\ + patch_ansible, call_main +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_active_directory_domain_controllers \ + import NetAppOntapActiveDirectoryDC as my_module, main as my_main # module under test + +# REST API canned responses when mocking send_request +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'DC_record': ( + 200, + { + "records": [ + { + "fqdn": "example.com", + "server_ip": "10.10.10.10", + 'svm': {"uuid": "3d52ad89-c278-11ed-a7b0-005056b3ed56"}, + } + ], + "num_records": 1 + }, None + ), + 'svm_record': ( + 200, + { + "records": [ + { + "uuid": "3d52ad89-c278-11ed-a7b0-005056b3ed56", + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'ansible', + 'fqdn': 'example.com', + 'server_ip': '10.10.10.10' +} + + +def test_rest_error_get_svm(): + '''Test error rest get svm''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']), + ('GET', 'svm/svms', SRR['generic_error']), + ]) + error = call_main(my_main, ARGS_REST, fail=True)['msg'] + msg = "Error fetching vserver ansible: calling: svm/svms: got Expected error." + assert msg in error + + +def test_rest_error_get(): + '''Test error rest get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['generic_error']), + ]) + error = call_main(my_main, ARGS_REST, fail=True)['msg'] + msg = "Error on fetching Active Directory preferred DC configuration of an SVM:" + assert msg in error + + +def test_rest_error_create_active_directory_preferred_domain_controllers(): + '''Test error rest create active_directory preferred domain_controllers''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['empty_records']), + ('POST', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['generic_error']), + ]) + error = call_main(my_main, ARGS_REST, fail=True)['msg'] + msg = "Error on adding Active Directory preferred DC configuration to an SVM:" + assert msg in error + + +def test_rest_create_active_directory_preferred_domain_controllers(): + '''Test rest create active_directory preferred domain_controllers''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['empty_records']), + ('POST', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['empty_good']), + ]) + module_args = { + 'fqdn': 'example.com', + 'server_ip': '10.10.10.10' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_rest_delete_active_directory_preferred_domain_controllers(): + '''Test rest delete active_directory preferred domain_controllers''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['DC_record']), + ('DELETE', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers/example.com/10.10.10.10', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_error_delete_active_directory_preferred_domain_controllers(): + '''Test error rest delete active_directory preferred domain_controllers''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['DC_record']), + ('DELETE', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers/example.com/10.10.10.10', + SRR['generic_error']), + ]) + module_args = { + 'fqdn': 'example.com', + 'server_ip': '10.10.10.10', + 'state': 'absent' + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on deleting Active Directory preferred DC configuration of an SVM:" + assert msg in error + + +def test_create_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['DC_record']), + ]) + module_args = { + 'state': 'present', + 'fqdn': 'example.com', + 'server_ip': '10.10.10.10' + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_delete_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_0']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'protocols/active-directory/3d52ad89-c278-11ed-a7b0-005056b3ed56/preferred-domain-controllers', SRR['empty_records']), + ]) + module_args = { + 'state': 'absent' + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_aggregate.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_aggregate.py new file mode 100644 index 000000000..c93260dcf --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_aggregate.py @@ -0,0 +1,627 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_aggregate """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses, build_zapi_error + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_aggregate \ + import NetAppOntapAggregate as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +AGGR_NAME = 'aggr_name' +OS_NAME = 'abc' + +aggr_info = {'num-records': 3, + 'attributes-list': + {'aggr-attributes': + {'aggregate-name': AGGR_NAME, + 'aggr-raid-attributes': { + 'state': 'online', + 'disk-count': '4', + 'encrypt-with-aggr-key': 'true'}, + 'aggr-snaplock-attributes': {'snaplock-type': 'snap_t'}} + }, + } + +object_store_info = {'num-records': 1, + 'attributes-list': + {'object-store-information': {'object-store-name': OS_NAME}} + } + +disk_info = {'num-records': 1, + 'attributes-list': [ + {'disk-info': + {'disk-name': '1', + 'disk-raid-info': + {'disk-aggregate-info': + {'plex-name': 'plex0'} + }}}, + {'disk-info': + {'disk-name': '2', + 'disk-raid-info': + {'disk-aggregate-info': + {'plex-name': 'plex0'} + }}}, + {'disk-info': + {'disk-name': '3', + 'disk-raid-info': + {'disk-aggregate-info': + {'plex-name': 'plexM'} + }}}, + {'disk-info': + {'disk-name': '4', + 'disk-raid-info': + {'disk-aggregate-info': + {'plex-name': 'plexM'} + }}}, + ]} + +ZRR = zapi_responses({ + 'aggr_info': build_zapi_response(aggr_info), + 'object_store_info': build_zapi_response(object_store_info), + 'disk_info': build_zapi_response(disk_info), + 'error_disk_add': build_zapi_error(13003, 'disk add operation is in progress'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': AGGR_NAME, + 'use_rest': 'never', + 'feature_flags': {'no_cserver_ems': True} +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + error = create_module(my_module, fail=True)['msg'] + print('Info: %s' % error) + assert 'missing required arguments:' in error + assert 'name' in error + + +def test_create(): + register_responses([ + ('aggr-get-iter', ZRR['empty']), + ('aggr-create', ZRR['empty']), + ('aggr-get-iter', ZRR['empty']), + ]) + module_args = { + 'disk_type': 'ATA', + 'raid_type': 'raid_dp', + 'snaplock_type': 'non_snaplock', + # 'spare_pool': 'Pool0', + 'disk_count': 4, + 'raid_size': 5, + 'disk_size': 10, + # 'disk_size_with_unit': 'dsize_unit', + 'is_mirrored': True, + 'ignore_pool_checks': True, + 'encryption': True, + 'nodes': ['node1', 'node2'] + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('aggr-destroy', ZRR['empty']) + ]) + module_args = { + 'state': 'absent', + 'disk_count': 3 + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_with_spare_pool(): + register_responses([ + ('aggr-get-iter', ZRR['empty']), + ('aggr-create', ZRR['empty']), + ('aggr-get-iter', ZRR['empty']), + ]) + module_args = { + 'disk_type': 'ATA', + 'raid_type': 'raid_dp', + 'snaplock_type': 'non_snaplock', + 'spare_pool': 'Pool0', + 'disk_count': 2, + 'raid_size': 5, + 'disk_size_with_unit': '10m', + # 'disk_size_with_unit': 'dsize_unit', + 'ignore_pool_checks': True, + 'encryption': True, + 'nodes': ['node1', 'node2'] + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_with_disks(): + register_responses([ + ('aggr-get-iter', ZRR['empty']), + ('aggr-create', ZRR['empty']), + ('aggr-get-iter', ZRR['empty']), + ]) + module_args = { + 'disk_type': 'ATA', + 'raid_type': 'raid_dp', + 'snaplock_type': 'non_snaplock', + 'disks': [1, 2], + 'mirror_disks': [11, 12], + 'raid_size': 5, + 'disk_size_with_unit': '10m', + 'ignore_pool_checks': True, + 'encryption': True, + 'nodes': ['node1', 'node2'] + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_create_wait_for_completion(mock_time): + register_responses([ + ('aggr-get-iter', ZRR['empty']), + ('aggr-create', ZRR['empty']), + ('aggr-get-iter', ZRR['empty']), + ('aggr-get-iter', ZRR['empty']), + ('aggr-get-iter', ZRR['aggr_info']), + ]) + module_args = { + 'disk_count': '2', + 'is_mirrored': 'true', + 'wait_for_online': 'true' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_with_object_store(): + register_responses([ + ('aggr-get-iter', ZRR['empty']), + ('aggr-create', ZRR['empty']), + ('aggr-get-iter', ZRR['empty']), + ('aggr-object-store-attach', ZRR['empty']), + ]) + module_args = { + 'disk_class': 'capacity', + 'disk_count': '2', + 'is_mirrored': 'true', + 'object_store_name': 'abc', + 'allow_flexgroups': True + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_is_mirrored(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ]) + module_args = { + 'disk_count': '4', + 'is_mirrored': 'true', + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_disks_list(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ]) + module_args = { + 'disks': ['1', '2'], + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_mirror_disks(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ]) + module_args = { + 'disks': ['1', '2'], + 'mirror_disks': ['3', '4'] + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_spare_pool(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ]) + module_args = { + 'disk_count': '4', + 'spare_pool': 'Pool1' + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_modify_encryption(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ]) + module_args = { + 'encryption': False + } + exc = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = 'Error: modifying encryption is not supported with ZAPI.' + assert msg in exc['msg'] + + +def test_rename(): + register_responses([ + ('aggr-get-iter', ZRR['empty']), # target does not exist + ('aggr-get-iter', ZRR['aggr_info']), # from exists + ('aggr-rename', ZRR['empty']), + ]) + module_args = { + 'from_name': 'test_name2' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rename_error_no_from(): + register_responses([ + ('aggr-get-iter', ZRR['empty']), # target does not exist + ('aggr-get-iter', ZRR['empty']), # from does not exist + ]) + module_args = { + 'from_name': 'test_name2' + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = 'Error renaming aggregate %s: no aggregate with from_name %s.' % (AGGR_NAME, module_args['from_name']) + assert msg in exception['msg'] + + +def test_rename_with_add_object_store(): # TODO: + register_responses([ + ('aggr-get-iter', ZRR['empty']), # target does not exist + ('aggr-get-iter', ZRR['aggr_info']), # from exists + ('aggr-object-store-get-iter', ZRR['empty']), # from does not have an OS + ('aggr-rename', ZRR['empty']), + ('aggr-object-store-attach', ZRR['empty']), + ]) + module_args = { + 'from_name': 'test_name2', + 'object_store_name': 'abc', + 'allow_flexgroups': False + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_object_store_present(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('aggr-object-store-get-iter', ZRR['object_store_info']), + ]) + module_args = { + 'object_store_name': 'abc' + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_object_store_create(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('aggr-object-store-get-iter', ZRR['empty']), # object_store is not attached + ('aggr-object-store-attach', ZRR['empty']), + ]) + module_args = { + 'object_store_name': 'abc', + 'allow_flexgroups': True + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_object_store_modify(): + ''' not supported ''' + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('aggr-object-store-get-iter', ZRR['object_store_info']), + ]) + module_args = { + 'object_store_name': 'def' + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = 'Error: object store %s is already associated with aggregate %s.' % (OS_NAME, AGGR_NAME) + assert msg in exception['msg'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('aggr-get-iter', ZRR['error']), + ('aggr-online', ZRR['error']), + ('aggr-offline', ZRR['error']), + ('aggr-create', ZRR['error']), + ('aggr-destroy', ZRR['error']), + ('aggr-rename', ZRR['error']), + ('aggr-get-iter', ZRR['error']), + ]) + module_args = { + 'service_state': 'online', + 'unmount_volumes': 'True', + 'from_name': 'test_name2', + } + + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.aggr_get_iter, 'fail', module_args.get('name'))['msg'] + assert 'Error getting aggregate: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.aggregate_online, 'fail')['msg'] + assert 'Error changing the state of aggregate' in error + + error = expect_and_capture_ansible_exception(my_obj.aggregate_offline, 'fail')['msg'] + assert 'Error changing the state of aggregate' in error + + error = expect_and_capture_ansible_exception(my_obj.create_aggr, 'fail')['msg'] + assert 'Error provisioning aggregate' in error + + error = expect_and_capture_ansible_exception(my_obj.delete_aggr, 'fail')['msg'] + assert 'Error removing aggregate' in error + + error = expect_and_capture_ansible_exception(my_obj.rename_aggregate, 'fail')['msg'] + assert 'Error renaming aggregate' in error + + my_obj.asup_log_for_cserver = Mock(return_value=None) + error = expect_and_capture_ansible_exception(my_obj.apply, 'fail')['msg'] + assert '12345:synthetic error for UT purpose' in error + + +def test_disks_bad_mapping(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ]) + module_args = { + 'disks': ['0'], + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = "Error mapping disks for aggregate %s: cannot match disks with current aggregate disks." % AGGR_NAME + assert exception['msg'].startswith(msg) + + +def test_disks_overlapping_mirror(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ]) + module_args = { + 'disks': ['1', '2', '3'], + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = "Error mapping disks for aggregate %s: found overlapping plexes:" % AGGR_NAME + assert exception['msg'].startswith(msg) + + +def test_disks_removing_disk(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ]) + module_args = { + 'disks': ['1'], + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = "Error removing disks is not supported. Aggregate %s: these disks cannot be removed: ['2']." % AGGR_NAME + assert exception['msg'].startswith(msg) + + +def test_disks_removing_mirror_disk(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ]) + module_args = { + 'disks': ['1', '2'], + 'mirror_disks': ['4', '6'] + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = "Error removing disks is not supported. Aggregate %s: these disks cannot be removed: ['3']." % AGGR_NAME + assert exception['msg'].startswith(msg) + + +def test_disks_add(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ]) + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ('aggr-add', ZRR['empty']), + ]) + module_args = { + 'disks': ['1', '2', '5'], + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_disks_add_and_offline(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ('aggr-add', ZRR['empty']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['success']), + # error if max tries attempted. + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ('aggr-add', ZRR['empty']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']), + ('aggr-offline', ZRR['error_disk_add']) + ]) + module_args = { + 'disks': ['1', '2', '5'], 'service_state': 'offline' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert 'disk add operation is in progres' in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_mirror_disks_add(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ('aggr-add', ZRR['empty']), + ]) + module_args = { + 'disks': ['1', '2', '5'], + 'mirror_disks': ['3', '4', '6'] + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_mirror_disks_add_unbalanced(): + register_responses([ + ('aggr-get-iter', ZRR['aggr_info']), + ('storage-disk-get-iter', ZRR['disk_info']), + ]) + module_args = { + 'disks': ['1', '2'], + 'mirror_disks': ['3', '4', '6'] + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = "Error cannot add mirror disks ['6'] without adding disks for aggregate %s." % AGGR_NAME + assert exception['msg'].startswith(msg) + + +def test_map_plex_to_primary_and_mirror_error_overlap(): + my_obj = create_module(my_module, DEFAULT_ARGS) + kwargs = { + 'plex_disks': {'plex1': [1, 2, 3], 'plex2': [4, 5, 6]}, + 'disks': [1, 4, 5], + 'mirror_disks': [] + } + error = expect_and_capture_ansible_exception(my_obj.map_plex_to_primary_and_mirror, 'fail', **kwargs)['msg'] + msg = "Error mapping disks for aggregate aggr_name: found overlapping plexes:" + assert error.startswith(msg) + + +def test_map_plex_to_primary_and_mirror_error_overlap_mirror(): + my_obj = create_module(my_module, DEFAULT_ARGS) + kwargs = { + 'plex_disks': {'plex1': [1, 2, 3], 'plex2': [4, 5, 6]}, + 'disks': [1, 4, 5], + 'mirror_disks': [1, 4, 5] + } + error = expect_and_capture_ansible_exception(my_obj.map_plex_to_primary_and_mirror, 'fail', **kwargs)['msg'] + msg = "Error mapping disks for aggregate aggr_name: found overlapping mirror plexes:" + error.startswith(msg) + + +def test_map_plex_to_primary_and_mirror_error_no_match(): + my_obj = create_module(my_module, DEFAULT_ARGS) + kwargs = { + 'plex_disks': {'plex1': [1, 2, 3], 'plex2': [4, 5, 6]}, + 'disks': [7, 8, 9], + 'mirror_disks': [10, 11, 12] + } + error = expect_and_capture_ansible_exception(my_obj.map_plex_to_primary_and_mirror, 'fail', **kwargs)['msg'] + msg = ("Error mapping disks for aggregate aggr_name: cannot match disks with current aggregate disks, " + "and cannot match mirror_disks with current aggregate disks.") + assert error.startswith(msg) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_missing_netapp_lib(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg == create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + + +def test_disk_get_iter_error(): + register_responses([ + ('storage-disk-get-iter', ZRR['error']), + ]) + msg = 'Error getting disks: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg == expect_and_capture_ansible_exception(create_module(my_module, DEFAULT_ARGS).disk_get_iter, 'fail', 'name')['msg'] + + +def test_object_store_get_iter_error(): + register_responses([ + ('aggr-object-store-get-iter', ZRR['error']), + ]) + msg = 'Error getting object store: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg == expect_and_capture_ansible_exception(create_module(my_module, DEFAULT_ARGS).object_store_get_iter, 'fail', 'name')['msg'] + + +def test_attach_object_store_to_aggr_error(): + register_responses([ + ('aggr-object-store-attach', ZRR['error']), + ]) + module_args = { + 'object_store_name': 'os12', + } + msg = 'Error attaching object store os12 to aggregate aggr_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg == expect_and_capture_ansible_exception(create_module(my_module, DEFAULT_ARGS, module_args).attach_object_store_to_aggr, 'fail')['msg'] + + +def test_add_disks_all_options_class(): + register_responses([ + ('aggr-add', ZRR['empty']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['ignore_pool_checks'] = True + my_obj.parameters['disk_class'] = 'performance' + assert my_obj.add_disks(count=2, disks=['1', '2'], disk_size=1, disk_size_with_unit='12GB') is None + + +def test_add_disks_all_options_type(): + register_responses([ + ('aggr-add', ZRR['empty']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['ignore_pool_checks'] = True + my_obj.parameters['disk_type'] = 'SSD' + assert my_obj.add_disks(count=2, disks=['1', '2'], disk_size=1, disk_size_with_unit='12GB') is None + + +def test_add_disks_error(): + register_responses([ + ('aggr-add', ZRR['error']), + ]) + msg = 'Error adding additional disks to aggregate aggr_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg == expect_and_capture_ansible_exception(create_module(my_module, DEFAULT_ARGS).add_disks, 'fail')['msg'] + + +def test_modify_aggr_offline(): + register_responses([ + ('aggr-offline', ZRR['empty']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + assert my_obj.modify_aggr({'service_state': 'offline'}) is None + + +def test_modify_aggr_online(): + register_responses([ + ('aggr-online', ZRR['empty']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + assert my_obj.modify_aggr({'service_state': 'online'}) is None diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_aggregate_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_aggregate_rest.py new file mode 100644 index 000000000..1fc6bfbf2 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_aggregate_rest.py @@ -0,0 +1,616 @@ + +# (c) 2022-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_aggregate when using REST """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_aggregate \ + import NetAppOntapAggregate as my_module, main as my_main # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + # module specific responses + 'one_record': (200, {'records': [ + {'uuid': 'ansible', '_tags': ['resource:cloud', 'main:aggr'], + 'block_storage': {'primary': {'disk_count': 5}}, + 'state': 'online', 'snaplock_type': 'snap'} + ]}, None), + 'two_records': (200, {'records': [ + {'uuid': 'ansible', + 'block_storage': {'primary': {'disk_count': 5}}, + 'state': 'online', 'snaplock_type': 'snap'}, + {'uuid': 'ansible', + 'block_storage': {'primary': {'disk_count': 5}}, + 'state': 'online', 'snaplock_type': 'snap'}, + ]}, None), + 'no_uuid': (200, {'records': [ + {'block_storage': {'primary': {'disk_count': 5}}, + 'state': 'online', 'snaplock_type': 'snap'}, + ]}, None), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'aggr_name' +} + + +def test_validate_options(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ], 'test_validate_options') + # no error! + my_obj = create_module(my_module, DEFAULT_ARGS) + assert my_obj.validate_options() is None + + my_obj.parameters['nodes'] = [1, 2] + + msg = 'Error when validating options: only one node can be specified when using rest' + assert msg in expect_and_capture_ansible_exception(my_obj.validate_options, 'fail')['msg'] + + my_obj.parameters['disk_count'] = 7 + my_obj.parameters.pop('nodes') + msg = 'Error when validating options: nodes is required when disk_count is present.' + assert msg in expect_and_capture_ansible_exception(my_obj.validate_options, 'fail')['msg'] + + my_obj.use_rest = False + my_obj.parameters['mirror_disks'] = [1, 2] + msg = 'Error when validating options: mirror_disks require disks options to be set.' + assert msg in expect_and_capture_ansible_exception(my_obj.validate_options, 'fail')['msg'] + + +def test_get_disk_size(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + + my_obj.parameters['disk_size'] = 1 + assert my_obj.get_disk_size() == 4096 + my_obj.parameters['disk_size'] = 1000 + assert my_obj.get_disk_size() == 4096000 + + my_obj.parameters.pop('disk_size') + my_obj.parameters['disk_size_with_unit'] = '1567' + assert my_obj.get_disk_size() == 1567 + my_obj.parameters['disk_size_with_unit'] = '1567K' + assert my_obj.get_disk_size() == 1567 * 1024 + my_obj.parameters['disk_size_with_unit'] = '1567gb' + assert my_obj.get_disk_size() == 1567 * 1024 * 1024 * 1024 + my_obj.parameters['disk_size_with_unit'] = '15.67gb' + assert my_obj.get_disk_size() == int(15.67 * 1024 * 1024 * 1024) + + my_obj.parameters['disk_size_with_unit'] = '1567rb' + error = expect_and_capture_ansible_exception(my_obj.get_disk_size, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: unexpected unit in disk_size_with_unit: 1567rb' == error + + my_obj.parameters['disk_size_with_unit'] = 'error' + error = expect_and_capture_ansible_exception(my_obj.get_disk_size, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: unexpected value in disk_size_with_unit: error' == error + + +def test_get_aggr_rest_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['generic_error']) + ]) + error = expect_and_capture_ansible_exception(create_module(my_module, DEFAULT_ARGS).get_aggr_rest, 'fail', 'aggr1')['msg'] + print('Info: %s' % error) + assert 'Error: failed to get aggregate aggr1: calling: storage/aggregates: got Expected error.' == error + + +def test_get_aggr_rest_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + assert create_module(my_module, DEFAULT_ARGS).get_aggr_rest(None) is None + + +def test_get_aggr_rest_one_record(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['one_record']) + ]) + assert create_module(my_module, DEFAULT_ARGS).get_aggr_rest('aggr1') is not None + + +def test_get_aggr_rest_not_found(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), + ]) + assert create_module(my_module, DEFAULT_ARGS).get_aggr_rest('aggr1') is None + + +def test_create_aggr(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'storage/aggregates', SRR['empty_good']) + ]) + assert create_module(my_module, DEFAULT_ARGS).create_aggr_rest() is None + assert get_mock_record().is_record_in_json({'name': 'aggr_name'}, 'POST', 'storage/aggregates') + + +def test_aggr_tags(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'storage/aggregates', SRR['zero_records']), + ('POST', 'storage/aggregates', SRR['empty_good']), + # idempotent check + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'storage/aggregates', SRR['one_record']), + # modify tags + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'storage/aggregates', SRR['one_record']), + ('PATCH', 'storage/aggregates/ansible', SRR['success']) + ]) + args = {'tags': ['resource:cloud', 'main:aggr']} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert create_and_apply(my_module, DEFAULT_ARGS, {'tags': ['main:aggr']})['changed'] + + +def test_create_aggr_all_options(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'storage/aggregates', SRR['empty_good']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['disk_class'] = 'capacity' + my_obj.parameters['disk_count'] = 12 + my_obj.parameters['disk_size_with_unit'] = '1567gb' + my_obj.parameters['is_mirrored'] = True + my_obj.parameters['nodes'] = ['node1'] + my_obj.parameters['raid_size'] = 4 + my_obj.parameters['raid_type'] = 'raid5' + my_obj.parameters['encryption'] = True + my_obj.parameters['snaplock_type'] = 'snap' + + assert my_obj.create_aggr_rest() is None + assert get_mock_record().is_record_in_json( + {'block_storage': {'primary': {'disk_class': 'capacity', 'disk_count': 12, 'raid_size': 4, 'raid_type': 'raid5'}, 'mirror': {'enabled': True}}}, + 'POST', 'storage/aggregates') + + +def test_create_aggr_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'storage/aggregates', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['disk_count'] = 12 + my_obj.parameters['disk_size_with_unit'] = '1567gb' + my_obj.parameters['is_mirrored'] = False + my_obj.parameters['nodes'] = ['node1'] + my_obj.parameters['raid_size'] = 4 + my_obj.parameters['raid_type'] = 'raid5' + my_obj.parameters['encryption'] = True + + error = expect_and_capture_ansible_exception(my_obj.create_aggr_rest, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: failed to create aggregate: calling: storage/aggregates: got Expected error.' == error + + +def test_delete_aggr(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('DELETE', 'storage/aggregates/aggr_uuid', SRR['empty_good']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.uuid = 'aggr_uuid' + assert my_obj.delete_aggr_rest() is None + + +def test_delete_aggr_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('DELETE', 'storage/aggregates/aggr_uuid', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.parameters['disk_size_with_unit'] = '1567gb' + my_obj.parameters['is_mirrored'] = False + my_obj.parameters['nodes'] = ['node1'] + my_obj.parameters['raid_size'] = 4 + my_obj.parameters['raid_type'] = 'raid5' + my_obj.parameters['encryption'] = True + my_obj.uuid = 'aggr_uuid' + + error = expect_and_capture_ansible_exception(my_obj.delete_aggr_rest, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: failed to delete aggregate: calling: storage/aggregates/aggr_uuid: got Expected error.' == error + + +def test_patch_aggr(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/aggregates/aggr_uuid', SRR['empty_good']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = 'aggr_uuid' + my_obj.patch_aggr_rest('act on', {'key': 'value'}) + assert get_mock_record().is_record_in_json({'key': 'value'}, 'PATCH', 'storage/aggregates/aggr_uuid') + + +def test_patch_aggr_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/aggregates/aggr_uuid', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = 'aggr_uuid' + + error = expect_and_capture_ansible_exception(my_obj.patch_aggr_rest, 'fail', 'act on', {'key': 'value'})['msg'] + print('Info: %s' % error) + assert 'Error: failed to act on aggregate: calling: storage/aggregates/aggr_uuid: got Expected error.' == error + + +def test_set_disk_count(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + current = {'disk_count': 2} + modify = {'disk_count': 5} + my_obj.set_disk_count(current, modify) + assert modify['disk_count'] == 3 + + +def test_set_disk_count_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + + current = {'disk_count': 9} + modify = {'disk_count': 5} + error = expect_and_capture_ansible_exception(my_obj.set_disk_count, 'fail', current, modify)['msg'] + print('Info: %s' % error) + assert 'Error: specified disk_count is less than current disk_count. Only adding disks is allowed.' == error + + +def test_add_disks(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/aggregates/aggr_uuid', SRR['empty_good']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['disk_class'] = 'performance' + my_obj.parameters['disk_count'] = 12 + my_obj.uuid = 'aggr_uuid' + my_obj.add_disks_rest(count=2) + assert get_mock_record().is_record_in_json({'block_storage': {'primary': {'disk_count': 12}}}, 'PATCH', 'storage/aggregates/aggr_uuid') + + +def test_add_disks_error_local(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = 'aggr_uuid' + + error = expect_and_capture_ansible_exception(my_obj.add_disks_rest, 'fail', disks=[1, 2])['msg'] + print('Info: %s' % error) + assert 'Error: disks or mirror disks are mot supported with rest: [1, 2], None.' == error + + +def test_add_disks_error_remote(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/aggregates/aggr_uuid', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['disk_count'] = 12 + my_obj.uuid = 'aggr_uuid' + + error = expect_and_capture_ansible_exception(my_obj.add_disks_rest, 'fail', count=2)['msg'] + print('Info: %s' % error) + assert 'Error: failed to increase disk count for aggregate: calling: storage/aggregates/aggr_uuid: got Expected error.' == error + + +def test_rename_aggr(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/aggregates/aggr_uuid', SRR['empty_good']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = 'aggr_uuid' + my_obj.rename_aggr_rest() + assert get_mock_record().is_record_in_json({'name': 'aggr_name'}, 'PATCH', 'storage/aggregates/aggr_uuid') + + +def test_offline_online_aggr_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/aggregates/aggr_uuid', SRR['generic_error']), + ('PATCH', 'storage/aggregates/aggr_uuid', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = 'aggr_uuid' + error = 'Error: failed to make service state online for aggregate' + assert error in expect_and_capture_ansible_exception(my_obj.aggregate_online, 'fail')['msg'] + error = 'Error: failed to make service state offline for aggregate' + assert error in expect_and_capture_ansible_exception(my_obj.aggregate_offline, 'fail')['msg'] + + +def test_rename_aggr_error_remote(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/aggregates/aggr_uuid', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = 'aggr_uuid' + + error = expect_and_capture_ansible_exception(my_obj.rename_aggr_rest, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: failed to rename aggregate: calling: storage/aggregates/aggr_uuid: got Expected error.' == error + + +def test_get_object_store(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates/aggr_uuid/cloud-stores', SRR['one_record']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = 'aggr_uuid' + record = my_obj.get_object_store_rest() + assert record + + +def test_get_object_store_error_remote(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates/aggr_uuid/cloud-stores', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = 'aggr_uuid' + + error = expect_and_capture_ansible_exception(my_obj.get_object_store_rest, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: failed to get cloud stores for aggregate: calling: storage/aggregates/aggr_uuid/cloud-stores: got Expected error.' == error + + +def test_get_cloud_target_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cloud/targets', SRR['one_record']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['object_store_name'] = 'os12' + my_obj.uuid = 'aggr_uuid' + record = my_obj.get_cloud_target_uuid_rest() + assert record + + +def test_get_cloud_target_uuid_error_remote(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cloud/targets', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['object_store_name'] = 'os12' + my_obj.uuid = 'aggr_uuid' + + error = expect_and_capture_ansible_exception(my_obj.get_cloud_target_uuid_rest, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: failed to find cloud store with name os12: calling: cloud/targets: got Expected error.' == error + + +def test_attach_object_store_to_aggr(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cloud/targets', SRR['one_record']), # get object store UUID + ('POST', 'storage/aggregates/aggr_uuid/cloud-stores', SRR['empty_good']) # attach (POST) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['object_store_name'] = 'os12' + my_obj.parameters['allow_flexgroups'] = True + my_obj.uuid = 'aggr_uuid' + assert my_obj.attach_object_store_to_aggr_rest() == {} + + +def test_attach_object_store_to_aggr_error_remote(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cloud/targets', SRR['one_record']), # get object store UUID + ('POST', 'storage/aggregates/aggr_uuid/cloud-stores', SRR['generic_error']) # attach (POST) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['object_store_name'] = 'os12' + my_obj.uuid = 'aggr_uuid' + + error = expect_and_capture_ansible_exception(my_obj.attach_object_store_to_aggr_rest, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: failed to attach cloud store with name os12: calling: storage/aggregates/aggr_uuid/cloud-stores: got Expected error.' == error + + +def test_apply_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get + ('POST', 'storage/aggregates', SRR['empty_good']), # create (POST) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + assert get_mock_record().is_record_in_json({'name': 'aggr_name'}, 'POST', 'storage/aggregates') + + +def test_apply_create_and_modify_service_state(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get + ('POST', 'storage/aggregates', SRR['empty_good']), # create (POST) + ('PATCH', 'storage/aggregates', SRR['success']), # modify service state + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'service_state': 'offline'})['changed'] + assert get_mock_record().is_record_in_json({'name': 'aggr_name'}, 'POST', 'storage/aggregates') + + +def test_apply_create_fail_to_read_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get + ('POST', 'storage/aggregates', SRR['two_records']), # create (POST) + ]) + msg = 'Error: failed to parse create aggregate response: calling: storage/aggregates: unexpected response' + assert msg in create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] + + +def test_apply_create_fail_to_read_uuid_key_missing(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get + ('POST', 'storage/aggregates', SRR['no_uuid']), # create (POST) + ]) + msg = 'Error: failed to parse create aggregate response: uuid key not present in' + assert msg in create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] + + +def test_apply_create_with_object_store(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get + ('POST', 'storage/aggregates', SRR['one_record']), # create (POST) + ('GET', 'cloud/targets', SRR['one_record']), # get object store uuid + ('POST', 'storage/aggregates/ansible/cloud-stores', SRR['empty_good']), # attach (POST) + ]) + module_args = { + 'object_store_name': 'os12' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert get_mock_record().is_record_in_json({'name': 'aggr_name'}, 'POST', 'storage/aggregates') + assert get_mock_record().is_record_in_json({'target': {'uuid': 'ansible'}}, 'POST', 'storage/aggregates/ansible/cloud-stores') + + +def test_apply_create_with_object_store_missing_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get + ('POST', 'storage/aggregates', SRR['empty_good']), # create (POST) + ]) + module_args = { + 'object_store_name': 'os12' + } + msg = 'Error: cannot attach cloud store with name os12: aggregate UUID is not set.' + assert create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + assert get_mock_record().is_record_in_json({'name': 'aggr_name'}, 'POST', 'storage/aggregates') + + +def test_apply_create_check_mode(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, check_mode=True)['changed'] + + +def test_apply_add_disks(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['one_record']), # get + ('PATCH', 'storage/aggregates/ansible', SRR['empty_good']), # patch (add disks) + ]) + module_args = { + 'disk_count': 12, + 'nodes': 'node1' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert get_mock_record().is_record_in_json({'block_storage': {'primary': {'disk_count': 12}}}, 'PATCH', 'storage/aggregates/ansible') + + +def test_apply_add_object_store(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['one_record']), # get + ('GET', 'storage/aggregates/ansible/cloud-stores', SRR['empty_records']), # get aggr cloud store + ('GET', 'cloud/targets', SRR['one_record']), # get object store uuid + ('POST', 'storage/aggregates/ansible/cloud-stores', SRR['empty_good']), # attach + ]) + module_args = { + 'object_store_name': 'os12', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert get_mock_record().is_record_in_json({'target': {'uuid': 'ansible'}}, 'POST', 'storage/aggregates/ansible/cloud-stores') + + +def test_apply_rename(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get aggr + ('GET', 'storage/aggregates', SRR['one_record']), # get from_aggr + ('PATCH', 'storage/aggregates/ansible', SRR['empty_good']), # patch (rename) + ]) + module_args = { + 'from_name': 'old_aggr', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert get_mock_record().is_record_in_json({'name': 'aggr_name'}, 'PATCH', 'storage/aggregates/ansible') + + +def test_apply_delete(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['one_record']), # get + ('DELETE', 'storage/aggregates/ansible', SRR['empty_good']), # delete + ]) + module_args = { + 'state': 'absent', + 'disk_count': 4 + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_get_aggr_actions_error_service_state_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + error = 'Error: Minimum version of ONTAP for service_state is (9, 11, 1)' + assert error in create_module(my_module, DEFAULT_ARGS, {'service_state': 'online', 'use_rest': 'always'}, fail=True)['msg'] + + +def test_get_aggr_actions_error_snaplock(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['one_record']), # get + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['snaplock_type'] = 'enterprise' + + error = expect_and_capture_ansible_exception(my_obj.get_aggr_actions, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error: snaplock_type is not modifiable. Cannot change to: enterprise.' == error + + +def test_main_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates', SRR['empty_records']), # get + ('POST', 'storage/aggregates', SRR['empty_good']), # create + ]) + set_module_args(DEFAULT_ARGS) + + assert expect_and_capture_ansible_exception(my_main, 'exit')['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_autosupport.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_autosupport.py new file mode 100644 index 000000000..c971520f1 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_autosupport.py @@ -0,0 +1,264 @@ +# (c) 2018-2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP autosupport Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_warning_was_raised, call_main, create_module, patch_ansible, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_autosupport \ + import NetAppONTAPasup as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'node_name': 'node1', + 'retry_count': '16', + 'transport': 'http', + 'ondemand_enabled': 'true' + +} + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'one_asup_record': (200, { + "records": [{ + 'node': 'node1', + 'state': True, + 'from': 'Postmaster', + 'support': True, + 'transport': 'http', + 'url': 'support.netapp.com/asupprod/post/1.0/postAsup', + 'proxy_url': 'username1:********@host.com:8080', + 'hostname_subj': True, + 'nht': False, + 'perf': True, + 'retry_count': 16, + 'reminder': True, + 'max_http_size': 10485760, + 'max_smtp_size': 5242880, + 'remove_private_data': False, + 'local_collection': True, + 'ondemand_state': True, + 'ondemand_server_url': 'https://support.netapp.com/aods/asupmessage', + 'partner_address': ['test@example.com'] + }], + 'num_records': 1 + }, None) +}) + +autosupport_info = { + 'attributes': { + 'autosupport-config-info': { + 'is-enabled': 'true', + 'node-name': 'node1', + 'transport': 'http', + 'post-url': 'support.netapp.com/asupprod/post/1.0/postAsup', + 'from': 'Postmaster', + 'proxy-url': 'username1:********@host.com:8080', + 'retry-count': '16', + 'max-http-size': '10485760', + 'max-smtp-size': '5242880', + 'is-support-enabled': 'true', + 'is-node-in-subject': 'true', + 'is-nht-data-enabled': 'false', + 'is-perf-data-enabled': 'true', + 'is-reminder-enabled': 'true', + 'is-private-data-removed': 'false', + 'is-local-collection-enabled': 'true', + 'is-ondemand-enabled': 'true', + 'validate-digital-certificate': 'true', + + } + } +} + +ZRR = zapi_responses({ + 'autosupport_info': build_zapi_response(autosupport_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + assert 'missing required arguments:' in call_main(my_main, {}, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + module_args = { + 'use_rest': 'never', + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_ensure_get_called(): + register_responses([ + ('ZAPI', 'autosupport-config-get', ZRR['autosupport_info']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_autosupport_config() is not None + + +def test_successful_modify(): + ''' modifying asup and testing idempotency ''' + register_responses([ + ('ZAPI', 'autosupport-config-get', ZRR['autosupport_info']), + ('ZAPI', 'autosupport-config-modify', ZRR['success']), + # idempotency + ('ZAPI', 'autosupport-config-get', ZRR['autosupport_info']), + ]) + module_args = { + 'use_rest': 'never', + 'ondemand_enabled': False, + 'partner_addresses': [], + 'post_url': 'some_url', + 'from_address': 'from_add', + 'to_addresses': 'to_add', + 'hostname_in_subject': False, + 'nht_data_enabled': True, + 'perf_data_enabled': False, + 'reminder_enabled': False, + 'private_data_removed': True, + 'local_collection_enabled': False, + 'retry_count': 3, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # idempotency + module_args = { + 'use_rest': 'never', + 'ondemand_enabled': True, + 'partner_addresses': [], + 'post_url': 'support.netapp.com/asupprod/post/1.0/postAsup', + 'from_address': 'Postmaster', + 'hostname_in_subject': True, + 'nht_data_enabled': False, + 'perf_data_enabled': True, + 'reminder_enabled': True, + 'private_data_removed': False, + 'local_collection_enabled': True, + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ZAPI', 'autosupport-config-get', ZRR['error']), + # idempotency + ('ZAPI', 'autosupport-config-get', ZRR['autosupport_info']), + ('ZAPI', 'autosupport-config-modify', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'ondemand_enabled': False, + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == zapi_error_message('Error fetching info') + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == zapi_error_message('Error modifying asup') + + +def test_rest_modify_no_action(): + ''' modify asup ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/system/node/autosupport', SRR['one_asup_record']), + ]) + module_args = { + 'use_rest': 'always', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_modify_prepopulate(): + ''' modify asup ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/system/node/autosupport', SRR['one_asup_record']), + ('PATCH', 'private/cli/system/node/autosupport', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'ondemand_enabled': False, + 'partner_addresses': [], + 'post_url': 'some_url', + 'from_address': 'from_add', + 'to_addresses': 'to_add', + 'hostname_in_subject': False, + 'nht_data_enabled': True, + 'perf_data_enabled': False, + 'reminder_enabled': False, + 'private_data_removed': True, + 'local_collection_enabled': False, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_modify_pasword(): + ''' modify asup ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/system/node/autosupport', SRR['one_asup_record']), + ('PATCH', 'private/cli/system/node/autosupport', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + # different password, but no action + 'proxy_url': 'username1:password2@host.com:8080' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised('na_ontap_autosupport is not idempotent because the password value in proxy_url cannot be compared.') + + +def test_rest_get_error(): + ''' modify asup ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/system/node/autosupport', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == rest_error_message('Error fetching info', 'private/cli/system/node/autosupport') + + +def test_rest_modify_error(): + ''' modify asup ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/system/node/autosupport', SRR['one_asup_record']), + ('PATCH', 'private/cli/system/node/autosupport', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + 'ondemand_enabled': False, + 'partner_addresses': [] + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == rest_error_message('Error modifying asup', 'private/cli/system/node/autosupport') diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_autosupport_invoke.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_autosupport_invoke.py new file mode 100644 index 000000000..872cffa1b --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_autosupport_invoke.py @@ -0,0 +1,103 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_autosupport_invoke ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_autosupport_invoke \ + import NetAppONTAPasupInvoke as invoke_module # module under test + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error") +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def invoke_successfully(self, xml, enable_tunneling): + raise netapp_utils.zapi.NaApiError('test', 'Expected error') + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_wwpn_alias ''' + + def setUp(self): + self.mock_invoke = { + 'name': 'test_node', + 'message': 'test_message', + 'type': 'all' + } + + def mock_args(self): + return { + 'message': self.mock_invoke['message'], + 'name': self.mock_invoke['name'], + 'type': self.mock_invoke['type'], + 'hostname': 'test_host', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_invoke_mock_object(self, use_rest=True): + invoke_obj = invoke_module() + if not use_rest: + invoke_obj.ems_log_event = Mock() + invoke_obj.server = MockONTAPConnection() + return invoke_obj + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_send(self, mock_request): + '''Test successful send message''' + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_invoke_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_send_error(self, mock_request): + '''Test rest send error''' + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_invoke_mock_object().apply() + msg = "Error on sending autosupport message to node %s: Expected error." % data['name'] + assert exc.value.args[0]['msg'] == msg + + def test_zapi_send_error(self): + '''Test rest send error''' + data = self.mock_args() + data['use_rest'] = 'Never' + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_invoke_mock_object(use_rest=False).apply() + msg = "Error on sending autosupport message to node %s: NetApp API failed. Reason - test:Expected error." % data['name'] + assert exc.value.args[0]['msg'] == msg diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_bgp_peer_group.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_bgp_peer_group.py new file mode 100644 index 000000000..ea13a47fe --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_bgp_peer_group.py @@ -0,0 +1,211 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, expect_and_capture_ansible_exception, call_main +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, \ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_bgp_peer_group \ + import NetAppOntapBgpPeerGroup as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'bgpv4peer', + 'use_rest': 'always', + 'local': { + 'interface': { + 'name': 'lif1' + } + }, + 'peer': { + 'address': '10.10.10.7', + 'asn': 0 + } +} + + +SRR = rest_responses({ + 'bgp_peer_info': (200, {"records": [ + { + "ipspace": {"name": "exchange"}, + "local": { + "interface": {"ip": {"address": "10.10.10.7"}, "name": "lif1"}, + "port": {"name": "e1b", "node": {"name": "node1"}} + }, + "name": "bgpv4peer", + "peer": {"address": "10.10.10.7", "asn": 0}, + "state": "up", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }], "num_records": 1}, None), + 'bgp_modified': (200, {"records": [ + { + "ipspace": {"name": "exchange"}, + "local": { + "interface": {"ip": {"address": "10.10.10.7"}, "name": "lif1"}, + "port": {"name": "e1b", "node": {"name": "node1"}} + }, + "name": "bgpv4peer", + "peer": {"address": "10.10.10.8", "asn": 0}, + "state": "up", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }], "num_records": 1}, None), + 'bgp_name_modified': (200, {"records": [ + { + "ipspace": {"name": "exchange"}, + "local": { + "interface": {"ip": {"address": "10.10.10.7"}, "name": "lif1"}, + "port": {"name": "e1b", "node": {"name": "node1"}} + }, + "name": "newbgpv4peer", + "peer": {"address": "10.10.10.8", "asn": 0}, + "state": "up", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }], "num_records": 1}, None), + 'bgp_peer_info_ipv6': (200, {"records": [ + { + "ipspace": {"name": "exchange"}, + "name": "bgpv6peer", + "peer": {"address": "2402:940::45", "asn": 0}, + "state": "up", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }], "num_records": 1}, None), + 'bgp_modified_ipv6': (200, {"records": [ + { + "ipspace": {"name": "exchange"}, + "name": "bgpv6peer", + "peer": {"address": "2402:940::46", "asn": 0}, + "state": "up", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }], "num_records": 1}, None), +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "name"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_create_bgp_peer_group(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['empty_records']), + ('POST', 'network/ip/bgp/peer-groups', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_peer_info']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_modify_bgp_peer_group(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_peer_info']), + ('PATCH', 'network/ip/bgp/peer-groups/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_modified']), + # ipv6 modify + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_peer_info_ipv6']), + ('PATCH', 'network/ip/bgp/peer-groups/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_modified_ipv6']) + ]) + args = {'peer': {'address': '10.10.10.8'}} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + args = {'name': 'bgpv6peer', 'peer': {'address': '2402:0940:000:000:00:00:0000:0046'}} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_rename_modify_bgp_peer_group(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_peer_info']), + ('PATCH', 'network/ip/bgp/peer-groups/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_name_modified']) + ]) + args = {'from_name': 'bgpv4peer', 'name': 'newbgpv4peer', 'peer': {'address': '10.10.10.8'}} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_bgp_peer_group(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_peer_info']), + ('DELETE', 'network/ip/bgp/peer-groups/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['empty_records']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_all_methods_catch_exception(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # GET/POST/PATCH/DELETE error. + ('GET', 'network/ip/bgp/peer-groups', SRR['generic_error']), + ('POST', 'network/ip/bgp/peer-groups', SRR['generic_error']), + ('PATCH', 'network/ip/bgp/peer-groups/1cd8a442', SRR['generic_error']), + ('DELETE', 'network/ip/bgp/peer-groups/1cd8a442', SRR['generic_error']) + ]) + bgp_obj = create_module(my_module, DEFAULT_ARGS) + bgp_obj.uuid = '1cd8a442' + assert 'Error fetching BGP peer' in expect_and_capture_ansible_exception(bgp_obj.get_bgp_peer_group, 'fail')['msg'] + assert 'Error creating BGP peer' in expect_and_capture_ansible_exception(bgp_obj.create_bgp_peer_group, 'fail')['msg'] + assert 'Error modifying BGP peer' in expect_and_capture_ansible_exception(bgp_obj.modify_bgp_peer_group, 'fail', {})['msg'] + assert 'Error deleting BGP peer' in expect_and_capture_ansible_exception(bgp_obj.delete_bgp_peer_group, 'fail')['msg'] + + +def test_modify_rename_create_error(): + register_responses([ + # Error if both name and from_name not exist. + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['empty_records']), + ('GET', 'network/ip/bgp/peer-groups', SRR['empty_records']), + # Error if try to modify asn. + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['bgp_peer_info']), + # Error if peer and local not present in args when creating peer groups. + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/bgp/peer-groups', SRR['empty_records']) + ]) + assert 'Error renaming BGP peer group' in create_and_apply(my_module, DEFAULT_ARGS, {'from_name': 'name'}, fail=True)['msg'] + args = {'peer': {'asn': 5}} + assert 'Error: cannot modify peer asn.' in create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['peer'] + del DEFAULT_ARGS_COPY['local'] + assert 'Error creating BGP peer group' in create_and_apply(my_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + + +def test_error_ontap96(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + assert 'requires ONTAP 9.7.0 or later' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_broadcast_domain.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_broadcast_domain.py new file mode 100644 index 000000000..5a38d3933 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_broadcast_domain.py @@ -0,0 +1,808 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings, set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain \ + import NetAppOntapBroadcastDomain as broadcast_domain_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.type = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'broadcast_domain': + xml = self.build_broadcast_domain_info(self.params) + self.xml_out = xml + return xml + + @staticmethod + def build_broadcast_domain_info(broadcast_domain_details): + ''' build xml data for broadcast_domain info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'net-port-broadcast-domain-info': { + 'broadcast-domain': broadcast_domain_details['name'], + 'ipspace': broadcast_domain_details['ipspace'], + 'mtu': broadcast_domain_details['mtu'], + 'ports': { + 'port-info': { + 'port': 'test_port_1' + } + } + } + + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.mock_broadcast_domain = { + 'name': 'test_broadcast_domain', + 'mtu': 1000, + 'ipspace': 'Default', + 'ports': 'test_port_1' + } + + def mock_args(self): + return { + 'name': self.mock_broadcast_domain['name'], + 'ipspace': self.mock_broadcast_domain['ipspace'], + 'mtu': self.mock_broadcast_domain['mtu'], + 'ports': self.mock_broadcast_domain['ports'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never', + 'feature_flags': {'no_cserver_ems': True} + } + + def get_broadcast_domain_mock_object(self, kind=None, data=None): + """ + Helper method to return an na_ontap_volume object + :param kind: passes this param to MockONTAPConnection() + :param data: passes this param to MockONTAPConnection() + :return: na_ontap_volume object + """ + broadcast_domain_obj = broadcast_domain_module() + broadcast_domain_obj.asup_log_for_cserver = Mock(return_value=None) + broadcast_domain_obj.cluster = Mock() + broadcast_domain_obj.cluster.invoke_successfully = Mock() + if kind is None: + broadcast_domain_obj.server = MockONTAPConnection() + else: + if data is None: + broadcast_domain_obj.server = MockONTAPConnection(kind='broadcast_domain', data=self.mock_broadcast_domain) + else: + broadcast_domain_obj.server = MockONTAPConnection(kind='broadcast_domain', data=data) + return broadcast_domain_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + broadcast_domain_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_net_route(self): + ''' Test if get_broadcast_domain returns None for non-existent broadcast_domain ''' + set_module_args(self.mock_args()) + result = self.get_broadcast_domain_mock_object().get_broadcast_domain() + assert result is None + + def test_create_error_missing_broadcast_domain(self): + ''' Test if create throws an error if broadcast_domain is not specified''' + data = self.mock_args() + del data['name'] + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_broadcast_domain_mock_object('broadcast_domain').create_broadcast_domain() + msg = 'missing required arguments: name' + assert exc.value.args[0]['msg'] == msg + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.create_broadcast_domain') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.get_broadcast_domain') + def test_successful_create(self, get_broadcast_domain, create_broadcast_domain): + ''' Test successful create ''' + data = self.mock_args() + set_module_args(data) + get_broadcast_domain.side_effect = [None] + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object().apply() + assert exc.value.args[0]['changed'] + create_broadcast_domain.assert_called_with(None) + + def test_create_idempotency(self): + ''' Test create idempotency ''' + set_module_args(self.mock_args()) + obj = self.get_broadcast_domain_mock_object('broadcast_domain') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.create_broadcast_domain') + def test_create_idempotency_identical_ports(self, create_broadcast_domain): + ''' Test create idemptency identical ports ''' + data = self.mock_args() + data['ports'] = ['test_port_1', 'test_port_1'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object('broadcast_domain').apply() + assert not exc.value.args[0]['changed'] + + def test_modify_mtu(self): + ''' Test successful modify mtu ''' + data = self.mock_args() + data['mtu'] = 1200 + data['from_ipspace'] = 'test' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object('broadcast_domain').apply() + assert exc.value.args[0]['changed'] + + def test_modify_ipspace_idempotency(self): + ''' Test modify ipsapce idempotency''' + data = self.mock_args() + data['ipspace'] = 'Default' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object('broadcast_domain').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.add_broadcast_domain_ports') + def test_add_ports(self, add_broadcast_domain_ports): + ''' Test successful modify ports ''' + data = self.mock_args() + data['ports'] = 'test_port_1,test_port_2' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object('broadcast_domain').apply() + assert exc.value.args[0]['changed'] + add_broadcast_domain_ports.assert_called_with(['test_port_2']) + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.delete_broadcast_domain_ports') + def test_delete_ports(self, delete_broadcast_domain_ports): + ''' Test successful modify ports ''' + data = self.mock_args() + data['ports'] = '' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object('broadcast_domain').apply() + assert exc.value.args[0]['changed'] + delete_broadcast_domain_ports.assert_called_with(['test_port_1']) + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.modify_broadcast_domain') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.split_broadcast_domain') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.get_broadcast_domain') + def test_split_broadcast_domain(self, get_broadcast_domain, split_broadcast_domain, modify_broadcast_domain): + ''' Test successful split broadcast domain ''' + data = self.mock_args() + data['from_name'] = 'test_broadcast_domain' + data['name'] = 'test_broadcast_domain_2' + data['ports'] = 'test_port_2' + set_module_args(data) + current = { + 'domain-name': 'test_broadcast_domain', + 'mtu': 1000, + 'ipspace': 'Default', + 'ports': ['test_port_1,test_port2'] + } + get_broadcast_domain.side_effect = [ + None, + current + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object().apply() + assert exc.value.args[0]['changed'] + modify_broadcast_domain.assert_not_called() + split_broadcast_domain.assert_called_with() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.delete_broadcast_domain') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.modify_broadcast_domain') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.get_broadcast_domain') + def test_split_broadcast_domain_modify_delete(self, get_broadcast_domain, modify_broadcast_domain, delete_broadcast_domain): + ''' Test successful split broadcast domain ''' + data = self.mock_args() + data['from_name'] = 'test_broadcast_domain' + data['name'] = 'test_broadcast_domain_2' + data['ports'] = ['test_port_1', 'test_port_2'] + data['mtu'] = 1200 + set_module_args(data) + current = { + 'name': 'test_broadcast_domain', + 'mtu': 1000, + 'ipspace': 'Default', + 'ports': ['test_port_1', 'test_port2'] + } + get_broadcast_domain.side_effect = [ + None, + current + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object().apply() + assert exc.value.args[0]['changed'] + delete_broadcast_domain.assert_called_with('test_broadcast_domain') + modify_broadcast_domain.assert_called_with() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.get_broadcast_domain') + def test_split_broadcast_domain_not_exist(self, get_broadcast_domain): + ''' Test split broadcast domain does not exist ''' + data = self.mock_args() + data['from_name'] = 'test_broadcast_domain' + data['name'] = 'test_broadcast_domain_2' + data['ports'] = 'test_port_2' + set_module_args(data) + + get_broadcast_domain.side_effect = [ + None, + None, + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_broadcast_domain_mock_object().apply() + msg = 'A domain cannot be split if it does not exist.' + assert exc.value.args[0]['msg'], msg + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.split_broadcast_domain') + def test_split_broadcast_domain_idempotency(self, split_broadcast_domain): + ''' Test successful split broadcast domain ''' + data = self.mock_args() + data['from_name'] = 'test_broadcast_domain' + data['ports'] = 'test_port_1' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object('broadcast_domain').apply() + assert not exc.value.args[0]['changed'] + split_broadcast_domain.assert_not_called() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.delete_broadcast_domain') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.get_broadcast_domain') + def test_delete_broadcast_domain(self, get_broadcast_domain, delete_broadcast_domain): + ''' test delete broadcast domain ''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + current = { + 'name': 'test_broadcast_domain', + 'mtu': 1000, + 'ipspace': 'Default', + 'ports': ['test_port_1', 'test_port2'] + } + get_broadcast_domain.side_effect = [current] + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object().apply() + assert exc.value.args[0]['changed'] + delete_broadcast_domain.assert_called_with(current=current) + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.delete_broadcast_domain') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.get_broadcast_domain') + def test_delete_broadcast_domain_idempotent(self, get_broadcast_domain, delete_broadcast_domain): + ''' test delete broadcast domain ''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + get_broadcast_domain.side_effect = [None] + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object().apply() + assert not exc.value.args[0]['changed'] + delete_broadcast_domain.assert_not_called() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.delete_broadcast_domain') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_broadcast_domain.NetAppOntapBroadcastDomain.get_broadcast_domain') + def test_delete_broadcast_domain_if_all_ports_are_removed(self, get_broadcast_domain, delete_broadcast_domain): + ''' test delete broadcast domain if all the ports are deleted ''' + data = self.mock_args() + data['ports'] = [] + data['state'] = 'present' + set_module_args(data) + current = { + 'name': 'test_broadcast_domain', + 'mtu': 1000, + 'ipspace': 'Default', + 'ports': ['test_port_1', 'test_port2'] + } + get_broadcast_domain.side_effect = [current] + with pytest.raises(AnsibleExitJson) as exc: + self.get_broadcast_domain_mock_object().apply() + assert exc.value.args[0]['changed'] + delete_broadcast_domain.assert_called_with(current=current) + + +def default_args(): + args = { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_9_7': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'port_detail_e0d': (200, { + "num_records": 1, + "records": [ + { + 'name': 'e0d', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': 'ea670505-2ab3-11ec-aa30-005056b3dfc8' + }] + }, None), + 'port_detail_e0a': (200, { + "num_records": 1, + "records": [ + { + 'name': 'e0a', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': 'ea63420b-2ab3-11ec-aa30-005056b3dfc8' + }] + }, None), + 'port_detail_e0b': (200, { + "num_records": 1, + "records": [ + { + 'name': 'e0b', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': 'ea64c0f2-2ab3-11ec-aa30-005056b3dfc8' + }] + }, None), + 'broadcast_domain_record': (200, { + "num_records": 1, + "records": [ + { + "uuid": "4475a2c8-f8a0-11e8-8d33-005056bb986f", + "name": "domain1", + "ipspace": {"name": "ip1"}, + "ports": [ + { + "uuid": "ea63420b-2ab3-11ec-aa30-005056b3dfc8", + "name": "e0a", + "node": { + "name": "mohan9cluster2-01" + } + }, + { + "uuid": "ea64c0f2-2ab3-11ec-aa30-005056b3dfc8", + "name": "e0b", + "node": { + "name": "mohan9cluster2-01" + } + }, + { + "uuid": "ea670505-2ab3-11ec-aa30-005056b3dfc8", + "name": "e0d", + "node": { + "name": "mohan9cluster2-01" + } + } + ], + "mtu": 9000 + }] + }, None), + 'broadcast_domain_record_split': (200, { + "num_records": 1, + "records": [ + { + "uuid": "4475a2c8-f8a0-11e8-8d33-005056bb986f", + "name": "domain2", + "ipspace": {"name": "ip1"}, + "ports": [ + { + "uuid": "ea63420b-2ab3-11ec-aa30-005056b3dfc8", + "name": "e0a", + "node": { + "name": "mohan9cluster2-01" + } + } + ], + "mtu": 9000 + }] + }, None) +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + broadcast_domain_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_create_broadcast_domain(mock_request, patch_ansible): + ''' test create broadcast domain ''' + args = dict(default_args()) + args['name'] = "domain1" + args['ipspace'] = "ip1" + args['mtu'] = "9000" + args['ports'] = ["mohan9cluster2-01:e0a", "mohan9cluster2-01:e0b", "mohan9cluster2-01:e0d"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0a'], + SRR['port_detail_e0b'], + SRR['port_detail_e0d'], + SRR['zero_record'], # get + SRR['empty_good'], # create + SRR['empty_good'], # add e0a + SRR['empty_good'], # add e0b + SRR['empty_good'], # add e0c + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_create_broadcast_domain_idempotency(mock_request, patch_ansible): + ''' test create broadcast domain ''' + args = dict(default_args()) + args['name'] = "domain1" + args['ipspace'] = "ip1" + args['mtu'] = 9000 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['broadcast_domain_record'], # get + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_create_broadcast_domain_idempotency_identical_ports(mock_request, patch_ansible): + ''' test create broadcast domain ''' + args = dict(default_args()) + args['name'] = "domain2" + args['ipspace'] = "ip1" + args['mtu'] = 9000 + args['ports'] = ['mohan9cluster2-01:e0a', 'mohan9cluster2-01:e0a'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0a'], + SRR['broadcast_domain_record_split'], # get + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_modify_broadcast_domain(mock_request, patch_ansible): + ''' test modify broadcast domain mtu ''' + args = dict(default_args()) + args['name'] = "domain1" + args['ipspace'] = "ip1" + args['mtu'] = 1500 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['broadcast_domain_record'], # get + SRR['empty_good'], # modify + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rename_broadcast_domain(mock_request, patch_ansible): + ''' test modify broadcast domain mtu ''' + args = dict(default_args()) + args['from_name'] = "domain1" + args['name'] = "domain2" + args['ipspace'] = "ip1" + args['mtu'] = 1500 + args['ports'] = ["mohan9cluster2-01:e0a", "mohan9cluster2-01:e0b", "mohan9cluster2-01:e0d"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0a'], + SRR['port_detail_e0b'], + SRR['port_detail_e0d'], + SRR['zero_record'], # get + SRR['broadcast_domain_record'], # get + SRR['empty_good'], # rename broadcast domain + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_split_broadcast_domain_create_domain2_with_e0a(mock_request, patch_ansible): + ''' test modify broadcast domain mtu ''' + args = dict(default_args()) + args['from_name'] = "domain1" + args['name'] = "domain2" + args['ipspace'] = "ip1" + args['mtu'] = 1500 + args['ports'] = ["mohan9cluster2-01:e0a"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0a'], + SRR['zero_record'], # get + SRR['broadcast_domain_record'], # get + SRR['empty_good'], # create broadcast domain + SRR['empty_good'], # add e0a to domain2 + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_split_broadcast_domain_create_domain2_with_e0a_idempotent(mock_request, patch_ansible): + ''' test modify broadcast domain mtu ''' + args = dict(default_args()) + args['from_name'] = "domain1" + args['name'] = "domain2" + args['ipspace'] = "ip1" + args['mtu'] = 1500 + args['ports'] = ["mohan9cluster2-01:e0a"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0a'], + SRR['broadcast_domain_record_split'], # get domain2 details + SRR['zero_record'], # empty record for domain1 + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_create_new_broadcast_domain_with_partial_match(mock_request, patch_ansible): + ''' test modify broadcast domain mtu ''' + args = dict(default_args()) + args['from_name'] = "domain2" + args['name'] = "domain1" + args['ipspace'] = "ip1" + args['mtu'] = 1500 + args['ports'] = ["mohan9cluster2-01:e0b"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['zero_record'], # empty record for domain1 + SRR['broadcast_domain_record_split'], # get domain2 details + SRR['empty_good'], # create broadcast domain domain1 + SRR['empty_good'], # add e0b to domain1 + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_delete_broadcast_domain(mock_request, patch_ansible): + ''' test delete broadcast domain mtu ''' + args = dict(default_args()) + args['name'] = "domain1" + args['ipspace'] = "ip1" + args['mtu'] = 1500 + args['state'] = "absent" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['broadcast_domain_record'], # get + SRR['empty_good'], # remove all the ports in broadcast domain + SRR['empty_good'], # delete broadcast domain + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_try_to_bad_format_port(mock_request, patch_ansible): + ''' test delete broadcast domain mtu ''' + args = dict(default_args()) + args['name'] = "domain1" + args['ipspace'] = "ip1" + args['mtu'] = 1500 + args['state'] = "present" + args['ports'] = ["mohan9cluster2-01e0a"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = broadcast_domain_module() + print('Info: %s' % exc.value.args[0]) + msg = "Error: Invalid value specified for port: mohan9cluster2-01e0a, provide port name as node_name:port_name" + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_try_to_create_domain_without_ipspace(mock_request, patch_ansible): + ''' test delete broadcast domain mtu ''' + args = dict(default_args()) + args['name'] = "domain1" + args['mtu'] = 1500 + args['state'] = "present" + args['ports'] = ["mohan9cluster2-01:e0a"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = broadcast_domain_module() + print('Info: %s' % exc.value.args[0]) + msg = "Error: ipspace space is a required option with REST" + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_modify_ipspace(mock_request, patch_ansible): + ''' test modify ipspace ''' + args = dict(default_args()) + args['name'] = "domain2" + args['from_ipspace'] = "ip1" + args['ipspace'] = "Default" + args['mtu'] = 1500 + args['ports'] = ["mohan9cluster2-01:e0b"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['zero_record'], # empty record for domain2 in ipspace Default + SRR['broadcast_domain_record_split'], # get domain2 details in ipspace ip1 + SRR['empty_good'], # modify ipspace + SRR['empty_good'], # add e0b to domain2 + SRR['empty_good'], # remove e0a + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_modify_name_and_ipspace(mock_request, patch_ansible): + ''' test modify ipspace ''' + args = dict(default_args()) + args['from_name'] = "domain2" + args['name'] = "domain1" + args['from_ipspace'] = "ip1" + args['ipspace'] = "Default" + args['mtu'] = 1500 + args['ports'] = ["mohan9cluster2-01:e0a"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0a'], + SRR['zero_record'], # empty record for domain2 in ipspace Default + SRR['broadcast_domain_record_split'], # get domain2 details in ipspace ip1 + SRR['empty_good'], # modify name, ipspace and mtu + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_split_name_ipspace_if_not_exact_match_of_ports(mock_request, patch_ansible): + ''' test create new domain as exact match not found ''' + args = dict(default_args()) + args['from_name'] = "domain2" + args['name'] = "domain1" + args['from_ipspace'] = "ip1" + args['ipspace'] = "Default" + args['mtu'] = 1500 + args['ports'] = ["mohan9cluster2-01:e0b"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['zero_record'], # empty record for domain1 in ipspace Default + SRR['broadcast_domain_record_split'], # get domain2 details in ipspace ip1 + SRR['empty_good'], # create new broadcast domain domain1 in ipspace Default + SRR['empty_good'], # Add e0b to domain1 + SRR['end_of_sequence'] + ] + my_obj = broadcast_domain_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cg_snapshot.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cg_snapshot.py new file mode 100644 index 000000000..78c35ba73 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cg_snapshot.py @@ -0,0 +1,81 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_cg_snapshot''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cg_snapshot \ + import NetAppONTAPCGSnapshot as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, parm1=None): + ''' save arguments ''' + self.type = kind + self.parm1 = parm1 + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'vserver': + xml = self.build_vserver_info(self.parm1) + self.xml_out = xml + return xml + + @staticmethod + def build_vserver_info(vserver): + ''' build xml data for vserser-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = netapp_utils.zapi.NaElement('attributes-list') + attributes.add_node_with_children('vserver-info', + **{'vserver-name': vserver}) + xml.add_child_elem(attributes) + # print(xml.to_string()) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_command_called(self): + ''' a more interesting test ''' + set_module_args({ + 'vserver': 'vserver', + 'volumes': 'volumes', + 'snapshot': 'snapshot', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + }) + my_obj = my_module() + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + my_obj.cgcreate() + msg = 'Error fetching CG ID for CG commit snapshot' + assert exc.value.args[0]['msg'] == msg diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs.py new file mode 100644 index 000000000..99aa0d140 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs.py @@ -0,0 +1,464 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_cifs ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + patch_ansible, call_main, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs \ + import NetAppONTAPCifsShare as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'cifs_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "name": 'cifs_share_name', + "path": '/', + "comment": 'CIFS share comment', + "unix_symlink": 'widelink', + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + }, + "access_based_enumeration": True, + "change_notify": True, + "encryption": False, + "home_directory": True, + "oplocks": False, + "continuously_available": True, + "show_snapshot": True, + "namespace_caching": True, + "allow_unencrypted_access": True, + "browsable": True, + "show_previous_versions": True + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + +cifs_record_info = { + 'num-records': 1, + 'attributes-list': { + 'cifs-share': { + 'share-name': 'cifs_share_name', + 'path': '/test', + 'vscan-fileop-profile': 'standard', + 'share-properties': [{'cifs-share-properties': 'browsable'}, {'cifs-share-properties': 'show_previous_versions'}], + 'symlink-properties': [{'cifs-share-symlink-properties': 'enable'}] + } + } +} + +ZRR = zapi_responses({ + 'cifs_record_info': build_zapi_response(cifs_record_info) +}) + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'admin', + 'password': 'netapp1!', + 'name': 'cifs_share_name', + 'path': '/test', + 'share_properties': ['browsable', 'show-previous-versions'], + 'symlink_properties': 'enable', + 'vscan_fileop_profile': 'standard', + 'vserver': 'abc', + 'use_rest': 'never' +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + error = 'missing required arguments:' + assert error in call_main(my_main, {}, fail=True)['msg'] + + +def test_get(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']) + ]) + cifs_obj = create_module(my_module, DEFAULT_ARGS) + result = cifs_obj.get_cifs_share() + assert result + + +def test_error_create(): + register_responses([ + ('cifs-share-get-iter', ZRR['empty']), + ('cifs-share-create', ZRR['error']), + ]) + module_args = { + 'state': 'present' + } + error = create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] + assert 'Error creating cifs-share' in error + + +def test_create(): + register_responses([ + ('cifs-share-get-iter', ZRR['empty']), + ('cifs-share-create', ZRR['success']), + ]) + module_args = { + 'state': 'present', + 'comment': 'some_comment' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']), + ('cifs-share-delete', ZRR['success']), + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_delete(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']), + ('cifs-share-delete', ZRR['error']), + ]) + module_args = { + 'state': 'absent' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error deleting cifs-share' in error + + +def test_modify_path(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']), + ('cifs-share-modify', ZRR['success']), + ]) + module_args = { + 'path': '//' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_comment(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']), + ('cifs-share-modify', ZRR['success']), + ]) + module_args = { + 'comment': 'cifs modify' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_share_properties(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']), + ('cifs-share-modify', ZRR['success']), + ]) + module_args = { + 'share_properties': 'oplocks' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_symlink_properties(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']), + ('cifs-share-modify', ZRR['success']), + ]) + module_args = { + 'symlink_properties': 'read_only' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_vscan_fileop_profile(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']), + ('cifs-share-modify', ZRR['success']), + ]) + module_args = { + 'vscan_fileop_profile': 'strict' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']), + ('cifs-share-modify', ZRR['error']), + ]) + module_args = { + 'symlink_properties': 'read' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error modifying cifs-share' in error + + +def test_create_idempotency(): + register_responses([ + ('cifs-share-get-iter', ZRR['cifs_record_info']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] is False + + +def test_delete_idempotency(): + register_responses([ + ('cifs-share-get-iter', ZRR['empty']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] is False + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('cifs-share-create', ZRR['error']), + ('cifs-share-modify', ZRR['error']), + ('cifs-share-delete', ZRR['error']) + ]) + module_args = {} + + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.create_cifs_share, 'fail')['msg'] + assert 'Error creating cifs-share cifs_share_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.modify_cifs_share, 'fail')['msg'] + assert 'Error modifying cifs-share cifs_share_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.delete_cifs_share, 'fail')['msg'] + assert 'Error deleting cifs-share cifs_share_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'test_vserver', + 'name': 'cifs_share_name', + 'path': '/', + 'unix_symlink': 'widelink', +} + + +def test_options_support(): + ''' test option support ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + module_args = { + 'show_snapshot': True, + 'allow_unencrypted_access': True, + 'browsable': True + } + error = 'Error: Minimum version of ONTAP' + assert error in create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + + +def test_rest_successful_create(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['empty_records']), + ('POST', 'protocols/cifs/shares', SRR['empty_good']), + ]) + module_args = { + 'comment': 'CIFS share comment', + 'unix_symlink': 'disable' + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_delete_rest(): + ''' Test delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('DELETE', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent', + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_rest_error_get(): + '''Test error rest get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on fetching cifs shares: calling: protocols/cifs/shares: got Expected error.' in error + + +def test_rest_error_create(): + '''Test error rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['empty_records']), + ('POST', 'protocols/cifs/shares', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on creating cifs shares:' in error + + +def test_error_delete_rest(): + ''' Test error delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('DELETE', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']), + ]) + module_args = { + 'state': 'absent' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on deleting cifs shares:' in error + + +def test_modify_cifs_share_path(): + ''' test modify CIFS share path ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/cifs_share_name', SRR['empty_good']), + ]) + module_args = { + 'path': "\\vol1" + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_modify_cifs_share_comment(): + ''' test modify CIFS share comment ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/cifs_share_name', SRR['empty_good']), + ]) + module_args = { + 'comment': "cifs comment modify" + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_modify_cifs_share_properties(): + ''' test modify CIFS share properties ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/cifs_share_name', SRR['empty_good']), + ]) + module_args = { + 'unix_symlink': "disable" + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_modify_cifs_share_properties_2(): + ''' test modify CIFS share properties ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/cifs_share_name', SRR['empty_good']), + ]) + module_args = { + "access_based_enumeration": False, + "change_notify": False, + "encryption": True, + "oplocks": True, + "continuously_available": False, + "show_snapshot": False, + "namespace_caching": False, + "allow_unencrypted_access": False, + "browsable": False, + "show_previous_versions": False + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_error_modify_cifs_share_path(): + ''' test modify CIFS share path error''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/cifs_share_name', SRR['generic_error']), + ]) + module_args = { + 'path': "\\vol1" + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on modifying cifs shares:' in error + + +def test_error_modify_cifs_share_comment(): + ''' test modify CIFS share comment error''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/cifs_share_name', SRR['generic_error']), + ]) + module_args = { + 'comment': "cifs comment modify" + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on modifying cifs shares:' in error + + +def test_rest_successful_create_idempotency(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']) + ]) + module_args = { + 'use_rest': 'always' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] is False + + +def test_rest_successful_delete_idempotency(): + '''Test successful rest delete''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['empty_records']) + ]) + module_args = {'use_rest': 'always', 'state': 'absent'} + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] is False + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_missing_netapp_lib(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg == call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_acl.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_acl.py new file mode 100644 index 000000000..1d0d565cd --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_acl.py @@ -0,0 +1,412 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_cifs_acl """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_acl \ + import NetAppONTAPCifsAcl as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +SHARE_NAME = 'share_name' + +acl_info = {'num-records': 1, + 'attributes-list': + {'cifs-share-access-control': + {'share': SHARE_NAME, + 'user-or-group': 'user123', + 'permission': 'full_control', + 'user-group-type': 'windows' + } + }, + } + +ZRR = zapi_responses({ + 'acl_info': build_zapi_response(acl_info), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'permission': 'full_control', + 'share_name': 'share_name', + 'user_or_group': 'user_or_group', + 'vserver': 'vserver', + 'use_rest': 'never', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + error_msg = create_module(my_module, fail=True)['msg'] + for fragment in 'missing required arguments:', 'hostname', 'share_name', 'user_or_group', 'vserver': + assert fragment in error_msg + assert 'permission' not in error_msg + + args = dict(DEFAULT_ARGS) + args.pop('permission') + msg = 'state is present but all of the following are missing: permission' + assert create_module(my_module, args, fail=True)['msg'] == msg + + +def test_create(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['empty']), + ('cifs-share-access-control-create', ZRR['success']), + ]) + module_args = { + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_with_type(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['empty']), + ('cifs-share-access-control-create', ZRR['success']), + ]) + module_args = { + 'type': 'unix_group' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['acl_info']), + ('cifs-share-access-control-delete', ZRR['success']), + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_idempotent(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['empty']), + ]) + module_args = { + 'state': 'absent' + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['acl_info']), + ('cifs-share-access-control-modify', ZRR['success']), + ]) + module_args = { + 'permission': 'no_access', + 'type': 'windows' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_modify_idempotent(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['acl_info']), + ]) + module_args = { + 'permission': 'full_control', + 'type': 'windows' + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_modify_with_type(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['acl_info']), + ]) + module_args = { + 'type': 'unix_group' + } + msg = 'Error: changing the type is not supported by ONTAP - current: windows, desired: unix_group' + assert create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_negative_modify_with_extra_stuff(): + register_responses([ + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + current = {'share_name': 'extra'} + msg = "Error: only permission can be changed - modify: {'share_name': 'share_name'}" + assert msg in expect_and_capture_ansible_exception(my_module_object.get_modify, 'fail', current)['msg'] + + current = {'share_name': 'extra', 'permission': 'permission'} + # don't check dict contents as order may differ + msg = "Error: only permission can be changed - modify:" + assert msg in expect_and_capture_ansible_exception(my_module_object.get_modify, 'fail', current)['msg'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['error']), + ('cifs-share-access-control-create', ZRR['error']), + ('cifs-share-access-control-modify', ZRR['error']), + ('cifs-share-access-control-delete', ZRR['error']), + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + + msg = 'Error getting cifs-share-access-control share_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_cifs_acl, 'fail')['msg'] + + msg = 'Error creating cifs-share-access-control share_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg in expect_and_capture_ansible_exception(my_module_object.create_cifs_acl, 'fail')['msg'] + + msg = 'Error modifying cifs-share-access-control permission share_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg in expect_and_capture_ansible_exception(my_module_object.modify_cifs_acl_permission, 'fail')['msg'] + + msg = 'Error deleting cifs-share-access-control share_name: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert msg in expect_and_capture_ansible_exception(my_module_object.delete_cifs_acl, 'fail')['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_missing_netapp_lib(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg in create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + + +def test_main(): + register_responses([ + ('cifs-share-access-control-get-iter', ZRR['empty']), + ('cifs-share-access-control-create', ZRR['success']), + ]) + set_module_args(DEFAULT_ARGS) + assert expect_and_capture_ansible_exception(my_main, 'exit')['changed'] + + +SRR = rest_responses({ + 'acl_record': (200, {"records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "share": "share_name", + "user_or_group": "Everyone", + "permission": "full_control", + "type": "windows" + } + ], "num_records": 1}, None), + 'cifs_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "name": 'share_name', + "path": '/', + "comment": 'CIFS share comment', + "unix_symlink": 'widelink', + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + } + } + ], + "num_records": 1 + }, None + ) +}) + +ARGS_REST = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'permission': 'full_control', + 'share_name': 'share_name', + 'user_or_group': 'Everyone', + 'vserver': 'vserver', + 'type': 'windows', + 'use_rest': 'always', +} + + +def test_error_get_acl_rest(): + ''' Test get error with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on fetching cifs shares acl:' in error + + +def test_error_get_share_rest(): + ''' Test get share not exists with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on fetching cifs shares:' in error + + +def test_error_get_no_share_rest(): + ''' Test get share not exists with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['empty_records']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error: the cifs share does not exist:' in error + + +def test_create_rest(): + ''' Test create with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['empty_records']), + ('POST', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['empty_good']), + ]) + assert create_and_apply(my_module, ARGS_REST) + + +def test_delete_rest(): + ''' Test delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['acl_record']), + ('DELETE', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls/Everyone/windows', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_create_error_rest(): + ''' Test create error with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['empty_records']), + ('POST', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on creating cifs share acl:' in error + + +def test_error_delete_rest(): + ''' Test delete error with rest API ''' + module_args = { + 'state': 'absent' + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['acl_record']), + ('DELETE', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls/Everyone/windows', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on deleting cifs share acl:' in error + + +def test_modify_rest(): + ''' Test modify with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['acl_record']), + ('PATCH', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls/Everyone/windows', SRR['empty_good']), + ]) + module_args = { + 'permission': 'no_access' + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_error_modify_rest(): + ''' Test modify error with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['acl_record']), + ('PATCH', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls/Everyone/windows', SRR['generic_error']) + ]) + module_args = {'permission': 'no_access'} + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + msg = 'Error modifying cifs share ACL permission: '\ + 'calling: protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls/Everyone/windows: got Expected error.' + assert msg == error + + +def test_error_get_modify_rest(): + ''' Test modify error with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['acl_record']), + ]) + module_args = { + 'type': 'unix_group' + } + msg = 'Error: changing the type is not supported by ONTAP - current: windows, desired: unix_group' + assert create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] == msg + + +def test_negative_modify_with_extra_stuff_rest(): + ''' Test modify error with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']) + ]) + my_module_object = create_module(my_module, ARGS_REST) + current = {'share_name': 'extra'} + msg = "Error: only permission can be changed - modify: {'share_name': 'share_name'}" + assert msg in expect_and_capture_ansible_exception(my_module_object.get_modify, 'fail', current)['msg'] + + current = {'share_name': 'extra', 'permission': 'permission'} + # don't check dict contents as order may differ + msg = "Error: only permission can be changed - modify:" + assert msg in expect_and_capture_ansible_exception(my_module_object.get_modify, 'fail', current)['msg'] + + +def test_delete_idempotent_rest(): + ''' Test delete idempotency with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['empty_records']), + ]) + module_args = { + 'state': 'absent' + } + assert not create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_create_modify_idempotent_rest(): + ''' Test create and modify idempotency with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/shares', SRR['cifs_record']), + ('GET', 'protocols/cifs/shares/671aa46e-11ad-11ec-a267-005056b30cfa/share_name/acls', SRR['acl_record']), + ]) + module_args = { + 'permission': 'full_control', + 'type': 'windows' + } + assert not create_and_apply(my_module, ARGS_REST, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_group.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_group.py new file mode 100644 index 000000000..afe73d191 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_group.py @@ -0,0 +1,218 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_cifs_local_group ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, call_main, create_module, expect_and_capture_ansible_exception, AnsibleFailJson, create_and_apply +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_local_group \ + import NetAppOntapCifsLocalGroup as group_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'group_record': (200, {"records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansible" + }, + 'name': 'BUILTIN\\Guests', + 'sid': 'S-1-5-21-256008430-3394229847-3930036330-1001', + } + ], "num_records": 1}, None), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + + +ARGS_REST = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS', +} + + +def test_get_existent_cifs_local_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ]) + cifs_obj = create_module(group_module, ARGS_REST) + result = cifs_obj.get_cifs_local_group_rest() + assert result + + +def test_error_get_existent_cifs_local_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['generic_error']), + ]) + module_args = { + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS', + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = 'Error on fetching cifs local-group:' + assert msg in error + + +def test_create_cifs_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['empty_records']), + ('POST', 'protocols/cifs/local-groups', SRR['empty_good']), + ]) + module_args = { + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_create_cifs_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['empty_records']), + ('POST', 'protocols/cifs/local-groups', SRR['generic_error']), + ]) + module_args = { + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS', + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on creating cifs local-group:" + assert msg in error + + +def test_delete_cifs_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('DELETE', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001', SRR['empty_good']), + ]) + module_args = { + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS', + 'state': 'absent' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_delete_cifs_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('DELETE', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001', SRR['generic_error']), + ]) + module_args = { + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS', + 'state': 'absent' + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on deleting cifs local-group:" + assert msg in error + + +def test_modify_cifs_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('PATCH', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001', SRR['empty_good']), + ]) + module_args = { + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS', + 'description': 'This is local group' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_modify_cifs_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('PATCH', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001', SRR['generic_error']), + ]) + module_args = { + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS', + 'description': 'This is local group' + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on modifying cifs local-group:" + assert msg in error + + +def test_rename_cifs_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('PATCH', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001', SRR['empty_good']), + ]) + module_args = { + 'vserver': 'ansible', + 'from_name': 'BUILTIN\\GUESTS', + 'name': 'ANSIBLE_CIFS\\test_users' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_rest_rename_cifs_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/cifs/local-groups', SRR['empty_records']), + ('GET', 'protocols/cifs/local-groups', SRR['empty_records']), + ]) + module_args = { + 'vserver': 'ansible', + 'from_name': 'BUILTIN\\GUESTS_user', + 'name': 'ANSIBLE_CIFS\\test_users' + } + error = create_and_apply(group_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error renaming cifs local group:' in error + + +def test_successfully_create_group_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ]) + module_args = { + 'vserver': 'ansible', + 'name': 'BUILTIN\\GUESTS', + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_successfully_destroy_group_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['empty_records']), + ]) + module_args = { + 'state': 'absent' + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_group_member.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_group_member.py new file mode 100644 index 000000000..8afd0c56a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_group_member.py @@ -0,0 +1,338 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_cifs_local_group_member ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, call_main, create_module, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_local_group_member \ + import NetAppOntapCifsLocalGroupMember as group_member_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'group_member_record': (200, {"records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "vserver" + }, + 'group_name': 'BUILTIN\\Guests', + 'member': 'test', + 'sid': 'S-1-5-21-256008430-3394229847-3930036330-1001', + } + ], "num_records": 1}, None), + 'group_record': (200, {"records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "vserver" + }, + 'group_name': 'BUILTIN\\Guests', + 'sid': 'S-1-5-21-256008430-3394229847-3930036330-1001', + } + ], "num_records": 1}, None), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + +group_member_info = {'num-records': 1, + 'attributes-list': + {'cifs-local-group-members': + {'group-name': 'BUILTIN\\GUESTS', + 'member': 'test', + 'vserver': 'ansible' + } + }, + } + +ZRR = zapi_responses({ + 'group_member_info': build_zapi_response(group_member_info) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + 'use_rest': 'never', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + group_member_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get_nonexistent_cifs_group_member(): + register_responses([ + ('cifs-local-group-members-get-iter', ZRR['empty']) + ]) + cifs_obj = create_module(group_member_module, DEFAULT_ARGS) + result = cifs_obj.get_cifs_local_group_member() + assert result is None + + +def test_get_existent_cifs_group_member(): + register_responses([ + ('cifs-local-group-members-get-iter', ZRR['group_member_info']) + ]) + cifs_obj = create_module(group_member_module, DEFAULT_ARGS) + result = cifs_obj.get_cifs_local_group_member() + assert result + + +def test_successfully_add_members_zapi(): + register_responses([ + ('cifs-local-group-members-get-iter', ZRR['empty']), + ('cifs-local-group-members-add-members', ZRR['success']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_add_members_zapi(): + register_responses([ + ('cifs-local-group-members-get-iter', ZRR['empty']), + ('cifs-local-group-members-add-members', ZRR['error']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error adding member" + assert msg in error + + +def test_successfully_remove_members_zapi(): + register_responses([ + ('cifs-local-group-members-get-iter', ZRR['group_member_info']), + ('cifs-local-group-members-remove-members', ZRR['success']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_remove_members_zapi(): + register_responses([ + ('cifs-local-group-members-get-iter', ZRR['group_member_info']), + ('cifs-local-group-members-remove-members', ZRR['error']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + 'state': 'absent' + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error removing member" + assert msg in error + + +def test_successfully_add_members_zapi_idempotency(): + register_responses([ + ('cifs-local-group-members-get-iter', ZRR['group_member_info']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_remove_members_zapi_idempotency(): + register_responses([ + ('cifs-local-group-members-get-iter', ZRR['empty']), + ]) + module_args = { + 'state': 'absent' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +ARGS_REST = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + 'use_rest': 'always', +} + + +def test_get_nonexistent_cifs_local_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['empty_records']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'nogroup', + 'member': 'test', + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = 'CIFS local group nogroup does not exist on vserver ansible' + assert msg in error + + +def test_get_existent_cifs_local_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('GET', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['group_member_record']), + ]) + cifs_obj = create_module(group_member_module, ARGS_REST) + result = cifs_obj.get_cifs_local_group_member() + assert result + + +def test_error_get_existent_cifs_local_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('GET', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['generic_error']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = 'Error getting CIFS local group members for group BUILTIN\\GUESTS on vserver ansible' + assert msg in error + + +def test_add_cifs_group_member_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('GET', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['empty_records']), + ('POST', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['empty_good']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_add_cifs_group_member_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('GET', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['empty_records']), + ('POST', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['generic_error']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error adding member test to cifs local group BUILTIN\\GUESTS on vserver" + assert msg in error + + +def test_remove_cifs_group_member_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('GET', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['group_member_record']), + ('DELETE', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['empty_good']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + 'state': 'absent' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_remove_cifs_group_member_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('GET', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['group_member_record']), + ('DELETE', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['generic_error']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + 'state': 'absent' + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error removing member test from cifs local group BUILTIN\\GUESTS on vserver ansible" + assert msg in error + + +def test_successfully_add_members_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('GET', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['group_member_record']), + ]) + module_args = { + 'vserver': 'ansible', + 'group': 'BUILTIN\\GUESTS', + 'member': 'test', + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_successfully_remove_members_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-groups', SRR['group_record']), + ('GET', 'protocols/cifs/local-groups/671aa46e-11ad-11ec-a267-005056b30cfa/' + 'S-1-5-21-256008430-3394229847-3930036330-1001/members', SRR['empty_records']), + ]) + module_args = { + 'state': 'absent' + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user.py new file mode 100644 index 000000000..812512a06 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user.py @@ -0,0 +1,204 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, call_main, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_local_user \ + import NetAppOntapCifsLocalUser as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'local_user_sid': (200, { + "records": [{ + "sid": "S-1-5-21-256008430-3394229847-3930036330-1001", + "members": [{ + "name": "string" + }], + "name": "SMB_SERVER01\\username", + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "description": "This is a local group", + "full_name": "User Name", + "account_disabled": False + }] + }, None), + 'svm_uuid': (200, {"records": [ + { + 'uuid': 'e3cb5c7f-cd20' + }], "num_records": 1}, None), +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'name': "username" +} + + +def test_low_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + error = create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error: na_ontap_cifs_local_user only supports REST, and requires ONTAP 9.10.1 or later.' + assert msg in error + + +def test_get_svm_uuid_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['generic_error']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.get_svm_uuid, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error fetching vserver vserver: calling: svm/svms: got Expected error.' == error + + +def test_get_cifs_local_user_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/cifs/local-users', SRR['zero_records']), + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_cifs_local_user() is None + + +def test_get_cifs_local_user_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/cifs/local-users', SRR['generic_error']), + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching cifs/local-user username: calling: protocols/cifs/local-users: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_cifs_local_user, 'fail')['msg'] + + +def test_get_cifs_local_user(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/cifs/local-users', SRR['local_user_sid']), + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_cifs_local_user() is not None + + +def test_create_cifs_local_user(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/cifs/local-users', SRR['empty_records']), + ('POST', 'protocols/cifs/local-users', SRR['empty_good']) + ]) + module_args = {'name': 'username', + 'user_password': 'password', + 'account_disabled': 'False', + 'full_name': 'User Name', + 'description': 'Test user'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_cifs_local_user_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('POST', 'protocols/cifs/local-users', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['name'] = 'username' + my_obj.parameters['user_password'] = 'password' + my_obj.parameters['account_disabled'] = False + my_obj.parameters['full_name'] = 'User Name' + my_obj.parameters['description'] = 'This is a local group' + error = expect_and_capture_ansible_exception(my_obj.create_cifs_local_user, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating CIFS local users with name username: calling: protocols/cifs/local-users: got Expected error.' == error + + +def test_delete_cifs_local_user(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/cifs/local-users', SRR['local_user_sid']), + ('DELETE', 'protocols/cifs/local-users/e3cb5c7f-cd20/S-1-5-21-256008430-3394229847-3930036330-1001', SRR['empty_good']) + ]) + module_args = {'name': 'username', + 'state': 'absent', + 'user_password': 'password', + 'description': 'This is a local group'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_cifs_local_user_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('DELETE', 'protocols/cifs/local-users/e3cb5c7f-cd20/S-1-5-21-256008430-3394229847-3930036330-1001', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.svm_uuid = 'e3cb5c7f-cd20' + my_obj.sid = 'S-1-5-21-256008430-3394229847-3930036330-1001' + my_obj.parameters['name'] = 'username' + my_obj.parameters['state'] = 'absent' + my_obj.parameters['user_password'] = 'password' + my_obj.parameters['description'] = 'This is a local group' + error = expect_and_capture_ansible_exception(my_obj.delete_cifs_local_user, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error while deleting CIFS local user: calling: '\ + 'protocols/cifs/local-users/e3cb5c7f-cd20/S-1-5-21-256008430-3394229847-3930036330-1001: got Expected error.' == error + + +def test_modify_cifs_local_user(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/cifs/local-users', SRR['local_user_sid']), + ('PATCH', 'protocols/cifs/local-users/e3cb5c7f-cd20/S-1-5-21-256008430-3394229847-3930036330-1001', SRR['empty_good']) + ]) + module_args = {'name': 'username', + 'user_password': 'mypassword', + 'description': 'This is a local group2', + 'account_disabled': True, + 'full_name': 'Full Name'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_cifs_local_user_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('PATCH', 'protocols/cifs/local-users/e3cb5c7f-cd20/S-1-5-21-256008430-3394229847-3930036330-1001', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.svm_uuid = 'e3cb5c7f-cd20' + my_obj.sid = 'S-1-5-21-256008430-3394229847-3930036330-1001' + my_obj.parameters['name'] = 'username' + my_obj.parameters['user_password'] = 'mypassword' + my_obj.parameters['description'] = 'This is a local group2' + current = {'description': 'This is a local group'} + error = expect_and_capture_ansible_exception(my_obj.modify_cifs_local_user, 'fail', current)['msg'] + print('Info: %s' % error) + assert 'Error while modifying CIFS local user: calling: '\ + 'protocols/cifs/local-users/e3cb5c7f-cd20/S-1-5-21-256008430-3394229847-3930036330-1001: got Expected error.' == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_modify.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_modify.py new file mode 100644 index 000000000..44e75a856 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_modify.py @@ -0,0 +1,223 @@ +''' unit tests ONTAP Ansible module: na_ontap_cifs_local_user_modify ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_local_user_modify \ + import NetAppOntapCifsLocalUserModify as cifs_user_module # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'cifs_user_record': (200, { + "records": [{ + 'vserver': 'ansible', + 'user_name': 'ANSIBLE\\Administrator', + 'is_account_disabled': False, + 'full_name': 'test user', + 'description': 'builtin admin' + }] + }, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'local_user': + xml = self.build_local_user_info() + elif self.type == 'local_user_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_local_user_info(): + ''' build xml data for cifs-local-user ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': { + 'cifs-local-user': { + 'user-name': 'ANSIBLE\\Administrator', + 'is-account-disabled': 'false', + 'vserver': 'ansible', + 'full-name': 'test user', + 'description': 'builtin admin' + } + } + } + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self, use_rest=None): + if self.onbox: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + vserver = 'ansible' + name = 'ANSIBLE\\Administrator' + is_account_disabled = False + + else: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + vserver = 'ansible' + name = 'ANSIBLE\\Administrator' + is_account_disabled = False + + args = dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'vserver': vserver, + 'name': name, + 'is_account_disabled': is_account_disabled + }) + + if use_rest is not None: + args['use_rest'] = use_rest + + return args + + @staticmethod + def get_local_user_mock_object(cx_type='zapi', kind=None): + local_user_obj = cifs_user_module() + if cx_type == 'zapi': + if kind is None: + local_user_obj.server = MockONTAPConnection() + else: + local_user_obj.server = MockONTAPConnection(kind=kind) + return local_user_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + cifs_user_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called(self): + ''' test get_cifs_local_user_modify for non-existent user''' + set_module_args(self.set_default_args(use_rest='Never')) + print('starting') + my_obj = cifs_user_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = self.server + assert my_obj.get_cifs_local_user is not None + + def test_ensure_get_called_existing(self): + ''' test get_cifs_local_user_modify for existing user''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = cifs_user_module() + my_obj.server = MockONTAPConnection(kind='local_user') + assert my_obj.get_cifs_local_user() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_local_user_modify.NetAppOntapCifsLocalUserModify.modify_cifs_local_user') + def test_successful_modify(self, modify_cifs_local_user): + ''' enabling local cifs user and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['is_account_disabled'] = True + set_module_args(data) + my_obj = cifs_user_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('local_user') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # to reset na_helper from remembering the previous 'changed' value + data = self.set_default_args(use_rest='Never') + set_module_args(data) + my_obj = cifs_user_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('local_user') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + data = self.set_default_args(use_rest='Never') + set_module_args(data) + my_obj = cifs_user_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('local_user_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_cifs_local_user(modify={}) + assert 'Error modifying local CIFS user' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_local_user_mock_object(cx_type='rest').apply() + msg = 'calling: private/cli/vserver/cifs/users-and-groups/local-user: got %s.' % SRR['generic_error'][2] + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_modify_rest(self, mock_request): + data = self.set_default_args() + data['is_account_disabled'] = True + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cifs_user_record'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_local_user_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_modify_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cifs_user_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_local_user_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_set_password.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_set_password.py new file mode 100644 index 000000000..62c8352b7 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_set_password.py @@ -0,0 +1,66 @@ +# (c) 2021-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP disks Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import call_main, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_local_user_set_password import main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +ZRR = zapi_responses({ +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'never', + 'user_password': 'test', + 'user_name': 'user1', + 'vserver': 'svm1', +} + + +def test_successful_set_password(patch_ansible): + ''' successful set ''' + register_responses([ + ('ZAPI', 'cifs-local-user-set-password', ZRR['success']), + ]) + assert call_main(my_main, DEFAULT_ARGS)['changed'] + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + error = 'missing required arguments:' + assert error in call_main(my_main, {}, fail=True)['msg'] + + +def test_if_all_methods_catch_exception(patch_ansible): + register_responses([ + ('ZAPI', 'cifs-local-user-set-password', ZRR['error']), + ]) + assert 'Error setting password ' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + error = 'Error: the python NetApp-Lib module is required. Import error: None' + assert error in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_set_password_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_set_password_rest.py new file mode 100644 index 000000000..f32a1b2fa --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_local_user_set_password_rest.py @@ -0,0 +1,101 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + patch_ansible, call_main, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_local_user_set_password \ + import NetAppONTAPCifsSetPassword as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'svm_uuid': (200, {"records": [ + { + 'uuid': 'e3cb5c7f-cd20' + }], "num_records": 1}, None), + 'local_user_sid': (200, {"records": [{'sid': '1234-sd'}]}, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'user_name': 'carchi8py', + 'user_password': 'p@SSWord', + 'vserver': 'vserver' +} + + +def test_change_password(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/cifs/local-users', SRR['local_user_sid']), + ('PATCH', 'protocols/cifs/local-users/e3cb5c7f-cd20/1234-sd', SRR['empty_good']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {})['changed'] + + +def test_get_svm_uuid_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['generic_error']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.get_svm_uuid, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error fetching vserver vserver: calling: svm/svms: got Expected error.' == error + + +def test_get_cifs_local_users_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/local-users', SRR['generic_error']), + # 2nd call + ('GET', 'protocols/cifs/local-users', SRR['zero_records']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.get_user_sid, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error fetching cifs/local-user carchi8py: calling: protocols/cifs/local-users: got Expected error.' == error + # no user + error = 'Error no cifs/local-user with name carchi8py' + assert error in expect_and_capture_ansible_exception(my_obj.get_user_sid, 'fail')['msg'] + + +def test_patch_cifs_local_users_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/cifs/local-users', SRR['local_user_sid']), + ('PATCH', 'protocols/cifs/local-users/e3cb5c7f-cd20/1234-sd', SRR['generic_error']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.cifs_local_set_passwd_rest, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error change password for user carchi8py: calling: protocols/cifs/local-users/e3cb5c7f-cd20/1234-sd: got Expected error.' == error + + +def test_fail_old_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always' + } + error = 'Error: REST requires ONTAP 9.10.1 or later for protocols/cifs/local-users APIs.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_server.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_server.py new file mode 100644 index 000000000..820c33d17 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cifs_server.py @@ -0,0 +1,770 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_cifs_server ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cifs_server \ + import NetAppOntapcifsServer as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'cifs_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "enabled": True, + "security": { + "encrypt_dc_connection": False, + "smb_encryption": False, + "kdc_encryption": False, + "smb_signing": False, + "restrict_anonymous": "no_enumeration", + "aes_netlogon_enabled": False, + "ldap_referral_enabled": False, + "session_security": "none", + "try_ldap_channel_binding": True, + "use_ldaps": False, + "use_start_tls": False + }, + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + }, + "name": "cifs_server_name" + } + ], + "num_records": 1 + }, None + ), + 'cifs_record_disabled': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "enabled": False, + "security": { + "encrypt_dc_connection": False, + "smb_encryption": False, + "kdc_encryption": False, + "smb_signing": False, + "restrict_anonymous": "no_enumeration", + "aes_netlogon_enabled": False, + "ldap_referral_enabled": False, + "session_security": "none", + "try_ldap_channel_binding": True, + "use_ldaps": False, + "use_start_tls": False + }, + "target": { + "nam,e": "20:05:00:50:56:b3:0c:fa" + }, + "name": "cifs_server_name" + } + ], + "num_records": 1 + }, None + ), + 'cifs_records_renamed': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "enabled": True, + "security": { + "encrypt_dc_connection": False, + "smb_encryption": False, + "kdc_encryption": False, + "smb_signing": False, + "restrict_anonymous": "no_enumeration", + "aes_netlogon_enabled": False, + "ldap_referral_enabled": False, + "session_security": "none", + "try_ldap_channel_binding": True, + "use_ldaps": False, + "use_start_tls": False + }, + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + }, + "name": "cifs" + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + + +cifs_record_info = { + 'num-records': 1, + 'attributes-list': { + 'cifs-server-config': { + 'cifs-server': 'cifs_server', + 'administrative-status': 'up'} + } +} +cifs_record_disabled_info = { + 'num-records': 1, + 'attributes-list': { + 'cifs-server-config': { + 'cifs-server': 'cifs_server', + 'administrative-status': 'down'} + } +} + +ZRR = zapi_responses({ + 'cifs_record_info': build_zapi_response(cifs_record_info), + 'cifs_record_disabled_info': build_zapi_response(cifs_record_disabled_info) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'cifs_server_name': 'cifs_server', + 'vserver': 'vserver', + 'use_rest': 'never', + 'feature_flags': {'no_cserver_ems': True} +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get(): + register_responses([ + ('cifs-server-get-iter', ZRR['cifs_record_info']) + ]) + cifs_obj = create_module(my_module, DEFAULT_ARGS) + result = cifs_obj.get_cifs_server() + assert result + + +def test_create_unsupport_zapi(): + """ check for zapi unsupported options """ + module_args = { + "use_rest": "never", + "encrypt_dc_connection": "false", + "smb_encryption": "false", + "kdc_encryption": "false", + "smb_signing": "false" + } + msg = 'Error: smb_signing ,encrypt_dc_connection ,kdc_encryption ,smb_encryption options supported only with REST.' + assert msg == create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_create(): + register_responses([ + ('cifs-server-get-iter', ZRR['empty']), + ('cifs-server-create', ZRR['success']) + ]) + module_args = { + 'workgroup': 'test', + 'ou': 'ou', + 'domain': 'test', + 'admin_user_name': 'user1', + 'admin_password': 'password' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_with_service_state_started(): + register_responses([ + ('cifs-server-get-iter', ZRR['empty']), + ('cifs-server-create', ZRR['success']), + # idempotent check + ('cifs-server-get-iter', ZRR['cifs_record_info']) + ]) + module_args = { + 'workgroup': 'test', + 'ou': 'ou', + 'domain': 'test', + 'admin_user_name': 'user1', + 'admin_password': 'password', + 'service_state': 'started' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_with_service_state_stopped(): + register_responses([ + ('cifs-server-get-iter', ZRR['empty']), + ('cifs-server-create', ZRR['success']), + # idempotent check + ('cifs-server-get-iter', ZRR['cifs_record_disabled_info']) + ]) + module_args = { + 'workgroup': 'test', + 'ou': 'ou', + 'domain': 'test', + 'admin_user_name': 'user1', + 'admin_password': 'password', + 'service_state': 'stopped' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_with_force(): + register_responses([ + ('cifs-server-get-iter', ZRR['empty']), + ('cifs-server-create', ZRR['success']), + ]) + module_args = { + 'workgroup': 'test', + 'ou': 'ou', + 'domain': 'test', + 'admin_user_name': 'user1', + 'admin_password': 'password', + 'force': 'true' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_idempotent(): + register_responses([ + ('cifs-server-get-iter', ZRR['cifs_record_info']) + ]) + module_args = { + 'state': 'present' + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_idempotent(): + register_responses([ + ('cifs-server-get-iter', ZRR['empty']) + ]) + module_args = { + 'state': 'absent' + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete(): + register_responses([ + ('cifs-server-get-iter', ZRR['cifs_record_info']), + ('cifs-server-delete', ZRR['success']), + ]) + module_args = { + 'workgroup': 'test', + 'ou': 'ou', + 'domain': 'test', + 'admin_user_name': 'user1', + 'admin_password': 'password', + 'force': 'false', + 'state': 'absent' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_start_service_state(): + register_responses([ + ('cifs-server-get-iter', ZRR['cifs_record_info']), + ('cifs-server-stop', ZRR['success']), + ]) + module_args = { + 'service_state': 'stopped' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args) + + +def test_stop_service_state(): + register_responses([ + ('cifs-server-get-iter', ZRR['cifs_record_disabled_info']), + ('cifs-server-start', ZRR['success']), + ]) + module_args = { + 'service_state': 'started' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args) + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('cifs-server-create', ZRR['error']), + ('cifs-server-start', ZRR['error']), + ('cifs-server-stop', ZRR['error']), + ('cifs-server-delete', ZRR['error']) + ]) + module_args = {} + + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.create_cifs_server, 'fail')['msg'] + assert 'Error Creating cifs_server cifs_server: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.start_cifs_server, 'fail')['msg'] + assert 'Error modifying cifs_server cifs_server: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.stop_cifs_server, 'fail')['msg'] + assert 'Error modifying cifs_server cifs_server: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.delete_cifs_server, 'fail')['msg'] + assert 'Error deleting cifs_server cifs_server: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'test_vserver', + 'name': 'cifs_server_name', +} + + +def test_rest_error_get(): + '''Test error rest get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on fetching cifs:' in error + + +def test_module_error_ontap_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + module_args = {'use_rest': 'always', 'force': True} + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Minimum version of ONTAP for force is (9, 11)' in error + + +def test_rest_successful_create(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['empty_good']), + ]) + assert create_and_apply(my_module, ARGS_REST) + + +def test_rest_successful_create_with_force(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['empty_good']), + ]) + module_args = { + 'force': True + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_rest_successful_create_with_user(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['empty_good']), + # idempotent check. + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ]) + module_args = { + 'admin_user_name': 'test_user', + 'admin_password': 'pwd' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + assert not create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_create_with_service_state(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['empty_good']), + # idempotent check. + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record_disabled']), + ]) + module_args = { + 'admin_user_name': 'test_user', + 'admin_password': 'pwd', + 'service_state': 'stopped' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + assert not create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_create_with_ou(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['empty_good']), + ]) + module_args = { + 'ou': 'ou' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_create_with_domain(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['empty_good']), + ]) + module_args = { + 'domain': 'domain' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_create_with_security(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['empty_good']), + ]) + module_args = { + 'smb_encryption': True, + 'smb_signing': True, + 'kdc_encryption': True, + 'encrypt_dc_connection': True, + 'restrict_anonymous': 'no_enumeration' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_version_error_with_security_encryption(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + module_args = { + 'use_rest': 'always', + 'encrypt_dc_connection': True, + } + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Minimum version of ONTAP for encrypt_dc_connection is (9, 8)' in error + + +def test_module_error_ontap_version_security(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']) + ]) + module_args = { + "aes_netlogon_enabled": False + } + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Minimum version of ONTAP for aes_netlogon_enabled is (9, 10, 1)' in error + + +def test_rest_error_create(): + '''Test error rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('POST', 'protocols/cifs/services', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on creating cifs:' in error + + +def test_delete_rest(): + ''' Test delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ('DELETE', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent', + 'admin_user_name': 'test_user', + 'admin_password': 'pwd' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_delete_with_force_rest(): + ''' Test delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ('DELETE', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent', + 'force': True, + 'admin_user_name': 'test_user', + 'admin_password': 'pwd' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_error_delete_rest(): + ''' Test error delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ('DELETE', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']), + ]) + module_args = { + 'state': 'absent' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on deleting cifs server:' in error + + +def test_rest_successful_disable(): + '''Test successful rest disable''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + 'service_state': 'stopped' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_enable(): + '''Test successful rest enable''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record_disabled']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + 'service_state': 'started' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_security_modify(): + '''Test successful rest enable''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record_disabled']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + 'smb_encryption': True, + 'smb_signing': True, + 'kdc_encryption': True, + 'restrict_anonymous': "no_enumeration" + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_security_modify_encrypt(): + '''Test successful rest enable''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'protocols/cifs/services', SRR['cifs_record_disabled']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + 'encrypt_dc_connection': True + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_negative_security_options_modify(): + '''Test error rest enable''' + register_responses([ + ]) + module_args = { + "aes_netlogon_enabled": True, + "ldap_referral_enabled": True, + "session_security": "seal", + "try_ldap_channel_binding": False, + "use_ldaps": True, + "use_start_tls": True + } + msg = 'parameters are mutually exclusive: use_ldaps|use_start_tls' + assert msg in create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_successful_security_options_modify(): + '''Test successful rest enable''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/cifs/services', SRR['cifs_record_disabled']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + "aes_netlogon_enabled": True, + "ldap_referral_enabled": True, + "session_security": "seal", + "try_ldap_channel_binding": False, + "use_ldaps": True + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_rename_cifs(): + '''Test successful rest rename''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('GET', 'protocols/cifs/services', SRR['cifs_record_disabled']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']) + ]) + module_args = { + 'from_name': 'cifs_server_name', + 'name': 'cifs', + 'force': True, + 'admin_user_name': 'test_user', + 'admin_password': 'pwd' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_rename_modify_cifs(): + '''Test successful rest rename''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']) + ]) + module_args = { + 'from_name': 'cifs_server_name', + 'name': 'cifs', + 'force': True, + 'admin_user_name': 'test_user', + 'admin_password': 'pwd', + 'service_state': 'stopped' + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_error_rest_rename_cifs_without_force(): + '''Test error rest rename with force false''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ]) + module_args = { + 'from_name': 'cifs_servers', + 'name': 'cifs1', + 'force': False, + 'admin_user_name': 'test_user', + 'admin_password': 'pwd' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error: cannot rename cifs server from cifs_servers to cifs1 without force.' in error + + +def test_error_rest_rename_error_state(): + '''Test error rest rename with service state as started''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']), + ]) + module_args = { + 'from_name': 'cifs_servers', + 'name': 'cifs1', + 'force': True, + 'admin_user_name': 'test_user', + 'admin_password': 'pwd', + 'service_state': 'started' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + msg = 'Error on modifying cifs server: calling: protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa:' + assert msg in error + + +def test_error_rest_rename_cifs(): + '''Test error rest rename''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ('GET', 'protocols/cifs/services', SRR['empty_records']), + ]) + module_args = { + 'from_name': 'cifs_servers_test', + 'name': 'cifs1', + 'force': True, + 'admin_user_name': 'test_user', + 'admin_password': 'pwd' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error renaming cifs server: cifs1 - no cifs server with from_name: cifs_servers_test' in error + + +def test_rest_error_disable(): + '''Test error rest disable''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']), + ('PATCH', 'protocols/cifs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']), + ]) + module_args = { + 'service_state': 'stopped' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on modifying cifs server:' in error + + +def test_rest_successful_create_idempotency(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['cifs_record']) + ]) + module_args = {'use_rest': 'always'} + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] is False + + +def test_rest_successful_delete_idempotency(): + '''Test successful rest delete''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/cifs/services', SRR['empty_records']) + ]) + module_args = {'use_rest': 'always', 'state': 'absent'} + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] is False diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster.py new file mode 100644 index 000000000..89fe069a3 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster.py @@ -0,0 +1,688 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_cluster ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock, call +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster \ + import NetAppONTAPCluster as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'cluster': + xml = self.build_cluster_info() + if self.type == 'cluster_success': + xml = self.build_cluster_info_success() + elif self.type == 'cluster_add': + xml = self.build_add_node_info() + elif self.type == 'cluster_extra_input': + self.type = 'cluster' # success on second call + raise netapp_utils.zapi.NaApiError(code='TEST1', message="Extra input: single-node-cluster") + elif self.type == 'cluster_extra_input_loop': + raise netapp_utils.zapi.NaApiError(code='TEST2', message="Extra input: single-node-cluster") + elif self.type == 'cluster_extra_input_other': + raise netapp_utils.zapi.NaApiError(code='TEST3', message="Extra input: other-unexpected-element") + elif self.type == 'cluster_fail': + raise netapp_utils.zapi.NaApiError(code='TEST4', message="This exception is from the unit test") + self.xml_out = xml + return xml + + def autosupport_log(self): + ''' mock autosupport log''' + return None + + @staticmethod + def build_cluster_info(): + ''' build xml data for cluster-create-join-progress-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'attributes': { + 'cluster-create-join-progress-info': { + 'is-complete': 'true', + 'status': 'whatever' + } + } + } + xml.translate_struct(attributes) + return xml + + @staticmethod + def build_cluster_info_success(): + ''' build xml data for cluster-create-join-progress-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'attributes': { + 'cluster-create-join-progress-info': { + 'is-complete': 'false', + 'status': 'success' + } + } + } + xml.translate_struct(attributes) + return xml + + @staticmethod + def build_add_node_info(): + ''' build xml data for cluster-create-add-node-status-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'attributes-list': { + 'cluster-create-add-node-status-info': { + 'failure-msg': '', + 'status': 'success' + } + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.use_vsim = False + + def set_default_args(self, use_rest='never'): + hostname = '10.10.10.10' + username = 'admin' + password = 'password' + cluster_name = 'abc' + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'cluster_name': cluster_name, + 'use_rest': use_rest + }) + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + @patch('time.sleep') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + def test_ensure_apply_for_cluster_called(self, get_cl_id, sleep_mock): + ''' creating cluster and checking idempotency ''' + get_cl_id.return_value = None + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + + @patch('time.sleep') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.create_cluster') + def test_cluster_create_called(self, cluster_create, get_cl_id, sleep_mock): + ''' creating cluster''' + get_cl_id.return_value = None + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_success') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + cluster_create.assert_called_with() + + @patch('time.sleep') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + def test_cluster_create_old_api(self, get_cl_id, sleep_mock): + ''' creating cluster''' + get_cl_id.return_value = None + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_extra_input') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + def test_cluster_create_old_api_loop(self, get_cl_id): + ''' creating cluster''' + get_cl_id.return_value = None + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_extra_input_loop') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = 'TEST2:Extra input: single-node-cluster' + print('Info: test_cluster_apply: %s' % repr(exc.value)) + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + def test_cluster_create_old_api_other_extra(self, get_cl_id): + ''' creating cluster''' + get_cl_id.return_value = None + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_extra_input_other') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = 'TEST3:Extra input: other-unexpected-element' + print('Info: test_cluster_apply: %s' % repr(exc.value)) + assert msg in exc.value.args[0]['msg'] + + @patch('time.sleep') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_ip_addresses') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.add_node') + def test_add_node_called(self, add_node, get_cl_id, get_cl_ips, sleep_mock): + ''' creating add_node''' + get_cl_ips.return_value = [] + get_cl_id.return_value = None + data = self.set_default_args() + del data['cluster_name'] + data['cluster_ip_address'] = '10.10.10.10' + set_module_args(data) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_add') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + add_node.assert_called_with() + assert exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_cluster() + assert 'Error creating cluster' in exc.value.args[0]['msg'] + data = self.set_default_args() + data['cluster_ip_address'] = '10.10.10.10' + set_module_args(data) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.add_node() + assert 'Error adding node with ip' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_ip_addresses') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.add_node') + def test_add_node_idempotent(self, add_node, get_cl_id, get_cl_ips): + ''' creating add_node''' + get_cl_ips.return_value = ['10.10.10.10'] + get_cl_id.return_value = None + data = self.set_default_args() + del data['cluster_name'] + data['cluster_ip_address'] = '10.10.10.10' + set_module_args(data) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_add') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + try: + add_node.assert_not_called() + except AttributeError: + # not supported with python <= 3.4 + pass + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_ip_addresses') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.remove_node') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.node_remove_wait') + def test_remove_node_ip(self, wait, remove_node, get_cl_id, get_cl_ips): + ''' creating add_node''' + get_cl_ips.return_value = ['10.10.10.10'] + get_cl_id.return_value = None + wait.return_value = None + data = self.set_default_args() + # del data['cluster_name'] + data['cluster_ip_address'] = '10.10.10.10' + data['state'] = 'absent' + set_module_args(data) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_add') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + remove_node.assert_called_with() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_ip_addresses') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.remove_node') + def test_remove_node_ip_idempotent(self, remove_node, get_cl_id, get_cl_ips): + ''' creating add_node''' + get_cl_ips.return_value = [] + get_cl_id.return_value = None + data = self.set_default_args() + # del data['cluster_name'] + data['cluster_ip_address'] = '10.10.10.10' + data['state'] = 'absent' + set_module_args(data) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_add') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + try: + remove_node.assert_not_called() + except AttributeError: + # not supported with python <= 3.4 + pass + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_nodes') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.remove_node') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.node_remove_wait') + def test_remove_node_name(self, wait, remove_node, get_cl_id, get_cl_nodes): + ''' creating add_node''' + get_cl_nodes.return_value = ['node1', 'node2'] + get_cl_id.return_value = None + wait.return_value = None + data = self.set_default_args() + # del data['cluster_name'] + data['node_name'] = 'node2' + data['state'] = 'absent' + data['force'] = True + set_module_args(data) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_add') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + remove_node.assert_called_with() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_nodes') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.get_cluster_identity') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster.NetAppONTAPCluster.remove_node') + def test_remove_node_name_idempotent(self, remove_node, get_cl_id, get_cl_nodes): + ''' creating add_node''' + get_cl_nodes.return_value = ['node1', 'node2'] + get_cl_id.return_value = None + data = self.set_default_args() + # del data['cluster_name'] + data['node_name'] = 'node3' + data['state'] = 'absent' + set_module_args(data) + my_obj = my_module() + my_obj.autosupport_log = Mock(return_value=None) + if not self.use_vsim: + my_obj.server = MockONTAPConnection('cluster_add') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_cluster_apply: %s' % repr(exc.value)) + try: + remove_node.assert_not_called() + except AttributeError: + # not supported with python <= 3.4 + pass + assert not exc.value.args[0]['changed'] + + def test_remove_node_name_and_id(self): + ''' creating add_node''' + data = self.set_default_args() + # del data['cluster_name'] + data['cluster_ip_address'] = '10.10.10.10' + data['node_name'] = 'node3' + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + print('Info: test_remove_node_name_and_id: %s' % repr(exc.value)) + msg = 'when state is "absent", parameters are mutually exclusive: cluster_ip_address|node_name' + assert msg in exc.value.args[0]['msg'] + + +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy_9_7_0')), None), + 'is_rest_95': (200, dict(version=dict(generation=9, major=5, minor=0, full='dummy_9_5_0')), None), + 'is_rest_96': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy_9_6_0')), None), + 'is_rest_97': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy_9_7_0')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': ({}, None, None), + 'zero_record': (200, {'records': []}, None), + 'precluster': (500, None, {'message': 'are available in precluster.'}), + 'cluster_identity': (200, {'location': 'Oz', 'name': 'abc'}, None), + 'nodes': (200, {'records': [ + {'name': 'node2', 'uuid': 'uuid2', 'cluster_interfaces': [{'ip': {'address': '10.10.10.2'}}]} + ]}, None), + 'end_of_sequence': (None, None, "Unexpected call to send_request"), + 'generic_error': (None, "Expected error"), +} + + +def set_default_args(use_rest='auto'): + hostname = '10.10.10.10' + username = 'admin' + password = 'password' + cluster_name = 'abc' + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'cluster_name': cluster_name, + 'use_rest': use_rest + }) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create(mock_request, patch_ansible): + ''' create cluster ''' + args = dict(set_default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['precluster'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create_timezone(mock_request, patch_ansible): + ''' create cluster ''' + args = dict(set_default_args()) + args['timezone'] = {'name': 'America/Los_Angeles'} + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['precluster'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create_single(mock_request, patch_ansible): + ''' create cluster ''' + args = dict(set_default_args()) + args['single_node_cluster'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['precluster'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + post_call = call('POST', 'cluster', {'return_timeout': 30, 'single_node_cluster': True}, json={'name': 'abc'}, headers=None, files=None) + assert post_call in mock_request.mock_calls + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify(mock_request, patch_ansible): + ''' modify cluster location ''' + args = dict(set_default_args()) + args['cluster_location'] = 'Mars' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cluster_identity'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is True + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify_timezone(mock_request, patch_ansible): + ''' modify cluster location ''' + args = dict(set_default_args()) + args['timezone'] = {'name': 'America/Los_Angeles'} + args['cluster_location'] = 'Mars' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cluster_identity'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is True + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify_idempotent(mock_request, patch_ansible): + ''' modify cluster location ''' + args = dict(set_default_args()) + args['cluster_location'] = 'Oz' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cluster_identity'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is False + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_add_node(mock_request, patch_ansible): + ''' modify cluster location ''' + args = dict(set_default_args()) + args['node_name'] = 'node2' + args['cluster_ip_address'] = '10.10.10.2' + + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cluster_identity'], # get + SRR['zero_record'], # get nodes + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is True + assert len(mock_request.mock_calls) == 4 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_remove_node_by_ip(mock_request, patch_ansible): + ''' modify cluster location ''' + args = dict(set_default_args()) + # args['node_name'] = 'node2' + args['cluster_ip_address'] = '10.10.10.2' + args['state'] = 'absent' + + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cluster_identity'], # get + SRR['nodes'], # get nodes + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is True + assert len(mock_request.mock_calls) == 4 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_remove_node_by_ip_idem(mock_request, patch_ansible): + ''' modify cluster location ''' + args = dict(set_default_args()) + # args['node_name'] = 'node2' + args['cluster_ip_address'] = '10.10.10.3' + args['state'] = 'absent' + + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cluster_identity'], # get + SRR['nodes'], # get nodes + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is False + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_remove_node_by_name(mock_request, patch_ansible): + ''' modify cluster location ''' + args = dict(set_default_args()) + args['node_name'] = 'node2' + # args['cluster_ip_address'] = '10.10.10.2' + args['state'] = 'absent' + args['force'] = True + + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cluster_identity'], # get + SRR['nodes'], # get nodes + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is True + assert len(mock_request.mock_calls) == 4 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_remove_node_by_name_idem(mock_request, patch_ansible): + ''' modify cluster location ''' + args = dict(set_default_args()) + args['node_name'] = 'node3' + # args['cluster_ip_address'] = '10.10.10.2' + args['state'] = 'absent' + + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['cluster_identity'], # get + SRR['nodes'], # get nodes + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] is False + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_remove_node_by_name_rest_96(mock_request, patch_ansible): + ''' revert to ZAPI for 9.6 ''' + args = dict(set_default_args()) + args['node_name'] = 'node3' + # args['cluster_ip_address'] = '10.10.10.2' + args['state'] = 'absent' + + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_96'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + # revert to ZAPI for 9.6 + assert not my_obj.use_rest diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_ha.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_ha.py new file mode 100644 index 000000000..a03f5c5aa --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_ha.py @@ -0,0 +1,140 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for ONTAP Ansible module: na_ontap_cluster_ha ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster_ha \ + import NetAppOntapClusterHA as cluster_ha # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + +DEFAULT_ARGS = { + 'hostname': '10.10.10.10', + 'username': 'user', + 'password': 'pass', + 'state': 'present', + 'use_rest': 'never' +} + +cluster_ha_enabled = { + 'attributes': { + 'cluster-ha-info': {'ha-configured': 'true'} + } +} + +cluster_ha_disabled = { + 'attributes': { + 'cluster-ha-info': {'ha-configured': 'false'} + } +} + + +ZRR = zapi_responses({ + 'cluster_ha_enabled': build_zapi_response(cluster_ha_enabled), + 'cluster_ha_disabled': build_zapi_response(cluster_ha_disabled) +}) + + +SRR = rest_responses({ + 'cluster_ha_enabled': (200, {"records": [{ + 'configured': True + }], "num_records": 1}, None), + 'cluster_ha_disabled': (200, {"records": [{ + 'configured': False + }], "num_records": 1}, None) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname"] + error = create_module(cluster_ha, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_enable_cluster_ha(): + ''' enable cluster ha ''' + register_responses([ + ('cluster-ha-get', ZRR['cluster_ha_disabled']), + ('cluster-ha-modify', ZRR['success']), + ('cluster-ha-get', ZRR['cluster_ha_enabled']) + ]) + assert create_and_apply(cluster_ha, DEFAULT_ARGS)['changed'] + assert not create_and_apply(cluster_ha, DEFAULT_ARGS)['changed'] + + +def test_disable_cluster_ha(): + ''' disable cluster ha ''' + register_responses([ + ('cluster-ha-get', ZRR['cluster_ha_enabled']), + ('cluster-ha-modify', ZRR['success']), + ('cluster-ha-get', ZRR['cluster_ha_disabled']), + ]) + assert create_and_apply(cluster_ha, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(cluster_ha, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('cluster-ha-get', ZRR['error']), + ('cluster-ha-modify', ZRR['error']), + ('cluster-ha-modify', ZRR['error']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'private/cli/cluster/ha', SRR['generic_error']), + ('PATCH', 'private/cli/cluster/ha', SRR['generic_error']), + ('PATCH', 'private/cli/cluster/ha', SRR['generic_error']) + ]) + ha_obj = create_module(cluster_ha, DEFAULT_ARGS) + assert 'Error fetching cluster HA' in expect_and_capture_ansible_exception(ha_obj.get_cluster_ha_enabled, 'fail')['msg'] + assert 'Error modifying cluster HA to true' in expect_and_capture_ansible_exception(ha_obj.modify_cluster_ha, 'fail', 'true')['msg'] + assert 'Error modifying cluster HA to false' in expect_and_capture_ansible_exception(ha_obj.modify_cluster_ha, 'fail', 'false')['msg'] + + ucm_obj = create_module(cluster_ha, DEFAULT_ARGS, {'use_rest': 'always'}) + assert 'Error fetching cluster HA' in expect_and_capture_ansible_exception(ucm_obj.get_cluster_ha_enabled, 'fail')['msg'] + assert 'Error modifying cluster HA to true' in expect_and_capture_ansible_exception(ucm_obj.modify_cluster_ha, 'fail', 'true')['msg'] + assert 'Error modifying cluster HA to false' in expect_and_capture_ansible_exception(ucm_obj.modify_cluster_ha, 'fail', 'false')['msg'] + + +def test_enable_cluster_ha_rest(): + ''' enable cluster ha in rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'private/cli/cluster/ha', SRR['cluster_ha_disabled']), + ('PATCH', 'private/cli/cluster/ha', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'private/cli/cluster/ha', SRR['cluster_ha_enabled']) + ]) + assert create_and_apply(cluster_ha, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + assert not create_and_apply(cluster_ha, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + + +def test_disable_cluster_ha_rest(): + ''' disable cluster ha in rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'private/cli/cluster/ha', SRR['cluster_ha_enabled']), + ('PATCH', 'private/cli/cluster/ha', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'private/cli/cluster/ha', SRR['cluster_ha_disabled']), + ]) + args = {'use_rest': 'always', 'state': 'absent'} + assert create_and_apply(cluster_ha, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(cluster_ha, DEFAULT_ARGS, args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_peer.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_peer.py new file mode 100644 index 000000000..7551a619e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_cluster_peer.py @@ -0,0 +1,305 @@ +''' unit tests ONTAP Ansible module: na_ontap_cluster_peer ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, patch_ansible, create_and_apply +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cluster_peer \ + import NetAppONTAPClusterPeer as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def update_cluster_peer_info_zapi(cluster_name, peer_addresses): + return { + 'num-records': 1, + 'attributes-list': { + 'cluster-peer-info': { + 'cluster-name': cluster_name, + 'peer-addresses': peer_addresses + } + } + } + + +ZRR = zapi_responses({ + 'cluster_peer_info_source': build_zapi_response(update_cluster_peer_info_zapi('cluster1', '1.2.3.6,1.2.3.7')), + 'cluster_peer_info_remote': build_zapi_response(update_cluster_peer_info_zapi('cluster2', '1.2.3.4,1.2.3.5')) +}) + + +DEFAULT_ARGS_ZAPI = { + 'source_intercluster_lifs': '1.2.3.4,1.2.3.5', + 'dest_intercluster_lifs': '1.2.3.6,1.2.3.7', + 'passphrase': 'netapp123', + 'dest_hostname': '10.20.30.40', + 'dest_cluster_name': 'cluster2', + 'encryption_protocol_proposed': 'none', + 'ipspace': 'Default', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'never', + 'feature_flags': {'no_cserver_ems': True} +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('cluster-peer-get-iter', ZRR['empty']), + ('cluster-peer-get-iter', ZRR['empty']), + ('cluster-peer-create', ZRR['empty']), + ('cluster-peer-create', ZRR['empty']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS_ZAPI) + + +def test_create_idempotency(): + ''' Test create idempotency ''' + register_responses([ + ('cluster-peer-get-iter', ZRR['cluster_peer_info_source']), + ('cluster-peer-get-iter', ZRR['cluster_peer_info_remote']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS_ZAPI) + + +def test_successful_delete(): + ''' Test delete existing cluster peer ''' + module_args = { + 'state': 'absent', + 'source_cluster_name': 'cluster1' + } + register_responses([ + ('cluster-peer-get-iter', ZRR['cluster_peer_info_source']), + ('cluster-peer-get-iter', ZRR['cluster_peer_info_remote']), + ('cluster-peer-delete', ZRR['empty']), + ('cluster-peer-delete', ZRR['empty']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS_ZAPI, module_args) + + +def test_delete_idempotency(): + ''' Test delete idempotency ''' + module_args = { + 'state': 'absent', + 'source_cluster_name': 'cluster1' + } + register_responses([ + ('cluster-peer-get-iter', ZRR['empty']), + ('cluster-peer-get-iter', ZRR['empty']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS_ZAPI, module_args) + + +def test_error_get_cluster_peer(): + ''' Test get error ''' + register_responses([ + ('cluster-peer-get-iter', ZRR['error']), + ]) + error = create_and_apply(my_module, DEFAULT_ARGS_ZAPI, fail=True)['msg'] + assert 'Error fetching cluster peer source: NetApp API failed. Reason - 12345:synthetic error for UT purpose' == error + + +def test_error_delete_cluster_peer(): + ''' Test delete error ''' + module_args = { + 'state': 'absent', + 'source_cluster_name': 'cluster1' + } + register_responses([ + ('cluster-peer-get-iter', ZRR['cluster_peer_info_source']), + ('cluster-peer-get-iter', ZRR['cluster_peer_info_remote']), + ('cluster-peer-delete', ZRR['error']) + ]) + error = create_and_apply(my_module, DEFAULT_ARGS_ZAPI, module_args, fail=True)['msg'] + assert 'Error deleting cluster peer cluster2: NetApp API failed. Reason - 12345:synthetic error for UT purpose' == error + + +def test_error_create_cluster_peer(): + ''' Test create error ''' + register_responses([ + ('cluster-peer-get-iter', ZRR['empty']), + ('cluster-peer-get-iter', ZRR['empty']), + ('cluster-peer-create', ZRR['error']) + ]) + error = create_and_apply(my_module, DEFAULT_ARGS_ZAPI, fail=True)['msg'] + assert 'Error creating cluster peer [\'1.2.3.6\', \'1.2.3.7\']: NetApp API failed. Reason - 12345:synthetic error for UT purpose' == error + + +SRR = rest_responses({ + 'cluster_peer_dst': (200, {"records": [ + { + "uuid": "1e698aba-2aa6-11ec-b7be-005056b366e1", + "name": "mohan9cluster2", + "remote": { + "name": "mohan9cluster2", + "serial_number": "1-80-000011", + "ip_addresses": ["10.193.179.180"] + } + } + ], "num_records": 1}, None), + 'cluster_peer_src': (200, {"records": [ + { + "uuid": "1fg98aba-2aa6-11ec-b7be-005fgvb366e1", + "name": "mohanontap98cluster", + "remote": { + "name": "mohanontap98cluster", + "serial_number": "1-80-000031", + "ip_addresses": ["10.193.179.57"] + } + } + ], "num_records": 1}, None), + 'passphrase_response': (200, {"records": [ + { + "uuid": "4b71a7fb-45ff-11ec-95ea-005056b3b297", + "name": "", + "authentication": { + "passphrase": "ajdHOvAFSs0LOO0S27GtJZfV", + "expiry_time": "2022-02-22T22:30:18-05:00" + } + } + ], "num_records": 1}, None) +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'source_cluster_name': 'mohan9cluster2', + 'source_intercluster_lifs': ['10.193.179.180'], + 'dest_hostname': '10.193.179.197', + 'dest_cluster_name': 'mohanontap98cluster', + 'dest_intercluster_lifs': ['10.193.179.57'], + 'passphrase': 'ontapcluster_peer', + 'encryption_protocol_proposed': 'none', + 'ipspace': 'Default' +} + + +def test_successful_create_rest(): + ''' Test successful create ''' + args = DEFAULT_ARGS + del args['encryption_protocol_proposed'] + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['empty_records']), + ('GET', 'cluster/peers', SRR['empty_records']), + ('POST', 'cluster/peers', SRR['empty_good']), + ('POST', 'cluster/peers', SRR['empty_good']) + ]) + assert create_and_apply(my_module, args) + + +def test_create_idempotency_rest(): + ''' Test successful create idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['cluster_peer_src']), + ('GET', 'cluster/peers', SRR['cluster_peer_dst']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS) + + +def test_successful_create_without_passphrase_rest(): + ''' Test successful create ''' + args = DEFAULT_ARGS + del args['passphrase'] + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['empty_records']), + ('GET', 'cluster/peers', SRR['empty_records']), + ('POST', 'cluster/peers', SRR['passphrase_response']), + ('POST', 'cluster/peers', SRR['empty_good']) + ]) + assert create_and_apply(my_module, args) + + +def test_successful_delete_rest(): + ''' Test successful delete ''' + module_args = {'state': 'absent'} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['cluster_peer_src']), + ('GET', 'cluster/peers', SRR['cluster_peer_dst']), + ('DELETE', 'cluster/peers/1fg98aba-2aa6-11ec-b7be-005fgvb366e1', SRR['empty_good']), + ('DELETE', 'cluster/peers/1e698aba-2aa6-11ec-b7be-005056b366e1', SRR['empty_good']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, module_args) + + +def test_delete_idempotency_rest(): + ''' Test delete idempotency ''' + module_args = {'state': 'absent'} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['empty_records']), + ('GET', 'cluster/peers', SRR['empty_records']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, module_args) + + +def test_error_get_cluster_peer_rest(): + ''' Test get error ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['generic_error']), + ]) + error = create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] + assert 'calling: cluster/peers: got Expected error.' == error + + +def test_error_delete_cluster_peer_rest(): + ''' Test delete error ''' + module_args = {'state': 'absent'} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['cluster_peer_src']), + ('GET', 'cluster/peers', SRR['cluster_peer_dst']), + ('DELETE', 'cluster/peers/1fg98aba-2aa6-11ec-b7be-005fgvb366e1', SRR['generic_error']), + ]) + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'calling: cluster/peers/1fg98aba-2aa6-11ec-b7be-005fgvb366e1: got Expected error.' == error + + +def test_error_create_cluster_peer_rest(): + ''' Test create error ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster/peers', SRR['empty_records']), + ('GET', 'cluster/peers', SRR['empty_records']), + ('POST', 'cluster/peers', SRR['generic_error']), + ]) + error = create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] + assert 'calling: cluster/peers: got Expected error.' == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_command.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_command.py new file mode 100644 index 000000000..38bc6ec96 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_command.py @@ -0,0 +1,246 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP Command Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_module, expect_and_capture_ansible_exception, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_command import NetAppONTAPCommand as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'use_rest', +} + + +def cli_output(priv, result, translate=True): + prefix = 'NetApp Release' + print('HERE', 'start') + if priv == 'advanced': + prefix = '\n' + prefix + if result == "u'77'": + result = u'77' + elif result == "b'77'": + print('HERE', b'77') + result = b'77' + elif result is None: + result = b'7' + return { + 'cli-output': prefix, + 'cli-result-value': result + } + + +def build_zapi_response_raw(contents): + """ when testing special encodings, we cannot use build_zapi_response as translate_struct converts to text + """ + if netapp_utils.has_netapp_lib(): + xml = netapp_utils.zapi.NaElement('results') + xml.add_attr('status', 'status_ok') + xml.add_new_child('cli-output', contents['cli-output']) + xml.add_new_child('cli-result-value', contents['cli-result-value']) + # print('XML ut:', xml.to_string()) + xml.add_attr('status', 'passed') + return (xml, 'valid') + return ('netapp-lib is required', 'invalid') + + +ZRR = zapi_responses({ + 'cli_version': build_zapi_response_raw(cli_output(None, None)), + 'cli_version_advanced': build_zapi_response_raw(cli_output('advanced', None)), + 'cli_version_77': build_zapi_response(cli_output(None, '77')), + 'cli_version_b77': build_zapi_response_raw(cli_output(None, "b'77'")), + 'cli_version_u77': build_zapi_response_raw(cli_output(None, "u'77'")), +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + } + error = 'missing required arguments: command' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_default_priv(): + ''' make sure privilege is not required ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['cli_version']), + ]) + module_args = { + 'command': 'version', + } + msg = call_main(my_main, DEFAULT_ARGS, module_args)['msg'] + needle = b'<cli-output>NetApp Release' + assert needle in msg + print('Version (raw): %s' % msg) + + +def test_admin_priv(): + ''' make sure admin is accepted ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['cli_version']), + ]) + module_args = { + 'command': 'version', + 'privilege': 'admin', + } + msg = call_main(my_main, DEFAULT_ARGS, module_args)['msg'] + needle = b'<cli-output>NetApp Release' + assert needle in msg + print('Version (raw): %s' % msg) + + +def test_advanced_priv(): + ''' make sure advanced is not required ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['cli_version_advanced']), + ]) + module_args = { + 'command': 'version', + 'privilege': 'advanced', + } + msg = call_main(my_main, DEFAULT_ARGS, module_args)['msg'] + # Interestingly, the ZAPI returns a slightly different response + needle = b'<cli-output>\nNetApp Release' + assert needle in msg + print('Version (raw): %s' % msg) + + +def get_dict_output(extra_args=None): + ''' get result value after calling command module ''' + module_args = { + 'command': 'version', + 'return_dict': 'true', + } + if extra_args: + module_args.update(extra_args) + dict_output = call_main(my_main, DEFAULT_ARGS, module_args)['msg'] + print('dict_output: %s' % repr(dict_output)) + return dict_output + + +def test_dict_output_77(): + ''' make sure correct value is returned ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['cli_version_77']), + ]) + result = '77' + assert get_dict_output()['result_value'] == int(result) + + +def test_dict_output_b77(): + ''' make sure correct value is returned ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['cli_version_b77']), + ]) + result = b'77' + assert get_dict_output()['result_value'] == int(result) + + +def test_dict_output_u77(): + ''' make sure correct value is returned ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['cli_version_u77']), + ]) + result = "u'77'" + assert get_dict_output()['result_value'] == int(eval(result)) + + +def test_dict_output_exclude(): + ''' make sure correct value is returned ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['cli_version']), + ('ZAPI', 'system-cli', ZRR['cli_version']), + ]) + dict_output = get_dict_output({'exclude_lines': 'NetApp Release'}) + assert len(dict_output['stdout_lines']) == 1 + assert len(dict_output['stdout_lines_filter']) == 0 + dict_output = get_dict_output({'exclude_lines': 'whatever'}) + assert len(dict_output['stdout_lines']) == 1 + assert len(dict_output['stdout_lines_filter']) == 1 + + +def test_dict_output_include(): + ''' make sure correct value is returned ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['cli_version']), + ('ZAPI', 'system-cli', ZRR['cli_version']), + ]) + dict_output = get_dict_output({'include_lines': 'NetApp Release'}) + assert len(dict_output['stdout_lines']) == 1 + assert len(dict_output['stdout_lines_filter']) == 1 + dict_output = get_dict_output({'include_lines': 'whatever'}) + assert len(dict_output['stdout_lines']) == 1 + assert len(dict_output['stdout_lines_filter']) == 0 + + +def test_check_mode(): + ''' make sure nothing is done ''' + register_responses([ + ]) + module_args = { + 'command': 'version', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.module.check_mode = True + msg = expect_and_capture_ansible_exception(my_obj.apply, 'exit')['msg'] + needle = "Would run command: '['version']'" + assert needle in msg + print('Version (raw): %s' % msg) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_missing_netapp_lib(mock_has_netapp_lib): + module_args = { + 'command': 'version', + } + mock_has_netapp_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_zapi_errors(): + ''' make sure nothing is done ''' + register_responses([ + ('ZAPI', 'system-cli', ZRR['error']), + ('ZAPI', 'system-cli', ZRR['cli_version']), + ('ZAPI', 'system-cli', ZRR['cli_version']), + ('ZAPI', 'system-cli', ZRR['cli_version']), + + ]) + module_args = { + 'command': 'version', + } + error = zapi_error_message("Error running command ['version']") + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + # EMS error is ignored + assert b'NetApp Release' in call_main(my_main, DEFAULT_ARGS, module_args, fail=False)['msg'] + # EMS cserver error is ignored + assert b'NetApp Release' in call_main(my_main, DEFAULT_ARGS, module_args, fail=False)['msg'] + # EMS vserver error is ignored + module_args = { + 'command': 'version', + 'vserver': 'svm' + } + assert b'NetApp Release' in call_main(my_main, DEFAULT_ARGS, module_args, fail=False)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_debug.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_debug.py new file mode 100644 index 000000000..55d6f2727 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_debug.py @@ -0,0 +1,344 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP debug Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import copy +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_debug \ + import NetAppONTAPDebug as my_module # module under test +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + assert_no_warnings, assert_no_warnings_except_zapi, call_main, create_and_apply, create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, zapi_responses + +# not available on 2.6 anymore +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +DEFAULT_ARGS = { + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'vserver': 'vserver', +} + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy_9_8_0')), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'one_vserver_record_with_intf': (200, { + "records": [{ + 'name': 'vserver1', + 'ip_interfaces': [ + dict(services=['management'])], + }], + 'num_records': 1 + }, None), + 'one_user_record': (200, { + "records": [{ + 'name': 'user1', + 'applications': [ + dict(application='http'), + dict(application='ontapi'), + ], + 'locked': False, + 'owner': {'name': 'vserver'} + }], + 'num_records': 1 + }, None), + 'one_user_record_admin': (200, { + "records": [{ + 'name': 'user1', + 'applications': [ + dict(application='http'), + dict(application='ontapi'), + ], + 'locked': False, + 'owner': {'name': 'vserver'}, + 'role': {'name': 'admin'} + }], + 'num_records': 1 + }, None), + 'ConnectTimeoutError': (400, None, "Connection timed out"), + 'Name or service not known': (400, None, "Name or service not known"), + 'not_authorized': (400, None, "not authorized for that command"), +}, allow_override=False) + +ZRR = zapi_responses({ + 'ConnectTimeoutError': build_zapi_error('123', 'ConnectTimeoutError'), + 'Name or service not known': build_zapi_error('123', 'Name or service not known'), +}, allow_override=False) + + +if netapp_utils.has_netapp_lib(): + REST_ZAPI_FLOW = [ + ('system-get-version', ZRR['version']), # get version + ('GET', 'cluster', SRR['is_rest_9_8']), # get_version + ] +else: + REST_ZAPI_FLOW = [ + ('GET', 'cluster', SRR['is_rest_9_8']), # get_version + ] + + +def test_success_no_vserver(): + ''' test get''' + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record_admin']) # get user + ]) + args = dict(DEFAULT_ARGS) + args.pop('vserver') + results = create_and_apply(my_module, args) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' in results + assert 'msg' in results + assert "NOTE: application console not found for user: user1: ['http', 'ontapi']" in results['notes'] + assert 'ZAPI connected successfully.' in results['msg'] + + +def test_success_with_vserver(): + ''' test get''' + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record']), # get user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_svms + ('GET', 'security/accounts', SRR['one_user_record']) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS) + print('Info: %s' % results) + # assert results['changed'] is False + assert_no_warnings_except_zapi() + assert 'notes' not in results + + +def test_fail_with_vserver_locked(): + ''' test get''' + user = copy.deepcopy(SRR['one_user_record']) + user[1]['records'][0]['locked'] = True + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record']), # get user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_svms + ('GET', 'security/accounts', user) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' in results + assert 'user: user1 is locked on vserver: vserver' in results['notes'][0] + + +def test_fail_with_vserver_missing_app(): + ''' test get''' + user = copy.deepcopy(SRR['one_user_record']) + user[1]['records'][0]['applications'] = [dict(application='http')] + + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record']), # get user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_svms + ('GET', 'security/accounts', user) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' in results + assert 'application ontapi not found for user: user1' in results['notes'][0] + assert 'Error: no unlocked user for ontapi on vserver: vserver' in results['msg'] + + +def test_fail_with_vserver_list_user_not_found(): + ''' test get''' + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record']), # get user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_svms + ('GET', 'security/accounts', SRR['empty_records']) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'Error getting accounts for: vserver: none found' in results['msg'] + + +def test_fail_with_vserver_list_user_error_on_get_users(): + ''' test get''' + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record']), # get user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_svms + ('GET', 'security/accounts', SRR['generic_error']) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'Error getting accounts for: vserver: calling: security/accounts: got Expected error.' in results['msg'] + + +def test_success_with_vserver_list_user_not_authorized(): + ''' test get''' + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record']), # get user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_svms + ('GET', 'security/accounts', SRR['not_authorized']) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'Not autorized to get accounts for: vserver: calling: security/accounts: got not authorized for that command.' in results['msg'] + + +def test_fail_with_vserver_no_interface(): + ''' test get''' + vserver = copy.deepcopy(SRR['one_vserver_record_with_intf']) + vserver[1]['records'][0].pop('ip_interfaces') + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record_admin']), # get user + ('GET', 'svm/svms', vserver), # get_svms + ('GET', 'security/accounts', SRR['one_user_record']) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' in results + assert "NOTE: application console not found for user: user1: ['http', 'ontapi']" in results['notes'] + assert 'Error vserver is not associated with a network interface: vserver' in results['msg'] + + +def test_fail_with_vserver_not_found(): + ''' test get''' + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record_admin']), # get user + ('GET', 'svm/svms', SRR['empty_records']), # get_svms + ('GET', 'security/accounts', SRR['one_user_record']) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' in results + assert "NOTE: application console not found for user: user1: ['http', 'ontapi']" in results['notes'] + assert 'Error getting vserver in list_interfaces: vserver: not found' in results['msg'] + + +def test_fail_with_vserver_error_on_get_svms(): + ''' test get''' + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record_admin']), # get user + ('GET', 'svm/svms', SRR['generic_error']), # get_svms + ('GET', 'security/accounts', SRR['one_user_record']) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' in results + assert "NOTE: application console not found for user: user1: ['http', 'ontapi']" in results['notes'] + assert 'Error getting vserver in list_interfaces: vserver: calling: svm/svms: got Expected error.' in results['msg'] + + +def test_note_with_vserver_no_management_service(): + ''' test get''' + vserver = copy.deepcopy(SRR['one_vserver_record_with_intf']) + vserver[1]['records'][0]['ip_interfaces'][0]['services'] = ['data_core'] + register_responses(REST_ZAPI_FLOW + [ + ('GET', 'security/accounts', SRR['one_user_record_admin']), # get user + ('GET', 'svm/svms', vserver), # get_svms + ('GET', 'security/accounts', SRR['one_user_record']) # get_users + ]) + + results = create_and_apply(my_module, DEFAULT_ARGS) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' in results + assert 'no management policy in services' in results['notes'][2] + + +def test_fail_zapi_error(): + ''' test get''' + register_responses([ + ('system-get-version', ZRR['error']), + ('GET', 'cluster', SRR['is_rest_9_8']), # get_version + ('GET', 'security/accounts', SRR['one_user_record']), # get_user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_vservers + ('GET', 'security/accounts', SRR['one_user_record']), # get_users + ('system-get-version', ZRR['ConnectTimeoutError']), + ('GET', 'cluster', SRR['is_rest_9_8']), # get_version + ('GET', 'security/accounts', SRR['one_user_record']), # get_user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_vservers + ('GET', 'security/accounts', SRR['one_user_record']), # get_users + ('system-get-version', ZRR['Name or service not known']), + ('GET', 'cluster', SRR['is_rest_9_8']), # get_version + ('GET', 'security/accounts', SRR['one_user_record']), # get_user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_vservers + ('GET', 'security/accounts', SRR['one_user_record']) # get_users + ]) + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' not in results + assert 'Unclassified, see msg' in results['msg'] + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + assert 'Error in hostname - Address does not exist or is not reachable: NetApp API failed. Reason - 123:ConnectTimeoutError' in results['msg'] + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + assert 'Error in hostname - DNS name cannot be resolved: NetApp API failed. Reason - 123:Name or service not known' in results['msg'] + + +def test_fail_rest_error(): + ''' test get''' + register_responses([ + ('system-get-version', ZRR['version']), + ('GET', 'cluster', SRR['is_zapi']), # get_version + ('system-get-version', ZRR['version']), + ('GET', 'cluster', SRR['ConnectTimeoutError']), # get_version + ('system-get-version', ZRR['version']), + ('GET', 'cluster', SRR['Name or service not known']), # get_version + ]) + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' not in results + assert 'Other error for hostname: 10.10.10.10 using REST: Unreachable.' in results['msg'] + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + assert 'Error in hostname - Address does not exist or is not reachable: Connection timed out' in results['msg'] + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + assert 'Error in hostname - DNS name cannot be resolved: Name or service not known' in results['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + ''' test get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8']), # get_version + ('GET', 'security/accounts', SRR['one_user_record']), # get_user + ('GET', 'svm/svms', SRR['one_vserver_record_with_intf']), # get_vservers + ('GET', 'security/accounts', SRR['one_user_record']) # get_users + ]) + + mock_has_netapp_lib.return_value = False + + results = create_and_apply(my_module, DEFAULT_ARGS, fail=True) + print('Info: %s' % results) + assert_no_warnings_except_zapi() + assert 'notes' not in results + assert 'Install the python netapp-lib module or a missing dependency' in results['msg'][0] + + +def test_check_connection_internal_error(): + ''' expecting REST or ZAPI ''' + error = 'Internal error, unexpected connection type: rest' + assert error == expect_and_capture_ansible_exception(create_module(my_module, DEFAULT_ARGS).check_connection, 'fail', 'rest')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_disk_options.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_disk_options.py new file mode 100644 index 000000000..d729b4edd --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_disk_options.py @@ -0,0 +1,151 @@ +# (c) 2021-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP fpolicy ext engine Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + call_main, create_and_apply, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_disk_options \ + import NetAppOntapDiskOptions as my_module, main as my_main # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'node': 'node1', + 'bkg_firmware_update': False, + 'autocopy': False, + 'autoassign': False, + 'autoassign_policy': 'default', + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' +} + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'one_disk_options_record': (200, { + "records": [{ + 'node': 'node1', + 'bkg_firmware_update': False, + 'autocopy': False, + 'autoassign': False, + 'autoassign_policy': 'default' + }] + }, None), + 'one_disk_options_record_on_off': (200, { + "records": [{ + 'node': 'node1', + 'bkg_firmware_update': 'on', + 'autocopy': 'off', + 'autoassign': 'on', + 'autoassign_policy': 'default' + }] + }, None), + 'one_disk_options_record_bad_value': (200, { + "records": [{ + 'node': 'node1', + 'bkg_firmware_update': 'whatisthis', + 'autocopy': 'off', + 'autoassign': 'on', + 'autoassign_policy': 'default' + }] + }, None) + +}, False) + + +def test_rest_modify_no_action(): + ''' modify fpolicy ext engine ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/storage/disk/option', SRR['one_disk_options_record']), + ]) + assert not create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_rest_modify_prepopulate(): + ''' modify disk options ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/storage/disk/option', SRR['one_disk_options_record']), + ('PATCH', 'private/cli/storage/disk/option', SRR['empty_good']), + ]) + args = {'autoassign': True, 'autocopy': True, 'bkg_firmware_update': True} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_rest_modify_on_off(): + ''' modify disk options ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/storage/disk/option', SRR['one_disk_options_record_on_off']), + ('PATCH', 'private/cli/storage/disk/option', SRR['empty_good']), + ]) + args = {'autoassign': True, 'autocopy': True, 'bkg_firmware_update': True} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_error_rest_get_not_on_off(): + ''' modify disk options ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/storage/disk/option', SRR['one_disk_options_record_bad_value']), + ]) + args = {'autoassign': True, 'autocopy': True, 'bkg_firmware_update': True} + assert create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] == 'Unexpected value for field bkg_firmware_update: whatisthis' + + +def test_error_rest_no_zapi_support(): + ''' modify disk options ''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ]) + args = {'use_rest': 'auto'} + assert "na_ontap_disk_options only supports REST, and requires ONTAP 9.6 or later." in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_error_get(): + ''' get disk options ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/storage/disk/option', SRR['generic_error']), + ]) + args = {'use_rest': 'auto'} + assert "calling: private/cli/storage/disk/option: got Expected error." in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_error_get_empty(): + ''' get disk options ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/storage/disk/option', SRR['empty_records']), + ]) + args = {'use_rest': 'auto'} + assert "Error on GET private/cli/storage/disk/option, no record." == call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_error_patch(): + ''' modify disk options ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/storage/disk/option', SRR['one_disk_options_record_on_off']), + ('PATCH', 'private/cli/storage/disk/option', SRR['generic_error']), + ]) + args = {'use_rest': 'auto'} + assert "calling: private/cli/storage/disk/option: got Expected error." in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_disks.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_disks.py new file mode 100644 index 000000000..b59ae7c83 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_disks.py @@ -0,0 +1,822 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP disks Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_disks \ + import NetAppOntapDisks as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + + try: + container_type = self.xml_in['query']['storage-disk-info']['disk-raid-info']['container-type'] + except LookupError: + container_type = None + try: + get_owned_disks = self.xml_in['query']['storage-disk-info']['disk-ownership-info']['home-node-name'] + except LookupError: + get_owned_disks = None + + api_call = self.xml_in.get_name() + + if self.type == 'fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + elif api_call == 'storage-disk-get-iter': + if container_type == 'spare': + xml = self.home_spare_disks() + elif get_owned_disks: + xml = self.owned_disks() + else: + xml = self.partner_spare_disks() + elif api_call == 'cf-status': + xml = self.partner_node_name() + self.xml_out = xml + return xml + + @staticmethod + def owned_disks(): + ''' build xml data for disk-inventory-info owned disks ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': [ + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.8' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.7' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.10' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.25' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.18' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.0' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.6' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.11' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.12' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.13' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.23' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.4' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.9' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.21' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.16' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.19' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.2' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.14' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.20' + } + } + } + ], + 'num-records': '19' + } + xml.translate_struct(data) + return xml + + @staticmethod + def home_spare_disks(): + ''' build xml data for disk-inventory-info home spare disks ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': [ + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.9' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.20' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.9' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.22' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.13' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.23' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.16' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.18' + } + } + } + ], + 'num-records': '8' + } + xml.translate_struct(data) + return xml + + @staticmethod + def partner_spare_disks(): + ''' build xml data for disk-inventory-info partner spare disks ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': [ + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.7' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.15' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.21' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.23' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.19' + } + } + }, + { + 'storage-disk-info': { + 'disk-inventory-info': { + 'disk-cluster-name': '1.0.11' + } + } + } + ], + 'num-records': '6' + } + xml.translate_struct(data) + return xml + + @staticmethod + def partner_node_name(): + ''' build xml data for partner node name''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'cf-status': { + 'partner-name': 'node2' + } + } + xml.translate_struct(data) + return xml + + @staticmethod + def unassigned_disk_count(): + ''' build xml data for the count of unassigned disks on a node ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'num-records': '0' + } + xml.translate_struct(data) + return xml + + +def default_args(): + args = { + 'disk_count': 15, + 'node': 'node1', + 'disk_type': 'SAS', + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'owned_disk_record': ( + 200, { + 'records': [ + { + "name": "1.0.8", + "type": "sas", + "container_type": "aggregate", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.7", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.10", + "type": "sas", + "container_type": "aggregate", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.18", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.0", + "type": "sas", + "container_type": "aggregate", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.6", + "type": "sas", + "container_type": "aggregate", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.11", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.12", + "type": "sas", + "container_type": "aggregate", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.13", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.23", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.22", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.4", + "type": "sas", + "container_type": "aggregate", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.9", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.21", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.16", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.19", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.2", + "type": "sas", + "container_type": "aggregate", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.14", + "type": "sas", + "container_type": "aggregate", + "home_node": { + "name": "node1" + } + }, + { + "name": "1.0.20", + "type": "sas", + "container_type": "spare", + "home_node": { + "name": "node1" + } + } + ], + 'num_records': 19}, + None), + + # 'owned_disk_record': (200, {'num_records': 15}), + 'unassigned_disk_record': ( + 200, { + 'records': [], + 'num_records': 0}, + None), + 'home_spare_disk_info_record': ( + 200, {'records': [ + { + 'name': '1.0.20', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node1'}}, + { + 'name': '1.0.9', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node1'}}, + { + 'name': '1.0.22', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node1'}}, + { + 'name': '1.0.13', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node1'}}, + { + 'name': '1.0.17', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node1'}}, + { + 'name': '1.0.23', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node1'}}, + { + 'name': '1.0.16', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node1'}}, + { + 'name': '1.0.18', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node1'}} + ], + 'num_records': 8, + '_links': {'self': {'href': '/api/storage/disks?home_node.name=node1&container_type=spare&type=SAS&fields=name'}}}, + None), + + 'partner_node_name_record': ( + 200, {'records': [ + { + 'uuid': 'c345c182-a6a0-11eb-af7b-00a0984839de', + 'name': 'node2', + 'ha': { + 'partners': [ + {'name': 'node1'} + ] + } + } + ], + 'num_records': 1}, + None), + + 'partner_spare_disk_info_record': ( + 200, {'records': [ + { + 'name': '1.0.7', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + }, + { + 'name': '1.0.15', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + }, + { + 'name': '1.0.21', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + }, + { + 'name': '1.0.23', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + }, + { + 'name': '1.0.19', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + }, + { + 'name': '1.0.11', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + } + ], + 'num_records': 6}, + None) +} + + +def test_successful_assign(patch_ansible): + ''' successful assign and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['disk_count'] = '20' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection() + my_obj.ems_log_event = Mock(return_value=None) + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + # mock_create.assert_called_with() + args['use_rest'] = 'never' + args['disk_count'] = '19' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection() + my_obj.ems_log_event = Mock(return_value=None) + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +def test_successful_unassign(patch_ansible): + ''' successful assign and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['disk_count'] = '17' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection() + my_obj.ems_log_event = Mock(return_value=None) + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + # mock_create.assert_called_with() + args['use_rest'] = 'never' + args['disk_count'] = '19' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection() + my_obj.ems_log_event = Mock(return_value=None) + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_ensure_get_called(patch_ansible): + ''' test get_disks ''' + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + print('starting') + my_obj = my_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = MockONTAPConnection() + assert my_obj.get_disks is not None + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument ##WHAT DOES THIS METHOD DO + ''' create scope ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +def test_if_all_methods_catch_exception(patch_ansible): + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.get_disks(container_type='owned', node='node1') + assert 'Error getting disk ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.get_disks(container_type='unassigned') + assert 'Error getting disk ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.get_disks(container_type='spare', node='node1') + assert 'Error getting disk ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.get_partner_node_name() + assert 'Error getting partner name ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.disk_assign(needed_disks=2) + assert 'Error assigning disks ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.disk_unassign(['1.0.0', '1.0.1']) + assert 'Error unassigning disks ' in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_assign(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['owned_disk_record'], + SRR['unassigned_disk_record'], + SRR['home_spare_disk_info_record'], + SRR['partner_node_name_record'], + SRR['partner_spare_disk_info_record'], + SRR['empty_good'], # unassign + SRR['empty_good'], # assign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 8 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_unassign(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['disk_count'] = 17 + print(args) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['owned_disk_record'], + SRR['unassigned_disk_record'], + SRR['home_spare_disk_info_record'], + SRR['partner_node_name_record'], + SRR['partner_spare_disk_info_record'], + SRR['empty_good'], # unassign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 6 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' disk_count matches arguments, do nothing ''' + args = dict(default_args()) + args['disk_count'] = 19 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['owned_disk_record'], + SRR['unassigned_disk_record'], + SRR['home_spare_disk_info_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 4 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_dns.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_dns.py new file mode 100644 index 000000000..c592f5c88 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_dns.py @@ -0,0 +1,388 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_dns''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import call_main, create_module, expect_and_capture_ansible_exception,\ + patch_ansible, assert_warning_was_raised, print_warnings + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_dns import main as my_main, NetAppOntapDns as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'dns_record': (200, {"records": [{"domains": ['test.com'], + "servers": ['0.0.0.0'], + "svm": {"name": "svm1", "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7"}}]}, None), + 'cluster_data': (200, {"dns_domains": ['test.com'], + "name_servers": ['0.0.0.0'], + "name": "cserver", + "uuid": "C2c9e252-41be-11e9-81d5-00a0986138f7"}, None), + 'cluster_name': (200, {"name": "cserver", + "uuid": "C2c9e252-41be-11e9-81d5-00a0986138f7"}, None), +}) + +dns_info = { + 'attributes': { + 'net-dns-info': { + 'name-servers': [{'ip-address': '0.0.0.0'}], + 'domains': [{'string': 'test.com'}], + 'skip-config-validation': 'true' + } + } +} + + +ZRR = zapi_responses({ + 'dns_info': build_zapi_response(dns_info), + 'error_15661': build_zapi_error(15661, 'not_found'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'nameservers': ['0.0.0.0'], + 'domains': ['test.com'], +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + } + error = 'Error: vserver is a required parameter with ZAPI.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_zapi_get_error(): + register_responses([ + ('ZAPI', 'net-dns-get', ZRR['error']), + ('ZAPI', 'net-dns-get', ZRR['error_15661']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'svm_abc', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + # get + error = zapi_error_message('Error getting DNS info') + assert error in expect_and_capture_ansible_exception(my_obj.get_dns, 'fail')['msg'] + assert my_obj.get_dns() is None + + +def test_idempotent_modify_dns(): + register_responses([ + ('ZAPI', 'net-dns-get', ZRR['dns_info']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'svm_abc', + } + + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_zapi_modify_dns(): + register_responses([ + ('ZAPI', 'net-dns-get', ZRR['dns_info']), + ('ZAPI', 'net-dns-modify', ZRR['success']), + # idempotency + ('ZAPI', 'net-dns-get', ZRR['dns_info']), + # error + ('ZAPI', 'net-dns-get', ZRR['dns_info']), + ('ZAPI', 'net-dns-modify', ZRR['error']), + ]) + module_args = { + 'domains': ['new_test.com'], + 'nameservers': ['1.2.3.4'], + 'skip_validation': True, + 'use_rest': 'never', + 'vserver': 'svm_abc', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args = { + 'domains': ['test.com'], + 'skip_validation': True, + 'use_rest': 'never', + 'vserver': 'svm_abc', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args = { + 'domains': ['new_test.com'], + 'nameservers': ['1.2.3.4'], + 'skip_validation': True, + 'use_rest': 'never', + 'vserver': 'svm_abc', + } + error = zapi_error_message('Error modifying dns') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_zapi_create_dns(): + register_responses([ + ('ZAPI', 'net-dns-get', ZRR['empty']), + ('ZAPI', 'net-dns-create', ZRR['success']), + # idempotency + ('ZAPI', 'net-dns-get', ZRR['dns_info']), + # error + ('ZAPI', 'net-dns-get', ZRR['empty']), + ('ZAPI', 'net-dns-create', ZRR['error']), + ]) + module_args = { + 'domains': ['test.com'], + 'skip_validation': True, + 'use_rest': 'never', + 'vserver': 'svm_abc', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + error = zapi_error_message('Error creating dns') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_zapi_delete_dns(): + register_responses([ + ('ZAPI', 'net-dns-get', ZRR['dns_info']), + ('ZAPI', 'net-dns-destroy', ZRR['success']), + # idempotency + ('ZAPI', 'net-dns-get', ZRR['empty']), + # error + ('ZAPI', 'net-dns-get', ZRR['dns_info']), + ('ZAPI', 'net-dns-destroy', ZRR['error']), + ]) + module_args = { + 'domains': ['new_test.com'], + 'state': 'absent', + 'use_rest': 'never', + 'vserver': 'svm_abc', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + error = zapi_error_message('Error destroying dns') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_error(): + module_args = { + 'use_rest': 'always', + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['generic_error']), + ('GET', 'cluster', SRR['is_rest_9_9_1']), + # create + ('PATCH', 'cluster', SRR['generic_error']), + ('PATCH', 'cluster', SRR['generic_error']), + ('POST', 'name-services/dns', SRR['generic_error']), + # delete + ('DELETE', 'name-services/dns/uuid', SRR['generic_error']), + # read + ('GET', 'name-services/dns', SRR['generic_error']), + # modify + ('PATCH', 'cluster', SRR['generic_error']), + ('PATCH', 'name-services/dns/uuid', SRR['generic_error']), + ]) + error = rest_error_message('Error getting cluster info', 'cluster') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + # create + my_obj.is_cluster = True + error = rest_error_message('Error updating cluster DNS options', 'cluster') + assert error in expect_and_capture_ansible_exception(my_obj.create_dns_rest, 'fail')['msg'] + my_obj.is_cluster = False + # still cluster scope, as verserver is not set + assert error in expect_and_capture_ansible_exception(my_obj.create_dns_rest, 'fail')['msg'] + my_obj.parameters['vserver'] = 'vserver' + error = rest_error_message('Error creating DNS service', 'name-services/dns') + assert error in expect_and_capture_ansible_exception(my_obj.create_dns_rest, 'fail')['msg'] + # delete + my_obj.is_cluster = True + error = 'Error: cluster scope when deleting DNS with REST requires ONTAP 9.9.1 or later.' + assert error in expect_and_capture_ansible_exception(my_obj.destroy_dns_rest, 'fail', {})['msg'] + my_obj.is_cluster = False + error = rest_error_message('Error deleting DNS service', 'name-services/dns/uuid') + assert error in expect_and_capture_ansible_exception(my_obj.destroy_dns_rest, 'fail', {'uuid': 'uuid'})['msg'] + # read, cluster scope + del my_obj.parameters['vserver'] + error = rest_error_message('Error getting DNS service', 'name-services/dns') + assert error in expect_and_capture_ansible_exception(my_obj.get_dns_rest, 'fail')['msg'] + # modify + dns_attrs = { + 'domains': [], + 'nameservers': [], + 'uuid': 'uuid', + } + my_obj.is_cluster = True + error = rest_error_message('Error updating cluster DNS options', 'cluster') + assert error in expect_and_capture_ansible_exception(my_obj.modify_dns_rest, 'fail', dns_attrs)['msg'] + my_obj.is_cluster = False + error = rest_error_message('Error modifying DNS configuration', 'name-services/dns/uuid') + assert error in expect_and_capture_ansible_exception(my_obj.modify_dns_rest, 'fail', dns_attrs)['msg'] + + +def test_rest_successfully_create(): + module_args = { + 'use_rest': 'always', + 'vserver': 'svm_abc', + 'skip_validation': True + } + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/dns', SRR['zero_records']), + ('POST', 'name-services/dns', SRR['success']), + ]) + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_create_is_cluster_vserver(): + module_args = { + 'use_rest': 'always', + 'vserver': 'cserver' + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'name-services/dns', SRR['zero_records']), + ('GET', 'cluster', SRR['cluster_name']), + ('PATCH', 'cluster', SRR['empty_good']), + ]) + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_idempotent_create_dns(): + module_args = { + 'use_rest': 'always', + 'vserver': 'svm_abc', + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'name-services/dns', SRR['dns_record']), + ]) + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_destroy(): + module_args = { + 'state': 'absent', + 'use_rest': 'always', + 'vserver': 'svm_abc', + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'name-services/dns', SRR['dns_record']), + ('DELETE', 'name-services/dns/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['success']), + ]) + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_idempotently_destroy(): + module_args = { + 'state': 'absent', + 'use_rest': 'always', + 'vserver': 'svm_abc', + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'name-services/dns', SRR['zero_records']), + ('GET', 'cluster', SRR['cluster_data']), + ]) + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_modify(): + module_args = { + 'domains': 'new_test.com', + 'state': 'present', + 'use_rest': 'always', + 'vserver': 'svm_abc' + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'name-services/dns', SRR['dns_record']), + ('PATCH', 'name-services/dns/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['success']), + ]) + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_modify_is_cluster_vserver(): + module_args = { + 'domains': 'new_test.com', + 'state': 'present', + 'use_rest': 'always', + 'vserver': 'cserver' + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'name-services/dns', SRR['zero_records']), + ('GET', 'cluster', SRR['cluster_data']), + ('PATCH', 'cluster', SRR['empty_good']), + ]) + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_idempotently_modify(): + module_args = { + 'state': 'present', + 'use_rest': 'always', + 'vserver': 'svm_abc', + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'name-services/dns', SRR['dns_record']), + ]) + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_modify_is_cluster_skip_validation(): + module_args = { + 'domains': 'new_test.com', + 'state': 'present', + 'use_rest': 'always', + 'skip_validation': True + } + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/dns', SRR['zero_records']), + ('PATCH', 'cluster', SRR['empty_good']), + # error if used skip_validation on earlier versions. + ('GET', 'cluster', SRR['is_rest']), + ]) + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_warning_was_raised("skip_validation is ignored for cluster DNS operations in REST.") + assert 'Error: Minimum version of ONTAP for skip_validation is (9, 9, 1)' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_has_netapp_lib(has_netapp_lib): + module_args = { + 'state': 'present', + 'use_rest': 'never', + 'vserver': 'svm_abc', + } + has_netapp_lib.return_value = False + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == 'Error: the python NetApp-Lib module is required. Import error: None' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_domain_tunnel.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_domain_tunnel.py new file mode 100644 index 000000000..eb08bf205 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_domain_tunnel.py @@ -0,0 +1,145 @@ +''' unit tests ONTAP Ansible module: na_ontap_domain_tunnel ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_domain_tunnel \ + import NetAppOntapDomainTunnel as domain_tunnel_module # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"), + 'generic_error': (400, None, {'message': "expected error", 'code': '5'}), + # module specific responses + 'domain_tunnel_record': (200, { + 'svm': { + 'name': 'ansible' + } + }, None) +} + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_domain_tunnel = { + "hostname": '10.10.10.10', + "username": 'username', + "password": 'password', + "vserver": 'ansible' + } + + def set_default_args(self): + return { + 'state': 'present', + 'hostname': self.mock_domain_tunnel['hostname'], + 'username': self.mock_domain_tunnel['username'], + 'password': self.mock_domain_tunnel['password'], + 'vserver': self.mock_domain_tunnel['vserver'] + } + + def get_domain_tunnel_mock_object(self): + domain_tunnel_obj = domain_tunnel_module() + return domain_tunnel_obj + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_domain_tunnel_mock_object().apply() + assert exc.value.args[0]['msg'] == SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_create_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_domain_tunnel_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_create_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['domain_tunnel_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_domain_tunnel_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_modify_rest(self, mock_request): + data = self.set_default_args() + data['vserver'] = ['ansible1'] + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['domain_tunnel_record'], # get + SRR['domain_tunnel_record'], # modify + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_domain_tunnel_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_delete_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['domain_tunnel_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_domain_tunnel_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_delete_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_domain_tunnel_mock_object().apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_efficiency_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_efficiency_policy.py new file mode 100644 index 000000000..05270aef1 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_efficiency_policy.py @@ -0,0 +1,422 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_vscan_scanner_pool ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_efficiency_policy \ + import NetAppOntapEfficiencyPolicy as efficiency_module # module under test +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'state': 'present', + 'vserver': 'svm3', + 'policy_name': 'test_policy', + 'comment': 'This policy is for x and y', + 'enabled': True, + 'qos_policy': 'background', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' +} + + +threshold_info = { + 'num-records': 1, + 'attributes-list': { + 'sis-policy-info': { + 'changelog-threshold-percent': 10, + 'comment': 'This policy is for x and y', + 'enabled': 'true', + 'policy-name': 'test_policy', + 'policy-type': 'threshold', + 'qos-policy': 'background', + 'vserver': 'svm3' + } + } +} + +schedule_info = { + 'num-records': 1, + 'attributes-list': { + 'sis-policy-info': { + 'comment': 'This policy is for x and y', + 'duration': 10, + 'enabled': 'true', + 'policy-name': 'test_policy', + 'policy-type': 'scheduled', + 'qos-policy': 'background', + 'vserver': 'svm3' + } + } +} + +ZRR = zapi_responses({ + 'threshold_info': build_zapi_response(threshold_info), + 'schedule_info': build_zapi_response(schedule_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + efficiency_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get_nonexistent_efficiency_policy(): + register_responses([ + ('sis-policy-get-iter', ZRR['empty']) + ]) + efficiency_obj = create_module(efficiency_module, DEFAULT_ARGS) + result = efficiency_obj.get_efficiency_policy() + assert not result + + +def test_get_existing_efficiency_policy(): + register_responses([ + ('sis-policy-get-iter', ZRR['threshold_info']) + ]) + efficiency_obj = create_module(efficiency_module, DEFAULT_ARGS) + result = efficiency_obj.get_efficiency_policy() + assert result + + +def test_successfully_create(): + register_responses([ + ('sis-policy-get-iter', ZRR['empty']), + ('sis-policy-create', ZRR['success']) + ]) + args = {'policy_type': 'threshold'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] + + +def test_create_idempotency(): + register_responses([ + ('sis-policy-get-iter', ZRR['threshold_info']) + ]) + args = {'policy_type': 'threshold'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS)['changed'] is False + + +def test_threshold_duration_failure(): + register_responses([ + ('sis-policy-get-iter', ZRR['threshold_info']) + ]) + args = {'duration': 1} + msg = create_and_apply(efficiency_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert "duration cannot be set if policy_type is threshold" == msg + + +def test_threshold_schedule_failure(): + register_responses([ + ('sis-policy-get-iter', ZRR['threshold_info']) + ]) + args = {'schedule': 'test_job_schedule'} + msg = create_and_apply(efficiency_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert "schedule cannot be set if policy_type is threshold" == msg + + +def test_scheduled_threshold_percent_failure(): + register_responses([ + ('sis-policy-get-iter', ZRR['schedule_info']) + ]) + args = {'changelog_threshold_percent': 30} + msg = create_and_apply(efficiency_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert "changelog_threshold_percent cannot be set if policy_type is scheduled" == msg + + +def test_successfully_delete(): + register_responses([ + ('sis-policy-get-iter', ZRR['threshold_info']), + ('sis-policy-delete', ZRR['success']) + ]) + args = {'state': 'absent'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_idempotency(): + register_responses([ + ('sis-policy-get-iter', ZRR['empty']) + ]) + args = {'state': 'absent'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_modify(): + register_responses([ + ('sis-policy-get-iter', ZRR['schedule_info']), + ('sis-policy-modify', ZRR['success']) + ]) + args = {'policy_type': 'threshold'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('sis-policy-get-iter', ZRR['error']), + ('sis-policy-create', ZRR['error']), + ('sis-policy-modify', ZRR['error']), + ('sis-policy-delete', ZRR['error']) + ]) + module_args = { + 'schedule': 'test_job_schedule' + } + + my_obj = create_module(efficiency_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.get_efficiency_policy, 'fail')['msg'] + assert 'Error searching for efficiency policy test_policy: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.create_efficiency_policy, 'fail')['msg'] + assert 'Error creating efficiency policy test_policy: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.modify_efficiency_policy, 'fail', modify={'schedule': 'test_job_schedule'})['msg'] + assert 'Error modifying efficiency policy test_policy: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.delete_efficiency_policy, 'fail')['msg'] + assert 'Error deleting efficiency policy test_policy: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + +def test_switch_to_zapi(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('sis-policy-get-iter', ZRR['schedule_info']) + ]) + args = {'use_rest': 'auto'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] is False + + +SRR = rest_responses({ + 'threshold_policy_info': (200, {"records": [ + { + "uuid": "d0845ae1-a8a8-11ec-aa26-005056b323e5", + "svm": {"name": "svm3"}, + "name": "test_policy", + "type": "threshold", + "start_threshold_percent": 30, + "qos_policy": "background", + "enabled": True, + "comment": "This policy is for x and y" + } + ], "num_records": 1}, None), + 'scheduled_policy_info': (200, {"records": [ + { + "uuid": "0d1f0860-a8a9-11ec-aa26-005056b323e5", + "svm": {"name": "svm3"}, + "name": "test_policy", + "type": "scheduled", + "duration": 5, + "schedule": {"name": "daily"}, + "qos_policy": "background", + "enabled": True, + "comment": "This policy is for x and y" + } + ], "num_records": 1}, None), +}) + + +def test_successful_create_rest(): + ''' Test successful create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['empty_records']), + ('POST', 'storage/volume-efficiency-policies', SRR['success']) + ]) + args = {'policy_type': 'threshold', 'use_rest': 'always'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args) + + +def test_create_idempotency_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['threshold_policy_info']) + ]) + args = {'policy_type': 'threshold', 'use_rest': 'always'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_threshold_duration_failure_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['threshold_policy_info']) + ]) + args = {'duration': 1, 'use_rest': 'always'} + msg = create_and_apply(efficiency_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert "duration cannot be set if policy_type is threshold" == msg + + +def test_threshold_schedule_failure_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['threshold_policy_info']) + ]) + args = {'schedule': 'test_job_schedule', 'use_rest': 'always'} + msg = create_and_apply(efficiency_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert "schedule cannot be set if policy_type is threshold" == msg + + +def test_scheduled_threshold_percent_failure_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['scheduled_policy_info']) + ]) + args = {'changelog_threshold_percent': 30, 'use_rest': 'always'} + msg = create_and_apply(efficiency_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert "changelog_threshold_percent cannot be set if policy_type is scheduled" == msg + + +def test_successfully_delete_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['scheduled_policy_info']), + ('DELETE', 'storage/volume-efficiency-policies/0d1f0860-a8a9-11ec-aa26-005056b323e5', SRR['success']) + ]) + args = {'state': 'absent', 'use_rest': 'always'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_idempotency_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['empty_records']) + ]) + args = {'state': 'absent', 'use_rest': 'always'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_modify_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['scheduled_policy_info']), + ('PATCH', 'storage/volume-efficiency-policies/0d1f0860-a8a9-11ec-aa26-005056b323e5', SRR['success']) + ]) + args = {'policy_type': 'threshold', 'use_rest': 'always'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_modify_duration_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['scheduled_policy_info']), + ('PATCH', 'storage/volume-efficiency-policies/0d1f0860-a8a9-11ec-aa26-005056b323e5', SRR['success']) + ]) + args = {'duration': 10, 'use_rest': 'always'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_modify_duration_set_hyphen_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['scheduled_policy_info']), + ('PATCH', 'storage/volume-efficiency-policies/0d1f0860-a8a9-11ec-aa26-005056b323e5', SRR['success']) + ]) + args = {'duration': "-", 'use_rest': 'always'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_modify_changelog_threshold_percent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['threshold_policy_info']), + ('PATCH', 'storage/volume-efficiency-policies/d0845ae1-a8a8-11ec-aa26-005056b323e5', SRR['success']) + ]) + args = {'changelog_threshold_percent': 40, 'use_rest': 'always'} + assert create_and_apply(efficiency_module, DEFAULT_ARGS, args)['changed'] + + +def test_if_all_methods_catch_exception_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volume-efficiency-policies', SRR['generic_error']), + ('POST', 'storage/volume-efficiency-policies', SRR['generic_error']), + ('PATCH', 'storage/volume-efficiency-policies', SRR['generic_error']), + ('DELETE', 'storage/volume-efficiency-policies', SRR['generic_error']) + ]) + module_args = { + 'schedule': 'test_job_schedule', + 'use_rest': 'always' + } + + my_obj = create_module(efficiency_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.get_efficiency_policy, 'fail')['msg'] + assert 'calling: storage/volume-efficiency-policies: got Expected error.' in error + + error = expect_and_capture_ansible_exception(my_obj.create_efficiency_policy, 'fail')['msg'] + assert 'calling: storage/volume-efficiency-policies: got Expected error.' in error + + error = expect_and_capture_ansible_exception(my_obj.modify_efficiency_policy, 'fail', modify={'schedule': 'test_job_schedule'})['msg'] + assert 'calling: storage/volume-efficiency-policies: got Expected error.' in error + + error = expect_and_capture_ansible_exception(my_obj.delete_efficiency_policy, 'fail')['msg'] + assert 'calling: storage/volume-efficiency-policies: got Expected error.' in error + + +def test_module_error_ontap_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + module_args = {'use_rest': 'always'} + msg = create_module(efficiency_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error: REST requires ONTAP 9.8 or later for efficiency_policy APIs.' == msg + + +def test_module_error_duration_in_threshold(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + module_args = { + 'use_rest': 'always', + 'policy_type': 'threshold', + 'duration': 1 + } + msg = create_module(efficiency_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'duration cannot be set if policy_type is threshold' == msg + + +def test_module_error_schedule_in_threshold(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + module_args = { + 'use_rest': 'always', + 'policy_type': 'threshold', + 'schedule': 'daily' + } + msg = create_module(efficiency_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'schedule cannot be set if policy_type is threshold' == msg + + +def test_module_error_changelog_threshold_percent_in_schedule(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + module_args = { + 'use_rest': 'always', + 'policy_type': 'scheduled', + 'changelog_threshold_percent': 20 + } + msg = create_module(efficiency_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'changelog_threshold_percent cannot be set if policy_type is scheduled' == msg diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_destination.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_destination.py new file mode 100644 index 000000000..ca951ba58 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_destination.py @@ -0,0 +1,226 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_ems_destination module ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception, call_main +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ems_destination \ + import NetAppOntapEmsDestination as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +SRR = rest_responses({ + 'ems_destination': (200, { + "records": [ + { + "name": "test", + "type": "rest-api", + "destination": "https://test.destination", + "filters": [ + { + "name": "test-filter" + } + ] + }], + "num_records": 1 + }, None), + 'missing_key': (200, { + "records": [ + { + "name": "test", + "type": "rest_api", + "destination": "https://test.destination" + }], + "num_records": 1 + }, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + +} + + +def test_get_ems_destination_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['empty_records']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_ems_destination('test') is None + + +def test_get_ems_destination_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['generic_error']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + my_module_object = create_module(my_module, DEFAULT_ARGS, module_args) + msg = 'Error fetching EMS destination for test: calling: support/ems/destinations: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_ems_destination, 'fail', 'test')['msg'] + + +def test_create_ems_destination(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['empty_records']), + ('POST', 'support/ems/destinations', SRR['empty_good']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_ems_destination_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('POST', 'support/ems/destinations', SRR['generic_error']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = expect_and_capture_ansible_exception(my_obj.create_ems_destination, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating EMS destinations for test: calling: support/ems/destinations: got Expected error.' == error + + +def test_delete_ems_destination(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination']), + ('DELETE', 'support/ems/destinations/test', SRR['empty_good']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter'], 'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_ems_destination_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('DELETE', 'support/ems/destinations/test', SRR['generic_error']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter'], 'state': 'absent'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = expect_and_capture_ansible_exception(my_obj.delete_ems_destination, 'fail', 'test')['msg'] + print('Info: %s' % error) + assert 'Error deleting EMS destination for test: calling: support/ems/destinations/test: got Expected error.' == error + + +def test_modify_ems_destination_filter(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['missing_key']), + ('PATCH', 'support/ems/destinations/test', SRR['empty_good']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['other-filter']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_destination_rest_api_idempotent(): + """ verify that rest-api is equivalent to rest_api """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination']), + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_destination_target(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination']), + ('PATCH', 'support/ems/destinations/test', SRR['empty_good']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://different.destination', 'filters': ['test-filter']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_destination_type(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination']), + ('DELETE', 'support/ems/destinations/test', SRR['empty_good']), + ('POST', 'support/ems/destinations', SRR['empty_good']) + ]) + module_args = {'name': 'test', 'type': 'email', 'destination': 'test@hq.com', 'filters': ['test-filter']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_destination_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('PATCH', 'support/ems/destinations/test', SRR['generic_error']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['other-filter']} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + modify = {'filters': ['other-filter']} + error = expect_and_capture_ansible_exception(my_obj.modify_ems_destination, 'fail', 'test', modify)['msg'] + print('Info: %s' % error) + assert 'Error modifying EMS destination for test: calling: support/ems/destinations/test: got Expected error.' == error + + +def test_module_fail_without_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_zapi']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + assert 'na_ontap_ems_destination is only supported with REST API' == error + + +def test_apply_returns_errors_from_get_destination(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['generic_error']) + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + assert 'Error fetching EMS destination for test: calling: support/ems/destinations: got Expected error.' == error + + +def test_check_mode_creates_no_destination(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['empty_records']), + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args, check_mode=True)['changed'] + + +def test_changed_set_to_ok_for_expected_values(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/destinations', SRR['ems_destination']), + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args, check_mode=True)['changed'] + + +def test_empty_modify_skips_patch(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = {'name': 'test', 'type': 'rest_api', 'destination': 'https://test.destination', 'filters': ['test-filter']} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.modify_ems_destination('test', {}) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_filter.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_filter.py new file mode 100644 index 000000000..f7f0a1feb --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ems_filter.py @@ -0,0 +1,308 @@ +# (c) 2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_ems_filter module ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception, call_main +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ems_filter \ + import NetAppOntapEMSFilters as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'ems_filter': (200, { + "name": "snmp-traphost", + "rules": [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "error,informational", + "name_pattern": "callhome.*", + } + }, { + "index": "2", + "type": "exclude", + "message_criteria": { + "severities": "*", + "name_pattern": "*", + "snmp_trap_types": "*", + } + }] + }, None), + 'ems_filter_2_riles': (200, { + "name": "snmp-traphost", + "rules": [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "error,informational", + "name_pattern": "callhome.*", + } + }, { + "index": "2", + "type": "include", + "message_criteria": { + "severities": "alert", + "name_pattern": "callhome.*", + } + }, { + "index": "3", + "type": "exclude", + "message_criteria": { + "severities": "*", + "name_pattern": "*", + "snmp_trap_types": "*", + } + }] + }, None), + 'ems_filter_no_rules': (200, { + "name": "snmp-traphost", + }, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': "snmp-traphost" +} + +DEFAULT_RULE = [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "error,informational", + "name_pattern": "callhome.*", + } +}] + + +DEFAULT_RULE_2_RULES = [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "error,informational", + "name_pattern": "callhome.*", + }}, { + "index": "2", + "type": "include", + "message_criteria": { + "severities": "alert", + "name_pattern": "callhome.*", + }}] + +DEFAULT_RULE_MODIFY_TYPE_2_RULES = [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "error,informational", + "name_pattern": "callhome.*", + } +}, { + "index": "2", + "type": "exclude", + "message_criteria": { + "severities": "alert", + "name_pattern": "callhome.*", + } +}] + +DEFAULT_RULE_MODIFY_SEVERITIES_2_RULES = [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "informational", + "name_pattern": "callhome.*", + } +}, { + "index": "2", + "type": "include", + "message_criteria": { + "severities": "alert", + "name_pattern": "callhome.*", + } +}] + +DEFAULT_RULE_MODIFY_NAME_PATTERN_2_RULES = [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "error,informational", + "name_pattern": "*", + } +}, { + "index": "2", + "type": "include", + "message_criteria": { + "severities": "alert", + "name_pattern": "callhome.*", + } +}] + +DEFAULT_RULE_STARS = [{ + "index": "1", + "type": "include", + "message_criteria": { + "severities": "*", + "name_pattern": "*", + } +}] + + +def test_get_ems_filter_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_ems_filter() is None + + +def test_get_ems_filter_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching ems filter snmp-traphost: calling: support/ems/filters: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_ems_filter, 'fail')['msg'] + + +def test_get_ems_filter_get(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_ems_filter() is not None + + +def test_create_ems_filter(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['empty_records']), + ('POST', 'support/ems/filters', SRR['empty_good']) + ]) + module_args = {'rules': DEFAULT_RULE} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_ems_filter_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('POST', 'support/ems/filters', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['rules'] = DEFAULT_RULE + error = expect_and_capture_ansible_exception(my_obj.create_ems_filter, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating EMS filter snmp-traphost: calling: support/ems/filters: got Expected error.' == error + + +def test_delete_ems_filter(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter']), + ('DELETE', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_ems_filter_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('DELETE', 'support/ems/filters/snmp-traphost', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['rules'] = DEFAULT_RULE + error = expect_and_capture_ansible_exception(my_obj.delete_ems_filter, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error deleting EMS filter snmp-traphost: calling: support/ems/filters/snmp-traphost: got Expected error.' == error + + +def test_modify_ems_filter_add_rule(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ]) + module_args = {'rules': DEFAULT_RULE_2_RULES} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_filter_change_type(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter_2_riles']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ]) + module_args = {'rules': DEFAULT_RULE_MODIFY_TYPE_2_RULES} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_filter_change_severities(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter_2_riles']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ]) + module_args = {'rules': DEFAULT_RULE_MODIFY_SEVERITIES_2_RULES} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_filter_change_name_pattern(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter_2_riles']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ]) + module_args = {'rules': DEFAULT_RULE_MODIFY_NAME_PATTERN_2_RULES} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ems_filter_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['rules'] = DEFAULT_RULE_2_RULES + error = expect_and_capture_ansible_exception(my_obj.modify_ems_filter, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error modifying EMS filter snmp-traphost: calling: support/ems/filters/snmp-traphost: got Expected error.' == error + + +def test_modify_ems_filter_no_rules(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter_no_rules']), + ]) + assert not create_and_apply(my_module, DEFAULT_ARGS, {})['changed'] + + +def test_modify_star_test(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'support/ems/filters', SRR['ems_filter']), + ('PATCH', 'support/ems/filters/snmp-traphost', SRR['empty_good']) + ]) + module_args = {'rules': DEFAULT_RULE_STARS} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy.py new file mode 100644 index 000000000..6d62fc497 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy.py @@ -0,0 +1,277 @@ +# (c) 2019-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_volume_export_policy ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_export_policy \ + import NetAppONTAPExportPolicy as policy_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_uuid_policy_id_export_policy': ( + 200, + { + "records": [{ + "svm": { + "uuid": "uuid", + "name": "svm"}, + "id": 123, + "name": "ansible" + }], + "num_records": 1}, None), + "no_record": ( + 200, + {"num_records": 0}, + None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'export_policy': + xml = self.build_export_policy_info(self.params) + self.xml_out = xml + return xml + + @staticmethod + def build_export_policy_info(export_policy_details): + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': {'export-policy-info': {'name': export_policy_details['name'] + }}} + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_job_schedule ''' + + def setUp(self): + self.mock_export_policy = { + 'name': 'test_policy', + 'vserver': 'test_vserver' + } + + def mock_args(self, rest=False): + if rest: + return { + 'vserver': self.mock_export_policy['vserver'], + 'name': self.mock_export_policy['name'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + else: + return { + 'vserver': self.mock_export_policy['vserver'], + 'name': self.mock_export_policy['name'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' + } + + def get_export_policy_mock_object(self, cx_type='zapi', kind=None): + policy_obj = policy_module() + if cx_type == 'zapi': + if kind is None: + policy_obj.server = MockONTAPConnection() + elif kind == 'export_policy': + policy_obj.server = MockONTAPConnection(kind='export_policy', data=self.mock_export_policy) + return policy_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + policy_module() + print('Info: %s' % exc.value.args[0]['msg']) + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_export_policy.NetAppONTAPExportPolicy.create_export_policy') + def test_successful_create(self, create_export_policy): + ''' Test successful create ''' + data = self.mock_args() + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_export_policy_mock_object().apply() + assert exc.value.args[0]['changed'] + create_export_policy.assert_called_with() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_export_policy.NetAppONTAPExportPolicy.get_export_policy') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_export_policy.NetAppONTAPExportPolicy.rename_export_policy') + def test_successful_rename(self, rename_export_policy, get_export_policy): + ''' Test successful rename ''' + data = self.mock_args() + data['from_name'] = 'old_policy' + set_module_args(data) + get_export_policy.side_effect = [ + None, + {'policy-name': 'old_policy'} + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_export_policy_mock_object().apply() + assert exc.value.args[0]['changed'] + rename_export_policy.assert_called_with() + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_create(self, mock_request): + '''Test successful rest create''' + data = self.mock_args(rest=True) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_record'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_export_policy_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_delete(self, mock_request): + '''Test successful rest delete''' + data = self.mock_args(rest=True) + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid_policy_id_export_policy'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_export_policy_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_fail_get_export_policy(self, mock_request): + '''Test successful rest delete''' + data = self.mock_args(rest=True) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_export_policy_mock_object(cx_type='rest').apply() + assert 'Error on fetching export policy: calling: protocols/nfs/export-policies/: got Expected error' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_ignore_from_name_when_state_absent(self, mock_request): + '''Test from_name is skipped for state absent''' + data = self.mock_args(rest=True) + data['from_name'] = 'ansible' + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid_policy_id_export_policy'], # this is record for name, from_name is skipped. + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_export_policy_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_rename(self, mock_request): + '''Test successful rest rename''' + data = self.mock_args(rest=True) + data['from_name'] = 'ansible' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_record'], + SRR['get_uuid_policy_id_export_policy'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_export_policy_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error_create(self, mock_request): + '''Test error rest create''' + data = self.mock_args(rest=True) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_record'], + SRR['generic_error'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_export_policy_mock_object(cx_type='rest').apply() + assert 'Error on creating export policy: calling: protocols/nfs/export-policies: got Expected error.' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error_delete(self, mock_request): + '''Test error rest delete''' + data = self.mock_args(rest=True) + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid_policy_id_export_policy'], + SRR['generic_error'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_export_policy_mock_object(cx_type='rest').apply() + print(exc.value.args[0]['msg']) + assert 'Error on deleting export policy: calling: protocols/nfs/export-policies/123: got Expected error.' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error_rename(self, mock_request): + '''Test error rest rename''' + data = self.mock_args(rest=True) + data['from_name'] = 'ansible' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_record'], + SRR['get_uuid_policy_id_export_policy'], + SRR['generic_error'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_export_policy_mock_object(cx_type='rest').apply() + print(exc.value.args[0]['msg']) + assert 'Error on renaming export policy: calling: protocols/nfs/export-policies/123: got Expected error.' in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy_rule.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy_rule.py new file mode 100644 index 000000000..66709fc0b --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy_rule.py @@ -0,0 +1,404 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_module, expect_and_capture_ansible_exception, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_export_policy_rule import NetAppontapExportRule as my_module, main as my_main + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +policy = { + 'attributes-list': { + 'export-policy-info': { + 'policy-name': 'name', + 'policy-id': '345' + }}} + +policy_rule = { + 'attributes-list': { + 'export-rule-info': { + 'policy-name': 'policy_name', + 'client-match': 'client_match', + 'ro-rule': [{ + 'security-flavor': 'any' + }], + 'rw-rule': [{ + 'security-flavor': 'any' + }], + 'protocol': [{ + 'access-protocol': 'protocol' + }], + 'super-user-security': { + 'security-flavor': 'any' + }, + 'is-allow-set-uid-enabled': 'false', + 'rule-index': 123, + 'anonymous-user-id': 'anonymous_user_id', + 'is-allow-dev-is-enabled': 'false', + 'export-chown-mode': 'restricted' + }}} + +policy_rule_two_records = { + 'attributes-list': [ + {'export-rule-info': { + 'policy-name': 'policy_name', + 'client-match': 'client_match1,client_match2', + 'ro-rule': [{ + 'security-flavor': 'any' + }], + 'rw-rule': [{ + 'security-flavor': 'any' + }], + 'protocol': [{ + 'access-protocol': 'protocol' + }], + 'super-user-security': { + 'security-flavor': 'any' + }, + 'is-allow-set-uid-enabled': 'false', + 'rule-index': 123, + 'anonymous-user-id': 'anonymous_user_id', + 'is-allow-dev-is-enabled': 'false', + 'export-chown-mode': 'restricted' + }}, + {'export-rule-info': { + 'policy-name': 'policy_name', + 'client-match': 'client_match2,client_match1', + 'ro-rule': [{ + 'security-flavor': 'any' + }], + 'rw-rule': [{ + 'security-flavor': 'any' + }], + 'protocol': [{ + 'access-protocol': 'protocol' + }], + 'super-user-security': { + 'security-flavor': 'any' + }, + 'is-allow-set-uid-enabled': 'false', + 'rule-index': 123, + 'anonymous-user-id': 'anonymous_user_id', + 'is-allow-dev-is-enabled': 'false', + 'export-chown-mode': 'restricted' + }}] +} + + +ZRR = zapi_responses({ + 'one_policy_record': build_zapi_response(policy, 1), + 'one_bad_policy_record': build_zapi_response({'error': 'no_policy_id'}, 1), + 'one_rule_record': build_zapi_response(policy_rule, 1), + 'two_rule_records': build_zapi_response(policy_rule_two_records, 2), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'never', + 'policy_name': 'policy_name', + 'vserver': 'vserver', + +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + args = dict(DEFAULT_ARGS) + args.pop('vserver') + error = 'missing required arguments:' + assert error in call_main(my_main, args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + error = 'Error: the python NetApp-Lib module is required. Import error: None' + assert error in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] + + +def test_get_nonexistent_rule(): + ''' Test if get_export_policy_rule returns None for non-existent policy ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['no_records']), + ]) + module_args = { + 'rule_index': 3 + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_export_policy_rule(3) is None + + +def test_get_nonexistent_policy(): + ''' Test if get_export_policy returns None for non-existent policy ''' + register_responses([ + ('ZAPI', 'export-policy-get-iter', ZRR['no_records']), + ]) + module_args = { + 'rule_index': 3 + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.set_export_policy_id() is None + + +def test_get_existing_rule(): + ''' Test if get_export_policy_rule returns rule details for existing policy ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['one_rule_record']), + ]) + module_args = { + 'rule_index': 3 + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + result = my_obj.get_export_policy_rule(3) + assert result + assert result['name'] == 'policy_name' + assert result['client_match'] == ['client_match'] + assert result['ro_rule'] == ['any'] + + +def test_get_existing_policy(): + ''' Test if get_export_policy returns policy details for existing policy ''' + register_responses([ + ('ZAPI', 'export-policy-get-iter', ZRR['one_policy_record']), + ]) + module_args = { + 'rule_index': 3 + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.set_export_policy_id() + assert my_obj.policy_id == '345' + + +def test_create_missing_param_error(): + ''' Test validation error from create ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['no_records']), + ('ZAPI', 'export-rule-get-iter', ZRR['no_records']), + ('ZAPI', 'export-policy-get-iter', ZRR['one_policy_record']), + ]) + module_args = { + 'client_match': 'client_match', + 'rw_rule': 'any', + 'rule_index': 3 + } + msg = 'Error: Missing required option for creating export policy rule: ro_rule' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_successful_create_with_index(): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['no_records']), + ('ZAPI', 'export-rule-get-iter', ZRR['no_records']), + ('ZAPI', 'export-policy-get-iter', ZRR['no_records']), + ('ZAPI', 'export-policy-create', ZRR['success']), + ('ZAPI', 'export-policy-get-iter', ZRR['one_policy_record']), + ('ZAPI', 'export-rule-create', ZRR['success']), + ]) + module_args = { + 'client_match': 'client_match', + 'rw_rule': 'any', + 'ro_rule': 'any', + 'rule_index': 123 + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_create_no_index(): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['no_records']), + ('ZAPI', 'export-policy-get-iter', ZRR['one_policy_record']), + ('ZAPI', 'export-rule-create', ZRR['success']), + ]) + module_args = { + 'client_match': 'client_match', + 'rw_rule': 'any', + 'ro_rule': 'any' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_idempotency(): + ''' Test create idempotency ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['one_rule_record']), + ]) + module_args = { + 'client_match': 'client_match', + 'rw_rule': 'any', + 'ro_rule': 'any' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete(): + ''' Test delete ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['one_rule_record']), + ('ZAPI', 'export-policy-get-iter', ZRR['one_policy_record']), + ('ZAPI', 'export-rule-destroy', ZRR['success']), + ]) + module_args = { + 'state': 'absent', + 'rule_index': 3 + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_idempotency(): + ''' Test delete idempotency ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['no_records']), + ]) + module_args = { + 'state': 'absent', + 'rule_index': 3 + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify(): + ''' Test successful modify protocol ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['one_rule_record']), + ('ZAPI', 'export-policy-get-iter', ZRR['one_policy_record']), + ('ZAPI', 'export-rule-modify', ZRR['success']), + ]) + module_args = { + 'protocol': ['cifs'], + 'allow_suid': True, + 'rule_index': 3, + 'allow_device_creation': True, + 'chown_mode': 'unrestricted' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_on_ambiguous_delete(): + ''' Test error if multiple entries match for a delete ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['two_rule_records']), + ]) + module_args = { + 'state': 'absent', + 'client_match': 'client_match1,client_match2', + 'rw_rule': 'any', + 'ro_rule': 'any' + } + error = "Error multiple records exist for query:" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_helper_query_parameters(): + ''' Test helper method set_query_parameters() ''' + register_responses([ + ]) + module_args = { + 'client_match': 'client_match1,client_match2', + 'rw_rule': 'any', + 'ro_rule': 'any' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + result = my_obj.set_query_parameters(10) + print(result) + assert 'query' in result + assert 'export-rule-info' in result['query'] + assert result['query']['export-rule-info']['rule-index'] == 10 + result = my_obj.set_query_parameters(None) + print(result) + assert 'client-match' not in result['query']['export-rule-info'] + assert result['query']['export-rule-info']['rw-rule'] == [{'security-flavor': 'any'}] + + +def test_error_calling_zapis(): + ''' Test error handing ''' + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['error']), + ('ZAPI', 'export-policy-get-iter', ZRR['error']), + ('ZAPI', 'export-policy-get-iter', ZRR['one_bad_policy_record']), + ('ZAPI', 'export-rule-create', ZRR['error']), + ('ZAPI', 'export-policy-create', ZRR['error']), + ('ZAPI', 'export-rule-destroy', ZRR['error']), + ('ZAPI', 'export-rule-modify', ZRR['error']), + ('ZAPI', 'export-rule-set-index', ZRR['error']), + ]) + module_args = { + 'client_match': 'client_match1,client_match2', + 'rw_rule': 'any', + 'ro_rule': 'any', + 'from_rule_index': 123, + 'rule_index': 124, + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = zapi_error_message('Error getting export policy rule policy_name') + assert error in expect_and_capture_ansible_exception(my_obj.get_export_policy_rule, 'fail', None)['msg'] + error = zapi_error_message('Error getting export policy policy_name') + assert error in expect_and_capture_ansible_exception(my_obj.set_export_policy_id, 'fail')['msg'] + error = 'Error getting export policy id for policy_name: got' + assert error in expect_and_capture_ansible_exception(my_obj.set_export_policy_id, 'fail')['msg'] + error = zapi_error_message('Error creating export policy rule policy_name') + assert error in expect_and_capture_ansible_exception(my_obj.create_export_policy_rule, 'fail')['msg'] + error = zapi_error_message('Error creating export policy policy_name') + assert error in expect_and_capture_ansible_exception(my_obj.create_export_policy, 'fail')['msg'] + error = zapi_error_message('Error deleting export policy rule policy_name') + assert error in expect_and_capture_ansible_exception(my_obj.delete_export_policy_rule, 'fail', 123)['msg'] + error = zapi_error_message('Error modifying export policy rule index 123') + assert error in expect_and_capture_ansible_exception(my_obj.modify_export_policy_rule, 'fail', {'rw_rule': ['any']}, 123)['msg'] + error = zapi_error_message('Error reindexing export policy rule index 123') + assert error in expect_and_capture_ansible_exception(my_obj.modify_export_policy_rule, 'fail', {'rule_index': 123}, 123, True)['msg'] + + +def test_index_existing_entry(): + """ validate entry can be found without index, and add index """ + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['no_records']), + ('ZAPI', 'export-rule-get-iter', ZRR['one_rule_record']), + ('ZAPI', 'export-policy-get-iter', ZRR['one_policy_record']), + ('ZAPI', 'export-rule-set-index', ZRR['success']), + ]) + module_args = { + 'client_match': 'client_match', + 'rw_rule': 'any', + 'ro_rule': 'any', + 'rule_index': 124, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_no_index(): + """ validate entry can be found without index, and deleted """ + register_responses([ + ('ZAPI', 'export-rule-get-iter', ZRR['two_rule_records']), + ('ZAPI', 'export-policy-get-iter', ZRR['one_policy_record']), + ('ZAPI', 'export-rule-destroy', ZRR['success']), + ]) + module_args = { + 'client_match': 'client_match2,client_match1', + 'rw_rule': 'any', + 'ro_rule': 'any', + 'state': 'absent', + 'force_delete_on_first_match': True, + 'allow_suid': False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy_rule_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy_rule_rest.py new file mode 100644 index 000000000..b1fb870e5 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_export_policy_rule_rest.py @@ -0,0 +1,387 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import copy +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + call_main, patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_export_policy_rule \ + import NetAppontapExportRule as policy_rule, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + 'get_uuid_policy_id_export_policy': (200, {"records": [ + { + "svm": {"uuid": "uuid", "name": "svm"}, + "id": 123, + "name": "ansible" + }], "num_records": 1}, None), + 'get_export_policy_rules': (200, {"records": [ + { + "rw_rule": ["any"], + "_links": {"self": {"href": "/api/resourcelink"}}, + "ro_rule": ["any"], + "allow_suid": True, + "chown_mode": "restricted", + "index": 10, + "superuser": ["any"], + "protocols": ["any"], + "anonymous_user": "1234", + "clients": [{"match": "10.10.0.0/16"}, {"match": "10.0.0.0/16"}, {"match": "10.20.0.0/16"}], + "ntfs_unix_security": "fail", + "allow_device_creation": True + }], "num_records": 1}, None), + 'get_export_policy_two_rules': (200, {"records": [ + { + "rw_rule": ["any"], + "_links": {"self": {"href": "/api/resourcelink"}}, + "ro_rule": ["any"], + "allow_suid": True, + "chown_mode": "restricted", + "index": 10, + "superuser": ["any"], + "protocols": ["any"], + "anonymous_user": "1234", + "clients": [{"match": "0.0.0.0/0"}], + "ntfs_unix_security": "fail", + "allow_device_creation": True + }, + { + "rw_rule": ["any"], + "ro_rule": ["any"], + "allow_suid": True, + "chown_mode": "restricted", + "index": 11, + "superuser": ["any"], + "protocols": ["any"], + "anonymous_user": "1234", + "clients": [{"match": "0.0.0.0/0"}], + "ntfs_unix_security": "fail", + "allow_device_creation": True + }], "num_records": 2}, None), + 'create_export_policy_rules': (200, {"records": [ + { + "rw_rule": ["any"], + "_links": {"self": {"href": "/api/resourcelink"}}, + "ro_rule": ["any"], + "allow_suid": True, + "chown_mode": "restricted", + "index": 1, + "superuser": ["any"], + "protocols": ["any"], + "anonymous_user": "1234", + "clients": [{"match": "0.0.0.0/0"}], + "ntfs_unix_security": "fail", + "allow_device_creation": True + }], "num_records": 1}, None), + 'error_does_not_exist': (400, None, {'message': "entry doesn't exist"}) +}) + + +DEFAULT_ARGS = { + 'name': 'test', + 'client_match': ['1.1.1.0', '0.0.0.0/0'], + 'vserver': 'test', + 'protocol': 'nfs', + 'anonymous_user_id': '65534', + 'super_user_security': ['any'], + 'ntfs_unix_security': 'fail', + 'ro_rule': 'any', + 'rw_rule': 'any', + 'allow_device_creation': True, + 'allow_suid': True, + 'chown_mode': 'restricted', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', +} + + +def test_rest_successful_create_rule(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/10', SRR['empty_records']), + ('GET', 'protocols/nfs/export-policies/123/rules', SRR['empty_records']), + ('POST', 'protocols/nfs/export-policies/123/rules?return_records=true', SRR['create_export_policy_rules']), + ('PATCH', 'protocols/nfs/export-policies/123/rules/1', SRR['empty_records']) + ]) + assert create_and_apply(policy_rule, DEFAULT_ARGS, {'rule_index': 10})['changed'] + + +def test_rest_error_get_policy(): + '''Test error rest get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['generic_error']) + ]) + my_module_object = create_module(policy_rule, DEFAULT_ARGS) + msg = 'Error on fetching export policy: calling: protocols/nfs/export-policies: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_export_policy_rule_rest, 'fail', 1)['msg'] + + +def test_rest_error_get_rule(): + '''Test error rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/10', SRR['generic_error']), + # 2nd try - this time without index + ('GET', 'protocols/nfs/export-policies/123/rules', SRR['generic_error']), + # 3rd try + ('GET', 'protocols/nfs/export-policies/123/rules', SRR['error_does_not_exist']), + # 4thtry + ('GET', 'protocols/nfs/export-policies/123/rules', SRR['get_export_policy_two_rules']), + ]) + module_args = { + 'anonymous_user_id': '1234', + 'protocol': 'any', + 'super_user_security': 'any', + 'client_match': ['0.0.0.0/0'], + 'ntfs_unix_security': 'fail', + 'ro_rule': ['any'], + 'rw_rule': ['any'], + 'rule_index': 10 + } + my_module_object = create_module(policy_rule, DEFAULT_ARGS, module_args) + msg = rest_error_message('Error on fetching export policy rule', 'protocols/nfs/export-policies/123/rules/10') + assert msg in expect_and_capture_ansible_exception(my_module_object.get_export_policy_rule, 'fail', 10)['msg'] + # error with no index + msg = rest_error_message('Error on fetching export policy rules', 'protocols/nfs/export-policies/123/rules') + assert msg in expect_and_capture_ansible_exception(my_module_object.get_export_policy_rule, 'fail', None)['msg'] + # does not exist error is ignored + assert my_module_object.get_export_policy_rule(None) is None + # multiple entries error + msg = 'Error multiple records exist for query:' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_export_policy_rule, 'fail', None)['msg'] + + +def test_rest_error_create_rule(): + '''Test error rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/10', SRR['empty_records']), + ('GET', 'protocols/nfs/export-policies/123/rules', SRR['empty_records']), + ('POST', 'protocols/nfs/export-policies/123/rules?return_records=true', SRR['generic_error']), + # 2nd call + ('GET', 'protocols/nfs/export-policies/123/rules/10', SRR['empty_records']), + ('GET', 'protocols/nfs/export-policies/123/rules', SRR['empty_records']), + ('POST', 'protocols/nfs/export-policies/123/rules?return_records=true', SRR['empty_records']) + ]) + my_module_object = create_module(policy_rule, DEFAULT_ARGS, {'rule_index': 10}) + msg = rest_error_message('Error on creating export policy rule', 'protocols/nfs/export-policies/123/rules?return_records=true') + assert msg in expect_and_capture_ansible_exception(my_module_object.apply, 'fail')['msg'] + msg = 'Error on creating export policy rule, returned response is invalid:' + assert msg in expect_and_capture_ansible_exception(my_module_object.apply, 'fail')['msg'] + + +def test_rest_successful_delete_rule(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/10', copy.deepcopy(SRR['get_export_policy_rules'])), + ('DELETE', 'protocols/nfs/export-policies/123/rules/10', SRR['empty_good']) + ]) + assert create_and_apply(policy_rule, DEFAULT_ARGS, {'rule_index': 10, 'state': 'absent'})['changed'] + + +def test_rest_error_delete(): + '''Test error rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/10', copy.deepcopy(SRR['get_export_policy_rules'])), + ('DELETE', 'protocols/nfs/export-policies/123/rules/10', SRR['generic_error']) + ]) + my_module_object = create_module(policy_rule, DEFAULT_ARGS, {'rule_index': 10, 'state': 'absent'}) + msg = 'Error on deleting export policy Rule: calling: protocols/nfs/export-policies/123/rules/10: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.apply, 'fail')['msg'] + + +def test_rest_successful_create_policy_and_rule(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['empty_records']), + ('GET', 'protocols/nfs/export-policies', SRR['empty_records']), + ('GET', 'protocols/nfs/export-policies', SRR['empty_records']), + ('POST', 'protocols/nfs/export-policies', SRR['empty_good']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('POST', 'protocols/nfs/export-policies/123/rules?return_records=true', SRR['create_export_policy_rules']), + ('PATCH', 'protocols/nfs/export-policies/123/rules/1', SRR['empty_records']) + ]) + assert create_and_apply(policy_rule, DEFAULT_ARGS, {'rule_index': 10})['changed'] + + +def test_rest_error_creating_policy(): + '''Test error rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['empty_records']), + ('GET', 'protocols/nfs/export-policies', SRR['empty_records']), + ('POST', 'protocols/nfs/export-policies', SRR['generic_error']), + ]) + my_module_object = create_module(policy_rule, DEFAULT_ARGS) + msg = 'Error on creating export policy: calling: protocols/nfs/export-policies: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.apply, 'fail')['msg'] + + +def test_rest_successful_modify(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/10', copy.deepcopy(SRR['get_export_policy_rules'])), + ('PATCH', 'protocols/nfs/export-policies/123/rules/10', SRR['empty_good']) + ]) + module_args = { + 'anonymous_user_id': '1234', + 'protocol': 'nfs4', + 'super_user_security': 'krb5i', + 'client_match': ['1.1.1.3', '1.1.0.3'], + 'ntfs_unix_security': 'ignore', + 'ro_rule': ['never'], + 'rw_rule': ['never'], + 'rule_index': 10, + 'allow_device_creation': False, + 'allow_suid': False, + 'chown_mode': 'unrestricted' + } + assert create_and_apply(policy_rule, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_modify(): + '''Test error rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/10', copy.deepcopy(SRR['get_export_policy_rules'])), + ('PATCH', 'protocols/nfs/export-policies/123/rules/10', SRR['generic_error']) + ]) + module_args = { + 'anonymous_user_id': '1234', + 'protocol': 'nfs4', + 'super_user_security': 'krb5i', + 'rule_index': 10 + } + + my_module_object = create_module(policy_rule, DEFAULT_ARGS, module_args) + msg = 'Error on modifying export policy Rule: calling: protocols/nfs/export-policies/123/rules/10: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.apply, 'fail')['msg'] + + +def test_rest_successful_rename(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/2', SRR['empty_records']), + ('GET', 'protocols/nfs/export-policies/123/rules/10', copy.deepcopy(SRR['get_export_policy_rules'])), + ('PATCH', 'protocols/nfs/export-policies/123/rules/10', SRR['empty_records']) + ]) + module_args = { + 'anonymous_user_id': '1234', + 'protocol': 'nfs4', + 'super_user_security': 'krb5i', + 'client_match': ['1.1.1.3', '1.1.0.3'], + 'ntfs_unix_security': 'ignore', + 'ro_rule': ['never'], + 'rw_rule': ['never'], + 'rule_index': 2, + 'from_rule_index': 10, + 'allow_device_creation': False, + 'allow_suid': False, + 'chown_mode': 'unrestricted' + } + assert create_and_apply(policy_rule, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successful_rename_no_from_index(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/2', SRR['error_does_not_exist']), + ('GET', 'protocols/nfs/export-policies/123/rules', copy.deepcopy(SRR['get_export_policy_rules'])), + ('PATCH', 'protocols/nfs/export-policies/123/rules/10', SRR['empty_records']) + ]) + module_args = { + 'anonymous_user_id': '1234', + 'protocol': 'any', + 'super_user_security': 'any', + 'client_match': ["10.10.0.0/16", "10.20.0.0/16", "10.0.0.0/16"], + 'ntfs_unix_security': 'fail', + 'ro_rule': ['any'], + 'rw_rule': ['any'], + 'rule_index': 2 + } + assert create_and_apply(policy_rule, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_rename_with_from_index_not_found(): + """ rename is requested but from rule is not found """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/3', SRR['error_does_not_exist']), + ('GET', 'protocols/nfs/export-policies/123/rules/2', SRR['error_does_not_exist']), + ]) + module_args = { + 'anonymous_user_id': '1234', + 'protocol': 'nfs4', + 'super_user_security': 'krb5i', + 'client_match': ['1.1.1.3', '1.1.0.3'], + 'ntfs_unix_security': 'ignore', + 'ro_rule': ['never'], + 'rw_rule': ['never'], + 'rule_index': 3, + 'from_rule_index': 2, + } + msg = 'Error reindexing: export policy rule 2 does not exist.' + assert msg in create_and_apply(policy_rule, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_delete_no_index_multiple(): + """ delete is requested but 2 rules are found """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules', SRR['get_export_policy_two_rules']), + # 2nd run + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules', SRR['get_export_policy_two_rules']), + ('DELETE', 'protocols/nfs/export-policies/123/rules/10', SRR['success']) + ]) + module_args = { + 'anonymous_user_id': '1234', + 'protocol': 'any', + 'super_user_security': 'any', + 'client_match': ['0.0.0.0/0'], + 'ntfs_unix_security': 'fail', + 'ro_rule': ['any'], + 'rw_rule': ['any'], + 'state': 'absent' + } + msg = 'Error multiple records exist for query:' + assert msg in create_and_apply(policy_rule, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['force_delete_on_first_match'] = True + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fcp_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fcp_rest.py new file mode 100644 index 000000000..4bd7c35a8 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fcp_rest.py @@ -0,0 +1,231 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import copy +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fcp \ + import NetAppOntapFCP as fcp # module under test + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'fcp_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "enabled": True, + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + } + } + ], + "num_records": 1 + }, None + ), + 'fcp_record_disabled': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "enabled": False, + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + } + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.data = data + self.xml_in = None + self.xml_out = None + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.mock_rule = {} + + def mock_args(self, rest=False): + if rest: + return { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'test_vserver', + } + + def get_mock_object(self, kind=None): + """ + Helper method to return an na_ontap_firewall_policy object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_firewall_policy object + """ + obj = fcp() + return obj + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error_get(self, mock_request): + '''Test error rest create''' + data = self.mock_args(rest=True) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_mock_object().apply() + assert 'Error on fetching fcp: calling: protocols/san/fcp/services: got Expected error.' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_create(self, mock_request): + '''Test successful rest create''' + data = self.mock_args(rest=True) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_record'], + SRR['empty_good'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error_create(self, mock_request): + '''Test error rest create''' + data = self.mock_args(rest=True) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_record'], + SRR['generic_error'], + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_mock_object().apply() + assert 'Error on creating fcp: calling: protocols/san/fcp/services: got Expected error.' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_delete(self, mock_request): + '''Test successful rest delete''' + data = self.mock_args(rest=True) + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + # the module under test modifies record directly, and may cause other tests to fail + copy.deepcopy(SRR['fcp_record']), + SRR['empty_good'], + SRR['empty_good'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error_delete(self, mock_request): + '''Test error rest delete''' + data = self.mock_args(rest=True) + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + copy.deepcopy(SRR['fcp_record']), + SRR['empty_good'], + SRR['generic_error'], + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_mock_object().apply() + assert 'Error on deleting fcp policy: calling: ' + \ + 'protocols/san/fcp/services/671aa46e-11ad-11ec-a267-005056b30cfa: got Expected error.' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_disable(self, mock_request): + '''Test successful rest disable''' + data = self.mock_args(rest=True) + data['status'] = 'down' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + copy.deepcopy(SRR['fcp_record']), + SRR['empty_good'], + + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_enable(self, mock_request): + '''Test successful rest enable''' + data = self.mock_args(rest=True) + data['status'] = 'up' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + copy.deepcopy(SRR['fcp_record_disabled']), + SRR['empty_good'], + + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error_enabled_change(self, mock_request): + '''Test error rest change''' + data = self.mock_args(rest=True) + data['status'] = 'down' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + copy.deepcopy(SRR['fcp_record']), + SRR['generic_error'], + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_mock_object().apply() + assert 'Error on modifying fcp: calling: ' + \ + 'protocols/san/fcp/services/671aa46e-11ad-11ec-a267-005056b30cfa: ' + \ + 'got Expected error.' in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdsd.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdsd.py new file mode 100644 index 000000000..5076af5f9 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdsd.py @@ -0,0 +1,136 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP na_ontap_fdsd Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fdsd \ + import NetAppOntapFDSD as my_module # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def default_args(): + args = { + 'name': 'test', + 'vserver': 'vserver1', + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'ntfs_record': ( + 200, { + 'records': [{ + 'vserver': 'vserver1', + 'ntfs_sd': 'sd1'}], + 'num_records': 1}, + None), + +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' test missing arguements ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_remove(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' remove Security Descriptor ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['ntfs_record'], + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Idempotent test ''' + args = dict(default_args()) + args['name'] = 'sd1' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['ntfs_record'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Create security descriptor''' + args = dict(default_args()) + args['name'] = 'new_sd' + print(args) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], + SRR['empty_good'], # create + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdsp.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdsp.py new file mode 100644 index 000000000..d523e7062 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdsp.py @@ -0,0 +1,134 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP na_ontap_fdsp Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fdsp \ + import NetAppOntapFDSP as my_module # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def default_args(): + args = { + 'name': 'test', + 'vserver': 'vserver1', + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'security_policy_record': ( + 200, { + 'records': [{ + 'vserver': 'vserver1', + 'policy_name': 'test'}], + 'num_records': 1}, + None), + +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' test missing arguements ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Create security policies''' + args = dict(default_args()) + args['name'] = 'new_security_policy' + print(args) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], + SRR['empty_good'], # create + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_remove(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' remove Security policies ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['security_policy_record'], + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Idempotent test ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['security_policy_record'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdss.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdss.py new file mode 100644 index 000000000..22e06fc1f --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fdss.py @@ -0,0 +1,102 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP na_ontap_fdsg Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fdss \ + import NetAppOntapFDSS as my_module # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def default_args(): + args = { + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver1', + 'name': 'policy1' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'job_id_record': ( + 200, { + 'job': { + 'uuid': '94b6e6a7-d426-11eb-ac81-00505690980f', + '_links': {'self': {'href': '/api/cluster/jobs/94b6e6a7-d426-11eb-ac81-00505690980f'}}}, + 'cli_output': ' Use the "job show -id 2379" command to view the status of this operation.'}, None), + 'job_response_record': ( + 200, { + "uuid": "f03ccbb6-d8bb-11eb-ac81-00505690980f", + "description": "File Directory Security Apply Job", + "state": "success", + "message": "Complete: Operation completed successfully. File ACLs modified using policy \"policy1\" on Vserver \"GBSMNAS80LD\". File count: 0. [0]", + "code": 0, + "start_time": "2021-06-29T05:25:26-04:00", + "end_time": "2021-06-29T05:25:26-04:00" + }, None + ) +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' test missing arguements ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_success(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Create job to apply policy to directory ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['job_id_record'], + SRR['job_response_record'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_directory_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_directory_policy.py new file mode 100644 index 000000000..94af48ed8 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_directory_policy.py @@ -0,0 +1,136 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_file_directory_policy ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_file_directory_policy \ + import NetAppOntapFilePolicy as policy_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + request = xml.to_string().decode('utf-8') + if self.kind == 'error': + raise netapp_utils.zapi.NaApiError('test', 'expect error') + elif request.startswith("<ems-autosupport-log>"): + xml = None # or something that may the logger happy, and you don't need @patch anymore + # or + # xml = build_ems_log_response() + elif request.startswith("<file-directory-security-policy-get-iter>"): + if self.kind == 'create': + xml = self.build_sd_info() + else: + xml = self.build_sd_info(self.params) + elif request.startswith("<file-directory-security-ntfs-modify>"): + xml = self.build_sd_info(self.params) + self.xml_out = xml + return xml + + @staticmethod + def build_sd_info(data=None): + xml = netapp_utils.zapi.NaElement('xml') + attributes = {} + if data is not None: + attributes = {'num-records': 1, + 'attributes-list': {'file-directory-security-policy': {'policy-name': data['policy_name']}}} + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_file_directory_policy ''' + + def mock_args(self): + return { + 'vserver': 'vserver', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_policy_mock_object(self, type='zapi', kind=None, status=None): + policy_obj = policy_module() + if type == 'zapi': + if kind is None: + policy_obj.server = MockONTAPConnection() + else: + policy_obj.server = MockONTAPConnection(kind=kind, data=status) + return policy_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + policy_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_successfully_create_policy(self): + data = self.mock_args() + data['policy_name'] = 'test_policy' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_policy_mock_object('zapi', 'create', data).apply() + assert exc.value.args[0]['changed'] + + def test_error(self): + data = self.mock_args() + data['policy_name'] = 'test_policy' + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_mock_object('zapi', 'error', data).get_policy_iter() + assert exc.value.args[0]['msg'] == 'Error fetching file-directory policy test_policy: NetApp API failed. Reason - test:expect error' + + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_mock_object('zapi', 'error', data).create_policy() + assert exc.value.args[0]['msg'] == 'Error creating file-directory policy test_policy: NetApp API failed. Reason - test:expect error' + + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_mock_object('zapi', 'error', data).remove_policy() + assert exc.value.args[0]['msg'] == 'Error removing file-directory policy test_policy: NetApp API failed. Reason - test:expect error' + + data['path'] = '/vol' + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_mock_object('zapi', 'error', data).get_task_iter() + assert exc.value.args[0]['msg'] == 'Error fetching task from file-directory policy test_policy: NetApp API failed. Reason - test:expect error' + + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_mock_object('zapi', 'error', data).add_task_to_policy() + assert exc.value.args[0]['msg'] == 'Error adding task to file-directory policy test_policy: NetApp API failed. Reason - test:expect error' + + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_mock_object('zapi', 'error', data).remove_task_from_policy() + assert exc.value.args[0]['msg'] == 'Error removing task from file-directory policy test_policy: NetApp API failed. Reason - test:expect error' + + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_mock_object('zapi', 'error', data).modify_task(dict()) + assert exc.value.args[0]['msg'] == 'Error modifying task in file-directory policy test_policy: NetApp API failed. Reason - test:expect error' + + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_mock_object('zapi', 'error', data).set_sd() + assert exc.value.args[0]['msg'] == 'Error applying file-directory policy test_policy: NetApp API failed. Reason - test:expect error' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_security_permissions.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_security_permissions.py new file mode 100644 index 000000000..b25dca7ab --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_security_permissions.py @@ -0,0 +1,647 @@ +# (c) 2022-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_warning_was_raised, print_warnings, \ + patch_ansible, call_main, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_file_security_permissions \ + import NetAppOntapFileSecurityPermissions as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def build_acl(user, access='access_allow', access_control='file_directory', apply_to=None, inherited=None, advanced_rights='all', rights=None): + if apply_to is None: + apply_to = {'this_folder': True} + if advanced_rights == 'all': + advanced_rights = { + 'append_data': True, + 'delete': True, + 'delete_child': True, + 'execute_file': True, + 'full_control': True, + 'read_attr': True, + 'read_data': True, + 'read_ea': True, + 'read_perm': True, + 'synchronize': True, + 'write_attr': True, + 'write_data': True, + 'write_ea': True, + 'write_owner': True, + 'write_perm': True + } + + acl = { + 'access': access, + 'access_control': access_control, + 'advanced_rights': advanced_rights, + 'apply_to': apply_to, + 'user': user + } + if inherited is not None: + acl['inherited'] = inherited + if rights is not None: + acl['rights'] = rights + return acl + + +SRR = rest_responses({ + 'non_acl': (200, { + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_only_inherited_acl': (200, { + 'acls': [ + build_acl('Everyone', inherited=True) + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_multiple_user': (200, { + 'acls': [ + build_acl('NETAPPAD\\mohan9'), + build_acl('SERVER_CIFS_TE\\mohan11'), + build_acl('Everyone', inherited=True) + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_single_user_deny': (200, { + 'acls': [ + build_acl('NETAPPAD\\mohan9', access='access_deny') + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_single_user_deny_empty_advrights': (200, { + 'acls': [ + build_acl('NETAPPAD\\mohan9', access='access_deny', advanced_rights={}) + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_single_user_deny_empty_advrights_mohan11': (200, { + 'acls': [ + build_acl('NETAPPAD\\mohan9', access='access_deny', advanced_rights={}) + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'SERVER_CIFS_TE\\mohan11', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_single_user_rights': (200, { + 'acls': [ + build_acl('NETAPPAD\\mohan9', access='access_deny', advanced_rights={}, rights='full_control') + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'slag_acl_same_user': (200, { + 'acls': [ + build_acl('SERVER_CIFS_TE\\mohan11', access_control='slag', apply_to={'files': True}, advanced_rights={"append_data": True}, access='access_deny'), + build_acl('SERVER_CIFS_TE\\mohan11', access_control='slag', apply_to={'files': True}, advanced_rights={"append_data": True}) + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'svm_id': (200, { + 'uuid': '55bcb009' + }, None), + 'error_655865': (400, None, {'code': 655865, 'message': 'Expected error'}), +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'path': '/vol200/aNewFile.txt', + 'acls': [ + { + "access": "access_allow", + "user": "SERVER_CIFS_TE\\mohan11", + "advanced_rights": {"append_data": True}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False} + }, + { + "access": "access_allow", + "user": "NETAPPAD\\mohan9", + "advanced_rights": {"append_data": True}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False} + }, + + ] +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "vserver", "path"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_create_file_directory_acl(): + ''' create file_directory acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['zero_records']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['non_acl']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl', SRR['success']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['zero_records']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['non_acl']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['non_acl']), + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS)['changed'] + # Add ACLs to an SD only record + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + # create SD only + args = dict(DEFAULT_ARGS) + args.pop('acls') + assert create_and_apply(my_module, args)['changed'] + assert not create_and_apply(my_module, args)['changed'] + + +def test_add_file_directory_acl(): + ''' add file_directory acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']), + ('DELETE', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/NETAPPAD%5Cmohan9', SRR['success']), + ('DELETE', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/SERVER_CIFS_TE%5Cmohan11', SRR['success']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny']) + ]) + args = { + 'acls': [{ + "access": "access_deny", + "user": "NETAPPAD\\mohan9", + "advanced_rights": {"append_data": True}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + }] + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_file_directory_acl(): + ''' add file_directory acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']), + ('DELETE', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/NETAPPAD%5Cmohan9', SRR['success']), + ('DELETE', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/SERVER_CIFS_TE%5Cmohan11', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_only_inherited_acl']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_only_inherited_acl']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['error_655865']), + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['generic_error']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['generic_error']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl', SRR['generic_error']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['generic_error']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/user1', SRR['generic_error']), + ('DELETE', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/user1', SRR['generic_error']) + ]) + + acl_obj = create_module(my_module, DEFAULT_ARGS) + acl_obj.svm_uuid = "55bcb009" + assert 'Error fetching file security' in expect_and_capture_ansible_exception(acl_obj.get_file_security_permissions, 'fail')['msg'] + assert 'Error creating file security' in expect_and_capture_ansible_exception(acl_obj.create_file_security_permissions, 'fail')['msg'] + assert 'Error adding file security' in expect_and_capture_ansible_exception(acl_obj.add_file_security_permissions_acl, 'fail', {})['msg'] + assert 'Error modifying file security' in expect_and_capture_ansible_exception(acl_obj.modify_file_security_permissions, 'fail', {})['msg'] + acl = {'user': 'user1'} + assert 'Error modifying file security' in expect_and_capture_ansible_exception(acl_obj.modify_file_security_permissions_acl, 'fail', acl)['msg'] + assert 'Error deleting file security permissions' in expect_and_capture_ansible_exception(acl_obj.delete_file_security_permissions_acl, 'fail', acl)['msg'] + # no network calls + assert 'Error: mismatch on path values: desired:' in expect_and_capture_ansible_exception( + acl_obj.get_modify_actions, 'fail', {'path': 'dummy'})['msg'] + + +def test_create_file_directory_slag(): + ''' create slag acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['zero_records']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['slag_acl_same_user']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['slag_acl_same_user']) + ]) + args = { + 'access_control': 'slag', + 'acls': [ + { + 'access': 'access_deny', + 'access_control': 'slag', + 'advanced_rights': {'append_data': True}, + 'apply_to': {'files': True, "this_folder": False, "sub_folders": False}, + 'user': 'SERVER_CIFS_TE\\mohan11' + }, + { + 'access': 'access_allow', + 'access_control': 'slag', + 'advanced_rights': {'append_data': True}, + 'apply_to': {'files': True, "this_folder": False, "sub_folders": False}, + 'user': 'SERVER_CIFS_TE\\mohan11' + } + ] + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_modify_file_directory_owner(): + ''' modify file owner ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny_empty_advrights']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny_empty_advrights_mohan11']), + ]) + args = { + 'acls': [{ + "access": "access_deny", + "user": "NETAPPAD\\mohan9", + "advanced_rights": {"append_data": False}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + }], + 'owner': 'SERVER_CIFS_TE\\mohan11' + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + # idempotency already tested in create and add + + +def test_modify_file_directory_acl_advrights(): + ''' add file_directory acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/NETAPPAD%5Cmohan9', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny_empty_advrights']), + ]) + args = { + 'acls': [{ + "access": "access_deny", + "user": "NETAPPAD\\mohan9", + "advanced_rights": {"append_data": False}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + }] + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + # idempotency already tested in create and add + + +def test_modify_file_directory_acl_rights(): + ''' add file_directory acl using rights + it always fails the validation check, as REST does not return rights + it is not idempotent for the same reason + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/NETAPPAD%5Cmohan9', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny_empty_advrights']), + # 2nd run + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/NETAPPAD%5Cmohan9', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny_empty_advrights']), + ]) + args = { + 'acls': [{ + "access": "access_deny", + "user": "NETAPPAD\\mohan9", + "rights": 'modify', + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + }], + 'validate_changes': 'error' + } + error = "Error - patch-acls still required for [{" + assert error in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + args['validate_changes'] = 'warn' + assert call_main(my_main, DEFAULT_ARGS, args)['changed'] + print_warnings() + assert_warning_was_raised('Error - patch-acls still required for [', partial_match=True) + + +def test_negative_acl_rights_and_advrights(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + args = { + 'access_control': 'file_directory', + 'acls': [{ + "access": "access_deny", + "user": "NETAPPAD\\mohan9", + "advanced_rights": {"append_data": False}, + "rights": 'modify', + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + }], + 'validate_changes': 'error' + + } + error = "Error: suboptions 'rights' and 'advanced_rights' are mutually exclusive." + assert error in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + del args['acls'][0]['rights'] + args['acls'][0]['access_control'] = "slag" + error = "Error: mismatch between top level value and ACL value for" + assert error in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + args['acls'][0]['apply_to'] = {"this_folder": False, "files": False, "sub_folders": False} + error = "Error: at least one suboption must be true for apply_to. Got: " + assert error in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_get_acl_actions_on_create(): + """ given a set of ACLs in self.parameters, split them in four groups, or fewer """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + + apply_to = {'this_folder': True, 'files': False, 'sub_folders': False} + + fd_prop_acls = [ + # All these ACLs fall into a single category, as file_directory and propagate are the defaults + {"access": "access_deny", "user": "user01", "apply_to": apply_to}, + {"access": "access_deny", "user": "user02", "apply_to": apply_to, 'access_control': 'file_directory'}, + {"access": "access_deny", "user": "user03", "apply_to": apply_to, 'access_control': 'file_directory', 'propagation_mode': 'propagate'}, + {"access": "access_deny", "user": "user04", "apply_to": apply_to, 'propagation_mode': 'propagate'} + ] + + fd_replace_acls = [ + {"access": "access_deny", "user": "user11", "apply_to": apply_to, 'access_control': 'file_directory', 'propagation_mode': 'replace'}, + {"access": "access_deny", "user": "user12", "apply_to": apply_to, 'propagation_mode': 'replace'} + ] + + slag_prop_acls = [ + {"access": "access_deny", "user": "user21", "apply_to": apply_to, 'access_control': 'slag'}, + {"access": "access_deny", "user": "user22", "apply_to": apply_to, 'access_control': 'slag', 'propagation_mode': 'propagate'}, + ] + + slag_replace_acls = [ + {"access": "access_deny", "user": "user31", "apply_to": apply_to, 'access_control': 'slag', 'propagation_mode': 'replace'}, + ] + + args = { + 'acls': fd_prop_acls, + 'validate_changes': 'error' + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + acls = my_obj.get_acl_actions_on_create() + assert not any(acls[x] for x in acls) + assert my_obj.parameters['acls'] == fd_prop_acls + + args = { + 'acls': fd_prop_acls + fd_replace_acls + slag_prop_acls + slag_replace_acls, + 'validate_changes': 'error' + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + acls = my_obj.get_acl_actions_on_create() + print('P_ACLS', acls) + print('C_ACLS', my_obj.parameters['acls']) + assert len(acls['post-acls']) == 5 + assert my_obj.parameters['acls'] == fd_prop_acls + + args = { + 'acls': slag_replace_acls, + 'validate_changes': 'error' + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + acls = my_obj.get_acl_actions_on_create() + assert not any(acls[x] for x in acls) + assert my_obj.parameters['acls'] == slag_replace_acls + + +def test_get_acl_actions_on_create_special(): + """ given a set of ACLs in self.parameters, split them in four groups, or fewer """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + + apply_to = {'this_folder': True, 'files': False, 'sub_folders': False} + + fd_prop_acls = [ + # All these ACLs fall into a single category, as file_directory and propagate are the defaults + {"access": "access_deny", "user": "user01", "apply_to": apply_to}, + {"access": "access_deny", "user": "user02", "apply_to": apply_to, 'access_control': 'file_directory'}, + {"access": "access_deny", "user": "user03", "apply_to": apply_to, 'access_control': 'file_directory', 'propagation_mode': 'propagate'}, + {"access": "access_deny", "user": "user04", "apply_to": apply_to, 'propagation_mode': 'propagate'} + ] + + fd_replace_acls = [ + {"access": "access_deny", "user": "user11", "apply_to": apply_to, 'access_control': 'file_directory', 'propagation_mode': 'replace'}, + {"access": "access_deny", "user": "user12", "apply_to": apply_to, 'propagation_mode': 'replace'} + ] + + slag_prop_acls = [ + {"access": "access_allowed_callback", "user": "user21", "apply_to": apply_to, 'access_control': 'slag'}, + {"access": "access_denied_callback", "user": "user22", "apply_to": apply_to, 'access_control': 'slag', 'propagation_mode': 'propagate'}, + ] + + slag_replace_acls = [ + {"access": "access_deny", "user": "user31", "apply_to": apply_to, 'access_control': 'slag', 'propagation_mode': 'replace'}, + ] + + fd_replace_acls_conflict = [ + {"access": "access_denied_callback", "user": "user11", "apply_to": apply_to, 'access_control': 'file_directory', 'propagation_mode': 'replace'}, + {"access": "access_allowed_callback", "user": "user12", "apply_to": apply_to, 'propagation_mode': 'replace'} + ] + + args = { + 'acls': fd_prop_acls + fd_replace_acls + slag_prop_acls + slag_replace_acls, + 'validate_changes': 'error' + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + acls = my_obj.get_acl_actions_on_create() + print('P_ACLS', acls) + print('C_ACLS', my_obj.parameters['acls']) + assert len(acls['post-acls']) == 7 + assert my_obj.parameters['acls'] == slag_prop_acls + + args = { + 'acls': fd_prop_acls + fd_replace_acls_conflict + slag_prop_acls + slag_replace_acls, + 'validate_changes': 'error' + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + error = 'with access access_allowed_callback conflicts with other ACLs using accesses' + assert error in expect_and_capture_ansible_exception(my_obj.get_acl_actions_on_create, 'fail')['msg'] + + +def test_negative_unsupported_version(): + ''' create slag acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_9_1']), + # ('GET', 'svm/svms', SRR['svm_id']), + # ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['non_acl']), + # ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['success']), + # ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['slag_acl_same_user']), + # ('GET', 'cluster', SRR['is_rest_9_10_1']), + # ('GET', 'svm/svms', SRR['svm_id']), + # ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['slag_acl_same_user']) + ]) + args = { + 'access_control': 'slag', + 'acls': [ + { + 'access': 'access_deny', + 'access_control': 'slag', + 'advanced_rights': {'append_data': True}, + 'apply_to': {'files': True, "this_folder": False, "sub_folders": False}, + 'user': 'SERVER_CIFS_TE\\mohan11' + }, + { + 'access': 'access_allow', + 'access_control': 'slag', + 'advanced_rights': {'append_data': True}, + 'apply_to': {'files': True, "this_folder": False, "sub_folders": False}, + 'user': 'SERVER_CIFS_TE\\mohan11' + } + ] + } + error = 'Error: na_ontap_file_security_permissions only supports REST, and requires ONTAP 9.9.1 or later. Found: 9.8.0.' + assert error in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + error = 'Minimum version of ONTAP for access_control is (9, 10, 1)' + msg = call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + assert error in msg + error = 'Minimum version of ONTAP for acls.access_control is (9, 10, 1)' + assert error in msg + + +def test_match_acl_with_acls(): + """ given a set of ACLs in self.parameters, split them in four groups, or fewer """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + + apply_to = {'this_folder': True, 'files': False, 'sub_folders': False} + + fd_prop_acls = [ + # All these ACLs fall into a single category, as file_directory and propagate are the defaults + {"access": "access_deny", "user": "user01", "apply_to": apply_to}, + {"access": "access_deny", "user": "user02", "apply_to": apply_to, 'access_control': 'file_directory'}, + {"access": "access_deny", "user": "user03", "apply_to": apply_to, 'access_control': 'file_directory', 'propagation_mode': 'propagate'}, + {"access": "access_deny", "user": "user04", "apply_to": apply_to, 'propagation_mode': 'propagate'} + ] + + fd_replace_acls = [ + {"access": "access_deny", "user": "user11", "apply_to": apply_to, 'access_control': 'file_directory', 'propagation_mode': 'replace'}, + {"access": "access_deny", "user": "user12", "apply_to": apply_to, 'propagation_mode': 'replace'} + ] + + acl = fd_prop_acls[3] + my_obj = create_module(my_module, DEFAULT_ARGS) + assert acl == my_obj.match_acl_with_acls(acl, fd_prop_acls) + assert my_obj.match_acl_with_acls(acl, fd_replace_acls) is None + error = 'Error: found more than one desired ACLs with same user, access, access_control and apply_to' + assert error in expect_and_capture_ansible_exception(my_obj.match_acl_with_acls, 'fail', acl, fd_prop_acls + fd_prop_acls)['msg'] + + +def test_validate_changes(): + """ verify nothing needs to be changed """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/file-security/permissions/None/%2Fvol200%2FaNewFile.txt', SRR['zero_records']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/file-security/permissions/None/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny']), + ]) + args = { + 'validate_changes': 'ignore' + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + assert my_obj.validate_changes('create', {}) is None + args = { + 'validate_changes': 'error' + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + error = 'Error - create still required after create' + assert error in expect_and_capture_ansible_exception(my_obj.validate_changes, 'fail', 'create', {})['msg'] + args = { + 'validate_changes': 'warn', + 'owner': 'new_owner' + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + warning = "Error - modify: {'owner': 'new_owner'} still required after {'a': 'b'}" + assert my_obj.validate_changes('create', {'a': 'b'}) is None + assert_warning_was_raised(warning, partial_match=True) + assert_warning_was_raised('post-acls still required for', partial_match=True) + assert_warning_was_raised('delete-acls still required for', partial_match=True) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_security_permissions_acl.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_security_permissions_acl.py new file mode 100644 index 000000000..510f04a9e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_file_security_permissions_acl.py @@ -0,0 +1,331 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + patch_ansible, assert_warning_was_raised, call_main, print_warnings, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + get_mock_record, patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_file_security_permissions_acl\ + import NetAppOntapFileSecurityPermissionsACL as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def build_acl(user, access='access_allow', access_control='file_directory', apply_to=None, inherited=None, advanced_rights='all', rights=None): + if apply_to is None: + apply_to = {'this_folder': True} + if advanced_rights == 'all': + advanced_rights = { + 'append_data': True, + 'delete': True, + 'delete_child': True, + 'execute_file': True, + 'full_control': True, + 'read_attr': True, + 'read_data': True, + 'read_ea': True, + 'read_perm': True, + 'synchronize': True, + 'write_attr': True, + 'write_data': True, + 'write_ea': True, + 'write_owner': True, + 'write_perm': True + } + + acl = { + 'access': access, + 'access_control': access_control, + 'advanced_rights': advanced_rights, + 'apply_to': apply_to, + 'user': user + } + if inherited is not None: + acl['inherited'] = inherited + if rights is not None: + acl['rights'] = rights + return acl + + +SRR = rest_responses({ + 'non_acl': (200, { + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_only_inherited_acl': (200, { + 'acls': [ + build_acl('Everyone', inherited=True) + ], + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_multiple_user': (200, { + 'acls': [ + build_acl('NETAPPAD\\mohan9'), + build_acl('SERVER_CIFS_TE\\mohan11'), + build_acl('Everyone', inherited=True) + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_multiple_user_adv_rights': (200, { + 'acls': [ + build_acl('NETAPPAD\\mohan9'), + build_acl('SERVER_CIFS_TE\\mohan11', advanced_rights={"append_data": True}), + build_acl('Everyone', inherited=True) + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'fd_acl_single_user_deny': (200, { + 'acls': [ + build_acl('SERVER_CIFS_TE\\mohan11', access='access_deny') + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'slag_acl_same_user': (200, { + 'acls': [ + build_acl('SERVER_CIFS_TE\\mohan11', access_control='slag', apply_to={'files': True}, advanced_rights={"append_data": True}, access='access_deny'), + build_acl('SERVER_CIFS_TE\\mohan11', access_control='slag', apply_to={'files': True}, advanced_rights={"append_data": True}) + ], + 'control_flags': '0x8014', + 'group': 'BUILTIN\\Administrators', + 'owner': 'BUILTIN\\Administrators', + 'path': '/vol200/aNewFile.txt', + 'svm': {'name': 'ansible_ipspace_datasvm', 'uuid': '55bcb009'} + }, None), + 'svm_id': (200, { + 'uuid': '55bcb009' + }, None), + 'error_655865': (400, None, {'code': 655865, 'message': 'Expected error'}), +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'path': '/vol200/aNewFile.txt', + 'access_control': 'file_directory', + "access": "access_allow", + "acl_user": "SERVER_CIFS_TE\\mohan11", +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "vserver", "path"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_create_file_directory_acl(): + ''' create file_directory acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['non_acl']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']) + ]) + module_args = { + "advanced_rights": {"append_data": True}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_file_directory_acl(): + ''' modify file_directory acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/SERVER_CIFS_TE%5Cmohan11', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user_adv_rights']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user_adv_rights']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user_adv_rights']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/SERVER_CIFS_TE%5Cmohan11', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user_adv_rights']), + ]) + module_args = { + 'advanced_rights': {'append_data': True, 'delete': False}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args = { + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + 'rights': 'full_control', + } + error = "Error - modify: {'rights': 'full_control'} still required after {'rights': 'full_control'}" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_delete_file_directory_acl(): + ''' add file_directory acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_multiple_user']), + ('DELETE', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/SERVER_CIFS_TE%5Cmohan11', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['non_acl']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_only_inherited_acl']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['error_655865']) + ]) + module_args = { + "advanced_rights": {"append_data": True}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + "state": "absent" + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_acl_rights_and_advrights(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + args = { + 'access_control': 'file_directory', + "access": "access_deny", + "acl_user": "NETAPPAD\\mohan9", + "advanced_rights": {"append_data": False}, + "rights": 'modify', + "apply_to": {"this_folder": True, "files": False, "sub_folders": False}, + 'validate_changes': 'error' + + } + error = "Error: suboptions 'rights' and 'advanced_rights' are mutually exclusive." + assert error in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + + del args['rights'] + args['apply_to'] = {"this_folder": False, "files": False, "sub_folders": False} + error = "Error: at least one suboption must be true for apply_to. Got: " + assert error in call_main(my_main, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['generic_error']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl', SRR['generic_error']), + ('PATCH', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/SERVER_CIFS_TE%5Cmohan11', SRR['generic_error']), + ('DELETE', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl/SERVER_CIFS_TE%5Cmohan11', SRR['generic_error']) + ]) + module_args = { + "advanced_rights": {"append_data": True}, + "apply_to": {"this_folder": True, "files": False, "sub_folders": False} + } + + acl_obj = create_module(my_module, DEFAULT_ARGS, module_args) + acl_obj.svm_uuid = "55bcb009" + assert 'Error fetching file security' in expect_and_capture_ansible_exception(acl_obj.get_file_security_permissions_acl, 'fail')['msg'] + assert 'Error creating file security' in expect_and_capture_ansible_exception(acl_obj.create_file_security_permissions_acl, 'fail')['msg'] + assert 'Error modifying file security' in expect_and_capture_ansible_exception(acl_obj.modify_file_security_permissions_acl, 'fail')['msg'] + assert 'Error deleting file security permissions' in expect_and_capture_ansible_exception(acl_obj.delete_file_security_permissions_acl, 'fail')['msg'] + assert 'Internal error - unexpected action bad_action' in expect_and_capture_ansible_exception(acl_obj.build_body, 'fail', 'bad_action')['msg'] + acl = build_acl('user') + acls = [acl, acl] + assert 'Error matching ACLs, found more than one match. Found' in expect_and_capture_ansible_exception(acl_obj.match_acl_with_acls, 'fail', + acl, acls)['msg'] + + +def test_create_file_directory_slag(): + ''' create slag acl and idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['non_acl']), + ('POST', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt/acl', SRR['success']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['slag_acl_same_user']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_id']), + ('GET', 'protocols/file-security/permissions/55bcb009/%2Fvol200%2FaNewFile.txt', SRR['slag_acl_same_user']) + ]) + module_args = { + 'access_control': 'slag', + 'access': 'access_deny', + 'advanced_rights': {'append_data': True}, + 'apply_to': {'files': True}, + 'acl_user': 'SERVER_CIFS_TE\\mohan11' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_validate_changes(): + """ verify nothing needs to be changed """ + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/file-security/permissions/None/%2Fvol200%2FaNewFile.txt', SRR['zero_records']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/file-security/permissions/None/%2Fvol200%2FaNewFile.txt', SRR['fd_acl_single_user_deny']), + ]) + args = { + "advanced_rights": {"append_data": True}, + 'apply_to': {'files': True}, + 'validate_changes': 'ignore', + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + assert my_obj.validate_changes('create', {}) is None + args = { + "advanced_rights": {"append_data": True}, + 'apply_to': {'files': True}, + 'validate_changes': 'error', + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + error = 'Error - create still required after create' + assert error in expect_and_capture_ansible_exception(my_obj.validate_changes, 'fail', 'create', {})['msg'] + args = { + 'access': 'access_deny', + 'advanced_rights': { + 'append_data': False, + }, + 'apply_to': {'this_folder': True}, + 'validate_changes': 'warn', + } + my_obj = create_module(my_module, DEFAULT_ARGS, args) + warning = "Error - modify: {'advanced_rights': {'append_data': False}} still required after {'a': 'b'}" + assert my_obj.validate_changes('create', {'a': 'b'}) is None + print_warnings() + assert_warning_was_raised(warning, partial_match=True) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_firewall_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_firewall_policy.py new file mode 100644 index 000000000..b23a897a3 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_firewall_policy.py @@ -0,0 +1,263 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_firewall_policy \ + import NetAppONTAPFirewallPolicy as fp_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.data = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'policy': + xml = self.build_policy_info(self.data) + if self.kind == 'config': + xml = self.build_firewall_config_info(self.data) + self.xml_out = xml + return xml + + @staticmethod + def build_policy_info(data): + ''' build xml data for net-firewall-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'net-firewall-policy-info': { + 'policy': data['policy'], + 'service': data['service'], + 'allow-list': [ + {'ip-and-mask': '1.2.3.0/24'} + ] + } + } + } + + xml.translate_struct(attributes) + return xml + + @staticmethod + def build_firewall_config_info(data): + ''' build xml data for net-firewall-config-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'attributes': { + 'net-firewall-config-info': { + 'is-enabled': 'true', + 'is-logging': 'false' + } + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_policy = { + 'policy': 'test', + 'service': 'none', + 'vserver': 'my_vserver', + 'allow_list': '1.2.3.0/24' + } + self.mock_config = { + 'node': 'test', + 'enable': 'enable', + 'logging': 'enable' + } + + def mock_policy_args(self): + return { + 'policy': self.mock_policy['policy'], + 'service': self.mock_policy['service'], + 'vserver': self.mock_policy['vserver'], + 'allow_list': [self.mock_policy['allow_list']], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def mock_config_args(self): + return { + 'node': self.mock_config['node'], + 'enable': self.mock_config['enable'], + 'logging': self.mock_config['logging'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_mock_object(self, kind=None): + """ + Helper method to return an na_ontap_firewall_policy object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_firewall_policy object + """ + obj = fp_module() + obj.autosupport_log = Mock(return_value=None) + if kind is None: + obj.server = MockONTAPConnection() + else: + mock_data = self.mock_config if kind == 'config' else self.mock_policy + obj.server = MockONTAPConnection(kind=kind, data=mock_data) + return obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + fp_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_helper_firewall_policy_attributes(self): + ''' helper returns dictionary with vserver, service and policy details ''' + data = self.mock_policy + set_module_args(self.mock_policy_args()) + result = self.get_mock_object('policy').firewall_policy_attributes() + del data['allow_list'] + assert data == result + + def test_helper_validate_ip_addresses_positive(self): + ''' test if helper validates if IP is a network address ''' + data = self.mock_policy_args() + data['allow_list'] = ['1.2.0.0/16', '1.2.3.0/24'] + set_module_args(data) + result = self.get_mock_object().validate_ip_addresses() + assert result is None + + def test_helper_validate_ip_addresses_negative(self): + ''' test if helper validates if IP is a network address ''' + data = self.mock_policy_args() + data['allow_list'] = ['1.2.0.10/16', '1.2.3.0/24'] + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_mock_object().validate_ip_addresses() + msg = 'Error: Invalid IP network value 1.2.0.10/16.' \ + ' Please specify a network address without host bits set: ' \ + '1.2.0.10/16 has host bits set.' + assert exc.value.args[0]['msg'] == msg + + def test_get_nonexistent_policy(self): + ''' Test if get_firewall_policy returns None for non-existent policy ''' + set_module_args(self.mock_policy_args()) + result = self.get_mock_object().get_firewall_policy() + assert result is None + + def test_get_existing_policy(self): + ''' Test if get_firewall_policy returns policy details for existing policy ''' + data = self.mock_policy_args() + set_module_args(data) + result = self.get_mock_object('policy').get_firewall_policy() + assert result['service'] == data['service'] + assert result['allow_list'] == ['1.2.3.0/24'] # from build_policy_info() + + def test_successful_create(self): + ''' Test successful create ''' + set_module_args(self.mock_policy_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object().apply() + assert exc.value.args[0]['changed'] + + def test_create_idempotency(self): + ''' Test create idempotency ''' + set_module_args(self.mock_policy_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('policy').apply() + assert not exc.value.args[0]['changed'] + + def test_successful_delete(self): + ''' Test delete existing job ''' + data = self.mock_policy_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('policy').apply() + assert exc.value.args[0]['changed'] + + def test_delete_idempotency(self): + ''' Test delete idempotency ''' + data = self.mock_policy_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object().apply() + assert not exc.value.args[0]['changed'] + + def test_successful_modify(self): + ''' Test successful modify allow_list ''' + data = self.mock_policy_args() + data['allow_list'] = ['1.2.0.0/16'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('policy').apply() + assert exc.value.args[0]['changed'] + + def test_successful_modify_mutiple_ips(self): + ''' Test successful modify allow_list ''' + data = self.mock_policy_args() + data['allow_list'] = ['1.2.0.0/16', '1.0.0.0/8'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('policy').apply() + assert exc.value.args[0]['changed'] + + def test_successful_modify_mutiple_ips_contain_existing(self): + ''' Test successful modify allow_list ''' + data = self.mock_policy_args() + data['allow_list'] = ['1.2.3.0/24', '1.0.0.0/8'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('policy').apply() + assert exc.value.args[0]['changed'] + + def test_get_nonexistent_config(self): + ''' Test if get_firewall_config returns None for non-existent node ''' + set_module_args(self.mock_config_args()) + result = self.get_mock_object().get_firewall_config_for_node() + assert result is None + + def test_get_existing_config(self): + ''' Test if get_firewall_config returns policy details for existing node ''' + data = self.mock_config_args() + set_module_args(data) + result = self.get_mock_object('config').get_firewall_config_for_node() + assert result['enable'] == 'enable' # from build_config_info() + assert result['logging'] == 'disable' # from build_config_info() + + def test_successful_modify_config(self): + ''' Test successful modify allow_list ''' + data = self.mock_config_args() + data['enable'] = 'disable' + data['logging'] = 'enable' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_mock_object('config').apply() + assert exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_firmware_upgrade.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_firmware_upgrade.py new file mode 100644 index 000000000..140b91cd7 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_firmware_upgrade.py @@ -0,0 +1,891 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_firmware_upgrade ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, call +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade\ + import NetAppONTAPFirmwareUpgrade as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def mock_warn(me, log): + print('WARNING', log) + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, parm1=None, parm2=None, parm3=None): + ''' save arguments ''' + self.type = kind + self.parm1 = parm1 + self.parm2 = parm2 + # self.parm3 = parm3 + self.xml_in = None + self.xml_out = None + self.firmware_type = 'None' + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + print('xml_in', xml.to_string()) + print('kind', self.type) + if self.type == 'firmware_upgrade': + xml = self.build_firmware_upgrade_info(self.parm1, self.parm2) + if self.type == 'acp': + xml = self.build_acp_firmware_info(self.firmware_type) + if self.type == 'disk_fw_info': + xml = self.build_disk_firmware_info(self.firmware_type) + if self.type == 'shelf_fw_info': + xml = self.build_shelf_firmware_info(self.firmware_type) + if self.type == 'firmware_download': + xml = self.build_system_cli_info(error=self.parm1) + if self.type == 'exception': + raise netapp_utils.zapi.NaApiError(self.parm1, self.parm2) + self.xml_out = xml + print('xml_out', xml.to_string()) + return xml + + @staticmethod + def build_firmware_upgrade_info(version, node): + ''' build xml data for service-processor firmware info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'num-records': 1, + 'attributes-list': {'service-processor-info': {'firmware-version': '3.4'}} + } + xml.translate_struct(data) + return xml + + @staticmethod + def build_acp_firmware_info(firmware_type): + ''' build xml data for acp firmware info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + # 'num-records': 1, + 'attributes-list': {'storage-shelf-acp-module': {'state': 'firmware_update_required'}} + } + xml.translate_struct(data) + return xml + + @staticmethod + def build_disk_firmware_info(firmware_type): + ''' build xml data for disk firmware info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'num-records': 1, + 'attributes-list': [{'storage-disk-info': {'disk-uid': '1', 'disk-inventory-info': {'firmware-revision': '1.2.3'}}}] + } + xml.translate_struct(data) + return xml + + @staticmethod + def build_shelf_firmware_info(firmware_type): + ''' build xml data for shelf firmware info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'num-records': 1, + 'attributes-list': [{'storage-shelf-info': {'shelf-modules': {'storage-shelf-module-info': {'module-id': '1', 'module-fw-revision': '1.2.3'}}}}] + } + xml.translate_struct(data) + return xml + + @staticmethod + def build_system_cli_info(error=None): + ''' build xml data for system-cli info ''' + if error is None: + # make it a string, to be able to compare easily + error = "" + xml = netapp_utils.zapi.NaElement('results') + output = "" if error == 'empty_output' else 'Download complete.' + data = { + 'cli-output': output, + 'cli-result-value': 1 + } + xml.translate_struct(data) + status = "failed" if error == 'status_failed' else "passed" + if error != 'no_status_attr': + xml.add_attr('status', status) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.use_vsim = False + + def set_default_args(self): + if self.use_vsim: + hostname = '10.10.10.10' + username = 'admin' + password = 'admin' + node = 'vsim1' + else: + hostname = 'hostname' + username = 'username' + password = 'password' + node = 'abc' + package = 'test1.zip' + force_disruptive_update = False + clear_logs = True + install_baseline_image = False + update_type = 'serial_full' + use_rest = 'never' + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'node': node, + 'package': package, + 'clear_logs': clear_logs, + 'install_baseline_image': install_baseline_image, + 'update_type': update_type, + 'https': 'true', + 'force_disruptive_update': force_disruptive_update, + 'use_rest': use_rest, + 'feature_flags': {'trace_apis': True} + }) + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_sp_firmware_get_called(self): + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'service-processor' + set_module_args(module_args) + my_obj = my_module() + my_obj.server = self.server + firmware_image_get = my_obj.firmware_image_get('node') + print('Info: test_firmware_upgrade_get: %s' % repr(firmware_image_get)) + assert firmware_image_get is None + + def test_negative_package_and_baseline_present(self): + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'service-processor' + module_args['package'] = 'test1.zip' + module_args['install_baseline_image'] = True + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(module_args) + my_module() + msg = 'With ZAPI and firmware_type set to service-processor: do not specify both package and install_baseline_image: true.' + print('info: ' + exc.value.args[0]['msg']) + assert exc.value.args[0]['msg'] == msg + + def test_negative_package_and_baseline_absent(self): + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'service-processor' + module_args.pop('package') + module_args['install_baseline_image'] = False + module_args['force_disruptive_update'] = True + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(module_args) + my_module() + msg = 'With ZAPI and firmware_type set to service-processor: specify at least one of package or install_baseline_image: true.' + print('info: ' + exc.value.args[0]['msg']) + assert exc.value.args[0]['msg'] == msg + + def test_ensure_acp_firmware_update_required_called(self): + ''' a test tp verify acp firmware upgrade is required or not ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'acp' + set_module_args(module_args) + my_obj = my_module() + # my_obj.server = self.server + my_obj.server = MockONTAPConnection(kind='acp') + acp_firmware_update_required = my_obj.acp_firmware_update_required() + print('Info: test_acp_firmware_upgrade_required_get: %s' % repr(acp_firmware_update_required)) + assert acp_firmware_update_required is True + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.sp_firmware_image_update') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.sp_firmware_image_update_progress_get') + def test_ensure_apply_for_firmware_upgrade_called(self, get_mock, upgrade_mock): + ''' updgrading firmware and checking idempotency ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package'] = 'test1.zip' + module_args['firmware_type'] = 'service-processor' + module_args['force_disruptive_update'] = True + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert not exc.value.args[0]['changed'] + if not self.use_vsim: + my_obj.server = MockONTAPConnection('firmware_upgrade', '3.5', 'true') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + upgrade_mock.assert_called_with() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.shelf_firmware_upgrade') + def test_shelf_firmware_upgrade(self, upgrade_mock): + ''' Test shelf firmware upgrade ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'shelf' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert not exc.value.args[0]['changed'] + assert not upgrade_mock.called + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.shelf_firmware_upgrade') + def test_shelf_firmware_upgrade_force(self, upgrade_mock): + ''' Test shelf firmware upgrade ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'shelf' + module_args['force_disruptive_update'] = True + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = self.server + upgrade_mock.return_value = True + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + assert upgrade_mock.called + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.shelf_firmware_upgrade') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.shelf_firmware_update_required') + def test_shelf_firmware_upgrade_force_update_required(self, update_required_mock, upgrade_mock): + ''' Test shelf firmware upgrade ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'shelf' + module_args['force_disruptive_update'] = True + module_args['shelf_module_fw'] = "version" + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = self.server + update_required_mock.return_value = True + upgrade_mock.return_value = True + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + assert upgrade_mock.called + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.acp_firmware_upgrade') + def test_acp_firmware_upgrade(self, upgrade_mock): + ''' Test ACP firmware upgrade ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'acp' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert not exc.value.args[0]['changed'] + assert not upgrade_mock.called + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.acp_firmware_upgrade') + def test_acp_firmware_upgrade_force(self, upgrade_mock): + ''' Test ACP firmware upgrade ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'acp' + module_args['force_disruptive_update'] = True + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection(kind='acp') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + assert upgrade_mock.called + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.disk_firmware_upgrade') + def test_disk_firmware_upgrade(self, upgrade_mock): + ''' Test disk firmware upgrade ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'disk' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert not exc.value.args[0]['changed'] + assert not upgrade_mock.called + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.disk_firmware_upgrade') + def test_disk_firmware_upgrade_force(self, upgrade_mock): + ''' Test disk firmware upgrade ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'disk' + module_args['force_disruptive_update'] = True + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + assert upgrade_mock.called + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.disk_firmware_upgrade') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_firmware_upgrade.NetAppONTAPFirmwareUpgrade.disk_firmware_update_required') + def test_disk_firmware_upgrade_force_update_required(self, update_required_mock, upgrade_mock): + ''' Test disk firmware upgrade ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['firmware_type'] = 'disk' + module_args['force_disruptive_update'] = True + module_args['disk_fw'] = "version" + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = self.server + update_required_mock.return_value = True + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: test_firmware_upgrade_apply: %s' % repr(exc.value)) + assert exc.value.args[0]['changed'] + assert upgrade_mock.called + + def test_acp_firmware_update_required(self): + ''' Test acp_firmware_update_required ''' + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('acp') + result = my_obj.acp_firmware_update_required() + assert result + + def test_acp_firmware_update_required_false(self): + ''' Test acp_firmware_update_required ''' + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection() + result = my_obj.acp_firmware_update_required() + assert not result + + def test_negative_acp_firmware_update_required(self): + ''' Test acp_firmware_update_required ''' + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('exception') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.acp_firmware_update_required() + msg = "Error fetching acp firmware details details: NetApp API failed. Reason - None:None" + assert msg in exc.value.args[0]['msg'] + + def test_disk_firmware_update_required(self): + ''' Test disk_firmware_update_required ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['disk_fw'] = '1.2.4' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('disk_fw_info') + result = my_obj.disk_firmware_update_required() + assert result + + def test_negative_disk_firmware_update_required(self): + ''' Test disk_firmware_update_required ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['disk_fw'] = '1.2.4' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('exception') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.disk_firmware_update_required() + msg = "Error fetching disk module firmware details: NetApp API failed. Reason - None:None" + assert msg in exc.value.args[0]['msg'] + + def test_shelf_firmware_update_required(self): + ''' Test shelf_firmware_update_required ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['shelf_module_fw'] = '1.2.4' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('shelf_fw_info') + result = my_obj.shelf_firmware_update_required() + assert result + + def test_negative_shelf_firmware_update_required(self): + ''' Test shelf_firmware_update_required ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['shelf_module_fw'] = '1.2.4' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('exception') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.shelf_firmware_update_required() + msg = "Error fetching shelf module firmware details: NetApp API failed. Reason - None:None" + assert msg in exc.value.args[0]['msg'] + + def test_firmware_download(self): + ''' Test firmware download ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package_url'] = 'dummy_url' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('firmware_download') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + msg = "Firmware download completed. Extra info: Download complete." + assert exc.value.args[0]['msg'] == msg + + def test_60(self): + ''' Test firmware download ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package_url'] = 'dummy_url' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('exception', 60, 'ZAPI timeout') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + msg = "Firmware download completed, slowly." + assert exc.value.args[0]['msg'] == msg + + def test_firmware_download_502(self): + ''' Test firmware download ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package_url'] = 'dummy_url' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('exception', 502, 'Bad GW') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + msg = "Firmware download still in progress." + assert exc.value.args[0]['msg'] == msg + + def test_firmware_download_502_as_error(self): + ''' Test firmware download ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package_url'] = 'dummy_url' + module_args['fail_on_502_error'] = True + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('exception', 502, 'Bad GW') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "NetApp API failed. Reason - 502:Bad GW" + assert msg in exc.value.args[0]['msg'] + + def test_firmware_download_no_num_error(self): + ''' Test firmware download ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package_url'] = 'dummy_url' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('exception', 'some error string', 'whatever') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "NetApp API failed. Reason - some error string:whatever" + assert msg in exc.value.args[0]['msg'] + + def test_firmware_download_no_status_attr(self): + ''' Test firmware download ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package_url'] = 'dummy_url' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('firmware_download', 'no_status_attr') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "unable to download package from dummy_url: 'status' attribute missing." + assert exc.value.args[0]['msg'].startswith(msg) + + def test_firmware_download_status_failed(self): + ''' Test firmware download ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package_url'] = 'dummy_url' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('firmware_download', 'status_failed') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "unable to download package from dummy_url: check 'status' value." + assert exc.value.args[0]['msg'].startswith(msg) + + def test_firmware_download_empty_output(self): + ''' Test firmware download ''' + module_args = {} + module_args.update(self.set_default_args()) + module_args['package_url'] = 'dummy_url' + set_module_args(module_args) + my_obj = my_module() + if not self.use_vsim: + my_obj.server = MockONTAPConnection('firmware_download', 'empty_output') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "unable to download package from dummy_url: check console permissions." + assert exc.value.args[0]['msg'].startswith(msg) + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, {'num_records': 0}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'uuid_record': (200, + {'records': [{"uuid": '1cd8a442-86d1-11e0-ae1c-123478563412'}]}, None), + 'nodes_record': (200, + {'records': [{"name": 'node1'}, {"name": 'node2'}]}, None), + 'net_routes_record': (200, + {'records': [{"destination": {"address": "176.0.0.0", + "netmask": "24", + "family": "ipv4"}, + "gateway": '10.193.72.1', + "uuid": '1cd8a442-86d1-11e0-ae1c-123478563412', + "svm": {"name": "test_vserver"}}]}, None), + 'modified_record': (200, + {'records': [{"destination": {"address": "0.0.0.0", + "netmask": "0", + "family": "ipv4"}, + "gateway": "10.193.72.1", + "uuid": '1cd8a442-86d1-11e0-ae1c-123478563412', + "svm": {"name": "test_vserver"}}]}, None), + 'sp_state_online': (200, + {'service_processor': {'state': 'online'}}, None), + 'sp_state_rebooting': (200, + {'service_processor': {'state': 'rebooting'}}, None), + 'unexpected_arg': (400, None, 'Unexpected argument "service_processor.action"'), +} + + +def set_default_module_args(use_rest='always'): + hostname = 'hostname' + username = 'username' + password = 'password' + use_rest = 'always' + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'https': 'true', + 'use_rest': use_rest, + 'package_url': 'https://download.site.com' + }) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_successfully_download(mock_request, patch_ansible): + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = False + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], # post download + SRR['is_rest'], + SRR['empty_good'], # post download + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + my_module().apply() + assert exc.value.args[0]['changed'] + print(mock_request.call_args) + json = {'url': 'https://download.site.com'} + expected = call('POST', 'cluster/software/download', None, json=json, headers=None, files=None) + assert mock_request.call_args == expected + data['server_username'] = 'user' + data['server_password'] = 'pass' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + my_module().apply() + print(mock_request.call_args) + json = {'url': 'https://download.site.com', 'username': 'user', 'password': 'pass'} + expected = call('POST', 'cluster/software/download', None, json=json, headers=None, files=None) + assert mock_request.call_args == expected + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_download(mock_request, patch_ansible): + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = False + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], # post download + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module().apply() + msg = 'Error downloading software: calling: cluster/software/download: got Expected error.' + assert msg in exc.value.args[0]['msg'] + + +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_successfully_reboot_sp_and_download(mock_request, dont_sleep, patch_ansible): + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = True + data['node'] = 'node4' + data['firmware_type'] = 'service-processor' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['uuid_record'], # get UUID + SRR['empty_good'], # patch reboot + SRR['empty_good'], # post download + SRR['sp_state_rebooting'], # get sp state + SRR['sp_state_rebooting'], # get sp state + SRR['sp_state_online'], # get sp state + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + my_module().apply() + assert exc.value.args[0]['changed'] + + +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_reboot_sp_and_download_bad_sp(mock_request, dont_sleep, patch_ansible): + """fail to read SP state""" + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = True + data['node'] = 'node4' + data['firmware_type'] = 'service-processor' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['uuid_record'], # get UUID + SRR['empty_good'], # patch reboot + SRR['empty_good'], # post download + SRR['sp_state_rebooting'], # get sp state + SRR['sp_state_rebooting'], # get sp state + SRR['generic_error'], # get sp state + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module().apply() + msg = 'Error getting node SP state:' + assert msg in exc.value.args[0]['msg'] + + +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_reboot_sp_and_download_sp_timeout(mock_request, dont_sleep, patch_ansible): + """fail to read SP state""" + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = True + data['node'] = 'node4' + data['firmware_type'] = 'service-processor' + set_module_args(data) + responses = [ + SRR['is_rest'], + SRR['uuid_record'], # get UUID + SRR['empty_good'], # patch reboot + SRR['empty_good'], # post download + ] + # 20 retries + responses.extend([SRR['sp_state_rebooting']] * 20) + responses.append(SRR['sp_state_online']) + responses.append(SRR['end_of_sequence']) + mock_request.side_effect = responses + with pytest.raises(AnsibleExitJson) as exc: + my_module().apply() + # msg = 'Error getting node SP state:' + # assert msg in exc.value.args[0]['msg'] + print('RETRIES', exc.value.args[0]) + + +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_successfully_reboot_sp_and_download_cli(mock_request, dont_sleep, patch_ansible): + ''' switch back to REST CLI for reboot ''' + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = True + data['node'] = 'node4' + data['firmware_type'] = 'service-processor' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['uuid_record'], # get UUID + SRR['unexpected_arg'], # patch reboot + SRR['empty_good'], # REST CLI reboot + SRR['empty_good'], # post download + SRR['sp_state_rebooting'], # get sp state + SRR['sp_state_rebooting'], # get sp state + SRR['sp_state_online'], # get sp state + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + my_module().apply() + assert exc.value.args[0]['changed'] + + +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_reboot_sp_and_download_cli(mock_request, dont_sleep, patch_ansible): + ''' switch back to REST CLI for reboot ''' + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = True + data['node'] = 'node4' + data['firmware_type'] = 'service-processor' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['uuid_record'], # get UUID + SRR['unexpected_arg'], # patch reboot + SRR['generic_error'], # REST CLI reboot + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module().apply() + msg = 'Error rebooting node SP: reboot_sp requires ONTAP 9.10.1 or newer, falling back to CLI passthrough failed' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_reboot_sp_and_download_uuid_error(mock_request, patch_ansible): + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = True + data['node'] = 'node4' + data['firmware_type'] = 'service-processor' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], # get UUID + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module().apply() + msg = 'Error reading node UUID: calling: cluster/nodes: got Expected error.' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_reboot_sp_and_download_node_not_found(mock_request, patch_ansible): + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = True + data['node'] = 'node4' + data['firmware_type'] = 'service-processor' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], # get UUID + SRR['nodes_record'], # get nodes + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module().apply() + msg = 'Error: node not found node4, current nodes: node1, node2.' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_reboot_sp_and_download_nodes_get_error(mock_request, patch_ansible): + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['reboot_sp'] = True + data['node'] = 'node4' + data['firmware_type'] = 'service-processor' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], # get UUID + SRR['generic_error'], # get nodes + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module().apply() + msg = 'Error reading nodes: calling: cluster/nodes: got Expected error.' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_unsupported_option_with_rest(mock_request, patch_ansible): + data = set_default_module_args(use_rest='always') + data['state'] = 'present' + data['clear_logs'] = False + data['node'] = 'node4' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module().apply() + msg = "REST API currently does not support 'clear_logs'" + assert msg in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_flexcache.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_flexcache.py new file mode 100644 index 000000000..07e01940a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_flexcache.py @@ -0,0 +1,838 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP FlexCache Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, build_zapi_error, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_warning_was_raised, call_main, create_module, expect_and_capture_ansible_exception, patch_ansible, print_warnings + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_flexcache import NetAppONTAPFlexCache as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +flexcache_info = { + 'vserver': 'vserver', + 'origin-vserver': 'ovserver', + 'origin-volume': 'ovolume', + 'origin-cluster': 'ocluster', + 'volume': 'flexcache_volume', +} + +flexcache_get_info = { + 'attributes-list': [{ + 'flexcache-info': flexcache_info + }] +} + +flexcache_get_info_double = { + 'attributes-list': [ + { + 'flexcache-info': flexcache_info + }, + { + 'flexcache-info': flexcache_info + } + ] +} + + +def results_info(status): + return { + 'result-status': status, + 'result-jobid': 'job12345', + } + + +def job_info(state, error): + return { + 'num-records': 1, + 'attributes': { + 'job-info': { + 'job-state': state, + 'job-progress': 'progress', + 'job-completion': error, + } + } + } + + +ZRR = zapi_responses({ + 'flexcache_get_info': build_zapi_response(flexcache_get_info, 1), + 'flexcache_get_info_double': build_zapi_response(flexcache_get_info_double, 2), + 'job_running': build_zapi_response(job_info('running', None)), + 'job_success': build_zapi_response(job_info('success', None)), + 'job_error': build_zapi_response(job_info('failure', 'failure')), + 'job_error_no_completion': build_zapi_response(job_info('failure', None)), + 'job_other': build_zapi_response(job_info('other', 'other')), + 'result_async': build_zapi_response(results_info('in_progress')), + 'result_error': build_zapi_response(results_info('whatever')), + 'error_160': build_zapi_error(160, 'Volume volume on Vserver ansibleSVM must be unmounted before being taken offline or restricted'), + 'error_13001': build_zapi_error(13001, 'Volume volume in Vserver ansibleSVM must be offline to be deleted'), + 'error_15661': build_zapi_error(15661, 'Job not found'), + 'error_size': build_zapi_error('size', 'Size "50MB" ("52428800B") is too small. Minimum size is "80MB" ("83886080B")'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'volume': 'flexcache_volume', + 'vserver': 'vserver', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never' + } + error = 'missing required arguments:' + assert error in call_main(my_main, {}, module_args, fail=True)['msg'] + + +def test_missing_parameters(): + ''' fail if origin volume and origin verser are missing ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + } + error = 'Missing parameters:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_missing_parameter(): + ''' fail if origin verser parameter is missing ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + 'origin_volume': 'origin_volume', + } + error = 'Missing parameter: origin_vserver' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_get_flexcache(): + ''' get flexcache info ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ]) + module_args = { + 'use_rest': 'never', + 'origin_volume': 'origin_volume', + 'origin_cluster': 'origin_cluster', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + info = my_obj.flexcache_get() + assert info + assert 'origin_cluster' in info + + +def test_get_flexcache_double(): + ''' get flexcache info returns 2 entries! ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info_double']), + ]) + module_args = { + 'use_rest': 'never', + 'origin_volume': 'origin_volume', + + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = 'Error fetching FlexCache info: Multiple records found for %s:' % DEFAULT_ARGS['volume'] + assert error in expect_and_capture_ansible_exception(my_obj.flexcache_get, 'fail')['msg'] + + +def test_create_flexcache(): + ''' create flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['job_success']), + ]) + module_args = { + 'use_rest': 'never', + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_flexcach_no_wait(): + ''' create flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['result_async']), + ]) + module_args = { + 'use_rest': 'never', + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'time_out': 0 + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_flexcache(): + ''' create flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['result_error']), + # 2nd run + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['error']), + # 3rd run + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['job_error']), + ]) + module_args = { + 'use_rest': 'never', + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + error = 'Unexpected error when creating flexcache: results is:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + error = zapi_error_message('Error fetching job info') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + error = 'Error when creating flexcache' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_create_flexcache_idempotent(): + ''' create flexcache - already exists ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ]) + module_args = { + 'use_rest': 'never', + 'aggr_list': 'aggr1', + 'origin_volume': 'ovolume', + 'origin_vserver': 'ovserver', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_flexcache_autoprovision(): + ''' create flexcache with autoprovision''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['job_success']), + ]) + module_args = { + 'use_rest': 'never', + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'auto_provision_as': 'flexgroup', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_flexcache_autoprovision_idempotent(): + ''' create flexcache with autoprovision - already exists ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ]) + module_args = { + 'use_rest': 'never', + 'origin_volume': 'ovolume', + 'origin_vserver': 'ovserver', + 'auto_provision_as': 'flexgroup', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_flexcache_multiplier(): + ''' create flexcache with aggregate multiplier''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['job_success']), + ]) + module_args = { + 'use_rest': 'never', + 'size': '90', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'aggr_list_multiplier': 2, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_flexcache_multiplier_idempotent(): + ''' create flexcache with aggregate multiplier - already exists ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ]) + module_args = { + 'use_rest': 'never', + 'aggr_list': 'aggr1', + 'origin_volume': 'ovolume', + 'origin_vserver': 'ovserver', + 'aggr_list_multiplier': 2, + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_flexcache_exists_no_force(): + ''' delete flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ('ZAPI', 'flexcache-destroy-async', ZRR['error_13001']), + ]) + module_args = { + 'use_rest': 'never', + 'state': 'absent' + } + error = zapi_error_message('Error deleting FlexCache', 13001, 'Volume volume in Vserver ansibleSVM must be offline to be deleted') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_delete_flexcache_exists_with_force(): + ''' delete flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ('ZAPI', 'volume-offline', ZRR['success']), + ('ZAPI', 'flexcache-destroy-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['job_success']), + ]) + module_args = { + 'use_rest': 'never', + 'force_offline': 'true', + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_flexcache_exists_with_force_no_wait(): + ''' delete flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ('ZAPI', 'volume-offline', ZRR['success']), + ('ZAPI', 'flexcache-destroy-async', ZRR['result_async']), + ]) + module_args = { + 'use_rest': 'never', + 'force_offline': 'true', + 'time_out': 0, + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_flexcache_exists_junctionpath_no_force(): + ''' delete flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ('ZAPI', 'volume-offline', ZRR['success']), + ('ZAPI', 'flexcache-destroy-async', ZRR['error_160']), + ]) + module_args = { + 'use_rest': 'never', + 'force_offline': 'true', + 'junction_path': 'jpath', + 'state': 'absent' + } + error = zapi_error_message('Error deleting FlexCache', 160, + 'Volume volume on Vserver ansibleSVM must be unmounted before being taken offline or restricted') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_delete_flexcache_exists_junctionpath_with_force(): + ''' delete flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ('ZAPI', 'volume-unmount', ZRR['success']), + ('ZAPI', 'volume-offline', ZRR['success']), + ('ZAPI', 'flexcache-destroy-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['job_success']), + ]) + module_args = { + 'use_rest': 'never', + 'force_offline': 'true', + 'junction_path': 'jpath', + 'force_unmount': 'true', + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_flexcache_not_exist(): + ''' delete flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + 'state': 'absent' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_delete_flexcache_exists_with_force(): + ''' delete flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ('ZAPI', 'volume-offline', ZRR['success']), + ('ZAPI', 'flexcache-destroy-async', ZRR['result_error']), + # 2nd run + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ('ZAPI', 'volume-offline', ZRR['success']), + ('ZAPI', 'flexcache-destroy-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['error']), + # 3rd run + ('ZAPI', 'flexcache-get-iter', ZRR['flexcache_get_info']), + ('ZAPI', 'volume-offline', ZRR['success']), + ('ZAPI', 'flexcache-destroy-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['job_error']), + ]) + module_args = { + 'use_rest': 'never', + 'force_offline': 'true', + 'state': 'absent' + } + error = 'Unexpected error when deleting flexcache: results is:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + error = zapi_error_message('Error fetching job info') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + error = 'Error when deleting flexcache' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_create_flexcache_size_error(): + ''' create flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['error_size']), + ]) + module_args = { + 'use_rest': 'never', + 'size': '50', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + error = zapi_error_message('Error creating FlexCache', 'size', 'Size "50MB" ("52428800B") is too small. Minimum size is "80MB" ("83886080B")') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_create_flexcache_time_out(dont_sleep): + ''' create flexcache ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['no_records']), + ('ZAPI', 'flexcache-create-async', ZRR['result_async']), + ('ZAPI', 'job-get', ZRR['job_running']), + ]) + module_args = { + 'use_rest': 'never', + 'size': '50', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'time_out': '2', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = 'Error when creating flexcache: job completion exceeded expected timer of: 2 seconds' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_error_zapi(): + ''' error in ZAPI calls ''' + register_responses([ + ('ZAPI', 'flexcache-get-iter', ZRR['error']), + ('ZAPI', 'volume-offline', ZRR['error']), + ('ZAPI', 'volume-unmount', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = zapi_error_message('Error fetching FlexCache info') + assert error in expect_and_capture_ansible_exception(my_obj.flexcache_get, 'fail')['msg'] + error = zapi_error_message('Error offlining FlexCache volume') + assert error in expect_and_capture_ansible_exception(my_obj.volume_offline, 'fail', None)['msg'] + error = zapi_error_message('Error unmounting FlexCache volume') + assert error in expect_and_capture_ansible_exception(my_obj.volume_unmount, 'fail', None)['msg'] + + +def test_check_job_status(): + ''' check_job_status ''' + register_responses([ + # job not found + ('ZAPI', 'job-get', ZRR['error_15661']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ('ZAPI', 'job-get', ZRR['error_15661']), + # cserver job not found + ('ZAPI', 'job-get', ZRR['error_15661']), + ('ZAPI', 'vserver-get-iter', ZRR['cserver']), + ('ZAPI', 'job-get', ZRR['error_15661']), + # missing job-completion + ('ZAPI', 'job-get', ZRR['job_error_no_completion']), + # bad status + ('ZAPI', 'job-get', ZRR['job_other']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + # error = zapi_error_message('Error fetching FlexCache info') + error = 'cannot locate job with id: 1' + assert error in my_obj.check_job_status('1') + assert error in my_obj.check_job_status('1') + assert 'progress' in my_obj.check_job_status('1') + error = 'Unexpected job status in:' + assert error in expect_and_capture_ansible_exception(my_obj.check_job_status, 'fail', '1')['msg'] + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'one_flexcache_record': (200, dict(records=[ + dict(uuid='a1b2c3', + name='flexcache_volume', + svm=dict(name='vserver'), + ) + ], num_records=1), None), + 'one_flexcache_record_with_path': (200, dict(records=[ + dict(uuid='a1b2c3', + name='flexcache_volume', + svm=dict(name='vserver'), + path='path' + ) + ], num_records=1), None), +}) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + module_args = { + "use_rest": "never" + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_missing_arguments(): + ''' create flexcache ''' + register_responses([ + + ]) + args = dict(DEFAULT_ARGS) + del args['hostname'] + module_args = { + 'use_rest': 'always', + } + error = 'missing required arguments: hostname' + assert error in call_main(my_main, args, module_args, fail=True)['msg'] + + +def test_rest_create(): + ''' create flexcache ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['zero_record']), + ('POST', 'storage/flexcache/flexcaches', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'size': '50', # 80MB minimum + 'size_unit': 'mb', # 80MB minimum + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'origin_cluster': 'ocluster', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_create_no_action(): + ''' create flexcache idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ]) + module_args = { + 'use_rest': 'always', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_delete_no_action(): + ''' delete flexcache ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['zero_record']), + ]) + module_args = { + 'use_rest': 'always', + 'state': 'absent' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_delete(): + ''' delete flexcache ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ('DELETE', 'storage/flexcache/flexcaches/a1b2c3', SRR['empty_good']), + ]) + module_args = { + 'use_rest': 'always', + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_delete_with_force(): + ''' delete flexcache, since there is no path, unmount is not called ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ('DELETE', 'storage/flexcache/flexcaches/a1b2c3', SRR['empty_good']), + ]) + module_args = { + 'use_rest': 'always', + 'force_unmount': True, + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_delete_with_force_and_path(): + ''' delete flexcache with unmount ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record_with_path']), + ('PATCH', 'storage/volumes/a1b2c3', SRR['empty_good']), + ('DELETE', 'storage/flexcache/flexcaches/a1b2c3', SRR['empty_good']), + ]) + module_args = { + 'use_rest': 'always', + 'force_unmount': True, + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_delete_with_force2_and_path(): + ''' delete flexcache with unmount and offline''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record_with_path']), + ('PATCH', 'storage/volumes/a1b2c3', SRR['empty_good']), + ('PATCH', 'storage/volumes/a1b2c3', SRR['empty_good']), + ('DELETE', 'storage/flexcache/flexcaches/a1b2c3', SRR['empty_good']), + ]) + module_args = { + 'use_rest': 'always', + 'force_offline': True, + 'force_unmount': True, + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_modify_prepopulate_no_action(): + ''' modify flexcache ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ]) + module_args = { + 'use_rest': 'always', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'prepopulate': { + 'dir_paths': ['/'], + 'force_prepopulate_if_already_created': False + } + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_modify_prepopulate(): + ''' modify flexcache ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ('PATCH', 'storage/flexcache/flexcaches/a1b2c3', SRR['empty_good']), + ]) + module_args = { + 'use_rest': 'always', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'prepopulate': { + 'dir_paths': ['/'], + 'force_prepopulate_if_already_created': True + } + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_modify_prepopulate_default(): + ''' modify flexcache ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ('PATCH', 'storage/flexcache/flexcaches/a1b2c3', SRR['empty_good']), + ]) + module_args = { + 'use_rest': 'always', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'prepopulate': { + 'dir_paths': ['/'], + } + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_modify_prepopulate_and_mount(): + ''' modify flexcache ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ('PATCH', 'storage/volumes/a1b2c3', SRR['empty_good']), + ('PATCH', 'storage/flexcache/flexcaches/a1b2c3', SRR['empty_good']), + ]) + module_args = { + 'use_rest': 'always', + 'aggr_list': 'aggr1', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'prepopulate': { + 'dir_paths': ['/'], + }, + 'path': '/mount_path' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_modify(): + ''' create flexcache idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ]) + module_args = { + 'use_rest': 'always', + 'aggr_list': 'aggr1', + 'volume': 'flexcache_volume2', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + } + error = 'FlexCache properties cannot be modified by this module. modify:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_warn_prepopulate(): + ''' create flexcache idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/flexcache/flexcaches', SRR['one_flexcache_record']), + ('PATCH', 'storage/volumes/a1b2c3', SRR['success']), + ('PATCH', 'storage/flexcache/flexcaches/a1b2c3', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'aggr_list': 'aggr1', + 'volume': 'flexcache_volume', + 'origin_volume': 'fc_vol_origin', + 'origin_vserver': 'ansibleSVM', + 'prepopulate': { + 'dir_paths': ['/'], + 'force_prepopulate_if_already_created': True + }, + 'junction_path': '' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised('na_ontap_flexcache is not idempotent when prepopulate is present and force_prepopulate_if_already_created=true') + assert_warning_was_raised('prepopulate requires the FlexCache volume to be mounted') + + +def test_error_missing_uuid(): + module_args = { + 'use_rest': 'akway', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + current = {} + error_template = 'Error in %s: Error, no uuid in current: {}' + error = error_template % 'rest_offline_volume' + assert error in expect_and_capture_ansible_exception(my_obj.rest_offline_volume, 'fail', current)['msg'] + error = error_template % 'rest_mount_volume' + assert error in expect_and_capture_ansible_exception(my_obj.rest_mount_volume, 'fail', current, 'path')['msg'] + error = error_template % 'flexcache_rest_delete' + assert error in expect_and_capture_ansible_exception(my_obj.flexcache_rest_delete, 'fail', current)['msg'] + + +def test_prepopulate_option_checks(): + ''' create flexcache idempotent ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always', + 'prepopulate': { + 'dir_paths': ['/'], + 'force_prepopulate_if_already_created': True, + 'exclude_dir_paths': ['/'] + }, + } + error = 'Error: using prepopulate requires ONTAP 9.8 or later and REST must be enabled - ONTAP version: 9.7.0.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + error = 'Error: using prepopulate: exclude_dir_paths requires ONTAP 9.9 or later and REST must be enabled - ONTAP version: 9.8.0.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_event.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_event.py new file mode 100644 index 000000000..a679f9ded --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_event.py @@ -0,0 +1,338 @@ +''' unit tests ONTAP Ansible module: na_ontap_fpolicy_event ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_event \ + import NetAppOntapFpolicyEvent as fpolicy_event_module # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {"num_records": 0}, None), + 'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'fpolicy_event_record': (200, { + "num_records": 1, + "records": [{ + 'svm': {'uuid': '3b21372b-64ae-11eb-8c0e-0050568176ec'}, + 'name': 'my_event2', + 'volume_monitoring': False + }] + }, None), + 'vserver_uuid_record': (200, { + 'records': [{ + 'uuid': '3b21372b-64ae-11eb-8c0e-0050568176ec' + }] + }, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'fpolicy_event': + xml = self.build_fpolicy_event_info() + elif self.type == 'fpolicy_event_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_fpolicy_event_info(): + ''' build xml data for fpolicy-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': { + 'fpolicy-event-options-config': { + "event-name": "my_event2", + "vserver": "svm1", + 'volume-operation': "false" + } + } + } + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self, use_rest=None): + if self.onbox: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + vserver = 'svm1' + name = 'my_event2' + volume_monitoring = False + + else: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + vserver = 'svm1' + name = 'my_event2' + volume_monitoring = False + + args = dict({ + 'state': 'present', + 'hostname': hostname, + 'username': username, + 'password': password, + 'vserver': vserver, + 'name': name, + 'volume_monitoring': volume_monitoring + }) + + if use_rest is not None: + args['use_rest'] = use_rest + + return args + + @staticmethod + def get_fpolicy_event_mock_object(cx_type='zapi', kind=None): + fpolicy_event_obj = fpolicy_event_module() + if cx_type == 'zapi': + if kind is None: + fpolicy_event_obj.server = MockONTAPConnection() + else: + fpolicy_event_obj.server = MockONTAPConnection(kind=kind) + return fpolicy_event_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + fpolicy_event_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called(self): + ''' test get_fpolicy_event for non-existent config''' + set_module_args(self.set_default_args(use_rest='Never')) + print('starting') + my_obj = fpolicy_event_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = self.server + assert my_obj.get_fpolicy_event is not None + + def test_ensure_get_called_existing(self): + ''' test get_fpolicy_event_config for existing config''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = fpolicy_event_module() + my_obj.server = MockONTAPConnection(kind='fpolicy_event') + assert my_obj.get_fpolicy_event() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_event.NetAppOntapFpolicyEvent.create_fpolicy_event') + def test_successful_create(self, create_fpolicy_event): + ''' creating fpolicy_event and test idempotency ''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = fpolicy_event_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + create_fpolicy_event.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = fpolicy_event_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_event') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_event.NetAppOntapFpolicyEvent.delete_fpolicy_event') + def test_successful_delete(self, delete_fpolicy_event): + ''' delete fpolicy_event and test idempotency ''' + data = self.set_default_args(use_rest='Never') + data['state'] = 'absent' + set_module_args(data) + my_obj = fpolicy_event_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_event') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # to reset na_helper from remembering the previous 'changed' value + my_obj = fpolicy_event_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_event.NetAppOntapFpolicyEvent.modify_fpolicy_event') + def test_successful_modify(self, modify_fpolicy_event): + ''' modifying fpolicy_event config and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['volume_monitoring'] = True + set_module_args(data) + my_obj = fpolicy_event_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_event') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # to reset na_helper from remembering the previous 'changed' value + data['volume_monitoring'] = False + set_module_args(data) + my_obj = fpolicy_event_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_event') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + data = self.set_default_args(use_rest='Never') + set_module_args(data) + my_obj = fpolicy_event_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_event_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_fpolicy_event() + assert 'Error creating fPolicy policy event ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_fpolicy_event() + assert 'Error deleting fPolicy policy event ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_fpolicy_event(modify={}) + assert 'Error modifying fPolicy policy event ' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_fpolicy_event_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['msg'] == SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_create_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['vserver_uuid_record'], + SRR['empty_good'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_event_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_create_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['vserver_uuid_record'], + SRR['fpolicy_event_record'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_event_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_delete_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['vserver_uuid_record'], + SRR['fpolicy_event_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_event_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_delete_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['vserver_uuid_record'], + SRR['empty_good'], # get + SRR['empty_good'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_event_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_modify_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'present' + data['volume_monitoring'] = True + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['vserver_uuid_record'], + SRR['fpolicy_event_record'], # get + SRR['empty_good'], # no response for modify + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_event_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_modify_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'present' + data['volume_monitoring'] = False + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['vserver_uuid_record'], + SRR['fpolicy_event_record'], # get + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_event_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_ext_engine.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_ext_engine.py new file mode 100644 index 000000000..c2304876c --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_ext_engine.py @@ -0,0 +1,395 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP fpolicy ext engine Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_ext_engine \ + import NetAppOntapFpolicyExtEngine as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'fpolicy_ext_engine': + xml = self.build_fpolicy_ext_engine_info() + elif self.type == 'fpolicy_ext_engine_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_fpolicy_ext_engine_info(): + ''' build xml data for fpolicy-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': { + 'fpolicy-external-engine-info': { + 'vserver': 'svm1', + 'engine-name': 'engine1', + 'primary-servers': [ + {'ip-address': '10.11.12.13'} + ], + 'port-number': '8787', + 'extern-engine-type': 'asynchronous', + 'ssl-option': 'no_auth' + } + } + } + xml.translate_struct(data) + return xml + + +def default_args(): + args = { + 'vserver': 'svm1', + 'name': 'engine1', + 'primary_servers': '10.11.12.13', + 'port': 8787, + 'extern_engine_type': 'asynchronous', + 'ssl_option': 'no_auth', + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'one_fpolicy_ext_engine_record': (200, { + "records": [{ + 'engine-name': 'engine1', + 'vserver': 'svm1', + 'primary-servers': ['10.11.12.13'], + 'port': 8787, + 'extern-engine-type': 'asynchronous', + 'ssl-option': 'no-auth' + }], + 'num_records': 1 + }, None) + +} + + +def get_fpolicy_ext_engine_mock_object(cx_type='zapi', kind=None): + fpolicy_ext_engine_obj = my_module() + if cx_type == 'zapi': + if kind is None: + fpolicy_ext_engine_obj.server = MockONTAPConnection() + else: + fpolicy_ext_engine_obj.server = MockONTAPConnection(kind=kind) + return fpolicy_ext_engine_obj + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_ensure_get_called(patch_ansible): + ''' test get_fpolicy_ext_engine for non-existent engine''' + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + print('starting') + my_obj = my_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = MockONTAPConnection() + assert my_obj.get_fpolicy_ext_engine is not None + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' create fpolicy ext engine ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_ext_engine.NetAppOntapFpolicyExtEngine.create_fpolicy_ext_engine') +def test_successful_create(self, patch_ansible): + ''' creating fpolicy_ext_engine and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection() + with patch.object(my_module, 'create_fpolicy_ext_engine', wraps=my_obj.create_fpolicy_ext_engine) as mock_create: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_create.assert_called_with() + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_ext_engine') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_ext_engine.NetAppOntapFpolicyExtEngine.delete_fpolicy_ext_engine') +def test_successful_delete(self, patch_ansible): + ''' delete fpolicy_ext_engine and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['state'] = 'absent' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_ext_engine') + with patch.object(my_module, 'delete_fpolicy_ext_engine', wraps=my_obj.delete_fpolicy_ext_engine) as mock_delete: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_delete.assert_called_with() + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + args['state'] = 'absent' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_ext_engine.NetAppOntapFpolicyExtEngine.modify_fpolicy_ext_engine') +def test_successful_modify(self, patch_ansible): + ''' modifying fpolicy_ext_engine and testing idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['port'] = '9999' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_ext_engine') + with patch.object(my_module, 'modify_fpolicy_ext_engine', wraps=my_obj.modify_fpolicy_ext_engine) as mock_modify: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Modify: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_modify.assert_called_with({'port': 9999}) + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_ext_engine') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Modify: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +def test_if_all_methods_catch_exception(patch_ansible): + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_ext_engine_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_fpolicy_ext_engine() + assert 'Error creating fPolicy external engine ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_fpolicy_ext_engine() + assert 'Error deleting fPolicy external engine ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_fpolicy_ext_engine(modify={}) + assert 'Error modifying fPolicy external engine ' in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' create fpolicy ext engine ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' create fpolicy ext engine idempotent ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_ext_engine_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_delete_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' delete fpolicy ext engine ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_delete(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' delete fpolicy ext engine ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_ext_engine_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' modify fpolicy ext engine ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_ext_engine_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify_prepopulate(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' modify fpolicy ext engine ''' + args = dict(default_args()) + args['port'] = 9999 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_ext_engine_record'], # get + SRR['empty_good'], # patch + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_delete_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' delete fpolicy ext engine ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify_prepopulate(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' delete fpolicy ext engine ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_ext_engine_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_policy.py new file mode 100644 index 000000000..fe065af33 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_policy.py @@ -0,0 +1,339 @@ +''' unit tests ONTAP Ansible module: na_ontap_fpolicy_policy ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_policy \ + import NetAppOntapFpolicyPolicy as fpolicy_policy_module # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'fpolicy_policy_record': (200, { + "records": [{ + "vserver": "svm1", + "policy_name": "policy1", + "events": ['my_event'], + "engine": "native", + "is_mandatory": False, + "allow_privileged_access": False, + "is_passthrough_read_enabled": False + }] + }, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'fpolicy_policy': + xml = self.build_fpolicy_policy_info() + elif self.type == 'fpolicy_policy_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_fpolicy_policy_info(): + ''' build xml data for fpolicy-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': { + 'fpolicy-policy-info': { + "vserver": "svm1", + "policy-name": "policy1", + "events": [ + {'event-name': 'my_event'} + ], + "engine-name": "native", + "is-mandatory": "False", + "allow-privileged-access": "False", + "is-passthrough-read-enabled": "False" + } + } + } + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self, use_rest=None): + if self.onbox: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + vserver = 'svm1' + name = 'policy1' + events = 'my_event' + is_mandatory = False + + else: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + vserver = 'svm1' + name = 'policy1' + events = 'my_event' + is_mandatory = False + + args = dict({ + 'state': 'present', + 'hostname': hostname, + 'username': username, + 'password': password, + 'vserver': vserver, + 'name': name, + 'events': events, + 'is_mandatory': is_mandatory + }) + + if use_rest is not None: + args['use_rest'] = use_rest + + return args + + @staticmethod + def get_fpolicy_policy_mock_object(cx_type='zapi', kind=None): + fpolicy_policy_obj = fpolicy_policy_module() + if cx_type == 'zapi': + if kind is None: + fpolicy_policy_obj.server = MockONTAPConnection() + else: + fpolicy_policy_obj.server = MockONTAPConnection(kind=kind) + return fpolicy_policy_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + fpolicy_policy_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called(self): + ''' test get_fpolicy_policy for non-existent config''' + set_module_args(self.set_default_args(use_rest='Never')) + print('starting') + my_obj = fpolicy_policy_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = self.server + assert my_obj.get_fpolicy_policy is not None + + def test_ensure_get_called_existing(self): + ''' test get_fpolicy_policy_config for existing config''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = fpolicy_policy_module() + my_obj.server = MockONTAPConnection(kind='fpolicy_policy') + assert my_obj.get_fpolicy_policy() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_policy.NetAppOntapFpolicyPolicy.create_fpolicy_policy') + def test_successful_create(self, create_fpolicy_policy): + ''' creating fpolicy_policy and test idempotency ''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = fpolicy_policy_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + create_fpolicy_policy.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = fpolicy_policy_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_policy.NetAppOntapFpolicyPolicy.delete_fpolicy_policy') + def test_successful_delete(self, delete_fpolicy_policy): + ''' delete fpolicy_policy and test idempotency ''' + data = self.set_default_args(use_rest='Never') + data['state'] = 'absent' + set_module_args(data) + my_obj = fpolicy_policy_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # to reset na_helper from remembering the previous 'changed' value + my_obj = fpolicy_policy_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_policy.NetAppOntapFpolicyPolicy.modify_fpolicy_policy') + def test_successful_modify(self, modify_fpolicy_policy): + ''' modifying fpolicy_policy config and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['is_mandatory'] = True + set_module_args(data) + my_obj = fpolicy_policy_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # to reset na_helper from remembering the previous 'changed' value + data['is_mandatory'] = False + set_module_args(data) + my_obj = fpolicy_policy_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + data = self.set_default_args(use_rest='Never') + set_module_args(data) + my_obj = fpolicy_policy_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('fpolicy_policy_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_fpolicy_policy() + assert 'Error creating fPolicy policy ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_fpolicy_policy() + assert 'Error deleting fPolicy policy ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_fpolicy_policy(modify={}) + assert 'Error modifying fPolicy policy ' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_fpolicy_policy_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['msg'] == SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_create_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_policy_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_create_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['fpolicy_policy_record'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_policy_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_delete_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['fpolicy_policy_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_policy_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_delete_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_policy_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_modify_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'present' + data['is_mandatory'] = 'True' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['fpolicy_policy_record'], # get + SRR['empty_good'], # no response for modify + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_policy_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_modify_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'present' + data['is_mandatory'] = False + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['fpolicy_policy_record'], # get + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_fpolicy_policy_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_scope.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_scope.py new file mode 100644 index 000000000..b09ab26ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_scope.py @@ -0,0 +1,351 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP fpolicy scope Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_scope \ + import NetAppOntapFpolicyScope as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'fpolicy_scope': + xml = self.build_fpolicy_scope_info() + elif self.type == 'fpolicy_scope_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_fpolicy_scope_info(): + ''' build xml data for fpolicy-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': { + 'fpolicy-scope-config': { + 'vserver': 'svm1', + 'policy-name': 'policy1', + 'export-policies-to-exclude': [ + {'string': 'export1'} + ], + 'is-file-extension-check-on-directories-enabled': True, + 'is-monitoring-of-objects-with-no-extension-enabled': False + } + } + } + xml.translate_struct(data) + return xml + + +def default_args(): + args = { + 'vserver': 'svm1', + 'name': 'policy1', + 'export_policies_to_exclude': 'export1', + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'one_fpolicy_scope_record': (200, { + "records": [{ + 'vserver': 'svm1', + 'policy_name': 'policy1', + 'export_policies_to_exclude': ['export1'], + 'is_file_extension_check_on_directories_enabled': True, + 'is_monitoring_of_objects_with_no_extension_enabled': False + }], + 'num_records': 1 + }, None) +} + + +def get_fpolicy_scope_mock_object(cx_type='zapi', kind=None): + fpolicy_scope_obj = my_module() + if cx_type == 'zapi': + if kind is None: + fpolicy_scope_obj.server = MockONTAPConnection() + else: + fpolicy_scope_obj.server = MockONTAPConnection(kind=kind) + return fpolicy_scope_obj + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_ensure_get_called(patch_ansible): + ''' test get_fpolicy_scope for non-existent policy''' + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + print('starting') + my_obj = my_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = MockONTAPConnection() + assert my_obj.get_fpolicy_scope is not None + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' create fpolicy scope ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_scope.NetAppOntapFpolicyScope.create_fpolicy_scope') +def test_successful_create(self, patch_ansible): + ''' creating fpolicy_scope and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection() + with patch.object(my_module, 'create_fpolicy_scope', wraps=my_obj.create_fpolicy_scope) as mock_create: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_create.assert_called_with() + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_scope') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_scope.NetAppOntapFpolicyScope.delete_fpolicy_scope') +def test_successful_delete(self, patch_ansible): + ''' delete fpolicy_scope and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['state'] = 'absent' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_scope') + with patch.object(my_module, 'delete_fpolicy_scope', wraps=my_obj.delete_fpolicy_scope) as mock_delete: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_delete.assert_called_with() + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + args['state'] = 'absent' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Delete: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_scope.NetAppOntapFpolicyScope.modify_fpolicy_scope') +def test_successful_modify(self, patch_ansible): + ''' modifying fpolicy_scope and testing idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['export_policies_to_exclude'] = 'export1,export2' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_scope') + with patch.object(my_module, 'modify_fpolicy_scope', wraps=my_obj.modify_fpolicy_scope) as mock_modify: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Modify: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_modify.assert_called_with({'export_policies_to_exclude': ['export1', 'export2']}) + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_scope') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Modify: ' + repr(exc.value)) + print(exc.value.args[0]['changed']) + assert not exc.value.args[0]['changed'] + + +def test_if_all_methods_catch_exception(patch_ansible): + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_scope_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_fpolicy_scope() + assert 'Error creating fPolicy policy scope ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_fpolicy_scope() + assert 'Error deleting fPolicy policy scope ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_fpolicy_scope(modify={}) + assert 'Error modifying fPolicy policy scope ' in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' create fpolicy scope ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' create fpolicy scope idempotent ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_scope_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_delete_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' delete fpolicy scope ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_delete(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' delete fpolicy scope ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_scope_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' modify fpolicy scope ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_scope_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify_prepopulate(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' modify fpolicy scope ''' + args = dict(default_args()) + args['export_policies_to_exclude'] = 'export1,export2' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['one_fpolicy_scope_record'], # get + SRR['empty_good'], # patch + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_status.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_status.py new file mode 100644 index 000000000..64674a3aa --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_fpolicy_status.py @@ -0,0 +1,286 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP fpolicy status Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_status \ + import NetAppOntapFpolicyStatus as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'fpolicy_policy_enabled': + xml = self.build_fpolicy_status_info_enabled() + elif self.type == 'fpolicy_policy_disabled': + xml = self.build_fpolicy_status_info_disabled() + elif self.type == 'fpolicy_policy_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_fpolicy_status_info_enabled(): + ''' build xml data for fpolicy-policy-status-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': { + 'fpolicy-policy-status-info': { + 'vserver': 'svm1', + 'policy-name': 'fPolicy1', + 'status': 'true' + } + } + } + xml.translate_struct(data) + return xml + + @staticmethod + def build_fpolicy_status_info_disabled(): + ''' build xml data for fpolicy-policy-status-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes-list': { + 'fpolicy-policy-status-info': { + 'vserver': 'svm1', + 'policy-name': 'fPolicy1', + 'status': 'false' + } + } + } + xml.translate_struct(data) + return xml + + +def default_args(): + args = { + 'vserver': 'svm1', + 'policy_name': 'fPolicy1', + 'sequence_number': '10', + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + # 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'uuid': (200, { + 'records': [{ + 'uuid': '56ab5d21' + }], + 'num_records': 1 + }, None), + 'fpolicy_status_info_enabled': (200, { + 'records': [{ + 'svm': { + 'uuid': '56ab5d21', + 'name': 'svm1' + }, + 'policies': [{ + 'name': 'fPolicy1', + 'enabled': True, + 'priority': 10 + }] + }], + 'num_records': 1 + }, None), + 'fpolicy_status_info_disabled': (200, { + 'records': [{ + 'svm': { + 'uuid': '56ab5d21', + 'name': 'svm1' + }, + 'policies': [{ + 'name': 'fPolicy1', + 'enabled': False + }] + }], + 'num_records': 1 + }, None) + +} + + +def get_fpolicy_status_mock_object(cx_type='zapi', kind=None): + fpolicy_status_obj = my_module() + if cx_type == 'zapi': + if kind is None: + fpolicy_status_obj.server = MockONTAPConnection() + else: + fpolicy_status_obj.server = MockONTAPConnection(kind=kind) + return fpolicy_status_obj + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_ensure_get_called(patch_ansible): + ''' test get_fpolicy_policy_status for non-existent fPolicy''' + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + print('starting') + my_obj = my_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = MockONTAPConnection('fpolicy_policy_enabled') + assert my_obj.get_fpolicy_policy_status is not None + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' enable fpolicy ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_status.NetAppOntapFpolicyStatus.enable_fpolicy_policy') +def test_successful_enable(self, patch_ansible): + ''' Enable fPolicy and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_policy_disabled') + with patch.object(my_module, 'enable_fpolicy_policy', wraps=my_obj.enable_fpolicy_policy) as mock_enable: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Enable: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_enable.assert_called_with() + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_policy_enabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Enable: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_fpolicy_status.NetAppOntapFpolicyStatus.disable_fpolicy_policy') +def test_successful_disable(self, patch_ansible): + ''' Disable fPolicy and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['state'] = 'absent' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_policy_enabled') + with patch.object(my_module, 'disable_fpolicy_policy', wraps=my_obj.disable_fpolicy_policy) as mock_disable: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Enable: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_disable.assert_called_with() + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + args['state'] = 'absent' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_policy_disabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Enable: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +def test_if_all_methods_catch_exception(patch_ansible): + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('fpolicy_policy_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.enable_fpolicy_policy() + print(str(exc.value.args[0]['msg'])) + assert 'Error enabling fPolicy policy ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.disable_fpolicy_policy() + assert 'Error disabling fPolicy policy ' in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_enable(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' enable fPolicy policy ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['uuid'], # get + SRR['fpolicy_status_info_disabled'], # get + SRR['empty_good'], # patch + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 4 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_disable(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' disable fPolicy policy ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['uuid'], # get + SRR['fpolicy_status_info_enabled'], # get + SRR['empty_good'], # patch + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 4 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_igroup.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_igroup.py new file mode 100644 index 000000000..5e5b7c64c --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_igroup.py @@ -0,0 +1,415 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, patch_ansible, create_module, create_and_apply, assert_warning_was_raised, assert_no_warnings, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_igroup \ + import NetAppOntapIgroup as igroup # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'vserver': 'vserver', + 'name': 'test', + 'initiator_names': 'init1', + 'ostype': 'linux', + 'initiator_group_type': 'fcp', + 'bind_portset': 'true', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'never' +} + +igroup_with_initiator = { + 'num-records': 1, + 'attributes-list': { + 'vserver': 'vserver', + 'initiator-group-os-type': 'linux', + 'initiator-group-info': { + 'initiators': [ + {'initiator-info': {'initiator-name': 'init1'}}, + {'initiator-info': {'initiator-name': 'init2'}} + ] + } + } +} + +igroup_without_initiator = { + 'num-records': 1, + 'attributes-list': { + 'initiator-group-info': {'vserver': 'test'} + } +} + +ZRR = zapi_responses({ + 'igroup_with_initiator_info': build_zapi_response(igroup_with_initiator), + 'igroup_without_initiator_info': build_zapi_response(igroup_without_initiator) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + igroup() + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +def test_get_nonexistent_igroup(): + ''' Test if get_igroup returns None for non-existent igroup ''' + register_responses([ + ('igroup-get-iter', ZRR['empty']) + ]) + igroup_obj = create_module(igroup, DEFAULT_ARGS) + result = igroup_obj.get_igroup('dummy') + assert result is None + + +def test_get_existing_igroup_with_initiators(): + ''' Test if get_igroup returns list of existing initiators ''' + register_responses([ + ('igroup-get-iter', ZRR['igroup_with_initiator_info']) + ]) + igroup_obj = create_module(igroup, DEFAULT_ARGS) + result = igroup_obj.get_igroup('igroup') + assert DEFAULT_ARGS['initiator_names'] in result['initiator_names'] + assert result['initiator_names'] == ['init1', 'init2'] + + +def test_get_existing_igroup_without_initiators(): + ''' Test if get_igroup returns empty list() ''' + register_responses([ + ('igroup-get-iter', ZRR['igroup_without_initiator_info']) + ]) + igroup_obj = create_module(igroup, DEFAULT_ARGS) + result = igroup_obj.get_igroup('igroup') + assert result['initiator_names'] == [] + + +def test_modify_initiator_calls_add_and_remove(): + '''Test remove_initiator() is called followed by add_initiator() on modify operation''' + register_responses([ + ('igroup-get-iter', ZRR['igroup_with_initiator_info']), + ('igroup-remove', ZRR['success']), + ('igroup-remove', ZRR['success']), + ('igroup-add', ZRR['success']) + ]) + igroup_obj = create_and_apply(igroup, DEFAULT_ARGS, {'initiator_names': 'replacewithme'})['changed'] + + +def test_modify_called_from_add(): + '''Test remove_initiator() and add_initiator() calls modify''' + register_responses([ + ('igroup-get-iter', ZRR['igroup_without_initiator_info']), + ('igroup-add', ZRR['success']) + ]) + igroup_obj = create_and_apply(igroup, DEFAULT_ARGS, {'initiator_names': 'replacewithme'})['changed'] + + +def test_modify_called_from_remove(): + '''Test remove_initiator() and add_initiator() calls modify''' + register_responses([ + ('igroup-get-iter', ZRR['igroup_with_initiator_info']), + ('igroup-remove', ZRR['success']), + ('igroup-remove', ZRR['success']) + ]) + igroup_obj = create_and_apply(igroup, DEFAULT_ARGS, {'initiator_names': ''})['changed'] + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('igroup-get-iter', ZRR['empty']), + ('igroup-create', ZRR['success']), + ('igroup-add', ZRR['success']) + ]) + igroup_obj = create_and_apply(igroup, DEFAULT_ARGS)['changed'] + + +def test_successful_delete(): + ''' Test successful delete ''' + register_responses([ + ('igroup-get-iter', ZRR['igroup_with_initiator_info']), + ('igroup-destroy', ZRR['success']) + ]) + igroup_obj = create_and_apply(igroup, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_successful_rename(): + '''Test successful rename''' + register_responses([ + ('igroup-get-iter', ZRR['empty']), + ('igroup-get-iter', ZRR['igroup_with_initiator_info']), + ('igroup-rename', ZRR['success']), + ('igroup-remove', ZRR['success']), + ]) + args = { + 'from_name': 'test', + 'name': 'test_new' + } + assert create_and_apply(igroup, DEFAULT_ARGS, args)['changed'] + + +def test_negative_modify_anything_zapi(): + ''' Test ZAPI option not currently supported in REST is rejected ''' + register_responses([ + ('igroup-get-iter', ZRR['igroup_with_initiator_info']), + ]) + args = { + 'vserver': 'my_vserver', + 'use_rest': 'never' + } + msg = "Error: modifying {'vserver': 'my_vserver'} is not supported in ZAPI" + assert msg in create_and_apply(igroup, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_negative_mutually_exclusive(): + ''' Test ZAPI option not currently supported in REST is rejected ''' + args = { + 'use_rest': 'auto', + 'igroups': 'my_group' + } + msg = "parameters are mutually exclusive: igroups|initiator_names" + assert msg in create_module(igroup, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_negative_igroups_require_rest(): + ''' Test ZAPI option not currently supported in REST is rejected ''' + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['initiator_names'] + args = { + 'igroups': 'my_group' + } + msg = "requires ONTAP 9.9.1 or later and REST must be enabled" + assert msg in create_module(igroup, DEFAULT_ARGS_COPY, args, fail=True)['msg'] + + +SRR = rest_responses({ + 'one_igroup_record': (200, dict(records=[ + dict(uuid='a1b2c3', + name='test', + svm=dict(name='vserver'), + initiators=[{'name': 'todelete'}], + protocol='fcp', + os_type='aix') + ], num_records=1), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None) +}) + + +def test_successful_create_rest(): + ''' Test successful create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['empty_records']), + ('POST', 'protocols/san/igroups', SRR['success']) + ]) + assert create_and_apply(igroup, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + + +def test_incomplete_record_rest(): + ''' Test successful create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['one_record_uuid']) + ]) + msg = "Error: unexpected igroup body:" + assert msg in create_and_apply(igroup, DEFAULT_ARGS, {'use_rest': 'always'}, fail=True)['msg'] + + +def test_successful_delete_rest(): + ''' Test successful delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['one_igroup_record']), + ('DELETE', 'protocols/san/igroups/a1b2c3', SRR['success']) + ]) + args = {'state': 'absent', 'use_rest': 'always'} + assert create_and_apply(igroup, DEFAULT_ARGS, args)['changed'] + + +def test_successful_modify_rest(): + ''' Test successful modify ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['one_igroup_record']), + ('DELETE', 'protocols/san/igroups/a1b2c3/initiators/todelete', SRR['success']), + ('POST', 'protocols/san/igroups/a1b2c3/initiators', SRR['success']), + ('PATCH', 'protocols/san/igroups/a1b2c3', SRR['success']) + ]) + assert create_and_apply(igroup, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + + +def test_successful_modify_initiator_objects_rest(): + ''' Test successful modify ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/igroups', SRR['one_igroup_record']), + ('DELETE', 'protocols/san/igroups/a1b2c3/initiators/todelete', SRR['success']), + ('POST', 'protocols/san/igroups/a1b2c3/initiators', SRR['success']), + ('PATCH', 'protocols/san/igroups/a1b2c3', SRR['success']) + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['initiator_names'] + DEFAULT_ARGS_COPY['initiator_objects'] = [{'name': 'init1', 'comment': 'comment1'}] + assert create_and_apply(igroup, DEFAULT_ARGS_COPY, {'use_rest': 'always'})['changed'] + + +def test_successful_modify_initiator_objects_comment_rest(): + ''' Test successful modify ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/igroups', SRR['one_igroup_record']), + ('PATCH', 'protocols/san/igroups/a1b2c3/initiators/todelete', SRR['success']), + ('PATCH', 'protocols/san/igroups/a1b2c3', SRR['success']) + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['initiator_names'] + DEFAULT_ARGS_COPY['initiator_objects'] = [{'name': 'todelete', 'comment': 'comment1'}] + assert create_and_apply(igroup, DEFAULT_ARGS_COPY, {'use_rest': 'always'})['changed'] + + +def test_successful_modify_igroups_rest(): + ''' Test successful modify ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/igroups', SRR['one_igroup_record']), + ('DELETE', 'protocols/san/igroups/a1b2c3/initiators/todelete', SRR['success']), + ('POST', 'protocols/san/igroups/a1b2c3/igroups', SRR['success']), + ('PATCH', 'protocols/san/igroups/a1b2c3', SRR['success']) + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['initiator_names'] + args = { + 'igroups': ['test_igroup'], + 'use_rest': 'auto', + 'force_remove_initiator': True + } + assert create_and_apply(igroup, DEFAULT_ARGS_COPY, args)['changed'] + + +def test_9_9_0_no_igroups_rest(): + ''' Test failed to use igroups ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['initiator_names'] + args = { + 'igroups': ['test_igroup'], + 'use_rest': 'always' + } + msg = 'Error: using igroups requires ONTAP 9.9.1 or later and REST must be enabled - ONTAP version: 9.9.0.' + assert msg in create_module(igroup, DEFAULT_ARGS_COPY, args, fail=True)['msg'] + + +def test_successful_rename_rest(): + '''Test successful rename''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/igroups', SRR['empty_records']), + ('GET', 'protocols/san/igroups', SRR['one_igroup_record']), + ('DELETE', 'protocols/san/igroups/a1b2c3/initiators/todelete', SRR['success']), + ('POST', 'protocols/san/igroups/a1b2c3/initiators', SRR['success']), + ('PATCH', 'protocols/san/igroups/a1b2c3', SRR['success']) + ]) + args = { + 'use_rest': 'always', + 'from_name': 'test', + 'name': 'test_new' + } + assert create_and_apply(igroup, DEFAULT_ARGS, args)['changed'] + + +def test_negative_zapi_or_rest99_option(): + ''' Test ZAPI option not currently supported in REST is rejected ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']) + ]) + args = { + 'use_rest': 'always', + 'bind_portset': 'my_portset' + } + create_module(igroup, DEFAULT_ARGS, args) + msg = "Warning: falling back to ZAPI: using bind_portset requires ONTAP 9.9 or later and REST must be enabled - ONTAP version: 9.8.0." + print_warnings() + assert_warning_was_raised(msg) + + +def test_positive_zapi_or_rest99_option(): + ''' Test ZAPI option not currently supported in REST forces ZAPI calls ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']) + ]) + args = { + 'use_rest': 'auto', + 'bind_portset': 'my_portset' + } + create_module(igroup, DEFAULT_ARGS, args) + msg = "Warning: falling back to ZAPI: using bind_portset requires ONTAP 9.9 or later and REST must be enabled - ONTAP version: 9.8.0." + print_warnings() + assert_warning_was_raised(msg) + + +def test_create_rest_99(): + ''' Test 9.9 option works with REST ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['empty_records']), + ('POST', 'protocols/san/igroups', SRR['success']) + ]) + args = { + 'use_rest': 'auto', + 'bind_portset': 'my_portset' + } + assert create_and_apply(igroup, DEFAULT_ARGS, args)['changed'] + print_warnings + assert_no_warnings() + + +def test_negative_modify_vserver_rest(): + ''' Test ZAPI option not currently supported in REST is rejected ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['one_igroup_record']) + ]) + args = { + 'vserver': 'my_vserver', + 'use_rest': 'always' + } + msg = "Error: modifying {'vserver': 'my_vserver'} is not supported in REST" + assert msg in create_and_apply(igroup, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_negative_igroups_require_9_9(): + ''' Test ZAPI option not currently supported in REST is rejected ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']) + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['initiator_names'] + args = { + 'igroups': 'test_igroup', + 'use_rest': 'always' + } + msg = "requires ONTAP 9.9.1 or later and REST must be enabled" + assert msg in create_module(igroup, DEFAULT_ARGS_COPY, args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_igroup_initiator.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_igroup_initiator.py new file mode 100644 index 000000000..7da908dcb --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_igroup_initiator.py @@ -0,0 +1,256 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_igroup_initiator \ + import NetAppOntapIgroupInitiator as initiator # module under test +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'state': 'present', + 'vserver': 'vserver', + 'name': 'init1', + 'initiator_group': 'test', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' +} + + +initiator_info = { + 'num-records': 1, + 'attributes-list': { + 'initiator-group-info': { + 'initiators': [ + {'initiator-info': {'initiator-name': 'init1'}}, + {'initiator-info': {'initiator-name': 'init2'}} + ] + } + } +} + + +ZRR = zapi_responses({ + 'initiator_info': build_zapi_response(initiator_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + initiator() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get_nonexistent_igroup(): + ''' Test if get_initiators returns None for non-existent initiator ''' + register_responses([ + ('igroup-get-iter', ZRR['empty']) + ]) + initiator_obj = create_module(initiator, DEFAULT_ARGS) + result = initiator_obj.get_initiators() + assert result == [] + + +def test_get_existing_initiator(): + ''' Test if get_initiator returns None for existing initiator ''' + register_responses([ + ('igroup-get-iter', ZRR['initiator_info']) + ]) + initiator_obj = create_module(initiator, DEFAULT_ARGS) + result = initiator_obj.get_initiators() + assert DEFAULT_ARGS['name'] in result + assert result == ['init1', 'init2'] # from build_igroup_initiators() + + +def test_successful_add(): + ''' Test successful add''' + register_responses([ + ('igroup-get-iter', ZRR['initiator_info']), + ('igroup-add', ZRR['success']) + ]) + args = {'name': 'init3'} + assert create_and_apply(initiator, DEFAULT_ARGS, args)['changed'] + + +def test_successful_add_idempotency(): + ''' Test successful add idempotency ''' + register_responses([ + ('igroup-get-iter', ZRR['initiator_info']) + ]) + assert create_and_apply(initiator, DEFAULT_ARGS)['changed'] is False + + +def test_successful_remove(): + ''' Test successful remove ''' + register_responses([ + ('igroup-get-iter', ZRR['initiator_info']), + ('igroup-remove', ZRR['success']) + ]) + args = {'state': 'absent'} + assert create_and_apply(initiator, DEFAULT_ARGS, args)['changed'] + + +def test_successful_remove_idempotency(): + ''' Test successful remove idempotency''' + register_responses([ + ('igroup-get-iter', ZRR['initiator_info']) + ]) + args = {'state': 'absent', 'name': 'alreadyremoved'} + assert create_and_apply(initiator, DEFAULT_ARGS)['changed'] is False + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('igroup-get-iter', ZRR['error']), + ('igroup-add', ZRR['error']), + ('igroup-remove', ZRR['error']) + ]) + initiator_obj = create_module(initiator, DEFAULT_ARGS) + + error = expect_and_capture_ansible_exception(initiator_obj.get_initiators, 'fail')['msg'] + assert 'Error fetching igroup info' in error + + error = expect_and_capture_ansible_exception(initiator_obj.modify_initiator, 'fail', 'init4', 'igroup-add')['msg'] + assert 'Error modifying igroup initiator' in error + + error = expect_and_capture_ansible_exception(initiator_obj.modify_initiator, 'fail', 'init4', 'igroup-remove')['msg'] + assert 'Error modifying igroup initiator' in error + + +SRR = rest_responses({ + 'initiator_info': (200, {"records": [ + { + "svm": {"name": "svm1"}, + "uuid": "897de45f-bbbf-11ec-9f18-005056b3b297", + "name": "init1", + "initiators": [ + {"name": "iqn.2001-04.com.example:abc123"}, + {"name": "iqn.2001-04.com.example:abc124"}, + {'name': 'init3'} + ] + } + ], "num_records": 1}, None), + 'igroup_without_intiators': (200, {"records": [ + { + "svm": {"name": "svm1"}, + "uuid": "897de45f-bbbf-11ec-9f18-005056alr297", + "name": "init22", + } + ], "num_records": 1}, None) +}) + + +def test_successful_add_rest(): + ''' Test successful add''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['initiator_info']), + ('POST', 'protocols/san/igroups/897de45f-bbbf-11ec-9f18-005056b3b297/initiators', SRR['success']) + ]) + assert create_and_apply(initiator, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + + +def test_successful_add_idempotency_rest(): + ''' Test successful add idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['initiator_info']) + ]) + args = {'use_rest': 'always', 'name': 'iqn.2001-04.com.example:abc123'} + assert create_and_apply(initiator, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_add_to_0_initiator_igroup_rest(): + ''' Test successful add''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['igroup_without_intiators']), + ('POST', 'protocols/san/igroups/897de45f-bbbf-11ec-9f18-005056alr297/initiators', SRR['success']) + ]) + assert create_and_apply(initiator, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + + +def test_successful_remove_rest(): + ''' Test successful remove ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['initiator_info']), + ('DELETE', 'protocols/san/igroups/897de45f-bbbf-11ec-9f18-005056b3b297/initiators/init3', SRR['success']) + ]) + args = {'use_rest': 'always', 'name': 'init3', 'state': 'absent'} + assert create_and_apply(initiator, DEFAULT_ARGS, args)['changed'] + + +def test_successful_remove_idempotency_rest(): + ''' Test successful remove idempotency''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['initiator_info']) + ]) + args = {'use_rest': 'always', 'name': 'alreadyremoved', 'state': 'absent'} + assert create_and_apply(initiator, DEFAULT_ARGS, args)['changed'] is False + + +def test_get_initiator_catch_exception_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['generic_error']) + ]) + error = create_and_apply(initiator, DEFAULT_ARGS, {'use_rest': 'always'}, 'fail')['msg'] + assert 'Error fetching igroup info' in error + + +def test_add_initiator_catch_exception_rest(): + ''' Test add error''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['initiator_info']), + ('POST', 'protocols/san/igroups/897de45f-bbbf-11ec-9f18-005056b3b297/initiators', SRR['generic_error']) + ]) + error = create_and_apply(initiator, DEFAULT_ARGS, {'use_rest': 'always'}, 'fail')['msg'] + assert 'Error modifying igroup initiator' in error + + +def test_remove_initiator_catch_exception_rest(): + ''' Test remove error''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['initiator_info']), + ('DELETE', 'protocols/san/igroups/897de45f-bbbf-11ec-9f18-005056b3b297/initiators/init3', SRR['generic_error']) + ]) + args = {'use_rest': 'always', 'name': 'init3', 'state': 'absent'} + error = create_and_apply(initiator, DEFAULT_ARGS, args, 'fail')['msg'] + assert 'Error modifying igroup initiator' in error + + +def test_error_uuid_not_found(): + ''' Test uuid error''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/igroups', SRR['empty_records']) + ]) + args = {'use_rest': 'always'} + error = create_and_apply(initiator, DEFAULT_ARGS, args, 'fail')['msg'] + assert 'Error modifying igroup initiator init1: igroup not found' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_info.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_info.py new file mode 100644 index 000000000..18c35c910 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_info.py @@ -0,0 +1,738 @@ +# (c) 2018-2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for ONTAP Ansible module na_ontap_info ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_warning_was_raised, expect_and_capture_ansible_exception, call_main, create_module, patch_ansible, print_warnings +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_info import NetAppONTAPGatherInfo as my_module, main as my_main +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_info import convert_keys as info_convert_keys, __finditem as info_finditem + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'use_rest', +} + + +def net_port_info(port_type): + return { + 'attributes-list': [{ + 'net-port-info': { + 'node': 'node_0', + 'port': 'port_0', + 'broadcast_domain': 'broadcast_domain_0', + 'ipspace': 'ipspace_0', + 'port_type': port_type + }}, { + 'net-port-info': { + 'node': 'node_1', + 'port': 'port_1', + 'broadcast_domain': 'broadcast_domain_1', + 'ipspace': 'ipspace_1', + 'port_type': port_type + }} + ] + } + + +def net_ifgrp_info(id): + return { + 'attributes': { + 'net-ifgrp-info': { + 'ifgrp-name': 'ifgrp_%d' % id, + 'node': 'node_%d' % id, + } + } + } + + +def aggr_efficiency_info(node): + attributes = { + 'aggregate': 'v2', + } + if node: + attributes['node'] = node + return { + 'attributes-list': [{ + 'aggr-efficiency-info': attributes + }] + } + + +def lun_info(path, next=False): + info = { + 'attributes-list': [{ + 'lun-info': { + 'serial-number': 'z6CcD+SK5mPb', + 'vserver': 'svm1', + 'path': path} + }] + } + if next: + info['next-tag'] = 'next_tag' + return info + + +list_of_one = [{'k1': 'v1'}] +list_of_two = [{'k1': 'v1'}, {'k2': 'v2'}] +list_of_two_dups = [{'k1': 'v1'}, {'k1': 'v2'}] + + +ZRR = zapi_responses({ + 'net_port_info': build_zapi_response(net_port_info('whatever'), 2), + 'net_port_info_with_ifgroup': build_zapi_response(net_port_info('if_group'), 2), + 'net_ifgrp_info_0': build_zapi_response(net_ifgrp_info(0), 1), + 'net_ifgrp_info_1': build_zapi_response(net_ifgrp_info(1), 1), + 'list_of_one': build_zapi_response(list_of_one), + 'list_of_two': build_zapi_response(list_of_two), + 'list_of_two_dups': build_zapi_response(list_of_two_dups), + 'aggr_efficiency_info': build_zapi_response(aggr_efficiency_info('v1')), + 'aggr_efficiency_info_no_node': build_zapi_response(aggr_efficiency_info(None)), + 'lun_info': build_zapi_response(lun_info('p1')), + 'lun_info_next_2': build_zapi_response(lun_info('p2', True)), + 'lun_info_next_3': build_zapi_response(lun_info('p3', True)), + 'lun_info_next_4': build_zapi_response(lun_info('p4', True)), +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + assert 'missing required arguments: hostname' in call_main(my_main, {}, fail=True)['msg'] + + +def test_ensure_command_called(): + ''' calling get_all will raise a KeyError exception ''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['version']), + ('ZAPI', 'net-interface-get-iter', ZRR['success']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + results = my_obj.get_all(['net_interface_info']) + assert 'net_interface_info' in results + + +def test_get_generic_get_iter(): + '''calling get_generic_get_iter will return expected dict''' + register_responses([ + ('ZAPI', 'net-port-get-iter', ZRR['net_port_info']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + result = obj.get_generic_get_iter( + 'net-port-get-iter', + attribute='net-port-info', + key_fields=('node', 'port'), + query={'max-records': '1024'} + ) + assert result.get('node_0:port_0') + assert result.get('node_1:port_1') + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_info.NetAppONTAPGatherInfo.get_all') +def test_main(get_all): + '''test main method - default: no state.''' + register_responses([ + ]) + get_all.side_effect = [ + {'test_get_all': {'vserver_login_banner_info': 'test_vserver_login_banner_info', 'vserver_info': 'test_vserver_info'}} + ] + results = call_main(my_main, DEFAULT_ARGS) + assert 'ontap_info' in results + assert 'test_get_all' in results['ontap_info'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_info.NetAppONTAPGatherInfo.get_all') +def test_main_with_state(get_all): + '''test main method with explicit state.''' + register_responses([ + ]) + module_args = {'state': 'some_state'} + get_all.side_effect = [ + {'test_get_all': {'vserver_login_banner_info': 'test_vserver_login_banner_info', 'vserver_info': 'test_vserver_info'}} + ] + results = call_main(my_main, DEFAULT_ARGS, module_args) + assert 'ontap_info' in results + assert 'test_get_all' in results['ontap_info'] + print_warnings() + assert_warning_was_raised("option 'state' is deprecated.") + + +def test_get_ifgrp_info_no_ifgrp(): + '''test get_ifgrp_info with empty ifgrp_info''' + register_responses([ + ('ZAPI', 'net-port-get-iter', ZRR['net_port_info']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + result = obj.get_ifgrp_info() + assert result == {} + + +def test_get_ifgrp_info_with_ifgrp(): + '''test get_ifgrp_info with empty ifgrp_info''' + register_responses([ + ('ZAPI', 'net-port-get-iter', ZRR['net_port_info_with_ifgroup']), + ('ZAPI', 'net-port-ifgrp-get', ZRR['net_ifgrp_info_0']), + ('ZAPI', 'net-port-ifgrp-get', ZRR['net_ifgrp_info_1']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + results = obj.get_ifgrp_info() + assert results.get('node_0:ifgrp_0') + assert results.get('node_1:ifgrp_1') + + +def test_ontapi_error(): + '''test ontapi will raise zapi error''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['error']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + error = zapi_error_message('Error calling API system-get-ontapi-version') + assert error in expect_and_capture_ansible_exception(obj.ontapi, 'fail')['msg'] + + +def test_call_api_error(): + '''test call_api will raise zapi error''' + register_responses([ + ('ZAPI', 'security-key-manager-key-get-iter', ZRR['error']), + ('ZAPI', 'lun-get-iter', ZRR['error_missing_api']), + ('ZAPI', 'nvme-get-iter', ZRR['error']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + # 1 error is ignored + assert obj.call_api('security-key-manager-key-get-iter') == (None, None) + # 2 missing API (cluster admin API not visible at vserver level) + error = zapi_error_message('Error invalid API. Most likely running a cluster level API as vserver', 13005) + assert error in expect_and_capture_ansible_exception(obj.call_api, 'fail', 'lun-get-iter')['msg'] + # 3 API error + error = zapi_error_message('Error calling API nvme-get-iter') + assert error in expect_and_capture_ansible_exception(obj.call_api, 'fail', 'nvme-get-iter')['msg'] + + +def test_get_generic_get_iter_key_error(): + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + keys = 'single_key' + error = "Error: key 'single_key' not found for lun-get-iter, got:" + assert error in expect_and_capture_ansible_exception(obj.get_generic_get_iter, 'fail', 'lun-get-iter', None, keys)['msg'] + keys = ('key1', 'path') + error = "Error: key 'key1' not found for lun-get-iter, got:" + assert error in expect_and_capture_ansible_exception(obj.get_generic_get_iter, 'fail', 'lun-get-iter', None, keys)['msg'] + # ignoring key_errors + module_args = {'continue_on_error': 'key_error'} + obj = create_module(my_module, DEFAULT_ARGS, module_args) + keys = 'single_key' + missing_key = 'Error_1_key_not_found_%s' % keys + results = obj.get_generic_get_iter('lun-get-iter', None, keys) + assert missing_key in results + keys = ('key1', 'path') + missing_key = 'Error_1_key_not_found_%s' % keys[0] + results = obj.get_generic_get_iter('lun-get-iter', None, keys) + assert missing_key in results + + +def test_find_item(): + '''test __find_item return expected key value''' + register_responses([ + ]) + obj = {"A": 1, "B": {"C": {"D": 2}}} + key = "D" + result = info_finditem(obj, key) + assert result == 2 + obj = {"A": 1, "B": {"C": {"D": None}}} + result = info_finditem(obj, key) + assert result == "None" + + +def test_subset_return_all_complete(): + ''' Check all returns all of the entries if version is high enough ''' + register_responses([ + ]) + version = '170' # change this if new ZAPIs are supported + obj = create_module(my_module, DEFAULT_ARGS) + subset = obj.get_subset(['all'], version) + assert set(obj.info_subsets.keys()) == subset + + +def test_subset_return_all_partial(): + ''' Check all returns a subset of the entries if version is low enough ''' + register_responses([ + ]) + version = '120' # low enough so that some ZAPIs are not supported + obj = create_module(my_module, DEFAULT_ARGS) + subset = obj.get_subset(['all'], version) + all_keys = obj.info_subsets.keys() + assert set(all_keys) > subset + supported_keys = filter(lambda key: obj.info_subsets[key]['min_version'] <= version, all_keys) + assert set(supported_keys) == subset + + +def test_subset_return_one(): + ''' Check single entry returns one ''' + register_responses([ + ]) + version = '120' # low enough so that some ZAPIs are not supported + obj = create_module(my_module, DEFAULT_ARGS) + subset = obj.get_subset(['net_interface_info'], version) + assert len(subset) == 1 + + +def test_subset_return_multiple(): + ''' Check that more than one entry returns the same number ''' + register_responses([ + ]) + version = '120' # low enough so that some ZAPIs are not supported + obj = create_module(my_module, DEFAULT_ARGS) + subset_entries = ['net_interface_info', 'net_port_info'] + subset = obj.get_subset(subset_entries, version) + assert len(subset) == len(subset_entries) + + +def test_subset_return_bad(): + ''' Check that a bad subset entry will error out ''' + register_responses([ + ]) + version = '120' # low enough so that some ZAPIs are not supported + obj = create_module(my_module, DEFAULT_ARGS) + error = 'Bad subset: my_invalid_subset' + assert error in expect_and_capture_ansible_exception(obj.get_subset, 'fail', ['net_interface_info', 'my_invalid_subset'], version)['msg'] + + +def test_subset_return_unsupported(): + ''' Check that a new subset entry will error out on an older system ''' + register_responses([ + ]) + version = '120' # low enough so that some ZAPIs are not supported + key = 'nvme_info' # only supported starting at 140 + obj = create_module(my_module, DEFAULT_ARGS) + error = 'Remote system at version %s does not support %s' % (version, key) + assert error in expect_and_capture_ansible_exception(obj.get_subset, 'fail', ['net_interface_info', key], version)['msg'] + + +def test_subset_return_none(): + ''' Check usable subset can be empty ''' + register_responses([ + ]) + version = '!' # lower then 0, so that no ZAPI is supported + obj = create_module(my_module, DEFAULT_ARGS) + subset = obj.get_subset(['all'], version) + assert len(subset) == 0 + + +def test_subset_return_all_expect_one(): + ''' Check !x returns all of the entries except x if version is high enough ''' + register_responses([ + ]) + version = '170' # change this if new ZAPIs are supported + obj = create_module(my_module, DEFAULT_ARGS) + subset = obj.get_subset(['!net_interface_info'], version) + assert len(obj.info_subsets.keys()) == len(subset) + 1 + subset.add('net_interface_info') + assert set(obj.info_subsets.keys()) == subset + + +def test_subset_return_all_expect_three(): + ''' Check !x,!y,!z returns all of the entries except x, y, z if version is high enough ''' + register_responses([ + ]) + version = '170' # change this if new ZAPIs are supported + obj = create_module(my_module, DEFAULT_ARGS) + subset = obj.get_subset(['!net_interface_info', '!nvme_info', '!ontap_version'], version) + assert len(obj.info_subsets.keys()) == len(subset) + 3 + subset.update(['net_interface_info', 'nvme_info', 'ontap_version']) + assert set(obj.info_subsets.keys()) == subset + + +def test_subset_return_none_with_exclusion(): + ''' Check usable subset can be empty with !x ''' + register_responses([ + ]) + version = '!' # lower then 0, so that no ZAPI is supported + key = 'net_interface_info' + obj = create_module(my_module, DEFAULT_ARGS) + error = 'Remote system at version %s does not support %s' % (version, key) + assert error in expect_and_capture_ansible_exception(obj.get_subset, 'fail', ['!' + key], version)['msg'] + + +def test_get_generic_get_iter_flatten_list_of_one(): + '''calling get_generic_get_iter will return expected dict''' + register_responses([ + ('ZAPI', 'list_of_one', ZRR['list_of_one']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + result = obj.get_generic_get_iter( + 'list_of_one', + attributes_list_tag=None, + ) + print(ZRR['list_of_one'][0].to_string()) + print(result) + assert isinstance(result, dict) + assert result.get('k1') == 'v1' + + +def test_get_generic_get_iter_flatten_list_of_two(): + '''calling get_generic_get_iter will return expected dict''' + register_responses([ + ('ZAPI', 'list_of_two', ZRR['list_of_two']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + result = obj.get_generic_get_iter( + 'list_of_two', + attributes_list_tag=None, + ) + print(result) + assert isinstance(result, dict) + assert result.get('k1') == 'v1' + assert result.get('k2') == 'v2' + + +def test_get_generic_get_iter_flatten_list_of_two_dups(): + '''calling get_generic_get_iter will return expected dict''' + register_responses([ + ('ZAPI', 'list_of_two_dups', ZRR['list_of_two_dups']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + result = obj.get_generic_get_iter( + 'list_of_two_dups', + attributes_list_tag=None, + ) + assert isinstance(result, list) + assert result[0].get('k1') == 'v1' + assert result[1].get('k1') == 'v2' + + +def test_check_underscore(): + ''' Check warning is recorded if '_' is found in key ''' + register_responses([ + ]) + test_dict = dict( + bad_key='something' + ) + test_dict['good-key'] = [dict( + other_bad_key=dict( + yet_another_bad_key=1 + ), + somekey=dict( + more_bad_key=2 + ) + )] + obj = create_module(my_module, DEFAULT_ARGS) + obj.check_for___in_keys(test_dict) + print('Info: %s' % repr(obj.warnings)) + for key in ['bad_key', 'other_bad_key', 'yet_another_bad_key', 'more_bad_key']: + msg = "Underscore in ZAPI tag: %s, do you mean '-'?" % key + assert msg in obj.warnings + obj.warnings.remove(msg) + # make sure there is no extra warnings (eg we found and removed all of them) + assert obj.warnings == list() + + +def d2us(astr): + return str.replace(astr, '-', '_') + + +def test_convert_keys_string(): + ''' no conversion ''' + register_responses([ + ]) + key = 'a-b-c' + assert info_convert_keys(key) == key + + +def test_convert_keys_tuple(): + ''' no conversion ''' + register_responses([ + ]) + key = 'a-b-c' + anobject = (key, key) + assert info_convert_keys(anobject) == anobject + + +def test_convert_keys_list(): + ''' no conversion ''' + register_responses([ + ]) + key = 'a-b-c' + anobject = [key, key] + assert info_convert_keys(anobject) == anobject + + +def test_convert_keys_simple_dict(): + ''' conversion of keys ''' + register_responses([ + ]) + key = 'a-b-c' + anobject = {key: 1} + assert list(info_convert_keys(anobject).keys())[0] == d2us(key) + + +def test_convert_keys_list_of_dict(): + ''' conversion of keys ''' + register_responses([ + ]) + key = 'a-b-c' + anobject = [{key: 1}, {key: 2}] + converted = info_convert_keys(anobject) + for adict in converted: + for akey in adict: + assert akey == d2us(key) + + +def test_set_error_flags_error_n(): + ''' Check set_error__flags return correct dict ''' + register_responses([ + ]) + module_args = {'continue_on_error': ['never', 'whatever']} + msg = "never needs to be the only keyword in 'continue_on_error' option." + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_set_error_flags_error_a(): + ''' Check set_error__flags return correct dict ''' + register_responses([ + ]) + module_args = {'continue_on_error': ['whatever', 'always']} + print('Info: %s' % call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg']) + msg = "always needs to be the only keyword in 'continue_on_error' option." + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_set_error_flags_error_u(): + ''' Check set_error__flags return correct dict ''' + register_responses([ + ]) + module_args = {'continue_on_error': ['whatever', 'else']} + + print('Info: %s' % call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg']) + msg = "whatever is not a valid keyword in 'continue_on_error' option." + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_set_error_flags_1_flag(): + ''' Check set_error__flags return correct dict ''' + register_responses([ + ]) + module_args = {'continue_on_error': ['missing_vserver_api_error']} + obj = create_module(my_module, DEFAULT_ARGS, module_args, 'vserver') + assert not obj.error_flags['missing_vserver_api_error'] + assert obj.error_flags['rpc_error'] + assert obj.error_flags['other_error'] + + +def test_set_error_flags_2_flags(): + ''' Check set_error__flags return correct dict ''' + register_responses([ + ]) + module_args = {'continue_on_error': ['missing_vserver_api_error', 'rpc_error']} + obj = create_module(my_module, DEFAULT_ARGS, module_args, 'vserver') + assert not obj.error_flags['missing_vserver_api_error'] + assert not obj.error_flags['rpc_error'] + assert obj.error_flags['other_error'] + + +def test_set_error_flags_3_flags(): + ''' Check set_error__flags return correct dict ''' + register_responses([ + ]) + module_args = {'continue_on_error': ['missing_vserver_api_error', 'rpc_error', 'other_error']} + obj = create_module(my_module, DEFAULT_ARGS, module_args, 'vserver') + assert not obj.error_flags['missing_vserver_api_error'] + assert not obj.error_flags['rpc_error'] + assert not obj.error_flags['other_error'] + + +def test_get_subset_missing_key(): + '''calling aggr_efficiency_info with missing key''' + register_responses([ + ('ZAPI', 'aggr-efficiency-get-iter', ZRR['aggr_efficiency_info']), + ('ZAPI', 'aggr-efficiency-get-iter', ZRR['aggr_efficiency_info_no_node']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + call = obj.info_subsets['aggr_efficiency_info'] + info = call['method'](**call['kwargs']) + print(info) + assert 'v1:v2' in info + call = obj.info_subsets['aggr_efficiency_info'] + info = call['method'](**call['kwargs']) + print(info) + assert 'key_not_present:v2' in info + + +def test_get_lun_with_serial(): + '''calling lun_info with serial-number key''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + # no records + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'lun-get-iter', ZRR['no_records']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + info = obj.get_all(['lun_info']) + print(info) + assert 'lun_info' in info + lun_info = info['lun_info']['svm1:p1'] + assert lun_info['serial_number'] == 'z6CcD+SK5mPb' + assert lun_info['serial_hex'] == '7a364363442b534b356d5062' + assert lun_info['naa_id'] == 'naa.600a0980' + '7a364363442b534b356d5062' + # no records + info = obj.get_all(['lun_info']) + assert 'lun_info' in info + assert info['lun_info'] is None + # error + + +def test_get_nothing(): + '''calling with !all''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + info = obj.get_all(['!all']) + print(info) + assert info == {'ontap_version': '0', 'ontapi_version': '0'} + + +def test_deprecation_ontap_version(): + '''calling ontap_version''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + info = obj.get_all(['ontap_version']) + assert info + assert 'deprecation_warning' in info + assert info['deprecation_warning'] == 'ontap_version is deprecated, please use ontapi_version' + + +def test_help(): + '''calling help''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ]) + obj = create_module(my_module, DEFAULT_ARGS) + info = obj.get_all(['help']) + assert info + assert 'help' in info + + +def test_desired_attributes(): + '''desired_attributes option''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'lun-get-iter', ZRR['success']), + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ]) + module_args = {'desired_attributes': {'attr': 'value'}} + obj = create_module(my_module, DEFAULT_ARGS, module_args) + info = obj.get_all(['lun_info']) + assert 'lun_info' in info + assert info['lun_info'] is None + error = 'desired_attributes option is only supported with a single subset' + assert error in expect_and_capture_ansible_exception(obj.get_all, 'fail', ['ontapi_version', 'ontap_system_version'])['msg'] + + +def test_query(): + '''query option''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ]) + module_args = {'query': {'attr': 'value', 'a_b': 'val'}} + obj = create_module(my_module, DEFAULT_ARGS, module_args) + info = obj.get_all(['ontapi_version']) + assert info == {'ontap_version': '0', 'ontapi_version': '0', 'module_warnings': ["Underscore in ZAPI tag: a_b, do you mean '-'?"]} + error = 'query option is only supported with a single subset' + assert error in expect_and_capture_ansible_exception(obj.get_all, 'fail', ['ontapi_version', 'ontap_system_version'])['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_info.NetAppONTAPGatherInfo.get_all') +def test_get_all_with_summary(mock_get_all): + '''all and summary''' + register_responses([ + ]) + module_args = {'summary': True, 'gather_subset': None} + mock_get_all.return_value = {'a_info': {'1': '1.1'}, 'b_info': {'2': '2.2'}} + info = call_main(my_main, DEFAULT_ARGS, module_args) + assert info + assert 'ontap_info' in info + assert info['ontap_info'] == {'a_info': {'1': None}.keys(), 'b_info': {'2': None}.keys()} + + +def test_repeated_get(): + '''query option''' + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info_next_2']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info_next_3']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info_next_4']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ]) + module_args = {'query': {}} + obj = create_module(my_module, DEFAULT_ARGS, module_args) + info = obj.get_all(['lun_info']) + assert info + assert 'lun_info' in info + assert len(info['lun_info']) == 4 + + +def test_repeated_get_error(): + '''query option''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['lun_info_next_2']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ]) + module_args = {'query': {}} + obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "'next-tag' is not expected for this API" + assert error in expect_and_capture_ansible_exception(obj.call_api, 'fail', 'lun-get-iter', attributes_list_tag=None)['msg'] + + +def test_attribute_error(): + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'license-v2-list-info', ZRR['no_records']), + ]) + module_args = {'gather_subset': ['license_info'], 'vserver': 'svm'} + error = "Error: attribute 'licenses' not found for license-v2-list-info, got:" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_continue_on_error(): + register_responses([ + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'license-v2-list-info', ZRR['error']), + ('ZAPI', 'system-get-ontapi-version', ZRR['success']), + ('ZAPI', 'license-v2-list-info', ZRR['error']), + ]) + module_args = {'gather_subset': ['license_info'], 'vserver': 'svm'} + error = zapi_error_message('Error calling API license-v2-list-info') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args = {'gather_subset': ['license_info'], 'vserver': 'svm', 'continue_on_error': 'always'} + info = call_main(my_main, DEFAULT_ARGS, module_args) + error = {'error': zapi_error_message('Error calling API license-v2-list-info')} + assert info is not None + assert info['ontap_info']['license_info'] == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_interface.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_interface.py new file mode 100644 index 000000000..129caa635 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_interface.py @@ -0,0 +1,1778 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import copy +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings,\ + assert_warning_was_raised, print_warnings, call_main, create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_interface \ + import NetAppOntapInterface as interface_module, main as my_main + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def interface_info(dns=True, address='2.2.2.2', netmask='1.1.1.1'): + info = { + 'attributes-list': { + 'net-interface-info': { + 'interface-name': 'abc_if', + 'administrative-status': 'up', + 'failover-group': 'failover_group', + 'failover-policy': 'up', + 'firewall-policy': 'up', + 'is-auto-revert': 'true', + 'home-node': 'node', + 'current-node': 'node', + 'home-port': 'e0c', + 'current-port': 'e0c', + 'address': address, + 'netmask': netmask, + 'role': 'data', + 'listen-for-dns-query': 'true', + 'is-dns-update-enabled': 'true', + 'is-ipv4-link-local': 'false', + 'service-policy': 'service_policy', + } + } + } + if dns: + info['attributes-list']['net-interface-info']['dns-domain-name'] = 'test.com' + return info + + +node_info = { + 'attributes-list': { + 'cluster-node-info': { + 'node-name': 'node_1', + } + } +} + + +ZRR = zapi_responses({ + 'interface_info': build_zapi_response(interface_info(), 1), + 'interface_ipv4': build_zapi_response(interface_info(address='10.10.10.13', netmask='255.255.255.0'), 1), + 'interface_info_no_dns': build_zapi_response(interface_info(dns=False), 1), + 'node_info': build_zapi_response(node_info, 1), + 'error_17': build_zapi_error(17, 'A LIF with the same name already exists'), + 'error_13003': build_zapi_error(13003, 'ZAPI is not enabled in pre-cluster mode.'), +}) + + +DEFAULT_ARGS = { + 'hostname': '10.10.10.10', + 'username': 'admin', + 'password': 'password', + 'home_port': 'e0c', + 'interface_name': 'abc_if', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + module_args = { + 'vserver': 'vserver', + 'use_rest': 'never' + } + error = create_module(interface_module, module_args, fail=True)['msg'] + assert 'missing required arguments:' in error + assert 'interface_name' in error + + +def test_create_error_missing_param(): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ]) + module_args = { + 'vserver': 'vserver', + 'home_node': 'node', + 'use_rest': 'never' + } + msg = 'Error: Missing one or more required parameters for creating interface:' + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg in error + assert 'role' in error + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'net-interface-create', ZRR['success']) + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'home_node': 'node', + 'role': 'data', + # 'subnet_name': 'subnet_name', + 'address': '10.10.10.13', + 'netmask': '255.255.255.0', + 'failover_policy': 'system-defined', + 'failover_group': 'failover_group', + 'firewall_policy': 'firewall_policy', + 'is_auto_revert': True, + 'admin_status': 'down', + 'force_subnet_association': True, + 'dns_domain_name': 'dns_domain_name', + 'listen_for_dns_query': True, + 'is_dns_update_enabled': True, + # 'is_ipv4_link_local': False, + 'service_policy': 'service_policy' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ip_subnet_cidr_mask(): + ''' Test successful modify ip/subnet mask ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-modify', ZRR['success']), + ('ZAPI', 'net-interface-get-iter', ZRR['interface_ipv4']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'home_node': 'node', + 'role': 'data', + 'address': '10.10.10.13', + 'netmask': '24' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_create_for_NVMe(): + ''' Test successful create for NVMe protocol''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-node-get-iter', ZRR['node_info']), + ('ZAPI', 'net-interface-create', ZRR['success']), + ]) + module_args = { + 'vserver': 'vserver', + # 'home_node': 'node', + 'role': 'data', + 'protocols': ['fc-nvme'], + 'subnet_name': 'subnet_name', + 'use_rest': 'never' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_idempotency_for_NVMe(): + ''' Test successful create for NVMe protocol''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ]) + module_args = { + 'vserver': 'vserver', + 'home_node': 'node', + 'role': 'data', + 'protocols': ['fc-nvme'], + 'use_rest': 'never' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_error_for_NVMe(): + ''' Test if create throws an error if required param 'protocols' uses NVMe''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ]) + msg = 'Error: Following parameters for creating interface are not supported for data-protocol fc-nvme:' + module_args = { + 'vserver': 'vserver', + 'protocols': ['fc-nvme'], + 'address': '1.1.1.1', + 'use_rest': 'never' + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg in error + for option in ('netmask', 'address', 'firewall_policy'): + assert option in error + + +def test_create_idempotency(): + ''' Test create idempotency, and ignore EMS logging error ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ]) + module_args = { + 'vserver': 'vserver', + 'use_rest': 'never' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_delete(): + ''' Test delete existing interface, and ignore EMS logging error ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info_no_dns']), + ('ZAPI', 'net-interface-modify', ZRR['success']), # offline + ('ZAPI', 'net-interface-delete', ZRR['success']), + ]) + module_args = { + 'state': 'absent', + 'vserver': 'vserver', + 'use_rest': 'never' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_idempotency(): + ''' Test delete idempotency ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ]) + module_args = { + 'state': 'absent', + 'vserver': 'vserver', + 'use_rest': 'never' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify(): + ''' Test successful modify interface_minutes ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-modify', ZRR['success']), + ]) + module_args = { + 'vserver': 'vserver', + 'dns_domain_name': 'test2.com', + 'home_port': 'e0d', + 'is_dns_update_enabled': False, + 'is_ipv4_link_local': True, + 'listen_for_dns_query': False, + 'use_rest': 'never' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_idempotency(): + ''' Test modify idempotency ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ]) + module_args = { + 'vserver': 'vserver', + 'use_rest': 'never' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_message(): + register_responses([ + # create, missing params + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-node-get-iter', ZRR['no_records']), + + # create - get home_node error + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-node-get-iter', ZRR['error']), + + # create error + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-node-get-iter', ZRR['error_13003']), + ('ZAPI', 'net-interface-create', ZRR['error']), + + # create error + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-node-get-iter', ZRR['no_records']), + ('ZAPI', 'net-interface-create', ZRR['error_17']), + + # modify error + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-modify', ZRR['error']), + + # rename error + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-rename', ZRR['error']), + + # delete error + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-modify', ZRR['success']), + ('ZAPI', 'net-interface-delete', ZRR['error']), + + # get error + ('ZAPI', 'net-interface-get-iter', ZRR['error']), + ]) + module_args = { + 'vserver': 'vserver', + 'use_rest': 'never', + } + msg = 'Error: Missing one or more required parameters for creating interface:' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['home_port'] = 'e0d' + module_args['role'] = 'data' + module_args['address'] = '10.11.12.13' + module_args['netmask'] = '255.192.0.0' + msg = 'Error fetching node for interface abc_if: NetApp API failed. Reason - 12345:' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = 'Error Creating interface abc_if: NetApp API failed. Reason - 12345:' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + # LIF already exists (error 17) + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['home_port'] = 'new_port' + msg = 'Error modifying interface abc_if: NetApp API failed. Reason - 12345:' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['from_name'] = 'old_name' + msg = 'Error renaming old_name to abc_if: NetApp API failed. Reason - 12345:' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['state'] = 'absent' + msg = 'Error deleting interface abc_if: NetApp API failed. Reason - 12345:' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = 'Error fetching interface details for abc_if: NetApp API failed. Reason - 12345:' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successful_rename(): + ''' Test successful ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-rename', ZRR['success']), + ('ZAPI', 'net-interface-modify', ZRR['success']), + ]) + module_args = { + 'vserver': 'vserver', + 'dns_domain_name': 'test2.com', + 'from_name': 'from_interface_name', + 'home_port': 'new_port', + 'is_dns_update_enabled': False, + 'listen_for_dns_query': False, + 'use_rest': 'never' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_rename_not_found(): + ''' Test from interface not found ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ('ZAPI', 'net-interface-get-iter', ZRR['no_records']), + ]) + msg = 'Error renaming interface abc_if: no interface with from_name from_interface_name.' + module_args = { + 'vserver': 'vserver', + 'dns_domain_name': 'test2.com', + 'from_name': 'from_interface_name', + 'home_port': 'new_port', + 'is_dns_update_enabled': False, + 'listen_for_dns_query': False, + 'use_rest': 'never' + } + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successful_migrate(): + ''' Test successful ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-modify', ZRR['success']), + ('ZAPI', 'net-interface-migrate', ZRR['success']), + ('ZAPI', 'net-interface-migrate', ZRR['success']), + ]) + module_args = { + 'vserver': 'vserver', + 'dns_domain_name': 'test2.com', + 'current_node': 'new_node', + 'is_dns_update_enabled': False, + 'listen_for_dns_query': False, + 'use_rest': 'never' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_migrate(): + ''' Test successful ''' + register_responses([ + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-modify', ZRR['success']), + + # 2nd try + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-modify', ZRR['success']), + ('ZAPI', 'net-interface-migrate', ZRR['error']), + + # 3rd try + ('ZAPI', 'net-interface-get-iter', ZRR['interface_info']), + ('ZAPI', 'net-interface-modify', ZRR['success']), + ('ZAPI', 'net-interface-migrate', ZRR['success']), + ('ZAPI', 'net-interface-migrate', ZRR['error']), + ]) + module_args = { + 'vserver': 'vserver', + 'dns_domain_name': 'test2.com', + 'current_port': 'new_port', + 'is_dns_update_enabled': False, + 'listen_for_dns_query': False, + 'use_rest': 'never' + } + msg = 'current_node must be set to migrate' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['current_node'] = 'new_node' + msg = 'Error migrating new_node: NetApp API failed. Reason - 12345' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = 'Error migrating new_node: NetApp API failed. Reason - 12345' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +SRR = rest_responses({ + 'one_record_home_node': (200, {'records': [ + {'name': 'node2_abc_if', + 'uuid': '54321', + 'enabled': True, + 'location': {'home_port': {'name': 'e0c'}, 'home_node': {'name': 'node2'}, 'node': {'name': 'node2'}, 'port': {'name': 'e0c'}} + }]}, None), + 'one_record_vserver': (200, {'records': [{ + 'name': 'abc_if', + 'uuid': '54321', + 'svm': {'name': 'vserver', 'uuid': 'svm_uuid'}, + 'dns_zone': 'netapp.com', + 'ddns_enabled': True, + 'data_protocol': ['nfs'], + 'enabled': True, + 'ip': {'address': '10.11.12.13', 'netmask': '10'}, + 'location': { + 'home_port': {'name': 'e0c'}, + 'home_node': {'name': 'node2'}, + 'node': {'name': 'node2'}, + 'port': {'name': 'e0c'}, + 'auto_revert': True, + 'failover': True + }, + 'service_policy': {'name': 'data-mgmt'}, + 'probe_port': 65431 + }]}, None), + 'one_record_vserver_subnet1': (200, {'records': [{ + 'name': 'abc_if', + 'uuid': '54321', + 'svm': {'name': 'vserver', 'uuid': 'svm_uuid'}, + 'dns_zone': 'netapp.com', + 'ddns_enabled': True, + 'data_protocol': ['nfs'], + 'enabled': True, + 'ip': {'address': '10.11.12.13', 'netmask': '10'}, + 'location': { + 'home_port': {'name': 'e0c'}, + 'home_node': {'name': 'node2'}, + 'node': {'name': 'node2'}, + 'port': {'name': 'e0c'}, + 'auto_revert': True, + 'failover': True + }, + 'service_policy': {'name': 'data-mgmt'}, + 'subnet': {'name': 'subnet1'} + }]}, None), + 'one_record_fcp': (200, {'records': [{ + 'data_protocol': 'fcp', + 'enabled': False, + 'location': { + 'home_node': {'name': 'ontap910-01', 'uuid': 'ecb4061b'}, + 'home_port': {'name': '1a', 'node': {'name': 'ontap910-01'}, 'uuid': '1c9a72de'}, + 'is_home': True, + 'node': {'name': 'ontap910-01', 'uuid': 'ecb4061b'}, + 'port': {'name': '1a', 'node': {'name': 'ontap910-01'}, 'uuid': '1c9a72de'} + }, + 'name': 'abc_if', + 'svm': {'name': 'svm0_iscsi', 'uuid': 'a59e775d'}, + 'uuid': 'a3935ab5' + }]}, None), + 'two_records': (200, {'records': [{'name': 'node2_abc_if'}, {'name': 'node2_abc_if'}]}, None), + 'error_precluster': (500, None, {'message': 'are available in precluster.'}), + 'cluster_identity': (200, {'location': 'Oz', 'name': 'abc'}, None), + 'nodes': (200, {'records': [ + {'name': 'node2', 'uuid': 'uuid2', 'cluster_interfaces': [{'ip': {'address': '10.10.10.2'}}]} + ]}, None), + 'nodes_two_records': (200, {'records': [ + {'name': 'node2', 'uuid': 'uuid2', 'cluster_interfaces': [{'ip': {'address': '10.10.10.2'}}]}, + {'name': 'node3', 'uuid': 'uuid2', 'cluster_interfaces': [{'ip': {'address': '10.10.10.2'}}]} + ]}, None), +}, False) + + +def test_rest_create_ip_no_svm(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ('POST', 'network/ip/interfaces', SRR['success']), # post + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_create_ip_no_svm_idempotent(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_create_ip_no_svm_idempotent_localhost(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'home_node': 'localhost', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_create_ip_with_svm(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ('POST', 'network/ip/interfaces', SRR['success']), # post + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'vserver': 'vserver', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_create_fc_with_svm(): + ''' create FC interface ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/fc/interfaces', SRR['zero_records']), # get FC + ('POST', 'network/fc/interfaces', SRR['success']), # post + ]) + module_args = { + 'use_rest': 'always', + 'vserver': 'vserver', + 'data_protocol': 'fc_nvme', + 'home_node': 'my_node', + 'protocols': 'fc-nvme' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_create_fc_with_svm_no_home_port(): + ''' create FC interface ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/fc/interfaces', SRR['zero_records']), # get FC + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ('POST', 'network/fc/interfaces', SRR['success']), # post + ]) + args = dict(DEFAULT_ARGS) + module_args = { + 'use_rest': 'always', + 'vserver': 'vserver', + 'data_protocol': 'fc_nvme', + 'protocols': 'fc-nvme', + 'current_port': args.pop('home_port'), + 'current_node': 'my_node', + } + assert call_main(my_main, args, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_create_ip_with_cluster_svm(dont_sleep): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ('POST', 'network/ip/interfaces', SRR['one_record_vserver']), # post + ('PATCH', 'network/ip/interfaces/54321', SRR['one_record_vserver']), # migrate + ('GET', 'network/ip/interfaces', SRR['one_record_vserver']), # get IP + ]) + module_args = { + 'use_rest': 'always', + 'admin_status': 'up', + 'current_port': 'e0c', + 'failover_scope': 'home_node_only', + 'ipspace': 'cluster', + 'vserver': 'vserver', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'role': 'intercluster', + 'probe_port': 65431, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised('Ignoring vserver with REST for non data SVM.') + + +def test_rest_negative_create_ip(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ('GET', 'cluster/nodes', SRR['zero_records']), # get nodes + ]) + msg = 'Error: Cannot guess home_node, home_node is required when home_port is present with REST.' + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + } + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_negative_create_ip_with_svm_no_home_port(): + ''' create FC interface ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + # ('POST', 'network/fc/interfaces', SRR['success']), # post + ]) + args = dict(DEFAULT_ARGS) + args.pop('home_port') + module_args = { + 'use_rest': 'always', + 'vserver': 'vserver', + 'interface_type': 'ip', + } + error = "Error: At least one of 'broadcast_domain', 'home_port', 'home_node' is required to create an IP interface." + assert error in call_main(my_main, args, module_args, fail=True)['msg'] + + +def test_rest_negative_create_no_ip_address(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ]) + msg = 'Error: Missing one or more required parameters for creating interface: interface_type.' + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + } + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_get_fc_no_svm(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ]) + module_args = { + 'use_rest': 'always', + 'interface_type': 'fc', + } + msg = "A data 'vserver' is required for FC interfaces." + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_negative_get_multiple_ip_if(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['two_records']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ]) + msg = 'Error: multiple records for: node2_abc_if' + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + } + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_negative_get_multiple_fc_if(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ('GET', 'network/fc/interfaces', SRR['two_records']), # get FC + ]) + msg = 'Error: unexpected records for name: abc_if, vserver: not_cluster' + module_args = { + 'use_rest': 'always', + 'vserver': 'not_cluster', + } + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_negative_get_multiple_ip_fc_if(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_vserver']), # get IP + ('GET', 'network/fc/interfaces', SRR['one_record_vserver']), # get FC + ]) + msg = 'Error fetching interface abc_if - found duplicate entries, please indicate interface_type.' + module_args = { + 'use_rest': 'always', + 'vserver': 'not_cluster', + } + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_modify_idempotent_ip_no_svm(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_modify_ip_no_svm(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'home_node': 'node2', + 'interface_name': 'new_name', + 'from_name': 'abc_if' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_modify_ip_svm(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_vserver']), # get IP + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'vserver': 'vserver', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'home_node': 'node1', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_migrate_ip_no_svm(sleep_mock): + ''' create cluster ''' + modified = copy.deepcopy(SRR['one_record_home_node']) + modified[1]['records'][0]['location']['node']['name'] = 'node1' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes (for get) + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get - no change + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', modified), + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'current_node': 'node1', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_migrate_ip_no_svm_port(sleep_mock): + ''' create cluster ''' + modified = copy.deepcopy(SRR['one_record_home_node']) + modified[1]['records'][0]['location']['port']['name'] = 'port1' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes (for get) + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get - no change + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', modified), + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'current_port': 'port1', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_migrate_ip_svm(sleep_mock): + ''' create cluster ''' + modified = copy.deepcopy(SRR['one_record_home_node']) + modified[1]['records'][0]['location']['node']['name'] = 'node1' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes (for get) + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', modified), + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'current_node': 'node1', + 'vserver': 'vserver' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_migrate_ip_error(sleep_mock): + ''' create cluster ''' + modified = copy.deepcopy(SRR['one_record_home_node']) + modified[1]['records'][0]['location']['node']['name'] = 'node1' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes (for get) + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'current_node': 'node1', + 'vserver': 'vserver' + } + error = rest_error_message('Errors waiting for migration to complete', 'network/ip/interfaces') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_rest_migrate_ip_timeout(sleep_mock): + ''' create cluster ''' + modified = copy.deepcopy(SRR['one_record_home_node']) + modified[1]['records'][0]['location']['node']['name'] = 'node1' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes (for get) + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'current_node': 'node1', + 'vserver': 'vserver' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_warning_was_raised('Failed to confirm interface is migrated after 120 seconds') + + +def test_rest_create_migrate_fc_error(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/fc/interfaces', SRR['empty_records']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/fc/interfaces', SRR['one_record_fcp']) + ]) + module_args = { + 'use_rest': 'always', + 'home_node': 'ontap910-01', + 'current_node': 'ontap910-02', + 'current_port': '1b', + 'interface_type': 'fc', + 'vserver': 'svm0_iscsi' + } + error = 'Error: Missing one or more required parameters for creating interface' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['home_port'] = '1a' + error = 'Error: cannot migrate FC interface' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_delete_ip_no_svm(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), # get IP + ('GET', 'cluster/nodes', SRR['nodes']), # get nodes (for get) + ('DELETE', 'network/ip/interfaces/54321', SRR['success']), # delete + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'state': 'absent', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_disable_delete_fc(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/fc/interfaces', SRR['one_record_vserver']), # get IP + ('PATCH', 'network/fc/interfaces/54321', SRR['success']), # disable fc before delete + ('DELETE', 'network/fc/interfaces/54321', SRR['success']), # delete + ]) + module_args = { + 'use_rest': 'always', + 'state': 'absent', + "admin_status": "up", + "protocols": "fc-nvme", + "role": "data", + "vserver": "svm3", + "current_port": "1a" + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_delete_idempotent_ip_no_svm(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), # get IP + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'address': '10.12.12.13', + 'netmask': '255.255.192.0', + 'state': 'absent', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_derive_fc_protocol_fcp(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'protocols': ['fcp'], + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + my_obj.derive_fc_data_protocol() + assert my_obj.parameters['data_protocol'] == 'fcp' + + +def test_derive_fc_protocol_nvme(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'protocols': ['fc-nvme'], + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + my_obj.derive_fc_data_protocol() + assert my_obj.parameters['data_protocol'] == 'fc_nvme' + + +def test_derive_fc_protocol_empty(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'protocols': [], + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + assert my_obj.derive_fc_data_protocol() is None + + +def test_negative_derive_fc_protocol_nvme(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'protocols': ['fc-nvme', 'fcp'], + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + msg = "A single protocol entry is expected for FC interface, got ['fc-nvme', 'fcp']." + assert msg in expect_and_capture_ansible_exception(my_obj.derive_fc_data_protocol, 'fail')['msg'] + + +def test_negative_derive_fc_protocol_nvme_mismatch(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'protocols': ['fc-nvme'], + 'data_protocol': 'fcp' + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + msg = "Error: mismatch between configured data_protocol: fcp and data_protocols: ['fc-nvme']" + assert msg in expect_and_capture_ansible_exception(my_obj.derive_fc_data_protocol, 'fail')['msg'] + + +def test_negative_derive_fc_protocol_unexpected(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'protocols': ['fc-unknown'], + 'data_protocol': 'fcp' + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + msg = "Unexpected protocol value fc-unknown." + assert msg in expect_and_capture_ansible_exception(my_obj.derive_fc_data_protocol, 'fail')['msg'] + + +def test_derive_interface_type_nvme(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'protocols': ['fc-nvme'], + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + my_obj.derive_interface_type() + assert my_obj.parameters['interface_type'] == 'fc' + + +def test_derive_interface_type_iscsi(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'protocols': ['iscsi'], + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + my_obj.derive_interface_type() + assert my_obj.parameters['interface_type'] == 'ip' + + +def test_derive_interface_type_cluster(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'role': 'cluster', + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + my_obj.derive_interface_type() + assert my_obj.parameters['interface_type'] == 'ip' + + +def test_negative_derive_interface_type_nvme_mismatch(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + msg = "Error: mismatch between configured interface_type: ip and derived interface_type: fc." + module_args = { + 'use_rest': 'always', + 'protocols': ['fc-nvme'], + 'interface_type': 'ip' + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + assert msg in expect_and_capture_ansible_exception(my_obj.derive_interface_type, 'fail')['msg'] + + +def test_negative_derive_interface_type_unknown(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + msg = "Error: unable to determine interface type, please set interface_type: unexpected value(s) for protocols: ['unexpected']" + module_args = { + 'use_rest': 'always', + 'protocols': ['unexpected'], + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + assert msg in expect_and_capture_ansible_exception(my_obj.derive_interface_type, 'fail')['msg'] + + +def test_negative_derive_interface_type_multiple(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + msg = "Error: unable to determine interface type, please set interface_type: incompatible value(s) for protocols: ['fc-nvme', 'cifs']" + module_args = { + 'use_rest': 'always', + 'protocols': ['fc-nvme', 'cifs'], + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + assert msg in expect_and_capture_ansible_exception(my_obj.derive_interface_type, 'fail')['msg'] + + +def test_derive_block_file_type_fcp(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + block_p, file_p, fcp = my_obj.derive_block_file_type(['fcp']) + assert block_p + assert not file_p + assert fcp + module_args['interface_type'] = 'fc' + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + block_p, file_p, fcp = my_obj.derive_block_file_type(None) + assert block_p + assert not file_p + assert fcp + + +def test_derive_block_file_type_iscsi(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + block_p, file_p, fcp = my_obj.derive_block_file_type(['iscsi']) + assert block_p + assert not file_p + assert not fcp + + +def test_derive_block_file_type_cifs(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + block_p, file_p, fcp = my_obj.derive_block_file_type(['cifs']) + assert not block_p + assert file_p + assert not fcp + + +def test_derive_block_file_type_mixed(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + error = "Cannot use any of ['fcp'] with ['cifs']" + assert expect_and_capture_ansible_exception(my_obj.derive_block_file_type, 'fail', ['cifs', 'fcp'])['msg'] == error + + +def test_map_failover_policy(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'failover_policy': 'local-only', + } + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + my_obj.map_failover_policy() + assert my_obj.parameters['failover_scope'] == 'home_node_only' + + +def test_rest_negative_unsupported_zapi_option_fail(): + ''' create cluster ''' + register_responses([ + ]) + msg = "REST API currently does not support 'is_ipv4_link_local'" + module_args = { + 'use_rest': 'always', + 'ipspace': 'cluster', + 'is_ipv4_link_local': True, + } + assert msg in create_module(interface_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_negative_rest_only_option(): + ''' create cluster ''' + register_responses([ + ]) + msg = "probe_port requires REST." + module_args = { + 'use_rest': 'never', + 'ipspace': 'cluster', + 'probe_port': 65431, + } + assert msg in create_module(interface_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_negative_unsupported_zapi_option_force_zapi_1(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + msg = "missing required argument with ZAPI: vserver" + module_args = { + 'use_rest': 'auto', + 'ipspace': 'cluster', + 'is_ipv4_link_local': True, + } + assert msg in create_module(interface_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_rest_negative_unsupported_zapi_option_force_zapi_2(mock_netapp_lib): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + mock_netapp_lib.return_value = False + msg = "the python NetApp-Lib module is required" + module_args = { + 'use_rest': 'auto', + 'ipspace': 'cluster', + 'is_ipv4_link_local': True, + } + assert msg in create_module(interface_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_negative_unsupported_rest_version(): + ''' create cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + msg = "Error: REST requires ONTAP 9.7 or later for interface APIs." + module_args = {'use_rest': 'always'} + assert msg == create_module(interface_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_auto_falls_back_to_zapi_if_ip_9_6(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + module_args = {'use_rest': 'auto'} + # vserver is a required parameter with ZAPI + msg = "missing required argument with ZAPI: vserver" + assert msg in create_module(interface_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + print_warnings + assert_warning_was_raised('Falling back to ZAPI: REST requires ONTAP 9.7 or later for interface APIs.') + + +def test_fix_errors(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']) + ]) + module_args = {'use_rest': 'auto'} + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + control = {'xx': 11, 'yy': 22} + # no role in error + errors = dict(control) + assert my_obj.fix_errors(None, errors) is None + assert errors == control + # role/firewall_policy/protocols/service_policy -> service_policy + tests = [ + ('data', 'data', ['nfs'], None, 'default-data-files', True), + ('data', 'data', ['cifs'], None, 'default-data-files', True), + ('data', 'data', ['iscsi'], None, 'default-data-blocks', True), + ('data', '', ['fc-nvme'], None, 'unchanged', True), + ('data', 'mgmt', ['ignored'], None, 'default-management', True), + ('data', '', ['nfs'], None, 'default-data-files', True), + ('data', '', ['cifs'], None, 'default-data-files', True), + ('data', '', ['iscsi'], None, 'default-data-blocks', True), + ('data', 'mgmt', ['ignored'], None, 'default-management', True), + ('intercluster', 'intercluster', ['ignored'], None, 'default-intercluster', True), + ('intercluster', '', ['ignored'], None, 'default-intercluster', True), + ('cluster', 'mgmt', ['ignored'], None, 'default-cluster', True), + ('cluster', '', ['ignored'], None, 'default-cluster', True), + ('cluster', 'other', ['ignored'], None, 'unchanged', False), + ] + for role, firewall_policy, protocols, service_policy, expected_service_policy, fixed in tests: + my_obj.parameters['protocols'] = protocols + if service_policy: + my_obj['service_policy'] = service_policy + options = {'service_policy': 'unchanged'} + errors = dict(control) + errors['role'] = role + if firewall_policy: + errors['firewall_policy'] = firewall_policy + assert my_obj.fix_errors(options, errors) is None + print('OPTIONS', options) + assert 'service_policy' in options + assert options['service_policy'] == expected_service_policy + assert errors == control or not fixed + assert fixed or 'role' in errors + + +def test_error_messages_get_interface_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'network/ip/interfaces', SRR['two_records']), # get IP + ('GET', 'cluster/nodes', SRR['generic_error']), # get nodes + # second call + ('GET', 'network/ip/interfaces', SRR['one_record_vserver']), # get IP + ('GET', 'network/fc/interfaces', SRR['generic_error']), # get FC + # third call + ('GET', 'network/ip/interfaces', SRR['generic_error']), # get IP + ('GET', 'network/fc/interfaces', SRR['one_record_vserver']), # get FC + # fourth call + ('GET', 'network/ip/interfaces', SRR['generic_error']), # get IP + ('GET', 'network/fc/interfaces', SRR['generic_error']), # get FC + # fifth call + ('GET', 'network/ip/interfaces', SRR['error_precluster']), # get IP + ]) + module_args = {'use_rest': 'auto'} + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + # first call + error = 'Error fetching cluster node info' + assert expect_and_capture_ansible_exception(my_obj.get_interface_rest, 'fail', 'my_lif')['msg'] == rest_error_message(error, 'cluster/nodes') + # second call + # reset value, as it was set for ip + del my_obj.parameters['interface_type'] + my_obj.parameters['vserver'] = 'not_cluster' + assert my_obj.get_interface_rest('my_lif') is not None + # third call + # reset value, as it was set for ip + del my_obj.parameters['interface_type'] + my_obj.parameters['vserver'] = 'not_cluster' + assert my_obj.get_interface_rest('my_lif') is not None + # fourth call + # reset value, as it was set for fc + del my_obj.parameters['interface_type'] + error = expect_and_capture_ansible_exception(my_obj.get_interface_rest, 'fail', 'my_lif')['msg'] + assert rest_error_message('Error fetching interface details for my_lif', 'network/ip/interfaces') in error + assert rest_error_message('', 'network/fc/interfaces') in error + # fifth call + error = 'This module cannot use REST in precluster mode, ZAPI can be forced with use_rest: never.' + assert error in expect_and_capture_ansible_exception(my_obj.get_interface_rest, 'fail', 'my_lif')['msg'] + + +def test_error_messages_rest_find_interface(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster/nodes', SRR['nodes_two_records']), # get nodes + ]) + module_args = {'use_rest': 'auto'} + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + # no calls + # no interface type + error = 'Error: missing option "interface_type (or could not be derived)' + assert error in expect_and_capture_ansible_exception(my_obj.get_net_int_api, 'fail')['msg'] + # multiple records for cluster + records = [ + {'name': 'node_name'}, + {'name': 'node_name'} + ] + error = 'Error: multiple records for: node_name - %s' % records + assert error in expect_and_capture_ansible_exception(my_obj.find_interface_record, 'fail', records, 'node', 'name')['msg'] + # multiple records with vserver + records = [1, 2] + my_obj.parameters['vserver'] = 'vserver' + error = 'Error: unexpected records for name: name, vserver: vserver - [1, 2]' + assert error in expect_and_capture_ansible_exception(my_obj.find_exact_match, 'fail', records, 'name')['msg'] + # multiple records with ambiguity, home_node set (warn) + del my_obj.parameters['vserver'] + my_obj.parameters['home_node'] = 'node' + records = [ + {'name': 'node_name'}, + {'name': 'node_name'} + ] + error = 'Error: multiple records for: node_name - %s' % records + assert error in expect_and_capture_ansible_exception(my_obj.find_exact_match, 'fail', records, 'name')['msg'] + records = [ + {'name': 'node_name'}, + {'name': 'name'} + ] + record = my_obj.find_exact_match(records, 'name') + assert record == {'name': 'node_name'} + assert_warning_was_raised("Found both ['name', 'node_name'], selecting node_name") + # fifth call (get nodes, cached) + # multiple records with different home nodes + del my_obj.parameters['home_node'] + records = [ + {'name': 'node2_name'}, + {'name': 'node3_name'} + ] + error = "Error: multiple matches for name: name: ['node2_name', 'node3_name']. Set home_node parameter." + assert error in expect_and_capture_ansible_exception(my_obj.find_exact_match, 'fail', records, 'name')['msg'] + # multiple records with home node and no home node + records = [ + {'name': 'node2_name'}, + {'name': 'name'} + ] + error = "Error: multiple matches for name: name: ['name', 'node2_name']. Set home_node parameter." + assert error in expect_and_capture_ansible_exception(my_obj.find_exact_match, 'fail', records, 'name')['msg'] + # sixth call + error = "Error: multiple matches for name: name: ['name', 'node2_name']. Set home_node parameter." + assert error in expect_and_capture_ansible_exception(my_obj.find_exact_match, 'fail', records, 'name')['msg'] + + +def test_error_messages_rest_misc(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('POST', 'network/type/interfaces', SRR['generic_error']), + ('PATCH', 'network/type/interfaces/uuid', SRR['generic_error']), + ('DELETE', 'network/type/interfaces/uuid', SRR['generic_error']), + ]) + module_args = {'use_rest': 'auto'} + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + # no calls + # no interface type + error = 'Error, expecting uuid in existing record' + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_payloads, 'fail', 'delete', {}, {})['msg'] + my_obj.parameters['interface_type'] = 'type' + error = rest_error_message('Error creating interface abc_if', 'network/type/interfaces') + assert error in expect_and_capture_ansible_exception(my_obj.create_interface_rest, 'fail', {})['msg'] + error = rest_error_message('Error modifying interface abc_if', 'network/type/interfaces/uuid') + assert error in expect_and_capture_ansible_exception(my_obj.modify_interface_rest, 'fail', 'uuid', {'xxx': 'yyy'})['msg'] + error = rest_error_message('Error deleting interface abc_if', 'network/type/interfaces/uuid') + assert error in expect_and_capture_ansible_exception(my_obj.delete_interface_rest, 'fail', 'uuid')['msg'] + + +def test_error_messages_build_rest_body_and_validations(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = {'use_rest': 'always'} + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + my_obj.parameters['home_node'] = 'node1' + my_obj.parameters['protocols'] = ['nfs'] + my_obj.parameters['role'] = 'intercluster' + error = 'Error: Missing one or more required parameters for creating interface: interface_type.' + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail')['msg'] + my_obj.parameters['interface_type'] = 'type' + error = 'Error: unexpected value for interface_type: type.' + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail')['msg'] + my_obj.parameters['interface_type'] = 'ip' + my_obj.parameters['ipspace'] = 'ipspace' + error = 'Error: Protocol cannot be specified for intercluster role, failed to create interface.' + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail')['msg'] + del my_obj.parameters['protocols'] + my_obj.parameters['interface_type'] = 'fc' + error = "Error: 'home_port' is not supported for FC interfaces with 9.7, use 'current_port', avoid home_node." + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail')['msg'] + print_warnings() + assert_warning_was_raised("Avoid 'home_node' with FC interfaces with 9.7, use 'current_node'.") + del my_obj.parameters['home_port'] + error = "Error: A data 'vserver' is required for FC interfaces." + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail')['msg'] + my_obj.parameters['current_port'] = '0a' + my_obj.parameters['data_protocol'] = 'fc' + my_obj.parameters['force_subnet_association'] = True + my_obj.parameters['failover_group'] = 'failover_group' + my_obj.parameters['vserver'] = 'vserver' + error = "Error: 'role' is deprecated, and 'data' is the only value supported for FC interfaces: found intercluster." + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail')['msg'] + my_obj.parameters['role'] = 'data' + error = "Error creating interface, unsupported options: {'failover_group': 'failover_group'}" + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail')['msg'] + del my_obj.parameters['failover_group'] + my_obj.parameters['broadcast_domain'] = 'BDD1' + error = "Error: broadcast_domain option only supported for IP interfaces: abc_if, interface_type: fc" + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail', None)['msg'] + my_obj.parameters['service_policy'] = 'svc_pol' + error = "Error: 'service_policy' is not supported for FC interfaces." + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail', None)['msg'] + del my_obj.parameters['service_policy'] + my_obj.parameters['probe_port'] = 65431 + error = "Error: 'probe_port' is not supported for FC interfaces." + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail', None)['msg'] + print_warnings() + assert_warning_was_raised('Ignoring force_subnet_association') + my_obj.parameters['interface_type'] = 'ip' + del my_obj.parameters['vserver'] + del my_obj.parameters['ipspace'] + error = 'Error: ipspace name must be provided if scope is cluster, or vserver for svm scope.' + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail')['msg'] + modify = {'ipspace': 'ipspace'} + error = "The following option cannot be modified: ipspace.name" + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail', modify)['msg'] + del my_obj.parameters['role'] + my_obj.parameters['current_port'] = 'port1' + my_obj.parameters['home_port'] = 'port1' + my_obj.parameters['ipspace'] = 'ipspace' + error = "Error: home_port and broadcast_domain are mutually exclusive for creating: abc_if" + assert error in expect_and_capture_ansible_exception(my_obj.build_rest_body, 'fail', None)['msg'] + + +def test_dns_domain_ddns_enabled(): + ''' domain and ddns enabled option test ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), + ('GET', 'cluster/nodes', SRR['nodes']), + ('POST', 'network/ip/interfaces', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'network/ip/interfaces', SRR['one_record_vserver']), + ('GET', 'cluster/nodes', SRR['nodes']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'network/fc/interfaces', SRR['zero_records']), + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + module_args = { + 'use_rest': 'always', + 'address': '10.11.12.13', + 'netmask': '255.192.0.0', + 'vserver': 'vserver', + 'dns_domain_name': 'netapp1.com', + 'is_dns_update_enabled': False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + del module_args['address'] + del module_args['netmask'] + args = {'data_protocol': 'fc_nvme', 'home_node': 'my_node', 'protocols': 'fc-nvme', 'interface_type': 'fc'} + module_args.update(args) + assert 'dns_domain_name, is_dns_update_enabled options only supported for IP interfaces' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error: Minimum version of ONTAP for is_dns_update_enabled is (9, 9, 1).' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_subnet_name(): + ''' domain and ddns enabled option test ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), + ('GET', 'cluster/nodes', SRR['nodes']), + ('POST', 'network/ip/interfaces', SRR['success']), + # idemptocency + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/interfaces', SRR['one_record_vserver_subnet1']), + # modify subnet + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/interfaces', SRR['one_record_vserver_subnet1']), + ('GET', 'cluster/nodes', SRR['nodes']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + # error cases + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/fc/interfaces', SRR['zero_records']), + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'Default', + 'subnet_name': 'subnet1', + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['subnet_name'] = 'subnet2' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert 'Minimum version of ONTAP for subnet_name is (9, 11, 1)' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + args = {'data_protocol': 'fc_nvme', 'home_node': 'my_node', 'protocols': 'fc-nvme', 'interface_type': 'fc'} + module_args.update(args) + assert 'subnet_name option only supported for IP interfaces' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_warning_was_raised('ipspace is ignored for FC interfaces.') + + +def test_fail_if_subnet_conflicts(): + ''' domain and ddns enabled option test ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), + ('GET', 'cluster/nodes', SRR['nodes']), + ('POST', 'network/ip/interfaces', SRR['success']), + # idemptocency + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/interfaces', SRR['one_record_vserver']), + # modify subnet + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/interfaces', SRR['one_record_vserver']), + ('GET', 'cluster/nodes', SRR['nodes']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + # error cases + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/fc/interfaces', SRR['zero_records']), + ]) + module_args = { + 'use_rest': 'always', + 'ipspace': 'Default', + 'fail_if_subnet_conflicts': False, + 'vserver': 'vserver', + 'address': '10.11.12.13', + 'netmask': '255.192.0.0', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['address'] = '10.11.12.14' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert 'Minimum version of ONTAP for fail_if_subnet_conflicts is (9, 11, 1)' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + args = {'data_protocol': 'fc_nvme', 'home_node': 'my_node', 'protocols': 'fc-nvme', 'interface_type': 'fc'} + module_args.update(args) + assert 'fail_if_subnet_conflicts option only supported for IP interfaces' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_warning_was_raised('ipspace is ignored for FC interfaces.') + + +def check_options(my_obj, parameters, exp_options, exp_migrate_options, exp_errors): + options, migrate_options, errors = my_obj.set_options_rest(parameters) + assert options == exp_options + assert migrate_options == exp_migrate_options + assert errors == exp_errors + + +def test_set_options_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + # ('GET', 'cluster/nodes', SRR['nodes']), + ]) + module_args = {'use_rest': 'always'} + my_obj = create_module(interface_module, DEFAULT_ARGS, module_args) + parameters = None + my_obj.parameters = { + 'interface_type': 'other' + } + check_options(my_obj, parameters, {}, {}, {}) + # unknown modify options + check_options(my_obj, {'x': 'y'}, {}, {}, {}) + # valid options + my_obj.parameters = { + 'interface_type': 'ip', + 'fail_if_subnet_conflicts': False + } + check_options(my_obj, parameters, {'fail_if_subnet_conflicts': False}, {}, {}) + check_options(my_obj, {'subnet_name': 'subnet1'}, {'subnet.name': 'subnet1'}, {}, {}) + my_obj.parameters['home_node'] = 'node1' + check_options(my_obj, {'home_node': 'node1', 'home_port': 'port1'}, {'location': {'home_port': {'name': 'port1', 'node': {'name': 'node1'}}}}, {}, {}) + my_obj.parameters['current_node'] = 'node1' + check_options(my_obj, {'current_node': 'node1', 'current_port': 'port1'}, {}, {'location': {'port': {'name': 'port1', 'node': {'name': 'node1'}}}}, {}) + + +def test_not_throw_warnings_in_rename(): + ''' assert no warnings raised during rename ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'network/ip/interfaces', SRR['zero_records']), + ('GET', 'network/ip/interfaces', SRR['one_record_vserver']), + ('GET', 'cluster/nodes', SRR['nodes']), + ('PATCH', 'network/ip/interfaces/54321', SRR['success']), + ]) + module_args = { + "from_name": "abc_if", + "interface_name": "abc_if_update", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_no_warnings() + + +def test_throw_warnings_modify_rename(): + ''' assert warnings raised when interface_name does not have node name in it. ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'network/ip/interfaces', SRR['one_record_home_node']), + ('GET', 'cluster/nodes', SRR['nodes']) + ]) + assert not call_main(my_main, DEFAULT_ARGS)['changed'] + print_warnings() + # current record name is 'node2_abc_if' and interface_name does not have node name in it. + # adjust to avoid rename attempt. + assert_warning_was_raised('adjusting name from abc_if to node2_abc_if') diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ipspace.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ipspace.py new file mode 100644 index 000000000..9a23f06b9 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ipspace.py @@ -0,0 +1,189 @@ +# (c) 2018, NTT Europe Ltd. +# (c) 2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit test for Ansible module: na_ontap_ipspace """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ipspace \ + import NetAppOntapIpspace as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +DEFAULT_ARGS = { + "hostname": "10.10.10.10", + "username": "admin", + "password": "netapp1!", + "validate_certs": "no", + "https": "yes", + "state": "present", + "name": "test_ipspace" +} + + +ipspace_info = { + 'num-records': 1, + 'attributes-list': { + 'net-ipspaces-info': { + 'ipspace': 'test_ipspace' + } + } +} + +ipspace_info_renamed = { + 'num-records': 1, + 'attributes-list': { + 'net-ipspaces-info': { + 'ipspace': 'test_ipspace_renamed' + } + } +} + +ZRR = zapi_responses({ + 'ipspace_info': build_zapi_response(ipspace_info), + 'ipspace_info_renamed': build_zapi_response(ipspace_info_renamed), +}) + +SRR = rest_responses({ + 'ipspace_record': (200, {'records': [{ + "name": "test_ipspace", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412"}]}, None), + 'ipspace_record_renamed': (200, {'records': [{ + "name": "test_ipspace_renamed", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412"}]}, None) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +def test_get_ipspace_iscalled(): + ''' test if get_ipspace() is called ''' + register_responses([ + ('net-ipspaces-get-iter', ZRR['empty']) + ]) + ipsace_obj = create_module(my_module, DEFAULT_ARGS, {'use_rest': 'never'}) + result = ipsace_obj.get_ipspace('dummy') + assert result is None + + +def test_ipspace_apply_iscalled(): + ''' test if apply() is called - create and rename''' + register_responses([ + # create + ('net-ipspaces-get-iter', ZRR['empty']), + ('net-ipspaces-create', ZRR['success']), + # create idempotent check + ('net-ipspaces-get-iter', ZRR['ipspace_info']), + # rename + ('net-ipspaces-get-iter', ZRR['empty']), + ('net-ipspaces-get-iter', ZRR['ipspace_info']), + ('net-ipspaces-rename', ZRR['success']), + # rename idempotent check + ('net-ipspaces-get-iter', ZRR['ipspace_info_renamed']), + # delete + ('net-ipspaces-get-iter', ZRR['ipspace_info']), + ('net-ipspaces-destroy', ZRR['success']) + ]) + args = {'use_rest': 'never'} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + args['from_name'] = 'test_ipspace' + args['name'] = 'test_ipspace_renamed' + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + args = {'use_rest': 'never', 'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_create_rest(): + ''' Test successful create and idempotent check''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'network/ipspaces', SRR['empty_records']), + ('POST', 'network/ipspaces', SRR['success']), + # idempotent + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'network/ipspaces', SRR['ipspace_record']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_successful_delete_rest(): + ''' Test successful delete and idempotent check''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'network/ipspaces', SRR['ipspace_record']), + ('DELETE', 'network/ipspaces/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + # idempotent + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'network/ipspaces', SRR['empty_records']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_successful_rename_rest(): + ''' Test successful rename and idempotent check''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'network/ipspaces', SRR['empty_records']), + ('GET', 'network/ipspaces', SRR['ipspace_record']), + ('PATCH', 'network/ipspaces/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + # idempotent + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'network/ipspaces', SRR['ipspace_record_renamed']) + ]) + args = {'from_name': 'test_ipspace', 'name': 'test_ipspace_renamed'} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_if_all_methods_catch_exception_zapi_rest(): + register_responses([ + # zapi + ('net-ipspaces-get-iter', ZRR['error']), + ('net-ipspaces-create', ZRR['error']), + ('net-ipspaces-rename', ZRR['error']), + ('net-ipspaces-destroy', ZRR['error']), + # REST + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'network/ipspaces', SRR['generic_error']), + ('POST', 'network/ipspaces', SRR['generic_error']), + ('PATCH', 'network/ipspaces/abdcdef', SRR['generic_error']), + ('DELETE', 'network/ipspaces/abdcdef', SRR['generic_error']) + + ]) + my_obj = create_module(my_module, DEFAULT_ARGS, {'from_name': 'test_ipspace_rename', 'use_rest': 'never'}) + assert 'Error getting ipspace' in expect_and_capture_ansible_exception(my_obj.get_ipspace, 'fail')['msg'] + assert 'Error provisioning ipspace' in expect_and_capture_ansible_exception(my_obj.create_ipspace, 'fail')['msg'] + assert 'Error renaming ipspace' in expect_and_capture_ansible_exception(my_obj.rename_ipspace, 'fail')['msg'] + assert 'Error removing ipspace' in expect_and_capture_ansible_exception(my_obj.delete_ipspace, 'fail')['msg'] + + my_obj = create_module(my_module, DEFAULT_ARGS, {'from_name': 'test_ipspace_rename'}) + my_obj.uuid = 'abdcdef' + assert 'Error getting ipspace' in expect_and_capture_ansible_exception(my_obj.get_ipspace, 'fail')['msg'] + assert 'Error provisioning ipspace' in expect_and_capture_ansible_exception(my_obj.create_ipspace, 'fail')['msg'] + assert 'Error renaming ipspace' in expect_and_capture_ansible_exception(my_obj.rename_ipspace, 'fail')['msg'] + assert 'Error removing ipspace' in expect_and_capture_ansible_exception(my_obj.delete_ipspace, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_iscsi.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_iscsi.py new file mode 100644 index 000000000..4d0a53fda --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_iscsi.py @@ -0,0 +1,339 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_iscsi ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_iscsi \ + import NetAppOntapISCSI as iscsi_module # module under test +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + "hostname": "10.10.10.10", + "username": "admin", + "password": "netapp1!", + "validate_certs": "no", + "https": "yes", + "state": "present", + "use_rest": "never", + "vserver": "svm1", + "service_state": "started" +} + + +iscsi_info_started = { + 'num-records': 1, + 'attributes-list': { + 'iscsi-service-info': { + 'is-available': 'true', + 'vserver': 'svm1' + } + } +} + +iscsi_info_stopped = { + 'num-records': 1, + 'attributes-list': { + 'iscsi-service-info': { + 'is-available': 'false', + 'vserver': 'svm1' + } + } +} + +ZRR = zapi_responses({ + 'iscsi_started': build_zapi_response(iscsi_info_started), + 'iscsi_stopped': build_zapi_response(iscsi_info_stopped) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + iscsi_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get_nonexistent_iscsi(): + register_responses([ + ('iscsi-service-get-iter', ZRR['empty']) + ]) + iscsi_obj = create_module(iscsi_module, DEFAULT_ARGS) + result = iscsi_obj.get_iscsi() + assert not result + + +def test_get_existing_iscsi(): + register_responses([ + ('iscsi-service-get-iter', ZRR['iscsi_started']) + ]) + iscsi_obj = create_module(iscsi_module, DEFAULT_ARGS) + result = iscsi_obj.get_iscsi() + assert result + + +def test_successfully_create(): + register_responses([ + ('iscsi-service-get-iter', ZRR['empty']), + ('iscsi-service-create', ZRR['success']) + ]) + assert create_and_apply(iscsi_module, DEFAULT_ARGS)['changed'] + + +def test_create_idempotency(): + register_responses([ + ('iscsi-service-get-iter', ZRR['iscsi_started']) + ]) + assert create_and_apply(iscsi_module, DEFAULT_ARGS)['changed'] is False + + +def test_successfully_create_stop_service(): + register_responses([ + ('iscsi-service-get-iter', ZRR['empty']), + ('iscsi-service-create', ZRR['success']) + ]) + args = {'service_state': 'stopped'} + assert create_and_apply(iscsi_module, DEFAULT_ARGS, args)['changed'] + + +def test_successfully_delete_when_service_started(): + register_responses([ + ('iscsi-service-get-iter', ZRR['iscsi_started']), + ('iscsi-service-stop', ZRR['success']), + ('iscsi-service-destroy', ZRR['success']) + ]) + args = {'state': 'absent'} + assert create_and_apply(iscsi_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_idempotent(): + register_responses([ + ('iscsi-service-get-iter', ZRR['empty']) + ]) + args = {'state': 'absent'} + assert create_and_apply(iscsi_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_start_iscsi(): + register_responses([ + ('iscsi-service-get-iter', ZRR['iscsi_stopped']), + ('iscsi-service-start', ZRR['success']) + ]) + assert create_and_apply(iscsi_module, DEFAULT_ARGS)['changed'] + + +def test_stop_iscsi(): + register_responses([ + ('iscsi-service-get-iter', ZRR['iscsi_started']), + ('iscsi-service-stop', ZRR['success']) + ]) + args = {'service_state': 'stopped'} + assert create_and_apply(iscsi_module, DEFAULT_ARGS, args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('iscsi-service-get-iter', ZRR['error']), + ('iscsi-service-create', ZRR['error']), + ('iscsi-service-start', ZRR['error']), + ('iscsi-service-stop', ZRR['error']), + ('iscsi-service-destroy', ZRR['error']) + ]) + + iscsi_obj = create_module(iscsi_module, DEFAULT_ARGS) + + error = expect_and_capture_ansible_exception(iscsi_obj.get_iscsi, 'fail')['msg'] + assert 'Error finding iscsi service in svm1: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(iscsi_obj.create_iscsi_service, 'fail')['msg'] + assert 'Error creating iscsi service: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(iscsi_obj.start_iscsi_service, 'fail')['msg'] + assert 'Error starting iscsi service on vserver svm1: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(iscsi_obj.stop_iscsi_service, 'fail')['msg'] + assert 'Error Stopping iscsi service on vserver svm1: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(iscsi_obj.delete_iscsi_service, 'fail', {'service_state': 'stopped'})['msg'] + assert 'Error deleting iscsi service on vserver svm1: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + +SRR = rest_responses({ + 'iscsi_started': (200, {"records": [ + { + "svm": {"uuid": "d08434fae1-a8a8-11fg-aa26-005055fhs3e5"}, + "enabled": True, + 'target': {'alias': 'ansibleSVM'} + } + ], "num_records": 1}, None), + 'iscsi_record': (200, {"records": [ + { + "svm": {"uuid": "d08434fae1-a8a8-11fg-aa26-005055fhs3e5"}, + "enabled": True, + 'target': {'alias': 'ansibleSVM'} + } + ], "num_records": 1}, None), + 'iscsi_stopped': (200, {"records": [ + { + "svm": {"uuid": "d08434fae1-a8a8-11fg-aa26-005055fhs3e5"}, + "enabled": False, + 'target': {'alias': 'ansibleSVM'} + } + ], "num_records": 1}, None), +}) + + +ARGS_REST = { + "hostname": "10.10.10.10", + "username": "admin", + "password": "netapp1!", + "validate_certs": "no", + "https": "yes", + "state": "present", + "use_rest": "always", + "vserver": "svm1", + "service_state": "started", + "target_alias": "ansibleSVM" +} + + +def test_successfully_create_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['empty_records']), + ('POST', 'protocols/san/iscsi/services', SRR['success']) + ]) + assert create_and_apply(iscsi_module, ARGS_REST, {'use_rest': 'always'})['changed'] + + +def test_create_idempotency_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['iscsi_started']), + ]) + assert create_and_apply(iscsi_module, ARGS_REST, {'use_rest': 'always'})['changed'] is False + + +def test_successfully_create_stop_service_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['empty_records']), + ('POST', 'protocols/san/iscsi/services', SRR['success']) + ]) + args = {'service_state': 'stopped'} + assert create_and_apply(iscsi_module, ARGS_REST, args)['changed'] + + +def test_successfully_delete_when_service_started_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['iscsi_started']), + ('PATCH', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['success']), + ('DELETE', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['success']), + ]) + args = {'state': 'absent'} + assert create_and_apply(iscsi_module, ARGS_REST, args)['changed'] + + +def test_delete_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['empty_records']), + ]) + args = {'state': 'absent'} + assert create_and_apply(iscsi_module, ARGS_REST, args)['changed'] is False + + +def test_start_iscsi_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['iscsi_stopped']), + ('PATCH', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['success']), + ]) + args = {'service_state': 'started'} + assert create_and_apply(iscsi_module, ARGS_REST, args)['changed'] + + +def test_modify_iscsi_target_alias_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['iscsi_started']), + ('PATCH', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['success']), + ]) + args = {"target_alias": "ansibleSVM_test"} + assert create_and_apply(iscsi_module, ARGS_REST, args)['changed'] + + +def test_modify_iscsi_target_alias_and_state_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['iscsi_stopped']), + ('PATCH', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['success']), + ('PATCH', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['success']), + ]) + args = {"target_alias": "ansibleSVM_test", 'service_state': 'started'} + assert create_and_apply(iscsi_module, ARGS_REST, args)['changed'] + + +def test_stop_iscsi_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['iscsi_started']), + ('PATCH', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['success']), + ]) + args = {'service_state': 'stopped'} + assert create_and_apply(iscsi_module, ARGS_REST, args)['changed'] + + +def test_if_all_methods_catch_exception_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/san/iscsi/services', SRR['generic_error']), + ('POST', 'protocols/san/iscsi/services', SRR['generic_error']), + ('PATCH', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['generic_error']), + ('PATCH', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['generic_error']), + ('DELETE', 'protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5', SRR['generic_error']) + ]) + + iscsi_obj = create_module(iscsi_module, ARGS_REST, {'use_rest': 'always'}) + iscsi_obj.uuid = "d08434fae1-a8a8-11fg-aa26-005055fhs3e5" + + error = expect_and_capture_ansible_exception(iscsi_obj.get_iscsi_rest, 'fail')['msg'] + msg = 'Error finding iscsi service in svm1: calling: protocols/san/iscsi/services: got Expected error.' + assert msg in error + + error = expect_and_capture_ansible_exception(iscsi_obj.create_iscsi_service_rest, 'fail')['msg'] + msg = 'Error creating iscsi service: calling: protocols/san/iscsi/services: got Expected error.' + assert msg in error + + error = expect_and_capture_ansible_exception(iscsi_obj.start_or_stop_iscsi_service_rest, 'fail', 'started')['msg'] + msg = 'Error starting iscsi service on vserver svm1: calling: protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5: got Expected error.' + assert msg in error + + error = expect_and_capture_ansible_exception(iscsi_obj.start_or_stop_iscsi_service_rest, 'fail', 'stopped')['msg'] + msg = 'Error stopping iscsi service on vserver svm1: calling: protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5: got Expected error.' + assert msg in error + + error = expect_and_capture_ansible_exception(iscsi_obj.delete_iscsi_service_rest, 'fail', {'service_state': 'stopped'})['msg'] + msg = 'Error deleting iscsi service on vserver svm1: calling: protocols/san/iscsi/services/d08434fae1-a8a8-11fg-aa26-005055fhs3e5: got Expected error.' + assert msg in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_iscsi_security.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_iscsi_security.py new file mode 100644 index 000000000..4cc168f2e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_iscsi_security.py @@ -0,0 +1,195 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_iscsi_security ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, expect_and_capture_ansible_exception, call_main, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, \ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_iscsi_security \ + import NetAppONTAPIscsiSecurity as iscsi_object, main as iscsi_module_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'get_uuid': (200, {"records": [{"uuid": "e2e89ccc-db35-11e9"}]}, None), + 'get_initiator': (200, {"records": [ + { + "svm": { + "uuid": "e2e89ccc-db35-11e9", + "name": "test_ansible" + }, + "initiator": "eui.0123456789abcdef", + "authentication_type": "chap", + "chap": { + "inbound": { + "user": "test_user_1" + }, + "outbound": { + "user": "test_user_2" + } + }, + "initiator_address": { + "ranges": [ + { + "start": "10.125.10.0", + "end": "10.125.10.10", + "family": "ipv4" + }, + { + "start": "10.10.10.7", + "end": "10.10.10.7", + "family": "ipv4" + } + ] + } + }], "num_records": 1}, None), + 'get_initiator_no_user': (200, {"records": [ + { + "svm": { + "uuid": "e2e89ccc-db35-11e9", + "name": "test_ansible" + }, + "initiator": "eui.0123456789abcdef", + "authentication_type": "chap", + "chap": { + }, + "initiator_address": { + "ranges": [ + ] + } + }], "num_records": 1}, None), + 'get_initiator_none': (200, {"records": [ + { + "svm": { + "uuid": "e2e89ccc-db35-11e9", + "name": "test_ansible" + }, + "initiator": "eui.0123456789abcdef", + "authentication_type": "none" + }], "num_records": 1}, None), +}) + + +DEFAULT_ARGS = { + 'initiator': "eui.0123456789abcdef", + 'inbound_username': "test_user_1", + 'inbound_password': "123", + 'outbound_username': "test_user_2", + 'outbound_password': "321", + 'auth_type': "chap", + 'address_ranges': ["10.125.10.0-10.125.10.10", "10.10.10.7"], + 'hostname': 'test', + 'vserver': 'test_vserver', + 'username': 'test_user', + 'password': 'test_pass!' +} + + +def test_rest_successful_create(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['get_uuid']), + ('GET', 'protocols/san/iscsi/credentials', SRR['zero_records']), + ('POST', 'protocols/san/iscsi/credentials', SRR['success']), + # idempotent check + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['get_uuid']), + ('GET', 'protocols/san/iscsi/credentials', SRR['get_initiator']), + ]) + assert create_and_apply(iscsi_object, DEFAULT_ARGS)['changed'] + assert not create_and_apply(iscsi_object, DEFAULT_ARGS)['changed'] + + +def test_rest_successful_modify_address(): + '''Test successful rest modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['get_uuid']), + ('GET', 'protocols/san/iscsi/credentials', SRR['get_initiator']), + ('PATCH', 'protocols/san/iscsi/credentials/e2e89ccc-db35-11e9/eui.0123456789abcdef', SRR['success']) + ]) + args = {'address_ranges': ['10.10.10.8']} + assert create_and_apply(iscsi_object, DEFAULT_ARGS, args)['changed'] + + +def test_rest_successful_modify_inbound_user(): + '''Test successful rest modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['get_uuid']), + ('GET', 'protocols/san/iscsi/credentials', SRR['get_initiator']), + ('PATCH', 'protocols/san/iscsi/credentials/e2e89ccc-db35-11e9/eui.0123456789abcdef', SRR['success']) + ]) + args = {'inbound_username': 'test_user_3'} + assert create_and_apply(iscsi_object, DEFAULT_ARGS, args)['changed'] + + +def test_rest_successful_modify_outbound_user(): + '''Test successful rest modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['get_uuid']), + ('GET', 'protocols/san/iscsi/credentials', SRR['get_initiator']), + ('PATCH', 'protocols/san/iscsi/credentials/e2e89ccc-db35-11e9/eui.0123456789abcdef', SRR['success']) + ]) + args = {'outbound_username': 'test_user_3'} + assert create_and_apply(iscsi_object, DEFAULT_ARGS, args)['changed'] + + +def test_rest_successful_modify_chap_no_user(): + '''Test successful rest modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['get_uuid']), + ('GET', 'protocols/san/iscsi/credentials', SRR['get_initiator_no_user']), + ('PATCH', 'protocols/san/iscsi/credentials/e2e89ccc-db35-11e9/eui.0123456789abcdef', SRR['success']) + ]) + assert create_and_apply(iscsi_object, DEFAULT_ARGS)['changed'] + + +def test_rest_successful_modify_chap(): + '''Test successful rest modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['get_uuid']), + ('GET', 'protocols/san/iscsi/credentials', SRR['get_initiator_none']), + ('PATCH', 'protocols/san/iscsi/credentials/e2e89ccc-db35-11e9/eui.0123456789abcdef', SRR['success']) + ]) + assert call_main(iscsi_module_main, DEFAULT_ARGS)['changed'] + + +def test_all_methods_catch_exception(): + ''' test exception in get/create/modify/delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['get_uuid']), + ('GET', 'svm/svms', SRR['generic_error']), + ('GET', 'svm/svms', SRR['empty_records']), + # GET/POST/PATCH error. + ('GET', 'protocols/san/iscsi/credentials', SRR['generic_error']), + ('POST', 'protocols/san/iscsi/credentials', SRR['generic_error']), + ('PATCH', 'protocols/san/iscsi/credentials/e2e89ccc-db35-11e9/eui.0123456789abcdef', SRR['generic_error']), + ('DELETE', 'protocols/san/iscsi/credentials/e2e89ccc-db35-11e9/eui.0123456789abcdef', SRR['generic_error']) + ]) + sec_obj = create_module(iscsi_object, DEFAULT_ARGS) + assert 'Error on fetching svm uuid' in expect_and_capture_ansible_exception(sec_obj.get_svm_uuid, 'fail')['msg'] + assert 'Error on fetching svm uuid, SVM not found' in expect_and_capture_ansible_exception(sec_obj.get_svm_uuid, 'fail')['msg'] + assert 'Error on fetching initiator' in expect_and_capture_ansible_exception(sec_obj.get_initiator, 'fail')['msg'] + assert 'Error on creating initiator' in expect_and_capture_ansible_exception(sec_obj.create_initiator, 'fail')['msg'] + assert 'Error on modifying initiator' in expect_and_capture_ansible_exception(sec_obj.modify_initiator, 'fail', {}, {})['msg'] + assert 'Error on deleting initiator' in expect_and_capture_ansible_exception(sec_obj.delete_initiator, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_job_schedule.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_job_schedule.py new file mode 100644 index 000000000..4ccec5115 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_job_schedule.py @@ -0,0 +1,451 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_job_schedule ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_job_schedule \ + import NetAppONTAPJob as job_module, main as uut_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'name': 'test_job', + 'job_minutes': [25], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' +} + + +cron_info = { + 'num-records': 1, + 'attributes-list': { + 'job-schedule-cron-info': { + 'job-schedule-cluster': 'cluster1', + 'job-schedule-name': 'test_job', + 'job-schedule-cron-minute': {'cron-minute': 25} + } + } +} + + +multiple_cron_info = { + 'num-records': 1, + 'attributes-list': { + 'job-schedule-cron-info': { + 'job-schedule-cluster': 'cluster1', + 'job-schedule-name': 'test_job', + 'job-schedule-cron-minute': [ + {'cron-minute': '25'}, + {'cron-minute': '35'} + ], + 'job-schedule-cron-month': [ + {'cron-month': '5'}, + {'cron-month': '10'} + ] + } + } +} + + +multiple_cron_minutes_info = { + 'num-records': 1, + 'attributes-list': { + 'job-schedule-cron-info': { + 'job-schedule-cluster': 'cluster1', + 'job-schedule-name': 'test_job', + 'job-schedule-cron-minute': [{'cron-minute': str(x)} for x in range(60)], + 'job-schedule-cron-month': [ + {'cron-month': '5'}, + {'cron-month': '10'} + ] + } + } +} + + +ZRR = zapi_responses({ + 'cron_info': build_zapi_response(cron_info), + 'multiple_cron_info': build_zapi_response(multiple_cron_info), + 'multiple_cron_minutes_info': build_zapi_response(multiple_cron_minutes_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors + with python 2.6, dictionaries are not ordered + ''' + fragments = ["missing required arguments:", "hostname", "name"] + error = create_module(job_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_get_nonexistent_job(): + ''' Test if get_job_schedule returns None for non-existent job ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['no_records']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS) + assert job_obj.get_job_schedule() is None + + +def test_get_existing_job(): + ''' Test if get_job_schedule retuns job details for existing job ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['cron_info']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS) + result = job_obj.get_job_schedule() + assert result['name'] == DEFAULT_ARGS['name'] + assert result['job_minutes'] == DEFAULT_ARGS['job_minutes'] + + +def test_get_existing_job_multiple_minutes(): + # sourcery skip: class-extract-method + ''' Test if get_job_schedule retuns job details for existing job ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['multiple_cron_info']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS) + result = job_obj.get_job_schedule() + assert result['name'] == DEFAULT_ARGS['name'] + assert result['job_minutes'] == [25, 35] + assert result['job_months'] == [5, 10] + + +def test_get_existing_job_multiple_minutes_0_offset(): + ''' Test if get_job_schedule retuns job details for existing job ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['multiple_cron_info']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS, {'month_offset': 0}) + result = job_obj.get_job_schedule() + assert result['name'] == DEFAULT_ARGS['name'] + assert result['job_minutes'] == [25, 35] + assert result['job_months'] == [5, 10] + + +def test_get_existing_job_multiple_minutes_1_offset(): + ''' Test if get_job_schedule retuns job details for existing job ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['multiple_cron_info']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS, {'month_offset': 1}) + result = job_obj.get_job_schedule() + assert result['name'] == DEFAULT_ARGS['name'] + assert result['job_minutes'] == [25, 35] + assert result['job_months'] == [5 + 1, 10 + 1] + + +def test_create_error_missing_param(): + ''' Test if create throws an error if job_minutes is not specified''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['no_records']) + ]) + args = DEFAULT_ARGS.copy() + del args['job_minutes'] + error = 'Error: missing required parameter job_minutes for create' + assert error in create_and_apply(job_module, args, fail=True)['msg'] + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['no_records']), + ('job-schedule-cron-create', ZRR['success']) + ]) + assert create_and_apply(job_module, DEFAULT_ARGS)['changed'] + + +def test_successful_create_0_offset(): + ''' Test successful create ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['no_records']), + ('job-schedule-cron-create', ZRR['success']) + ]) + args = {'month_offset': 0, 'job_months': [0, 8]} + assert create_and_apply(job_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_create_1_offset(): + ''' Test successful create ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['no_records']), + ('job-schedule-cron-create', ZRR['success']) + ]) + args = {'month_offset': 1, 'job_months': [1, 9], 'cluster': 'cluster1'} + assert create_and_apply(job_module, DEFAULT_ARGS, args)['changed'] + + +def test_create_idempotency(): + ''' Test create idempotency ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['cron_info']) + ]) + assert not create_and_apply(job_module, DEFAULT_ARGS)['changed'] + + +def test_successful_delete(): + ''' Test delete existing job ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['cron_info']), + ('job-schedule-cron-destroy', ZRR['success']) + ]) + assert create_and_apply(job_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_delete_idempotency(): + ''' Test delete idempotency ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['no_records']) + ]) + assert not create_and_apply(job_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_successful_modify(): + ''' Test successful modify job_minutes ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['cron_info']), + ('job-schedule-cron-modify', ZRR['success']) + ]) + assert create_and_apply(job_module, DEFAULT_ARGS, {'job_minutes': '20'})['changed'] + + +def test_modify_idempotency(): + ''' Test modify idempotency ''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['cron_info']) + ]) + assert not create_and_apply(job_module, DEFAULT_ARGS)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_negative_no_netapp_lib(mock_has): + mock_has.return_value = False + error = 'the python NetApp-Lib module is required' + assert error in create_module(job_module, DEFAULT_ARGS, fail=True)['msg'] + + +def test_zapi_get_all_minutes(): + register_responses([ + ('job-schedule-cron-get-iter', ZRR['multiple_cron_minutes_info']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS) + schedule = job_obj.get_job_schedule() + assert schedule + assert 'job_minutes' in schedule + assert schedule['job_minutes'] == [-1] + + +def test_if_all_methods_catch_exception_zapi(): + ''' test error zapi - get/create/modify/delete''' + register_responses([ + ('job-schedule-cron-get-iter', ZRR['error']), + ('job-schedule-cron-create', ZRR['error']), + ('job-schedule-cron-modify', ZRR['error']), + ('job-schedule-cron-destroy', ZRR['error']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS) + + assert 'Error fetching job schedule' in expect_and_capture_ansible_exception(job_obj.get_job_schedule, 'fail')['msg'] + assert 'Error creating job schedule' in expect_and_capture_ansible_exception(job_obj.create_job_schedule, 'fail')['msg'] + assert 'Error modifying job schedule' in expect_and_capture_ansible_exception(job_obj.modify_job_schedule, 'fail', {}, {})['msg'] + assert 'Error deleting job schedule' in expect_and_capture_ansible_exception(job_obj.delete_job_schedule, 'fail')['msg'] + + +SRR = rest_responses({ + 'get_schedule': (200, {"records": [ + { + "uuid": "010df156-e0a9-11e9-9f70-005056b3df08", + "name": "test_job", + "cron": { + "minutes": [25], + "hours": [0], + "weekdays": [0], + "months": [5, 6] + } + } + ], "num_records": 1}, None), + 'get_all_minutes': (200, {"records": [ + { + "uuid": "010df156-e0a9-11e9-9f70-005056b3df08", + "name": "test_job", + "cron": { + "minutes": range(60), + "hours": [0], + "weekdays": [0], + "months": [5, 6] + } + } + ], "num_records": 1}, None) +}) + + +DEFAULT_ARGS_REST = { + 'name': 'test_job', + 'job_minutes': [25], + 'job_hours': [0], + 'job_days_of_week': [0], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always' +} + + +def test_rest_successful_create(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['zero_records']), + ('POST', 'cluster/schedules', SRR['success']), + ]) + assert create_and_apply(job_module, DEFAULT_ARGS_REST)['changed'] + + +def test_rest_create_idempotency(): + '''Test rest create idempotency''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['get_schedule']) + ]) + assert not create_and_apply(job_module, DEFAULT_ARGS_REST)['changed'] + + +def test_rest_get_0_offset(): + '''Test rest get using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['get_schedule']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS_REST, {'month_offset': 0}) + record = job_obj.get_job_schedule_rest() + assert record + assert record['job_months'] == [x - 1 for x in SRR['get_schedule'][1]['records'][0]['cron']['months']] + + +def test_rest_get_1_offset(): + '''Test rest get using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['get_schedule']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS_REST, {'month_offset': 1}) + record = job_obj.get_job_schedule_rest() + assert record + assert record['job_months'] == SRR['get_schedule'][1]['records'][0]['cron']['months'] + + +def test_rest_create_all_minutes(): + '''Test rest create using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['zero_records']), + ('POST', 'cluster/schedules', SRR['success']) + ]) + assert create_and_apply(job_module, DEFAULT_ARGS_REST, {'job_minutes': [-1]})['changed'] + + +def test_rest_create_0_offset(): + '''Test rest create using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['zero_records']), + ('POST', 'cluster/schedules', SRR['success']) + ]) + args = {'month_offset': 0, 'job_months': [0, 8]} + assert create_and_apply(job_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_rest_create_1_offset(): + '''Test rest create using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['zero_records']), + ('POST', 'cluster/schedules', SRR['success']) + ]) + args = {'month_offset': 1, 'job_months': [1, 9]} + assert create_and_apply(job_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_rest_modify_0_offset(): + '''Test rest modify using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['get_schedule']), + ('PATCH', 'cluster/schedules/010df156-e0a9-11e9-9f70-005056b3df08', SRR['success']) + ]) + args = {'month_offset': 0, 'job_months': [0, 8]} + assert create_and_apply(job_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_rest_modify_1_offset(): + '''Test rest modify using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['get_schedule']), + ('PATCH', 'cluster/schedules/010df156-e0a9-11e9-9f70-005056b3df08', SRR['success']) + ]) + args = {'month_offset': 1, 'job_months': [1, 9], 'cluster': 'cluster1'} + assert create_and_apply(job_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_negative_month_of_0(): + '''Test rest modify using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + args = {'month_offset': 1, 'job_months': [0, 9]} + error = 'Error: 0 is not a valid value in months if month_offset is set to 1' + assert error in create_module(job_module, DEFAULT_ARGS_REST, args, fail=True)['msg'] + + +def test_rest_get_all_minutes(): + '''Test rest modify using month offset''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['get_all_minutes']) + ]) + args = {'month_offset': 1, 'job_months': [1, 9]} + job_obj = create_module(job_module, DEFAULT_ARGS_REST, args) + schedule = job_obj.get_job_schedule() + assert schedule + assert 'job_minutes' in schedule + assert schedule['job_minutes'] == [-1] + + +def test_if_all_methods_catch_exception_rest(): + ''' test error zapi - get/create/modify/delete''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/schedules', SRR['generic_error']), + ('POST', 'cluster/schedules', SRR['generic_error']), + ('PATCH', 'cluster/schedules/abcd', SRR['generic_error']), + ('DELETE', 'cluster/schedules/abcd', SRR['generic_error']) + ]) + job_obj = create_module(job_module, DEFAULT_ARGS_REST) + job_obj.uuid = 'abcd' + assert 'Error fetching job schedule' in expect_and_capture_ansible_exception(job_obj.get_job_schedule, 'fail')['msg'] + assert 'Error creating job schedule' in expect_and_capture_ansible_exception(job_obj.create_job_schedule, 'fail')['msg'] + assert 'Error modifying job schedule' in expect_and_capture_ansible_exception(job_obj.modify_job_schedule, 'fail', {}, {})['msg'] + assert 'Error deleting job schedule' in expect_and_capture_ansible_exception(job_obj.delete_job_schedule, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_interface.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_interface.py new file mode 100644 index 000000000..ada9b4328 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_interface.py @@ -0,0 +1,107 @@ +# Copyright: NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, expect_and_capture_ansible_exception, call_main, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, \ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_kerberos_interface \ + import NetAppOntapKerberosInterface as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'enabled': False, + 'interface_name': 'lif1', + 'vserver': 'ansibleSVM' +} + + +SRR = rest_responses({ + 'kerberos_int_conf_enabled': (200, {"records": [{ + "spn": "nfs/life2@RELAM2", + "machine_account": "account1", + "interface": { + "ip": {"address": "10.10.10.7"}, + "name": "lif1", + "uuid": "1cd8a442" + }, + "enabled": True, + }], "num_records": 1}, None), + 'kerberos_int_conf_disabled': (200, {"records": [{ + "interface": { + "ip": {"address": "10.10.10.7"}, + "name": "lif1", + "uuid": "1cd8a442" + }, + "enabled": False, + }], "num_records": 1}, None), +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_enable_kerberos_int_conf(): + ''' enable kerberos int conf ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/nfs/kerberos/interfaces', SRR['kerberos_int_conf_disabled']), + ('PATCH', 'protocols/nfs/kerberos/interfaces/1cd8a442', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/nfs/kerberos/interfaces', SRR['kerberos_int_conf_enabled']) + ]) + args = { + "spn": "nfs/life2@RELAM2", + "machine_account": "account1", + "admin_username": "user1", + "admin_password": "pass1", + "enabled": True + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_all_methods_catch_exception(): + ''' test exception in get/create/modify/delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # GET/PATCH error. + ('GET', 'protocols/nfs/kerberos/interfaces', SRR['generic_error']), + ('PATCH', 'protocols/nfs/kerberos/interfaces/1cd8a442', SRR['generic_error']) + ]) + ker_obj = create_module(my_module, DEFAULT_ARGS) + ker_obj.uuid = '1cd8a442' + assert 'Error fetching kerberos interface' in expect_and_capture_ansible_exception(ker_obj.get_kerberos_interface, 'fail')['msg'] + assert 'Error modifying kerberos interface' in expect_and_capture_ansible_exception(ker_obj.modify_kerberos_interface, 'fail')['msg'] + + +def test_error_ontap97(): + ''' test module supported from 9.7 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + assert 'requires ONTAP 9.7.0 or later' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_realm.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_realm.py new file mode 100644 index 000000000..30f577d4c --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_kerberos_realm.py @@ -0,0 +1,213 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP Kerberos Realm module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import pytest +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_kerberos_realm \ + import NetAppOntapKerberosRealm as my_module # module under test +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': True, + 'validate_certs': False, + 'use_rest': 'never', + 'realm': 'NETAPP.COM', + 'vserver': 'vserver1', + 'kdc_ip': '192.168.0.1', + 'kdc_vendor': 'other' +} + +kerberos_info = { + 'num-records': "1", + 'attributes-list': { + 'kerberos-realm': { + 'admin-server-ip': "192.168.0.1", + 'admin-server-port': "749", + 'clock-skew': "5", + 'kdc-ip': "192.168.0.1", + 'kdc-port': "88", + 'kdc-vendor': "other", + 'password-server-ip': "192.168.0.1", + 'password-server-port': "464", + "permitted-enc-types": { + "string": ["des", "des3", "aes_128", "aes_256"] + }, + 'realm': "NETAPP.COM", + 'vserver-name': "vserver1" + } + } +} + + +ZRR = zapi_responses({ + 'kerberos_info': build_zapi_response(kerberos_info) +}) + + +SRR = rest_responses({ + 'kerberos_info': (200, {"records": [{ + "svm": { + "uuid": "89368b07", + "name": "svm3" + }, + "name": "name1", + "kdc": { + "vendor": "microsoft", + "ip": "10.193.115.116", + "port": 88 + }, + "comment": "mohan", + "ad_server": { + "name": "netapp", + "address": "10.193.115.116" + } + }], "num_records": 1}, None) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "realm", "vserver"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_module_fail_when_state_present_required_args_missing(): + ''' required arguments are reported as errors ''' + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['kdc_ip'] + del DEFAULT_ARGS_COPY['kdc_vendor'] + error = "state is present but all of the following are missing: kdc_vendor, kdc_ip" + assert error in create_module(my_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + + +def test_get_existing_realm(): + ''' Test if get_krbrealm returns details for existing kerberos realm ''' + register_responses([ + ('kerberos-realm-get-iter', ZRR['kerberos_info']) + ]) + kerb_obj = create_module(my_module, DEFAULT_ARGS) + assert kerb_obj.get_krbrealm() + + +def test_successfully_modify_realm(): + ''' Test modify realm successful for modifying kdc_ip. ''' + register_responses([ + ('kerberos-realm-get-iter', ZRR['kerberos_info']), + ('kerberos-realm-modify', ZRR['success']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'kdc_ip': '10.1.1.20'}) + + +def test_successfully_delete_realm(): + ''' Test successfully delete realm ''' + register_responses([ + ('kerberos-realm-get-iter', ZRR['kerberos_info']), + ('kerberos-realm-delete', ZRR['success']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'}) + + +def test_successfully_create_realm(): + ''' Test successfully create realm ''' + register_responses([ + ('kerberos-realm-get-iter', ZRR['no_records']), + ('kerberos-realm-create', ZRR['success']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS) + + +def test_required_if(): + ''' required arguments are reported as errors ''' + error = "kdc_vendor is microsoft but all of the following are missing: ad_server_ip, ad_server_name" + assert error in create_module(my_module, DEFAULT_ARGS, {'kdc_vendor': 'microsoft'}, fail=True)['msg'] + + error = "kdc_vendor is microsoft but all of the following are missing: ad_server_name" + args = {'kdc_vendor': 'microsoft', 'ad_server_ip': '10.0.0.1'} + assert error in create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('kerberos-realm-get-iter', ZRR['error']), + ('kerberos-realm-create', ZRR['error']), + ('kerberos-realm-modify', ZRR['error']), + ('kerberos-realm-delete', ZRR['error']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/nfs/kerberos/realms', SRR['generic_error']), + ('POST', 'protocols/nfs/kerberos/realms', SRR['generic_error']), + ('PATCH', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['generic_error']), + ('DELETE', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['generic_error']) + ]) + kerb_obj = create_module(my_module, DEFAULT_ARGS) + assert 'Error fetching kerberos realm' in expect_and_capture_ansible_exception(kerb_obj.get_krbrealm, 'fail')['msg'] + assert 'Error creating Kerberos Realm' in expect_and_capture_ansible_exception(kerb_obj.create_krbrealm, 'fail')['msg'] + assert 'Error modifying Kerberos Realm' in expect_and_capture_ansible_exception(kerb_obj.modify_krbrealm, 'fail', {})['msg'] + assert 'Error deleting Kerberos Realm' in expect_and_capture_ansible_exception(kerb_obj.delete_krbrealm, 'fail')['msg'] + + kerb_obj = create_module(my_module, DEFAULT_ARGS, {'use_rest': 'always'}) + kerb_obj.svm_uuid = '89368b07' + assert 'Error fetching kerberos realm' in expect_and_capture_ansible_exception(kerb_obj.get_krbrealm, 'fail')['msg'] + assert 'Error creating Kerberos Realm' in expect_and_capture_ansible_exception(kerb_obj.create_krbrealm, 'fail')['msg'] + assert 'Error modifying Kerberos Realm' in expect_and_capture_ansible_exception(kerb_obj.modify_krbrealm, 'fail', {})['msg'] + assert 'Error deleting Kerberos Realm' in expect_and_capture_ansible_exception(kerb_obj.delete_krbrealm, 'fail')['msg'] + + +def test_successfully_create_realm_rest(): + ''' Test successfully create realm ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/nfs/kerberos/realms', SRR['empty_records']), + ('POST', 'protocols/nfs/kerberos/realms', SRR['success']), + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'use_rest': 'always'}) + + +def test_successfully_modify_realm_rest(): + ''' Test modify realm successful for modifying kdc_ip. ''' + register_responses([ + # modify ip. + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/nfs/kerberos/realms', SRR['kerberos_info']), + ('PATCH', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['success']), + # modify port. + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/nfs/kerberos/realms', SRR['kerberos_info']), + ('PATCH', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['success']), + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'use_rest': 'always', 'kdc_ip': '10.1.1.20'}) + assert create_and_apply(my_module, DEFAULT_ARGS, {'use_rest': 'always', 'kdc_port': '8088'}) + + +def test_successfully_delete_realm_rest(): + ''' Test successfully delete realm ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'protocols/nfs/kerberos/realms', SRR['kerberos_info']), + ('DELETE', 'protocols/nfs/kerberos/realms/89368b07/NETAPP.COM', SRR['success']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'use_rest': 'always', 'state': 'absent'}) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ldap_client.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ldap_client.py new file mode 100644 index 000000000..4df8d9fee --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ldap_client.py @@ -0,0 +1,481 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_ldap_client ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, call_main, create_module, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ldap_client \ + import NetAppOntapLDAPClient as client_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'ldap_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "vserver" + }, + "servers": ['10.193.115.116'], + "schema": 'RFC-2307', + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + } + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None), + "svm": ( + 200, + {"records": [{"uuid": "671aa46e"}]}, + None) +}) + + +ldap_client_info = {'num-records': 1, + 'attributes-list': + {'ldap-client': + {'ldap-client-config': 'test_ldap', + 'schema': 'RFC-2307', + 'ldap-servers': [{"ldap-server": '10.193.115.116'}, ] + } + }, + } + +ZRR = zapi_responses({ + 'ldap_client_info': build_zapi_response(ldap_client_info) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'name': 'test_ldap', + 'schema': 'RFC-2307', + 'use_rest': 'never', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + client_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get_nonexistent_client(): + ''' Test if get ldap client returns None for non-existent job ''' + register_responses([ + ('ldap-client-get-iter', ZRR['empty']) + ]) + ldap_obj = create_module(client_module, DEFAULT_ARGS) + result = ldap_obj.get_ldap_client() + assert result is None + + +def test_error_name_required_zapi(): + ''' name is required with ZAPI ''' + error = 'Error: name is a required field with ZAPI.' + assert error in create_module(client_module, DEFAULT_ARGS, {'name': None}, fail=True)['msg'] + + +def test_get_existing_client(): + ''' Test if get ldap client returns None for non-existent job ''' + register_responses([ + ('ldap-client-get-iter', ZRR['ldap_client_info']) + ]) + ldap_obj = create_module(client_module, DEFAULT_ARGS) + result = ldap_obj.get_ldap_client() + assert result + + +def test_successfully_create_zapi(): + register_responses([ + ('ldap-client-get-iter', ZRR['empty']), + ('ldap-client-create', ZRR['success']), + ]) + module_args = { + 'name': 'test_ldap', + 'ldap_servers': ['10.193.115.116'], + 'schema': 'RFC-2307' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_zapi(): + register_responses([ + ('ldap-client-get-iter', ZRR['empty']), + ('ldap-client-create', ZRR['error']), + ]) + module_args = { + 'name': 'test_ldap', + 'ldap_servers': ['10.193.115.116'], + 'schema': 'RFC-2307' + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error creating LDAP client" + assert msg in error + + +def test_error_create_ad_zapi(): + register_responses([ + ('ldap-client-get-iter', ZRR['empty']), + ('ldap-client-create', ZRR['error']), + ]) + module_args = { + 'name': 'test_ldap', + 'ad_domain': 'ad.netapp.com', + 'preferred_ad_servers': ['10.193.115.116'], + 'schema': 'RFC-2307' + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error creating LDAP client" + assert msg in error + + +def test_create_idempotency(): + register_responses([ + ('ldap-client-get-iter', ZRR['ldap_client_info']), + ]) + module_args = { + 'name': 'test_ldap', + 'servers': ['10.193.115.116'], + 'schema': 'RFC-2307', + 'state': 'present' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_delete(): + register_responses([ + ('ldap-client-get-iter', ZRR['ldap_client_info']), + ('ldap-client-delete', ZRR['success']), + ]) + module_args = { + 'name': 'test_ldap', + 'ldap_servers': ['10.193.115.116'], + 'schema': 'RFC-2307', + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_delete_zapi(): + register_responses([ + ('ldap-client-get-iter', ZRR['ldap_client_info']), + ('ldap-client-delete', ZRR['error']), + ]) + module_args = { + 'name': 'test_ldap', + 'ldap_servers': ['10.193.115.116'], + 'schema': 'RFC-2307', + 'state': 'absent' + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error deleting LDAP client configuration" + assert msg in error + + +def test_delete_idempotency(): + register_responses([ + ('ldap-client-get-iter', ZRR['empty']), + ]) + module_args = { + 'state': 'absent' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ldap_servers(): + register_responses([ + ('ldap-client-get-iter', ZRR['ldap_client_info']), + ('ldap-client-modify', ZRR['success']), + ]) + module_args = { + 'name': 'test_ldap', + 'ldap_servers': ['10.195.64.121'], + 'schema': 'RFC-2307', + 'ldaps_enabled': True, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ldap_ad_servers(): + register_responses([ + ('ldap-client-get-iter', ZRR['ldap_client_info']), + ('ldap-client-modify', ZRR['success']), + ]) + module_args = { + 'name': 'test_ldap', + 'ad_domain': 'ad.netapp.com', + 'preferred_ad_servers': ['10.195.64.121'], + 'schema': 'RFC-2307', + 'ldaps_enabled': True, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ldap_schema_zapi(): + register_responses([ + ('ldap-client-get-iter', ZRR['ldap_client_info']), + ('ldap-client-modify', ZRR['success']), + ]) + module_args = { + 'name': 'test_ldap', + 'ldap_servers': ['10.195.64.121'], + 'schema': 'MS-AD-BIS', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ldap-client-create', ZRR['error']), + ('ldap-client-delete', ZRR['error']), + ('ldap-client-modify', ZRR['error']) + ]) + module_args = {'name': 'test_ldap'} + my_obj = create_module(client_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.create_ldap_client, 'fail')['msg'] + assert 'Error creating LDAP client test_ldap: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.delete_ldap_client, 'fail')['msg'] + assert 'Error deleting LDAP client configuration test_ldap: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.modify_ldap_client, 'fail', 'ldap-client-modify')['msg'] + assert 'Error modifying LDAP client test_ldap: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'vserver', + 'servers': ['10.193.115.116'], + 'schema': 'RFC-2307', +} + + +def test_get_nonexistent_ldap_config_rest(): + ''' Test if get_unix_user returns None for non-existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['empty_records']), + ]) + ldap_obj = create_module(client_module, ARGS_REST) + result = ldap_obj.get_ldap_client_rest() + assert result is None + + +def test_get_existent_ldap_config_rest(): + ''' Test if get_unix_user returns existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['ldap_record']), + ]) + ldap_obj = create_module(client_module, ARGS_REST) + result = ldap_obj.get_ldap_client_rest() + assert result + + +def test_get_error_ldap_config_rest(): + ''' Test if get_unix_user returns existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['generic_error']), + ]) + error = call_main(my_main, ARGS_REST, fail=True)['msg'] + msg = "Error on getting idap client info:" + assert msg in error + + +def test_create_ldap_client_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['empty_records']), + ('GET', 'svm/svms', SRR['svm']), + ('POST', 'name-services/ldap', SRR['empty_good']), + ]) + module_args = { + 'ldap_servers': ['10.193.115.116'], + 'schema': 'RFC-2307' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_create_ldap_client_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['empty_records']), + ('GET', 'svm/svms', SRR['svm']), + ('POST', 'name-services/ldap', SRR['generic_error']), + ]) + module_args = { + 'servers': ['10.193.115.116'], + 'schema': 'RFC-2307' + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on creating ldap client:" + assert msg in error + + +def test_delete_ldap_client_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['ldap_record']), + ('DELETE', 'name-services/ldap/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + 'servers': ['10.193.115.116'], + 'schema': 'RFC-2307', + 'state': 'absent' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_delete_ldap_client_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['ldap_record']), + ('DELETE', 'name-services/ldap/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']), + ]) + module_args = { + 'servers': ['10.193.115.116'], + 'schema': 'RFC-2307', + 'state': 'absent' + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on deleting ldap client rest:" + assert msg in error + + +def test_create_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['ldap_record']), + ]) + module_args = { + 'state': 'present', + 'servers': ['10.193.115.116'], + 'schema': 'RFC-2307', + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_on_cluster_vserver(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['empty_records']), + ('GET', 'svm/svms', SRR['empty_records']), + ]) + module_args = { + 'state': 'present', + 'servers': ['10.193.115.116'], + 'schema': 'RFC-2307', + } + assert 'is not a data vserver.' in call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + + +def test_delete_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['empty_records']) + ]) + module_args = { + 'state': 'absent' + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_modify_schema_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['ldap_record']), + ('PATCH', 'name-services/ldap/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']) + ]) + module_args = { + 'state': 'present', + 'servers': ['10.193.115.116'], + 'schema': 'AD-IDMU', + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_modify_ldap_servers_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['ldap_record']), + ('PATCH', 'name-services/ldap/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']) + ]) + module_args = { + 'state': 'present', + 'servers': ['10.195.64.121'], + 'schema': 'AD-IDMU', + 'ldaps_enabled': True, + 'skip_config_validation': True + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_negative_modify_ldap_servers_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['ldap_record']), + ('PATCH', 'name-services/ldap/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']) + ]) + module_args = { + 'state': 'present', + 'servers': ['10.195.64.121'], + 'schema': 'AD-IDMU', + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on modifying ldap client config:" + assert msg in error + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + module_args = { + 'use_rest': 'never', + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_error_no_server(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/ldap', SRR['ldap_record']), + ]) + args = dict(ARGS_REST) + args.pop('servers') + error = 'Required one of servers or ad_domain' + assert error in call_main(my_main, args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_license.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_license.py new file mode 100644 index 000000000..1683d2577 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_license.py @@ -0,0 +1,432 @@ +# (c) 2022-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP license Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import sys +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_no_warnings, call_main, create_module, expect_and_capture_ansible_exception, patch_ansible, print_warnings +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, build_zapi_error, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_license import NetAppOntapLicense as my_module, main as my_main, HAS_DEEPDIFF + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +def license_status(fcp_method): + return { + 'license-v2-status': [ + {'license-v2-status-info': + { + 'package': 'base', + 'method': 'site' + }}, + {'license-v2-status-info': + { + 'package': 'capacitypool', + 'method': 'none' + }}, + {'license-v2-status-info': + { + 'package': 'cifs', + 'method': 'site' + }}, + {'license-v2-status-info': + { + 'package': 'fcp', + 'method': fcp_method + }}, + ] + } + + +ZRR = zapi_responses({ + 'license_status_fcp_none': build_zapi_response(license_status('none')), + 'license_status_fcp_site': build_zapi_response(license_status('site')), + 'error_object_not_found': build_zapi_error('15661', 'license is not active') +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', +} + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + module_args = { + "use_rest": "never" + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_module_add_license_zapi(): + ''' Test add license ''' + register_responses([ + ('ZAPI', 'license-v2-status-list-info', ZRR['license_status_fcp_none']), + ('ZAPI', 'license-v2-add', ZRR['success']), + ('ZAPI', 'license-v2-status-list-info', ZRR['license_status_fcp_site']), + ]) + module_args = { + 'use_rest': 'never', + 'license_codes': 'LICENSECODE', + } + print('ZRR', build_zapi_response(license_status('site'))[0].to_string()) + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_module_add_license_idempotent_zapi(): + ''' Test add license idempotent ''' + register_responses([ + ('ZAPI', 'license-v2-status-list-info', ZRR['license_status_fcp_site']), + ('ZAPI', 'license-v2-add', ZRR['success']), + ('ZAPI', 'license-v2-status-list-info', ZRR['license_status_fcp_site']), + ]) + module_args = { + 'use_rest': 'never', + 'license_codes': 'LICENSECODE', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_module_remove_license_zapi(): + ''' Test remove license ''' + register_responses([ + ('ZAPI', 'license-v2-status-list-info', ZRR['license_status_fcp_site']), + ('ZAPI', 'license-v2-delete', ZRR['success']), + ('ZAPI', 'license-v2-delete', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'serial_number': '1-8-000000', + 'license_names': 'cifs,fcp', + 'state': 'absent', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_module_remove_license_idempotent_zapi(): + ''' Test remove license idempotent ''' + register_responses([ + ('ZAPI', 'license-v2-status-list-info', ZRR['license_status_fcp_site']), + ('ZAPI', 'license-v2-delete', ZRR['error_object_not_found']), + ('ZAPI', 'license-v2-delete', ZRR['error_object_not_found']), + ]) + module_args = { + 'use_rest': 'never', + 'serial_number': '1-8-000000', + 'license_names': 'cifs,fcp', + 'state': 'absent', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_module_remove_unused_expired_zapi(): + ''' Test remove unused expired license ''' + register_responses([ + ('ZAPI', 'license-v2-status-list-info', ZRR['license_status_fcp_site']), + ('ZAPI', 'license-v2-delete-unused', ZRR['success']), + ('ZAPI', 'license-v2-delete-expired', ZRR['success']), + ('ZAPI', 'license-v2-status-list-info', ZRR['license_status_fcp_none']), + ]) + module_args = { + 'use_rest': 'never', + 'remove_unused': True, + 'remove_expired': True, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_module_try_to_remove_non_existent_package_license_zapi(): + ''' Try to remove non existent license ''' + register_responses([ + ('ZAPI', 'license-v2-delete', ZRR['error_object_not_found']), + ]) + module_args = { + 'use_rest': 'never', + 'serial_number': '1-8-000000', + 'license_names': 'cifs', + 'state': 'absent', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + license_exist = my_obj.remove_licenses('cifs') + assert not license_exist + + +def test_module_error_add_license_zapi(): + ''' Test error add license ''' + register_responses([ + ('ZAPI', 'license-v2-add', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'license_codes': 'random', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'Error adding licenses' in expect_and_capture_ansible_exception(my_obj.add_licenses, 'fail')['msg'] + + +def test_module_error_remove_license_zapi(): + ''' Test error remove license ''' + register_responses([ + ('ZAPI', 'license-v2-delete', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'serial_number': '1-8-000000', + 'license_names': 'random', + 'state': 'absent', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'Error removing license' in expect_and_capture_ansible_exception(my_obj.remove_licenses, 'fail', 'random')['msg'] + + +def test_module_error_get_and_remove_unused_expired_license_zapi(): + ''' Test error get and remove unused/expired license ''' + register_responses([ + ('ZAPI', 'license-v2-status-list-info', ZRR['error']), + ('ZAPI', 'license-v2-delete-unused', ZRR['error']), + ('ZAPI', 'license-v2-delete-expired', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'Error checking license status' in expect_and_capture_ansible_exception(my_obj.get_licensing_status, 'fail')['msg'] + assert 'Error removing unused licenses' in expect_and_capture_ansible_exception(my_obj.remove_unused_licenses, 'fail')['msg'] + assert 'Error removing expired licenses' in expect_and_capture_ansible_exception(my_obj.remove_expired_licenses, 'fail')['msg'] + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'error_entry_does_not_exist': (404, None, "entry doesn't exist"), + 'license_record': (200, { + "num_records": 3, + "records": [ + { + "name": "base", + "scope": "cluster", + "state": "compliant" + }, + { + "name": "nfs", + "scope": "not_available", + "state": "unlicensed" + }, + { + "name": "cifs", + "scope": "site", + "state": "compliant" + }] + }, None), + 'license_record_nfs': (200, { + "num_records": 3, + "records": [ + { + "name": "base", + "scope": "cluster", + "state": "compliant" + }, + { + "name": "nfs", + "scope": "site", + "state": "compliant" + }, + { + "name": "cifs", + "scope": "site", + "state": "compliant" + }] + }, None), + 'license_record_no_nfs': (200, { + "num_records": 3, + "records": [ + { + "name": "base", + "scope": "cluster", + "state": "compliant" + }, + { + "name": "cifs", + "scope": "site", + "state": "compliant" + }] + }, None) +}, False) + + +def test_module_fail_when_unsupported_rest_present(): + ''' error if unsupported rest properties present ''' + register_responses([ + ]) + module_args = { + 'remove_unused': True, + 'remove_expired': True, + 'use_rest': 'always' + } + error = 'REST API currently does not support' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_ensure_get_license_status_called_rest(): + ''' test get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + ]) + module_args = { + 'use_rest': 'always' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_no_warnings() + + +def test_module_error_get_license_rest(): + ''' test add license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always' + } + error = rest_error_message('', 'cluster/licensing/licenses') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_module_add_license_rest(): + ''' test add license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), # get license information + ('POST', 'cluster/licensing/licenses', SRR['empty_good']), # Apply license + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), # get updated license information + ]) + module_args = { + 'license_codes': 'LICENCECODE', + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + if HAS_DEEPDIFF: + assert_no_warnings() + + +def test_module_error_add_license_rest(): + ''' test add license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), # get license information + ('POST', 'cluster/licensing/licenses', SRR['generic_error']), # Error in adding license + ]) + module_args = { + 'license_codes': 'INVALIDLICENCECODE', + 'use_rest': 'always' + } + error = 'calling: cluster/licensing/licenses: got Expected error.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_module_remove_license(): + ''' test remove license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), + ('DELETE', 'cluster/licensing/licenses/nfs', SRR['empty_good']), # remove license + ]) + module_args = { + 'license_names': 'nfs', + 'serial_number': '1-23-45678', + 'state': 'absent', + 'use_rest': 'always' + } + print_warnings() + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + assert_no_warnings() + + +def test_module_error_remove_license_rest(): + ''' test remove license error''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), # get license information + ('DELETE', 'cluster/licensing/licenses/nfs', SRR['generic_error']), # Error in removing license + ]) + module_args = { + 'license_names': 'nfs', + 'serial_number': '1-23-45678', + 'state': 'absent', + 'use_rest': 'always' + } + error = rest_error_message('Error removing license for serial number 1-23-45678 and nfs', 'cluster/licensing/licenses/nfs') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_module_try_to_remove_license_not_present_rest(): + ''' test remove license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + ('DELETE', 'cluster/licensing/licenses/nfs', SRR['error_entry_does_not_exist']), # license not active. + + ]) + module_args = { + 'license_names': 'nfs', + 'serial_number': '1-23-45678', + 'state': 'absent', + 'use_rest': 'always' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_no_warnings() + + +@patch('time.sleep') +def test_error_mismatch_in_package_list_rest(dont_sleep): + ''' test remove license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + # 2nd test + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + # 3rd test + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ]) + module_args = { + 'license_names': 'non-existent-package', + 'serial_number': '1-23-45678', + 'use_rest': 'always' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + previous_license_status = {'base': 'compliant', 'nfs': 'unlicensed', 'cifs': 'compliant'} + assert my_obj.compare_license_status(previous_license_status) == [] + previous_license_status = {'base': 'compliant', 'nfs': 'unlicensed', 'cifs': 'unlicensed'} + assert my_obj.compare_license_status(previous_license_status) == ['cifs'] + error = "Error: mismatch in license package names: 'nfs'. Expected:" + assert error in expect_and_capture_ansible_exception(my_obj.compare_license_status, 'fail', previous_license_status)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_license_nlf.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_license_nlf.py new file mode 100644 index 000000000..b4128499d --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_license_nlf.py @@ -0,0 +1,461 @@ +# (c) 2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP license Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import sys +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_no_warnings, assert_warning_was_raised, call_main, create_module, expect_and_capture_ansible_exception, patch_ansible, print_warnings +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_license import NetAppOntapLicense as my_module, main as my_main, HAS_DEEPDIFF + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', +} + +NLF = """ +{"statusResp":{"statusCode":"SUCCESS","message":"Information sent successfully","filter":"SOA","serialNumber":"12345678","cmatID":"0000000", +"product":"%s","version":"2","licenses":{"legacyKey":"Generate NetApp License File (NLF)","HostID":"12345678","type":"capacity", +"package":["CIFS","NFS","S3","FCP","iSCSI","NVMe_oF","FlexClone","SnapRestore","SnapMirror","SnapMirror_Sync","SnapManagerSuite","SnapVault","S3_SnapMirror","VE","TPM"], +"capacity":"1","evaluation":"false","entitlementLastUpdated":"2023-01-04T07:58:16.000-07:00","licenseScope":"node","licenseProtocol":"ENT_ENCRYPT_ED_CAP_3", +"enforcementAttributes":[{"name":"DO-Capacity-Warn","metric":"5:1", +"msg":"You've exceeded your capacity limit. Add capacity to your license to ensure your product use is unaffected.","operatingPolicy":"na"}, +{"name":"DO-Capacity-Enforce","metric":"6:1", +"msg":"You've exceeded your capacity limit. Add capacity to your license to ensure your product use is unaffected.","operatingPolicy":"ndo"}]}}, +"Signature":"xxxx"} +""".replace('\n', '') + +NLF_EE = NLF % "Enterprise Edition" +NLF_CB = NLF % "Core Bundle" + +NLF_MULTIPLE = "%s\n%s" % (NLF_EE, NLF_CB) + +NLF_DICT_NO_PRODUCT = {"statusResp": {"serialNumber": "12345678"}} +NLF_DICT_NO_SERIAL = {"statusResp": {"product": "Enterprise Edition"}} +NLF_DICT_PRODUCT_SN = {"statusResp": {"product": "Enterprise Edition", "serialNumber": "12345678"}} +NLF_DICT_PRODUCT_SN_STAR = {"statusResp": {"product": "Enterprise Edition", "serialNumber": "*"}} + + +def test_module_error_zapi_not_supported(): + ''' Test add license ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + 'license_codes': [NLF_EE], + } + error = 'Error: NLF license format is not supported with ZAPI.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args = { + 'use_rest': 'never', + 'license_codes': [NLF_EE], + 'state': 'absent' + } + error = 'Error: NLF license format is not supported with ZAPI.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'error_entry_does_not_exist': (404, None, "entry doesn't exist"), + 'license_record': (200, { + "num_records": 3, + "records": [ + { + "name": "base", + "scope": "cluster", + "state": "compliant" + }, + { + "name": "nfs", + "scope": "not_available", + "state": "unlicensed" + }, + { + "name": "cifs", + "scope": "site", + "state": "compliant" + }] + }, None), + 'license_record_nfs': (200, { + "num_records": 3, + "records": [ + { + "name": "base", + "scope": "cluster", + "state": "compliant" + }, + { + "name": "nfs", + "scope": "site", + "state": "compliant", + "licenses": [ + { + "installed_license": "Enterprise Edition", + "serial_number": "12345678", + "maximum_size": 1099511627776 + } + + ] + }, + { + "name": "cifs", + "scope": "site", + "state": "compliant" + }] + }, None), + 'license_record_no_nfs': (200, { + "num_records": 3, + "records": [ + { + "name": "base", + "scope": "cluster", + "state": "compliant" + }, + { + "name": "cifs", + "scope": "site", + "state": "compliant" + }] + }, None), + 'conflict_error': (409, None, 'license with conflicts error message'), + 'failed_to_install_error': (400, None, + 'Failed to install the license at index 0. The system received a licensing request with an invalid digital signature.'), +}, False) + + +def test_module_add_nlf_license_rest(): + ''' test add license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), # get license information + ('POST', 'cluster/licensing/licenses', SRR['empty_good']), # Apply license + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), # get updated license information + ]) + module_args = { + 'license_codes': [NLF_EE], + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + if HAS_DEEPDIFF: + assert_no_warnings() + + +def test_module_error_add_nlf_license_rest(): + ''' test add license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + ('POST', 'cluster/licensing/licenses', SRR['conflict_error']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), # get updated license information + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + ('POST', 'cluster/licensing/licenses', SRR['failed_to_install_error']), + ]) + module_args = { + 'license_codes': [NLF_EE], + 'use_rest': 'always' + } + error = rest_error_message('Error: some licenses were updated, but others were in conflict', 'cluster/licensing/licenses', + got='got license with conflicts error message') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + if HAS_DEEPDIFF: + assert_no_warnings() + error = rest_error_message('Error adding license', 'cluster/licensing/licenses', + got='got Failed to install the license at index 0') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + if HAS_DEEPDIFF: + assert_no_warnings() + + +def test_module_remove_nlf_license(): + ''' test remove license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), + ('DELETE', 'cluster/licensing/licenses', SRR['empty_good']), + ]) + module_args = { + 'license_codes': [NLF_EE], + 'state': 'absent', + 'use_rest': 'always' + } + print_warnings() + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + assert_no_warnings() + + +def test_module_remove_nlf_license_by_name(): + ''' test remove license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), + ('DELETE', 'cluster/licensing/licenses', SRR['empty_good']), + ]) + module_args = { + 'license_names': "Enterprise Edition", + 'state': 'absent', + 'use_rest': 'always', + 'serial_number': '12345678' + } + print_warnings() + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + assert_no_warnings() + + +def test_module_error_remove_nlf_license_rest(): + ''' test remove license error''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), + ('DELETE', 'cluster/licensing/licenses', SRR['generic_error']), + ]) + module_args = { + 'license_codes': [NLF_EE], + 'state': 'absent', + 'use_rest': 'always' + } + error = rest_error_message('Error removing license for serial number 12345678 and Enterprise Edition', 'cluster/licensing/licenses') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_module_try_to_remove_nlf_license_not_present_rest(): + ''' test remove license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), + ]) + module_args = { + 'license_codes': [NLF_CB], + 'state': 'absent', + 'use_rest': 'always' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_no_warnings() + + +@patch('time.sleep') +def test_compare_license_status(dont_sleep): + ''' test remove license''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + # 2nd test + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + # deepdiff 1 + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), + # deepdiff 2 + ('GET', 'cluster/licensing/licenses', SRR['license_record_nfs']), + # retries + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record']), + # Error, no records + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ('GET', 'cluster/licensing/licenses', SRR['license_record_no_nfs']), + ]) + module_args = { + 'license_names': 'non-existent-package', + 'serial_number': '1-23-45678', + 'use_rest': 'always' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + previous_license_status = {'base': 'compliant', 'nfs': 'unlicensed', 'cifs': 'compliant'} + assert my_obj.compare_license_status(previous_license_status) == [] + previous_license_status = {'base': 'compliant', 'nfs': 'compliant', 'cifs': 'compliant'} + assert my_obj.compare_license_status(previous_license_status) == ['nfs'] + previous_license_status = {'base': 'compliant', 'nfs': 'unlicensed', 'cifs': 'compliant'} + # deepdiffs + my_obj.previous_records = [{'name': 'base', 'scope': 'cluster', 'state': 'compliant'}] + assert my_obj.compare_license_status(previous_license_status) == (['nfs', 'cifs'] if HAS_DEEPDIFF else ['nfs']) + if HAS_DEEPDIFF: + assert_no_warnings() + with patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_license.HAS_DEEPDIFF', False): + assert my_obj.compare_license_status(previous_license_status) == ['nfs'] + print_warnings() + assert_warning_was_raised('deepdiff is required to identify detailed changes') + # retries, success + previous_license_status = {'base': 'compliant', 'nfs': 'unlicensed', 'cifs': 'unlicensed'} + assert my_obj.compare_license_status(previous_license_status) == (['cifs', 'nfs'] if HAS_DEEPDIFF else ['cifs']) + # retries, error + error = "Error: mismatch in license package names: 'nfs'. Expected:" + assert error in expect_and_capture_ansible_exception(my_obj.compare_license_status, 'fail', previous_license_status)['msg'] + + +def test_format_post_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always', + 'state': 'absent', + 'license_codes': [] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.format_post_error('some_error', {}) == 'some_error' + rest_error = 'The system received a licensing request with an invalid digital signature.' + error = my_obj.format_post_error(rest_error, {}) + assert error == rest_error + rest_error += ' Failed to install the license at index 0' + error = my_obj.format_post_error(rest_error, {'keys': ["'statusResp'"]}) + assert 'Original NLF contents were modified by Ansible.' in error + error = my_obj.format_post_error(rest_error, {'keys': ["'whatever'"]}) + assert 'Original NLF contents were modified by Ansible.' not in error + + +def test_nlf_is_installed(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always', + 'state': 'absent', + 'license_codes': [] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert not my_obj.nlf_is_installed(NLF_DICT_NO_PRODUCT) + assert not my_obj.nlf_is_installed(NLF_DICT_NO_SERIAL) + my_obj.license_status = {} + assert not my_obj.nlf_is_installed(NLF_DICT_PRODUCT_SN) + my_obj.license_status['installed_licenses'] = [] + assert my_obj.nlf_is_installed(NLF_DICT_PRODUCT_SN_STAR) + + +def test_validate_delete_action(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = 'Error: product not found in NLF file' + assert error in expect_and_capture_ansible_exception(my_obj.validate_delete_action, 'fail', NLF_DICT_NO_PRODUCT)['msg'] + error = 'Error: serialNumber not found in NLF file' + assert error in expect_and_capture_ansible_exception(my_obj.validate_delete_action, 'fail', NLF_DICT_NO_SERIAL)['msg'] + my_obj.parameters['serial_number'] = 'otherSN' + error = 'Error: mismatch is serial numbers otherSN vs 12345678' + assert error in expect_and_capture_ansible_exception(my_obj.validate_delete_action, 'fail', NLF_DICT_PRODUCT_SN)['msg'] + + +def test_scan_license_codes_for_nlf(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + nlf = NLF_EE.replace("'", "\\'") + nlf = nlf.replace('"', "'") + license_code, nlf_dict, is_nlf = my_obj.scan_license_codes_for_nlf(nlf) + assert len(nlf_dict) == 2 + assert len(nlf_dict['statusResp']) == 8 + + with patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_license.HAS_AST', False): + error = 'Error: ast and json packages are required to install NLF license files.' + assert error in expect_and_capture_ansible_exception(my_obj.scan_license_codes_for_nlf, 'fail', nlf)['msg'] + + with patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_license.HAS_JSON', False): + error = 'Error: ast and json packages are required to install NLF license files.' + assert error in expect_and_capture_ansible_exception(my_obj.scan_license_codes_for_nlf, 'fail', nlf)['msg'] + + with patch('json.dumps') as json_dumps: + json_dumps.side_effect = Exception('exception for test') + error = 'Error: unable to encode input:' + assert error in expect_and_capture_ansible_exception(my_obj.scan_license_codes_for_nlf, 'fail', nlf)['msg'] + + with patch('json.loads') as json_loads: + json_loads.side_effect = Exception('exception for test') + error = 'Error: the license contents cannot be read. Unable to decode input:' + assert error in expect_and_capture_ansible_exception(my_obj.scan_license_codes_for_nlf, 'fail', nlf)['msg'] + + nlf = "'statusResp':" + # older versions of python report unexpected EOF while parsing + # but python 3.10.2 reports exception: invalid syntax (<unknown>, line 1) + error = "Error: malformed input: 'statusResp':, exception:" + assert error in expect_and_capture_ansible_exception(my_obj.scan_license_codes_for_nlf, 'fail', nlf)['msg'] + + nlf = '"statusResp":' * 2 + error = "Error: NLF license files with multiple licenses are not supported, found 2 in" + assert error in expect_and_capture_ansible_exception(my_obj.scan_license_codes_for_nlf, 'fail', nlf)['msg'] + nlf = '"statusResp":' + ('"serialNumber":' * 2) + error = "Error: NLF license files with multiple serial numbers are not supported, found 2 in" + assert error in expect_and_capture_ansible_exception(my_obj.scan_license_codes_for_nlf, 'fail', nlf)['msg'] + nlf = '"statusResp":' + my_obj.scan_license_codes_for_nlf(nlf) + print_warnings() + assert_warning_was_raised('The license will be installed without checking for idempotency.', partial_match=True) + assert_warning_was_raised('Unable to decode input', partial_match=True) + with patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_license.HAS_JSON', False): + my_obj.scan_license_codes_for_nlf(nlf) + print_warnings() + assert_warning_was_raised('The license will be installed without checking for idempotency.', partial_match=True) + assert_warning_was_raised('the json package is required to process NLF license files', partial_match=True) + + +def test_error_nlf_and_legacy(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always', + 'license_codes': [NLF, 'xxxxxxxxxxxxxxxx'] + } + error = 'Error: cannot mix legacy licenses and NLF licenses; found 1 NLF licenses out of 2 license_codes.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_split_nlfs(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always', + 'license_codes': [NLF_MULTIPLE] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert len(my_obj.parameters['license_codes']) == 2 + # force error: + error = 'Error: unexpected format found 2 entries and 3 lines' + assert error in expect_and_capture_ansible_exception(my_obj.split_nlf, 'fail', '%s\nyyyyy' % NLF_MULTIPLE)['msg'] + + +def test_remove_licenses_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'use_rest': 'always', + 'license_codes': [NLF_MULTIPLE] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = 'Error: serial_number is required to delete a license.' + assert error in expect_and_capture_ansible_exception(my_obj.remove_licenses_rest, 'fail', 'bundle name', {})['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_local_hosts.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_local_hosts.py new file mode 100644 index 000000000..15de03a8b --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_local_hosts.py @@ -0,0 +1,178 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_local_hosts \ + import NetAppOntapLocalHosts as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'host_record': (200, { + "records": [ + { + "owner": {"name": "svm", "uuid": "e3cb5c7fcd20"}, + "address": "10.10.10.10", + "host": "example.com", + "aliases": ["ex1.com", "ex2.com"] + }], + "num_records": 1 + }, None), +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'address': '10.10.10.10', + 'owner': 'svm', +} + + +def test_get_local_host_rest_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['empty_records']) + ]) + module_args = {'address': '10.10.10.10', 'owner': 'svm'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_local_host_rest() is None + + +def test_get_local_host_rest_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['generic_error']) + ]) + module_args = {'address': '10.10.10.10', 'owner': 'svm'} + my_module_object = create_module(my_module, DEFAULT_ARGS, module_args) + msg = 'Error fetching IP to hostname mappings for svm: calling: name-services/local-hosts: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_local_host_rest, 'fail')['msg'] + + +def test_create_local_host_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['empty_records']), + ('POST', 'name-services/local-hosts', SRR['empty_good']) + ]) + module_args = { + 'address': '10.10.10.10', + 'owner': 'svm', + 'host': 'example.com', + 'aliases': ['ex.com', 'ex1.com']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_local_host_rest_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['empty_records']), + ('POST', 'name-services/local-hosts', SRR['generic_error']) + ]) + module_args = { + 'address': '10.10.10.10', + 'owner': 'svm', + 'host': 'example.com', + 'aliases': ['ex.com', 'ex1.com']} + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error creating IP to hostname mappings for svm: calling: name-services/local-hosts: got Expected error.' + assert msg in error + + +def test_create_local_host_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['host_record']) + ]) + module_args = {'state': 'present'} + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_local_host(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['host_record']), + ('DELETE', 'name-services/local-hosts/e3cb5c7fcd20/10.10.10.10', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_local_host_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['host_record']), + ('DELETE', 'name-services/local-hosts/e3cb5c7fcd20/10.10.10.10', SRR['generic_error']) + ]) + module_args = {'state': 'absent'} + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error deleting IP to hostname mappings for svm: calling: name-services/local-hosts/e3cb5c7fcd20/10.10.10.10: got Expected error.' + assert msg in error + + +def test_delete_local_host_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['empty_records']) + ]) + module_args = {'state': 'absent'} + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_local_host(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['host_record']), + ('PATCH', 'name-services/local-hosts/e3cb5c7fcd20/10.10.10.10', SRR['empty_good']) + ]) + module_args = { + 'address': '10.10.10.10', + 'owner': 'svm', + 'host': 'example1.com', + 'aliases': ['ex.com', 'ex1.com', 'ex2.com']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_local_host_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'name-services/local-hosts', SRR['host_record']), + ('PATCH', 'name-services/local-hosts/e3cb5c7fcd20/10.10.10.10', SRR['generic_error']) + ]) + module_args = { + 'address': '10.10.10.10', + 'owner': 'svm', + 'host': 'example1.com', + 'aliases': ['ex.com', 'ex1.com', 'ex2.com']} + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error updating IP to hostname mappings for svm: calling: name-services/local-hosts/e3cb5c7fcd20/10.10.10.10: got Expected error.' + assert msg in error + + +def validate_input_ipaddress(): + register_responses([ + ]) + module_args = {'address': '2001:0000:3238:DFE1:63:0000:0000:FEFBSS', 'owner': 'svm'} + error = create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error: Invalid IP address value 2001:0000:3238:DFE1:63:0000:0000:FEFBSS' + assert msg in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_log_forward.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_log_forward.py new file mode 100644 index 000000000..5214b76d2 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_log_forward.py @@ -0,0 +1,343 @@ +''' unit tests ONTAP Ansible module: na_ontap_log_forward ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_log_forward \ + import NetAppOntapLogForward as log_forward_module # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'log_forward_record': (200, { + "records": [{ + "address": "10.11.12.13", + "facility": "user", + "port": 514, + "protocol": "udp_unencrypted", + "verify_server": False + }] + }, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'log_forward': + xml = self.build_log_forward_info() + elif self.type == 'log_forward_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_log_forward_info(): + ''' build xml data for cluster-log-forward-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes': { + 'cluster-log-forward-info': { + 'destination': '10.11.12.13', + 'facility': 'user', + 'port': '514', + 'protocol': 'udp_unencrypted', + 'verify-server': 'false' + } + } + } + + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self, use_rest=None): + if self.onbox: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + destination = '10.11.12.13' + port = 514 + facility = 'user' + force = True + protocol = 'udp_unencrypted' + verify_server = False + else: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + destination = '10.11.12.13' + port = 514 + facility = 'user' + force = True + protocol = 'udp_unencrypted' + verify_server = False + + args = dict({ + 'state': 'present', + 'hostname': hostname, + 'username': username, + 'password': password, + 'destination': destination, + 'port': port, + 'facility': facility, + 'force': force, + 'protocol': protocol, + 'verify_server': verify_server + }) + + if use_rest is not None: + args['use_rest'] = use_rest + + return args + + @staticmethod + def get_log_forward_mock_object(cx_type='zapi', kind=None): + log_forward_obj = log_forward_module() + if cx_type == 'zapi': + if kind is None: + log_forward_obj.server = MockONTAPConnection() + else: + log_forward_obj.server = MockONTAPConnection(kind=kind) + return log_forward_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + log_forward_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called(self): + ''' test get_log_forward_config for non-existent config''' + set_module_args(self.set_default_args(use_rest='Never')) + print('starting') + my_obj = log_forward_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = self.server + assert my_obj.get_log_forward_config is not None + + def test_ensure_get_called_existing(self): + ''' test get_log_forward_config for existing config''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = log_forward_module() + my_obj.server = MockONTAPConnection(kind='log_forward') + assert my_obj.get_log_forward_config() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_log_forward.NetAppOntapLogForward.create_log_forward_config') + def test_successful_create(self, create_log_forward_config): + ''' creating log_forward config and testing idempotency ''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = log_forward_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + create_log_forward_config.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = log_forward_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('log_forward') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_log_forward.NetAppOntapLogForward.destroy_log_forward_config') + def test_successful_delete(self, destroy_log_forward): + ''' deleting log_forward config and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['state'] = 'absent' + set_module_args(data) + my_obj = log_forward_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('log_forward') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # destroy_log_forward_config.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + my_obj = log_forward_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_log_forward.NetAppOntapLogForward.modify_log_forward_config') + def test_successful_modify(self, modify_log_forward_config): + ''' modifying log_forward config and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['facility'] = 'kern' + set_module_args(data) + my_obj = log_forward_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('log_forward') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + + # modify_log_forward_config.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + data['facility'] = 'user' + set_module_args(data) + my_obj = log_forward_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('log_forward') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + data = self.set_default_args(use_rest='Never') + set_module_args(data) + my_obj = log_forward_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('log_forward_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_log_forward_config() + assert 'Error creating log forward config with destination ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.destroy_log_forward_config() + assert 'Error destroying log forward destination ' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_log_forward_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['msg'] == SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_create_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_log_forward_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_create_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['log_forward_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_log_forward_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_delete_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['log_forward_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_log_forward_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_delete_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_good'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_log_forward_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_modify_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'present' + data['facility'] = 'kern' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['log_forward_record'], # get + SRR['empty_good'], # delete + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_log_forward_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_modify_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'present' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['log_forward_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_log_forward_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_login_messages.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_login_messages.py new file mode 100644 index 000000000..ac628e8e2 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_login_messages.py @@ -0,0 +1,332 @@ +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_login_messages''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import call_main, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_login_messages import main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') +HAS_NETAPP_ZAPI_MSG = "pip install netapp_lib is required" + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'svm_uuid': (200, {"records": [{"uuid": "test_uuid"}], "num_records": 1}, None), + 'login_info': (200, { + "records": [{ + "banner": "banner", + "message": "message", + "show_cluster_message": True, + "uuid": "uuid_uuid" + }], + "num_records": 1}, None), + 'login_info_trailing_newline': (200, { + "records": [{ + "banner": "banner\n", + "message": "message\n", + "show_cluster_message": True, + "uuid": "uuid_uuid" + }], + "num_records": 1}, None), +}) + + +banner_info = { + 'num-records': 1, + 'attributes-list': [{'vserver-login-banner-info': { + 'message': 'banner message', + }}]} + + +banner_info_empty = { + 'num-records': 1, + 'attributes-list': [{'vserver-login-banner-info': { + 'message': '-', + 'vserver': 'vserver' + }}]} + + +motd_info = { + 'num-records': 1, + 'attributes-list': [{'vserver-motd-info': { + 'is-cluster-message-enabled': 'true', + 'message': 'motd message', + 'vserver': 'vserver' + }}]} + + +motd_info_empty = { + 'num-records': 1, + 'attributes-list': [{'vserver-motd-info': { + 'is-cluster-message-enabled': 'true', + 'vserver': 'vserver' + }}]} + + +ZRR = zapi_responses({ + 'banner_info': build_zapi_response(banner_info), + 'banner_info_empty': build_zapi_response(banner_info_empty), + 'motd_info': build_zapi_response(motd_info), + 'motd_info_empty': build_zapi_response(motd_info_empty), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + } + assert "Error: vserver is a required parameter when using ZAPI." == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + module_args = { + 'use_rest': 'never', + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successfully_create_banner(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + ('ZAPI', 'vserver-login-banner-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-login-banner-modify-iter', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'banner': 'test banner', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_banner_idempotency(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + ('ZAPI', 'vserver-login-banner-get-iter', ZRR['banner_info']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'banner': 'banner message', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_create_motd(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info_empty']), + ('ZAPI', 'vserver-login-banner-get-iter', ZRR['banner_info_empty']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'motd_message': 'test message', + 'show_cluster_motd': False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_motd_idempotency(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + ('ZAPI', 'vserver-login-banner-get-iter', ZRR['banner_info']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'motd_message': 'motd message', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_motd_modify(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + ('ZAPI', 'vserver-login-banner-get-iter', ZRR['banner_info']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'motd_message': 'motd message', + 'show_cluster_motd': False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_get_banner_error(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + ('ZAPI', 'vserver-login-banner-get-iter', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == zapi_error_message('Error fetching login_banner info') + + +def test_get_motd_error(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == zapi_error_message('Error fetching motd info') + + +def test_modify_banner_error(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-login-banner-get-iter', ZRR['banner_info']), + ('ZAPI', 'vserver-login-banner-modify-iter', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'banner': 'modify to new banner', + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == zapi_error_message('Error modifying login_banner') + + +def test_modify_motd_error(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + ('ZAPI', 'vserver-login-banner-get-iter', ZRR['banner_info']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'vserver': 'vserver', + 'motd_message': 'modify to new motd', + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == zapi_error_message('Error modifying motd') + + +def test_successfully_create_banner_rest(): + register_responses([ + # no vserver, cluster scope + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['login_info']), + ('PATCH', 'security/login/messages/uuid_uuid', SRR['success']), + # with vserver + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['zero_records']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('PATCH', 'security/login/messages/test_uuid', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'banner': 'test banner', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['vserver'] = 'vserver' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_banner_rest(): + register_responses([ + # no vserver, cluster scope + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['login_info']), + ('PATCH', 'security/login/messages/uuid_uuid', SRR['success']), + # idempotent check + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['login_info_trailing_newline']) + ]) + module_args = { + 'use_rest': 'always', + 'banner': 'banner\n', + 'message': 'message\n', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is False + + +def test_successfully_create_motd_rest(): + register_responses([ + # no vserver, cluster scope + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['login_info']), + ('PATCH', 'security/login/messages/uuid_uuid', SRR['success']), + # with vserver + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['login_info']), + ('PATCH', 'security/login/messages/uuid_uuid', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'motd_message': 'test motd', + 'show_cluster_motd': False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['vserver'] = 'vserver' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_banner_error_rest(): + register_responses([ + # no vserver, cluster scope + # error fetching info + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['generic_error']), + # error no info at cluster level + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['zero_records']), + # with vserver + # error fetching SVM UUID + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['zero_records']), + ('GET', 'svm/svms', SRR['generic_error']), + # error, SVM not found + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['zero_records']), + ('GET', 'svm/svms', SRR['zero_records']), + # error, on patch + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/login/messages', SRR['login_info']), + ('PATCH', 'security/login/messages/uuid_uuid', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + 'banner': 'test banner', + # 'show_cluster_motd': False + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == rest_error_message( + 'Error fetching login_banner info', 'security/login/messages') + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == 'Error fetching login_banner info for cluster - no data.' + module_args['vserver'] = 'vserver' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == rest_error_message('Error fetching vserver vserver', 'svm/svms') + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] ==\ + 'Error fetching vserver vserver. Please make sure vserver name is correct. For cluster vserver, don\'t set vserver.' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == rest_error_message( + 'Error modifying banner', 'security/login/messages/uuid_uuid') diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun.py new file mode 100644 index 000000000..5331458e1 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun.py @@ -0,0 +1,308 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_module, expect_and_capture_ansible_exception, patch_ansible +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, build_zapi_response, zapi_error_message, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun import NetAppOntapLUN as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def lun_info(name, next_tag=None): + info = { + 'num-records': 1, + 'attributes-list': [{ + 'lun_info': { + 'path': "/what/ever/%s" % name, + 'size': 5368709120, + 'is-space-alloc-enabled': "false", + 'is-space-reservation-enabled': "true", + 'multiprotocol-type': 'linux', + 'qos-policy-group': 'qospol', + 'qos-adaptive-policy-group': 'qosadppol', + } + }] + } + if next_tag: + info['next-tag'] = next_tag + return info + + +ZRR = zapi_responses({ + 'lun_info': build_zapi_response(lun_info('lun_name')), + 'lun_info_from': build_zapi_response(lun_info('lun_from_name')), + 'lun_info_with_tag': build_zapi_response(lun_info('lun_name', 'more to come')), + 'error_9042': build_zapi_error(9042, 'new size == old size, more or less'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'use_rest', + 'name': 'lun_name', + 'vserver': 'lunsvm_name', +} + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + module_args = { + 'use_rest': 'never', + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never' + } + print('Info: %s' % call_main(my_main, {}, module_args, fail=True)['msg']) + + +def test_create_error_missing_param(): + ''' Test if create throws an error if required param 'destination_vserver' is not specified''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + } + msg = "Error: 'flexvol_name' option is required when using ZAPI." + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['flexvol_name'] = 'xxx' + msg = 'size is a required parameter for create.' + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['no_records']), + ('ZAPI', 'lun-create-by-size', ZRR['success']), + # second create + ('ZAPI', 'lun-get-iter', ZRR['no_records']), + ('ZAPI', 'lun-create-by-size', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'comment': 'some comment', + 'flexvol_name': 'vol_name', + 'qos_adaptive_policy_group': 'new_adaptive_pol', + 'size': 5, + 'space_allocation': False, + 'space_reserve': False, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args = { + 'use_rest': 'never', + 'comment': 'some comment', + 'flexvol_name': 'vol_name', + 'os_type': 'windows', + 'qos_policy_group': 'new_pol', + 'size': 5, + 'space_allocation': False, + 'space_reserve': False, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_rename_idempotency(): + ''' Test create idempotency ''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ]) + module_args = { + 'use_rest': 'never', + 'flexvol_name': 'vol_name', + 'size': 5, + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_lun(): + ''' Test delete and idempotency ''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ('ZAPI', 'lun-destroy', ZRR['success']), + # idempotency + ('ZAPI', 'lun-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + 'flexvol_name': 'vol_name', + 'state': 'absent', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_lun_no_input(): + ''' Nothing to delete! ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + 'state': 'absent', + } + msg = "Error: 'flexvol_name' option is required when using ZAPI." + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successful_resize(): + ''' Test successful resize ''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ('ZAPI', 'lun-resize', ZRR['success']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ('ZAPI', 'lun-resize', ZRR['error_9042']), + ]) + module_args = { + 'use_rest': 'never', + 'flexvol_name': 'vol_name', + 'size': 7 + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify(): + ''' Test successful modify ''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ('ZAPI', 'lun-set-comment', ZRR['success']), + ('ZAPI', 'lun-set-qos-policy-group', ZRR['success']), + ('ZAPI', 'lun-set-space-alloc', ZRR['success']), + # second call + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ('ZAPI', 'lun-set-comment', ZRR['success']), + ('ZAPI', 'lun-set-qos-policy-group', ZRR['success']), + ('ZAPI', 'lun-set-space-reservation-info', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'comment': 'some comment', + 'flexvol_name': 'vol_name', + 'qos_policy_group': 'new_pol', + 'space_allocation': True, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args = { + 'use_rest': 'never', + 'comment': 'some comment', + 'flexvol_name': 'vol_name', + 'qos_adaptive_policy_group': 'new_adaptive_pol', + 'space_allocation': False, + 'space_reserve': False, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_modify(): + ''' Test successful modify ''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['lun_info']), + ]) + module_args = { + 'use_rest': 'never', + 'flexvol_name': 'vol_name', + 'comment': 'some comment', + 'os_type': 'windows', + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == 'os_type cannot be modified: current: linux, desired: windows' + + +def test_successful_rename(): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['no_records']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info_from']), + ('ZAPI', 'lun-move', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'flexvol_name': 'vol_name', + 'from_name': 'lun_from_name' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_failed_rename(): + ''' Test failed rename ''' + register_responses([ + ('ZAPI', 'lun-get-iter', ZRR['no_records']), + ('ZAPI', 'lun-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + 'flexvol_name': 'vol_name', + 'from_name': 'lun_from_name' + } + msg = 'Error renaming lun: lun_from_name does not exist' + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_zapi_errors(): + register_responses([ + # get error + ('ZAPI', 'lun-get-iter', ZRR['error']), + # error on next tag + ('ZAPI', 'lun-get-iter', ZRR['lun_info_with_tag']), + ('ZAPI', 'lun-get-iter', ZRR['lun_info_with_tag']), + ('ZAPI', 'lun-get-iter', ZRR['error']), + # create error + ('ZAPI', 'lun-create-by-size', ZRR['error']), + # resize error + ('ZAPI', 'lun-resize', ZRR['error']), + # rename error + ('ZAPI', 'lun-move', ZRR['error']), + # modify error + ('ZAPI', 'lun-set-space-reservation-info', ZRR['error']), + # delete error + ('ZAPI', 'lun-destroy', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'flexvol_name': 'vol_name', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + msg = 'Error fetching luns for vol_name' + assert zapi_error_message(msg) == expect_and_capture_ansible_exception(my_obj.get_luns, 'fail')['msg'] + assert zapi_error_message(msg) == expect_and_capture_ansible_exception(my_obj.get_luns, 'fail')['msg'] + + my_obj.parameters['size'] = 123456 + msg = 'Error provisioning lun lun_name of size 123456' + assert zapi_error_message(msg) == expect_and_capture_ansible_exception(my_obj.create_lun, 'fail')['msg'] + + msg = 'Error resizing lun path' + assert zapi_error_message(msg) == expect_and_capture_ansible_exception(my_obj.resize_lun, 'fail', 'path')['msg'] + + my_obj.parameters.pop('size') + msg = 'Error moving lun old_path' + assert zapi_error_message(msg) == expect_and_capture_ansible_exception(my_obj.rename_lun, 'fail', 'old_path', 'new_path')['msg'] + + msg = 'Error setting lun option space_reserve' + assert zapi_error_message(msg) == expect_and_capture_ansible_exception(my_obj.modify_lun, 'fail', 'path', {'space_reserve': True})['msg'] + + msg = 'Error deleting lun path' + assert zapi_error_message(msg) == expect_and_capture_ansible_exception(my_obj.delete_lun, 'fail', 'path')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_app_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_app_rest.py new file mode 100644 index 000000000..0a874f2a6 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_app_rest.py @@ -0,0 +1,584 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import copy +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock, call +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible, assert_warning_was_raised, print_warnings +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun \ + import NetAppOntapLUN as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest_96': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_97': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy')), None), + 'is_rest_98': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {'records': []}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_apps_empty': (200, + {'records': [], + 'num_records': 0 + }, + None + ), + 'get_apps_found': (200, + {'records': [dict(name='san_appli', uuid='1234')], + 'num_records': 1 + }, + None + ), + 'get_app_components': (200, + {'records': [dict(name='san_appli', uuid='1234')], + 'num_records': 1 + }, + None + ), + 'get_app_details': (200, + dict(name='san_appli', uuid='1234', + san=dict(application_components=[dict(name='lun_name', lun_count=3, total_size=1000)]), + statistics=dict(space=dict(provisioned=1100)) + ), + None + ), + 'get_app_component_details': (200, + {'backing_storage': dict(luns=[]), + }, + None + ), + 'get_volumes_found': (200, + {'records': [dict(name='san_appli', uuid='1234')], + 'num_records': 1 + }, + None + ), + 'get_lun_path': (200, + {'records': [{'uuid': '1234', 'path': '/vol/lun_name/lun_name'}], + 'num_records': 1 + }, + None + ), + 'one_lun': (200, + {'records': [{ + 'uuid': "1234", + 'name': '/vol/lun_name/lun_name', + 'path': '/vol/lun_name/lun_name', + 'size': 9871360, + 'comment': None, + 'flexvol_name': None, + 'os_type': 'xyz', + 'qos_policy_group': None, + 'space_reserve': False, + 'space_allocation': False + }], + }, None), + 'get_storage': (200, + {'backing_storage': dict(luns=[{'path': '/vol/lun_name/lun_name', + 'uuid': '1234', + 'size': 15728640, + 'creation_timestamp': '2022-07-26T20:35:50+00:00' + }]), + }, None), + +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, parm1=None): + ''' save arguments ''' + self.type = kind + self.parm1 = parm1 + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'lun': + xml = self.build_lun_info(self.parm1) + self.xml_out = xml + return xml + + @staticmethod + def build_lun_info(lun_name): + ''' build xml data for lun-info ''' + xml = netapp_utils.zapi.NaElement('xml') + lun = dict( + lun_info=dict( + path="/what/ever/%s" % lun_name, + size=10 + ) + ) + attributes = { + 'num-records': 1, + 'attributes-list': [lun] + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_lun_args = { + 'vserver': 'ansible', + 'name': 'lun_name', + 'flexvol_name': 'vol_name', + 'state': 'present' + } + + def mock_args(self): + return { + 'vserver': self.mock_lun_args['vserver'], + 'name': self.mock_lun_args['name'], + 'flexvol_name': self.mock_lun_args['flexvol_name'], + 'state': self.mock_lun_args['state'], + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + } + # self.server = MockONTAPConnection() + + def get_lun_mock_object(self, kind=None, parm1=None): + """ + Helper method to return an na_ontap_lun object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_interface object + """ + lun_obj = my_module() + lun_obj.autosupport_log = Mock(return_value=None) + lun_obj.server = MockONTAPConnection(kind=kind, parm1=parm1) + return lun_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_create_error_missing_param(self): + ''' Test if create throws an error if required param 'destination_vserver' is not specified''' + data = self.mock_args() + set_module_args(data) + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli') + with pytest.raises(AnsibleFailJson) as exc: + self.get_lun_mock_object().apply() + msg = 'size is a required parameter for create.' + assert msg == exc.value.args[0]['msg'] + + def test_create_error_missing_param2(self): + ''' Test if create throws an error if required param 'destination_vserver' is not specified''' + data = self.mock_args() + data.pop('flexvol_name') + data['size'] = 5 + data['san_application_template'] = dict(lun_count=6) + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_lun_mock_object().apply() + msg = 'missing required arguments: name found in san_application_template' + assert msg == exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_create_appli(self, mock_request): + ''' Test successful create ''' + mock_request.side_effect = [ + SRR['is_rest_98'], + SRR['get_apps_empty'], # GET application/applications + SRR['get_apps_empty'], # GET volumes + SRR['empty_good'], # POST application/applications + SRR['end_of_sequence'] + ] + data = dict(self.mock_args()) + data['size'] = 5 + data.pop('flexvol_name') + tiering = dict(control='required') + data['san_application_template'] = dict(name='san_appli', tiering=tiering) + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_lun_mock_object().apply() + assert exc.value.args[0]['changed'] + expected_json = {'name': 'san_appli', 'svm': {'name': 'ansible'}, 'smart_container': True, + 'san': {'application_components': + [{'name': 'lun_name', 'lun_count': 1, 'total_size': 5368709120, 'tiering': {'control': 'required'}}]}} + expected_call = call( + 'POST', 'application/applications', {'return_timeout': 30, 'return_records': 'true'}, json=expected_json, headers=None, files=None) + assert expected_call in mock_request.mock_calls + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_create_appli_idem(self, mock_request): + ''' Test successful create idempotent ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + SRR['get_apps_found'], # GET application/applications/<uuid>/components + SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data['size'] = 5 + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli') + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_lun_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_create_appli_idem_no_comp(self, mock_request): + ''' Test successful create idempotent ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + SRR['get_apps_empty'], # GET application/applications/<uuid>/components + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data['size'] = 5 + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli') + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_lun_mock_object().apply() + # print(mock_request.call_args_list) + msg = 'Error: no component for application san_appli' + assert msg == exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_delete_appli(self, mock_request): + ''' Test successful create ''' + mock_request.side_effect = [ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['empty_good'], # POST application/applications + SRR['end_of_sequence'] + ] + data = dict(self.mock_args()) + data['size'] = 5 + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli') + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_lun_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_delete_appli_idem(self, mock_request): + ''' Test successful delete idempotent ''' + mock_request.side_effect = [ + SRR['is_rest_98'], + SRR['get_apps_empty'], # GET application/applications + SRR['end_of_sequence'] + ] + data = dict(self.mock_args()) + data['size'] = 5 + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli') + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_lun_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_modify_appli(self, mock_request): + ''' Test successful modify application ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + SRR['get_apps_found'], # GET application/applications/<uuid>/components + SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['empty_good'], + SRR['get_lun_path'], + SRR['get_storage'], + SRR['one_lun'], + SRR['empty_good'], + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data['os_type'] = 'xyz' + data['space_reserve'] = True + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', lun_count=5, total_size=1000, igroup_name='abc') + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_lun_mock_object().apply() + print(exc.value.args[0]) + # print(mock_request.call_args_list) + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_error_modify_appli_missing_igroup(self, mock_request): + ''' Test successful modify application ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + # SRR['get_apps_found'], # GET application/applications/<uuid>/components + # SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data['size'] = 5 + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', lun_count=5) + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_lun_mock_object().apply() + msg = 'Error: igroup_name is a required parameter when increasing lun_count.' + assert msg in exc.value.args[0]['msg'] + msg = 'Error: total_size is a required parameter when increasing lun_count.' + assert msg in exc.value.args[0]['msg'] + msg = 'Error: os_type is a required parameter when increasing lun_count.' + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_no_action(self, mock_request): + ''' Test successful modify application ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + SRR['get_apps_found'], # GET application/applications/<uuid>/components + SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data['name'] = 'unknown' + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', lun_count=5) + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_lun_mock_object().apply() + print(exc.value.args[0]) + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_no_96(self, mock_request): + ''' Test SAN application not supported on 9.6 ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_96'], + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data['name'] = 'unknown' + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', lun_count=5) + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_lun_mock_object().apply() + print(exc.value.args[0]['msg']) + msg = 'Error: using san_application_template requires ONTAP 9.7 or later and REST must be enabled.' + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_no_modify_on97(self, mock_request): + ''' Test modify SAN application not supported on 9.7 ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_97'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + SRR['get_apps_found'], # GET application/applications/<uuid>/components + SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data.pop('flexvol_name') + data['os_type'] = 'xyz' + data['san_application_template'] = dict(name='san_appli', lun_count=5, total_size=1000, igroup_name='abc') + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_lun_mock_object().apply() + print(exc.value.args[0]) + msg = 'Error: modifying lun_count, total_size is not supported on ONTAP 9.7' + # in python 2.6, keys() is not sorted! + msg2 = 'Error: modifying total_size, lun_count is not supported on ONTAP 9.7' + assert msg in exc.value.args[0]['msg'] or msg2 in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_no_modify_on97_2(self, mock_request): + ''' Test modify SAN application not supported on 9.7 ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_97'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + SRR['get_apps_found'], # GET application/applications/<uuid>/components + SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', total_size=1000) + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_lun_mock_object().apply() + print(exc.value.args[0]) + msg = 'Error: modifying total_size is not supported on ONTAP 9.7' + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_app_changes_reduction_not_allowed(self, mock_request): + ''' Test modify SAN application - can't decrease size ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + # SRR['get_apps_found'], # GET application/applications/<uuid>/components + # SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', total_size=899, total_size_unit='b') + set_module_args(data) + lun_object = self.get_lun_mock_object() + with pytest.raises(AnsibleFailJson) as exc: + lun_object.app_changes('scope') + msg = "Error: can't reduce size: total_size=1000, provisioned=1100, requested=899" + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_app_changes_reduction_small_enough_10(self, mock_request): + ''' Test modify SAN application - a 10% reduction is ignored ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + # SRR['get_apps_found'], # GET application/applications/<uuid>/components + # SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', total_size=900, total_size_unit='b') + set_module_args(data) + lun_object = self.get_lun_mock_object() + results = lun_object.app_changes('scope') + print(results) + print(lun_object.debug) + msg = "Ignoring small reduction (10.0 %) in total size: total_size=1000, provisioned=1100, requested=900" + assert_warning_was_raised(msg) + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_app_changes_reduction_small_enough_17(self, mock_request): + ''' Test modify SAN application - a 1.7% reduction is ignored ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + # SRR['get_apps_found'], # GET application/applications/<uuid>/components + # SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', total_size=983, total_size_unit='b') + set_module_args(data) + lun_object = self.get_lun_mock_object() + results = lun_object.app_changes('scope') + print(results) + print(lun_object.debug) + print_warnings() + msg = "Ignoring small reduction (1.7 %) in total size: total_size=1000, provisioned=1100, requested=983" + assert_warning_was_raised(msg) + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_app_changes_increase_small_enough(self, mock_request): + ''' Test modify SAN application - a 1.7% reduction is ignored ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + # SRR['get_apps_found'], # GET application/applications/<uuid>/components + # SRR['get_app_component_details'], # GET application/applications/<uuid>/components/<cuuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data.pop('flexvol_name') + data['san_application_template'] = dict(name='san_appli', total_size=1050, total_size_unit='b') + set_module_args(data) + lun_object = self.get_lun_mock_object() + results = lun_object.app_changes('scope') + print(results) + print(lun_object.debug) + msg = "Ignoring increase: requested size is too small: total_size=1000, provisioned=1100, requested=1050" + assert_warning_was_raised(msg) + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_convert_to_appli(self, mock_request): + ''' Test successful convert to application + Appli does not exist, but the volume does. + ''' + mock_request.side_effect = copy.deepcopy([ + SRR['is_rest_98'], + SRR['get_apps_empty'], # GET application/applications + SRR['get_volumes_found'], # GET volumes + SRR['empty_good'], # POST application/applications + SRR['get_apps_found'], # GET application/applications + SRR['get_app_details'], # GET application/applications/<uuid> + SRR['end_of_sequence'] + ]) + data = dict(self.mock_args()) + data['size'] = 5 + data.pop('flexvol_name') + tiering = dict(control='required') + data['san_application_template'] = dict(name='san_appli', tiering=tiering, scope='application') + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_lun_mock_object().apply() + # assert exc.value.args[0]['changed'] + print(mock_request.mock_calls) + print(exc.value.args[0]) + expected_json = {'name': 'san_appli', 'svm': {'name': 'ansible'}, 'smart_container': True, + 'san': {'application_components': + [{'name': 'lun_name'}]}} + expected_call = call( + 'POST', 'application/applications', {'return_timeout': 30, 'return_records': 'true'}, json=expected_json, headers=None, files=None) + assert expected_call in mock_request.mock_calls + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_negative_convert_to_appli(self, mock_request): + ''' Test successful convert to application + Appli does not exist, but the volume does. + ''' + mock_request.side_effect = [ + SRR['is_rest_97'], + SRR['get_apps_empty'], # GET application/applications + SRR['get_volumes_found'], # GET volumes + SRR['end_of_sequence'] + ] + data = dict(self.mock_args()) + data['size'] = 5 + data.pop('flexvol_name') + tiering = dict(control='required') + data['san_application_template'] = dict(name='san_appli', tiering=tiering, scope='application') + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_lun_mock_object().apply() + msg = "Error: converting a LUN volume to a SAN application container requires ONTAP 9.8 or better." + assert msg in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_copy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_copy.py new file mode 100644 index 000000000..93d446809 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_copy.py @@ -0,0 +1,113 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + create_module, create_and_apply, expect_and_capture_ansible_exception +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun_copy \ + import NetAppOntapLUNCopy as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + +DEFAULT_ARGS = { + 'source_vserver': 'ansible', + 'destination_path': '/vol/test/test_copy_dest_dest_new_reviewd_new', + 'source_path': '/vol/test/test_copy_1', + 'destination_vserver': 'ansible', + 'state': 'present', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'never' +} + + +ZRR = zapi_responses({ + 'lun_info': build_zapi_response({'num-records': 1}) +}) + + +SRR = rest_responses({ + 'lun_info': (200, {"records": [{ + "name": "/vol/vol0/lun1_10" + }], "num_records": 1}, None) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "destination_vserver", "destination_path", "source_path"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_create_error_missing_param(): + ''' Test if create throws an error if required param 'destination_vserver' is not specified''' + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['destination_vserver'] + msg = 'missing required arguments: destination_vserver' + assert msg in create_module(my_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + + +def test_successful_copy(): + ''' Test successful create and idempotent check ''' + register_responses([ + ('lun-get-iter', ZRR['empty']), + ('lun-copy-start', ZRR['success']), + ('lun-get-iter', ZRR['lun_info']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('lun-get-iter', ZRR['error']), + ('lun-copy-start', ZRR['error']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/luns', SRR['generic_error']), + ('POST', 'storage/luns', SRR['generic_error']), + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/luns', SRR['empty_records']) + ]) + lun_obj = create_module(my_module, DEFAULT_ARGS) + assert 'Error getting lun info' in expect_and_capture_ansible_exception(lun_obj.get_lun, 'fail')['msg'] + assert 'Error copying lun from' in expect_and_capture_ansible_exception(lun_obj.copy_lun, 'fail')['msg'] + lun_obj = create_module(my_module, DEFAULT_ARGS, {'use_rest': 'always'}) + assert 'Error getting lun info' in expect_and_capture_ansible_exception(lun_obj.get_lun_rest, 'fail')['msg'] + assert 'Error copying lun from' in expect_and_capture_ansible_exception(lun_obj.copy_lun_rest, 'fail')['msg'] + assert 'REST requires ONTAP 9.10.1 or later' in create_module(my_module, DEFAULT_ARGS, {'use_rest': 'always'}, fail=True)['msg'] + args = {'use_rest': 'always', 'destination_vserver': 'some_vserver'} + assert 'REST does not supports inter-Vserver lun copy' in create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_successful_copy_rest(): + ''' Test successful create and idempotent check in REST ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/luns', SRR['empty_records']), + ('POST', 'storage/luns', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/luns', SRR['lun_info']), + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map.py new file mode 100644 index 000000000..120e5f7b3 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map.py @@ -0,0 +1,159 @@ +''' unit tests ONTAP Ansible module: na_ontap_lun_map ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun_map \ + import NetAppOntapLUNMap as my_module + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'lun_map': + xml = self.build_lun_info() + elif self.type == 'lun_map_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_lun_info(): + ''' build xml data for lun-map-entry ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'initiator-groups': [{'initiator-group-info': {'initiator-group-name': 'ansible', 'lun-id': 2}}]} + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self): + if self.onbox: + hostname = '10.10.10.10' + username = 'admin' + password = 'password' + initiator_group_name = 'ansible' + vserver = 'ansible' + path = '/vol/ansible/test' + lun_id = 2 + else: + hostname = 'hostname' + username = 'username' + password = 'password' + initiator_group_name = 'ansible' + vserver = 'ansible' + path = '/vol/ansible/test' + lun_id = 2 + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'initiator_group_name': initiator_group_name, + 'vserver': vserver, + 'path': path, + 'lun_id': lun_id, + 'use_rest': 'false' + }) + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called(self): + ''' test get_lun_map for non-existent lun''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = self.server + assert my_obj.get_lun_map is not None + + def test_ensure_get_called_existing(self): + ''' test get_lun_map for existing lun''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = MockONTAPConnection(kind='lun_map') + assert my_obj.get_lun_map() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun_map.NetAppOntapLUNMap.create_lun_map') + def test_successful_create(self, create_lun_map): + ''' mapping lun and testing idempotency ''' + data = self.set_default_args() + set_module_args(data) + my_obj = my_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + create_lun_map.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + set_module_args(self.set_default_args()) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('lun_map') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun_map.NetAppOntapLUNMap.delete_lun_map') + def test_successful_delete(self, delete_lun_map): + ''' unmapping lun and testing idempotency ''' + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('lun_map') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + delete_lun_map.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('lun_map_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_lun_map() + assert 'Error mapping lun' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_lun_map() + assert 'Error unmapping lun' in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map_reporting_nodes.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map_reporting_nodes.py new file mode 100644 index 000000000..e09016eda --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map_reporting_nodes.py @@ -0,0 +1,170 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP lun reporting nodes Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + create_module, create_and_apply, expect_and_capture_ansible_exception +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun_map_reporting_nodes \ + import NetAppOntapLUNMapReportingNodes as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +DEFAULT_ARGS = { + 'initiator_group_name': 'igroup1', + "path": "/vol/lun1/lun1_1", + "vserver": "svm1", + 'nodes': 'ontap910-01', + 'state': 'present', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'never' +} + + +node_info = { + 'num-records': "1", + 'attributes-list': { + 'lun-map-info': { + 'reporting-nodes': [{"node-name": "ontap910-01"}] + } + } +} + + +nodes_info = { + 'num-records': "1", + 'attributes-list': { + 'lun-map-info': { + 'reporting-nodes': [{"node-name": "ontap910-01"}, {"node-name": "ontap910-02"}] + } + } +} + + +ZRR = zapi_responses({ + 'node_info': build_zapi_response(node_info), + 'nodes_info': build_zapi_response(nodes_info) +}) + + +SRR = rest_responses({ + 'node_info': (200, {"records": [{ + "svm": {"name": "svm1"}, + "lun": {"uuid": "ea78ec41", "name": "/vol/ansibleLUN/ansibleLUN"}, + "igroup": {"uuid": "8b8aa177", "name": "testme_igroup"}, + "reporting_nodes": [{"uuid": "20f6b3d5", "name": "ontap910-01"}] + }], "num_records": 1}, None), + 'nodes_info': (200, {"records": [{ + "svm": {"name": "svm1"}, + "lun": {"uuid": "ea78ec41", "name": "/vol/ansibleLUN/ansibleLUN"}, + "igroup": {"uuid": "8b8aa177", "name": "testme_igroup"}, + "reporting_nodes": [{"uuid": "20f6b3d5", "name": "ontap910-01"}, {"uuid": "20f6b3d6", "name": "ontap910-02"}] + }], "num_records": 1}, None) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "initiator_group_name", "vserver", "path", "nodes"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_successful_add_node(): + ''' Test successful add and idempotent check ''' + register_responses([ + ('lun-map-get-iter', ZRR['node_info']), + ('lun-map-add-reporting-nodes', ZRR['success']), + ('lun-map-get-iter', ZRR['nodes_info']), + ]) + args = {'nodes': ['ontap910-01', 'ontap910-02']} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_remove_node(): + ''' Test successful remove and idempotent check ''' + register_responses([ + ('lun-map-get-iter', ZRR['nodes_info']), + ('lun-map-remove-reporting-nodes', ZRR['success']), + ('lun-map-get-iter', ZRR['node_info']), + ]) + args = {'nodes': 'ontap910-02', 'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('lun-map-get-iter', ZRR['no_records']), + ('lun-map-get-iter', ZRR['error']), + ('lun-map-add-reporting-nodes', ZRR['error']), + ('lun-map-remove-reporting-nodes', ZRR['error']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/san/lun-maps', SRR['generic_error']), + ('POST', 'protocols/san/lun-maps/3edf6t/3edf62/reporting-nodes', SRR['generic_error']), + ('DELETE', 'protocols/san/lun-maps/3edf6t/3edf62/reporting-nodes/3dr567', SRR['generic_error']), + ('GET', 'cluster', SRR['is_rest_9_9_1']) + ]) + node_obj = create_module(my_module, DEFAULT_ARGS) + assert 'Error: LUN map not found' in expect_and_capture_ansible_exception(node_obj.apply, 'fail')['msg'] + assert 'Error getting LUN' in expect_and_capture_ansible_exception(node_obj.get_lun_map_reporting_nodes, 'fail')['msg'] + assert 'Error creating LUN map reporting nodes' in expect_and_capture_ansible_exception(node_obj.add_lun_map_reporting_nodes, 'fail', 'node1')['msg'] + assert 'Error deleting LUN map reporting node' in expect_and_capture_ansible_exception(node_obj.remove_lun_map_reporting_nodes, 'fail', 'node1')['msg'] + + node_obj = create_module(my_module, DEFAULT_ARGS, {'use_rest': 'always'}) + node_obj.lun_uuid, node_obj.igroup_uuid = '3edf6t', '3edf62' + node_obj.nodes_uuids = {'node1': '3dr567'} + assert 'Error getting LUN' in expect_and_capture_ansible_exception(node_obj.get_lun_map_reporting_nodes, 'fail')['msg'] + assert 'Error creating LUN map reporting node' in expect_and_capture_ansible_exception(node_obj.add_lun_map_reporting_nodes_rest, 'fail', 'node1')['msg'] + assert 'Error deleting LUN map reporting node' in expect_and_capture_ansible_exception(node_obj.remove_lun_map_reporting_nodes_rest, 'fail', 'node1')['msg'] + assert 'REST requires ONTAP 9.10.1 or later' in create_module(my_module, DEFAULT_ARGS, {'use_rest': 'always'}, fail=True)['msg'] + + +def test_successful_add_node_rest(): + ''' Test successful add and idempotent check ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/san/lun-maps', SRR['node_info']), + ('POST', 'protocols/san/lun-maps/ea78ec41/8b8aa177/reporting-nodes', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/san/lun-maps', SRR['nodes_info']) + ]) + args = {'nodes': ['ontap910-01', 'ontap910-02'], 'use_rest': 'always'} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_remove_node_rest(): + ''' Test successful remove and idempotent check ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/san/lun-maps', SRR['nodes_info']), + ('DELETE', 'protocols/san/lun-maps/ea78ec41/8b8aa177/reporting-nodes/20f6b3d6', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/san/lun-maps', SRR['node_info']) + ]) + args = {'nodes': 'ontap910-02', 'state': 'absent', 'use_rest': 'always'} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map_rest.py new file mode 100644 index 000000000..1881ee37a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_map_rest.py @@ -0,0 +1,200 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun_map \ + import NetAppOntapLUNMap as my_module, main as my_main # module under test + +# needed for get and modify/delete as they still use ZAPI +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +# REST API canned responses when mocking send_request + +SRR = rest_responses({ + 'lun': (200, {"records": [ + { + "uuid": "2f030603-3daa-4e19-9888-f9c3ac9a9117", + "name": "/vol/ansibleLUN_vol1/ansibleLUN", + "os_type": "linux", + "serial_number": "wOpku+Rjd-YL", + "space": { + "size": 5242880 + }, + "status": { + "state": "online" + } + }]}, None), + 'lun_map': (200, {"records": [ + { + "igroup": { + "uuid": "1ad8544d-8cd1-91e0-9e1c-723478563412", + "name": "igroup1", + }, + "logical_unit_number": 1, + "lun": { + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412", + "name": "this/is/a/path", + }, + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + } + } + ]}, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'path': 'this/is/a/path', + 'initiator_group_name': 'igroup1', + 'vserver': 'svm1', + 'use_rest': 'always', +} + + +def test_get_lun_map_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/san/lun-maps', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_lun_map_rest() is None + + +def test_get_lun_map_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/san/lun-maps', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error getting lun_map this/is/a/path: calling: protocols/san/lun-maps: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_lun_map_rest, 'fail')['msg'] + + +def test_get_lun_map_one_record(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/san/lun-maps', SRR['lun_map']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_lun_map_rest() is not None + + +def test_get_lun_one_record(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['lun']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_lun_rest() is not None + + +def test_get_lun_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error getting lun this/is/a/path: calling: storage/luns: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_lun_rest, 'fail')['msg'] + + +def test_create_lun_map(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ('GET', 'protocols/san/lun-maps', SRR['empty_records']), + ('POST', 'protocols/san/lun-maps', SRR['empty_good']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {})['changed'] + + +def test_create_lun_map_with_lun_id(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ('GET', 'protocols/san/lun-maps', SRR['empty_records']), + ('POST', 'protocols/san/lun-maps', SRR['empty_good']) + ]) + module_args = {'lun_id': '1'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_lun_map_with_lun_id_idempotent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['lun']), + ('GET', 'protocols/san/lun-maps', SRR['lun_map']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'lun_id': '1'})['changed'] is False + + +def test_create_lun_map_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'protocols/san/lun-maps', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error creating lun_map this/is/a/path: calling: protocols/san/lun-maps: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.create_lun_map_rest, 'fail')['msg'] + + +def test_delete_lun_map(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ('GET', 'protocols/san/lun-maps', SRR['lun_map']), + ('DELETE', 'protocols/san/lun-maps/1cd8a442-86d1-11e0-ae1c-123478563412/1ad8544d-8cd1-91e0-9e1c-723478563412', + SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_lun_map_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ('GET', 'protocols/san/lun-maps', SRR['lun_map']), + ]) + module_args = {'initiator_group_name': 'new name'} + msg = 'Modification of lun_map not allowed' + assert msg in create_and_apply(my_module, DEFAULT_ARGS, module_args, 'fail')['msg'] + + +def test_delete_lun_map_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('DELETE', 'protocols/san/lun-maps/1cd8a442-86d1-11e0-ae1c-123478563412/1ad8544d-8cd1-91e0-9e1c-723478563412', + SRR['generic_error']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + my_obj.parameters['state'] = 'absent' + my_obj.igroup_uuid = '1ad8544d-8cd1-91e0-9e1c-723478563412' + my_obj.lun_uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + msg = 'Error deleting lun_map this/is/a/path: calling: ' \ + 'protocols/san/lun-maps/1cd8a442-86d1-11e0-ae1c-123478563412/1ad8544d-8cd1-91e0-9e1c-723478563412: got Expected error.' + assert msg == expect_and_capture_ansible_exception(my_obj.delete_lun_map_rest, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_rest.py new file mode 100644 index 000000000..fd65062d0 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_lun_rest.py @@ -0,0 +1,558 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_lun \ + import NetAppOntapLUN as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'one_lun': (200, { + "records": [ + { + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412", + "qos_policy": { + "name": "qos1", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + "os_type": "aix", + "enabled": True, + "location": { + "volume": { + "name": "volume1", + "uuid": "028baa66-41bd-11e9-81d5-00a0986138f7" + }, + }, + "name": "/vol/volume1/qtree1/lun1", + "space": { + "scsi_thin_provisioning_support_enabled": True, + "guarantee": { + "requested": True, + }, + "size": 1073741824 + }, + "lun_maps": [ + { + "igroup": { + "name": "igroup1", + "uuid": "4ea7a442-86d1-11e0-ae1c-123478563412" + }, + "logical_unit_number": 0, + } + ], + "comment": "string", + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + } + ], + }, None), + 'two_luns': (200, { + "records": [ + { + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412", + "qos_policy": { + "name": "qos1", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + "os_type": "aix", + "enabled": True, + "location": { + "volume": { + "name": "volume1", + "uuid": "028baa66-41bd-11e9-81d5-00a0986138f7" + }, + }, + "name": "/vol/volume1/qtree1/lun1", + "space": { + "scsi_thin_provisioning_support_enabled": True, + "guarantee": { + "requested": True, + }, + "size": 1073741824 + }, + "lun_maps": [ + { + "igroup": { + "name": "igroup1", + "uuid": "4ea7a442-86d1-11e0-ae1c-123478563412" + }, + "logical_unit_number": 0, + } + ], + "comment": "string", + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + }, + { + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563413", + "qos_policy": { + "name": "qos2", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563413" + }, + "os_type": "aix", + "enabled": True, + "location": { + "volume": { + "name": "volume2", + "uuid": "028baa66-41bd-11e9-81d5-00a0986138f3" + }, + }, + "name": "/vol/volume1/qtree1/lun2", + "space": { + "scsi_thin_provisioning_support_enabled": True, + "guarantee": { + "requested": True, + }, + "size": 1073741824 + }, + "lun_maps": [ + { + "igroup": { + "name": "igroup2", + "uuid": "4ea7a442-86d1-11e0-ae1c-123478563413" + }, + "logical_unit_number": 0, + } + ], + "comment": "string", + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f3" + }, + } + ], + }, None), + 'error_same_size': (400, None, 'New LUN size is the same as the old LUN size - this mau happen ...') +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': '/vol/volume1/qtree1/lun1', + 'flexvol_name': 'volume1', + 'vserver': 'svm1', + 'use_rest': 'always', +} + +DEFAULT_ARGS_NO_VOL = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': '/vol/volume1/qtree1/lun1', + 'vserver': 'svm1', + 'use_rest': 'always', +} + +DEFAULT_ARGS_MIN = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'svm1', + 'use_rest': 'always', +} + + +def test_get_lun_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + assert my_obj.get_luns_rest() is None + + +def test_get_lun_one(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['one_lun']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + get_results = my_obj.get_luns_rest() + assert len(get_results) == 1 + assert get_results[0]['name'] == '/vol/volume1/qtree1/lun1' + + +def test_get_lun_one_no_path(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['one_lun']) + ]) + module_args = { + 'name': 'lun1', + 'flexvol_name': 'volume1', + } + my_obj = create_module(my_module, DEFAULT_ARGS_MIN, module_args) + get_results = my_obj.get_luns_rest() + assert len(get_results) == 1 + assert get_results[0]['name'] == '/vol/volume1/qtree1/lun1' + + +def test_get_lun_more(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['two_luns']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + get_results = my_obj.get_luns_rest() + assert len(get_results) == 2 + assert get_results[0]['name'] == '/vol/volume1/qtree1/lun1' + assert get_results[1]['name'] == '/vol/volume1/qtree1/lun2' + + +def test_error_get_lun_with_flexvol(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.get_luns_rest, 'fail')['msg'] + print('Info: %s' % error) + assert "Error getting LUN's for flexvol volume1: calling: storage/luns: got Expected error." == error + + +def test_error_get_lun_with_lun_path(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['lun_path'] = '/vol/volume1/qtree1/lun1' + my_obj.parameters.pop('flexvol_name') + + error = expect_and_capture_ansible_exception(my_obj.get_luns_rest, 'fail', '/vol/volume1/qtree1/lun1')['msg'] + print('Info: %s' % error) + assert "Error getting lun_path /vol/volume1/qtree1/lun1: calling: storage/luns: got Expected error." == error + + +def test_successfully_create_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ('POST', 'storage/luns', SRR['one_lun']), + ]) + module_args = { + 'size': 1073741824, + 'size_unit': 'bytes', + 'os_type': 'linux', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_create_lun_without_path(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ('POST', 'storage/luns', SRR['one_lun']), + ]) + module_args = { + 'size': 1073741824, + 'size_unit': 'bytes', + 'os_type': 'linux', + 'flexvol_name': 'volume1', + 'name': 'lun' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_lun_missing_os_type(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['size'] = 1073741824 + my_obj.parameters['size_unit'] = 'bytes' + error = expect_and_capture_ansible_exception(my_obj.apply, 'fail')['msg'] + print('Info: %s' % error) + assert "The os_type parameter is required for creating a LUN with REST." == error + + +def test_error_create_lun_missing_size(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['os_type'] = 'linux' + error = expect_and_capture_ansible_exception(my_obj.apply, 'fail')['msg'] + print('Info: %s' % error) + assert "size is a required parameter for create." == error + + +def test_error_create_lun_missing_name(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + # Not sure why test_error_create_lun_missing_os_type require this... but this test dosn't. they should follow the + # same path (unless we don't do a get with flexvol_name isn't set) + # ('GET', 'storage/luns', SRR['empty_records']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters.pop('flexvol_name') + my_obj.parameters['os_type'] = 'linux' + my_obj.parameters['size'] = 1073741824 + my_obj.parameters['size_unit'] = 'bytes' + error = expect_and_capture_ansible_exception(my_obj.apply, 'fail')['msg'] + print('Info: %s' % error) + assert "The flexvol_name parameter is required for creating a LUN." == error + + +def test_successfully_create_lun_all_options(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/luns', SRR['empty_records']), + ('POST', 'storage/luns', SRR['one_lun']), + ]) + module_args = { + 'size': '1073741824', + 'os_type': 'linux', + 'space_reserve': True, + 'space_allocation': True, + 'comment': 'carchi8py was here', + 'qos_policy_group': 'qos_policy_group_1', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'storage/luns', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['size'] = 1073741824 + my_obj.parameters['size_unit'] = 'bytes' + my_obj.parameters['os_type'] = 'linux' + + error = expect_and_capture_ansible_exception(my_obj.create_lun_rest, 'fail')['msg'] + print('Info: %s' % error) + assert "Error creating LUN /vol/volume1/qtree1/lun1: calling: storage/luns: got Expected error." == error + + +def test_successfully_delete_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['one_lun']), + ('DELETE', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_records']), + ]) + module_args = { + 'size': 1073741824, + 'size_unit': 'bytes', + 'os_type': 'linux', + 'state': 'absent', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_delete_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('DELETE', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['size'] = 1073741824 + my_obj.parameters['size_unit'] = 'bytes' + my_obj.parameters['os_type'] = 'linux' + my_obj.parameters['os_type'] = 'absent' + my_obj.uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + + error = expect_and_capture_ansible_exception(my_obj.delete_lun_rest, 'fail')['msg'] + print('Info: %s' % error) + assert "Error deleting LUN /vol/volume1/qtree1/lun1: calling: storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412: got Expected error." == error + + +def test_error_delete_lun_missing_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['size'] = 1073741824 + my_obj.parameters['size_unit'] = 'bytes' + my_obj.parameters['os_type'] = 'linux' + my_obj.parameters['os_type'] = 'absent' + + error = expect_and_capture_ansible_exception(my_obj.delete_lun_rest, 'fail')['msg'] + print('Info: %s' % error) + assert "Error deleting LUN /vol/volume1/qtree1/lun1: UUID not found" == error + + +def test_successfully_rename_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['empty_records']), + ('GET', 'storage/luns', SRR['one_lun']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_records']), + ]) + module_args = { + 'name': '/vol/volume1/qtree12/lun1', + 'from_name': '/vol/volume1/qtree1/lun1', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_rename_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['name'] = '/vol/volume1/qtree12/lun1' + my_obj.parameters['from_name'] = '/vol/volume1/qtree1/lun1' + my_obj.uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + error = expect_and_capture_ansible_exception(my_obj.rename_lun_rest, 'fail', '/vol/volume1/qtree12/lun1')['msg'] + print('Info: %s' % error) + assert "Error renaming LUN /vol/volume1/qtree12/lun1: calling: storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412: got Expected error." == error + + +def test_error_rename_lun_missing_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['name'] = '/vol/volume1/qtree12/lun1' + my_obj.parameters['from_name'] = '/vol/volume1/qtree1/lun1' + error = expect_and_capture_ansible_exception(my_obj.rename_lun_rest, 'fail', '/vol/volume1/qtree12/lun1')['msg'] + print('Info: %s' % error) + assert "Error renaming LUN /vol/volume1/qtree12/lun1: UUID not found" == error + + +def test_successfully_resize_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['one_lun']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_records']), + ]) + module_args = { + 'size': 2147483648, + 'size_unit': 'bytes', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_resize_lun(): + ''' assert that + resize fails on error, except for a same size issue because of rounding errors + resize correctly return True/False to indicate that the size was changed or not + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['generic_error']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['error_same_size']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['size'] = 2147483648 + my_obj.parameters['size_unit'] = 'bytes' + my_obj.uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + error = expect_and_capture_ansible_exception(my_obj.resize_lun_rest, 'fail')['msg'] + print('Info: %s' % error) + assert "Error resizing LUN /vol/volume1/qtree1/lun1: calling: storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412: got Expected error." == error + assert not my_obj.resize_lun_rest() + assert my_obj.resize_lun_rest() + + +def test_error_resize_lun_missing_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['size'] = 2147483648 + my_obj.parameters['size_unit'] = 'bytes' + error = expect_and_capture_ansible_exception(my_obj.resize_lun_rest, 'fail')['msg'] + print('Info: %s' % error) + assert "Error resizing LUN /vol/volume1/qtree1/lun1: UUID not found" == error + + +def test_successfully_modify_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/luns', SRR['one_lun']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_records']), + ]) + module_args = { + 'comment': 'carchi8py was here', + 'qos_policy_group': 'qos_policy_group_12', + 'space_reserve': False, + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_modify_lun_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/luns', SRR['one_lun']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_records']), + ]) + module_args = { + 'comment': 'carchi8py was here', + 'qos_policy_group': 'qos_policy_group_12', + 'space_allocation': False, + 'space_reserve': False, + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify_lun(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'carchi8py was here' + my_obj.parameters['qos_policy_group'] = 'qos_policy_group_12' + my_obj.parameters['space_allocation'] = False + my_obj.parameters['space_reserve'] = False + my_obj.uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + modify = {'comment': 'carchi8py was here', 'qos_policy_group': 'qos_policy_group_12', 'space_reserve': False, 'space_allocation': False} + error = expect_and_capture_ansible_exception(my_obj.modify_lun_rest, 'fail', modify)['msg'] + print('Info: %s' % error) + assert "Error modifying LUN /vol/volume1/qtree1/lun1: calling: storage/luns/1cd8a442-86d1-11e0-ae1c-123478563412: got Expected error." == error + + +def test_error_modify_lun_missing_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'carchi8py was here' + my_obj.parameters['qos_policy_group'] = 'qos_policy_group_12' + my_obj.parameters['space_allocation'] = False + my_obj.parameters['space_reserve'] = False + modify = {'comment': 'carchi8py was here', 'qos_policy_group': 'qos_policy_group_12', 'space_reserve': False, 'space_allocation': False} + error = expect_and_capture_ansible_exception(my_obj.modify_lun_rest, 'fail', modify)['msg'] + print('Info: %s' % error) + assert "Error modifying LUN /vol/volume1/qtree1/lun1: UUID not found" == error + + +def test_error_modify_lun_extra_option(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'carchi8py was here' + my_obj.parameters['qos_policy_group'] = 'qos_policy_group_12' + my_obj.parameters['space_allocation'] = False + my_obj.parameters['space_reserve'] = False + my_obj.uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + modify = {'comment': 'carchi8py was here', 'qos_policy_group': 'qos_policy_group_12', 'space_reserve': False, 'space_allocation': False, 'fake': 'fake'} + error = expect_and_capture_ansible_exception(my_obj.modify_lun_rest, 'fail', modify)['msg'] + print('Info: %s' % error) + assert "Error modifying LUN /vol/volume1/qtree1/lun1: Unknown parameters: {'fake': 'fake'}" == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_mcc_mediator.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_mcc_mediator.py new file mode 100644 index 000000000..0259edf03 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_mcc_mediator.py @@ -0,0 +1,124 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_metrocluster ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_mcc_mediator \ + import NetAppOntapMccipMediator as mediator_module # module under test + +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_mediator_with_no_results': (200, {'num_records': 0}, None), + 'get_mediator_with_results': (200, { + 'num_records': 1, + 'records': [{ + 'ip_address': '10.10.10.10', + 'uuid': 'ebe27c49-1adf-4496-8335-ab862aebebf2' + }] + }, None) +} + + +class TestMyModule(unittest.TestCase): + """ Unit tests for na_ontap_metrocluster """ + + def setUp(self): + self.mock_mediator = { + 'mediator_address': '10.10.10.10', + 'mediator_user': 'carchi', + 'mediator_password': 'netapp1!' + } + + def mock_args(self): + return { + 'mediator_address': self.mock_mediator['mediator_address'], + 'mediator_user': self.mock_mediator['mediator_user'], + 'mediator_password': self.mock_mediator['mediator_password'], + 'hostname': 'test_host', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_alias_mock_object(self): + alias_obj = mediator_module() + return alias_obj + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_create(self, mock_request): + """Test successful rest create""" + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_mediator_with_no_results'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_create_idempotency(self, mock_request): + """Test successful rest create""" + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_mediator_with_results'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_delete(self, mock_request): + """Test successful rest create""" + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_mediator_with_results'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_delete(self, mock_request): + """Test successful rest create""" + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_mediator_with_no_results'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_metrocluster.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_metrocluster.py new file mode 100644 index 000000000..5ccc3eb95 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_metrocluster.py @@ -0,0 +1,117 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_metrocluster ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_metrocluster \ + import NetAppONTAPMetroCluster as metrocluster_module # module under test + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_metrocluster_with_results': (200, {"local": { + "cluster": { + 'name': 'cluster1' + }, + "configuration_state": "configuration_error", # TODO: put correct state + "partner_cluster_reachable": "true", + }}, None), + 'get_metrocluster_with_no_results': (200, None, None), + 'metrocluster_post': (200, {'job': { + 'uuid': 'fde79888-692a-11ea-80c2-005056b39fe7', + '_links': { + 'self': { + 'href': '/api/cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7'}}} + }, None), + 'job': (200, { + "uuid": "cca3d070-58c6-11ea-8c0c-005056826c14", + "description": "POST /api/cluster/metrocluster", + "state": "success", + "message": "There are not enough disks in Pool1.", + "code": 2432836, + "start_time": "2020-02-26T10:35:44-08:00", + "end_time": "2020-02-26T10:47:38-08:00", + "_links": { + "self": { + "href": "/api/cluster/jobs/cca3d070-58c6-11ea-8c0c-005056826c14" + } + } + }, None) +} + + +class TestMyModule(unittest.TestCase): + """ Unit tests for na_ontap_metrocluster """ + + def setUp(self): + self.mock_metrocluster = { + 'partner_cluster_name': 'cluster1', + 'node_name': 'carchi_vsim1', + 'partner_node_name': 'carchi_vsim3' + } + + def mock_args(self): + return { + 'dr_pairs': [{ + 'node_name': self.mock_metrocluster['node_name'], + 'partner_node_name': self.mock_metrocluster['partner_node_name'], + }], + 'partner_cluster_name': self.mock_metrocluster['partner_cluster_name'], + 'hostname': 'test_host', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_alias_mock_object(self): + alias_obj = metrocluster_module() + return alias_obj + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_create(self, mock_request): + """Test successful rest create""" + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_metrocluster_with_no_results'], + SRR['metrocluster_post'], + SRR['job'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_create_idempotency(self, mock_request): + """Test rest create idempotency""" + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_metrocluster_with_results'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_metrocluster_dr_group.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_metrocluster_dr_group.py new file mode 100644 index 000000000..2bcc558aa --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_metrocluster_dr_group.py @@ -0,0 +1,164 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_metrocluster ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_metrocluster_dr_group \ + import NetAppONTAPMetroClusterDRGroup as mcc_dr_pairs_module # module under test + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_mcc_dr_pair_with_no_results': (200, {'records': [], 'num_records': 0}, None), + 'get_mcc_dr_pair_with_results': (200, {'records': [{'partner_cluster': {'name': 'rha2-b2b1_siteB'}, + 'dr_pairs': [{'node': {'name': 'rha17-a2'}, + 'partner': {'name': 'rha17-b2'}}, + {'node': {'name': 'rha17-b2'}, + 'partner': {'name': 'rha17-b1'}}], + 'id': '2'}], + 'num_records': 1}, None), + 'mcc_dr_pair_post': (200, {'job': { + 'uuid': 'fde79888-692a-11ea-80c2-005056b39fe7', + '_links': { + 'self': { + 'href': '/api/cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7'}}} + }, None), + 'get_mcc_dr_node': (200, {'records': [{'dr_group_id': '1'}], 'num_records': 1}, None), + 'get_mcc_dr_node_none': (200, {'records': [], 'num_records': 0}, None), + 'job': (200, { + "uuid": "cca3d070-58c6-11ea-8c0c-005056826c14", + "description": "POST /api/cluster/metrocluster", + "state": "success", + "message": "There are not enough disks in Pool1.", + "code": 2432836, + "start_time": "2020-02-26T10:35:44-08:00", + "end_time": "2020-02-26T10:47:38-08:00", + "_links": { + "self": { + "href": "/api/cluster/jobs/cca3d070-58c6-11ea-8c0c-005056826c14" + } + } + }, None) +} + + +class TestMyModule(unittest.TestCase): + """ Unit tests for na_ontap_metrocluster """ + + def setUp(self): + self.mock_mcc_dr_pair = { + 'partner_cluster_name': 'rha2-b2b1_siteB', + 'node_name': 'rha17-a2', + 'partner_node_name': 'rha17-b2', + 'node_name2': 'rha17-b2', + 'partner_node_name2': 'rha17-b1' + + } + + def mock_args(self): + return { + 'dr_pairs': [{ + 'node_name': self.mock_mcc_dr_pair['node_name'], + 'partner_node_name': self.mock_mcc_dr_pair['partner_node_name'], + }, { + 'node_name': self.mock_mcc_dr_pair['node_name2'], + 'partner_node_name': self.mock_mcc_dr_pair['partner_node_name2'], + }], + 'partner_cluster_name': self.mock_mcc_dr_pair['partner_cluster_name'], + 'hostname': 'test_host', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_alias_mock_object(self): + alias_obj = mcc_dr_pairs_module() + return alias_obj + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_create(self, mock_request): + """Test successful rest create""" + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_mcc_dr_pair_with_no_results'], + SRR['get_mcc_dr_pair_with_no_results'], + SRR['mcc_dr_pair_post'], + SRR['job'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_create_idempotency(self, mock_request): + """Test rest create idempotency""" + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_mcc_dr_pair_with_results'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_delete(self, mock_request): + """Test successful rest delete""" + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_mcc_dr_pair_with_results'], + SRR['mcc_dr_pair_post'], + SRR['job'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_delete_idempotency(self, mock_request): + """Test rest delete idempotency""" + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_mcc_dr_pair_with_no_results'], + SRR['get_mcc_dr_pair_with_no_results'], + SRR['get_mcc_dr_node_none'], + SRR['get_mcc_dr_node_none'], + SRR['job'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_motd.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_motd.py new file mode 100644 index 000000000..64626e5ec --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_motd.py @@ -0,0 +1,164 @@ +# (c) 2019-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_motd """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_warning_was_raised, expect_and_capture_ansible_exception, call_main, create_module, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_motd import NetAppONTAPMotd as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def motd_info(msg): + return { + 'num-records': 1, + 'attributes-list': { + 'vserver-motd-info': { + 'message': msg, + 'vserver': 'ansible', + 'is-cluster-message-enabled': 'true'}} + } + + +ZRR = zapi_responses({ + 'motd_info': build_zapi_response(motd_info('motd_message')), + 'motd_none': build_zapi_response(motd_info('None')), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'use_rest', + 'motd_message': 'motd_message', + 'vserver': 'ansible', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + } + print('Info: %s' % call_main(my_main, module_args, fail=True)['msg']) + + +def test_ensure_motd_get_called(): + ''' fetching details of motd ''' + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.motd_get() is None + + +def test_ensure_get_called_existing(): + ''' test for existing motd''' + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.motd_get() + + +def test_motd_create(): + ''' test for creating motd''' + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['success']), + # idempotency + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + # modify + ('ZAPI', 'vserver-motd-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['message'] = 'new_message' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_motd_delete(): + ''' test for deleting motd''' + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_info']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['motd_info']), + ('ZAPI', 'vserver-motd-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-motd-get-iter', ZRR['motd_none']), + ]) + module_args = { + 'state': 'absent', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['error']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert expect_and_capture_ansible_exception(my_obj.motd_get, 'fail')['msg'] == zapi_error_message('Error fetching motd info') + assert expect_and_capture_ansible_exception(my_obj.modify_motd, 'fail')['msg'] == zapi_error_message('Error creating motd') + + +def test_rest_required(): + module_args = { + 'use_rest': 'always', + } + error_msg = 'netapp.ontap.na_ontap_motd is deprecated and only supports ZAPI. Please use netapp.ontap.na_ontap_login_messages.' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == 'Error: %s' % error_msg + register_responses([ + ('ZAPI', 'vserver-motd-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['success']), + ('ZAPI', 'vserver-motd-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-motd-modify-iter', ZRR['success']), + ]) + module_args = { + 'use_rest': 'auto', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_warning_was_raised('Falling back to ZAPI: %s' % error_msg) + module_args = { + 'use_rest': 'NevEr', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_warning_was_raised(error_msg) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + module_args = { + 'use_rest': 'never', + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_name_mappings.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_name_mappings.py new file mode 100644 index 000000000..5294a9537 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_name_mappings.py @@ -0,0 +1,282 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_name_mappings """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_name_mappings \ + import NetAppOntapNameMappings as my_module # module under test + + +# REST API canned responses when mocking send_request +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'mapping_record': ( + 200, + { + "records": [ + { + "client_match": "10.254.101.111/28", + "direction": "win_unix", + "index": 1, + "pattern": "ENGCIFS_AD_USER", + "replacement": "unix_user1", + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + } + } + ], + "num_records": 1 + }, None + ), + 'mapping_record1': ( + 200, + { + "records": [ + { + "direction": "win_unix", + "index": 2, + "pattern": "ENGCIFS_AD_USERS", + "replacement": "unix_user", + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + } + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'vserver': 'svm1', + 'direction': 'win_unix', + 'index': '1' +} + + +def test_get_name_mappings_rest(): + ''' Test retrieving name mapping record ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['mapping_record']), + ]) + name_obj = create_module(my_module, DEFAULT_ARGS) + result = name_obj.get_name_mappings_rest() + assert result + + +def test_error_get_name_mappings_rest(): + ''' Test error retrieving name mapping record ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['generic_error']), + ]) + error = create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] + msg = "calling: name-services/name-mappings: got Expected error." + assert msg in error + + +def test_error_direction_s3_choices(): + ''' Test error when set s3 choices in older version ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + error = create_module(my_module, DEFAULT_ARGS, {'direction': 's3_unix'}, fail=True)['msg'] + msg = "Error: direction s3_unix requires ONTAP 9.12.1 or later" + assert msg in error + + +def test_create_name_mappings_rest(): + ''' Test create name mapping record ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['empty_records']), + ('POST', 'name-services/name-mappings', SRR['empty_good']), + ]) + module_args = { + "pattern": "ENGCIFS_AD_USER", + "replacement": "unix_user1", + "client_match": "10.254.101.111/28", + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_name_mappings_rest(): + ''' Test error create name mapping record ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['empty_records']), + ('POST', 'name-services/name-mappings', SRR['generic_error']), + ]) + module_args = { + "pattern": "ENGCIFS_AD_USER", + "replacement": "unix_user1", + "client_match": "10.254.101.111/28", + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error on creating name mappings rest:" + assert msg in error + + +def test_delete_name_mappings_rest(): + ''' Test delete name mapping record ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['mapping_record']), + ('DELETE', 'name-services/name-mappings/02c9e252-41be-11e9-81d5-00a0986138f7/win_unix/1', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_name_mappings_rest_error(): + ''' Test error delete name mapping record ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['mapping_record']), + ('DELETE', 'name-services/name-mappings/02c9e252-41be-11e9-81d5-00a0986138f7/win_unix/1', SRR['generic_error']), + ]) + module_args = { + 'state': 'absent' + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error on deleting name mappings rest:" + assert msg in error + + +def test_create_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['mapping_record']) + ]) + module_args = { + "pattern": "ENGCIFS_AD_USER", + "replacement": "unix_user1", + "client_match": "10.254.101.111/28", + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['empty_records']) + ]) + module_args = { + 'state': 'absent' + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_name_mappings_pattern_rest(): + ''' Test modify name mapping pattern ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['mapping_record']), + ('PATCH', 'name-services/name-mappings/02c9e252-41be-11e9-81d5-00a0986138f7/win_unix/1', SRR['empty_good']), + ]) + module_args = { + "pattern": "ENGCIFS_AD_USERS", + "replacement": "unix_user2", + "client_match": "10.254.101.112/28", + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_name_mappings_replacement_rest(): + ''' Test modify name mapping replacement ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['mapping_record1']), + ('PATCH', 'name-services/name-mappings/02c9e252-41be-11e9-81d5-00a0986138f7/win_unix/1', SRR['empty_good']), + ]) + module_args = { + "replacement": "unix_user2" + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_name_mappings_client_match_rest(): + ''' Test modify name mapping client match ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['mapping_record']), + ('PATCH', 'name-services/name-mappings/02c9e252-41be-11e9-81d5-00a0986138f7/win_unix/1', SRR['empty_good']), + ]) + module_args = { + "client_match": "10.254.101.112/28", + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify_name_mappings_rest(): + ''' Test error modify name mapping ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['mapping_record']), + ('PATCH', 'name-services/name-mappings/02c9e252-41be-11e9-81d5-00a0986138f7/win_unix/1', SRR['generic_error']), + ]) + module_args = { + "pattern": "ENGCIFS_AD_USERS", + "replacement": "unix_user2", + "client_match": "10.254.101.112/28", + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error on modifying name mappings rest:" + assert msg in error + + +def test_swap_name_mappings_new_index_rest(): + ''' Test swap name mapping positions ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/name-mappings', SRR['empty_records']), + ('GET', 'name-services/name-mappings', SRR['mapping_record1']), + ('PATCH', 'name-services/name-mappings/02c9e252-41be-11e9-81d5-00a0986138f7/win_unix/2', SRR['empty_good']), + ]) + module_args = { + "index": "1", + "from_index": "2" + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_parameters_for_create_name_mappings_rest(): + ''' Validate parameters for create name mapping record ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'name-services/name-mappings', SRR['empty_records']), + ]) + module_args = { + "client_match": "10.254.101.111/28", + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error creating name mappings for an SVM, pattern and replacement are required in create." + assert msg in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_name_service_switch.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_name_service_switch.py new file mode 100644 index 000000000..3b91e9be7 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_name_service_switch.py @@ -0,0 +1,181 @@ +# (c) 2019-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_name_service_switch ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import create_module,\ + patch_ansible, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_name_service_switch \ + import NetAppONTAPNsswitch as nss_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'https': 'True', + 'use_rest': 'never', + 'state': 'present', + 'vserver': 'test_vserver', + 'database_type': 'namemap', + 'sources': 'files,ldap', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' +} + + +nss_info = { + 'num-records': 1, + 'attributes-list': { + 'namservice-nsswitch-config-info': { + 'nameservice-database': 'namemap', + 'nameservice-sources': {'nss-source-type': 'files,ldap'} + } + } +} + + +ZRR = zapi_responses({ + 'nss_info': build_zapi_response(nss_info) +}) + + +def test_module_fail_when_required_args_missing(): + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "vserver", "database_type"] + error = create_module(nss_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_get_nonexistent_nss(): + register_responses([ + ('nameservice-nsswitch-get-iter', ZRR['no_records']) + ]) + nss_obj = create_module(nss_module, DEFAULT_ARGS) + assert nss_obj.get_name_service_switch() is None + + +def test_get_existing_nss(): + register_responses([ + ('nameservice-nsswitch-get-iter', ZRR['nss_info']) + ]) + nss_obj = create_module(nss_module, DEFAULT_ARGS) + assert nss_obj.get_name_service_switch() + + +def test_successfully_create(): + register_responses([ + ('nameservice-nsswitch-get-iter', ZRR['no_records']), + ('nameservice-nsswitch-create', ZRR['success']) + ]) + assert create_and_apply(nss_module, DEFAULT_ARGS)['changed'] + + +def test_successfully_modify(): + register_responses([ + ('nameservice-nsswitch-get-iter', ZRR['nss_info']), + ('nameservice-nsswitch-modify', ZRR['success']) + ]) + assert create_and_apply(nss_module, DEFAULT_ARGS, {'sources': 'files'})['changed'] + + +def test_successfully_delete(): + register_responses([ + ('nameservice-nsswitch-get-iter', ZRR['nss_info']), + ('nameservice-nsswitch-destroy', ZRR['success']) + ]) + assert create_and_apply(nss_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_if_all_methods_catch_exception_zapi(): + ''' test error zapi - get/create/modify/delete''' + register_responses([ + ('nameservice-nsswitch-get-iter', ZRR['error']), + ('nameservice-nsswitch-create', ZRR['error']), + ('nameservice-nsswitch-modify', ZRR['error']), + ('nameservice-nsswitch-destroy', ZRR['error']) + ]) + nss_obj = create_module(nss_module, DEFAULT_ARGS) + + assert 'Error fetching name service switch' in expect_and_capture_ansible_exception(nss_obj.get_name_service_switch, 'fail')['msg'] + assert 'Error on creating name service switch' in expect_and_capture_ansible_exception(nss_obj.create_name_service_switch, 'fail')['msg'] + assert 'Error on modifying name service switch' in expect_and_capture_ansible_exception(nss_obj.modify_name_service_switch, 'fail', {})['msg'] + assert 'Error on deleting name service switch' in expect_and_capture_ansible_exception(nss_obj.delete_name_service_switch, 'fail')['msg'] + + +SRR = rest_responses({ + 'nss_info': (200, {"records": [ + { + 'nsswitch': { + 'group': ['files'], + 'hosts': ['files', 'dns'], + 'namemap': ['files'], + 'netgroup': ['files'], + 'passwd': ['files'] + }, + 'uuid': '6647fa13'} + ], 'num_records': 1}, None), + 'nss_info_no_record': (200, {"records": [ + {'uuid': '6647fa13'} + ], 'num_records': 1}, None), + 'svm_uuid': (200, {"records": [ + {'uuid': '6647fa13'} + ], "num_records": 1}, None) +}) + + +def test_successfully_modify_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['nss_info_no_record']), + ('PATCH', 'svm/svms/6647fa13', SRR['success']), + ]) + args = {'sources': 'files', 'use_rest': 'always'} + assert create_and_apply(nss_module, DEFAULT_ARGS, args)['changed'] + + +def test_error_get_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['zero_records']) + ]) + error = "Error: Specified vserver test_vserver not found" + assert error in create_and_apply(nss_module, DEFAULT_ARGS, {'use_rest': 'always'}, fail=True)['msg'] + + +def test_error_delete_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['nss_info']) + ]) + args = {'state': 'absent', 'use_rest': 'always'} + error = "Error: deleting name service switch not supported in REST." + assert error in create_and_apply(nss_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_if_all_methods_catch_exception_rest(): + ''' test error rest - get/modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['generic_error']), + ('PATCH', 'svm/svms/6647fa13', SRR['generic_error']), + ]) + nss_obj = create_module(nss_module, DEFAULT_ARGS, {'use_rest': 'always'}) + nss_obj.svm_uuid = '6647fa13' + assert 'Error fetching name service switch' in expect_and_capture_ansible_exception(nss_obj.get_name_service_switch, 'fail')['msg'] + assert 'Error on modifying name service switch' in expect_and_capture_ansible_exception(nss_obj.modify_name_service_switch_rest, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ndmp.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ndmp.py new file mode 100644 index 000000000..78278bc7b --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ndmp.py @@ -0,0 +1,196 @@ +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ndmp \ + import NetAppONTAPNdmp as ndmp_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'get_uuid': (200, {'records': [{'uuid': 'testuuid'}]}, None), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, 'Error fetching ndmp from ansible: NetApp API failed. Reason - Unexpected error:', + "REST API currently does not support 'backup_log_enable, ignore_ctime_enabled'"), + 'get_ndmp_uuid': (200, {"records": [{"svm": {"name": "svm1", "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7"}}]}, None), + 'get_ndmp': (200, {"enabled": True, "authentication_types": ["test"], + "records": [{"svm": {"name": "svm1", "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7"}}]}, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.type = kind + self.data = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'ndmp': + xml = self.build_ndmp_info(self.data) + if self.type == 'error': + error = netapp_utils.zapi.NaApiError('test', 'error') + raise error + self.xml_out = xml + return xml + + @staticmethod + def build_ndmp_info(ndmp_details): + ''' build xml data for ndmp ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'ndmp-vserver-attributes-info': { + 'ignore_ctime_enabled': ndmp_details['ignore_ctime_enabled'], + 'backup_log_enable': ndmp_details['backup_log_enable'], + + 'authtype': [ + {'ndmpd-authtypes': 'plaintext'}, + {'ndmpd-authtypes': 'challenge'} + ] + } + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_ndmp = { + 'ignore_ctime_enabled': True, + 'backup_log_enable': 'false', + 'authtype': 'plaintext', + 'enable': True + } + + def mock_args(self, rest=False): + if rest: + return { + 'authtype': self.mock_ndmp['authtype'], + 'enable': True, + 'vserver': 'ansible', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': 'False' + } + else: + return { + 'vserver': 'ansible', + 'authtype': self.mock_ndmp['authtype'], + 'ignore_ctime_enabled': self.mock_ndmp['ignore_ctime_enabled'], + 'backup_log_enable': self.mock_ndmp['backup_log_enable'], + 'enable': self.mock_ndmp['enable'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' + } + + def get_ndmp_mock_object(self, kind=None, cx_type='zapi'): + """ + Helper method to return an na_ontap_ndmp object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_ndmp object + """ + obj = ndmp_module() + if cx_type == 'zapi': + obj.asup_log_for_cserver = Mock(return_value=None) + obj.server = Mock() + obj.server.invoke_successfully = Mock() + if kind is None: + obj.server = MockONTAPConnection() + else: + obj.server = MockONTAPConnection(kind=kind, data=self.mock_ndmp) + return obj + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ndmp.NetAppONTAPNdmp.ndmp_get_iter') + def test_successful_modify(self, ger_ndmp): + ''' Test successful modify ndmp''' + data = self.mock_args() + set_module_args(data) + current = { + 'ignore_ctime_enabled': False, + 'backup_log_enable': True + } + ger_ndmp.side_effect = [ + current + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_ndmp_mock_object('ndmp').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ndmp.NetAppONTAPNdmp.ndmp_get_iter') + def test_modify_error(self, ger_ndmp): + ''' Test modify error ''' + data = self.mock_args() + set_module_args(data) + current = { + 'ignore_ctime_enabled': False, + 'backup_log_enable': True + } + ger_ndmp.side_effect = [ + current + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_ndmp_mock_object('error').apply() + assert exc.value.args[0]['msg'] == 'Error modifying ndmp on ansible: NetApp API failed. Reason - test:error' + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.mock_args() + data['use_rest'] = 'Always' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_ndmp_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['msg'] == SRR['generic_error'][3] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successfully_modify(self, mock_request): + data = self.mock_args(rest=True) + data['use_rest'] = 'Always' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], # Was not called because of Always, but we now call it :) + SRR['get_ndmp_uuid'], # for get svm uuid: protocols/ndmp/svms + SRR['get_ndmp'], # for get ndmp details: '/protocols/ndmp/svms/' + uuid + SRR['get_ndmp_uuid'], # for get svm uuid: protocols/ndmp/svms (before modify) + SRR['empty_good'], # modify (patch) + SRR['end_of_sequence'], + ] + my_obj = ndmp_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_ifgrp.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_ifgrp.py new file mode 100644 index 000000000..7e3e58783 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_ifgrp.py @@ -0,0 +1,737 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings, set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp \ + import NetAppOntapIfGrp as ifgrp_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'ifgrp': + xml = self.build_ifgrp_info(self.params) + elif self.kind == 'ifgrp-ports': + xml = self.build_ifgrp_ports_info(self.params) + elif self.kind == 'ifgrp-fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_ifgrp_info(ifgrp_details): + ''' build xml data for ifgrp-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'net-port-info': { + 'port': ifgrp_details['name'], + 'ifgrp-distribution-function': 'mac', + 'ifgrp-mode': ifgrp_details['mode'], + 'node': ifgrp_details['node'] + } + } + } + xml.translate_struct(attributes) + return xml + + @staticmethod + def build_ifgrp_ports_info(data): + ''' build xml data for ifgrp-ports ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'attributes': { + 'net-ifgrp-info': { + 'ports': [ + {'lif-bindable': data['ports'][0]}, + {'lif-bindable': data['ports'][1]}, + {'lif-bindable': data['ports'][2]} + ] + } + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_ifgrp = { + 'name': 'test', + 'port': 'a1', + 'node': 'test_vserver', + 'mode': 'something' + } + + def mock_args(self): + return { + 'name': self.mock_ifgrp['name'], + 'distribution_function': 'mac', + 'ports': [self.mock_ifgrp['port']], + 'node': self.mock_ifgrp['node'], + 'mode': self.mock_ifgrp['mode'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'feature_flags': {'no_cserver_ems': True}, + 'use_rest': 'never' + } + + def get_ifgrp_mock_object(self, kind=None, data=None): + """ + Helper method to return an na_ontap_net_ifgrp object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_net_ifgrp object + """ + obj = ifgrp_module() + obj.autosupport_log = Mock(return_value=None) + if data is None: + data = self.mock_ifgrp + obj.server = MockONTAPConnection(kind=kind, data=data) + return obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + ifgrp_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_ifgrp(self): + ''' Test if get_ifgrp returns None for non-existent ifgrp ''' + set_module_args(self.mock_args()) + result = self.get_ifgrp_mock_object().get_if_grp() + assert result is None + + def test_get_existing_ifgrp(self): + ''' Test if get_ifgrp returns details for existing ifgrp ''' + set_module_args(self.mock_args()) + result = self.get_ifgrp_mock_object('ifgrp').get_if_grp() + assert result['name'] == self.mock_ifgrp['name'] + + def test_successful_create(self): + ''' Test successful create ''' + data = self.mock_args() + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_ifgrp_mock_object().apply() + assert exc.value.args[0]['changed'] + + def test_successful_delete(self): + ''' Test delete existing volume ''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_ifgrp_mock_object('ifgrp').apply() + assert exc.value.args[0]['changed'] + + def test_successful_modify(self): + ''' Test delete existing volume ''' + data = self.mock_args() + data['ports'] = ['1', '2', '3'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_ifgrp_mock_object('ifgrp').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.get_if_grp') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.create_if_grp') + def test_create_called(self, create_ifgrp, get_ifgrp): + data = self.mock_args() + set_module_args(data) + get_ifgrp.return_value = None + with pytest.raises(AnsibleExitJson) as exc: + self.get_ifgrp_mock_object().apply() + get_ifgrp.assert_called_with() + create_ifgrp.assert_called_with() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.add_port_to_if_grp') + def test_if_ports_are_added_after_create(self, add_ports): + ''' Test successful create ''' + data = self.mock_args() + set_module_args(data) + self.get_ifgrp_mock_object().create_if_grp() + add_ports.assert_called_with('a1') + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.get_if_grp') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.delete_if_grp') + def test_delete_called(self, delete_ifgrp, get_ifgrp): + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + get_ifgrp.return_value = Mock() + with pytest.raises(AnsibleExitJson) as exc: + self.get_ifgrp_mock_object().apply() + get_ifgrp.assert_called_with() + delete_ifgrp.assert_called_with(None) + + def test_get_return_value(self): + data = self.mock_args() + set_module_args(data) + result = self.get_ifgrp_mock_object('ifgrp').get_if_grp() + assert result['name'] == data['name'] + assert result['mode'] == data['mode'] + assert result['node'] == data['node'] + + def test_get_ports_list(self): + data = self.mock_args() + data['ports'] = ['e0a', 'e0b', 'e0c'] + set_module_args(data) + result = self.get_ifgrp_mock_object('ifgrp-ports', data).get_if_grp_ports() + assert result['ports'] == data['ports'] + + def test_add_port_packet(self): + data = self.mock_args() + set_module_args(data) + obj = self.get_ifgrp_mock_object('ifgrp') + obj.add_port_to_if_grp('addme') + assert obj.server.xml_in['port'] == 'addme' + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.remove_port_to_if_grp') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.add_port_to_if_grp') + def test_modify_ports_calls_remove_existing_ports(self, add_port, remove_port): + ''' Test if already existing ports are not being added again ''' + data = self.mock_args() + data['ports'] = ['1', '2'] + set_module_args(data) + self.get_ifgrp_mock_object('ifgrp').modify_ports(current_ports=['1', '2', '3']) + assert remove_port.call_count == 1 + assert add_port.call_count == 0 + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.remove_port_to_if_grp') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_ifgrp.NetAppOntapIfGrp.add_port_to_if_grp') + def test_modify_ports_calls_add_new_ports(self, add_port, remove_port): + ''' Test new ports are added ''' + data = self.mock_args() + data['ports'] = ['1', '2', '3', '4'] + set_module_args(data) + self.get_ifgrp_mock_object('ifgrp').modify_ports(current_ports=['1', '2']) + assert remove_port.call_count == 0 + assert add_port.call_count == 2 + + def test_get_ports_returns_none(self): + set_module_args(self.mock_args()) + result = self.get_ifgrp_mock_object().get_if_grp_ports() + assert result['ports'] == [] + result = self.get_ifgrp_mock_object().get_if_grp() + assert result is None + + def test_if_all_methods_catch_exception(self): + set_module_args(self.mock_args()) + with pytest.raises(AnsibleFailJson) as exc: + self.get_ifgrp_mock_object('ifgrp-fail').get_if_grp() + assert 'Error getting if_group test' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + self.get_ifgrp_mock_object('ifgrp-fail').create_if_grp() + assert 'Error creating if_group test' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + self.get_ifgrp_mock_object('ifgrp-fail').get_if_grp_ports() + assert 'Error getting if_group ports test' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + self.get_ifgrp_mock_object('ifgrp-fail').add_port_to_if_grp('test-port') + assert 'Error adding port test-port to if_group test' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + self.get_ifgrp_mock_object('ifgrp-fail').remove_port_to_if_grp('test-port') + assert 'Error removing port test-port to if_group test' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + self.get_ifgrp_mock_object('ifgrp-fail').delete_if_grp() + assert 'Error deleting if_group test' in exc.value.args[0]['msg'] + + +def default_args(): + args = { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_9_7': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'ifgrp_record': (200, { + "num_records": 2, + "records": [ + { + 'lag': { + 'distribution_policy': 'ip', + 'mode': 'multimode_lacp' + }, + 'name': 'a0b', + 'node': {'name': 'mohan9cluster2-01'}, + 'type': 'lag', + 'uuid': '1b830a46-47cd-11ec-90df-005056b3dfc8' + }, + { + 'broadcast_domain': { + 'ipspace': {'name': 'ip1'}, + 'name': 'test1' + }, + 'lag': { + 'distribution_policy': 'ip', + 'member_ports': [ + { + 'name': 'e0d', + 'node': {'name': 'mohan9cluster2-01'}, + }], + 'mode': 'multimode_lacp'}, + 'name': 'a0d', + 'node': {'name': 'mohan9cluster2-01'}, + 'type': 'lag', + 'uuid': '5aeebc96-47d7-11ec-90df-005056b3dfc8' + }, + { + 'broadcast_domain': { + 'ipspace': {'name': 'ip1'}, + 'name': 'test1' + }, + 'lag': { + 'distribution_policy': 'ip', + 'member_ports': [ + { + 'name': 'e0c', + 'node': {'name': 'mohan9cluster2-01'}, + }, + { + 'name': 'e0a', + 'node': {'name': 'mohan9cluster2-01'}, + }], + 'mode': 'multimode_lacp' + }, + 'name': 'a0d', + 'node': {'name': 'mohan9cluster2-01'}, + 'type': 'lag', + 'uuid': '5aeebc96-47d7-11ec-90df-005056b3dsd4' + }] + }, None), + 'ifgrp_record_create': (200, { + "num_records": 1, + "records": [ + { + 'lag': { + 'distribution_policy': 'ip', + 'mode': 'multimode_lacp' + }, + 'name': 'a0b', + 'node': {'name': 'mohan9cluster2-01'}, + 'type': 'lag', + 'uuid': '1b830a46-47cd-11ec-90df-005056b3dfc8' + }] + }, None), + 'ifgrp_record_modify': (200, { + "num_records": 1, + "records": [ + { + 'broadcast_domain': { + 'ipspace': {'name': 'ip1'}, + 'name': 'test1' + }, + 'lag': { + 'distribution_policy': 'ip', + 'member_ports': [ + { + 'name': 'e0c', + 'node': {'name': 'mohan9cluster2-01'}, + }, + { + 'name': 'e0d', + 'node': {'name': 'mohan9cluster2-01'}, + }], + 'mode': 'multimode_lacp' + }, + 'name': 'a0d', + 'node': {'name': 'mohan9cluster2-01'}, + 'type': 'lag', + 'uuid': '5aeebc96-47d7-11ec-90df-005056b3dsd4' + }] + }, None) +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + ifgrp_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +def test_module_fail_when_broadcast_domain_ipspace(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + ifgrp_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_broadcast_domain_ipspace_rest_ontap96(mock_request, patch_ansible): + '''throw error if broadcast_domain and ipspace are not set''' + args = dict(default_args()) + args['ports'] = "e0c" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + args['node'] = "mohan9cluster2-01" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_6'], # get version + ] + with pytest.raises(AnsibleFailJson) as exc: + ifgrp_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'are mandatory fields with ONTAP 9.6 and 9.7' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_broadcast_domain_ipspace_rest_required_together(mock_request, patch_ansible): + '''throw error if one of broadcast_domain or ipspace only set''' + args = dict(default_args()) + args['ports'] = "e0c" + args['distribution_function'] = "ip" + args['ipspace'] = "Default" + args['mode'] = "multimode_lacp" + args['node'] = "mohan9cluster2-01" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_6'], # get version + ] + with pytest.raises(AnsibleFailJson) as exc: + ifgrp_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'parameters are required together: broadcast_domain, ipspace' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_ifgrp_not_found_from_lag_ports(mock_request, patch_ansible): + ''' throw error if lag not found with both ports and from_lag_ports ''' + args = dict(default_args()) + args['node'] = "mohan9-vsim1" + args['ports'] = "e0f" + args['from_lag_ports'] = "e0l" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record'] # get for ports + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = "Error: cannot find LAG matching from_lag_ports: '['e0l']'." + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_from_lag_ports_1_or_more_ports_not_in_current(mock_request, patch_ansible): + ''' throw error if 1 or more from_lag_ports not found in current ''' + args = dict(default_args()) + args['node'] = "mohan9-vsim1" + args['ports'] = "e0f" + args['from_lag_ports'] = "e0d,e0h" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + ] + my_obj = ifgrp_module() + my_obj.current_records = SRR['ifgrp_record'][1]['records'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = "Error: cannot find LAG matching from_lag_ports: '['e0d', 'e0h']'." + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_from_lag_ports_are_in_different_LAG(mock_request, patch_ansible): + ''' throw error if ports in from_lag_ports are in different LAG ''' + args = dict(default_args()) + args['node'] = "mohan9-vsim1" + args['ports'] = "e0f" + args['from_lag_ports'] = "e0d,e0c" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record'] # get + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = "'e0d, e0c' are in different LAG" + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_try_to_delete_only_partial_match_found(mock_request, patch_ansible): + ''' delete only with exact match of ports''' + args = dict(default_args()) + args['node'] = "mohan9cluster2-01" + args['ports'] = "e0c" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + args['broadcast_domain'] = "test1" + args['ipspace'] = "ip1" + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record'], # get + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_try_to_delete_ports_in_different_LAG(mock_request, patch_ansible): + ''' if ports are in different LAG, not to delete and returk ok''' + args = dict(default_args()) + args['node'] = "mohan9cluster2-01" + args['ports'] = "e0c,e0d" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + args['broadcast_domain'] = "test1" + args['ipspace'] = "ip1" + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record'], # get + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_partial_match(mock_request, patch_ansible): + '''fail if partial match only found in from_lag_ports''' + args = dict(default_args()) + args['node'] = "mohan9cluster2-01" + args['from_lag_ports'] = "e0c,e0a,e0v" + args['ports'] = "e0n" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + args['state'] = 'present' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record'], # get + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = "Error: cannot find LAG matching from_lag_ports: '['e0c', 'e0a', 'e0v']'." + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_partial_match_ports_empty_record_from_lag_ports(mock_request, patch_ansible): + ''' remove port e0a from ifgrp a0d with ports e0d,e0c''' + args = dict(default_args()) + args['node'] = "mohan9cluster2-01" + args['ports'] = "e0c" + args['from_lag_ports'] = "e0k" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record_modify'] # get + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = "Error: cannot find LAG matching from_lag_ports: '['e0k']'." + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_create_ifgrp_port(mock_request, patch_ansible): + ''' test create ifgrp ''' + args = dict(default_args()) + args['node'] = "mohan9-vsim1" + args['ports'] = "e0c,e0a" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record_create'], # get + SRR['empty_good'], # create + SRR['end_of_sequence'] + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_create_ifgrp_port_idempotent(mock_request, patch_ansible): + ''' test create ifgrp idempotent ''' + args = dict(default_args()) + args['node'] = "mohan9cluster2-01" + args['ports'] = "e0c,e0a" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record'], # get + SRR['end_of_sequence'] + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_modify_ifgrp_port(mock_request, patch_ansible): + ''' remove port e0a from ifgrp a0d with ports e0d,e0c''' + args = dict(default_args()) + args['node'] = "mohan9cluster2-01" + args['ports'] = "e0c" + args['from_lag_ports'] = "e0c,e0d" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record_modify'], # get + SRR['empty_good'], # modify + SRR['end_of_sequence'] + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_modify_ifgrp_broadcast_domain(mock_request, patch_ansible): + ''' modify broadcast domain and ipspace''' + args = dict(default_args()) + args['node'] = "mohan9cluster2-01" + args['ports'] = "e0c,e0a" + args['from_lag_ports'] = 'e0c' + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + args['broadcast_domain'] = "test1" + args['ipspace'] = "Default" + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record'], # get + SRR['empty_good'], # modify + SRR['end_of_sequence'] + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_delete_ifgrp(mock_request, patch_ansible): + ''' test delete LAG''' + args = dict(default_args()) + args['node'] = "mohan9cluster2-01" + args['ports'] = "e0c,e0a" + args['distribution_function'] = "ip" + args['mode'] = "multimode_lacp" + args['broadcast_domain'] = "test1" + args['ipspace'] = "ip1" + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['ifgrp_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = ifgrp_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_port.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_port.py new file mode 100644 index 000000000..b58e02d1b --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_port.py @@ -0,0 +1,331 @@ +# (c) 2018-2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings, set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_port \ + import NetAppOntapNetPort as port_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.type = kind + self.data = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + if self.type == 'raise': + raise netapp_utils.zapi.NaApiError(code='1111', message='forcing an error') + self.xml_in = xml + if self.type == 'port': + xml = self.build_port_info(self.data) + self.xml_out = xml + return xml + + @staticmethod + def build_port_info(port_details): + ''' build xml data for net-port-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'net-port-info': { + # 'port': port_details['port'], + 'mtu': str(port_details['mtu']), + 'is-administrative-auto-negotiate': 'true', + 'is-administrative-up': str(port_details['up_admin']).lower(), # ZAPI uses 'true', 'false' + 'ipspace': 'default', + 'administrative-flowcontrol': port_details['flowcontrol_admin'], + 'node': port_details['node'] + } + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.mock_port = { + 'node': 'test', + 'ports': 'a1', + 'up_admin': True, + 'flowcontrol_admin': 'something', + 'mtu': 1000 + } + + def mock_args(self): + return { + 'node': self.mock_port['node'], + 'flowcontrol_admin': self.mock_port['flowcontrol_admin'], + 'ports': [self.mock_port['ports']], + 'mtu': self.mock_port['mtu'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'feature_flags': {'no_cserver_ems': True}, + 'use_rest': 'never' + } + + def get_port_mock_object(self, kind=None, data=None): + """ + Helper method to return an na_ontap_net_port object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_net_port object + """ + obj = port_module() + obj.autosupport_log = Mock(return_value=None) + if data is None: + data = self.mock_port + obj.server = MockONTAPConnection(kind=kind, data=data) + return obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + port_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_port(self): + ''' Test if get_net_port returns None for non-existent port ''' + set_module_args(self.mock_args()) + result = self.get_port_mock_object().get_net_port('test') + assert result is None + + def test_get_existing_port(self): + ''' Test if get_net_port returns details for existing port ''' + set_module_args(self.mock_args()) + result = self.get_port_mock_object('port').get_net_port('test') + assert result['mtu'] == self.mock_port['mtu'] + assert result['flowcontrol_admin'] == self.mock_port['flowcontrol_admin'] + assert result['up_admin'] == self.mock_port['up_admin'] + + def test_successful_modify(self): + ''' Test modify_net_port ''' + data = self.mock_args() + data['mtu'] = '2000' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert exc.value.args[0]['changed'] + + def test_successful_modify_int(self): + ''' Test modify_net_port ''' + data = self.mock_args() + data['mtu'] = 2000 + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert exc.value.args[0]['changed'] + print(exc.value.args[0]['modify']) + + def test_successful_modify_bool(self): + ''' Test modify_net_port ''' + data = self.mock_args() + data['up_admin'] = False + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert exc.value.args[0]['changed'] + print(exc.value.args[0]['modify']) + + def test_successful_modify_str(self): + ''' Test modify_net_port ''' + data = self.mock_args() + data['flowcontrol_admin'] = 'anything' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert exc.value.args[0]['changed'] + print(exc.value.args[0]['modify']) + + def test_successful_modify_multiple_ports(self): + ''' Test modify_net_port ''' + data = self.mock_args() + data['ports'] = ['a1', 'a2'] + data['mtu'] = '2000' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_port.NetAppOntapNetPort.get_net_port') + def test_get_called(self, get_port): + ''' Test get_net_port ''' + data = self.mock_args() + data['ports'] = ['a1', 'a2'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object('port').apply() + assert get_port.call_count == 2 + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_port.NetAppOntapNetPort.get_net_port') + def test_negative_not_found_1(self, get_port): + ''' Test get_net_port ''' + data = self.mock_args() + data['ports'] = ['a1'] + set_module_args(data) + get_port.return_value = None + with pytest.raises(AnsibleFailJson) as exc: + self.get_port_mock_object('port').apply() + msg = 'Error: port: a1 not found on node: test - check node name.' + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_port.NetAppOntapNetPort.get_net_port') + def test_negative_not_found_2(self, get_port): + ''' Test get_net_port ''' + data = self.mock_args() + data['ports'] = ['a1', 'a2'] + set_module_args(data) + get_port.return_value = None + with pytest.raises(AnsibleFailJson) as exc: + self.get_port_mock_object('port').apply() + msg = 'Error: ports: a1, a2 not found on node: test - check node name.' + assert msg in exc.value.args[0]['msg'] + + def test_negative_zapi_exception_in_get(self): + ''' Test get_net_port ''' + data = self.mock_args() + data['ports'] = ['a1', 'a2'] + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_port_mock_object('raise').get_net_port('a1') + msg = 'Error getting net ports for test: NetApp API failed. Reason - 1111:forcing an error' + assert msg in exc.value.args[0]['msg'] + + def test_negative_zapi_exception_in_modify(self): + ''' Test get_net_port ''' + data = self.mock_args() + data['ports'] = ['a1', 'a2'] + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_port_mock_object('raise').modify_net_port('a1', dict()) + msg = 'Error modifying net ports for test: NetApp API failed. Reason - 1111:forcing an error' + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') + def test_negative_no_netapp_lib(self, get_port): + ''' Test get_net_port ''' + data = self.mock_args() + set_module_args(data) + get_port.return_value = False + with pytest.raises(AnsibleFailJson) as exc: + self.get_port_mock_object('port').apply() + msg = 'the python NetApp-Lib module is required' + assert msg in exc.value.args[0]['msg'] + + +def default_args(): + return { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'use_rest': 'always' + } + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_9_7': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'vlan_record': (200, { + "num_records": 1, + "records": [{ + 'broadcast_domain': { + 'ipspace': {'name': 'Default'}, + 'name': 'test1' + }, + 'enabled': False, + 'name': 'e0c-15', + 'node': {'name': 'mohan9-vsim1'}, + 'uuid': '97936a14-30de-11ec-ac4d-005056b3d8c8' + }] + }, None) +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + port_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_unsupported_rest_properties(mock_request, patch_ansible): + '''throw error if unsupported rest properties are set''' + args = dict(default_args()) + args['node'] = "mohan9-vsim1" + args['ports'] = "e0d,e0d-15" + args['mtu'] = 1500 + args['duplex_admin'] = 'admin' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(args) + port_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'REST API currently does not support' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_enable_port(mock_request, patch_ansible): + ''' test enable vlan''' + args = dict(default_args()) + args['node'] = "mohan9-vsim1" + args['ports'] = "e0c-15" + args['up_admin'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['vlan_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_routes.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_routes.py new file mode 100644 index 000000000..a886e87a3 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_routes.py @@ -0,0 +1,359 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import create_module,\ + patch_ansible, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, build_zapi_error, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_routes \ + import NetAppOntapNetRoutes as net_route_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +DEFAULT_ARGS = { + 'https': 'True', + 'use_rest': 'never', + 'state': 'present', + 'destination': '176.0.0.0/24', + 'gateway': '10.193.72.1', + 'vserver': 'test_vserver', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'metric': 70 +} + + +def route_info_zapi(destination='176.0.0.0/24', gateway='10.193.72.1', metric=70): + return { + 'attributes': { + 'net-vs-routes-info': { + 'address-family': 'ipv4', + 'destination': destination, + 'gateway': gateway, + 'metric': metric, + 'vserver': 'test_vserver' + } + } + } + + +ZRR = zapi_responses({ + 'net_route_info': build_zapi_response(route_info_zapi()), + 'net_route_info_gateway': build_zapi_response(route_info_zapi(gateway='10.193.0.1', metric=40)), + 'net_route_info_destination': build_zapi_response(route_info_zapi(destination='178.0.0.1/24', metric=40)), + 'error_15661': build_zapi_error(15661, 'not_exists_error'), + 'error_13001': build_zapi_error(13001, 'already exists') +}) + + +SRR = rest_responses({ + 'net_routes_record': (200, { + 'records': [ + { + "destination": {"address": "176.0.0.0", "netmask": "24", "family": "ipv4"}, + "gateway": '10.193.72.1', + "uuid": '1cd8a442-86d1-11e0-ae1c-123478563412', + "metric": 70, + "svm": {"name": "test_vserver"} + } + ] + }, None), + 'net_routes_cluster': (200, { + 'records': [ + { + "destination": {"address": "176.0.0.0", "netmask": "24", "family": "ipv4"}, + "gateway": '10.193.72.1', + "uuid": '1cd8a442-86d1-11e0-ae1c-123478563412', + "metric": 70, + "scope": "cluster" + } + ] + }, None), + 'modified_record': (200, { + 'records': [ + { + "destination": {"address": "0.0.0.0", "netmask": "0", "family": "ipv4"}, + "gateway": '10.193.72.1', + "uuid": '1cd8a442-86d1-11e0-ae1c-123478563412', + "scope": "cluster", + "metric": 90 + } + ] + }, None) +}) + + +def test_module_fail_when_required_args_missing(): + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "destination", "gateway"] + error = create_module(net_route_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_get_nonexistent_net_route(): + ''' Test if get_net_route returns None for non-existent net_route ''' + register_responses([ + ('net-routes-get', ZRR['no_records']) + ]) + assert create_module(net_route_module, DEFAULT_ARGS).get_net_route() is None + + +def test_get_nonexistent_net_route_15661(): + ''' Test if get_net_route returns None for non-existent net_route + when ZAPI returns an exception for a route not found + ''' + register_responses([ + ('net-routes-get', ZRR['error_15661']) + ]) + assert create_module(net_route_module, DEFAULT_ARGS).get_net_route() is None + + +def test_get_existing_route(): + ''' Test if get_net_route returns details for existing net_route ''' + register_responses([ + ('net-routes-get', ZRR['net_route_info']) + ]) + result = create_module(net_route_module, DEFAULT_ARGS).get_net_route() + assert result['destination'] == DEFAULT_ARGS['destination'] + assert result['gateway'] == DEFAULT_ARGS['gateway'] + + +def test_create_error_missing_param(): + ''' Test if create throws an error if destination is not specified''' + error = 'missing required arguments: destination' + assert error in create_module(net_route_module, {'hostname': 'host', 'gateway': 'gate'}, fail=True)['msg'] + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('net-routes-get', ZRR['empty']), + ('net-routes-create', ZRR['success']), + ('net-routes-get', ZRR['net_route_info']), + ]) + assert create_and_apply(net_route_module, DEFAULT_ARGS)['changed'] + assert not create_and_apply(net_route_module, DEFAULT_ARGS)['changed'] + + +def test_create_zapi_ignore_route_exist(): + ''' Test NaApiError on create ''' + register_responses([ + ('net-routes-get', ZRR['empty']), + ('net-routes-create', ZRR['error_13001']) + ]) + assert create_and_apply(net_route_module, DEFAULT_ARGS)['changed'] + + +def test_successful_create_zapi_no_metric(): + ''' Test successful create ''' + register_responses([ + ('net-routes-get', ZRR['empty']), + ('net-routes-create', ZRR['success']) + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['metric'] + assert create_and_apply(net_route_module, DEFAULT_ARGS)['changed'] + + +def test_successful_delete(): + ''' Test successful delete ''' + register_responses([ + ('net-routes-get', ZRR['net_route_info']), + ('net-routes-destroy', ZRR['success']), + ('net-routes-get', ZRR['empty']), + ]) + assert create_and_apply(net_route_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(net_route_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_successful_modify_metric(): + ''' Test successful modify metric ''' + register_responses([ + ('net-routes-get', ZRR['net_route_info']), + ('net-routes-destroy', ZRR['success']), + ('net-routes-create', ZRR['success']) + ]) + assert create_and_apply(net_route_module, DEFAULT_ARGS, {'metric': '40'})['changed'] + + +def test_successful_modify_gateway(): + ''' Test successful modify gateway ''' + register_responses([ + ('net-routes-get', ZRR['empty']), + ('net-routes-get', ZRR['net_route_info']), + ('net-routes-destroy', ZRR['success']), + ('net-routes-create', ZRR['success']), + ('net-routes-get', ZRR['net_route_info_gateway']) + ]) + args = {'from_gateway': '10.193.72.1', 'gateway': '10.193.0.1', 'metric': 40} + assert create_and_apply(net_route_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(net_route_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_modify_destination(): + ''' Test successful modify destination ''' + register_responses([ + ('net-routes-get', ZRR['empty']), + ('net-routes-get', ZRR['net_route_info']), + ('net-routes-destroy', ZRR['success']), + ('net-routes-create', ZRR['success']), + ('net-routes-get', ZRR['net_route_info_gateway']) + ]) + args = {'from_destination': '176.0.0.0/24', 'destination': '178.0.0.1/24', 'metric': 40} + assert create_and_apply(net_route_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(net_route_module, DEFAULT_ARGS, args)['changed'] + + +def test_if_all_methods_catch_exception_zapi(): + ''' test error zapi - get/create/modify/delete''' + register_responses([ + # ZAPI get/create/delete error. + ('net-routes-get', ZRR['error']), + ('net-routes-create', ZRR['error']), + ('net-routes-destroy', ZRR['error']), + # ZAPI modify error. + ('net-routes-get', ZRR['net_route_info']), + ('net-routes-destroy', ZRR['success']), + ('net-routes-create', ZRR['error']), + ('net-routes-create', ZRR['success']), + # REST get/create/delete error. + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['generic_error']), + ('POST', 'network/ip/routes', SRR['generic_error']), + ('DELETE', 'network/ip/routes/12345', SRR['generic_error']), + # REST modify error. + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['net_routes_record']), + ('DELETE', 'network/ip/routes/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + ('POST', 'network/ip/routes', SRR['generic_error']), + ('POST', 'network/ip/routes', SRR['success']), + ]) + net_route_obj = create_module(net_route_module, DEFAULT_ARGS) + assert 'Error fetching net route' in expect_and_capture_ansible_exception(net_route_obj.get_net_route, 'fail')['msg'] + assert 'Error creating net route' in expect_and_capture_ansible_exception(net_route_obj.create_net_route, 'fail')['msg'] + current = {'destination': '', 'gateway': ''} + assert 'Error deleting net route' in expect_and_capture_ansible_exception(net_route_obj.delete_net_route, 'fail', current)['msg'] + error = 'Error modifying net route' + assert error in create_and_apply(net_route_module, DEFAULT_ARGS, {'metric': 80}, fail=True)['msg'] + + net_route_obj = create_module(net_route_module, DEFAULT_ARGS, {'use_rest': 'always'}) + assert 'Error fetching net route' in expect_and_capture_ansible_exception(net_route_obj.get_net_route, 'fail')['msg'] + assert 'Error creating net route' in expect_and_capture_ansible_exception(net_route_obj.create_net_route, 'fail')['msg'] + current = {'uuid': '12345'} + assert 'Error deleting net route' in expect_and_capture_ansible_exception(net_route_obj.delete_net_route, 'fail', current)['msg'] + assert error in create_and_apply(net_route_module, DEFAULT_ARGS, {'metric': 80, 'use_rest': 'always'}, fail=True)['msg'] + + +def test_rest_successfully_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['empty_records']), + ('POST', 'network/ip/routes', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['net_routes_record']) + ]) + assert create_and_apply(net_route_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + assert not create_and_apply(net_route_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + + +def test_rest_successfully_create_cluster_scope(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['empty_records']), + ('POST', 'network/ip/routes', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['net_routes_cluster']), + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['vserver'] + assert create_and_apply(net_route_module, DEFAULT_ARGS_COPY, {'use_rest': 'always'})['changed'] + assert not create_and_apply(net_route_module, DEFAULT_ARGS_COPY, {'use_rest': 'always'})['changed'] + + +def test_rest_successfully_destroy(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['net_routes_record']), + ('DELETE', 'network/ip/routes/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['empty_records']), + ]) + args = {'use_rest': 'always', 'state': 'absent'} + assert create_and_apply(net_route_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(net_route_module, DEFAULT_ARGS, args)['changed'] + + +def test_rest_successfully_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['empty_records']), + ('GET', 'network/ip/routes', SRR['net_routes_record']), + ('DELETE', 'network/ip/routes/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['success']), + ('POST', 'network/ip/routes', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['modified_record']) + ]) + args = {'use_rest': 'always', 'metric': '90', 'from_destination': '176.0.0.0/24', 'destination': '0.0.0.0/24'} + assert create_and_apply(net_route_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(net_route_module, DEFAULT_ARGS, args)['changed'] + + +def test_rest_negative_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'network/ip/routes', SRR['empty_records']), + ('GET', 'network/ip/routes', SRR['empty_records']) + ]) + error = 'Error modifying: route 176.0.0.0/24 does not exist' + args = {'use_rest': 'auto', 'from_destination': '176.0.0.0/24'} + assert error in create_and_apply(net_route_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_negative_zapi_no_netapp_lib(mock_has_lib): + mock_has_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required.' + assert msg in create_module(net_route_module, DEFAULT_ARGS, fail=True)['msg'] + + +def test_negative_non_supported_option(): + error = "REST API currently does not support 'from_metric'" + args = {'use_rest': 'always', 'from_metric': 23} + assert error in create_module(net_route_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_negative_zapi_requires_vserver(): + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['vserver'] + error = "Error: vserver is a required parameter when using ZAPI" + assert error in create_module(net_route_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + + +def test_negative_dest_format(): + error = "Error: Expecting '/' in '1.2.3.4'." + assert error in create_module(net_route_module, DEFAULT_ARGS, {'destination': '1.2.3.4'}, fail=True)['msg'] + + +def test_negative_from_dest_format(): + args = {'destination': '1.2.3.4', 'from_destination': '5.6.7.8'} + error_msg = create_module(net_route_module, DEFAULT_ARGS, args, fail=True)['msg'] + msg = "Error: Expecting '/' in '1.2.3.4'." + assert msg in error_msg + msg = "Expecting '/' in '5.6.7.8'." + assert msg in error_msg diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_subnet.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_subnet.py new file mode 100644 index 000000000..ac284c8d7 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_subnet.py @@ -0,0 +1,275 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_subnet \ + import NetAppOntapSubnet as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +DEFAULT_ARGS = { + 'name': 'test_subnet', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'broadcast_domain': 'Default', + 'gateway': '10.0.0.1', + 'ipspace': 'Default', + 'subnet': '10.0.0.0/24', + 'ip_ranges': ['10.0.0.10-10.0.0.20', '10.0.0.30'], + 'use_rest': 'never' +} + + +def subnet_info(name): + return { + 'num-records': 1, + 'attributes-list': { + 'net-subnet-info': { + 'broadcast-domain': DEFAULT_ARGS['broadcast_domain'], + 'gateway': DEFAULT_ARGS['gateway'], + 'ip-ranges': [{'ip-range': elem} for elem in DEFAULT_ARGS['ip_ranges']], + 'ipspace': DEFAULT_ARGS['ipspace'], + 'subnet': DEFAULT_ARGS['subnet'], + 'subnet-name': name, + } + } + } + + +ZRR = zapi_responses({ + 'subnet_info': build_zapi_response(subnet_info(DEFAULT_ARGS['name'])), + 'subnet_info_renamed': build_zapi_response(subnet_info('new_test_subnet')), +}) + + +SRR = rest_responses({ + 'subnet_info': (200, {"records": [{ + "uuid": "82610703", + "name": "test_subnet", + "ipspace": {"name": "Default"}, + "gateway": "10.0.0.1", + "broadcast_domain": {"name": "Default"}, + "subnet": {"address": "10.0.0.0", "netmask": "24", "family": "ipv4"}, + "available_ip_ranges": [ + {"start": "10.0.0.10", "end": "10.0.0.20", "family": "ipv4"}, + {"start": "10.0.0.30", "end": "10.0.0.30", "family": "ipv4"} + ] + }], "num_records": 1}, None), + 'subnet_info_renamed': (200, {"records": [{ + "uuid": "82610703", + "name": "new_test_subnet", + "ipspace": {"name": "Default"}, + "gateway": "10.0.0.1", + "broadcast_domain": {"name": "Default"}, + "subnet": {"address": "10.0.0.0", "netmask": "24", "family": "ipv4"}, + "available_ip_ranges": [ + {"start": "10.0.0.10", "end": "10.0.0.20", "family": "ipv4"}, + {"start": "10.0.0.30", "end": "10.0.0.30", "family": "ipv4"} + ] + }], "num_records": 1}, None) +}) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + assert 'Error: the python NetApp-Lib module is required. Import error: None' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] + + +def test_successful_create(): + register_responses([ + + ('ZAPI', 'net-subnet-get-iter', ZRR['no_records']), + ('ZAPI', 'net-subnet-create', ZRR['success']), + # idempotency + + ('ZAPI', 'net-subnet-get-iter', ZRR['subnet_info']), + ]) + assert call_main(my_main, DEFAULT_ARGS)['changed'] + # idempotency + assert not call_main(my_main, DEFAULT_ARGS)['changed'] + + +def test_successful_delete(): + register_responses([ + ('ZAPI', 'net-subnet-get-iter', ZRR['subnet_info']), + ('ZAPI', 'net-subnet-destroy', ZRR['success']), + # idempotency + ('ZAPI', 'net-subnet-get-iter', ZRR['no_records']), + ]) + assert call_main(my_main, DEFAULT_ARGS, {'state': 'absent'})['changed'] + # idempotency + assert not call_main(my_main, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_successful_modify(): + register_responses([ + ('ZAPI', 'net-subnet-get-iter', ZRR['subnet_info']), + ('ZAPI', 'net-subnet-modify', ZRR['success']), + # idempotency + ('ZAPI', 'net-subnet-get-iter', ZRR['subnet_info']), + ]) + module_args = {'ip_ranges': ['10.0.0.10-10.0.0.25', '10.0.0.30']} + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # idempotency + module_args.pop('ip_ranges') + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_rename(): + register_responses([ + ('ZAPI', 'net-subnet-get-iter', ZRR['no_records']), + ('ZAPI', 'net-subnet-get-iter', ZRR['subnet_info']), + ('ZAPI', 'net-subnet-rename', ZRR['success']), + # idempotency + ('ZAPI', 'net-subnet-get-iter', ZRR['subnet_info']), + ]) + module_args = {'from_name': DEFAULT_ARGS['name'], 'name': 'new_test_subnet'} + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # idempotency + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_modify_broadcast_domain(): + register_responses([ + ('ZAPI', 'net-subnet-get-iter', ZRR['subnet_info']), + ]) + module_args = {'broadcast_domain': 'cannot change'} + error = 'Error modifying subnet test_subnet: cannot modify broadcast_domain parameter, desired "cannot change", currrent "Default"' + assert error == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rename(): + register_responses([ + ('ZAPI', 'net-subnet-get-iter', ZRR['no_records']), + ('ZAPI', 'net-subnet-get-iter', ZRR['no_records']), + ]) + module_args = {'from_name': DEFAULT_ARGS['name'], 'name': 'new_test_subnet'} + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == 'Error renaming: subnet test_subnet does not exist' + + +def test_negative_create(): + register_responses([ + ('ZAPI', 'net-subnet-get-iter', ZRR['no_records']), + # second test + ('ZAPI', 'net-subnet-get-iter', ZRR['no_records']), + # third test + ('ZAPI', 'net-subnet-get-iter', ZRR['no_records']), + ]) + args = dict(DEFAULT_ARGS) + args.pop('subnet') + assert call_main(my_main, args, fail=True)['msg'] == 'Error - missing required arguments: subnet.' + args = dict(DEFAULT_ARGS) + args.pop('broadcast_domain') + assert call_main(my_main, args, fail=True)['msg'] == 'Error - missing required arguments: broadcast_domain.' + args.pop('subnet') + assert call_main(my_main, args, fail=True)['msg'] == 'Error - missing required arguments: subnet.' + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ZAPI', 'net-subnet-get-iter', ZRR['error']), + ('ZAPI', 'net-subnet-create', ZRR['error']), + ('ZAPI', 'net-subnet-destroy', ZRR['error']), + ('ZAPI', 'net-subnet-modify', ZRR['error']), + ('ZAPI', 'net-subnet-rename', ZRR['error']), + # REST exception + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['generic_error']), + ('POST', 'network/ip/subnets', SRR['generic_error']), + ('PATCH', 'network/ip/subnets/82610703', SRR['generic_error']), + ('DELETE', 'network/ip/subnets/82610703', SRR['generic_error']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + assert zapi_error_message('Error fetching subnet test_subnet') == expect_and_capture_ansible_exception(my_obj.get_subnet, 'fail')['msg'] + assert zapi_error_message('Error creating subnet test_subnet') == expect_and_capture_ansible_exception(my_obj.create_subnet, 'fail')['msg'] + assert zapi_error_message('Error deleting subnet test_subnet') == expect_and_capture_ansible_exception(my_obj.delete_subnet, 'fail')['msg'] + assert zapi_error_message('Error modifying subnet test_subnet') == expect_and_capture_ansible_exception(my_obj.modify_subnet, 'fail', {})['msg'] + assert zapi_error_message('Error renaming subnet test_subnet') == expect_and_capture_ansible_exception(my_obj.rename_subnet, 'fail')['msg'] + my_obj = create_module(my_module, DEFAULT_ARGS, {'use_rest': 'always'}) + my_obj.uuid = '82610703' + assert 'Error fetching subnet test_subnet' in expect_and_capture_ansible_exception(my_obj.get_subnet, 'fail')['msg'] + assert 'Error creating subnet test_subnet' in expect_and_capture_ansible_exception(my_obj.create_subnet, 'fail')['msg'] + assert 'Error modifying subnet test_subnet' in expect_and_capture_ansible_exception(my_obj.modify_subnet, 'fail', {})['msg'] + assert 'Error deleting subnet test_subnet' in expect_and_capture_ansible_exception(my_obj.delete_subnet, 'fail')['msg'] + modify = {'subnet': '192.168.1.2'} + assert 'Error: Invalid value specified for subnet' in expect_and_capture_ansible_exception(my_obj.form_create_modify_body_rest, 'fail', modify)['msg'] + + +def test_successful_create_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['empty_records']), + ('POST', 'network/ip/subnets', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['subnet_info']), + ]) + assert call_main(my_main, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + # idempotency + assert not call_main(my_main, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + + +def test_successful_modify_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['subnet_info']), + ('PATCH', 'network/ip/subnets/82610703', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['subnet_info']) + ]) + module_args = {'ip_ranges': ['10.0.0.10-10.0.0.25', '10.0.0.30'], 'use_rest': 'always'} + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # idempotency + module_args.pop('ip_ranges') + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_rename_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['empty_records']), + ('GET', 'network/ip/subnets', SRR['subnet_info']), + ('PATCH', 'network/ip/subnets/82610703', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['subnet_info_renamed']), + ]) + module_args = {'from_name': DEFAULT_ARGS['name'], 'name': 'new_test_subnet', 'use_rest': 'always'} + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # idempotency + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_delete_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['subnet_info']), + ('DELETE', 'network/ip/subnets/82610703', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'network/ip/subnets', SRR['empty_records']), + ]) + assert call_main(my_main, DEFAULT_ARGS, {'state': 'absent', 'use_rest': 'always'})['changed'] + # idempotency + assert not call_main(my_main, DEFAULT_ARGS, {'state': 'absent', 'use_rest': 'always'})['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_vlan.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_vlan.py new file mode 100644 index 000000000..bbcb9e7fe --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_net_vlan.py @@ -0,0 +1,252 @@ +# (c) 2018-2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP net vlan Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import sys +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings, set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_net_vlan \ + import NetAppOntapVlan as my_module # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +def default_args(): + args = { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_9_7': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'vlan_record': (200, { + "num_records": 1, + "records": [{ + 'broadcast_domain': { + 'ipspace': {'name': 'Default'}, + 'name': 'test1' + }, + 'enabled': True, + 'name': 'e0c-15', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': '97936a14-30de-11ec-ac4d-005056b3d8c8' + }] + }, None), + 'vlan_record_create': (200, { + "num_records": 1, + "records": [{ + 'broadcast_domain': { + 'ipspace': {'name': 'Default'}, + 'name': 'test2' + }, + 'enabled': True, + 'name': 'e0c-16', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': '97936a14-30de-11ec-ac4d-005056b3d8c8' + }] + }, None), + 'vlan_record_modify': (200, { + "num_records": 1, + "records": [{ + 'broadcast_domain': { + 'ipspace': {'name': 'Default'}, + 'name': 'test1' + }, + 'enabled': False, + 'name': 'e0c-16', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': '97936a14-30de-11ec-ac4d-005056b3d8c8' + }] + }, None) +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_when_required_args_missing_ONTAP96(mock_request, patch_ansible): + ''' required arguments are reported as errors for ONTAP 9.6''' + args = dict(default_args()) + args['node'] = 'mohan9cluster2-01' + args['vlanid'] = 154 + args['parent_interface'] = 'e0c' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_6'] # get version + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'broadcast_domain and ipspace are required fields with ONTAP 9.6 and 9.7' + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_fail_when_required_args_missing_ONTAP97(mock_request, patch_ansible): + ''' required arguments are reported as errors for ONTAP 9.7''' + args = dict(default_args()) + args['node'] = 'mohan9cluster2-01' + args['vlanid'] = 154 + args['parent_interface'] = 'e0c' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_7'] # get version + ] + with pytest.raises(AnsibleFailJson) as exc: + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'broadcast_domain and ipspace are required fields with ONTAP 9.6 and 9.7' + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_get_vlan_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['node'] = 'mohan9cluster2-01' + args['vlanid'] = 15 + args['parent_interface'] = 'e0c' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['vlan_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_create_vlan_called(mock_request, patch_ansible): + ''' test create''' + args = dict(default_args()) + args['node'] = 'mohan9cluster2-01' + args['vlanid'] = 16 + args['parent_interface'] = 'e0c' + args['broadcast_domain'] = 'test2' + args['ipspace'] = 'Default' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['empty_good'], # create + SRR['vlan_record_create'], # get created vlan record to check PATCH call required + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_modify_vlan_called(mock_request, patch_ansible): + ''' test modify''' + args = dict(default_args()) + args['node'] = 'mohan9cluster2-01' + args['vlanid'] = 16 + args['parent_interface'] = 'e0c' + args['broadcast_domain'] = 'test1' + args['ipspace'] = 'Default' + args['enabled'] = 'no' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['vlan_record_create'], # get + SRR['empty_good'], # patch call + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_vlan_called(mock_request, patch_ansible): + ''' test delete''' + args = dict(default_args()) + args['node'] = 'mohan9cluster2-01' + args['vlanid'] = 15 + args['parent_interface'] = 'e0c' + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['vlan_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_vlan_idempotent(mock_request, patch_ansible): + ''' test delete idempotent''' + args = dict(default_args()) + args['node'] = 'mohan9cluster2-01' + args['vlanid'] = 15 + args['parent_interface'] = 'e0c' + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nfs.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nfs.py new file mode 100644 index 000000000..116f25f06 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nfs.py @@ -0,0 +1,338 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import copy +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_no_warnings, assert_no_warnings_except_zapi, assert_warning_was_raised, call_main, create_and_apply, print_warnings, set_module_args,\ + AnsibleExitJson, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_nfs \ + import NetAppONTAPNFS as nfs_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +nfs_info = { + "attributes-list": { + "nfs-info": { + "auth-sys-extended-groups": "false", + "cached-cred-harvest-timeout": "86400000", + "cached-cred-negative-ttl": "7200000", + "cached-cred-positive-ttl": "86400000", + "cached-transient-err-ttl": "30000", + "chown-mode": "use_export_policy", + "enable-ejukebox": "true", + "extended-groups-limit": "32", + "file-session-io-grouping-count": "5000", + "file-session-io-grouping-duration": "120", + "ignore-nt-acl-for-root": "false", + "is-checksum-enabled-for-replay-cache": "true", + "is-mount-rootonly-enabled": "true", + "is-netgroup-dns-domain-search": "true", + "is-nfs-access-enabled": "false", + "is-nfs-rootonly-enabled": "false", + "is-nfsv2-enabled": "false", + "is-nfsv3-64bit-identifiers-enabled": "false", + "is-nfsv3-connection-drop-enabled": "true", + "is-nfsv3-enabled": "true", + "is-nfsv3-fsid-change-enabled": "true", + "is-nfsv4-fsid-change-enabled": "true", + "is-nfsv4-numeric-ids-enabled": "true", + "is-nfsv40-acl-enabled": "false", + "is-nfsv40-enabled": "true", + "is-nfsv40-migration-enabled": "false", + "is-nfsv40-read-delegation-enabled": "false", + "is-nfsv40-referrals-enabled": "false", + "is-nfsv40-req-open-confirm-enabled": "false", + "is-nfsv40-write-delegation-enabled": "false", + "is-nfsv41-acl-enabled": "false", + "is-nfsv41-acl-preserve-enabled": "true", + "is-nfsv41-enabled": "true", + "is-nfsv41-migration-enabled": "false", + "is-nfsv41-pnfs-enabled": "true", + "is-nfsv41-read-delegation-enabled": "false", + "is-nfsv41-referrals-enabled": "false", + "is-nfsv41-state-protection-enabled": "true", + "is-nfsv41-write-delegation-enabled": "false", + "is-qtree-export-enabled": "false", + "is-rquota-enabled": "false", + "is-tcp-enabled": "false", + "is-udp-enabled": "false", + "is-v3-ms-dos-client-enabled": "false", + "is-validate-qtree-export-enabled": "true", + "is-vstorage-enabled": "false", + "map-unknown-uid-to-default-windows-user": "true", + "mountd-port": "635", + "name-service-lookup-protocol": "udp", + "netgroup-trust-any-ns-switch-no-match": "false", + "nfsv4-acl-max-aces": "400", + "nfsv4-grace-seconds": "45", + "nfsv4-id-domain": "defaultv4iddomain.com", + "nfsv4-lease-seconds": "30", + "nfsv41-implementation-id-domain": "netapp.com", + "nfsv41-implementation-id-name": "NetApp Release Kalyaniblack__9.4.0", + "nfsv41-implementation-id-time": "1541070767", + "nfsv4x-session-num-slots": "180", + "nfsv4x-session-slot-reply-cache-size": "640", + "nlm-port": "4045", + "nsm-port": "4046", + "ntacl-display-permissive-perms": "false", + "ntfs-unix-security-ops": "use_export_policy", + "permitted-enc-types": { + "string": ["des", "des3", "aes_128", "aes_256"] + }, + "rpcsec-ctx-high": "0", + "rpcsec-ctx-idle": "0", + "rquotad-port": "4049", + "showmount": "true", + "showmount-timestamp": "1548372452", + "skip-root-owner-write-perm-check": "false", + "tcp-max-xfer-size": "1048576", + "udp-max-xfer-size": "32768", + "v3-search-unconverted-filename": "false", + "v4-inherited-acl-preserve": "false", + "vserver": "ansible" + } + }, + "num-records": "1" +} + +nfs_info_no_tcp_max_xfer_size = copy.deepcopy(nfs_info) +del nfs_info_no_tcp_max_xfer_size['attributes-list']['nfs-info']['tcp-max-xfer-size'] + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None, job_error=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'nfs': + xml = self.build_nfs_info(self.params) + self.xml_out = xml + if self.kind == 'nfs_status': + xml = self.build_nfs_status_info(self.params) + return xml + + @staticmethod + def build_nfs_info(nfs_details): + ''' build xml data for volume-attributes ''' + xml = netapp_utils.zapi.NaElement('xml') + xml.translate_struct(nfs_info) + return xml + + @staticmethod + def build_nfs_status_info(nfs_status_details): + ''' build xml data for volume-attributes ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'is-enabled': "true" + } + xml.translate_struct(attributes) + return xml + + +DEFAULT_ARGS = { + 'vserver': 'nfs_vserver', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': 'false', + 'use_rest': 'never' +} + + +SRR = zapi_responses({ + 'nfs_info': build_zapi_response(nfs_info), + 'nfs_info_no_tcp_max_xfer_size': build_zapi_response(nfs_info_no_tcp_max_xfer_size) +}) + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_nfs_group = { + 'vserver': DEFAULT_ARGS['vserver'], + } + + def mock_args(self): + return dict(DEFAULT_ARGS) + + def get_nfs_mock_object(self, kind=None): + """ + Helper method to return an na_ontap_volume object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_volume object + """ + nfsy_obj = nfs_module() + nfsy_obj.asup_log_for_cserver = Mock(return_value=None) + nfsy_obj.cluster = Mock() + nfsy_obj.cluster.invoke_successfully = Mock() + if kind is None: + nfsy_obj.server = MockONTAPConnection() + else: + nfsy_obj.server = MockONTAPConnection(kind=kind, data=self.mock_nfs_group) + return nfsy_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + error = 'missing required arguments' + assert error in call_main(my_main, {}, fail=True)['msg'] + + def test_get_nonexistent_nfs(self): + ''' Test if get_nfs_service returns None for non-existent nfs ''' + set_module_args(self.mock_args()) + result = self.get_nfs_mock_object().get_nfs_service() + assert result is None + + def test_get_existing_nfs(self): + ''' Test if get_policy_group returns details for existing nfs ''' + set_module_args(self.mock_args()) + result = self.get_nfs_mock_object('nfs').get_nfs_service() + assert result['nfsv3'] + + def test_get_nonexistent_nfs_status(self): + ''' Test if get__nfs_status returns None for non-existent nfs ''' + set_module_args(self.mock_args()) + result = self.get_nfs_mock_object().get_nfs_status() + assert result is None + + def test_get_existing_nfs_status(self): + ''' Test if get__nfs_status returns details for nfs ''' + set_module_args(self.mock_args()) + result = self.get_nfs_mock_object('nfs_status').get_nfs_status() + assert result + + def test_modify_nfs(self): + ''' Test if modify_nfs runs for existing nfs ''' + data = self.mock_args() + current = { + 'nfsv3': 'enabled', + 'nfsv3_fsid_change': 'enabled', + 'nfsv4': 'enabled', + 'nfsv41': 'enabled', + 'vstorage_state': 'enabled', + 'tcp': 'enabled', + 'udp': 'enabled', + 'nfsv4_id_domain': 'nfsv4_id_domain', + 'nfsv40_acl': 'enabled', + 'nfsv40_read_delegation': 'enabled', + 'nfsv40_write_delegation': 'enabled', + 'nfsv41_acl': 'enabled', + 'nfsv41_read_delegation': 'enabled', + 'nfsv41_write_delegation': 'enabled', + 'showmount': 'enabled', + 'tcp_max_xfer_size': '1048576', + } + + data.update(current) + set_module_args(data) + self.get_nfs_mock_object('nfs_status').modify_nfs_service(current) + + def test_successfully_modify_nfs(self): + ''' Test modify nfs successful for modifying tcp max xfer size. ''' + data = self.mock_args() + data['tcp_max_xfer_size'] = 8192 + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_nfs_mock_object('nfs').apply() + assert exc.value.args[0]['changed'] + + def test_modify_nfs_idempotency(self): + ''' Test modify nfs idempotency ''' + data = self.mock_args() + data['tcp_max_xfer_size'] = '1048576' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_nfs_mock_object('nfs').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_nfs.NetAppONTAPNFS.delete_nfs_service') + def test_successfully_delete_nfs(self, delete_nfs_service): + ''' Test successfully delete nfs ''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + obj = self.get_nfs_mock_object('nfs') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + delete_nfs_service.assert_called_with() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_nfs.NetAppONTAPNFS.get_nfs_service') + def test_successfully_enable_nfs(self, get_nfs_service): + ''' Test successfully enable nfs on non-existent nfs ''' + data = self.mock_args() + data['state'] = 'present' + set_module_args(data) + get_nfs_service.side_effect = [ + None, + {} + ] + obj = self.get_nfs_mock_object('nfs') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert exc.value.args[0]['changed'] + + +def test_modify_tcp_max_xfer_size(): + ''' if ZAPI returned a None value, a modify is attempted ''' + register_responses([ + # ONTAP 9.4 and later, tcp_max_xfer_size is an INT + ('ZAPI', 'nfs-service-get-iter', SRR['nfs_info']), + ('ZAPI', 'nfs-status', SRR['success']), + ('ZAPI', 'nfs-service-modify', SRR['success']), + # ONTAP 9.4 and later, tcp_max_xfer_size is an INT, idempotency + ('ZAPI', 'nfs-service-get-iter', SRR['nfs_info']), + # ONTAP 9.3 and earlier, tcp_max_xfer_size is not set + ('ZAPI', 'nfs-service-get-iter', SRR['nfs_info_no_tcp_max_xfer_size']), + ]) + module_args = { + 'tcp_max_xfer_size': 4500 + } + assert create_and_apply(nfs_module, DEFAULT_ARGS, module_args)['changed'] + module_args = { + 'tcp_max_xfer_size': 1048576 + } + assert not create_and_apply(nfs_module, DEFAULT_ARGS, module_args)['changed'] + error = 'Error: tcp_max_xfer_size is not supported on ONTAP 9.3 or earlier.' + assert create_and_apply(nfs_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + assert_no_warnings_except_zapi() + + +def test_warning_on_nfsv41_alias(): + ''' if ZAPI returned a None value, a modify is attempted ''' + register_responses([ + # ONTAP 9.4 and later, tcp_max_xfer_size is an INT + ('ZAPI', 'nfs-service-get-iter', SRR['nfs_info']), + ('ZAPI', 'nfs-status', SRR['success']), + ('ZAPI', 'nfs-service-modify', SRR['success']), + ]) + module_args = { + 'nfsv4.1': 'disabled' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised('Error: "nfsv4.1" option conflicts with Ansible naming conventions - please use "nfsv41".') diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nfs_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nfs_rest.py new file mode 100644 index 000000000..995dbeb6f --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nfs_rest.py @@ -0,0 +1,324 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + AnsibleFailJson, AnsibleExitJson, patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_nfs \ + import NetAppONTAPNFS as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +# REST API canned responses when mocking send_request. +# The rest_factory provides default responses shared across testcases. +SRR = rest_responses({ + # module specific responses + 'one_record': (200, {"records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "transport": { + "udp_enabled": True, + "tcp_enabled": True + }, + "protocol": { + "v3_enabled": True, + "v4_id_domain": "carchi8py.com", + "v40_enabled": False, + "v41_enabled": False, + "v40_features": { + "acl_enabled": False, + "read_delegation_enabled": False, + "write_delegation_enabled": False + }, + "v41_features": { + "acl_enabled": False, + "read_delegation_enabled": False, + "write_delegation_enabled": False, + "pnfs_enabled": False + } + }, + "vstorage_enabled": False, + "showmount_enabled": True, + "root": { + "ignore_nt_acl": False, + "skip_write_permission_check": False + }, + "security": { + "chown_mode": "restricted", + "nt_acl_display_permission": False, + "ntfs_unix_security": "fail", + "permitted_encryption_types": ["des3"], + "rpcsec_context_idle": 5 + }, + "windows":{ + "v3_ms_dos_client_enabled": False, + "map_unknown_uid_to_default_user": True, + "default_user": "test_user" + }, + "tcp_max_xfer_size": "16384" + } + ]}, None), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'ansibleSVM', + 'use_rest': 'always', +} + + +def set_default_args(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'ansibleSVM', + 'use_rest': 'always', + }) + + +def test_get_nfs_rest_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/nfs/services', SRR['empty_records']) + ]) + set_module_args(set_default_args()) + my_obj = my_module() + assert my_obj.get_nfs_service_rest() is None + + +def test_partially_supported_rest(): + register_responses([('GET', 'cluster', SRR['is_rest_96'])]) + module_args = set_default_args() + module_args['showmount'] = 'enabled' + set_module_args(module_args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = "Error: Minimum version of ONTAP for showmount is (9, 8)." + assert msg in exc.value.args[0]['msg'] + + +def test_get_nfs_rest_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/nfs/services', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error getting nfs services for SVM ansibleSVM: calling: protocols/nfs/services: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_nfs_service_rest, 'fail')['msg'] + + +def test_get_nfs_rest_one_record(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/nfs/services', SRR['one_record']) + ]) + set_module_args(set_default_args()) + my_obj = my_module() + assert my_obj.get_nfs_service_rest() is not None + + +def test_create_nfs(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/nfs/services', SRR['empty_records']), + ('POST', 'protocols/nfs/services', SRR['empty_good']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {})['changed'] + + +def test_create_nfs_all_options(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'protocols/nfs/services', SRR['empty_good']) + ]) + set_module_args(set_default_args()) + my_obj = my_module() + my_obj.parameters['nfsv3'] = True + my_obj.parameters['nfsv4'] = False + my_obj.parameters['nfsv41'] = False + my_obj.parameters['nfsv41_pnfs'] = False + my_obj.parameters['vstorage_state'] = False + my_obj.parameters['nfsv4_id_domain'] = 'carchi8py.com' + my_obj.parameters['tcp'] = True + my_obj.parameters['udp'] = True + my_obj.parameters['nfsv40_acl'] = False + my_obj.parameters['nfsv40_read_delegation'] = False + my_obj.parameters['nfsv40_write_delegation'] = False + my_obj.parameters['nfsv41_acl'] = False + my_obj.parameters['nfsv41_read_delegation'] = False + my_obj.parameters['nfsv41_write_delegation'] = False + my_obj.parameters['showmount'] = True + my_obj.parameters['service_state'] = 'stopped' + my_obj.create_nfs_service_rest() + assert get_mock_record().is_record_in_json({'svm.name': 'ansibleSVM'}, 'POST', 'protocols/nfs/services') + + +def test_create_nfs_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'protocols/nfs/services', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error creating nfs service for SVM ansibleSVM: calling: protocols/nfs/services: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.create_nfs_service_rest, 'fail')['msg'] + + +def test_delete_nfs(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/nfs/services', SRR['one_record']), + ('DELETE', 'protocols/nfs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']) + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_nfs_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('DELETE', 'protocols/nfs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']) + ]) + set_module_args(set_default_args()) + my_obj = my_module() + my_obj.parameters['state'] = 'absent' + my_obj.svm_uuid = '671aa46e-11ad-11ec-a267-005056b30cfa' + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_nfs_service_rest() + print('Info: %s' % exc.value.args[0]['msg']) + msg = "Error deleting nfs service for SVM ansibleSVM" + assert msg == exc.value.args[0]['msg'] + + +def test_delete_nfs_no_uuid_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + module_args = { + 'state': 'absent' + } + my_module_object = create_module(my_module, DEFAULT_ARGS, module_args) + msg = "Error deleting nfs service for SVM ansibleSVM: svm.uuid is None" + assert msg in expect_and_capture_ansible_exception(my_module_object.delete_nfs_service_rest, 'fail')['msg'] + + +def test_modify_nfs(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/nfs/services', SRR['one_record']), + ('PATCH', 'protocols/nfs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']) + ]) + set_module_args(set_default_args()) + my_obj = my_module() + my_obj.parameters['nfsv3'] = 'disabled' + my_obj.svm_uuid = '671aa46e-11ad-11ec-a267-005056b30cfa' + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + + +def test_modify_nfs_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'protocols/nfs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']) + ]) + set_module_args(set_default_args()) + my_obj = my_module() + my_obj.parameters['nfsv3'] = 'disabled' + my_obj.svm_uuid = '671aa46e-11ad-11ec-a267-005056b30cfa' + modify = {'nfsv3': False} + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_nfs_service_rest(modify) + print('Info: %s' % exc.value.args[0]['msg']) + msg = "Error modifying nfs service for SVM ansibleSVM: calling: protocols/nfs/services/671aa46e-11ad-11ec-a267-005056b30cfa: got Expected error." + assert msg == exc.value.args[0]['msg'] + + +def test_modify_nfs_no_uuid_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + set_module_args(set_default_args()) + my_obj = my_module() + my_obj.parameters['nfsv3'] = 'disabled' + modify = {'nfsv3': False} + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_nfs_service_rest(modify) + print('Info: %s' % exc.value.args[0]['msg']) + msg = "Error modifying nfs service for SVM ansibleSVM: svm.uuid is None" + assert msg == exc.value.args[0]['msg'] + + +def test_modify_nfs_root(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/nfs/services', SRR['one_record']), + ('PATCH', 'protocols/nfs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['success']) + ]) + module_args = { + "root": + { + "ignore_nt_acl": True, + "skip_write_permission_check": True + } + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_nfs_security(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/nfs/services', SRR['one_record']), + ('PATCH', 'protocols/nfs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['success']) + ]) + module_args = { + "security": + { + "chown_mode": "restricted", + "nt_acl_display_permission": "true", + "ntfs_unix_security": "fail", + "permitted_encryption_types": ["des3"], + "rpcsec_context_idle": 5 + } + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_nfs_windows(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_0']), + ('GET', 'protocols/nfs/services', SRR['one_record']), + ('PATCH', 'protocols/nfs/services/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['success']) + ]) + module_args = { + "windows": + { + "v3_ms_dos_client_enabled": True, + "map_unknown_uid_to_default_user": False, + "default_user": "test_user" + }, + "tcp_max_xfer_size": "16384" + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_node.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_node.py new file mode 100644 index 000000000..d29c5c64f --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_node.py @@ -0,0 +1,222 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_node \ + import NetAppOntapNode as node_module # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'node_record': (200, {"num_records": 1, "records": [ + { + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7", + "name": 'node1', + "location": 'myloc'} + ]}, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.type = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'node': + xml = self.build_node_info() + elif self.type == 'node_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_node_info(): + ''' build xml data for node-details-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'attributes': { + 'node-details-info': { + "node": "node1", + "node-location": "myloc", + "node-asset-tag": "mytag" + } + } + } + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def set_default_args(self, use_rest=None): + hostname = '10.10.10.10' + username = 'username' + password = 'password' + name = 'node1' + + args = dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'name': name, + 'location': 'myloc' + }) + + if use_rest is not None: + args['use_rest'] = use_rest + + return args + + @staticmethod + def get_node_mock_object(cx_type='zapi', kind=None): + node_obj = node_module() + if cx_type == 'zapi': + if kind is None: + node_obj.server = MockONTAPConnection() + else: + node_obj.server = MockONTAPConnection(kind=kind) + return node_obj + + def test_ensure_get_called(self): + ''' test get_node for non-existent entry''' + set_module_args(self.set_default_args(use_rest='Never')) + print('starting') + my_obj = node_module() + print('use_rest:', my_obj.use_rest) + my_obj.cluster = MockONTAPConnection('node') + assert my_obj.get_node is not None + + def test_successful_rename(self): + ''' renaming node and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['from_name'] = 'node1' + data['name'] = 'node2' + set_module_args(data) + my_obj = node_module() + my_obj.cluster = MockONTAPConnection('node') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # to reset na_helper from remembering the previous 'changed' value + data['name'] = 'node1' + set_module_args(data) + my_obj = node_module() + my_obj.cluster = MockONTAPConnection('node') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_successful_modify(self): + ''' modifying node and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['location'] = 'myloc1' + set_module_args(data) + my_obj = node_module() + my_obj.cluster = MockONTAPConnection('node') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # to reset na_helper from remembering the previous 'changed' value + data['location'] = 'myloc' + set_module_args(data) + my_obj = node_module() + my_obj.cluster = MockONTAPConnection('node') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + data = self.set_default_args(use_rest='Never') + data['from_name'] = 'node1' + data['name'] = 'node2' + set_module_args(data) + my_obj = node_module() + my_obj.cluster = MockONTAPConnection('node_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.rename_node() + assert 'Error renaming node: ' in exc.value.args[0]['msg'] + data = self.set_default_args(use_rest='Never') + data['location'] = 'myloc1' + set_module_args(data) + my_obj1 = node_module() + my_obj1.cluster = MockONTAPConnection('node_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj1.modify_node() + assert 'Error modifying node: ' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_modify_rest(self, mock_request): + data = self.set_default_args() + data['from_name'] = 'node2' + data['location'] = 'mylocnew' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['node_record'], # get + SRR['empty_good'], # no response for modify + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_node_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_rename_rest(self, mock_request): + data = self.set_default_args() + data['from_name'] = 'node' + data['name'] = 'node2' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['node_record'], # get + SRR['empty_good'], # no response for modify + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_node_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_modify_location_rest(self, mock_request): + data = self.set_default_args() + data['location'] = 'mylocnew' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['node_record'], # get + SRR['empty_good'], # no response for modify + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_node_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntfs_dacl.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntfs_dacl.py new file mode 100644 index 000000000..da8e15ffc --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntfs_dacl.py @@ -0,0 +1,232 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_ntfs_dacl''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntfs_dacl \ + import NetAppOntapNtfsDacl as dacl_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') +HAS_NETAPP_ZAPI_MSG = "pip install netapp_lib is required" + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + request = xml.to_string().decode('utf-8') + if self.kind == 'error': + raise netapp_utils.zapi.NaApiError('test', 'expect error') + elif request.startswith("<ems-autosupport-log>"): + xml = None # or something that may the logger happy, and you don't need @patch anymore + # or + # xml = build_ems_log_response() + elif request.startswith("<file-directory-security-ntfs-dacl-get-iter>"): + if self.kind == 'create': + xml = self.build_dacl_info() + else: + xml = self.build_dacl_info(self.params) + elif request.startswith("<file-directory-security-ntfs-dacl-modify>"): + xml = self.build_dacl_info(self.params) + self.xml_out = xml + return xml + + @staticmethod + def build_dacl_info(data=None): + xml = netapp_utils.zapi.NaElement('xml') + vserver = 'vserver' + attributes = {'num-records': '0', + 'attributes-list': {'file-directory-security-ntfs-dacl': {'vserver': vserver}}} + + if data is not None: + attributes['num-records'] = '1' + if data.get('access_type'): + attributes['attributes-list']['file-directory-security-ntfs-dacl']['access-type'] = data['access_type'] + if data.get('account'): + attributes['attributes-list']['file-directory-security-ntfs-dacl']['account'] = data['account'] + if data.get('rights'): + attributes['attributes-list']['file-directory-security-ntfs-dacl']['rights'] = data['rights'] + if data.get('advanced_rights'): + attributes['attributes-list']['file-directory-security-ntfs-dacl']['advanced-rights'] = data['advanced_rights'] + if data.get('apply_to'): + tmp = [] + for target in data['apply_to']: + tmp.append({'inheritance-level': target}) + attributes['attributes-list']['file-directory-security-ntfs-dacl']['apply-to'] = tmp + if data.get('security_descriptor'): + attributes['attributes-list']['file-directory-security-ntfs-dacl']['ntfs-sd'] = data['security_descriptor'] + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_ntfs_dacl ''' + + def mock_args(self): + return { + 'vserver': 'vserver', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_dacl_mock_object(self, type='zapi', kind=None, status=None): + dacl_obj = dacl_module() + dacl_obj.autosupport_log = Mock(return_value=None) + if type == 'zapi': + if kind is None: + dacl_obj.server = MockONTAPConnection() + else: + dacl_obj.server = MockONTAPConnection(kind=kind, data=status) + return dacl_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + dacl_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_dacl_error(self): + data = self.mock_args() + data['access_type'] = 'allow' + data['account'] = 'acc_test' + data['rights'] = 'full_control' + data['security_descriptor'] = 'sd_test' + data['apply_to'] = 'this_folder,files' + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_dacl_mock_object('zapi', 'error', data).apply() + msg = 'Error fetching allow DACL for account acc_test for security descriptor sd_test: NetApp API failed. Reason - test:expect error' + assert exc.value.args[0]['msg'] == msg + + def test_successfully_create_dacl(self): + data = self.mock_args() + data['access_type'] = 'allow' + data['account'] = 'acc_test' + data['rights'] = 'full_control' + data['security_descriptor'] = 'sd_test' + data['apply_to'] = 'this_folder,files' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_dacl_mock_object('zapi', 'create', data).apply() + assert exc.value.args[0]['changed'] + + def test_create_dacl_idempotency(self): + data = self.mock_args() + data['access_type'] = 'allow' + data['account'] = 'acc_test' + data['rights'] = 'full_control' + data['security_descriptor'] = 'sd_test' + data['apply_to'] = ['this_folder', 'files'] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_dacl_mock_object('zapi', 'create_idempotency', data).apply() + assert not exc.value.args[0]['changed'] + + def test_successfully_modify_dacl(self): + data = self.mock_args() + data['access_type'] = 'allow' + data['account'] = 'acc_test' + data['rights'] = 'full_control' + data['security_descriptor'] = 'sd_test' + data['apply_to'] = ['this_folder', 'files'] + set_module_args(data) + data['advanced_rights'] = 'read_data,write_data' + with pytest.raises(AnsibleExitJson) as exc: + self.get_dacl_mock_object('zapi', 'create', data).apply() + assert exc.value.args[0]['changed'] + + def test_modify_dacl_idempotency(self): + data = self.mock_args() + data['access_type'] = 'allow' + data['account'] = 'acc_test' + data['rights'] = 'full_control' + data['security_descriptor'] = 'sd_test' + data['apply_to'] = ['this_folder', 'files'] + set_module_args(data) + data['rights'] = 'full_control' + with pytest.raises(AnsibleExitJson) as exc: + self.get_dacl_mock_object('zapi', 'modify_idempotency', data).apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntfs_dacl.NetAppOntapNtfsDacl.get_dacl') + def test_modify_error(self, get_info): + data = self.mock_args() + data['access_type'] = 'allow' + data['account'] = 'acc_test' + data['rights'] = 'full_control' + data['security_descriptor'] = 'sd_test' + set_module_args(data) + get_info.side_effect = [ + { + 'access_type': 'allow', + 'account': 'acc_test', + 'security_descriptor': 'sd_test', + 'rights': 'modify', + 'apply_to': ['this_folder', 'files'] + } + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_dacl_mock_object('zapi', 'error', data).apply() + msg = 'Error modifying allow DACL for account acc_test for security descriptor sd_test: NetApp API failed. Reason - test:expect error' + assert exc.value.args[0]['msg'] == msg + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntfs_dacl.NetAppOntapNtfsDacl.get_dacl') + def test_create_error(self, get_info): + data = self.mock_args() + data['access_type'] = 'allow' + data['account'] = 'acc_test' + data['rights'] = 'full_control' + data['security_descriptor'] = 'sd_test' + set_module_args(data) + get_info.side_effect = [ + None + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_dacl_mock_object('zapi', 'error', data).apply() + msg = 'Error adding allow DACL for account acc_test for security descriptor sd_test: NetApp API failed. Reason - test:expect error' + assert exc.value.args[0]['msg'] == msg + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntfs_dacl.NetAppOntapNtfsDacl.get_dacl') + def test_delete_error(self, get_info): + data = self.mock_args() + data['access_type'] = 'allow' + data['account'] = 'acc_test' + data['rights'] = 'full_control' + data['security_descriptor'] = 'sd_test' + data['state'] = 'absent' + set_module_args(data) + get_info.side_effect = [ + { + 'access_type': 'allow', + 'account': 'acc_test', + 'security_descriptor': 'sd_test', + 'rights': 'modify' + } + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_dacl_mock_object('zapi', 'error', data).apply() + msg = 'Error deleting allow DACL for account acc_test for security descriptor sd_test: NetApp API failed. Reason - test:expect error' + assert exc.value.args[0]['msg'] == msg diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntfs_sd.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntfs_sd.py new file mode 100644 index 000000000..6f1f78b34 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntfs_sd.py @@ -0,0 +1,189 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_ntfs_sd''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntfs_sd \ + import NetAppOntapNtfsSd as sd_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + request = xml.to_string().decode('utf-8') + if request.startswith("<ems-autosupport-log>"): + xml = None # or something that may the logger happy, and you don't need @patch anymore + # or + # xml = build_ems_log_response() + elif self.kind == 'error': + raise netapp_utils.zapi.NaApiError('test', 'expect error') + elif request.startswith("<file-directory-security-ntfs-get-iter>"): + if self.kind == 'create': + xml = self.build_sd_info() + else: + xml = self.build_sd_info(self.params) + elif request.startswith("<file-directory-security-ntfs-modify>"): + xml = self.build_sd_info(self.params) + self.xml_out = xml + return xml + + @staticmethod + def build_sd_info(data=None): + xml = netapp_utils.zapi.NaElement('xml') + vserver = 'vserver' + attributes = {'num-records': 1, + 'attributes-list': {'file-directory-security-ntfs': {'vserver': vserver}}} + if data is not None: + if data.get('name'): + attributes['attributes-list']['file-directory-security-ntfs']['ntfs-sd'] = data['name'] + if data.get('owner'): + attributes['attributes-list']['file-directory-security-ntfs']['owner'] = data['owner'] + if data.get('group'): + attributes['attributes-list']['file-directory-security-ntfs']['group'] = data['group'] + if data.get('control_flags_raw'): + attributes['attributes-list']['file-directory-security-ntfs']['control-flags-raw'] = str(data['control_flags_raw']) + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_ntfs_sd ''' + + def mock_args(self): + return { + 'vserver': 'vserver', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_sd_mock_object(self, type='zapi', kind=None, status=None): + sd_obj = sd_module() + # netapp_utils.ems_log_event = Mock(return_value=None) + if type == 'zapi': + if kind is None: + sd_obj.server = MockONTAPConnection() + else: + sd_obj.server = MockONTAPConnection(kind=kind, data=status) + return sd_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + sd_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_successfully_create_sd(self): + data = self.mock_args() + data['name'] = 'sd_test' + data['owner'] = 'user_test' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_sd_mock_object('zapi', 'create', data).apply() + assert exc.value.args[0]['changed'] + + def test_create_sd_idempotency(self): + data = self.mock_args() + data['name'] = 'sd_test' + data['owner'] = 'user_test' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_sd_mock_object('zapi', 'create_idempotency', data).apply() + assert not exc.value.args[0]['changed'] + + def test_successfully_modify_sd(self): + data = self.mock_args() + data['name'] = 'sd_test' + data['owner'] = 'user_test' + data['control_flags_raw'] = 1 + set_module_args(data) + data['control_flags_raw'] = 2 + with pytest.raises(AnsibleExitJson) as exc: + self.get_sd_mock_object('zapi', 'create', data).apply() + assert exc.value.args[0]['changed'] + + def test_modify_sd_idempotency(self): + data = self.mock_args() + data['name'] = 'sd_test' + data['owner'] = 'user_test' + data['control_flags_raw'] = 2 + set_module_args(data) + data['control_flags_raw'] = 2 + with pytest.raises(AnsibleExitJson) as exc: + self.get_sd_mock_object('zapi', 'modify_idempotency', data).apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntfs_sd.NetAppOntapNtfsSd.get_ntfs_sd') + def test_modify_error(self, get_info): + data = self.mock_args() + data['name'] = 'sd_test' + data['owner'] = 'user_test' + data['control_flags_raw'] = 2 + set_module_args(data) + get_info.side_effect = [ + { + 'name': 'sd_test', + 'control_flags_raw': 1 + } + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_sd_mock_object('zapi', 'error', data).apply() + print(exc) + assert exc.value.args[0]['msg'] == 'Error modifying NTFS security descriptor sd_test: NetApp API failed. Reason - test:expect error' + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntfs_sd.NetAppOntapNtfsSd.get_ntfs_sd') + def test_create_error(self, get_info): + data = self.mock_args() + data['name'] = 'sd_test' + data['owner'] = 'user_test' + set_module_args(data) + get_info.side_effect = [ + None + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_sd_mock_object('zapi', 'error', data).apply() + print(exc) + assert exc.value.args[0]['msg'] == 'Error creating NTFS security descriptor sd_test: NetApp API failed. Reason - test:expect error' + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntfs_sd.NetAppOntapNtfsSd.get_ntfs_sd') + def test_delete_error(self, get_info): + data = self.mock_args() + data['name'] = 'sd_test' + data['owner'] = 'user_test' + data['state'] = 'absent' + set_module_args(data) + get_info.side_effect = [ + { + 'name': 'sd_test', + 'owner': 'user_test' + } + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_sd_mock_object('zapi', 'error', data).apply() + print(exc) + assert exc.value.args[0]['msg'] == 'Error deleting NTFS security descriptor sd_test: NetApp API failed. Reason - test:expect error' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntp.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntp.py new file mode 100644 index 000000000..0632cff98 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntp.py @@ -0,0 +1,143 @@ +# (c) 2018-2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP snmp Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings, set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntp \ + import NetAppOntapNTPServer as my_module, main as uut_main # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def default_args(): + return { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'use_rest': 'always' + } + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'server_record': (200, { + "records": [{ + "server": "0.0.0.0", + "version": "auto", + }], + 'num_records': 1 + }, None), + 'create_server': (200, { + 'job': { + 'uuid': 'fde79888-692a-11ea-80c2-005056b39fe7', + '_links': { + 'self': { + 'href': '/api/cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7'}}} + }, None), + 'job': (200, { + "uuid": "fde79888-692a-11ea-80c2-005056b39fe7", + "state": "success", + "start_time": "2020-02-26T10:35:44-08:00", + "end_time": "2020-02-26T10:47:38-08:00", + "_links": { + "self": { + "href": "/api/cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7" + } + } + }, None) +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments: server_name' + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_get_server_called(mock_request, patch_ansible): + args = dict(default_args()) + args['server_name'] = '0.0.0.0' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['server_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_create_server_called(mock_request, patch_ansible): + args = dict(default_args()) + args['server_name'] = '0.0.0.0' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['create_server'], # create + SRR['job'], # Job + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_server_called(mock_request, patch_ansible): + args = dict(default_args()) + args['server_name'] = '0.0.0.0' + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['server_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntp_key.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntp_key.py new file mode 100644 index 000000000..9e4ed661e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ntp_key.py @@ -0,0 +1,141 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ntp_key \ + import NetAppOntapNTPKey as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'ntp_key': (200, { + "records": [ + { + "id": 1, + "digest_type": "sha1", + "value": "addf120b430021c36c232c99ef8d926aea2acd6b" + }], + "num_records": 1 + }, None), + 'svm_uuid': (200, {"records": [ + { + 'uuid': 'e3cb5c7f-cd20' + }], "num_records": 1}, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' +} + + +def test_get_ntp_key_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster/ntp/keys', SRR['empty_records']) + ]) + module_args = {'id': 1, 'digest_type': 'sha1', 'value': 'test'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_ntp_key() is None + + +def test_get_ntp_key_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster/ntp/keys', SRR['generic_error']) + ]) + module_args = {'id': 1, 'digest_type': 'sha1', 'value': 'test'} + my_module_object = create_module(my_module, DEFAULT_ARGS, module_args) + msg = 'Error fetching key with id 1: calling: cluster/ntp/keys: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_ntp_key, 'fail')['msg'] + + +def test_create_ntp_key(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster/ntp/keys', SRR['empty_records']), + ('POST', 'cluster/ntp/keys', SRR['empty_good']) + ]) + module_args = {'id': 1, 'digest_type': 'sha1', 'value': 'test'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_ntp_key_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('POST', 'cluster/ntp/keys', SRR['generic_error']) + ]) + module_args = {'id': 1, 'digest_type': 'sha1', 'value': 'test'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = expect_and_capture_ansible_exception(my_obj.create_ntp_key, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating key with id 1: calling: cluster/ntp/keys: got Expected error.' == error + + +def test_delete_ntp_key(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster/ntp/keys', SRR['ntp_key']), + ('DELETE', 'cluster/ntp/keys/1', SRR['empty_good']) + ]) + module_args = {'state': 'absent', 'id': 1, 'digest_type': 'sha1', 'value': 'test'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_ntp_key_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('DELETE', 'cluster/ntp/keys/1', SRR['generic_error']) + ]) + module_args = {'id': 1, 'digest_type': 'sha1', 'value': 'test', 'state': 'absent'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = expect_and_capture_ansible_exception(my_obj.delete_ntp_key, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error deleting key with id 1: calling: cluster/ntp/keys/1: got Expected error.' == error + + +def test_modify_ntp_key(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster/ntp/keys', SRR['ntp_key']), + ('PATCH', 'cluster/ntp/keys/1', SRR['empty_good']) + ]) + module_args = {'id': 1, 'digest_type': 'sha1', 'value': 'test2'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_ntp_key_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/ntp/keys', SRR['ntp_key']), + ('PATCH', 'cluster/ntp/keys/1', SRR['generic_error']) + ]) + module_args = {'id': 1, 'digest_type': 'sha1', 'value': 'test2'} + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + assert 'Error modifying key with id 1: calling: cluster/ntp/keys/1: got Expected error.' == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme.py new file mode 100644 index 000000000..b24c0e289 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme.py @@ -0,0 +1,185 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_nvme''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme \ + import NetAppONTAPNVMe as my_module + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'nvme': + xml = self.build_nvme_info() + elif self.type == 'nvme_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_nvme_info(): + ''' build xml data for nvme-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': [{'nvme-target-service-info': {'is-available': 'true'}}]} + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self): + if self.onbox: + hostname = '10.193.75.3' + username = 'admin' + password = 'netapp1!' + vserver = 'ansible' + status_admin = True + else: + hostname = 'hostname' + username = 'username' + password = 'password' + vserver = 'vserver' + status_admin = True + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'vserver': vserver, + 'status_admin': status_admin, + 'use_rest': 'never', + }) + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called(self): + ''' test get_nvme() for non-existent nvme''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = self.server + assert my_obj.get_nvme() is None + + def test_ensure_get_called_existing(self): + ''' test get_nvme() for existing nvme''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = MockONTAPConnection(kind='nvme') + assert my_obj.get_nvme() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme.NetAppONTAPNVMe.create_nvme') + def test_successful_create(self, create_nvme): + ''' creating nvme and testing idempotency ''' + set_module_args(self.set_default_args()) + my_obj = my_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + create_nvme.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('nvme') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme.NetAppONTAPNVMe.delete_nvme') + def test_successful_delete(self, delete_nvme): + ''' deleting nvme and testing idempotency ''' + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('nvme') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + delete_nvme.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme.NetAppONTAPNVMe.modify_nvme') + def test_successful_modify(self, modify_nvme): + ''' modifying nvme and testing idempotency ''' + data = self.set_default_args() + data['status_admin'] = False + set_module_args(data) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('nvme') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + modify_nvme.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + data = self.set_default_args() + set_module_args(data) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('nvme') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('nvme_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.get_nvme() + assert 'Error fetching nvme info:' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_nvme() + assert 'Error creating nvme' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_nvme() + assert 'Error deleting nvme' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_nvme() + assert 'Error modifying nvme' in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_namespace.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_namespace.py new file mode 100644 index 000000000..b70b9fd10 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_namespace.py @@ -0,0 +1,168 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_nvme_namespace''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme_namespace \ + import NetAppONTAPNVMENamespace as my_module + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'namespace': + xml = self.build_namespace_info() + elif self.type == 'quota_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_namespace_info(): + ''' build xml data for namespace-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 2, + 'attributes-list': [{'nvme-namespace-info': {'path': 'abcd/vol'}}, + {'nvme-namespace-info': {'path': 'xyz/vol'}}]} + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self): + if self.onbox: + hostname = '10.193.75.3' + username = 'admin' + password = 'netapp1!' + vserver = 'ansible' + ostype = 'linux' + path = 'abcd/vol' + size = 20 + size_unit = 'mb' + else: + hostname = 'hostname' + username = 'username' + password = 'password' + vserver = 'vserver' + ostype = 'linux' + path = 'abcd/vol' + size = 20 + size_unit = 'mb' + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'ostype': ostype, + 'vserver': vserver, + 'path': path, + 'size': size, + 'size_unit': size_unit, + 'use_rest': 'never', + }) + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called(self): + ''' test get_namespace() for non-existent namespace''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = self.server + assert my_obj.get_namespace() is None + + def test_ensure_get_called_existing(self): + ''' test get_namespace() for existing namespace''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = MockONTAPConnection(kind='namespace') + assert my_obj.get_namespace() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme_namespace.NetAppONTAPNVMENamespace.create_namespace') + def test_successful_create(self, create_namespace): + ''' creating namespace and testing idempotency ''' + set_module_args(self.set_default_args()) + my_obj = my_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + create_namespace.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('namespace') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme_namespace.NetAppONTAPNVMENamespace.delete_namespace') + def test_successful_delete(self, delete_namespace): + ''' deleting namespace and testing idempotency ''' + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('namespace') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + delete_namespace.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('quota_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.get_namespace() + assert 'Error fetching namespace info:' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_namespace() + assert 'Error creating namespace for path' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_namespace() + assert 'Error deleting namespace for path' in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_namespace_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_namespace_rest.py new file mode 100644 index 000000000..648caaf87 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_namespace_rest.py @@ -0,0 +1,121 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_aggregate when using REST """ + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme_namespace \ + import NetAppONTAPNVMENamespace as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'nvme_namespace': (200, { + "name": "/vol/test/disk1", + "uuid": "81068ae6-4674-4d78-a8b7-dadb23f67edf", + "svm": { + "name": "ansibleSVM" + }, + "enabled": True + }, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'vserver': 'ansibleSVM', + 'ostype': 'linux', + 'path': '/vol/test/disk1', + 'size': 10, + 'size_unit': 'mb', + 'block_size': 4096 +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "path", "vserver"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_get_namespace_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/namespaces', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_namespace_rest() is None + + +def test_get_namespace_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/namespaces', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching namespace info for vserver: ansibleSVM' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_namespace_rest, 'fail')['msg'] + + +def test_create_namespace(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/namespaces', SRR['empty_records']), + ('POST', 'storage/namespaces', SRR['empty_good']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_create_namespace_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('POST', 'storage/namespaces', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.create_namespace_rest, 'fail')['msg'] + msg = 'Error creating namespace for vserver ansibleSVM: calling: storage/namespaces: got Expected error.' + assert msg == error + + +def test_delete_namespace(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/namespaces', SRR['nvme_namespace']), + ('DELETE', 'storage/namespaces/81068ae6-4674-4d78-a8b7-dadb23f67edf', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_namespace_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('DELETE', 'storage/namespaces/81068ae6-4674-4d78-a8b7-dadb23f67edf', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.namespace_uuid = '81068ae6-4674-4d78-a8b7-dadb23f67edf' + error = expect_and_capture_ansible_exception(my_obj.delete_namespace_rest, 'fail')['msg'] + msg = 'Error deleting namespace for vserver ansibleSVM: calling: storage/namespaces/81068ae6-4674-4d78-a8b7-dadb23f67edf: got Expected error.' + assert msg == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_rest.py new file mode 100644 index 000000000..db23a8e72 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_rest.py @@ -0,0 +1,131 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_aggregate when using REST """ + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme \ + import NetAppONTAPNVMe as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'nvme_service': (200, { + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "enabled": True + }, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'vserver': 'svm1' +} + + +def test_get_nvme_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/services', SRR['empty_records']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + assert my_obj.get_nvme_rest() is None + + +def test_get_nvme_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/services', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching nvme info for vserver: svm1' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_nvme_rest, 'fail')['msg'] + + +def test_create_nvme(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/services', SRR['empty_records']), + ('POST', 'protocols/nvme/services', SRR['empty_good']) + ]) + module_args = {'status_admin': True} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_nvme_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('POST', 'protocols/nvme/services', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['status_admin'] = True + error = expect_and_capture_ansible_exception(my_obj.create_nvme_rest, 'fail')['msg'] + msg = 'Error creating nvme for vserver svm1: calling: protocols/nvme/services: got Expected error.' + assert msg == error + + +def test_delete_nvme(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/services', SRR['nvme_service']), + ('PATCH', 'protocols/nvme/services/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['empty_good']), + ('DELETE', 'protocols/nvme/services/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_nvme_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('DELETE', 'protocols/nvme/services/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.svm_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + error = expect_and_capture_ansible_exception(my_obj.delete_nvme_rest, 'fail')['msg'] + msg = 'Error deleting nvme for vserver svm1: calling: protocols/nvme/services/02c9e252-41be-11e9-81d5-00a0986138f7: got Expected error.' + assert msg == error + + +def test_modify_nvme(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/services', SRR['nvme_service']), + ('PATCH', 'protocols/nvme/services/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['empty_good']) + ]) + module_args = {'status_admin': False} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_nvme_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('PATCH', 'protocols/nvme/services/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['status_admin'] = False + my_obj.svm_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + error = expect_and_capture_ansible_exception(my_obj.modify_nvme_rest, 'fail', {'status': False})['msg'] + msg = 'Error modifying nvme for vserver: svm1' + assert msg == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_subsystem.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_subsystem.py new file mode 100644 index 000000000..0e6acdbec --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_subsystem.py @@ -0,0 +1,225 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_nvme_subsystem ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + expect_and_capture_ansible_exception, call_main, create_module, patch_ansible +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme_subsystem import NetAppONTAPNVMESubsystem as my_module, main as my_main + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +subsystem_info = { + 'attributes-list': [{'nvme-target-subsystem-map-info': {'path': 'abcd/vol'}}, + {'nvme-target-subsystem-map-info': {'path': 'xyz/vol'}}]} + +subsystem_info_one_path = { + 'attributes-list': [{'nvme-target-subsystem-map-info': {'path': 'abcd/vol'}}]} + +subsystem_info_one_host = { + 'attributes-list': [{'nvme-target-subsystem-map-info': {'host-nqn': 'host-nqn'}}]} + +ZRR = zapi_responses({ + 'subsystem_info': build_zapi_response(subsystem_info, 2), + 'subsystem_info_one_path': build_zapi_response(subsystem_info_one_path, 1), + 'subsystem_info_one_host': build_zapi_response(subsystem_info_one_host, 1), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'never', + 'subsystem': 'subsystem', + 'vserver': 'vserver', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + args = dict(DEFAULT_ARGS) + args.pop('subsystem') + error = 'missing required arguments: subsystem' + assert error in call_main(my_main, args, fail=True)['msg'] + + +def test_ensure_get_called(): + ''' test get_subsystem() for non-existent subsystem''' + register_responses([ + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_subsystem() is None + + +def test_ensure_get_called_existing(): + ''' test get_subsystem() for existing subsystem''' + register_responses([ + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['subsystem_info']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_subsystem() + + +def test_successful_create(): + ''' creating subsystem and testing idempotency ''' + register_responses([ + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['no_records']), + ('ZAPI', 'nvme-subsystem-create', ZRR['success']), + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['subsystem_info']), + # idempptency + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['subsystem_info']), + ]) + module_args = { + 'use_rest': 'never', + 'ostype': 'windows' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_delete(): + ''' deleting subsystem and testing idempotency ''' + register_responses([ + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['subsystem_info']), + ('ZAPI', 'nvme-subsystem-delete', ZRR['success']), + # idempptency + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + 'state': 'absent', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_get_host_map_called(): + ''' test get_subsystem_host_map() for non-existent subsystem''' + register_responses([ + ('ZAPI', 'nvme-subsystem-map-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_subsystem_host_map('paths') is None + + +def test_ensure_get_host_map_called_existing(): + ''' test get_subsystem_host_map() for existing subsystem''' + register_responses([ + ('ZAPI', 'nvme-subsystem-map-get-iter', ZRR['subsystem_info']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_subsystem_host_map('paths') + + +def test_successful_add(): + ''' adding subsystem host/map and testing idempotency ''' + register_responses([ + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['subsystem_info']), + ('ZAPI', 'nvme-subsystem-host-get-iter', ZRR['no_records']), + ('ZAPI', 'nvme-subsystem-map-get-iter', ZRR['no_records']), + ('ZAPI', 'nvme-subsystem-host-add', ZRR['success']), + ('ZAPI', 'nvme-subsystem-map-add', ZRR['success']), + ('ZAPI', 'nvme-subsystem-map-add', ZRR['success']), + # idempptency + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['subsystem_info']), + ('ZAPI', 'nvme-subsystem-host-get-iter', ZRR['subsystem_info_one_host']), + ('ZAPI', 'nvme-subsystem-map-get-iter', ZRR['subsystem_info']), + ]) + module_args = { + 'use_rest': 'never', + 'ostype': 'windows', + 'paths': ['abcd/vol', 'xyz/vol'], + 'hosts': 'host-nqn' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_remove(): + ''' removing subsystem host/map and testing idempotency ''' + register_responses([ + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['subsystem_info']), + ('ZAPI', 'nvme-subsystem-map-get-iter', ZRR['subsystem_info']), + ('ZAPI', 'nvme-subsystem-map-remove', ZRR['success']), + # idempptency + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['subsystem_info_one_path']), + ('ZAPI', 'nvme-subsystem-map-get-iter', ZRR['subsystem_info_one_path']), + ]) + module_args = { + 'use_rest': 'never', + 'paths': ['abcd/vol'], + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + module_args = { + "use_rest": "never" + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_error_handling(): + ''' test error handling on ZAPI calls ''' + register_responses([ + ('ZAPI', 'nvme-subsystem-get-iter', ZRR['error']), + ('ZAPI', 'nvme-subsystem-create', ZRR['error']), + ('ZAPI', 'nvme-subsystem-delete', ZRR['error']), + ('ZAPI', 'nvme-subsystem-map-get-iter', ZRR['error']), + ('ZAPI', 'nvme-subsystem-host-add', ZRR['error']), + ('ZAPI', 'nvme-subsystem-map-add', ZRR['error']), + ('ZAPI', 'nvme-subsystem-host-remove', ZRR['error']), + ('ZAPI', 'nvme-subsystem-map-remove', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'ostype': 'windows' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = zapi_error_message('Error fetching subsystem info') + assert error in expect_and_capture_ansible_exception(my_obj.get_subsystem, 'fail')['msg'] + error = zapi_error_message('Error creating subsystem for subsystem') + assert error in expect_and_capture_ansible_exception(my_obj.create_subsystem, 'fail')['msg'] + error = zapi_error_message('Error deleting subsystem for subsystem') + assert error in expect_and_capture_ansible_exception(my_obj.delete_subsystem, 'fail')['msg'] + error = zapi_error_message('Error fetching subsystem path info') + assert error in expect_and_capture_ansible_exception(my_obj.get_subsystem_host_map, 'fail', 'paths')['msg'] + error = zapi_error_message('Error adding hostname for subsystem subsystem') + assert error in expect_and_capture_ansible_exception(my_obj.add_subsystem_host_map, 'fail', ['hostname'], 'hosts')['msg'] + error = zapi_error_message('Error adding pathname for subsystem subsystem') + assert error in expect_and_capture_ansible_exception(my_obj.add_subsystem_host_map, 'fail', ['pathname'], 'paths')['msg'] + error = zapi_error_message('Error removing hostname for subsystem subsystem') + assert error in expect_and_capture_ansible_exception(my_obj.remove_subsystem_host_map, 'fail', ['hostname'], 'hosts')['msg'] + error = zapi_error_message('Error removing pathname for subsystem subsystem') + assert error in expect_and_capture_ansible_exception(my_obj.remove_subsystem_host_map, 'fail', ['pathname'], 'paths')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_subsystem_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_subsystem_rest.py new file mode 100644 index 000000000..693816662 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_nvme_subsystem_rest.py @@ -0,0 +1,256 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_nvme_subsystem when using REST """ + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + patch_ansible, call_main, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_nvme_subsystem\ + import NetAppONTAPNVMESubsystem as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'nvme_subsystem': (200, { + "hosts": [{ + "nqn": "nqn.1992-08.com.netapp:sn.f2207584d03611eca164005056b3bd39:subsystem.test3" + }], + "name": "subsystem1", + "uuid": "81068ae6-4674-4d78-a8b7-dadb23f67edf", + "comment": "string", + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "os_type": "hyper_v", + "subsystem_maps": [{ + "namespace": { + "name": "/vol/test3/disk1", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + }], + "enabled": True, + }, None), + # 'nvme_host': (200, [{ + # "nqn": "nqn.1992-08.com.netapp:sn.f2207584d03611eca164005056b3bd39:subsystem.test3" + # }], None), + 'nvme_map': (200, { + "records": [{ + "namespace": { + "name": "/vol/test3/disk1", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412", + }, + }], "num_records": 1, + }, None), + + 'nvme_host': (200, { + "records": [ + { + "nqn": "nqn.1992-08.com.netapp:sn.f2207584d03611eca164005056b3bd39:subsystem.test3", + "subsystem": { + "uuid": "81068ae6-4674-4d78-a8b7-dadb23f67edf" + } + } + ], + "num_records": 1 + }, None), + + 'error_svm_not_found': (400, None, 'SVM "ansibleSVM" does not exist.') +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'vserver': 'ansibleSVM', + 'ostype': 'linux', + 'subsystem': 'subsystem1', +} + + +def test_get_subsystem_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['empty_records']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + assert my_module_object.get_subsystem_rest() is None + + +def test_get_subsystem_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['generic_error']), + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching subsystem info for vserver: ansibleSVM' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_subsystem_rest, 'fail')['msg'] + + +def test_create_subsystem(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['empty_records']), + ('POST', 'protocols/nvme/subsystems', SRR['empty_good']), + ('GET', 'protocols/nvme/subsystems', SRR['nvme_subsystem']), + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_create_subsystem_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('POST', 'protocols/nvme/subsystems', SRR['generic_error']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['zero_records']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + error = 'Error creating subsystem for vserver ansibleSVM: calling: protocols/nvme/subsystems: got Expected error.' + assert error in expect_and_capture_ansible_exception(my_obj.create_subsystem_rest, 'fail')['msg'] + args = dict(DEFAULT_ARGS) + del args['ostype'] + error = "Error: Missing required parameter 'ostype' for creating subsystem" + assert error in call_main(my_main, args, fail=True)['msg'] + + +def test_delete_subsystem(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['nvme_subsystem']), + ('DELETE', 'protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_subsystem_no_vserver(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['error_svm_not_found']), + ]) + module_args = {'state': 'absent'} + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_subsystem_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('DELETE', 'protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.subsystem_uuid = '81068ae6-4674-4d78-a8b7-dadb23f67edf' + error = expect_and_capture_ansible_exception(my_obj.delete_subsystem_rest, 'fail')['msg'] + msg = 'Error deleting subsystem for vserver ansibleSVM: calling: protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf: got Expected error.' + assert msg == error + + +def test_add_subsystem_host(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['empty_records']), + ('POST', 'protocols/nvme/subsystems', SRR['empty_good']), + ('GET', 'protocols/nvme/subsystems', SRR['nvme_subsystem']), + ('POST', 'protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf/hosts', SRR['empty_good']) + ]) + module_args = {'hosts': ['nqn.1992-08.com.netapp:sn.f2207584d03611eca164005056b3bd39:subsystem.test3']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_add_only_subsystem_host(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['nvme_subsystem']), + ('GET', 'protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf/hosts', SRR['empty_records']), + ('POST', 'protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf/hosts', SRR['empty_good']) + ]) + module_args = {'hosts': ['nqn.1992-08.com.netapp:sn.f2207584d03611eca164005056b3bd39:subsystem.test3']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_add_subsystem_map(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['empty_records']), + ('POST', 'protocols/nvme/subsystems', SRR['empty_good']), + ('GET', 'protocols/nvme/subsystems', SRR['nvme_subsystem']), + ('POST', 'protocols/nvme/subsystem-maps', SRR['empty_good']) + ]) + module_args = {'paths': ['/vol/test3/disk1']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_add_only_subsystem_map(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['nvme_subsystem']), + ('GET', 'protocols/nvme/subsystem-maps', SRR['empty_records']), + ('POST', 'protocols/nvme/subsystem-maps', SRR['empty_good']) + ]) + module_args = {'paths': ['/vol/test3/disk1']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_remove_only_subsystem_host(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['nvme_subsystem']), + ('GET', 'protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf/hosts', SRR['nvme_host']), + ('POST', 'protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf/hosts', SRR['empty_good']), + ('DELETE', 'protocols/nvme/subsystems/81068ae6-4674-4d78-a8b7-dadb23f67edf/' + 'hosts/nqn.1992-08.com.netapp:sn.f2207584d03611eca164005056b3bd39:subsystem.test3', SRR['empty_good']) + ]) + module_args = {'hosts': ['nqn.1992-08.com.netapp:sn.f2207584d03611eca164005056b3bd39:subsystem.test']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_remove_only_subsystem_map(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems', SRR['nvme_subsystem']), + ('GET', 'protocols/nvme/subsystem-maps', SRR['nvme_map']), + ('POST', 'protocols/nvme/subsystem-maps', SRR['empty_good']), + ('DELETE', 'protocols/nvme/subsystem-maps/81068ae6-4674-4d78-a8b7-dadb23f67edf/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_good']) + ]) + module_args = {'paths': ['/vol/test2/disk1']} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_errors(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'protocols/nvme/subsystems/None/hosts', SRR['generic_error']), + ('GET', 'protocols/nvme/subsystem-maps', SRR['generic_error']), + ('POST', 'protocols/nvme/subsystems/None/hosts', SRR['generic_error']), + ('POST', 'protocols/nvme/subsystem-maps', SRR['generic_error']), + ('DELETE', 'protocols/nvme/subsystems/None/hosts/host', SRR['generic_error']), + ('DELETE', 'protocols/nvme/subsystem-maps/None/None', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + error = rest_error_message('Error fetching subsystem host info for vserver: ansibleSVM', 'protocols/nvme/subsystems/None/hosts') + assert error in expect_and_capture_ansible_exception(my_module_object.get_subsystem_host_map_rest, 'fail', 'hosts')['msg'] + error = rest_error_message('Error fetching subsystem map info for vserver: ansibleSVM', 'protocols/nvme/subsystem-maps') + assert error in expect_and_capture_ansible_exception(my_module_object.get_subsystem_host_map_rest, 'fail', 'paths')['msg'] + error = rest_error_message('Error adding [] for subsystem subsystem1', 'protocols/nvme/subsystems/None/hosts') + assert error in expect_and_capture_ansible_exception(my_module_object.add_subsystem_host_map_rest, 'fail', [], 'hosts')['msg'] + error = rest_error_message('Error adding path for subsystem subsystem1', 'protocols/nvme/subsystem-maps') + assert error in expect_and_capture_ansible_exception(my_module_object.add_subsystem_host_map_rest, 'fail', ['path'], 'paths')['msg'] + error = rest_error_message('Error removing host for subsystem subsystem1', 'protocols/nvme/subsystems/None/hosts/host') + assert error in expect_and_capture_ansible_exception(my_module_object.remove_subsystem_host_map_rest, 'fail', ['host'], 'hosts')['msg'] + error = rest_error_message('Error removing path for subsystem subsystem1', 'protocols/nvme/subsystem-maps/None/None') + assert error in expect_and_capture_ansible_exception(my_module_object.remove_subsystem_host_map_rest, 'fail', ['path'], 'paths')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_object_store.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_object_store.py new file mode 100644 index 000000000..91430883c --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_object_store.py @@ -0,0 +1,538 @@ +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_object_store """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_object_store \ + import NetAppOntapObjectStoreConfig as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_uuid': (200, {'records': [{'uuid': 'ansible'}]}, None), + 'get_object_store': (200, + {'uuid': 'ansible', + 'name': 'ansible', + 'provider_type': 'abc', + 'access_key': 'abc', + 'owner': 'fabricpool' + }, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + print('IN:', xml.to_string()) + if self.type == 'object_store': + xml = self.build_object_store_info() + elif self.type == 'object_store_not_found': + self.type = 'object_store' + raise netapp_utils.zapi.NaApiError(code='15661', message="This exception is from the unit test - 15661") + elif self.type == 'object_store_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + print('OUT:', xml.to_string()) + return xml + + @staticmethod + def build_object_store_info(): + ''' build xml data for object store ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'attributes': + {'aggr-object-store-config-info': + {'object-store-name': 'ansible', + 'provider-type': 'abc', + 'access-key': 'abc', + 'server': 'abc', + 's3-name': 'abc', + 'ssl-enabled': 'true', + 'port': '1234', + 'is-certificate-validation-enabled': 'true'} + } + } + xml.translate_struct(data) + print(xml.to_string()) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + # whether to use a mock or a simulator + self.onbox = False + + def set_default_args(self): + if self.onbox: + hostname = '10.10.10.10' + username = 'admin' + password = 'password' + name = 'ansible' + else: + hostname = 'hostname' + username = 'username' + password = 'password' + name = 'ansible' + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'name': name, + 'feature_flags': {'no_cserver_ems': True} + }) + + def call_command(self, module_args): + ''' utility function to call apply ''' + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + # mock the connection + my_obj.server = MockONTAPConnection('object_store') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + return exc.value.args[0]['changed'] + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_negative_netapp_lib(self, mock_request, mock_has_netapp_lib): + ''' fetching details of object store ''' + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + mock_has_netapp_lib.return_value = False + set_module_args(self.set_default_args()) + with pytest.raises(AnsibleFailJson) as exc: + my_main() + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_ensure_object_store_get_called(self, mock_request): + ''' fetching details of object store ''' + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = self.server + assert my_obj.get_aggr_object_store() is None + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_ensure_get_called_existing(self, mock_request): + ''' test for existing object store''' + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = MockONTAPConnection(kind='object_store') + object_store = my_obj.get_aggr_object_store() + assert object_store + assert 'name' in object_store + assert object_store['name'] == 'ansible' + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_object_store_create(self, mock_request): + ''' test for creating object store''' + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + module_args = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc', + 'access_key': 'abc', + 'secret_password': 'abc', + 'port': 1234, + 'certificate_validation_enabled': True, + 'ssl_enabled': True + } + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.onbox: + # mock the connection + my_obj.server = MockONTAPConnection(kind='object_store_not_found') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_object_store_negative_create_bad_owner(self, mock_request): + ''' test for creating object store''' + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + module_args = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc', + 'access_key': 'abc', + 'secret_password': 'abc', + 'port': 1234, + 'certificate_validation_enabled': True, + 'ssl_enabled': True, + 'owner': 'snapmirror' + } + module_args.update(self.set_default_args()) + set_module_args(module_args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + print(exc.value.args[0]) + assert exc.value.args[0]['msg'] == 'Error: unsupported value for owner: snapmirror when using ZAPI.' + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_object_store_delete(self, mock_request): + ''' test for deleting object store''' + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + module_args = { + 'state': 'absent', + } + changed = self.call_command(module_args) + assert changed + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_negative_object_store_modify(self, mock_request): + ''' test for modifying object store''' + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + module_args = { + 'provider_type': 'abc', + 'server': 'abc2', + 'container': 'abc', + 'access_key': 'abc2', + 'secret_password': 'abc' + } + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + my_obj.server = MockONTAPConnection(kind='object_store') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = 'Error - modify is not supported with ZAPI' + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + set_module_args(self.set_default_args()) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + assert exc.value.args[0]['msg'] == 'Error calling: cloud/targets: got %s.' % SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_create(self, mock_request): + data = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc', + 'access_key': 'abc', + 'secret_password': 'abc', + 'port': 1234, + 'certificate_validation_enabled': True, + 'ssl_enabled': True + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_negative_create_missing_parameter(self, mock_request): + data = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc', + 'secret_password': 'abc', + 'port': 1234, + 'certificate_validation_enabled': True, + 'ssl_enabled': True + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = 'Error provisioning object store ansible: one of the following parameters are missing' + assert msg in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_negative_create_api_error(self, mock_request): + data = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc', + 'access_key': 'abc', + 'secret_password': 'abc', + 'port': 1234, + 'certificate_validation_enabled': True, + 'ssl_enabled': True + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_main() + assert exc.value.args[0]['msg'] == 'Error calling: cloud/targets: got %s.' % SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_modify(self, mock_request): + data = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc2', + 'access_key': 'abc2', + 'secret_password': 'abc' + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_object_store'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_negative_modify_rest_error(self, mock_request): + data = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc2', + 'access_key': 'abc2', + 'secret_password': 'abc' + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_object_store'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + assert exc.value.args[0]['msg'] == 'Error calling: cloud/targets/ansible: got %s.' % SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_negative_modify_owner(self, mock_request): + data = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc2', + 'access_key': 'abc2', + 'secret_password': 'abc', + 'owner': 'snapmirror' + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_object_store'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + assert exc.value.args[0]['msg'] == 'Error modifying object store, owner cannot be changed. Found: snapmirror.' + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_modify_password(self, mock_request): + data = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc', + 'access_key': 'abc', + 'secret_password': 'abc2', + 'change_password': True + + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_object_store'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert exc.value.args[0]['changed'] + assert 'secret_password' in exc.value.args[0]['modify'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_idempotent(self, mock_request): + data = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc', + 'access_key': 'abc', + 'secret_password': 'abc2' + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_object_store'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print(mock_request.mock_calls) + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_delete(self, mock_request): + data = { + 'state': 'absent', + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['get_object_store'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_negative_delete(self, mock_request): + data = { + 'state': 'absent', + } + data.update(self.set_default_args()) + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_object_store'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + assert exc.value.args[0]['msg'] == 'Error calling: cloud/targets/ansible: got %s.' % SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_if_all_methods_catch_exception(self, mock_request): + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + module_args = { + 'provider_type': 'abc', + 'server': 'abc', + 'container': 'abc', + 'access_key': 'abc', + 'secret_password': 'abc' + } + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('object_store_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.get_aggr_object_store() + assert '' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_aggr_object_store(None) + assert 'Error provisioning object store config ' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_aggr_object_store() + assert 'Error removing object store config ' in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_partitions.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_partitions.py new file mode 100644 index 000000000..975ffb161 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_partitions.py @@ -0,0 +1,515 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP disks Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_partitions \ + import NetAppOntapPartitions as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def default_args(): + args = { + 'disk_type': 'SAS', + 'partitioning_method': 'root_data', + 'partition_type': 'data', + 'partition_count': 13, + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'node': 'node1', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'owned_partitions_record': (200, { + "records": [ + { + "partition": "1.0.0.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "fas2552-rtp-13-02", + "owner_node_name": "fas2552-rtp-13-02" + }, + { + "partition": "1.0.2.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "fas2552-rtp-13-02", + "owner_node_name": "fas2552-rtp-13-02" + }, + { + "partition": "1.0.4.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "fas2552-rtp-13-02", + "owner_node_name": "fas2552-rtp-13-02" + } + ], + "num_records": 3 + }, None), + + 'unassigned_partitions_record': (200, { + "records": [ + { + "partition": "1.0.25.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "fas2552-rtp-13-02", + "owner_node_name": "fas2552-rtp-13-02" + }, + { + "partition": "1.0.27.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "fas2552-rtp-13-02", + "owner_node_name": "fas2552-rtp-13-02" + }, + ], + "num_records": 2 + }, None), + + 'unassigned_disks_record': (200, { + "records": [ + { + 'name': '1.0.27', + 'type': 'sas', + 'container_type': 'unassigned', + 'home_node': {'name': 'node1'}}, + { + 'name': '1.0.28', + 'type': 'sas', + 'container_type': 'unassigned', + 'home_node': {'name': 'node1'}} + ], + 'num_records': 2}, None), + + 'home_spare_disk_info_record': (200, { + 'records': [], + 'num_records': 2}, None), + + 'spare_partitions_record': (200, { + "records": [ + { + "partition": "1.0.0.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "fas2552-rtp-13-02", + "owner_node_name": "fas2552-rtp-13-02" + }, + { + "partition": "1.0.1.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "fas2552-rtp-13-02", + "owner_node_name": "fas2552-rtp-13-02" + } + ], 'num_records': 2 + }, None), + + 'partner_spare_partitions_record': (200, { + "records": [ + { + "partition": "1.0.1.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "node2", + "owner_node_name": "node2" + }, + { + "partition": "1.0.3.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "node2", + "owner_node_name": "node2" + }, + { + "partition": "1.0.5.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "node2", + "owner_node_name": "node2" + }, + { + "partition": "1.0.23.P1", + "container_type": "spare", + "partitioning_method": "root_data", + "is_root": False, + "disk_type": "sas", + "home_node_name": "node2", + "owner_node_name": "node2" + } + ], "num_records": 4 + }, None), + + 'partner_node_name_record': (200, { + 'records': [ + { + 'uuid': 'c345c182-a6a0-11eb-af7b-00a0984839de', + 'name': 'node2', + 'ha': { + 'partners': [ + {'name': 'node1'} + ] + } + } + ], 'num_records': 1 + }, None), + + 'partner_spare_disks_record': (200, { + 'records': [ + { + 'name': '1.0.22', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + }, + { + 'name': '1.0.20', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + }, + { + 'name': '1.0.18', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + }, + { + 'name': '1.0.16', + 'type': 'sas', + 'container_type': 'spare', + 'home_node': {'name': 'node2'} + } + ], 'num_records': 4 + }, None), + + 'adp2_owned_partitions_record': (200, { + "records": [ + { + "partition": "1.0.0.P1", + "container_type": "spare", + "partitioning_method": "root_data1_data2", + "is_root": False, + "disk_type": "ssd", + "home_node_name": "aff300-rtp-2b", + "owner_node_name": "aff300-rtp-2b" + }, + { + "partition": "1.0.1.P1", + "container_type": "spare", + "partitioning_method": "root_data1_data2", + "is_root": False, + "disk_type": "ssd", + "home_node_name": "aff300-rtp-2b", + "owner_node_name": "aff300-rtp-2b" + }, + { + "partition": "1.0.23.P1", + "container_type": "spare", + "partitioning_method": "root_data1_data2", + "is_root": False, + "disk_type": "ssd", + "home_node_name": "aff300-rtp-2b", + "owner_node_name": "aff300-rtp-2b" + } + ], "num_records": 3 + }, None), +} + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument ##WHAT DOES THIS METHOD DO + ''' create scope ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +# get unassigned partitions +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_assign_unassigned_disks(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['partition_count'] = 5 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['owned_partitions_record'], + SRR['home_spare_disk_info_record'], + SRR['unassigned_partitions_record'], + SRR['unassigned_disks_record'], + SRR['partner_node_name_record'], + SRR['partner_spare_partitions_record'], + SRR['partner_spare_disks_record'], + SRR['empty_good'], # assign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 6 + + +# assign unassigned partitions + steal 2 partner spare partitions +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_assign_unassigned_and_partner_spare_disks(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['partition_count'] = 7 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['owned_partitions_record'], + SRR['home_spare_disk_info_record'], + SRR['unassigned_partitions_record'], + SRR['unassigned_disks_record'], + SRR['partner_node_name_record'], + SRR['partner_spare_partitions_record'], + SRR['partner_spare_disks_record'], + SRR['empty_good'], # unassign + SRR['empty_good'], # assign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 9 + + +# assign unassigned partitions + steal 2 partner spare partitions +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_assign_unassigned_and_partner_spare_partitions_and_disks(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['partition_count'] = 6 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['owned_partitions_record'], + SRR['home_spare_disk_info_record'], + SRR['unassigned_partitions_record'], + SRR['unassigned_disks_record'], + SRR['partner_node_name_record'], + SRR['partner_spare_partitions_record'], + SRR['partner_spare_disks_record'], + SRR['empty_good'], # unassign + SRR['empty_good'], # assign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 8 + + +# Should unassign partitions +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_unassign(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['partition_count'] = 2 # change this number to be less than currently assigned partions to the node + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['owned_partitions_record'], + SRR['unassigned_partitions_record'], + SRR['spare_partitions_record'], + SRR['empty_good'], # unassign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 5 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' disk_count matches arguments, do nothing ''' + args = dict(default_args()) + args['partition_count'] = 3 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['owned_partitions_record'], + SRR['unassigned_partitions_record'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +# ADP2 +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_assign_unassigned_disks_adp2(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['partitioning_method'] = 'root_data1_data2' + args['partition_type'] = 'data1' + args['partition_count'] = 5 # change this dependant on data1 partitions + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['adp2_owned_partitions_record'], + SRR['home_spare_disk_info_record'], + SRR['unassigned_partitions_record'], + SRR['unassigned_disks_record'], + SRR['partner_node_name_record'], + SRR['partner_spare_partitions_record'], + SRR['partner_spare_disks_record'], + SRR['empty_good'], # assign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 6 + + +# assign unassigned partitions + steal 2 partner spare partitions +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_assign_unassigned_and_partner_spare_disks_adp2(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['partitioning_method'] = 'root_data1_data2' + args['partition_type'] = 'data1' + args['partition_count'] = 7 # data1 partitions + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['adp2_owned_partitions_record'], + SRR['home_spare_disk_info_record'], + SRR['unassigned_partitions_record'], + SRR['unassigned_disks_record'], + SRR['partner_node_name_record'], + SRR['partner_spare_partitions_record'], + SRR['partner_spare_disks_record'], + SRR['empty_good'], # unassign + SRR['empty_good'], # assign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 9 + + +# assign unassigned partitions + steal 2 partner spare partitions +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_assign_unassigned_and_partner_spare_partitions_and_disks_adp2(mock_request, patch_ansible): + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['partitioning_method'] = 'root_data1_data2' + args['partition_type'] = 'data1' + args['partition_count'] = 6 # change this dependant on data1 partitions + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['adp2_owned_partitions_record'], + SRR['home_spare_disk_info_record'], + SRR['unassigned_partitions_record'], + SRR['unassigned_disks_record'], + SRR['partner_node_name_record'], + SRR['partner_spare_partitions_record'], + SRR['partner_spare_disks_record'], + SRR['empty_good'], # unassign + SRR['empty_good'], # assign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 8 + + +# Should unassign partitions +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_unassign_adp2(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Steal disks from partner node and assign them to the requested node ''' + args = dict(default_args()) + args['partitioning_method'] = 'root_data1_data2' + args['partition_type'] = 'data1' + args['partition_count'] = 2 # change this number to be less than currently assigned partions to the node + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['adp2_owned_partitions_record'], + SRR['unassigned_partitions_record'], + SRR['spare_partitions_record'], + SRR['empty_good'], # unassign + # SRR['empty_good'], # assign + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 5 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ports.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ports.py new file mode 100644 index 000000000..8256024ae --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ports.py @@ -0,0 +1,864 @@ +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for ONTAP Ansible module: na_ontap_port''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings, set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports \ + import NetAppOntapPorts as port_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.type = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + self.xml_out = xml + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def mock_args(self, choice): + if choice == 'broadcast_domain': + return { + 'names': ['test_port_1', 'test_port_2'], + 'resource_name': 'test_domain', + 'resource_type': 'broadcast_domain', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' + } + elif choice == 'portset': + return { + 'names': ['test_lif'], + 'resource_name': 'test_portset', + 'resource_type': 'portset', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'vserver': 'test_vserver', + 'use_rest': 'never' + } + + def get_port_mock_object(self): + """ + Helper method to return an na_ontap_port object + """ + port_obj = port_module() + port_obj.server = MockONTAPConnection() + return port_obj + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.add_broadcast_domain_ports') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.get_broadcast_domain_ports') + def test_successfully_add_broadcast_domain_ports(self, get_broadcast_domain_ports, add_broadcast_domain_ports): + ''' Test successful add broadcast domain ports ''' + data = self.mock_args('broadcast_domain') + set_module_args(data) + get_broadcast_domain_ports.side_effect = [ + [] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.add_broadcast_domain_ports') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.get_broadcast_domain_ports') + def test_add_broadcast_domain_ports_idempotency(self, get_broadcast_domain_ports, add_broadcast_domain_ports): + ''' Test add broadcast domain ports idempotency ''' + data = self.mock_args('broadcast_domain') + set_module_args(data) + get_broadcast_domain_ports.side_effect = [ + ['test_port_1', 'test_port_2'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.add_portset_ports') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.portset_get') + def test_successfully_add_portset_ports(self, portset_get, add_portset_ports): + ''' Test successful add portset ports ''' + data = self.mock_args('portset') + set_module_args(data) + portset_get.side_effect = [ + [] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.add_portset_ports') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.portset_get') + def test_add_portset_ports_idempotency(self, portset_get, add_portset_ports): + ''' Test add portset ports idempotency ''' + data = self.mock_args('portset') + set_module_args(data) + portset_get.side_effect = [ + ['test_lif'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.add_broadcast_domain_ports') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.get_broadcast_domain_ports') + def test_successfully_remove_broadcast_domain_ports(self, get_broadcast_domain_ports, add_broadcast_domain_ports): + ''' Test successful remove broadcast domain ports ''' + data = self.mock_args('broadcast_domain') + data['state'] = 'absent' + set_module_args(data) + get_broadcast_domain_ports.side_effect = [ + ['test_port_1', 'test_port_2'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.add_portset_ports') + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_ports.NetAppOntapPorts.portset_get') + def test_remove_add_portset_ports(self, portset_get, add_portset_ports): + ''' Test successful remove portset ports ''' + data = self.mock_args('portset') + data['state'] = 'absent' + set_module_args(data) + portset_get.side_effect = [ + ['test_lif'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_port_mock_object().apply() + assert exc.value.args[0]['changed'] + + +def default_args(choice=None, resource_name=None, portset_type=None): + args = { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'use_rest': 'always' + } + if choice == 'broadcast_domain': + args['resource_type'] = "broadcast_domain" + args['resource_name'] = "domain2" + args['ipspace'] = "ip1" + args['names'] = ["mohan9cluster2-01:e0b", "mohan9cluster2-01:e0d"] + return args + if choice == 'portset': + args['portset_type'] = portset_type + args['resource_name'] = resource_name + args['resource_type'] = 'portset' + args['vserver'] = 'svm3' + return args + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_9_7': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'port_detail_e0d': (200, { + "num_records": 1, + "records": [ + { + 'name': 'e0d', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': 'ea670505-2ab3-11ec-aa30-005056b3dfc8' + }] + }, None), + 'port_detail_e0a': (200, { + "num_records": 1, + "records": [ + { + 'name': 'e0a', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': 'ea63420b-2ab3-11ec-aa30-005056b3dfc8' + }] + }, None), + 'port_detail_e0b': (200, { + "num_records": 1, + "records": [ + { + 'name': 'e0b', + 'node': {'name': 'mohan9cluster2-01'}, + 'uuid': 'ea64c0f2-2ab3-11ec-aa30-005056b3dfc8' + }] + }, None), + 'broadcast_domain_record': (200, { + "num_records": 1, + "records": [ + { + "uuid": "4475a2c8-f8a0-11e8-8d33-005056bb986f", + "name": "domain1", + "ipspace": {"name": "ip1"}, + "ports": [ + { + "uuid": "ea63420b-2ab3-11ec-aa30-005056b3dfc8", + "name": "e0a", + "node": { + "name": "mohan9cluster2-01" + } + }, + { + "uuid": "ea64c0f2-2ab3-11ec-aa30-005056b3dfc8", + "name": "e0b", + "node": { + "name": "mohan9cluster2-01" + } + }, + { + "uuid": "ea670505-2ab3-11ec-aa30-005056b3dfc8", + "name": "e0d", + "node": { + "name": "mohan9cluster2-01" + } + } + ], + "mtu": 9000 + }] + }, None), + 'broadcast_domain_record1': (200, { + "num_records": 1, + "records": [ + { + "uuid": "4475a2c8-f8a0-11e8-8d33-005056bb986f", + "name": "domain2", + "ipspace": {"name": "ip1"}, + "ports": [ + { + "uuid": "ea63420b-2ab3-11ec-aa30-005056b3dfc8", + "name": "e0a", + "node": { + "name": "mohan9cluster2-01" + } + } + ], + "mtu": 9000 + }] + }, None), + 'iscsips': (200, { + "num_records": 1, + "records": [ + { + "uuid": "52e31a9d-72e2-11ec-95ea-005056b3b297", + "svm": {"name": "svm3"}, + "name": "iscsips" + }] + }, None), + 'iscsips_updated': (200, { + "num_records": 1, + "records": [ + { + "uuid": "52e31a9d-72e2-ec11-95ea-005056b3b298", + "svm": {"name": "svm3"}, + "name": "iscsips_updated", + "interfaces": [ + { + "uuid": "6a82e94a-72da-11ec-95ea-005056b3b297", + "ip": {"name": "lif_svm3_856"} + }] + }] + }, None), + 'mixedps': (200, { + "num_records": 1, + "records": [ + { + "uuid": "ba02916a-72da-11ec-95ea-005056b3b297", + "svm": { + "name": "svm3" + }, + "name": "mixedps", + "interfaces": [ + { + "uuid": "2c373289-728f-11ec-95ea-005056b3b297", + "fc": {"name": "lif_svm3_681_2"} + }, + { + "uuid": "d229cc03-7797-11ec-95ea-005056b3b297", + "fc": {"name": "lif_svm3_681_1_1"} + }, + { + "uuid": "d24e03c6-7797-11ec-95ea-005056b3b297", + "fc": {"name": "lif_svm3_681_1_2"} + }] + }] + }, None), + 'mixedps_updated': (200, { + "num_records": 1, + "records": [ + { + "uuid": "ba02916a-72da-11ec-95ea-005056b3b297", + "svm": { + "name": "svm3" + }, + "name": "mixedps_updated", + "interfaces": [ + { + "uuid": "6a82e94a-72da-11ec-95ea-005056b3b297", + "ip": {"name": "lif_svm3_856"} + }, + { + "uuid": "2bf30606-728f-11ec-95ea-005056b3b297", + "fc": {"name": "lif_svm3_681_1"} + }, + { + "uuid": "2c373289-728f-11ec-95ea-005056b3b297", + "fc": {"name": "lif_svm3_681_2"} + }, + { + "uuid": "d229cc03-7797-11ec-95ea-005056b3b297", + "fc": {"name": "lif_svm3_681_1_1"} + }, + { + "uuid": "d24e03c6-7797-11ec-95ea-005056b3b297", + "fc": {"name": "lif_svm3_681_1_2"} + }] + }] + }, None), + 'lif_svm3_681_1_1': (200, { + "num_records": 1, + "records": [{"uuid": "d229cc03-7797-11ec-95ea-005056b3b297"}] + }, None), + 'lif_svm3_681_1_2': (200, { + "num_records": 1, + "records": [{"uuid": "d24e03c6-7797-11ec-95ea-005056b3b297"}] + }, None), + 'lif_svm3_681_1': (200, { + "num_records": 1, + "records": [{"uuid": "2bf30606-728f-11ec-95ea-005056b3b297"}] + }, None), + 'lif_svm3_681_2': (200, { + "num_records": 1, + "records": [{"uuid": "2c373289-728f-11ec-95ea-005056b3b297"}] + }, None), + 'lif_svm3_856': (200, { + "num_records": 1, + "records": [{"uuid": "6a82e94a-72da-11ec-95ea-005056b3b297"}] + }, None) +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + port_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments:' + assert msg in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_add_broadcast_domain_port_rest(mock_request, patch_ansible): + ''' test add broadcast domain port''' + args = dict(default_args('broadcast_domain')) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['port_detail_e0d'], + SRR['broadcast_domain_record1'], # get + SRR['empty_good'], # add e0b + SRR['empty_good'], # add e0d + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_add_broadcast_domain_port_rest_idempotent(mock_request, patch_ansible): + ''' test add broadcast domain port''' + args = dict(default_args('broadcast_domain')) + args['resource_name'] = "domain2" + args['names'] = ["mohan9cluster2-01:e0a"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0a'], + SRR['broadcast_domain_record1'], # get + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_remove_broadcast_domain_port_rest(mock_request, patch_ansible): + ''' test remove broadcast domain port''' + args = dict(default_args('broadcast_domain')) + args['resource_name'] = "domain1" + args['names'] = ["mohan9cluster2-01:e0b", "mohan9cluster2-01:e0d"] + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['port_detail_e0d'], + SRR['broadcast_domain_record'], # get + SRR['empty_good'], # remove e0b and e0d + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_remove_broadcast_domain_port_rest_idempotent(mock_request, patch_ansible): + ''' test remove broadcast domain port''' + args = dict(default_args('broadcast_domain')) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['port_detail_e0d'], + SRR['broadcast_domain_record1'], # get + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_get_ports_rest(mock_request, patch_ansible): + ''' test get port ''' + args = dict(default_args('broadcast_domain')) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['generic_error'], # Error in getting ports + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = port_module() + print('Info: %s' % exc.value.args[0]) + msg = 'calling: network/ethernet/ports: got Expected error.' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_get_broadcast_domain_ports_rest(mock_request, patch_ansible): + ''' test get broadcast domain ''' + args = dict(default_args('broadcast_domain')) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['port_detail_e0d'], + SRR['generic_error'], # Error in getting broadcast domain ports + ] + my_obj = port_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'calling: network/ethernet/broadcast-domains: got Expected error.' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_add_broadcast_domain_ports_rest(mock_request, patch_ansible): + ''' test add broadcast domain ports ''' + args = dict(default_args('broadcast_domain')) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['port_detail_e0d'], + SRR['broadcast_domain_record1'], # get + SRR['generic_error'], # Error in adding ports + ] + my_obj = port_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'got Expected error.' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_remove_broadcast_domain_ports_rest(mock_request, patch_ansible): + ''' test remove broadcast domain ports ''' + args = dict(default_args('broadcast_domain')) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['port_detail_e0b'], + SRR['port_detail_e0d'], + SRR['broadcast_domain_record'], # get + SRR['generic_error'], # Error in removing ports + ] + my_obj = port_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error removing ports: calling: private/cli/network/port/broadcast-domain/remove-ports: got Expected error.' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_invalid_ports_rest(mock_request, patch_ansible): + ''' test remove broadcast domain ports ''' + args = dict(default_args('broadcast_domain')) + args['names'] = ["mohan9cluster2-01e0b"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['generic_error'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = port_module() + print('Info: %s' % exc.value.args[0]) + msg = 'Error: Invalid value specified for port: mohan9cluster2-01e0b, provide port name as node_name:port_name' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_broadcast_domain_missing_ports_rest(mock_request, patch_ansible): + ''' test get ports ''' + args = dict(default_args('broadcast_domain')) + args['names'] = ["mohan9cluster2-01:e0l"] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = port_module() + print('Info: %s' % exc.value.args[0]) + msg = 'Error: ports: mohan9cluster2-01:e0l not found' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_add_portset_port_iscsi_rest(mock_request, patch_ansible): + ''' test add portset port''' + args = dict(default_args('portset', 'iscsips', 'iscsi')) + args['names'] = ['lif_svm3_856'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['iscsips'], # get portset + SRR['empty_good'], # add lif_svm3_856 + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_add_portset_port_iscsi_rest_idempotent(mock_request, patch_ansible): + ''' test add portset port''' + args = dict(default_args('portset', 'iscsips_updated', 'iscsi')) + args['names'] = ['lif_svm3_856'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['iscsips_updated'], # get + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_remove_portset_port_iscsi_rest(mock_request, patch_ansible): + ''' test remove portset port''' + args = dict(default_args('portset', 'iscsips_updated', 'iscsi')) + args['names'] = ['lif_svm3_856'] + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['iscsips_updated'], + SRR['empty_good'], # remove lif_svm3_856 + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_add_portset_port_mixed_rest(mock_request, patch_ansible): + ''' test add portset port''' + args = dict(default_args('portset', 'mixedps', 'mixed')) + args['names'] = ['lif_svm3_856', 'lif_svm3_681_1'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], # get lif_svm3_856 in ip + SRR['zero_record'], # lif_svm3_856 not found in fc + SRR['zero_record'], # lif_svm3_681_1 not found in ip + SRR['lif_svm3_681_1'], # get lif_svm3_681_1 in fc + SRR['mixedps'], # get portset + SRR['empty_good'], # Add both ip and fc to mixed portset + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_get_portset_fetching_rest(mock_request, patch_ansible): + ''' test get port ''' + args = dict(default_args('portset', 'iscsips_updated', 'mixed')) + args['names'] = ['lif_svm3_856'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['generic_error'], # Error in getting portset + SRR['generic_error'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = port_module() + print('Info: %s' % exc.value.args[0]) + msg = 'Error fetching lifs details for lif_svm3_856: calling: network/ip/interfaces: got Expected error.' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_get_portset_fetching_portset_ip_rest(mock_request, patch_ansible): + ''' test get port ip''' + args = dict(default_args('portset', 'iscsips_updated', 'ip')) + args['names'] = ['lif_svm3_856'] + del args['portset_type'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['generic_error'], + SRR['iscsips_updated'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + my_obj = port_module() + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_get_portset_fetching_portset_fcp_rest(mock_request, patch_ansible): + ''' test get port fcp''' + args = dict(default_args('portset', 'mixedps_updated', 'fcp')) + args['names'] = ['lif_svm3_681_1'] + del args['portset_type'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['generic_error'], + SRR['lif_svm3_681_1'], + SRR['mixedps_updated'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + my_obj = port_module() + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_get_portset_rest(mock_request, patch_ansible): + ''' test get portset ''' + args = dict(default_args('portset', 'iscsips_updated', 'iscsi')) + args['names'] = ['lif_svm3_856'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['generic_error'], # Error in getting portset + ] + my_obj = port_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'calling: protocols/san/portsets: got Expected error' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_get_portset_error_rest(mock_request, patch_ansible): + ''' test get portset ''' + args = dict(default_args('portset', 'iscsips_updated', 'iscsi')) + args['names'] = ['lif_svm3_856'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['zero_record'], + SRR['generic_error'], # Error in getting portset + ] + my_obj = port_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = "Error: Portset 'iscsips_updated' does not exist" + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_get_portset_missing_rest(mock_request, patch_ansible): + ''' test get portset ''' + args = dict(default_args('portset', 'iscsips_updated', 'iscsi')) + args['names'] = ['lif_svm3_856'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['zero_record'], + SRR['generic_error'], # Error in getting portset + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = port_module() + print('Info: %s' % exc.value.args[0]) + msg = "Error: lifs: lif_svm3_856 of type iscsi not found in vserver svm3" + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_get_portset_missing_state_absent_rest(mock_request, patch_ansible): + ''' test get portset ''' + args = dict(default_args('portset', 'iscsips_updated', 'iscsi')) + args['names'] = ['lif_svm3_856'] + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['zero_record'], + SRR['end_of_sequence'] + ] + my_obj = port_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_add_portset_ports_rest(mock_request, patch_ansible): + ''' test add portset ports ''' + args = dict(default_args('portset', 'iscsips', 'iscsi')) + args['names'] = ['lif_svm3_856'] + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['iscsips'], + SRR['generic_error'], # Error in adding ports + ] + my_obj = port_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'calling: protocols/san/portsets/52e31a9d-72e2-11ec-95ea-005056b3b297/interfaces: got Expected error.' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_module_error_remove_portset_ports_rest(mock_request, patch_ansible): + ''' test remove broadcast domain ports ''' + args = dict(default_args('portset', 'iscsips_updated', 'iscsi')) + args['names'] = ['lif_svm3_856'] + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], # get version + SRR['lif_svm3_856'], + SRR['iscsips_updated'], + SRR['generic_error'], # Error in removing ports + ] + my_obj = port_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'calling: protocols/san/portsets/52e31a9d-72e2-ec11-95ea-005056b3b298/interfaces/6a82e94a-72da-11ec-95ea-005056b3b297: got Expected error.' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_portset.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_portset.py new file mode 100644 index 000000000..2e68e58c9 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_portset.py @@ -0,0 +1,390 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for ONTAP Ansible module: na_ontap_portset''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_portset \ + import NetAppONTAPPortset as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +DEFAULT_ARGS = { + 'state': 'present', + 'name': 'test', + 'type': 'mixed', + 'vserver': 'ansible_test', + 'ports': ['a1', 'a2'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' +} + + +portset_info = { + 'num-records': 1, + 'attributes-list': { + 'portset-info': { + 'portset-name': 'test', + 'vserver': 'ansible_test', + 'portset-type': 'mixed', + 'portset-port-total': '2', + 'portset-port-info': [ + {'portset-port-name': 'a1'}, + {'portset-port-name': 'a2'} + ] + } + } +} + + +ZRR = zapi_responses({ + 'portset_info': build_zapi_response(portset_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "name", "vserver"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_ensure_portset_get_called(): + ''' a more interesting test ''' + register_responses([ + ('portset-get-iter', ZRR['empty']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + portset = my_obj.portset_get() + assert portset is None + + +def test_create_portset(): + ''' Test successful create ''' + register_responses([ + ('portset-get-iter', ZRR['empty']), + ('portset-create', ZRR['success']), + ('portset-add', ZRR['success']), + ('portset-add', ZRR['success']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_modify_ports(): + ''' Test modify_portset method ''' + register_responses([ + ('portset-get-iter', ZRR['portset_info']), + ('portset-add', ZRR['success']), + ('portset-add', ZRR['success']), + ('portset-remove', ZRR['success']), + ('portset-remove', ZRR['success']) + ]) + args = {'ports': ['l1', 'l2']} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_portset(): + ''' Test successful delete ''' + register_responses([ + ('portset-get-iter', ZRR['portset_info']), + ('portset-destroy', ZRR['success']) + ]) + args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_error_type_create(): + register_responses([ + ('portset-get-iter', ZRR['empty']) + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['type'] + error = 'Error: Missing required parameter for create (type)' + assert error in create_and_apply(my_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('portset-get-iter', ZRR['error']), + ('portset-create', ZRR['error']), + ('portset-add', ZRR['error']), + ('portset-remove', ZRR['error']), + ('portset-destroy', ZRR['error']) + ]) + portset_obj = create_module(my_module, DEFAULT_ARGS) + + error = expect_and_capture_ansible_exception(portset_obj.portset_get, 'fail')['msg'] + assert 'Error fetching portset' in error + + error = expect_and_capture_ansible_exception(portset_obj.create_portset, 'fail')['msg'] + assert 'Error creating portse' in error + + error = expect_and_capture_ansible_exception(portset_obj.modify_port, 'fail', 'a1', 'portset-add', 'adding')['msg'] + assert 'Error adding port in portset' in error + + error = expect_and_capture_ansible_exception(portset_obj.modify_port, 'fail', 'a2', 'portset-remove', 'removing')['msg'] + assert 'Error removing port in portset' in error + + error = expect_and_capture_ansible_exception(portset_obj.delete_portset, 'fail')['msg'] + assert 'Error deleting portset' in error + + +SRR = rest_responses({ + 'mixed_portset_info': (200, {"records": [{ + "interfaces": [ + { + "fc": { + "name": "lif_1", + "uuid": "d229cc03" + } + }, + { + "ip": { + "name": "lif_2", + "uuid": "1cd8a442" + } + } + ], + "name": "mixed_ps", + "protocol": "mixed", + "uuid": "312aa85b" + }], "num_records": 1}, None), + 'fc_portset_info': (200, {"records": [{ + "interfaces": [ + { + "fc": { + "name": "fc_1", + "uuid": "3a09cd42" + } + }, + { + "fc": { + "name": "fc_2", + "uuid": "d24e03c6" + } + } + ], + "name": "fc_ps", + "protocol": "fcp", + "uuid": "5056b3b297" + }], "num_records": 1}, None), + 'lif_1': (200, { + "num_records": 1, + "records": [{"uuid": "d229cc03"}] + }, None), + 'lif_2': (200, { + "num_records": 1, + "records": [{"uuid": "d24e03c6"}] + }, None), + 'fc_1': (200, { + "num_records": 1, + "records": [{"uuid": "3a09cd42"}] + }, None), + 'fc_2': (200, { + "num_records": 1, + "records": [{"uuid": "1cd8b542"}] + }, None) +}) + + +def test_create_portset_rest(): + ''' Test successful create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['empty_records']), + ('GET', 'network/ip/interfaces', SRR['empty_records']), + ('GET', 'network/fc/interfaces', SRR['lif_1']), + ('GET', 'network/ip/interfaces', SRR['lif_2']), + ('GET', 'network/fc/interfaces', SRR['empty_records']), + ('POST', 'protocols/san/portsets', SRR['success']) + ]) + args = {'use_rest': 'always'} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_create_portset_idempotency_rest(): + ''' Test successful create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']) + ]) + args = {'use_rest': 'always', "ports": ["lif_1", "lif_2"]} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_modify_remove_ports_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']), + ('DELETE', 'protocols/san/portsets/312aa85b/interfaces/1cd8a442', SRR['success']) + ]) + args = {'use_rest': 'always', "ports": ["lif_1"]} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_modify_add_ports_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']), + ('GET', 'network/ip/interfaces', SRR['empty_records']), + ('GET', 'network/fc/interfaces', SRR['fc_1']), + ('POST', 'protocols/san/portsets/312aa85b/interfaces', SRR['success']) + ]) + args = {'use_rest': 'always', "ports": ["lif_1", "lif_2", "fc_1"]} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_portset_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']), + ('DELETE', 'protocols/san/portsets/312aa85b', SRR['success']) + ]) + args = {'use_rest': 'always', 'state': 'absent', 'ports': ['lif_1', 'lif_2']} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_get_portset_error_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['generic_error']) + ]) + args = {'use_rest': 'always', "ports": ["lif_1", "lif_2", "fc_1"]} + error = 'Error fetching portset' + assert error in create_and_apply(my_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_create_portset_error_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['empty_records']), + ('POST', 'protocols/san/portsets', SRR['generic_error']) + ]) + args = {'use_rest': 'always', "ports": []} + error = 'Error creating portset' + assert error in create_and_apply(my_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_delete_portset_error_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']), + ('DELETE', 'protocols/san/portsets/312aa85b', SRR['generic_error']) + ]) + args = {'use_rest': 'always', 'state': 'absent', "ports": ["lif_1", "lif_2"]} + error = 'Error deleting portset' + assert error in create_and_apply(my_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_add_portset_error_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']), + ('GET', 'network/ip/interfaces', SRR['empty_records']), + ('GET', 'network/fc/interfaces', SRR['fc_1']), + ('POST', 'protocols/san/portsets/312aa85b/interfaces', SRR['generic_error']) + ]) + args = {'use_rest': 'always', "ports": ["lif_1", "lif_2", "fc_1"]} + error = "Error adding port in portset" + assert error in create_and_apply(my_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_remove_portset_error_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']), + ('DELETE', 'protocols/san/portsets/312aa85b/interfaces/1cd8a442', SRR['generic_error']) + ]) + args = {'use_rest': 'always', "ports": ["lif_1"]} + error = "Error removing port in portset" + assert error in create_and_apply(my_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_add_ip_port_to_fc_error_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['fc_portset_info']), + ('GET', 'network/fc/interfaces', SRR['empty_records']) + ]) + args = {'use_rest': 'always', "type": "fcp", "ports": ["fc_1", "fc_2", "lif_2"]} + error = 'Error: lifs: lif_2 of type fcp not found in vserver' + assert error in create_and_apply(my_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_get_lif_error_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']), + ('GET', 'network/ip/interfaces', SRR['generic_error']), + ('GET', 'network/fc/interfaces', SRR['generic_error']) + ]) + args = {'use_rest': 'always', "ports": ["lif_1", "lif_2", "fc_1"]} + error = "Error fetching lifs details for fc_1" + assert error in create_and_apply(my_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_try_to_modify_protocol_error_rest(): + ''' Test modify_portset method ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'protocols/san/portsets', SRR['mixed_portset_info']) + ]) + args = {'use_rest': 'always', "type": "iscsi", "ports": ["lif_1", "lif_2"]} + error = "modify protocol(type) not supported" + assert error in create_and_apply(my_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_invalid_value_port_rest(): + ''' Test invalid error ''' + args = {'use_rest': 'always', "type": "iscsi", "ports": ["lif_1", ""]} + error = "Error: invalid value specified for ports" + assert error in create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_module_ontap_9_9_0_rest_auto(): + ''' Test fall back to ZAPI ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + args = {'use_rest': 'auto'} + assert create_module(my_module, DEFAULT_ARGS, args).use_rest is False + + +def test_module_ontap_9_9_0_rest_always(): + ''' Test error when rest below 9.9.1 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + args = {'use_rest': 'always'} + msg = "Error: REST requires ONTAP 9.9.1 or later for portset APIs." + assert msg in create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_publickey.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_publickey.py new file mode 100644 index 000000000..d72d8c8eb --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_publickey.py @@ -0,0 +1,471 @@ +# (c) 2018-2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP publickey Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible, assert_warning_was_raised, assert_no_warnings, print_warnings + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_publickey \ + import NetAppOntapPublicKey as my_module, main as uut_main # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def default_args(): + return { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'account': 'user123', + 'public_key': '161245ASDF', + 'vserver': 'vserver', + } + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'one_pk_record': (200, { + "records": [{ + 'account': dict(name='user123'), + 'owner': dict(uuid='98765'), + 'public_key': '161245ASDF', + 'index': 12, + 'comment': 'comment_123', + }], + 'num_records': 1 + }, None), + 'two_pk_records': (200, { + "records": [{ + 'account': dict(name='user123'), + 'owner': dict(uuid='98765'), + 'public_key': '161245ASDF', + 'index': 12, + 'comment': 'comment_123', + }, + { + 'account': dict(name='user123'), + 'owner': dict(uuid='98765'), + 'public_key': '161245ASDF', + 'index': 13, + 'comment': 'comment_123', + }], + 'num_records': 2 + }, None) +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments: account' + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_get_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['index'] = 12 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['one_pk_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_create_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['index'] = 13 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['empty_good'], # create + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_create_idempotent(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'always' + args['index'] = 12 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['one_pk_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_create_always_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['empty_good'], # create + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + print_warnings() + assert_warning_was_raised('Module is not idempotent if index is not provided with state=present.') + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_modify_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['index'] = 12 + args['comment'] = 'new_comment' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['one_pk_record'], # get + SRR['empty_good'], # modify + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['index'] = 12 + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['one_pk_record'], # get + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_idempotent(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'always' + args['index'] = 12 + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_failed_N_records(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['two_pk_records'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error: index is required as more than one public_key exists for user account user123' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_succeeded_N_records(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['state'] = 'absent' + args['delete_all'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['two_pk_records'], # get + SRR['empty_good'], # delete + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_succeeded_N_records_cluster(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['state'] = 'absent' + args['delete_all'] = True + args['vserver'] = None # cluster scope + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['two_pk_records'], # get + SRR['empty_good'], # delete + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + uut_main() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_negative_extra_record(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['state'] = 'present' + args['index'] = 14 + args['vserver'] = None # cluster scope + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['two_pk_records'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + uut_main() + print('Info: %s' % exc.value.args[0]) + msg = 'Error in get_public_key: calling: security/authentication/publickeys: unexpected response' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_negative_extra_arg_in_modify(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['state'] = 'present' + args['index'] = 14 + args['vserver'] = None # cluster scope + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['one_pk_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + uut_main() + print('Info: %s' % exc.value.args[0]) + msg = "Error: attributes not supported in modify: {'index': 14}" + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_negative_empty_body_in_modify(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['end_of_sequence'] + ] + current = dict(owner=dict(uuid=''), account=dict(name=''), index=0) + modify = {} + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_public_key(current, modify) + print('Info: %s' % exc.value.args[0]) + msg = 'Error: nothing to change - modify called with: {}' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_negative_create_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['index'] = 13 + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['generic_error'], # create + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error in create_public_key: Expected error' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_negative_delete_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['index'] = 12 + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['one_pk_record'], # get + SRR['generic_error'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error in delete_public_key: Expected error' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_negative_modify_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['index'] = 12 + args['comment'] = 'change_me' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['one_pk_record'], # get + SRR['generic_error'], # modify + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + msg = 'Error in modify_public_key: Expected error' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_negative_older_version(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'auto' + args['index'] = 12 + args['comment'] = 'change_me' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_6'], # get version + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = my_module() + print('Info: %s' % exc.value.args[0]) + msg = 'Error: na_ontap_publickey only supports REST, and requires ONTAP 9.7.0 or later. Found: 9.6.0' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_negative_zapi_only(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['index'] = 12 + args['comment'] = 'change_me' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_6'], # get version + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + my_obj = my_module() + print('Info: %s' % exc.value.args[0]) + msg = 'Error: REST is required for this module, found: "use_rest: never"' + assert msg in exc.value.args[0]['msg'] + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_adaptive_policy_group.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_adaptive_policy_group.py new file mode 100644 index 000000000..f568ed17a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_adaptive_policy_group.py @@ -0,0 +1,313 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_qos_adaptive_policy_group \ + import NetAppOntapAdaptiveQosPolicyGroup as qos_policy_group_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'policy': + xml = self.build_policy_group_info(self.params) + if self.kind == 'error': + error = netapp_utils.zapi.NaApiError('test', 'error') + raise error + self.xml_out = xml + return xml + + @staticmethod + def build_policy_group_info(vol_details): + ''' build xml data for volume-attributes ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'qos-adaptive-policy-group-info': { + 'absolute-min-iops': '50IOPS', + 'expected-iops': '150IOPS/TB', + 'peak-iops': '220IOPS/TB', + 'peak-iops-allocation': 'used_space', + 'num-workloads': 0, + 'pgid': 6941, + 'policy-group': vol_details['name'], + 'vserver': vol_details['vserver'] + } + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_policy_group = { + 'name': 'policy_1', + 'vserver': 'policy_vserver', + 'absolute_min_iops': '50IOPS', + 'expected_iops': '150IOPS/TB', + 'peak_iops': '220IOPS/TB', + 'peak_iops_allocation': 'used_space' + } + + def mock_args(self): + return { + 'name': self.mock_policy_group['name'], + 'vserver': self.mock_policy_group['vserver'], + 'absolute_min_iops': '50IOPS', + 'expected_iops': '150IOPS/TB', + 'peak_iops': '220IOPS/TB', + 'peak_iops_allocation': 'used_space', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': 'False' + } + + def get_policy_group_mock_object(self, kind=None): + """ + Helper method to return an na_ontap_volume object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_volume object + """ + policy_obj = qos_policy_group_module() + policy_obj.autosupport_log = Mock(return_value=None) + policy_obj.cluster = Mock() + policy_obj.cluster.invoke_successfully = Mock() + if kind is None: + policy_obj.server = MockONTAPConnection() + else: + policy_obj.server = MockONTAPConnection(kind=kind, data=self.mock_policy_group) + return policy_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + qos_policy_group_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_policy(self): + ''' Test if get_policy_group returns None for non-existent policy_group ''' + set_module_args(self.mock_args()) + result = self.get_policy_group_mock_object().get_policy_group() + assert result is None + + def test_get_existing_policy_group(self): + ''' Test if get_policy_group returns details for existing policy_group ''' + set_module_args(self.mock_args()) + result = self.get_policy_group_mock_object('policy').get_policy_group() + assert result['name'] == self.mock_policy_group['name'] + assert result['vserver'] == self.mock_policy_group['vserver'] + + def test_create_error_missing_param(self): + ''' Test if create throws an error if name is not specified''' + data = self.mock_args() + del data['name'] + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_group_mock_object('policy').create_policy_group() + msg = 'missing required arguments: name' + assert exc.value.args[0]['msg'] == msg + + def test_successful_create(self): + ''' Test successful create ''' + data = self.mock_args() + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_policy_group_mock_object().apply() + assert exc.value.args[0]['changed'] + + def test_create_idempotency(self): + ''' Test create idempotency ''' + set_module_args(self.mock_args()) + obj = self.get_policy_group_mock_object('policy') + with pytest.raises(AnsibleExitJson) as exc: + obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qos_adaptive_policy_group.NetAppOntapAdaptiveQosPolicyGroup.get_policy_group') + def test_create_error(self, get_policy_group): + ''' Test create error ''' + set_module_args(self.mock_args()) + get_policy_group.side_effect = [ + None + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_group_mock_object('error').apply() + assert exc.value.args[0]['msg'] == 'Error creating adaptive qos policy group policy_1: NetApp API failed. Reason - test:error' + + def test_successful_delete(self): + ''' Test delete existing volume ''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_policy_group_mock_object('policy').apply() + assert exc.value.args[0]['changed'] + + def test_delete_idempotency(self): + ''' Test delete idempotency ''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_policy_group_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qos_adaptive_policy_group.NetAppOntapAdaptiveQosPolicyGroup.get_policy_group') + def test_delete_error(self, get_policy_group): + ''' Test create idempotency''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + current = { + 'absolute_min_iops': '50IOPS', + 'expected_iops': '150IOPS/TB', + 'peak_iops': '220IOPS/TB', + 'peak_iops_allocation': 'used_space', + 'name': 'policy_1', + 'vserver': 'policy_vserver' + } + get_policy_group.side_effect = [ + current + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_group_mock_object('error').apply() + assert exc.value.args[0]['msg'] == 'Error deleting adaptive qos policy group policy_1: NetApp API failed. Reason - test:error' + + def test_successful_modify_expected_iops(self): + ''' Test successful modify expected iops ''' + data = self.mock_args() + data['expected_iops'] = '175IOPS' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_policy_group_mock_object('policy').apply() + assert exc.value.args[0]['changed'] + + def test_modify_expected_iops_idempotency(self): + ''' Test modify idempotency ''' + data = self.mock_args() + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_policy_group_mock_object('policy').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qos_adaptive_policy_group.NetAppOntapAdaptiveQosPolicyGroup.get_policy_group') + def test_modify_error(self, get_policy_group): + ''' Test create idempotency ''' + data = self.mock_args() + data['expected_iops'] = '175IOPS' + set_module_args(data) + current = { + 'absolute_min_iops': '50IOPS', + 'expected_iops': '150IOPS/TB', + 'peak_iops': '220IOPS/TB', + 'peak_iops_allocation': 'used_space', + 'name': 'policy_1', + 'vserver': 'policy_vserver' + } + get_policy_group.side_effect = [ + current + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_group_mock_object('error').apply() + assert exc.value.args[0]['msg'] == 'Error modifying adaptive qos policy group policy_1: NetApp API failed. Reason - test:error' + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qos_adaptive_policy_group.NetAppOntapAdaptiveQosPolicyGroup.get_policy_group') + def test_rename(self, get_policy_group): + ''' Test rename idempotency ''' + data = self.mock_args() + data['name'] = 'policy_2' + data['from_name'] = 'policy_1' + set_module_args(data) + current = { + 'absolute_min_iops': '50IOPS', + 'expected_iops': '150IOPS/TB', + 'peak_iops': '220IOPS/TB', + 'peak_iops_allocation': 'used_space', + 'name': 'policy_1', + 'vserver': 'policy_vserver' + } + get_policy_group.side_effect = [ + None, + current + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_policy_group_mock_object('policy').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qos_adaptive_policy_group.NetAppOntapAdaptiveQosPolicyGroup.get_policy_group') + def test_rename_idempotency(self, get_policy_group): + ''' Test rename idempotency ''' + data = self.mock_args() + data['name'] = 'policy_1' + data['from_name'] = 'policy_1' + current = { + 'absolute_min_iops': '50IOPS', + 'expected_iops': '150IOPS/TB', + 'peak_iops': '220IOPS/TB', + 'peak_iops_allocation': 'used_space', + 'name': 'policy_1', + 'vserver': 'policy_vserver' + } + get_policy_group.side_effect = [ + current, + current + ] + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_policy_group_mock_object('policy').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_qos_adaptive_policy_group.NetAppOntapAdaptiveQosPolicyGroup.get_policy_group') + def test_rename_error(self, get_policy_group): + ''' Test create idempotency ''' + data = self.mock_args() + data['from_name'] = 'policy_1' + data['name'] = 'policy_2' + set_module_args(data) + current = { + 'absolute_min_iops': '50IOPS', + 'expected_iops': '150IOPS/TB', + 'peak_iops': '220IOPS/TB', + 'peak_iops_allocation': 'used_space', + 'is_shared': 'true', + 'name': 'policy_1', + 'vserver': 'policy_vserver' + } + get_policy_group.side_effect = [ + None, + current + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_policy_group_mock_object('error').apply() + assert exc.value.args[0]['msg'] == 'Error renaming adaptive qos policy group policy_1: NetApp API failed. Reason - test:error' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_policy_group.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_policy_group.py new file mode 100644 index 000000000..c14b13151 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qos_policy_group.py @@ -0,0 +1,578 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_qos_policy_group \ + import NetAppOntapQosPolicyGroup as qos_policy_group_module # module under test +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'name': 'policy_1', + 'vserver': 'policy_vserver', + 'max_throughput': '800KB/s,800IOPS', + 'is_shared': True, + 'min_throughput': '100IOPS', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': 'True', + 'use_rest': 'never' +} + + +qos_policy_group_info = { + 'num-records': 1, + 'attributes-list': { + 'qos-policy-group-info': { + 'is-shared': 'true', + 'max-throughput': '800KB/s,800IOPS', + 'min-throughput': '100IOPS', + 'num-workloads': 0, + 'pgid': 8690, + 'policy-group': 'policy_1', + 'vserver': 'policy_vserver' + } + } +} + + +ZRR = zapi_responses({ + 'qos_policy_info': build_zapi_response(qos_policy_group_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + qos_policy_group_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get_nonexistent_policy(): + ''' Test if get_policy_group returns None for non-existent policy_group ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['empty']) + ]) + qos_policy_obj = create_module(qos_policy_group_module, DEFAULT_ARGS) + result = qos_policy_obj.get_policy_group() + assert result is None + + +def test_get_existing_policy_group(): + ''' Test if get_policy_group returns details for existing policy_group ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']) + ]) + qos_policy_obj = create_module(qos_policy_group_module, DEFAULT_ARGS) + result = qos_policy_obj.get_policy_group() + assert result['name'] == DEFAULT_ARGS['name'] + assert result['vserver'] == DEFAULT_ARGS['vserver'] + + +def test_create_error_missing_param(): + ''' Test if create throws an error if name is not specified''' + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['name'] + error = create_module(qos_policy_group_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + assert 'missing required arguments: name' in error + + +def test_error_if_fixed_qos_options_present(): + ''' Test hrows an error if fixed_qos_options is specified in ZAPI''' + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['max_throughput'] + del DEFAULT_ARGS_COPY['min_throughput'] + del DEFAULT_ARGS_COPY['is_shared'] + DEFAULT_ARGS_COPY['fixed_qos_options'] = {'max_throughput_iops': 100} + error = create_module(qos_policy_group_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + assert "Error: 'fixed_qos_options' not supported with ZAPI, use 'max_throughput' and 'min_throughput'" in error + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['empty']), + ('qos-policy-group-create', ZRR['success']) + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS)['changed'] + + +def test_create_idempotency(): + ''' Test create idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']) + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS)['changed'] is False + + +def test_create_error(): + ''' Test create error ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['empty']), + ('qos-policy-group-create', ZRR['error']) + ]) + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS, fail=True)['msg'] + assert 'Error creating qos policy group policy_1' in error + + +def test_successful_delete(): + ''' Test delete existing volume ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']), + ('qos-policy-group-delete', ZRR['success']) + ]) + args = { + 'state': 'absent', + 'force': True + } + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_idempotency(): + ''' Test delete idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['empty']) + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] is False + + +def test_delete_error(): + ''' Test create idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']), + ('qos-policy-group-delete', ZRR['error']) + ]) + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS, {'state': 'absent'}, fail=True)['msg'] + assert 'Error deleting qos policy group policy_1' in error + + +def test_successful_modify_max_throughput(): + ''' Test successful modify max throughput ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']), + ('qos-policy-group-modify', ZRR['success']) + ]) + args = {'max_throughput': '900KB/s,800iops'} + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS, args)['changed'] + + +def test_modify_max_throughput_idempotency(): + ''' Test modify idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']) + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS)['changed'] is False + + +def test_modify_error(): + ''' Test create idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']), + ('qos-policy-group-modify', ZRR['error']) + ]) + args = {'max_throughput': '900KB/s,800iops'} + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error modifying qos policy group policy_1' in error + + +def test_modify_is_shared_error(): + ''' Test create idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']) + ]) + args = { + 'is_shared': False, + 'max_throughput': '900KB/s,900IOPS' + } + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert "Error cannot modify 'is_shared' attribute." in error + + +def test_rename(): + ''' Test rename idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['empty']), + ('qos-policy-group-get-iter', ZRR['qos_policy_info']), + ('qos-policy-group-rename', ZRR['success']) + ]) + args = { + 'name': 'policy_2', + 'from_name': 'policy_1' + } + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS, args)['changed'] + + +def test_rename_idempotency(): + ''' Test rename idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['qos_policy_info']) + ]) + args = { + 'from_name': 'policy_1' + } + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_rename_error(): + ''' Test create idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['empty']), + ('qos-policy-group-get-iter', ZRR['qos_policy_info']), + ('qos-policy-group-rename', ZRR['error']) + ]) + args = { + 'name': 'policy_2', + 'from_name': 'policy_1' + } + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error renaming qos policy group policy_1' in error + + +def test_rename_non_existent_policy(): + ''' Test create idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['empty']), + ('qos-policy-group-get-iter', ZRR['empty']) + ]) + args = { + 'name': 'policy_10', + 'from_name': 'policy_11' + } + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error renaming qos policy group: cannot find' in error + + +def test_get_policy_error(): + ''' Test create idempotency ''' + register_responses([ + ('qos-policy-group-get-iter', ZRR['error']) + ]) + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS, fail=True)['msg'] + assert 'Error fetching qos policy group' in error + + +DEFAULT_ARGS_REST = { + 'name': 'policy_1', + 'vserver': 'policy_vserver', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': 'True', + 'use_rest': 'always', + 'fixed_qos_options': { + 'capacity_shared': False, + 'max_throughput_iops': 1000, + 'max_throughput_mbps': 100, + 'min_throughput_iops': 100, + 'min_throughput_mbps': 50 + } +} + + +SRR = rest_responses({ + 'qos_policy_info': (200, {"records": [ + { + "uuid": "e4f703dc-bfbc-11ec-a164-005056b3bd39", + "svm": {"name": "policy_vserver"}, + "name": "policy_1", + "fixed": { + "max_throughput_iops": 1000, + "max_throughput_mbps": 100, + "min_throughput_iops": 100, + 'min_throughput_mbps': 50, + "capacity_shared": False + } + } + ], 'num_records': 1}, None), + 'adaptive_policy_info': (200, {"records": [ + { + 'uuid': '30d2fdd6-c45a-11ec-a164-005056b3bd39', + 'svm': {'name': 'policy_vserver'}, + 'name': 'policy_1_', + 'adaptive': { + 'expected_iops': 200, + 'peak_iops': 500, + 'absolute_min_iops': 100 + } + } + ], 'num_records': 1}, None) +}) + + +def test_successful_create_rest(): + ''' Test successful create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['empty_records']), + ('POST', 'storage/qos/policies', SRR['success']) + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST)['changed'] + + +def test_create_idempotency_rest(): + ''' Test create idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']), + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST)['changed'] is False + + +def test_successful_create_adaptive_rest(): + ''' Test successful create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['empty_records']), + ('POST', 'storage/qos/policies', SRR['success']), + # with block size + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/qos/policies', SRR['empty_records']), + ('POST', 'storage/qos/policies', SRR['success']), + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS_REST.copy() + del DEFAULT_ARGS_COPY['fixed_qos_options'] + DEFAULT_ARGS_COPY['adaptive_qos_options'] = { + "absolute_min_iops": 100, + "expected_iops": 200, + "peak_iops": 500 + } + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_COPY)['changed'] + DEFAULT_ARGS_COPY['adaptive_qos_options']['block_size'] = '4k' + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_COPY)['changed'] + + +def test_partially_supported_option_rest(): + ''' Test delete error ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + error = create_module(qos_policy_group_module, DEFAULT_ARGS_REST, fail=True)['msg'] + assert "Minimum version of ONTAP for 'fixed_qos_options.min_throughput_mbps' is (9, 8, 0)" in error + DEFAULT_ARGS_COPY = DEFAULT_ARGS_REST.copy() + del DEFAULT_ARGS_COPY['fixed_qos_options'] + DEFAULT_ARGS_COPY['adaptive_qos_options'] = { + "absolute_min_iops": 100, + "expected_iops": 200, + "peak_iops": 500, + "block_size": "4k" + } + error = create_module(qos_policy_group_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + assert "Minimum version of ONTAP for 'adaptive_qos_options.block_size' is (9, 10, 1)" in error + + +def test_error_create_adaptive_rest(): + ''' Test successful create ''' + DEFAULT_ARGS_COPY = DEFAULT_ARGS_REST.copy() + del DEFAULT_ARGS_COPY['fixed_qos_options'] + DEFAULT_ARGS_COPY['adaptive_qos_options'] = { + "absolute_min_iops": 100, + "expected_iops": 200 + } + error = create_module(qos_policy_group_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + assert "missing required arguments: peak_iops found in adaptive_qos_options" in error + + +def test_create_error_rest(): + ''' Test create error ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['empty_records']), + ('POST', 'storage/qos/policies', SRR['generic_error']), + ]) + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, fail=True)['msg'] + assert 'Error creating qos policy group policy_1' in error + + +def test_successful_delete_rest(): + ''' Test delete existing volume ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']), + ('DELETE', 'storage/qos/policies/e4f703dc-bfbc-11ec-a164-005056b3bd39', SRR['success']) + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, {'state': 'absent'})['changed'] + + +def test_delete_idempotency_rest(): + ''' Test delete idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['empty_records']) + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, {'state': 'absent'})['changed'] is False + + +def test_create_error_fixed_adaptive_qos_options_missing(): + ''' Error if fixed_qos_optios not present in create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['empty_records']) + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS_REST.copy() + del DEFAULT_ARGS_COPY['fixed_qos_options'] + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + assert "Error: atleast one throughput in 'fixed_qos_options' or all 'adaptive_qos_options'" in error + + +def test_delete_error_rest(): + ''' Test delete error ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']), + ('DELETE', 'storage/qos/policies/e4f703dc-bfbc-11ec-a164-005056b3bd39', SRR['generic_error']) + ]) + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, {'state': 'absent'}, fail=True)['msg'] + assert 'Error deleting qos policy group policy_1' in error + + +def test_successful_modify_max_throughput_rest(): + ''' Test successful modify max throughput ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']), + ('PATCH', 'storage/qos/policies/e4f703dc-bfbc-11ec-a164-005056b3bd39', SRR['success']) + ]) + args = {'fixed_qos_options': { + 'max_throughput_iops': 2000, + 'max_throughput_mbps': 300, + 'min_throughput_iops': 400, + 'min_throughput_mbps': 700 + }} + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_modify_max_throughput_idempotency_rest(): + ''' Test modify idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']) + ]) + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST)['changed'] is False + + +def test_successful_modify_adaptive_qos_options_rest(): + ''' Test successful modify max throughput ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/qos/policies', SRR['adaptive_policy_info']), + ('PATCH', 'storage/qos/policies/30d2fdd6-c45a-11ec-a164-005056b3bd39', SRR['success']) + ]) + DEFAULT_ARGS_REST_COPY = DEFAULT_ARGS_REST.copy() + del DEFAULT_ARGS_REST_COPY['fixed_qos_options'] + args = { + 'adaptive_qos_options': { + 'expected_iops': 300, + 'peak_iops': 600, + 'absolute_min_iops': 200, + 'block_size': '4k' + } + } + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST_COPY, args)['changed'] + + +def test_error_adaptive_qos_options_zapi(): + ''' Test error adaptive_qos_options zapi ''' + DEFAULT_ARGS_REST_COPY = DEFAULT_ARGS_REST.copy() + del DEFAULT_ARGS_REST_COPY['fixed_qos_options'] + DEFAULT_ARGS_REST_COPY['use_rest'] = 'never' + args = { + 'adaptive_qos_options': { + 'expected_iops': 300, + 'peak_iops': 600, + 'absolute_min_iops': 200 + } + } + error = create_module(qos_policy_group_module, DEFAULT_ARGS_REST_COPY, args, fail=True)['msg'] + assert "Error: use 'na_ontap_qos_adaptive_policy_group' module for create/modify/delete adaptive policy with ZAPI" in error + + +def test_modify_error_rest(): + ''' Test modify error rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']), + ('PATCH', 'storage/qos/policies/e4f703dc-bfbc-11ec-a164-005056b3bd39', SRR['generic_error']) + ]) + args = {'fixed_qos_options': { + 'max_throughput_iops': 2000, + 'max_throughput_mbps': 300, + 'min_throughput_iops': 400, + 'min_throughput_mbps': 700 + }} + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, args, fail=True)['msg'] + assert 'Error modifying qos policy group policy_1' in error + + +def test_rename_rest(): + ''' Test rename ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['empty_records']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']), + ('PATCH', 'storage/qos/policies/e4f703dc-bfbc-11ec-a164-005056b3bd39', SRR['success']) + ]) + args = { + 'name': 'policy_2', + 'from_name': 'policy_1' + } + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_rename_idempotency_rest(): + ''' Test rename idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']) + ]) + args = { + 'from_name': 'policy_1' + } + assert create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, args)['changed'] is False + + +def test_rename_error_rest(): + ''' Test create idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['empty_records']), + ('GET', 'storage/qos/policies', SRR['qos_policy_info']), + ('PATCH', 'storage/qos/policies/e4f703dc-bfbc-11ec-a164-005056b3bd39', SRR['generic_error']) + ]) + args = { + 'name': 'policy_2', + 'from_name': 'policy_1' + } + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, args, fail=True)['msg'] + assert 'Error renaming qos policy group policy_1' in error + + +def test_get_policy_error_rest(): + ''' Test get policy error rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/qos/policies', SRR['generic_error']) + ]) + error = create_and_apply(qos_policy_group_module, DEFAULT_ARGS_REST, fail=True)['msg'] + assert 'Error fetching qos policy group policy_1' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qtree.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qtree.py new file mode 100644 index 000000000..e88fcb852 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_qtree.py @@ -0,0 +1,404 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_quotas ''' +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + call_main, create_module, create_and_apply, expect_and_capture_ansible_exception, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_qtree \ + import NetAppOntapQTree as qtree_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +DEFAULT_ARGS = { + 'state': 'present', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'ansible', + 'vserver': 'ansible', + 'flexvol_name': 'ansible', + 'export_policy': 'ansible', + 'security_style': 'unix', + 'unix_permissions': '755', + 'use_rest': 'never' +} + + +qtree_info = { + 'num-records': 1, + 'attributes-list': { + 'qtree-info': { + 'export-policy': 'ansible', + 'vserver': 'ansible', + 'qtree': 'ansible', + 'oplocks': 'enabled', + 'security-style': 'unix', + 'mode': '755', + 'volume': 'ansible' + } + } +} + + +ZRR = zapi_responses({ + 'qtree_info': build_zapi_response(qtree_info) +}) + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'qtree_record': (200, {"records": [{ + "svm": {"name": "ansible"}, + "id": 1, + "name": "ansible", + "security_style": "unix", + "unix_permissions": 755, + "export_policy": {"name": "ansible"}, + "volume": {"uuid": "uuid", "name": "volume1"}} + ]}, None), + 'job_info': (200, { + "job": { + "uuid": "d78811c1-aebc-11ec-b4de-005056b30cfa", + "_links": {"self": {"href": "/api/cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa"}} + }}, None), + 'job_not_found': (404, "", {"message": "entry doesn't exist", "code": "4", "target": "uuid"}) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "name", "vserver", "flexvol_name"] + error = create_module(qtree_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_ensure_get_called(): + ''' test get_qtree for non-existent qtree''' + register_responses([ + ('qtree-list-iter', ZRR['empty']) + ]) + my_obj = create_module(qtree_module, DEFAULT_ARGS) + portset = my_obj.get_qtree() + assert portset is None + + +def test_ensure_get_called_existing(): + ''' test get_qtree for existing qtree''' + register_responses([ + ('qtree-list-iter', ZRR['qtree_info']) + ]) + my_obj = create_module(qtree_module, DEFAULT_ARGS) + assert my_obj.get_qtree() + + +def test_successful_create(): + ''' creating qtree ''' + register_responses([ + ('qtree-list-iter', ZRR['empty']), + ('qtree-create', ZRR['success']) + ]) + module_args = { + 'oplocks': 'enabled' + } + assert create_and_apply(qtree_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_delete(): + ''' deleting qtree ''' + register_responses([ + ('qtree-list-iter', ZRR['qtree_info']), + ('qtree-delete', ZRR['success']) + ]) + args = {'state': 'absent'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_delete_idempotency(): + ''' deleting qtree idempotency ''' + register_responses([ + ('qtree-list-iter', ZRR['empty']) + ]) + args = {'state': 'absent'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_modify(): + ''' modifying qtree ''' + register_responses([ + ('qtree-list-iter', ZRR['qtree_info']), + ('qtree-modify', ZRR['success']) + ]) + args = { + 'export_policy': 'test', + 'oplocks': 'enabled' + } + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] + + +def test_failed_rename(): + ''' test error rename qtree ''' + register_responses([ + ('qtree-list-iter', ZRR['empty']), + ('qtree-list-iter', ZRR['empty']) + ]) + args = {'from_name': 'test'} + error = 'Error renaming: qtree %s does not exist' % args['from_name'] + assert error in create_and_apply(qtree_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_successful_rename(): + ''' rename qtree ''' + register_responses([ + ('qtree-list-iter', ZRR['empty']), + ('qtree-list-iter', ZRR['qtree_info']), + ('qtree-rename', ZRR['success']) + ]) + args = {'from_name': 'ansible_old'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] + + +def test_if_all_methods_catch_exception(): + ''' test error zapi - get/create/rename/modify/delete''' + register_responses([ + ('qtree-list-iter', ZRR['error']), + ('qtree-create', ZRR['error']), + ('qtree-rename', ZRR['error']), + ('qtree-modify', ZRR['error']), + ('qtree-delete', ZRR['error']) + ]) + qtree_obj = create_module(qtree_module, DEFAULT_ARGS, {'from_name': 'name'}) + + assert 'Error fetching qtree' in expect_and_capture_ansible_exception(qtree_obj.get_qtree, 'fail')['msg'] + assert 'Error creating qtree' in expect_and_capture_ansible_exception(qtree_obj.create_qtree, 'fail')['msg'] + assert 'Error renaming qtree' in expect_and_capture_ansible_exception(qtree_obj.rename_qtree, 'fail')['msg'] + assert 'Error modifying qtree' in expect_and_capture_ansible_exception(qtree_obj.modify_qtree, 'fail')['msg'] + assert 'Error deleting qtree' in expect_and_capture_ansible_exception(qtree_obj.delete_qtree, 'fail')['msg'] + + +def test_get_error_rest(): + ''' test get qtree error in rest''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['generic_error']) + ]) + error = 'Error fetching qtree' + assert error in create_and_apply(qtree_module, DEFAULT_ARGS, {'use_rest': 'always'}, 'fail')['msg'] + + +def test_create_error_rest(): + ''' test get qtree error in rest''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['empty_records']), + ('POST', 'storage/qtrees', SRR['generic_error']) + ]) + error = 'Error creating qtree' + assert error in create_and_apply(qtree_module, DEFAULT_ARGS, {'use_rest': 'always'}, 'fail')['msg'] + + +def test_modify_error_rest(): + ''' test get qtree error in rest''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['qtree_record']), + ('PATCH', 'storage/qtrees/uuid/1', SRR['generic_error']) + ]) + args = {'use_rest': 'always', 'unix_permissions': '777'} + error = 'Error modifying qtree' + assert error in create_and_apply(qtree_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_rename_error_rest(): + ''' test get qtree error in rest''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['empty_records']), + ('GET', 'storage/qtrees', SRR['empty_records']) + ]) + args = {'use_rest': 'always', 'from_name': 'abcde', 'name': 'qtree'} + error = 'Error renaming: qtree' + assert error in create_and_apply(qtree_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_delete_error_rest(): + ''' test get qtree error in rest''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['qtree_record']), + ('DELETE', 'storage/qtrees/uuid/1', SRR['generic_error']) + ]) + args = {'use_rest': 'always', 'state': 'absent'} + error = 'Error deleting qtree' + assert error in create_and_apply(qtree_module, DEFAULT_ARGS, args, 'fail')['msg'] + + +def test_successful_create_rest(): + ''' test create qtree rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['empty_records']), + ('POST', 'storage/qtrees', SRR['success']) + ]) + assert create_and_apply(qtree_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + + +def test_idempotent_create_rest(): + ''' test create qtree idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['qtree_record']) + ]) + assert create_and_apply(qtree_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] is False + + +@patch('time.sleep') +def test_successful_create_rest_job_error(sleep): + ''' test create qtree rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['empty_records']), + ('POST', 'storage/qtrees', SRR['job_info']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']) + ]) + assert create_and_apply(qtree_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + print_warnings() + assert_warning_was_raised('Ignoring job status, assuming success.') + + +def test_successful_delete_rest(): + ''' test delete qtree rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['qtree_record']), + ('DELETE', 'storage/qtrees/uuid/1', SRR['success']) + ]) + args = {'use_rest': 'always', 'state': 'absent'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] + + +def test_idempotent_delete_rest(): + ''' test delete qtree idempotency''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['empty_records']) + ]) + args = {'use_rest': 'always', 'state': 'absent'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_modify_rest(): + ''' test modify qtree rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['qtree_record']), + ('PATCH', 'storage/qtrees/uuid/1', SRR['success']) + ]) + args = {'use_rest': 'always', 'unix_permissions': '777'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] + + +def test_idempotent_modify_rest(): + ''' test modify qtree idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['qtree_record']) + ]) + args = {'use_rest': 'always'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] is False + + +def test_successful_rename_rest(): + ''' test rename qtree rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['zero_records']), + ('GET', 'storage/qtrees', SRR['qtree_record']), + ('PATCH', 'storage/qtrees/uuid/1', SRR['success']) + ]) + args = {'use_rest': 'always', 'from_name': 'abcde', 'name': 'qtree'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] + + +def test_successful_rename_rest_idempotent(): + ''' test rename qtree in rest - idempotency''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['qtree_record']) + ]) + args = {'use_rest': 'always', 'from_name': 'abcde'} + assert create_and_apply(qtree_module, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_rename_and_modify_rest(): + ''' test rename and modify qtree rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['empty_records']), + ('GET', 'storage/qtrees', SRR['qtree_record']), + ('PATCH', 'storage/qtrees/uuid/1', SRR['success']) + ]) + args = { + 'use_rest': 'always', + 'from_name': 'abcde', + 'name': 'qtree', + 'unix_permissions': '744', + 'unix_user': 'user', + 'unix_group': 'group', + } + assert call_main(my_main, DEFAULT_ARGS, args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_missing_netapp_lib(mock_has_netapp_lib): + module_args = { + 'use_rest': 'never' + } + mock_has_netapp_lib.return_value = False + error = 'Error: the python NetApp-Lib module is required. Import error: None' + assert error == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_force_delete_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'storage/qtrees', SRR['qtree_record']), + ]) + module_args = { + 'use_rest': 'always', + 'force_delete': False, + 'state': 'absent' + } + error = 'Error: force_delete option is not supported for REST, unless set to true.' + assert error == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rename_qtree_not_used_with_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ]) + module_args = { + 'use_rest': 'always', + } + my_obj = create_module(qtree_module, DEFAULT_ARGS, module_args) + error = 'Internal error, use modify with REST' + assert error in expect_and_capture_ansible_exception(my_obj.rename_qtree, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_quota_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_quota_policy.py new file mode 100644 index 000000000..e7eb3283c --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_quota_policy.py @@ -0,0 +1,174 @@ +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_quota_policy ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_quota_policy \ + import NetAppOntapQuotaPolicy as quota_policy_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'quota': + xml = self.build_quota_policy_info(self.params, True) + if self.kind == 'quota_not_assigned': + xml = self.build_quota_policy_info(self.params, False) + elif self.kind == 'zapi_error': + error = netapp_utils.zapi.NaApiError('test', 'error') + raise error + self.xml_out = xml + return xml + + @staticmethod + def build_quota_policy_info(params, assigned): + xml = netapp_utils.zapi.NaElement('xml') + attributes = {'num-records': 1, + 'attributes-list': { + 'quota-policy-info': { + 'policy-name': params['name']}, + 'vserver-info': { + 'quota-policy': params['name'] if assigned else 'default'} + }} + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_quota_policy ''' + + def setUp(self): + self.mock_quota_policy = { + 'state': 'present', + 'vserver': 'test_vserver', + 'name': 'test_policy' + } + + def mock_args(self): + return { + 'state': self.mock_quota_policy['state'], + 'vserver': self.mock_quota_policy['vserver'], + 'name': self.mock_quota_policy['name'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_quota_policy_mock_object(self, kind=None): + policy_obj = quota_policy_module() + if kind is None: + policy_obj.server = MockONTAPConnection() + else: + policy_obj.server = MockONTAPConnection(kind=kind, data=self.mock_quota_policy) + return policy_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + quota_policy_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_successfully_create(self): + set_module_args(self.mock_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_quota_policy_mock_object().apply() + assert exc.value.args[0]['changed'] + + def test_create_idempotency(self): + set_module_args(self.mock_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_quota_policy_mock_object('quota').apply() + assert not exc.value.args[0]['changed'] + + def test_cannot_delete(self): + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_quota_policy_mock_object('quota').apply() + msg = 'Error policy test_policy cannot be deleted as it is assigned to the vserver test_vserver' + assert msg == exc.value.args[0]['msg'] + + def test_successfully_delete(self): + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_quota_policy_mock_object('quota_not_assigned').apply() + assert exc.value.args[0]['changed'] + + def test_delete_idempotency(self): + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_quota_policy_mock_object().apply() + assert not exc.value.args[0]['changed'] + + def test_successfully_assign(self): + data = self.mock_args() + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_quota_policy_mock_object('quota_not_assigned').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_quota_policy.NetAppOntapQuotaPolicy.get_quota_policy') + def test_successful_rename(self, get_volume): + data = self.mock_args() + data['name'] = 'new_policy' + data['from_name'] = 'test_policy' + set_module_args(data) + current = { + 'name': 'test_policy' + } + get_volume.side_effect = [ + None, + current + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_quota_policy_mock_object('quota').apply() + assert exc.value.args[0]['changed'] + + def test_error(self): + data = self.mock_args() + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_quota_policy_mock_object('zapi_error').get_quota_policy() + assert exc.value.args[0]['msg'] == 'Error fetching quota policy test_policy: NetApp API failed. Reason - test:error' + with pytest.raises(AnsibleFailJson) as exc: + self.get_quota_policy_mock_object('zapi_error').create_quota_policy() + assert exc.value.args[0]['msg'] == 'Error creating quota policy test_policy: NetApp API failed. Reason - test:error' + with pytest.raises(AnsibleFailJson) as exc: + self.get_quota_policy_mock_object('zapi_error').delete_quota_policy() + assert exc.value.args[0]['msg'] == 'Error deleting quota policy test_policy: NetApp API failed. Reason - test:error' + data['name'] = 'new_policy' + data['from_name'] = 'test_policy' + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + self.get_quota_policy_mock_object('zapi_error').rename_quota_policy() + assert exc.value.args[0]['msg'] == 'Error renaming quota policy test_policy: NetApp API failed. Reason - test:error' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_quotas.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_quotas.py new file mode 100644 index 000000000..cd03989c6 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_quotas.py @@ -0,0 +1,853 @@ +# (c) 2019-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_quotas ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings,\ + assert_warning_was_raised, call_main, patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_quotas \ + import NetAppONTAPQuotas as my_module, main as my_main + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +SRR = rest_responses({ + # module specific responses + 'quota_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansible" + }, + "files": { + "hard_limit": "100", + "soft_limit": "80" + }, + "qtree": { + "id": "1", + "name": "qt1" + }, + "space": { + "hard_limit": "1222800", + "soft_limit": "51200" + }, + "type": "user", + "user_mapping": False, + "users": [{"name": "quota_user"}], + "uuid": "264a9e0b-2e03-11e9-a610-005056a7b72d", + "volume": {"name": "fv", "uuid": "264a9e0b-2e03-11e9-a610-005056a7b72da"}, + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + }, + } + ], + "num_records": 1 + }, None + ), + 'quota_record_0_empty_limtis': (200, {"records": [{ + "svm": {"name": "ansible"}, + "files": {"hard_limit": 0}, + "qtree": {"id": "1", "name": "qt1"}, + "space": {"hard_limit": 0}, + "type": "user", + "user_mapping": False, + "users": [{"name": "quota_user"}], + "uuid": "264a9e0b-2e03-11e9-a610-005056a7b72d", + "volume": {"name": "fv", "uuid": "264a9e0b-2e03-11e9-a610-005056a7b72da"}, + "target": {"name": "20:05:00:50:56:b3:0c:fa"}, + }], "num_records": 1}, None), + 'quota_status': ( + 200, + { + "records": [ + { + "quota": {"state": "off"} + } + ], + "num_records": 1 + }, None + ), + 'quota_on': ( + 200, + { + "records": [ + { + "quota": {"state": "on"} + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None), + "error_5308572": (409, None, {'code': 5308572, 'message': 'Expected delete error'}), + "error_5308569": (409, None, {'code': 5308569, 'message': 'Expected delete error'}), + "error_5308568": (409, None, {'code': 5308568, 'message': 'Expected create error'}), + "error_5308571": (409, None, {'code': 5308571, 'message': 'Expected create error'}), + "error_5308567": (409, None, {'code': 5308567, 'message': 'Expected modify error'}), + 'error_rest': (404, None, {"message": "temporarily locked from changes", "code": "4", "target": "uuid"}), + "volume_uuid": (200, {"records": [{ + 'uuid': 'sdgthfd' + }], 'num_records': 1}, None), + 'job_info': (200, { + "job": { + "uuid": "d78811c1-aebc-11ec-b4de-005056b30cfa", + "_links": {"self": {"href": "/api/cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa"}} + }}, None), + 'job_not_found': (404, "", {"message": "entry doesn't exist", "code": "4", "target": "uuid"}) +}) + + +quota_policy = { + 'num-records': 1, + 'attributes-list': {'quota-entry': {'volume': 'ansible', 'policy-name': 'policy_name', 'perform-user-mapping': 'true', + 'file-limit': '-', 'disk-limit': '-', 'quota-target': '/vol/ansible', + 'soft-file-limit': '-', 'soft-disk-limit': '-', 'threshold': '-'}}, +} + +quota_policies = { + 'num-records': 2, + 'attributes-list': [{'quota-policy-info': {'policy-name': 'p1'}}, + {'quota-policy-info': {'policy-name': 'p2'}}], +} + +ZRR = zapi_responses({ + 'quota_policy': build_zapi_response(quota_policy, 1), + 'quota_on': build_zapi_response({'status': 'on'}, 1), + 'quota_off': build_zapi_response({'status': 'off'}, 1), + 'quota_policies': build_zapi_response(quota_policies, 1), + 'quota_fail': build_zapi_error('TEST', 'This exception is from the unit test'), + 'quota_fail_13001': build_zapi_error('13001', 'success'), + 'quota_fail_14958': build_zapi_error('14958', 'No valid quota rules found'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'volume': 'ansible', + 'vserver': 'ansible', + 'quota_target': '/vol/ansible', + 'type': 'user', + 'use_rest': 'never' +} + + +def test_module_fail_when_required_args_missing(): + error = create_module(my_module, fail=True)['msg'] + assert 'missing required arguments:' in error + + +def test_ensure_get_called(): + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['empty']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + quotas = my_obj.get_quotas() + print('QUOTAS', quotas) + assert quotas is None + + +def test_ensure_get_quota_not_called(): + args = dict(DEFAULT_ARGS) + args.pop('quota_target') + args.pop('type') + my_obj = create_module(my_module, args) + assert my_obj.get_quotas() is None + + +def test_ensure_get_called_existing(): + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + quotas = my_obj.get_quotas() + print('QUOTAS', quotas) + assert quotas + + +def test_successful_create(): + ''' creating quota and testing idempotency ''' + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['no_records']), + ('ZAPI', 'quota-status', ZRR['quota_on']), + ('ZAPI', 'quota-set-entry', ZRR['success']), + ('ZAPI', 'quota-resize', ZRR['success']), + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ('ZAPI', 'quota-status', ZRR['quota_on']), + ]) + module_args = { + 'file_limit': '3', + 'disk_limit': '4', + 'perform_user_mapping': False, + 'policy': 'policy', + 'soft_file_limit': '3', + 'soft_disk_limit': '4', + 'threshold': '10', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_successful_delete(): + ''' deleting quota and testing idempotency ''' + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ('ZAPI', 'quota-status', ZRR['quota_on']), + ('ZAPI', 'quota-delete-entry', ZRR['success']), + ('ZAPI', 'quota-resize', ZRR['success']), + ('ZAPI', 'quota-list-entries-iter', ZRR['no_records']), + ('ZAPI', 'quota-status', ZRR['quota_on']), + ]) + module_args = { + 'policy': 'policy', + 'state': 'absent' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_successful_modify(dont_sleep): + ''' modifying quota and testing idempotency ''' + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ('ZAPI', 'quota-status', ZRR['quota_on']), + ('ZAPI', 'quota-modify-entry', ZRR['success']), + ('ZAPI', 'quota-off', ZRR['success']), + ('ZAPI', 'quota-on', ZRR['success']), + ]) + module_args = { + 'activate_quota_on_change': 'reinitialize', + 'file_limit': '3', + 'policy': 'policy', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_quota_on_off(): + ''' quota set on or off ''' + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ('ZAPI', 'quota-status', ZRR['quota_off']), + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ('ZAPI', 'quota-status', ZRR['quota_on']), + ('ZAPI', 'quota-off', ZRR['success']), + ]) + module_args = {'set_quota_status': False} + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ZAPI', 'quota-status', ZRR['quota_fail']), + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_fail']), + ('ZAPI', 'quota-set-entry', ZRR['quota_fail']), + ('ZAPI', 'quota-delete-entry', ZRR['quota_fail']), + ('ZAPI', 'quota-modify-entry', ZRR['quota_fail']), + ('ZAPI', 'quota-on', ZRR['quota_fail']), + ('ZAPI', 'quota-policy-get-iter', ZRR['quota_fail']), + ('ZAPI', 'quota-resize', ZRR['quota_fail']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + assert 'Error fetching quotas status info' in expect_and_capture_ansible_exception(my_obj.get_quota_status, 'fail')['msg'] + assert 'Error fetching quotas info' in expect_and_capture_ansible_exception(my_obj.get_quotas, 'fail')['msg'] + assert 'Error adding/modifying quota entry' in expect_and_capture_ansible_exception(my_obj.quota_entry_set, 'fail')['msg'] + assert 'Error deleting quota entry' in expect_and_capture_ansible_exception(my_obj.quota_entry_delete, 'fail')['msg'] + assert 'Error modifying quota entry' in expect_and_capture_ansible_exception(my_obj.quota_entry_modify, 'fail', {})['msg'] + assert 'Error setting quota-on for ansible' in expect_and_capture_ansible_exception(my_obj.on_or_off_quota, 'fail', 'quota-on')['msg'] + assert 'Error fetching quota policies' in expect_and_capture_ansible_exception(my_obj.get_quota_policies, 'fail')['msg'] + assert 'Error setting quota-resize for ansible:' in expect_and_capture_ansible_exception(my_obj.resize_quota, 'fail')['msg'] + + +def test_get_quota_policies(): + register_responses([ + ('ZAPI', 'quota-policy-get-iter', ZRR['quota_policies']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + policies = my_obj.get_quota_policies() + assert len(policies) == 2 + + +def test_debug_quota_get_error_fail(): + register_responses([ + ('ZAPI', 'quota-policy-get-iter', ZRR['quota_policies']), + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.debug_quota_get_error, 'fail', 'dummy error')['msg'] + assert error.startswith('Error fetching quotas info: dummy error - current vserver policies: ') + + +def test_debug_quota_get_error_success(): + register_responses([ + ('ZAPI', 'quota-policy-get-iter', ZRR['quota_policy']), + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + quotas = my_obj.debug_quota_get_error('dummy error') + print('QUOTAS', quotas) + assert quotas + + +def test_get_no_quota_retry_on_13001(): + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_fail_13001']), + ]) + module_args = {'policy': 'policy'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = expect_and_capture_ansible_exception(my_obj.get_quotas, 'fail')['msg'] + assert error.startswith('Error fetching quotas info for policy policy') + + +def test_get_quota_retry_on_13001(): + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_fail_13001']), + ('ZAPI', 'quota-policy-get-iter', ZRR['quota_policy']), + ('ZAPI', 'quota-list-entries-iter', ZRR['quota_policy']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + quotas = my_obj.get_quotas() + print('QUOTAS', quotas) + assert quotas + + +def test_resize_warning(): + ''' warning as resize is not allowed if all rules were deleted ''' + register_responses([ + ('ZAPI', 'quota-resize', ZRR['quota_fail_14958']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.resize_quota('delete') + assert_warning_was_raised('Last rule deleted, but quota is on as resize is not allowed.') + + +def test_quota_on_warning(): + ''' warning as quota-on is not allowed if all rules were deleted ''' + register_responses([ + ('ZAPI', 'quota-on', ZRR['quota_fail_14958']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.on_or_off_quota('quota-on', 'delete') + print_warnings() + assert_warning_was_raised('Last rule deleted, quota is off.') + + +def test_convert_size_format(): + module_args = {'disk_limit': '10MB'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.convert_to_kb_or_bytes('disk_limit') + print(my_obj.parameters) + assert my_obj.parameters['disk_limit'] == '10240' + my_obj.parameters['disk_limit'] = '10' + assert my_obj.convert_to_kb_or_bytes('disk_limit') + print(my_obj.parameters) + assert my_obj.parameters['disk_limit'] == '10' + my_obj.parameters['disk_limit'] = '10tB' + assert my_obj.convert_to_kb_or_bytes('disk_limit') + print(my_obj.parameters) + assert my_obj.parameters['disk_limit'] == str(10 * 1024 * 1024 * 1024) + my_obj.parameters['disk_limit'] = '' + assert not my_obj.convert_to_kb_or_bytes('disk_limit') + print(my_obj.parameters) + assert my_obj.parameters['disk_limit'] == '' + + +def test_error_convert_size_format(): + module_args = { + 'disk_limit': '10MBi', + 'quota_target': '' + } + error = create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error.startswith('disk_limit input string is not a valid size format') + module_args = { + 'soft_disk_limit': 'MBi', + 'quota_target': '' + } + error = create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error.startswith('soft_disk_limit input string is not a valid size format') + module_args = { + 'soft_disk_limit': '10MB10', + 'quota_target': '' + } + error = create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error.startswith('soft_disk_limit input string is not a valid size format') + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_has_netapp_lib(has_netapp_lib): + has_netapp_lib.return_value = False + assert call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] == 'Error: the python NetApp-Lib module is required. Import error: None' + + +def create_from_main(): + register_responses([ + ('ZAPI', 'quota-list-entries-iter', ZRR['no_records']), + ('ZAPI', 'quota-status', ZRR['quota_on']), + ('ZAPI', 'quota-set-entry', ZRR['success']), + ]) + assert call_main(my_main, DEFAULT_ARGS)['changed'] + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'volume': 'ansible', + 'vserver': 'ansible', + 'quota_target': 'quota_user', + 'qtree': 'qt1', + 'type': 'user' +} + + +def test_rest_error_get(): + '''Test error rest get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on getting quota rule info' in error + + +def test_rest_successful_create(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('POST', 'storage/quota/rules', SRR['empty_good']), + ]) + module_args = { + "users": [{"name": "quota_user"}], + } + assert create_and_apply(my_module, ARGS_REST) + + +@patch('time.sleep') +def test_rest_successful_create_job_error(sleep): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('POST', 'storage/quota/rules', SRR['job_info']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'storage/volumes', SRR['volume_uuid']) + ]) + module_args = { + "users": [{"name": "quota_user"}], + } + assert create_and_apply(my_module, ARGS_REST) + print_warnings() + assert_warning_was_raised('Ignoring job status, assuming success.') + + +def test_rest_error_create(): + '''Test error rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('POST', 'storage/quota/rules', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on creating quotas rule:' in error + + +def test_delete_rest(): + ''' Test delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('DELETE', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_error_delete_rest(): + ''' Test error delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('DELETE', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['generic_error']), + ]) + module_args = { + 'state': 'absent' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on deleting quotas rule:' in error + + +def test_modify_files_limit_rest(): + ''' Test modify with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_on']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['empty_good']), + ]) + module_args = { + "file_limit": "122", "soft_file_limit": "90" + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_modify_space_limit_rest(): + ''' Test modify with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_on']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['empty_good']), + ]) + module_args = { + "disk_limit": "1024", "soft_disk_limit": "80" + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_modify_rest_error(): + ''' Test negative modify with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['generic_error']), + ]) + module_args = { + 'perform_user_mapping': True + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on modifying quotas rule:' in error + + +@patch('time.sleep') +def test_modify_rest_temporary_locked_error(sleep): + ''' Test negative modify with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + # wait for 60s if we get temporary locl error. + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_rest']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_rest']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['success']), + + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + # error persist even after 60s + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_rest']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_rest']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_rest']), + + # wait 60s in create for temporary locked error. + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('POST', 'storage/quota/rules', SRR['error_rest']), + ('POST', 'storage/quota/rules', SRR['success']), + ]) + module_args = { + 'perform_user_mapping': True + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + assert 'Error on modifying quotas rule:' in create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + + +def test_rest_successful_create_idempotency(): + '''Test successful rest create''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record_0_empty_limtis']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record_0_empty_limtis']), + ('GET', 'storage/volumes', SRR['quota_status']) + ]) + assert create_and_apply(my_module, ARGS_REST)['changed'] is False + module_args = { + "disk_limit": "0", "soft_disk_limit": "-", "file_limit": 0, "soft_file_limit": "-" + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] is False + module_args = { + "disk_limit": "0", "soft_disk_limit": "-1", "file_limit": "0", "soft_file_limit": "-1" + } + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] is False + + +def test_rest_successful_delete_idempotency(): + '''Test successful rest delete''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_status']), + ]) + module_args = {'use_rest': 'always', 'state': 'absent'} + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] is False + + +def test_modify_quota_status_rest(): + ''' Test modify quota status and error with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('PATCH', 'storage/volumes/264a9e0b-2e03-11e9-a610-005056a7b72da', SRR['empty_good']) + ]) + module_args = {"set_quota_status": "on"} + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_error_convert_size_format_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ]) + module_args = { + 'disk_limit': '10MBi', + 'quota_target': '' + } + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert error.startswith('disk_limit input string is not a valid size format') + module_args = { + 'soft_disk_limit': 'MBi', + 'quota_target': '' + } + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert error.startswith('soft_disk_limit input string is not a valid size format') + module_args = { + 'soft_disk_limit': '10MB10', + 'quota_target': '' + } + error = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert error.startswith('soft_disk_limit input string is not a valid size format') + + +def test_convert_size_format_rest(): + module_args = {'disk_limit': '10MB'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.convert_to_kb_or_bytes('disk_limit') + print(my_obj.parameters) + assert my_obj.parameters['disk_limit'] == '10240' + my_obj.parameters['disk_limit'] = '10' + assert my_obj.convert_to_kb_or_bytes('disk_limit') + print(my_obj.parameters) + assert my_obj.parameters['disk_limit'] == '10' + my_obj.parameters['disk_limit'] = '10tB' + assert my_obj.convert_to_kb_or_bytes('disk_limit') + print(my_obj.parameters) + assert my_obj.parameters['disk_limit'] == str(10 * 1024 * 1024 * 1024) + my_obj.parameters['disk_limit'] = '' + assert not my_obj.convert_to_kb_or_bytes('disk_limit') + print(my_obj.parameters) + assert my_obj.parameters['disk_limit'] == '' + + +def test_warning_rest_delete_5308572(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('DELETE', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_5308572']) + ]) + assert create_and_apply(my_module, ARGS_REST, {'state': 'absent'})['changed'] + # assert 'Error on deleting quotas rule:' in error + msg = "Quota policy rule delete opertation succeeded. However the rule is still being enforced. To stop enforcing, "\ + "reinitialize(disable and enable again) the quota for volume ansible in SVM ansible." + assert_warning_was_raised(msg) + + +@patch('time.sleep') +def test_no_warning_rest_delete_5308572(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_on']), + ('DELETE', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_5308572']), + ('PATCH', 'storage/volumes/264a9e0b-2e03-11e9-a610-005056a7b72da', SRR['success']), + ('PATCH', 'storage/volumes/264a9e0b-2e03-11e9-a610-005056a7b72da', SRR['success']) + ]) + assert create_and_apply(my_module, ARGS_REST, {'state': 'absent', 'activate_quota_on_change': 'reinitialize'})['changed'] + assert_no_warnings() + + +def test_warning_rest_delete_5308569(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('DELETE', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_5308569']) + ]) + assert create_and_apply(my_module, ARGS_REST, {'state': 'absent'})['changed'] + # assert 'Error on deleting quotas rule:' in error + msg = "Quota policy rule delete opertation succeeded. However quota resize failed due to an internal error. To make quotas active, "\ + "reinitialize(disable and enable again) the quota for volume ansible in SVM ansible." + assert_warning_was_raised(msg) + + +@patch('time.sleep') +def test_no_warning_rest_delete_5308569(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_on']), + ('DELETE', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_5308569']), + ('PATCH', 'storage/volumes/264a9e0b-2e03-11e9-a610-005056a7b72da', SRR['success']), + ('PATCH', 'storage/volumes/264a9e0b-2e03-11e9-a610-005056a7b72da', SRR['success']) + ]) + assert create_and_apply(my_module, ARGS_REST, {'state': 'absent', 'activate_quota_on_change': 'reinitialize'})['changed'] + assert_no_warnings() + + +def test_warning_rest_create_5308568(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('POST', 'storage/quota/rules', SRR['error_5308568']), + ('GET', 'storage/volumes', SRR['volume_uuid']) + ]) + assert create_and_apply(my_module, ARGS_REST)['changed'] + msg = "Quota policy rule create opertation succeeded. However quota resize failed due to an internal error. To make quotas active, "\ + "reinitialize(disable and enable again) the quota for volume ansible in SVM ansible." + assert_warning_was_raised(msg) + + +@patch('time.sleep') +def test_no_warning_rest_create_5308568(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_on']), + ('POST', 'storage/quota/rules', SRR['error_5308568']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('PATCH', 'storage/volumes/sdgthfd', SRR['success']), + ('PATCH', 'storage/volumes/sdgthfd', SRR['success']) + ]) + assert create_and_apply(my_module, ARGS_REST, {'activate_quota_on_change': 'reinitialize'})['changed'] + assert_no_warnings() + + +def test_warning_rest_create_5308571(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_status']), + ('POST', 'storage/quota/rules', SRR['error_5308571']), + ('GET', 'storage/volumes', SRR['volume_uuid']) + ]) + assert create_and_apply(my_module, ARGS_REST)['changed'] + msg = "Quota policy rule create opertation succeeded. but quota resize is skipped. To make quotas active, "\ + "reinitialize(disable and enable again) the quota for volume ansible in SVM ansible." + assert_warning_was_raised(msg) + + +@patch('time.sleep') +def test_no_warning_rest_create_5308571(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['quota_on']), + ('POST', 'storage/quota/rules', SRR['error_5308568']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('PATCH', 'storage/volumes/sdgthfd', SRR['success']), + ('PATCH', 'storage/volumes/sdgthfd', SRR['success']) + ]) + assert create_and_apply(my_module, ARGS_REST, {'activate_quota_on_change': 'reinitialize'})['changed'] + assert_no_warnings() + + +def test_warning_rest_modify_5308567(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_on']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_5308567']), + ]) + module_args = {"soft_file_limit": "100"} + assert create_and_apply(my_module, ARGS_REST, module_args) + msg = "Quota policy rule modify opertation succeeded. However quota resize failed due to an internal error. To make quotas active, "\ + "reinitialize(disable and enable again) the quota for volume ansible in SVM ansible." + assert_warning_was_raised(msg) + + +@patch('time.sleep') +def test_no_warning_rest_modify_5308567(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/quota/rules', SRR['quota_record']), + ('GET', 'storage/volumes', SRR['quota_on']), + ('PATCH', 'storage/quota/rules/264a9e0b-2e03-11e9-a610-005056a7b72d', SRR['error_5308567']), + ('PATCH', 'storage/volumes/264a9e0b-2e03-11e9-a610-005056a7b72da', SRR['success']), + ('PATCH', 'storage/volumes/264a9e0b-2e03-11e9-a610-005056a7b72da', SRR['success']) + ]) + module_args = {"soft_file_limit": "100", 'activate_quota_on_change': 'reinitialize'} + assert create_and_apply(my_module, ARGS_REST, module_args)['changed'] + assert_no_warnings() + + +def test_if_all_methods_catch_exception_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/quota/rules', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('POST', 'storage/quota/rules', SRR['generic_error']), + ('DELETE', 'storage/quota/rules/abdcdef', SRR['generic_error']), + ('PATCH', 'storage/quota/rules/abdcdef', SRR['generic_error']), + ('PATCH', 'storage/volumes/ghijklmn', SRR['generic_error']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + + ]) + my_obj = create_module(my_module, ARGS_REST) + my_obj.quota_uuid = 'abdcdef' + my_obj.volume_uuid = 'ghijklmn' + assert 'Error on getting quota rule info' in expect_and_capture_ansible_exception(my_obj.get_quotas_rest, 'fail')['msg'] + assert 'Error on getting quota status info' in expect_and_capture_ansible_exception(my_obj.get_quota_status_or_volume_id_rest, 'fail')['msg'] + assert 'Error on getting volume' in expect_and_capture_ansible_exception(my_obj.get_quota_status_or_volume_id_rest, 'fail', True)['msg'] + assert 'does not exist' in expect_and_capture_ansible_exception(my_obj.get_quota_status_or_volume_id_rest, 'fail', True)['msg'] + assert 'Error on creating quotas rule' in expect_and_capture_ansible_exception(my_obj.quota_entry_set_rest, 'fail')['msg'] + assert 'Error on deleting quotas rule' in expect_and_capture_ansible_exception(my_obj.quota_entry_delete_rest, 'fail')['msg'] + assert 'Error on modifying quotas rule' in expect_and_capture_ansible_exception(my_obj.quota_entry_modify_rest, 'fail', {})['msg'] + assert 'Error setting quota-on for ansible' in expect_and_capture_ansible_exception(my_obj.on_or_off_quota_rest, 'fail', 'quota-on')['msg'] + error = "Error: Qtree cannot be specified for a tree type rule" + assert error in create_module(my_module, ARGS_REST, {'qtree': 'qtree1', 'type': 'tree'}, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_cli.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_cli.py new file mode 100644 index 000000000..d9f89a21a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_cli.py @@ -0,0 +1,128 @@ +# (c) 2019-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_rest_cli''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_module, expect_and_capture_ansible_exception, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_rest_cli import NetAppONTAPCommandREST as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'allow': (200, {'Allow': ['GET', 'WHATEVER']}, None) +}, False) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'auto', + 'command': 'volume', + 'verb': 'GET', + 'params': {'fields': 'size,percent_used'} +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + args = dict(DEFAULT_ARGS) + args.pop('verb') + error = 'missing required arguments: verb' + assert error in call_main(my_main, args, fail=True)['msg'] + + +def test_rest_cli(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/volume', SRR['empty_good']), + ]) + assert call_main(my_main, DEFAULT_ARGS)['changed'] is False + + +def test_rest_cli_options(): + module_args = {'verb': 'OPTIONS'} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('OPTIONS', 'private/cli/volume', SRR['allow']), + ]) + exit_json = call_main(my_main, DEFAULT_ARGS, module_args) + assert not exit_json['changed'] + assert 'Allow' in exit_json['msg'] + + +def test_negative_connection_error(): + module_args = {'verb': 'OPTIONS'} + register_responses([ + ('GET', 'cluster', SRR['generic_error']), + ]) + msg = "failed to connect to REST over hostname: ['Expected error']. Use na_ontap_command for non-rest CLI." + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def check_verb(verb): + module_args = {'verb': verb} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + (verb, 'private/cli/volume', SRR['allow']), + ], "test_verbs") + + exit_json = call_main(my_main, DEFAULT_ARGS, module_args) + assert not exit_json['changed'] if verb in ['GET', 'OPTIONS'] else exit_json['changed'] + assert 'Allow' in exit_json['msg'] + # assert mock_request.call_args[0][0] == verb + + +def test_verbs(): + for verb in ['POST', 'DELETE', 'PATCH', 'OPTIONS', 'PATCH']: + check_verb(verb) + + +def test_check_mode(): + module_args = {'verb': 'GET'} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.module.check_mode = True + result = expect_and_capture_ansible_exception(my_obj.apply, 'exit') + assert result['changed'] is False + msg = "Would run command: 'volume'" + assert msg in result['msg'] + + +def test_negative_verb(): + module_args = {'verb': 'GET'} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.verb = 'INVALID' + msg = 'Error: unexpected verb INVALID' + assert msg in expect_and_capture_ansible_exception(my_obj.apply, 'fail')['msg'] + + +def test_negative_error(): + module_args = {'verb': 'GET'} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'private/cli/volume', SRR['generic_error']), + ]) + msg = 'Error: Expected error' + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_info.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_info.py new file mode 100644 index 000000000..bf678e3ac --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_rest_info.py @@ -0,0 +1,1195 @@ +# (c) 2020-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' Unit Tests NetApp ONTAP REST APIs Ansible module: na_ontap_rest_info ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import call_main, create_module, \ + expect_and_capture_ansible_exception, patch_ansible, create_and_apply, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_rest_info \ + import NetAppONTAPGatherInfo as ontap_rest_info_module, main as my_main + +if sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # common responses + 'validate_ontap_version_pass': ( + 200, dict(version=dict(generation=9, major=10, minor=1, full='dummy_9_10_1')), None), + 'validate_ontap_version_fail': (200, None, 'API not found error'), + 'error_invalid_api': (500, None, {'code': 3, 'message': 'Invalid API'}), + 'error_user_is_not_authorized': (500, None, {'code': 6, 'message': 'user is not authorized'}), + 'error_no_processing': (500, None, {'code': 123, 'message': 'error reported as is'}), + 'error_no_aggr_recommendation': ( + 500, None, {'code': 19726344, 'message': 'No recommendation can be made for this cluster'}), + 'get_subset_info': (200, + {'_links': {'self': {'href': 'dummy_href'}}, + 'num_records': 3, + 'records': [{'name': 'dummy_vol1'}, + {'name': 'dummy_vol2'}, + {'name': 'dummy_vol3'}], + 'version': 'ontap_version'}, None), + 'get_subset_info_with_next': (200, + {'_links': {'self': {'href': 'dummy_href'}, + 'next': {'href': '/api/next_record_api'}}, + 'num_records': 3, + 'records': [{'name': 'dummy_vol1'}, + {'name': 'dummy_vol2'}, + {'name': 'dummy_vol3'}], + 'version': 'ontap_version'}, None), + 'get_next_record': (200, + {'_links': {'self': {'href': 'dummy_href'}}, + 'num_records': 2, + 'records': [{'name': 'dummy_vol1'}, + {'name': 'dummy_vol2'}], + 'version': 'ontap_version'}, None), + 'metrocluster_post': (200, + {'job': { + 'uuid': 'fde79888-692a-11ea-80c2-005056b39fe7', + '_links': { + 'self': { + 'href': '/api/cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7'}} + }}, + None), + 'metrocluster_return': (200, + {"_links": { + "self": { + "href": "/api/cluster/metrocluster/diagnostics" + } + }, "aggregate": { + "state": "ok", + "summary": { + "message": "" + }, "timestamp": "2020-07-22T16:42:51-07:00" + }}, None), + 'job': (200, + { + "uuid": "cca3d070-58c6-11ea-8c0c-005056826c14", + "description": "POST /api/cluster/metrocluster", + "state": "success", + "message": "There are not enough disks in Pool1.", + "code": 2432836, + "start_time": "2020-02-26T10:35:44-08:00", + "end_time": "2020-02-26T10:47:38-08:00", + "_links": { + "self": { + "href": "/api/cluster/jobs/cca3d070-58c6-11ea-8c0c-005056826c14" + } + } + }, None), + 'get_private_cli_subset_info': (200, + { + 'records': [ + {'node': 'node1', 'check_type': 'type'}, + {'node': 'node1', 'check_type': 'type'}, + {'node': 'node1', 'check_type': 'type'}], + "num_records": 3}, None), + 'get_private_cli_vserver_security_file_directory_info': ( + 200, + { + 'records': [ + {'acls': ['junk', 'junk', 'DACL - ACEs', 'AT-user-0x123']}, + {'node': 'node1', 'check_type': 'type'}, + {'node': 'node1', 'check_type': 'type'}], + "num_records": 3}, None), + 'lun_info': (200, {'records': [{"serial_number": "z6CcD+SK5mPb"}]}, None), + 'volume_info': (200, {"uuid": "7882901a-1aef-11ec-a267-005056b30cfa"}, None), + 'svm_uuid': (200, {"records": [{"uuid": "test_uuid"}], "num_records": 1}, None), + 'get_uuid_policy_id_export_policy': ( + 200, + { + "records": [{ + "svm": { + "uuid": "uuid", + "name": "svm"}, + "id": 123, + "name": "ansible" + }], + "num_records": 1}, None), + 'vscan_on_access_policies': ( + 200, {"records": [ + { + "name": "on-access-test", + "mandatory": True, + "scope": { + "scan_readonly_volumes": True, + "exclude_paths": [ + "\\dir1\\dir2\\name", + "\\vol\\a b", + "\\vol\\a,b\\" + ], + "scan_without_extension": True, + "include_extensions": [ + "mp*", + "txt" + ], + "exclude_extensions": [ + "mp*", + "txt" + ], + "only_execute_access": True, + "max_file_size": "2147483648" + }, + "enabled": True + } + ]}, None + ), + 'vscan_on_demand_policies': ( + 200, {"records": [ + { + "log_path": "/vol0/report_dir", + "scan_paths": [ + "/vol1/", + "/vol2/cifs/" + ], + "name": "task-1", + "svm": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "scope": { + "exclude_paths": [ + "/vol1/cold-files/", + "/vol1/cifs/names" + ], + "scan_without_extension": True, + "include_extensions": [ + "vmdk", + "mp*" + ], + "exclude_extensions": [ + "mp3", + "mp4" + ], + "max_file_size": "10737418240" + }, + "schedule": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "weekly", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + } + } + ]}, None + ), + 'vscan_scanner_pools': ( + 200, {"records": [ + { + "cluster": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "cluster1", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + "name": "scanner-1", + "servers": [ + "1.1.1.1", + "10.72.204.27", + "vmwin204-27.fsct.nb" + ], + "privileged_users": [ + "cifs\\u1", + "cifs\\u2" + ], + "svm": { + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "role": "primary" + } + ]}, None + ) +}) + +ALL_SUBSETS = ['application/applications', + 'application/consistency-groups', + 'application/templates', + 'cloud/targets', + 'cluster', + 'cluster/chassis', + 'cluster/counter/tables', + 'cluster/fireware/history', + 'cluster/jobs', + 'cluster/licensing/capacity-pools', + 'cluster/licensing/license-managers', + 'cluster/licensing/licenses', + 'cluster/mediators', + 'cluster/metrics', + 'cluster/metrocluster', + 'cluster/metrocluster/diagnostics', + 'cluster/metrocluster/dr-groups', + 'cluster/metrocluster/interconnects', + 'cluster/metrocluster/nodes', + 'cluster/metrocluster/operations', + 'cluster/metrocluster/svms', + 'cluster/nodes', + 'cluster/ntp/keys', + 'cluster/ntp/servers', + 'cluster/peers', + 'cluster/schedules', + 'cluster/sensors', + 'cluster/software', + 'cluster/software/download', + 'cluster/software/history', + 'cluster/software/packages', + 'cluster/web', + 'name-services/cache/group-membership/settings', + 'name-services/cache/host/settings', + 'name-services/cache/netgroup/settings', + 'name-services/cache/setting', + 'name-services/cache/unix-group/settings', + 'name-services/dns', + 'name-services/ldap', + 'name-services/ldap-schemas', + 'name-services/local-hosts', + 'name-services/name-mappings', + 'name-services/nis', + 'name-services/unix-groups', + 'name-services/unix-users', + 'network/ethernet/broadcast-domains', + 'network/ethernet/ports', + 'network/ethernet/switch/ports', + 'network/ethernet/switches', + 'network/fc/fabrics', + 'network/fc/interfaces', + 'network/fc/logins', + 'network/fc/ports', + 'network/fc/wwpn-aliases', + 'network/http-proxy', + 'network/ip/bgp/peer-groups', + 'network/ip/interfaces', + 'network/ip/routes', + 'network/ip/service-policies', + 'network/ip/subnets', + 'network/ipspaces', + 'private/support/alerts', + 'protocols/active-directory', + 'protocols/audit', + 'protocols/cifs/connections', + 'protocols/cifs/domains', + 'protocols/cifs/group-policies', + 'protocols/cifs/home-directory/search-paths', + 'protocols/cifs/local-groups', + 'protocols/cifs/local-users', + 'protocols/cifs/netbios', + 'protocols/cifs/services', + 'protocols/cifs/session/files', + 'protocols/cifs/sessions', + 'protocols/cifs/shadow-copies', + 'protocols/cifs/shadowcopy-sets', + 'protocols/cifs/shares', + 'protocols/cifs/users-and-groups/privileges', + 'protocols/cifs/unix-symlink-mapping', + 'protocols/fpolicy', + 'protocols/locks', + 'protocols/ndmp', + 'protocols/ndmp/nodes', + 'protocols/ndmp/sessions', + 'protocols/ndmp/svms', + 'protocols/nfs/connected-clients', + 'protocols/nfs/connected-client-maps', + 'protocols/nfs/connected-client-settings', + 'protocols/nfs/export-policies', + 'protocols/nfs/kerberos/interfaces', + 'protocols/nfs/kerberos/realms', + 'protocols/nfs/services', + 'protocols/nvme/interfaces', + 'protocols/nvme/services', + 'protocols/nvme/subsystems', + 'protocols/nvme/subsystem-controllers', + 'protocols/nvme/subsystem-maps', + 'protocols/s3/buckets', + 'protocols/s3/services', + 'protocols/san/fcp/services', + 'protocols/san/igroups', + 'protocols/san/iscsi/credentials', + 'protocols/san/iscsi/services', + 'protocols/san/iscsi/sessions', + 'protocols/san/lun-maps', + 'protocols/san/portsets', + 'protocols/san/vvol-bindings', + 'protocols/vscan', + 'protocols/vscan/server-status', + 'security', + 'security/accounts', + 'security/anti-ransomware/suspects', + 'security/audit', + 'security/audit/destinations', + 'security/audit/messages', + 'security/authentication/cluster/ad-proxy', + 'security/authentication/cluster/ldap', + 'security/authentication/cluster/nis', + 'security/authentication/cluster/saml-sp', + 'security/authentication/publickeys', + 'security/aws-kms', + 'security/azure-key-vaults', + 'security/certificates', + 'security/gcp-kms', + 'security/ipsec', + 'security/ipsec/ca-certificates', + 'security/ipsec/policies', + 'security/ipsec/security-associations', + 'security/key-manager-configs', + 'security/key-managers', + 'security/key-stores', + 'security/login/messages', + 'security/multi-admin-verify', + 'security/multi-admin-verify/approval-groups', + 'security/multi-admin-verify/requests', + 'security/multi-admin-verify/rules', + 'security/roles', + 'security/ssh', + 'security/ssh/svms', + 'snapmirror/policies', + 'snapmirror/relationships', + 'storage/aggregates', + 'storage/bridges', + 'storage/cluster', + 'storage/disks', + 'storage/file/clone/split-loads', + 'storage/file/clone/split-status', + 'storage/file/clone/tokens', + 'storage/file/moves', + 'storage/flexcache/flexcaches', + 'storage/flexcache/origins', + 'storage/luns', + 'storage/namespaces', + 'storage/pools', + 'storage/ports', + 'storage/qos/policies', + 'storage/qos/workloads', + 'storage/qtrees', + 'storage/quota/reports', + 'storage/quota/rules', + 'storage/shelves', + 'storage/snaplock/audit-logs', + 'storage/snaplock/compliance-clocks', + 'storage/snaplock/event-retention/operations', + 'storage/snaplock/event-retention/policies', + 'storage/snaplock/file-fingerprints', + 'storage/snaplock/litigations', + 'storage/snapshot-policies', + 'storage/switches', + 'storage/tape-devices', + 'storage/volumes', + 'storage/volume-efficiency-policies', + 'support/autosupport', + 'support/autosupport/check', + 'support/autosupport/messages', + 'support/auto-update', + 'support/auto-update/configurations', + 'support/auto-update/updates', + 'support/configuration-backup', + 'support/configuration-backup/backups', + 'support/coredump/coredumps', + 'support/ems', + 'support/ems/destinations', + 'support/ems/events', + 'support/ems/filters', + 'support/ems/messages', + 'support/snmp', + 'support/snmp/traphosts', + 'support/snmp/users', + 'svm/migrations', + 'svm/peers', + 'svm/peer-permissions', + 'svm/svms'] + +# Super Important, Metrocluster doesn't call get_subset_info and has 3 api calls instead of 1!!!! +# The metrocluster calls need to be in the correct place. The Module return the keys in a sorted list. +ALL_RESPONSES = [ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'application/applications', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'application/templates', SRR['get_subset_info']), + ('GET', 'cloud/targets', SRR['get_subset_info']), + ('GET', 'cluster', SRR['get_subset_info']), + ('GET', 'cluster/chassis', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'cluster/jobs', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'cluster/licensing/licenses', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'cluster/metrics', SRR['get_subset_info']), + ('GET', 'cluster/metrocluster', SRR['get_subset_info']), + # MCC DIAGs + ('POST', 'cluster/metrocluster/diagnostics', SRR['metrocluster_post']), + ('GET', 'cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7', SRR['job']), + ('GET', 'cluster/metrocluster/diagnostics', SRR['metrocluster_return']), + # Back to normal + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'cluster/metrocluster/nodes', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'cluster/nodes', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'cluster/ntp/servers', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'support/ems/filters', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', '*', SRR['get_subset_info']), + ('GET', 'svm/peer-permissions', SRR['get_subset_info']), + ('GET', 'svm/peers', SRR['get_subset_info']), + ('GET', 'svm/svms', SRR['get_private_cli_subset_info']), +] + + +def set_default_args(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False + }) + + +def set_args_run_ontap_version_check(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'max_records': 1024, + 'gather_subset': ['volume_info'] + }) + + +def set_args_run_metrocluster_diag(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'max_records': 1024, + 'gather_subset': ['cluster/metrocluster/diagnostics'] + }) + + +def set_args_run_ontap_gather_facts_for_vserver_info(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'max_records': 1024, + 'gather_subset': ['vserver_info'] + }) + + +def set_args_run_ontap_gather_facts_for_volume_info(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'max_records': 1024, + 'gather_subset': ['volume_info'] + }) + + +def set_args_run_ontap_gather_facts_for_all_subsets(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'max_records': 1024, + 'gather_subset': ['all'] + }) + + +def set_args_run_ontap_gather_facts_for_all_subsets_with_fields_section_pass(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'max_records': 1024, + 'fields': '*', + 'gather_subset': ['all'] + }) + + +def set_args_run_ontap_gather_facts_for_all_subsets_with_fields_section_fail(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'max_records': 1024, + 'fields': ['uuid', 'name', 'node'], + 'gather_subset': ['all'] + }) + + +def set_args_run_ontap_gather_facts_for_aggregate_info_with_fields_section_pass(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'fields': ['uuid', 'name', 'node'], + 'validate_certs': False, + 'max_records': 1024, + 'gather_subset': ['aggregate_info'] + }) + + +def set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'https': True, + 'validate_certs': False, + 'max_records': 3, + 'gather_subset': ['volume_info'] + }) + + +def test_run_ontap_version_check_for_9_6_pass(): + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['get_subset_info']), + ]) + assert not create_and_apply(ontap_rest_info_module, set_args_run_ontap_version_check())['changed'] + + +def test_run_ontap_version_check_for_10_2_pass(): + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['get_subset_info']), + ]) + assert not create_and_apply(ontap_rest_info_module, set_args_run_ontap_version_check())['changed'] + + +def test_run_ontap_version_check_for_9_2_fail(): + ''' Test for Checking the ONTAP version ''' + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_fail']), + ]) + assert call_main(my_main, set_args_run_ontap_version_check(), + fail=True)['msg'] == 'Error using REST for version, error: %s.' % SRR['validate_ontap_version_fail'][2] + + +def test_version_warning_message(): + gather_subset = ['cluster/metrocluster/diagnostics'] + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + create_and_apply(ontap_rest_info_module, set_args_run_metrocluster_diag()) + assert_warning_was_raised('The following subset have been removed from your query as they are not supported on ' + + 'your version of ONTAP cluster/metrocluster/diagnostics requires (9, 8), ') + + +# metrocluster/diagnostics doesn't call get_subset_info and has 3 api calls instead of 1 +def test_run_metrocluster_pass(): + gather_subset = ['cluster/metrocluster/diagnostics'] + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('POST', 'cluster/metrocluster/diagnostics', SRR['metrocluster_post']), + ('GET', 'cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7', SRR['job']), + ('GET', 'cluster/metrocluster/diagnostics', SRR['metrocluster_return']), + ]) + assert set(create_and_apply(ontap_rest_info_module, set_args_run_metrocluster_diag())['ontap_info']) == set( + gather_subset) + + +def test_run_ontap_gather_facts_for_vserver_info_pass(): + gather_subset = ['svm/svms'] + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'svm/svms', SRR['get_subset_info']), + ]) + assert set(create_and_apply(ontap_rest_info_module, set_args_run_ontap_gather_facts_for_vserver_info())['ontap_info']) == set(gather_subset) + + +def test_run_ontap_gather_facts_for_volume_info_pass(): + gather_subset = ['storage/volumes'] + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['get_subset_info']), + ]) + assert set(create_and_apply(ontap_rest_info_module, set_args_run_ontap_gather_facts_for_volume_info())['ontap_info']) == set(gather_subset) + + +def test_run_ontap_gather_facts_for_all_subsets_pass(): + gather_subset = ALL_SUBSETS + register_responses(ALL_RESPONSES) + assert set(create_and_apply(ontap_rest_info_module, set_args_run_ontap_gather_facts_for_all_subsets())['ontap_info']) == set(gather_subset) + + +def test_run_ontap_gather_facts_for_all_subsets_with_fields_section_pass(): + gather_subset = ALL_SUBSETS + register_responses(ALL_RESPONSES) + assert set(create_and_apply(ontap_rest_info_module, + set_args_run_ontap_gather_facts_for_all_subsets_with_fields_section_pass() + )['ontap_info']) == set(gather_subset) + + +def test_run_ontap_gather_facts_for_all_subsets_with_fields_section_fail(): + error_message = "Error: fields: %s, only one subset will be allowed." \ + % set_args_run_ontap_gather_facts_for_aggregate_info_with_fields_section_pass()['fields'] + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ]) + assert \ + create_and_apply(ontap_rest_info_module, + set_args_run_ontap_gather_facts_for_all_subsets_with_fields_section_fail(), + fail=True + )['msg'] == error_message + + +def test_run_ontap_gather_facts_for_aggregate_info_pass_with_fields_section_pass(): + gather_subset = ['storage/aggregates'] + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/aggregates', SRR['get_subset_info']), + ]) + assert set(create_and_apply(ontap_rest_info_module, + set_args_run_ontap_gather_facts_for_aggregate_info_with_fields_section_pass() + )['ontap_info']) == set(gather_subset) + + +def test_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass(): + total_records = 5 + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['get_subset_info_with_next']), + ('GET', '/next_record_api', SRR['get_next_record']), + ]) + assert create_and_apply(ontap_rest_info_module, + set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + )['ontap_info']['storage/volumes']['num_records'] == total_records + + +def test_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass_python_keys(): + args = set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + args['use_python_keys'] = True + args['state'] = 'info' + total_records = 5 + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['get_subset_info_with_next']), + ('GET', '/next_record_api', SRR['get_next_record']), + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info']['storage_volumes']['num_records'] == total_records + + +def test_get_all_records_for_volume_info_with_parameters(): + args = set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + args['use_python_keys'] = True + args['parameters'] = {'fields': '*'} + total_records = 5 + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['get_subset_info_with_next']), + ('GET', '/next_record_api', SRR['get_next_record']), + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info']['storage_volumes']['num_records'] == total_records + + +def test_negative_error_on_get_next(): + args = set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + args['use_python_keys'] = True + args['parameters'] = {'fields': '*'} + total_records = 5 + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['get_subset_info_with_next']), + ('GET', '/next_record_api', SRR['generic_error']), + ]) + assert create_and_apply(ontap_rest_info_module, args, fail=True)['msg'] == 'Expected error' + + +def test_negative_bad_api(): + args = set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + args['use_python_keys'] = True + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['error_invalid_api']), + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info']['storage_volumes'] == 'Invalid API' + + +def test_negative_error_no_aggr_recommendation(): + args = set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + args['use_python_keys'] = True + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['error_no_aggr_recommendation']), + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info']['storage_volumes'] == 'No recommendation can be made for this cluster' + + +def test_negative_error_not_authorized(): + args = set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + args['use_python_keys'] = True + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['error_user_is_not_authorized']), + ]) + assert 'user is not authorized to make' in create_and_apply(ontap_rest_info_module, args, fail=True)['msg'] + + +def test_negative_error_no_processing(): + args = set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + args['use_python_keys'] = True + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['error_no_processing']), + ]) + assert create_and_apply(ontap_rest_info_module, args, fail=True)['msg']['message'] == 'error reported as is' + + +def test_strip_dacls(): + record = {} + response = { + 'records': [record] + } + assert ontap_rest_info_module.strip_dacls(response) is None + record['acls'] = [] + assert ontap_rest_info_module.strip_dacls(response) is None + record['acls'] = ['junk', 'junk', 'DACL - ACEs'] + assert ontap_rest_info_module.strip_dacls(response) == [] + record['acls'] = ['junk', 'junk', 'DACL - ACEs', 'AT-user-0x123'] + assert ontap_rest_info_module.strip_dacls(response) == [{'access_type': 'AT', 'user_or_group': 'user'}] + record['acls'] = ['junk', 'junk', 'DACL - ACEs', 'AT-user-0x123', 'AT2-group-0xABC'] + assert ontap_rest_info_module.strip_dacls(response) == [{'access_type': 'AT', 'user_or_group': 'user'}, + {'access_type': 'AT2', 'user_or_group': 'group'}] + + +def test_private_cli_vserver_security_file_directory(): + args = set_args_get_all_records_for_volume_info_to_check_next_api_call_functionality_pass() + args['gather_subset'] = 'private/cli/vserver/security/file-directory' + args['use_python_keys'] = True + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'private/cli/vserver/security/file-directory', SRR['get_private_cli_vserver_security_file_directory_info']), + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info'] == { + 'private_cli_vserver_security_file_directory': [{'access_type': 'AT', 'user_or_group': 'user'}]} + + +def test_get_ontap_subset_info_all_with_field(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'some/api', SRR['get_subset_info']), + ]) + my_obj = create_module(ontap_rest_info_module, set_default_args()) + subset_info = {'subset': {'api_call': 'some/api'}} + assert my_obj.get_ontap_subset_info_all('subset', 'fields', subset_info)['num_records'] == 3 + + +def test_negative_get_ontap_subset_info_all_bad_subset(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + my_obj = create_module(ontap_rest_info_module, set_default_args()) + msg = 'Specified subset bad_subset is not found, supported subsets are []' + assert expect_and_capture_ansible_exception(my_obj.get_ontap_subset_info_all, 'fail', 'bad_subset', None, {})['msg'] == msg + + +def test_demo_subset(): + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'cluster/software', SRR['get_subset_info']), + ('GET', 'svm/svms', SRR['get_subset_info']), + ('GET', 'cluster/nodes', SRR['get_subset_info']), + ]) + assert 'cluster/nodes' in call_main(my_main, set_default_args(), {'gather_subset': 'demo'})['ontap_info'] + + +def test_subset_with_default_fields(): + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/aggregates', SRR['get_subset_info']), + ]) + assert 'storage/aggregates' in \ + create_and_apply(ontap_rest_info_module, set_default_args(), {'gather_subset': 'aggr_efficiency_info'})[ + 'ontap_info'] + + +def test_negative_error_on_post(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('POST', 'api', SRR['generic_error']), + ]) + assert create_module(ontap_rest_info_module, set_default_args()).run_post({'api_call': 'api'}) is None + + +@patch('time.sleep') +def test_negative_error_on_wait_after_post(sleep_mock): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('POST', 'api', SRR['metrocluster_post']), + ('GET', 'cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7', SRR['generic_error']), + ('GET', 'cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7', SRR['generic_error']), # retries + ('GET', 'cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7', SRR['generic_error']), + ('GET', 'cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7', SRR['generic_error']), + ]) + my_obj = create_module(ontap_rest_info_module, set_default_args()) + assert expect_and_capture_ansible_exception(my_obj.run_post, 'fail', {'api_call': 'api'})['msg'] == ' - '.join( + ['Expected error'] * 4) + + +def test_owning_resource_snapshot(): + args = set_default_args() + args['gather_subset'] = 'storage/volumes/snapshots' + args['owning_resource'] = {'volume_name': 'vol1', 'svm_name': 'svm1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['volume_info']), + ('GET', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa/snapshots', SRR['volume_info']) + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info'] + + +def test_owning_resource_snapshot_missing_1_resource(): + args = set_default_args() + args['gather_subset'] = 'storage/volumes/snapshots' + args['owning_resource'] = {'volume_name': 'vol1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ]) + msg = 'Error: volume_name, svm_name are required for storage/volumes/snapshots' + assert create_and_apply(ontap_rest_info_module, args, fail=True)['msg'] == msg + + +def test_owning_resource_snapshot_missing_resource(): + args = set_default_args() + args['gather_subset'] = 'storage/volumes/snapshots' + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ]) + msg = 'Error: volume_name, svm_name are required for storage/volumes/snapshots' + assert create_and_apply(ontap_rest_info_module, args, fail=True)['msg'] == msg + + +def test_owning_resource_snapshot_volume_not_found(): + args = set_default_args() + args['gather_subset'] = 'storage/volumes/snapshots' + args['owning_resource'] = {'volume_name': 'vol1', 'svm_name': 'svm1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/volumes', SRR['generic_error']), + ]) + msg = 'Could not find volume vol1 on SVM svm1' + assert create_and_apply(ontap_rest_info_module, args, fail=True)['msg'] == msg + + +def test_owning_resource_vscan_on_access_policies(): + args = set_default_args() + args['gather_subset'] = 'protocols/vscan/on-access-policies' + args['owning_resource'] = {'svm_name': 'svm1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/test_uuid/on-access-policies', SRR['vscan_on_access_policies']) + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info'] + + +def test_owning_resource_vscan_on_demand_policies(): + args = set_default_args() + args['gather_subset'] = 'protocols/vscan/on-demand-policies' + args['owning_resource'] = {'svm_name': 'svm1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/test_uuid/on-demand-policies', SRR['vscan_on_access_policies']) + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info'] + + +def test_owning_resource_vscan_scanner_pools(): + args = set_default_args() + args['gather_subset'] = 'protocols/vscan/scanner-pools' + args['owning_resource'] = {'svm_name': 'svm1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/test_uuid/scanner-pools', SRR['vscan_scanner_pools']) + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info'] + + +def test_owning_resource_export_policies_rules(): + args = set_default_args() + args['gather_subset'] = 'protocols/nfs/export-policies/rules' + args['owning_resource'] = {'policy_name': 'policy_name', 'svm_name': 'svm1', 'rule_index': '1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'protocols/nfs/export-policies', SRR['get_uuid_policy_id_export_policy']), + ('GET', 'protocols/nfs/export-policies/123/rules/1', SRR['get_uuid_policy_id_export_policy']) + ]) + assert create_and_apply(ontap_rest_info_module, args)['ontap_info'] + + +def test_owning_resource_export_policies_rules_missing_resource(): + args = set_default_args() + args['gather_subset'] = 'protocols/nfs/export-policies/rules' + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ]) + msg = 'Error: policy_name, svm_name, rule_index are required for protocols/nfs/export-policies/rules' + assert create_and_apply(ontap_rest_info_module, args, fail=True)['msg'] == msg + + +def test_owning_resource_export_policies_rules_missing_1_resource(): + args = set_default_args() + args['gather_subset'] = 'protocols/nfs/export-policies/rules' + args['owning_resource'] = {'policy_name': 'policy_name', 'svm_name': 'svm1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ]) + msg = 'Error: policy_name, svm_name, rule_index are required for protocols/nfs/export-policies/rules' + assert create_and_apply(ontap_rest_info_module, args, fail=True)['msg'] == msg + + +def test_owning_resource_export_policies_rules_policy_not_found(): + args = set_default_args() + args['gather_subset'] = 'protocols/nfs/export-policies/rules' + args['owning_resource'] = {'policy_name': 'policy_name', 'svm_name': 'svm1', 'rule_index': '1'} + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'protocols/nfs/export-policies', SRR['generic_error']), + ]) + msg = 'Could not find export policy policy_name on SVM svm1' + assert create_and_apply(ontap_rest_info_module, args, fail=True)['msg'] == msg + + +def test_lun_info_with_serial(): + args = set_default_args() + args['gather_subset'] = 'storage/luns' + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/luns', SRR['lun_info']), + ]) + info = create_and_apply(ontap_rest_info_module, args) + assert 'ontap_info' in info + assert 'storage/luns' in info['ontap_info'] + assert 'records' in info['ontap_info']['storage/luns'] + records = info['ontap_info']['storage/luns']['records'] + assert records + lun_info = records[0] + print('INFO', lun_info) + assert lun_info['serial_number'] == 'z6CcD+SK5mPb' + assert lun_info['serial_hex'] == '7a364363442b534b356d5062' + assert lun_info['naa_id'] == 'naa.600a0980' + '7a364363442b534b356d5062' + + +def test_ignore_api_errors(): + args = set_default_args() + args['gather_subset'] = 'storage/luns' + args['ignore_api_errors'] = ['something', 'Expected error'] + args['fields'] = ['**'] + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ('GET', 'storage/luns', SRR['error_record']), + ]) + info = create_and_apply(ontap_rest_info_module, args) + assert 'ontap_info' in info + assert 'storage/luns' in info['ontap_info'] + assert 'error' in info['ontap_info']['storage/luns'] + error = info['ontap_info']['storage/luns']['error'] + assert error + assert error['code'] == 6 + assert error['message'] == 'Expected error' + print_warnings() + assert_warning_was_raised('Using ** can put an extra load on the system and should not be used in production') + + +def test_private_cli_fields(): + register_responses([ + ('GET', 'cluster', SRR['validate_ontap_version_pass']), + ]) + args = set_default_args() + my_obj = create_module(ontap_rest_info_module, args) + error = 'Internal error, no field for unknown_api' + assert error in expect_and_capture_ansible_exception(my_obj.private_cli_fields, 'fail', 'unknown_api')['msg'] + assert my_obj.private_cli_fields('private/cli/vserver/security/file-directory') == 'acls' + assert my_obj.private_cli_fields('support/autosupport/check') == 'node,corrective-action,status,error-detail,check-type,check-category' + my_obj.parameters['fields'] = ['f1', 'f2'] + assert my_obj.private_cli_fields('private/cli/vserver/security/file-directory') == 'f1,f2' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_restit.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_restit.py new file mode 100644 index 000000000..89289386a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_restit.py @@ -0,0 +1,346 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_cluster ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, call +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_restit \ + import NetAppONTAPRestAPI as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy_9_7_0')), None), + 'is_rest_95': (200, dict(version=dict(generation=9, major=5, minor=0, full='dummy_9_5_0')), None), + 'is_rest_96': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy_9_6_0')), None), + 'is_rest_97': (200, dict(version=dict(generation=9, major=7, minor=0, full='dummy_9_7_0')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': ({}, None, None), + 'zero_record': (200, {'records': []}, None), + 'job_id_record': ( + 200, { + 'job': { + 'uuid': '94b6e6a7-d426-11eb-ac81-00505690980f', + '_links': {'self': {'href': '/api/cluster/jobs/94b6e6a7-d426-11eb-ac81-00505690980f'}}}, + 'cli_output': ' Use the "job show -id 2379" command to view the status of this operation.'}, None), + 'job_response_record': ( + 200, { + "uuid": "f03ccbb6-d8bb-11eb-ac81-00505690980f", + "description": "File Directory Security Apply Job", + "state": "success", + "message": "Complete: Operation completed successfully. File ACLs modified using policy \"policy1\" on Vserver \"GBSMNAS80LD\". File count: 0. [0]", + "code": 0, + "start_time": "2021-06-29T05:25:26-04:00", + "end_time": "2021-06-29T05:25:26-04:00" + }, None), + 'job_response_record_running': ( + 200, { + "uuid": "f03ccbb6-d8bb-11eb-ac81-00505690980f", + "description": "File Directory Security Apply Job", + "state": "running", + "message": "Complete: Operation completed successfully. File ACLs modified using policy \"policy1\" on Vserver \"GBSMNAS80LD\". File count: 0. [0]", + "code": 0, + "start_time": "2021-06-29T05:25:26-04:00", + "end_time": "2021-06-29T05:25:26-04:00" + }, None), + 'job_response_record_failure': ( + 200, { + "uuid": "f03ccbb6-d8bb-11eb-ac81-00505690980f", + "description": "File Directory Security Apply Job", + "state": "failure", + "message": "Forcing some error for UT.", + "code": 0, + "start_time": "2021-06-29T05:25:26-04:00", + "end_time": "2021-06-29T05:25:26-04:00" + }, None), + 'generic_error': (500, None, "Expected error"), + 'rest_error': (400, None, {'message': '-error_message-', 'code': '-error_code-'}), + 'end_of_sequence': (None, None, "Unexpected call to send_request"), +} + + +def set_default_args(use_rest='auto'): + hostname = '10.10.10.10' + username = 'admin' + password = 'password' + api = 'abc' + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'api': api, + 'use_rest': use_rest + }) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_run_default_get(mock_request, patch_ansible): + ''' if no method is given, GET is the default ''' + args = dict(set_default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 1 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_run_any(mock_request, patch_ansible): + ''' We don't validate the method name, so ANYthing goes ''' + args = dict(set_default_args()) + args['method'] = 'ANY' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + args['files'] = {'fkey1': 'fitem1', 'fkey2': 'fitem2'} + set_module_args(args) + mock_request.side_effect = [ + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 1 + headers = my_obj.rest_api.build_headers(accept='application/json') + expected_call = call('ANY', 'abc', args['query'], args['body'], headers, args['files']) + assert expected_call in mock_request.mock_calls + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_run_any_rest_error(mock_request, patch_ansible): + ''' We don't validate the method name, so ANYthing goes ''' + args = dict(set_default_args()) + args['method'] = 'ANY' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + set_module_args(args) + mock_request.side_effect = [ + SRR['rest_error'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "Error when calling 'abc': check error_message and error_code for details." + assert msg == exc.value.args[0]['msg'] + assert '-error_message-' == exc.value.args[0]['error_message'] + assert '-error_code-' == exc.value.args[0]['error_code'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_run_any_other_error(mock_request, patch_ansible): + ''' We don't validate the method name, so ANYthing goes ''' + args = dict(set_default_args()) + args['method'] = 'ANY' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + set_module_args(args) + mock_request.side_effect = [ + SRR['generic_error'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "Error when calling 'abc': Expected error" + assert msg == exc.value.args[0]['msg'] + assert 'Expected error' == exc.value.args[0]['error_message'] + assert exc.value.args[0]['error_code'] is None + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_run_post_async_no_job(mock_request, patch_ansible): + ''' POST async, but returns immediately ''' + args = dict(set_default_args()) + args['method'] = 'POST' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + args['wait_for_completion'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 1 + headers = my_obj.rest_api.build_headers(accept='application/json') + args['query'].update({'return_timeout': 30}) + expected_call = call('POST', 'abc', args['query'], json=args['body'], headers=headers, files=None) + assert expected_call in mock_request.mock_calls + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_run_post_async_with_job(mock_request, patch_ansible): + ''' POST async, but returns immediately ''' + args = dict(set_default_args()) + args['method'] = 'POST' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + args['wait_for_completion'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['job_id_record'], + SRR['job_response_record'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 + headers = my_obj.rest_api.build_headers(accept='application/json') + args['query'].update({'return_timeout': 30}) + expected_call = call('POST', 'abc', args['query'], json=args['body'], headers=headers, files=None) + assert expected_call in mock_request.mock_calls + + +# patch time to not wait between job retries +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_run_patch_async_with_job_loop(mock_request, mock_sleep, patch_ansible): + ''' POST async, but returns immediately ''' + args = dict(set_default_args()) + args['method'] = 'PATCH' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + args['wait_for_completion'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['job_id_record'], + SRR['job_response_record_running'], + SRR['job_response_record'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + headers = my_obj.rest_api.build_headers(accept='application/json') + args['query'].update({'return_timeout': 30}) + expected_call = call('PATCH', 'abc', args['query'], json=args['body'], headers=headers, files=None) + assert expected_call in mock_request.mock_calls + + +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_run_negative_delete(mock_request, mock_sleep, patch_ansible): + ''' POST async, but returns immediately ''' + args = dict(set_default_args()) + args['method'] = 'DELETE' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + args['wait_for_completion'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['job_id_record'], + SRR['job_response_record_running'], + SRR['job_response_record_failure'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "Error when calling 'abc': Forcing some error for UT." + assert msg == exc.value.args[0]['msg'] + assert 'Forcing some error for UT.' == exc.value.args[0]['error_message'] + assert exc.value.args[0]['error_code'] is None + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + headers = my_obj.rest_api.build_headers(accept='application/json') + args['query'].update({'return_timeout': 30}) + expected_call = call('DELETE', 'abc', args['query'], json=None, headers=headers) + assert expected_call in mock_request.mock_calls + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_run_any_async(mock_request, patch_ansible): + ''' We don't validate the method name, so ANYthing goes ''' + args = dict(set_default_args()) + args['method'] = 'ANY' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + args['files'] = {'fkey1': 'fitem1', 'fkey2': 'fitem2'} + args['wait_for_completion'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['empty_good'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 1 + headers = my_obj.rest_api.build_headers(accept='application/json') + expected_call = call('ANY', 'abc', args['query'], args['body'], headers, args['files']) + assert expected_call in mock_request.mock_calls + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_run_main(mock_request, patch_ansible): + ''' We don't validate the method name, so ANYthing goes ''' + args = dict(set_default_args()) + args['method'] = 'ANY' + args['body'] = {'bkey1': 'bitem1', 'bkey2': 'bitem2'} + args['query'] = {'qkey1': 'qitem1', 'qkey2': 'qitem2'} + args['wait_for_completion'] = True + set_module_args(args) + mock_request.side_effect = [ + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + my_main() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 1 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_build_headers(mock_request, patch_ansible): + ''' create cluster ''' + args = dict(set_default_args()) + set_module_args(args) + my_obj = my_module() + headers = my_obj.build_headers() + # TODO: in UT (and only in UT) module._name is not set properly. It shows as basic.py instead of 'na_ontap_restit' + assert headers == {'X-Dot-Client-App': 'basic.py/%s' % netapp_utils.COLLECTION_VERSION, 'accept': 'application/json'} + args['hal_linking'] = True + set_module_args(args) + my_obj = my_module() + headers = my_obj.build_headers() + assert headers == {'X-Dot-Client-App': 'basic.py/%s' % netapp_utils.COLLECTION_VERSION, 'accept': 'application/hal+json'} + # Accept header + args['accept_header'] = "multipart/form-data" + set_module_args(args) + my_obj = my_module() + headers = my_obj.build_headers() + assert headers == {'X-Dot-Client-App': 'basic.py/%s' % netapp_utils.COLLECTION_VERSION, 'accept': 'multipart/form-data'} diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_buckets.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_buckets.py new file mode 100644 index 000000000..2e15239da --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_buckets.py @@ -0,0 +1,739 @@ +# (c) 2022-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, \ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_s3_buckets \ + import NetAppOntapS3Buckets as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'nas_s3_bucket': (200, {"records": [{ + 'comment': '', + 'name': 'carchi-test-bucket1', + 'nas_path': '/', + 'policy': { + 'statements': [ + { + 'actions': ['GetObject', 'PutObject', 'DeleteObject', 'ListBucket'], + 'conditions': [ + {'operator': 'ip_address', 'source_ips': ['1.1.1.1/32', '1.2.2.0/24']}, + ], + 'effect': 'deny', + 'principals': [], + 'resources': ['carchi-test-bucket1', 'carchi-test-bucket1/*'], + 'sid': 1 + } + ] + }, + 'svm': { + 'name': 'ansibleSVM', + 'uuid': '685bd228' + }, + 'type': 'nas', + 'uuid': '3e5c4ac8'}], "num_records": 1}, None), + 'nas_s3_bucket_modify': (200, {"records": [{ + 'comment': '', + 'name': 'carchi-test-bucket1', + 'nas_path': '/', + 'policy': {'statements': []}, + 'svm': { + 'name': 'ansibleSVM', + 'uuid': '685bd228' + }, + 'type': 'nas', + 'uuid': '3e5c4ac8'}], "num_records": 1}, None), + 's3_bucket_more_policy': (200, {"records": [{ + 'comment': 'carchi8py was here again', + 'name': 'bucket1', + 'policy': { + 'statements': [ + { + "sid": 1, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1/32", "1.2.2.0/24"]}], + "principals": ["user1", "user2"], + "resources": ["bucket1", "bucket1/*"] + }, + { + "sid": 2, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1/32", "1.2.2.0/24"]}], + "principals": ["user1", "user2"], + "resources": ["bucket1", "bucket1/*"] + } + ] + }, + 'qos_policy': { + 'max_throughput_iops': 100, + 'max_throughput_mbps': 150, + 'min_throughput_iops': 0, + 'min_throughput_mbps': 0, + 'name': 'ansibleSVM_auto_gen_policy_9be26687_2849_11ed_9696_005056b3b297', + 'uuid': '9be28517-2849-11ed-9696-005056b3b297' + }, + 'size': 938860800, + 'svm': {'name': 'ansibleSVM', 'uuid': '969ansi97'}, + 'uuid': '9bdefd59-2849-11ed-9696-005056b3b297', + 'type': 's3', + 'volume': {'uuid': '1cd8a442-86d1-11e0-abcd-123478563412'}}], "num_records": 1}, None), + 's3_bucket_without_condition': (200, {"records": [{ + 'comment': 'carchi8py was here again', + 'name': 'bucket1', + 'policy': { + 'statements': [ + { + "sid": 1, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "principals": ["user1", "user2"], + "resources": ["bucket1", "bucket1/*"] + }, + { + "sid": 2, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "principals": ["user1", "user2"], + "resources": ["bucket1", "bucket1/*"] + } + ] + }, + 'qos_policy': { + 'max_throughput_iops': 100, + 'max_throughput_mbps': 150, + 'min_throughput_iops': 0, + 'min_throughput_mbps': 0, + 'name': 'ansibleSVM_auto_gen_policy_9be26687_2849_11ed_9696_005056b3b297', + 'uuid': '9be28517-2849-11ed-9696-005056b3b297' + }, + 'size': 938860800, + 'svm': {'name': 'ansibleSVM', 'uuid': '969ansi97'}, + 'uuid': '9bdefd59-2849-11ed-9696-005056b3b297', + 'volume': {'uuid': '1cd8a442-86d1-11e0-abcd-123478563412'}}], "num_records": 1}, None), + 's3_bucket_9_10': (200, { + "logical_used_size": 0, + "uuid": "414b29a1-3b26-11e9-bd58-0050568ea055", + "size": 1677721600, + "protection_status": {"destination": {}}, + "constituents_per_aggregate": 4, + "qos_policy": { + "max_throughput_iops": 10000, + "max_throughput_mbps": 500, + "name": "performance", + "min_throughput_iops": 2000, + "min_throughput_mbps": 500, + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + "policy": { + "statements": [ + { + "sid": "FullAccessToUser1", + "resources": ["bucket1", "bucket1/*"], + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "allow", + "conditions": [ + { + "operator": "ip-address", + "max_keys": ["1000"], + "delimiters": ["/"], + "source-ips": ["1.1.1.1", "1.2.2.0/24"], + "prefixes": ["pref"], + "usernames": ["user1"] + } + ], + "principals": ["user1", "group/grp1"] + } + ] + }, + "storage_service_level": "value", + "audit_event_selector": {"access": "all", "permission": "all"}, + "name": "bucket1", + "comment": "S3 bucket.", + "svm": {"name": "svm1", "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7"}, + "volume": {"uuid": "1cd8a442-86d1-11e0-abcd-123478563412"} + }, None), + 's3_bucket_9_8': (200, { + "logical_used_size": 0, + "uuid": "414b29a1-3b26-11e9-bd58-0050568ea055", + "size": 1677721600, + "protection_status": {"destination": {}}, + "constituents_per_aggregate": 4, + "qos_policy": { + "max_throughput_iops": 10000, + "max_throughput_mbps": 500, + "name": "performance", + "min_throughput_iops": 2000, + "min_throughput_mbps": 500, + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + "policy": { + "statements": [ + { + "sid": "FullAccessToUser1", + "resources": ["bucket1", "bucket1/*"], + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "allow", + "conditions": [ + { + "operator": "ip-address", + "max_keys": ["1000"], + "delimiters": ["/"], + "source-ips": ["1.1.1.1", "1.2.2.0/24"], + "prefixes": ["pref"], + "usernames": ["user1"] + } + ], + "principals": ["user1", "group/grp1"] + } + ] + }, + "storage_service_level": "value", + "name": "bucket1", + "comment": "S3 bucket.", + "svm": {"name": "svm1", "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7"}, + "volume": {"uuid": "1cd8a442-86d1-11e0-abcd-123478563412"} + }, None), + 'volume_info': (200, { + "aggregates": [{"name": "aggr1", "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412"}], + }, None), +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'bucket1', + 'vserver': 'vserver' +} + +POLICY_ARGS = { + "statements": [{ + "sid": "FullAccessToUser1", + "resources": ["bucket1", "bucket1/*"], + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "allow", + "conditions": [ + { + "operator": "ip_address", + "max_keys": ["1000"], + "delimiters": ["/"], + "source_ips": ["1.1.1.1", "1.2.2.0/24"], + "prefixes": ["pref"], + "usernames": ["user1"] + } + ], + "principals": ["user1", "group/grp1"] + }] +} + +REAL_POLICY_ARGS = { + "statements": [{ + "sid": "FullAccessToUser1", + "resources": ["bucket1", "bucket1/*"], + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "allow", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1", "1.2.2.0/24"]}], + "principals": ["user1", "group/grp1"] + }] +} + +REAL_POLICY_WTIH_NUM_ARGS = { + "statements": [{ + "sid": 1, + "resources": ["bucket1", "bucket1/*"], + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "allow", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1", "1.2.2.0/24"]}], + "principals": ["user1", "group/grp1"] + }] +} + +MODIFY_POLICY_ARGS = { + "statements": [{ + "sid": "FullAccessToUser1", + "resources": ["bucket1", "bucket1/*"], + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "allow", + "conditions": [ + { + "operator": "ip_address", + "max_keys": ["100"], + "delimiters": ["/"], + "source_ips": ["2.2.2.2", "1.2.2.0/24"], + "prefixes": ["pref"], + "usernames": ["user2"] + } + ], + "principals": ["user1", "group/grp1"] + }] +} + + +MULTIPLE_POLICY_STATEMENTS = { + "statements": [ + { + "sid": 1, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1", "1.2.2.0/24"]}], + "principals": ["user1", "user2"], + "resources": ["*"] + }, + { + "sid": 2, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1", "1.2.2.0/24"]}], + "principals": ["user1", "user2"], + "resources": ["*"] + } + ] +} + + +SAME_POLICY_STATEMENTS = { + "statements": [ + { + "sid": 1, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1", "1.2.2.0/24"]}], + "principals": ["user1", "user2"], + "resources": ["*"] + }, + { + "sid": 1, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1", "1.2.2.0/24"]}], + "principals": ["user1", "user2"], + "resources": ["*"] + }, + ] +} + + +MULTIPLE_POLICY_CONDITIONS = { + "statements": [ + { + "sid": 1, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "conditions": [ + {"operator": "ip_address", "source_ips": ["1.1.1.1", "1.2.2.0/24"]}, + {"operator": "not_ip_address", "source_ips": ["2.1.1.1", "1.2.2.0/24"]} + ], + "principals": ["user1", "user2"], + "resources": ["*"] + }, + { + "sid": 2, + "actions": ["GetObject", "PutObject", "DeleteObject", "ListBucket"], + "effect": "deny", + "conditions": [{"operator": "ip_address", "source_ips": ["1.1.1.1", "1.2.2.0/24"]}], + "principals": ["user1", "user2"], + "resources": ["*"] + } + ] +} + + +NAS_S3_BUCKET = { + 'comment': '', + 'name': 'carchi-test-bucket1', + 'nas_path': '/', + 'policy': { + 'statements': [ + { + 'actions': ['GetObject', 'PutObject', 'DeleteObject', 'ListBucket'], + 'conditions': [{'operator': 'ip_address', 'source_ips': ['1.1.1.1/32', '1.2.2.0/24']}], + 'effect': 'deny', + 'principals': [], + 'resources': ['carchi-test-bucket1', 'carchi-test-bucket1/*'], + 'sid': 1 + } + ] + }, + 'vserver': 'ansibleSVM', + 'type': 'nas' +} + + +QOS_ARGS = { + "max_throughput_iops": 10000, + "max_throughput_mbps": 500, + "name": "performance", + "min_throughput_iops": 2000, + "min_throughput_mbps": 500, +} + +MODIFY_QOS_ARGS = { + "max_throughput_iops": 20000, + "max_throughput_mbps": 400, + "name": "performance", + "min_throughput_iops": 3000, + "min_throughput_mbps": 400, +} + +AUDIT_EVENT = { + "access": "all", + "permission": "all" +} + +MODIFY_AUDIT_EVENT = { + "access": "read", + "permission": "allow" +} + + +def test_low_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']) + ]) + error = create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error: na_ontap_s3_bucket only supports REST, and requires ONTAP 9.8.0 or later. Found: 9.7.0.' + assert msg in error + + +def test_get_s3_bucket_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_s3_bucket() is None + + +def test_get_s3_bucket_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching S3 bucket bucket1: calling: protocols/s3/buckets: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_s3_bucket, 'fail')['msg'] + + +def test_get_s3_bucket_9_8(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_8']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_s3_bucket() is not None + + +def test_get_s3_bucket_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_10']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_s3_bucket() is not None + + +def test_create_s3_bucket_9_8(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'protocols/s3/buckets', SRR['empty_records']), + ('POST', 'protocols/s3/buckets', SRR['empty_good']) + ]) + module_args = {'comment': 'carchi8py was here', + 'aggregates': ['aggr1'], + 'constituents_per_aggregate': 4, + 'size': 838860800, + 'policy': POLICY_ARGS, + 'qos_policy': QOS_ARGS} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_s3_bucket_9_10_and_9_12(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['empty_records']), + ('POST', 'protocols/s3/buckets', SRR['empty_good']), + # create with type + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/s3/buckets', SRR['empty_records']), + ('POST', 'protocols/s3/buckets', SRR['empty_good']) + ]) + module_args = {'comment': 'carchi8py was here', + 'aggregates': ['aggr1'], + 'constituents_per_aggregate': 4, + 'size': 838860800, + 'policy': POLICY_ARGS, + 'qos_policy': QOS_ARGS, + 'audit_event_selector': AUDIT_EVENT} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + module_args['type'] = 's3' + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_s3_nas_bucket_create_modify_delete(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/s3/buckets', SRR['empty_records']), + ('POST', 'protocols/s3/buckets', SRR['success']), + # idemptent check + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/s3/buckets', SRR['nas_s3_bucket']), + # modify empty policy + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/s3/buckets', SRR['nas_s3_bucket']), + ('PATCH', 'protocols/s3/buckets/685bd228/3e5c4ac8', SRR['success']), + # idempotent check + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/s3/buckets', SRR['nas_s3_bucket_modify']), + # delete nas bucket + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/s3/buckets', SRR['nas_s3_bucket_modify']), + ('DELETE', 'protocols/s3/buckets/685bd228/3e5c4ac8', SRR['success']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, NAS_S3_BUCKET)['changed'] + assert create_and_apply(my_module, DEFAULT_ARGS, NAS_S3_BUCKET)['changed'] is False + NAS_S3_BUCKET['policy']['statements'] = [] + assert create_and_apply(my_module, DEFAULT_ARGS, NAS_S3_BUCKET)['changed'] + assert create_and_apply(my_module, DEFAULT_ARGS, NAS_S3_BUCKET)['changed'] is False + NAS_S3_BUCKET['state'] = 'absent' + assert create_and_apply(my_module, DEFAULT_ARGS, NAS_S3_BUCKET)['changed'] + + +def test_modify_s3_bucket_type_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_more_policy']) + ]) + assert 'Error: cannot modify bucket type.' in create_and_apply(my_module, DEFAULT_ARGS, {'type': 'nas'}, fail=True)['msg'] + + +def test_create_with_real_policy_s3_bucket_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['empty_records']), + ('POST', 'protocols/s3/buckets', SRR['empty_good']) + ]) + module_args = {'comment': 'carchi8py was here', + 'aggregates': ['aggr1'], + 'constituents_per_aggregate': 4, + 'size': 838860800, + 'policy': REAL_POLICY_ARGS, + 'qos_policy': QOS_ARGS, + 'audit_event_selector': AUDIT_EVENT} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_with_real_policy_with_sid_as_number_s3_bucket_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['empty_records']), + ('POST', 'protocols/s3/buckets', SRR['empty_good']) + ]) + module_args = {'comment': 'carchi8py was here', + 'aggregates': ['aggr1'], + 'constituents_per_aggregate': 4, + 'size': 838860800, + 'policy': REAL_POLICY_WTIH_NUM_ARGS, + 'qos_policy': QOS_ARGS, + 'audit_event_selector': AUDIT_EVENT} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_s3_bucket_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('POST', 'protocols/s3/buckets', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'carchi8py was here' + my_obj.parameters['aggregates'] = ['aggr1'] + my_obj.parameters['constituents_per_aggregate'] = 4 + my_obj.parameters['size'] = 838860800 + error = expect_and_capture_ansible_exception(my_obj.create_s3_bucket, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating S3 bucket bucket1: calling: protocols/s3/buckets: got Expected error.' == error + + +def test_delete_s3_bucket(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_10']), + ('DELETE', 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055', + SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_s3_bucket_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('DELETE', 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055', + SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.uuid = '414b29a1-3b26-11e9-bd58-0050568ea055' + my_obj.svm_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + error = expect_and_capture_ansible_exception(my_obj.delete_s3_bucket, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error deleting S3 bucket bucket1: calling: ' \ + 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055: got Expected error.' == error + + +def test_modify_s3_bucket_9_8(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_8']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ('PATCH', 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055', + SRR['empty_good']) + ]) + module_args = {'comment': 'carchi8py was here', + 'size': 943718400, + 'policy': MODIFY_POLICY_ARGS, + 'qos_policy': MODIFY_QOS_ARGS} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_s3_bucket_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_10']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ('PATCH', 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055', + SRR['empty_good']) + ]) + module_args = {'comment': 'carchi8py was here', + 'size': 943718400, + 'policy': MODIFY_POLICY_ARGS, + 'qos_policy': MODIFY_QOS_ARGS, + 'audit_event_selector': MODIFY_AUDIT_EVENT} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_s3_bucket_policy_statements(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_10']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ('PATCH', 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055', + SRR['empty_good']), + # add multiple statements. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_more_policy']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + # try to modify with identical statements. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_more_policy']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ('PATCH', 'protocols/s3/buckets/969ansi97/9bdefd59-2849-11ed-9696-005056b3b297', SRR['empty_good']), + # empty policy statements. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_10']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ('PATCH', 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055', + SRR['empty_good']) + ]) + module_args = {'policy': MULTIPLE_POLICY_STATEMENTS} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + module_args = {'policy': SAME_POLICY_STATEMENTS} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert create_and_apply(my_module, DEFAULT_ARGS, {'policy': {'statements': []}}) + + +def test_modify_s3_bucket_policy_statements_conditions(): + register_responses([ + # modify if desired statements has conditions and current statement conditions is None. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_without_condition']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ('PATCH', 'protocols/s3/buckets/969ansi97/9bdefd59-2849-11ed-9696-005056b3b297', SRR['empty_good']), + # empty policy statements conditions. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_more_policy']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ('PATCH', 'protocols/s3/buckets/969ansi97/9bdefd59-2849-11ed-9696-005056b3b297', SRR['empty_good']), + # add multiple conditions. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_more_policy']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ('PATCH', 'protocols/s3/buckets/969ansi97/9bdefd59-2849-11ed-9696-005056b3b297', SRR['empty_good']) + ]) + module_args = {'policy': MULTIPLE_POLICY_STATEMENTS} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + module_args = {'policy': MULTIPLE_POLICY_STATEMENTS.copy()} + module_args['policy']['statements'][0]['conditions'] = [] + module_args['policy']['statements'][1]['conditions'] = [] + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + module_args = {'policy': MULTIPLE_POLICY_CONDITIONS} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_when_try_set_empty_dict_to_policy(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + module_args = {'policy': {'statements': [{}]}} + assert 'cannot set empty dict' in create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_modify_s3_bucket_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('PATCH', 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055', + SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'carchi8py was here' + my_obj.parameters['size'] = 943718400 + current = {'comment': 'carchi8py was here', 'size': 943718400} + my_obj.uuid = '414b29a1-3b26-11e9-bd58-0050568ea055' + my_obj.svm_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + error = expect_and_capture_ansible_exception(my_obj.modify_s3_bucket, 'fail', current)['msg'] + print('Info: %s' % error) + assert 'Error modifying S3 bucket bucket1: calling: ' \ + 'protocols/s3/buckets/02c9e252-41be-11e9-81d5-00a0986138f7/414b29a1-3b26-11e9-bd58-0050568ea055: got Expected error.' == error + + +def test_new_aggr_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_8']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['volume_info']), + ]) + module_args = {'aggregates': ['aggr2']} + error = 'Aggregates cannot be modified for S3 bucket bucket1' + assert create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_volume_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'protocols/s3/buckets', SRR['s3_bucket_9_8']), + ('GET', 'storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412', SRR['generic_error']), + ]) + module_args = {'aggregates': ['aggr2']} + error = 'calling: storage/volumes/1cd8a442-86d1-11e0-abcd-123478563412: got Expected error.' + assert create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_groups.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_groups.py new file mode 100644 index 000000000..6b204eadd --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_groups.py @@ -0,0 +1,319 @@ +# (c) 2022-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_s3_groups \ + import NetAppOntapS3Groups as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 's3_group_no_user_policy': (200, { + "records": [ + { + "comment": "Admin group", + "name": "carchi8py_group", + "id": "5", + "svm": { + "name": "svm1", + "uuid": "e3cb5c7f-cd20" + } + } + ], + "num_records": 1 + }, None), + 's3_group': (200, { + "records": [ + { + "comment": "Admin group", + "name": "carchi8py_group", + "users": [ + { + "name": "carchi8py", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + } + ], + "policies": [ + { + "name": "my_policy", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + } + ], + "id": "5", + "svm": { + "name": "svm1", + "uuid": "e3cb5c7f-cd20" + } + } + ], + "num_records": 1 + }, None), + 's3_group2': (200, { + "records": [ + { + "comment": "Admin group", + "name": "carchi8py_group", + "users": [ + { + "name": "carchi8py", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + }, + { + "name": "user2", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + } + ], + "policies": [ + { + "name": "my_policy", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + }, + { + "name": "my_policy2", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + } + ], + "id": "5", + "svm": { + "name": "svm1", + "uuid": "e3cb5c7f-cd20" + } + } + ], + "num_records": 1 + }, None), + 'svm_uuid': (200, {"records": [ + { + 'uuid': 'e3cb5c7f-cd20' + }], "num_records": 1}, None) +}) + +USER = { + 'name': 'carchi8py' +} + +POLICY = { + 'name': 'my_policy' +} + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'carchi8py_group', + 'vserver': 'vserver', + 'users': [USER], + 'policies': [POLICY] +} + + +def test_low_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_97']) + ]) + error = create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error: na_ontap_s3_groups only supports REST, and requires ONTAP 9.8.0 or later. Found: 9.7.0.' + assert msg in error + + +def test_get_s3_groups_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_s3_groups() is None + + +def test_get_s3_groups_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching S3 groups carchi8py_group: calling: protocols/s3/services/e3cb5c7f-cd20/groups: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_s3_groups, 'fail')['msg'] + + +def test_create_s3_group(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['empty_records']), + ('POST', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['empty_good']) + ]) + module_args = { + 'comment': 'this is a s3 group', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_s3_group_with_multi_user_policies(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['empty_records']), + ('POST', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['empty_good']) + ]) + module_args = { + 'comment': 'this is a s3 group', + 'users': [{'name': 'carchi8py'}, {'name': 'foo'}], + 'policies': [{'name': 'policy1'}, {'name': 'policy2'}] + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_s3_group_error_no_users(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['empty_records']), + ]) + args = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'carchi8py_group', + 'vserver': 'vserver', + 'policies': [POLICY] + } + error = create_and_apply(my_module, args, {}, 'fail')['msg'] + print('Info: %s' % error) + assert 'policies and users are required for a creating a group.' == error + + +def test_create_s3_group_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('POST', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'this is a s3 group' + my_obj.svm_uuid = 'e3cb5c7f-cd20' + error = expect_and_capture_ansible_exception(my_obj.create_s3_groups, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating S3 groups carchi8py_group: calling: protocols/s3/services/e3cb5c7f-cd20/groups: got Expected error.' == error + + +def test_delete_s3_group(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['s3_group']), + ('DELETE', 'protocols/s3/services/e3cb5c7f-cd20/groups/5', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_s3_group_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('DELETE', 'protocols/s3/services/e3cb5c7f-cd20/groups/5', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.svm_uuid = 'e3cb5c7f-cd20' + my_obj.group_id = 5 + error = expect_and_capture_ansible_exception(my_obj.delete_s3_groups, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error deleting S3 group carchi8py_group: calling: protocols/s3/services/e3cb5c7f-cd20/groups/5: got Expected error.' == error + + +def test_modify_s3_group(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['s3_group']), + ('PATCH', 'protocols/s3/services/e3cb5c7f-cd20/groups/5', SRR['empty_good']) + ]) + module_args = { + 'comment': 'this is a modify comment', + 'users': [{'name': 'carchi8py'}, {'name': 'user2'}], + 'policies': [{'name': 'policy1'}, {'name': 'policy2'}] + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_s3_group_no_current_user_policy(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/groups', SRR['s3_group_no_user_policy']), + ('PATCH', 'protocols/s3/services/e3cb5c7f-cd20/groups/5', SRR['empty_good']) + ]) + module_args = { + 'users': [{'name': 'carchi8py'}, {'name': 'user2'}], + 'policies': [{'name': 'policy1'}, {'name': 'policy2'}] + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_s3_group_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('PATCH', 'protocols/s3/services/e3cb5c7f-cd20/groups/5', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'this is a modified s3 service' + current = {'comment': 'this is a modified s3 service'} + my_obj.svm_uuid = 'e3cb5c7f-cd20' + my_obj.group_id = 5 + error = expect_and_capture_ansible_exception(my_obj.modify_s3_groups, 'fail', current)['msg'] + print('Info: %s' % error) + assert 'Error modifying S3 group carchi8py_group: calling: protocols/s3/services/e3cb5c7f-cd20/groups/5: got Expected error.' == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_policies.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_policies.py new file mode 100644 index 000000000..eacb4e8c1 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_policies.py @@ -0,0 +1,220 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_s3_policies \ + import NetAppOntapS3Policies as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 's3_policy': (200, { + "records": [ + { + "statements": [ + { + "sid": "FullAccessToBucket1", + "resources": [ + "bucket1", + "bucket1/*" + ], + "index": 0, + "actions": [ + "GetObject", + "PutObject", + "DeleteObject", + "ListBucket" + ], + "effect": "allow" + } + ], + "comment": "S3 policy.", + "name": "Policy1", + "svm": { + "name": "policy_name", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "read-only": True + } + ], + "num_records": 1 + }, None), + 'svm_uuid': (200, {"records": [ + { + 'uuid': 'e3cb5c7f-cd20' + }], "num_records": 1}, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'policy_name', + 'vserver': 'vserver' +} + +STATEMENT = { + "sid": "FullAccessToUser1", + "resources": [ + "bucket1", + "bucket1/*" + ], + "actions": [ + "GetObject", + "PutObject", + "DeleteObject", + "ListBucket" + ], + "effect": "allow", +} + +STATEMENT2 = { + "sid": "FullAccessToUser1", + "resources": [ + "bucket1", + "bucket1/*", + "bucket2", + "bucket2/*" + ], + "actions": [ + "GetObject", + "PutObject", + "DeleteObject", + "ListBucket" + ], + "effect": "allow", +} + + +def test_low_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_97']) + ]) + error = create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error: na_ontap_s3_policies only supports REST, and requires ONTAP 9.8.0 or later. Found: 9.7.0.' + assert msg in error + + +def test_get_s3_policies_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/policies', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_s3_policies() is None + + +def test_get_s3_policies_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/policies', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching S3 policy policy_name: calling: protocols/s3/services/e3cb5c7f-cd20/policies: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_s3_policies, 'fail')['msg'] + + +def test_create_s3_policies(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/policies', SRR['empty_records']), + ('POST', 'protocols/s3/services/e3cb5c7f-cd20/policies', SRR['empty_good']) + ]) + module_args = { + 'comment': 'this is a s3 user', + 'statements': [STATEMENT] + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_s3_policies_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('POST', 'protocols/s3/services/e3cb5c7f-cd20/policies', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'this is a s3 policies' + my_obj.parameters['statements'] = [STATEMENT] + my_obj.svm_uuid = 'e3cb5c7f-cd20' + error = expect_and_capture_ansible_exception(my_obj.create_s3_policies, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating S3 policy policy_name: calling: protocols/s3/services/e3cb5c7f-cd20/policies: got Expected error.' == error + + +def test_delete_s3_policies(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/policies', SRR['s3_policy']), + ('DELETE', 'protocols/s3/services/e3cb5c7f-cd20/policies/policy_name', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_s3_policies_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('DELETE', 'protocols/s3/services/e3cb5c7f-cd20/policies/policy_name', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.svm_uuid = 'e3cb5c7f-cd20' + error = expect_and_capture_ansible_exception(my_obj.delete_s3_policies, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error deleting S3 policy policy_name: calling: protocols/s3/services/e3cb5c7f-cd20/policies/policy_name: got Expected error.' == error + + +def test_modify_s3_policies(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/policies', SRR['s3_policy']), + ('PATCH', 'protocols/s3/services/e3cb5c7f-cd20/policies/policy_name', SRR['empty_good']) + ]) + module_args = {'comment': 'this is a modify comment', 'statements': [STATEMENT2]} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_s3_policies_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('PATCH', 'protocols/s3/services/e3cb5c7f-cd20/policies/policy_name', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'this is a modified s3 service' + my_obj.parameters['statements'] = [STATEMENT2] + current = {'comment': 'this is a modified s3 service', 'statements': [STATEMENT2]} + my_obj.svm_uuid = 'e3cb5c7f-cd20' + error = expect_and_capture_ansible_exception(my_obj.modify_s3_policies, 'fail', current)['msg'] + print('Info: %s' % error) + assert 'Error modifying S3 policy policy_name: calling: protocols/s3/services/e3cb5c7f-cd20/policies/policy_name: got Expected error.' == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_services.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_services.py new file mode 100644 index 000000000..fce59093a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_services.py @@ -0,0 +1,176 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_s3_services \ + import NetAppOntapS3Services as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 's3_service': (200, { + "svm": { + "uuid": "08c8a385-b1ac-11ec-bd2e-005056b3b297", + "name": "ansibleSVM", + }, + "name": "carchi-test", + "enabled": True, + "buckets": [ + { + "name": "carchi-test-bucket2" + }, + { + "name": "carchi-test-bucket" + } + ], + "users": [ + { + "name": "root" + } + ], + "comment": "this is a s3 service", + "certificate": { + "name": "ansibleSVM_16E1C1284D889609", + }, + }, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'service1', + 'vserver': 'vserver' +} + + +def test_low_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_97']) + ]) + error = create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error: na_ontap_s3_services only supports REST, and requires ONTAP 9.8.0 or later. Found: 9.7.0.' + assert msg in error + + +def test_get_s3_service_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/services', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_s3_service() is None + + +def test_get_s3_service_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/services', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching S3 service service1: calling: protocols/s3/services: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_s3_service, 'fail')['msg'] + + +def test_create_s3_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/services', SRR['empty_records']), + ('POST', 'protocols/s3/services', SRR['empty_good']) + ]) + module_args = { + 'enabled': True, + 'comment': 'this is a s3 service', + 'certificate_name': 'cert1', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_s3_service_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('POST', 'protocols/s3/services', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['enabled'] = True + my_obj.parameters['comment'] = 'this is a s3 service' + my_obj.parameters['certificate_name'] = 'cert1' + error = expect_and_capture_ansible_exception(my_obj.create_s3_service, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating S3 service service1: calling: protocols/s3/services: got Expected error.' == error + + +def test_delete_s3_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/services', SRR['s3_service']), + ('DELETE', 'protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_s3_service_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('DELETE', 'protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.svm_uuid = '08c8a385-b1ac-11ec-bd2e-005056b3b297' + error = expect_and_capture_ansible_exception(my_obj.delete_s3_service, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error deleting S3 service service1: calling: protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297: got Expected error.' == error + + +def test_modify_s3_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/s3/services', SRR['s3_service']), + ('PATCH', 'protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297', SRR['empty_good']) + ]) + module_args = {'comment': 'this is a modified s3 service', + 'enabled': False, + 'certificate_name': 'cert2', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_s3_service_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('PATCH', 'protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'this is a modified s3 service' + current = {'comment': 'this is a modified s3 service'} + my_obj.svm_uuid = '08c8a385-b1ac-11ec-bd2e-005056b3b297' + error = expect_and_capture_ansible_exception(my_obj.modify_s3_service, 'fail', current)['msg'] + print('Info: %s' % error) + assert 'Error modifying S3 service service1: calling: protocols/s3/services/08c8a385-b1ac-11ec-bd2e-005056b3b297: got Expected error.' == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_users.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_users.py new file mode 100644 index 000000000..71850e510 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_s3_users.py @@ -0,0 +1,194 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_s3_users \ + import NetAppOntapS3Users as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 's3_user': (200, { + "records": [ + { + "comment": "S3 user", + "name": "carchi8py", + "svm": { + "name": "svm1", + "uuid": "e3cb5c7f-cd20" + } + } + ], + "num_records": 1 + }, None), + 's3_user_created': (200, { + "records": [ + { + 'access_key': 'random_access_key', + 'secret_key': 'random_secret_key' + } + ], + "num_records": 1 + }, None), + 'svm_uuid': (200, {"records": [ + { + 'uuid': 'e3cb5c7f-cd20' + }], "num_records": 1}, None) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'carchi8py', + 'vserver': 'vserver' +} + + +def test_low_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_97']) + ]) + error = create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error: na_ontap_s3_users only supports REST, and requires ONTAP 9.8.0 or later. Found: 9.7.0.' + assert msg in error + + +def test_get_s3_users_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_s3_user() is None + + +def test_get_s3_users_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error fetching S3 user carchi8py: calling: protocols/s3/services/e3cb5c7f-cd20/users: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_s3_user, 'fail')['msg'] + + +def test_create_s3_users(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['empty_records']), + ('POST', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['s3_user_created']) + ]) + module_args = { + 'comment': 'this is a s3 user', + } + result = create_and_apply(my_module, DEFAULT_ARGS, module_args) + assert result['changed'] + assert result['secret_key'] == 'random_secret_key' + assert result['access_key'] == 'random_access_key' + + +def test_create_s3_users_fail_randomly(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['empty_records']), + ('POST', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['empty_good']) + ]) + module_args = { + 'comment': 'this is a s3 user', + } + error = create_and_apply(my_module, DEFAULT_ARGS, module_args, 'fail')['msg'] + assert 'Error creating S3 user carchi8py' == error + + +def test_create_s3_user_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('POST', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'this is a s3 user' + my_obj.svm_uuid = 'e3cb5c7f-cd20' + error = expect_and_capture_ansible_exception(my_obj.create_s3_user, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating S3 user carchi8py: calling: protocols/s3/services/e3cb5c7f-cd20/users: got Expected error.' == error + + +def test_delete_s3_user(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['s3_user']), + ('DELETE', 'protocols/s3/services/e3cb5c7f-cd20/users/carchi8py', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_s3_user_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('DELETE', 'protocols/s3/services/e3cb5c7f-cd20/users/carchi8py', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['state'] = 'absent' + my_obj.svm_uuid = 'e3cb5c7f-cd20' + error = expect_and_capture_ansible_exception(my_obj.delete_s3_user, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error deleting S3 user carchi8py: calling: protocols/s3/services/e3cb5c7f-cd20/users/carchi8py: got Expected error.' == error + + +def test_modify_s3_user(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/s3/services/e3cb5c7f-cd20/users', SRR['s3_user']), + ('PATCH', 'protocols/s3/services/e3cb5c7f-cd20/users/carchi8py', SRR['empty_good']) + ]) + module_args = {'comment': 'this is a modify comment'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_s3_user_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('PATCH', 'protocols/s3/services/e3cb5c7f-cd20/users/carchi8py', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['comment'] = 'this is a modified s3 service' + current = {'comment': 'this is a modified s3 service'} + my_obj.svm_uuid = 'e3cb5c7f-cd20' + error = expect_and_capture_ansible_exception(my_obj.modify_s3_user, 'fail', current)['msg'] + print('Info: %s' % error) + assert 'Error modifying S3 user carchi8py: calling: protocols/s3/services/e3cb5c7f-cd20/users/carchi8py: got Expected error.' == error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_certificates.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_certificates.py new file mode 100644 index 000000000..866dd3a58 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_certificates.py @@ -0,0 +1,509 @@ +# (c) 2019-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_security_certificates """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import copy +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_security_certificates \ + import NetAppOntapSecurityCertificates as my_module, main as my_main # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'empty_records': (200, {'records': []}, None), + 'get_uuid': (200, {'records': [{'uuid': 'ansible'}]}, None), + 'get_multiple_records': (200, {'records': [{'uuid': 'ansible'}, {'uuid': 'second'}]}, None), + 'error_unexpected_name': (200, None, {'message': 'Unexpected argument "name".'}), + 'error_duplicate_entry': (200, None, {'message': 'duplicate entry', 'target': 'uuid'}), + 'error_some_error': (200, None, {'message': 'some error'}), +} + +NAME_ERROR = "Error calling API: security/certificates - ONTAP 9.6 and 9.7 do not support 'name'. Use 'common_name' and 'type' as a work-around." +TYPE_ERROR = "Error calling API: security/certificates - When using 'common_name', 'type' is required." +EXPECTED_ERROR = "Error calling API: security/certificates - Expected error" + + +def set_default_args(): + return dict({ + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'name_for_certificate' + }) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + set_module_args({}) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_get_certificate_called(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['end_of_sequence'] + ] + set_module_args(set_default_args()) + my_obj = my_module() + assert my_obj.get_certificate() is not None + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_error(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + set_module_args(set_default_args()) + with pytest.raises(AnsibleFailJson) as exc: + my_main() + assert exc.value.args[0]['msg'] == EXPECTED_ERROR + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create_failed(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], # validate data vserver exist. + SRR['empty_records'], # get certificate -> not found + SRR['empty_good'], + SRR['end_of_sequence'] + ] + data = { + 'type': 'client_ca', + 'vserver': 'abc', + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = 'Error creating or installing certificate: one or more of the following options are missing:' + assert exc.value.args[0]['msg'].startswith(msg) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_successful_create(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], # validate data vserver exist. + SRR['empty_records'], # get certificate -> not found + SRR['empty_good'], + SRR['end_of_sequence'] + ] + data = { + 'type': 'client_ca', + 'vserver': 'abc', + 'common_name': 'cname' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_idempotent_create(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], # validate data vserver exist. + SRR['get_uuid'], # get certificate -> found + SRR['end_of_sequence'] + ] + data = { + 'type': 'client_ca', + 'vserver': 'abc', + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_create_duplicate_entry(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_records'], # get certificate -> not found + copy.deepcopy(SRR['error_duplicate_entry']), # code under test modifies error in place + SRR['end_of_sequence'] + ] + data = { + 'type': 'client_ca', + 'common_name': 'cname' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + print('EXC', exc.value.args[0]['msg']) + for fragment in ('Error creating or installing certificate: {', + "'message': 'duplicate entry. Same certificate may already exist under a different name.'", + "'target': 'cluster'"): + assert fragment in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_successful_delete(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], # get certificate -> found + SRR['empty_good'], + SRR['end_of_sequence'] + ] + data = { + 'state': 'absent', + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_idempotent_delete(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_records'], # get certificate -> not found + SRR['empty_good'], + SRR['end_of_sequence'] + ] + data = { + 'state': 'absent', + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_delete(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], # get certificate -> found + SRR['error_some_error'], + SRR['end_of_sequence'] + ] + data = { + 'state': 'absent', + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "Error deleting certificate: {'message': 'some error'}" + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_multiple_records(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_multiple_records'], # get certificate -> 2 records! + SRR['end_of_sequence'] + ] + data = { + 'state': 'absent', + 'common_name': 'cname', + 'type': 'client_ca', + } + data.update(set_default_args()) + data.pop('name') + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "Duplicate records with same common_name are preventing safe operations: {'records': [{'uuid': 'ansible'}, {'uuid': 'second'}]}" + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_successful_sign(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['get_uuid'], # get certificate -> found + SRR['empty_good'], + SRR['end_of_sequence'] + ] + data = { + 'vserver': 'abc', + 'signing_request': 'CSR', + 'expiry_time': 'et' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_sign(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['get_uuid'], # get certificate -> found + SRR['error_some_error'], + SRR['end_of_sequence'] + ] + data = { + 'vserver': 'abc', + 'signing_request': 'CSR', + 'expiry_time': 'et' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "Error signing certificate: {'message': 'some error'}" + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_failed_sign_missing_ca(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['empty_records'], # get certificate -> not found + SRR['empty_good'], + SRR['end_of_sequence'] + ] + data = { + 'vserver': 'abc', + 'signing_request': 'CSR' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "signing certificate with name '%s' not found on svm: %s" % (data['name'], data['vserver']) + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_failed_sign_absent(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['get_uuid'], # get certificate -> found + SRR['end_of_sequence'] + ] + data = { + 'vserver': 'abc', + 'signing_request': 'CSR', + 'state': 'absent' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "'signing_request' is not supported with 'state' set to 'absent'" + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_failed_on_name(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['error_unexpected_name'], # get certificate -> error + SRR['end_of_sequence'] + ] + data = { + 'vserver': 'abc', + 'signing_request': 'CSR', + 'state': 'absent', + 'ignore_name_if_not_supported': False, + 'common_name': 'common_name', + 'type': 'root_ca' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + assert exc.value.args[0]['msg'] == NAME_ERROR + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_cannot_ignore_name_error_no_common_name(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['error_unexpected_name'], # get certificate -> error + SRR['end_of_sequence'] + ] + data = { + 'vserver': 'abc', + 'signing_request': 'CSR', + 'state': 'absent', + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + assert exc.value.args[0]['msg'] == NAME_ERROR + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_cannot_ignore_name_error_no_type(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['error_unexpected_name'], # get certificate -> error + SRR['end_of_sequence'] + ] + data = { + 'vserver': 'abc', + 'signing_request': 'CSR', + 'state': 'absent', + 'common_name': 'common_name' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + assert exc.value.args[0]['msg'] == TYPE_ERROR + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_ignore_name_error(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['error_unexpected_name'], # get certificate -> error + SRR['get_uuid'], # get certificate -> found + SRR['end_of_sequence'] + ] + data = { + 'vserver': 'abc', + 'signing_request': 'CSR', + 'state': 'absent', + 'common_name': 'common_name', + 'type': 'root_ca' + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + msg = "'signing_request' is not supported with 'state' set to 'absent'" + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_successful_create_name_error(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_uuid'], + SRR['error_unexpected_name'], # get certificate -> error + SRR['empty_records'], # get certificate -> not found + SRR['empty_good'], + SRR['end_of_sequence'] + ] + data = { + 'common_name': 'cname', + 'type': 'client_ca', + 'vserver': 'abc', + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + print(mock_request.mock_calls) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_data_vserver_not_exist(mock_request): + mock_request.side_effect = [ + SRR['is_rest'], + SRR['empty_records'], + SRR['end_of_sequence'] + ] + data = { + 'common_name': 'cname', + 'type': 'client_ca', + 'vserver': 'abc', + } + data.update(set_default_args()) + set_module_args(data) + my_obj = my_module() + with pytest.raises(AnsibleFailJson) as exc: + my_obj.apply() + assert 'Error vserver abc does not exist or is not a data vserver.' in exc.value.args[0]['msg'] + + +def test_rest_negative_no_name_and_type(): + data = { + 'common_name': 'cname', + # 'type': 'client_ca', + 'vserver': 'abc', + } + data.update(set_default_args()) + data.pop('name') + set_module_args(data) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = "Error: 'name' or ('common_name' and 'type') are required parameters." + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_negative_ZAPI_only(mock_request): + mock_request.side_effect = [ + SRR['is_zapi'], + SRR['end_of_sequence'] + ] + set_module_args(set_default_args()) + with pytest.raises(AnsibleFailJson) as exc: + my_obj = my_module() + print(exc.value.args[0]) + msg = "na_ontap_security_certificates only supports REST, and requires ONTAP 9.6 or later. - Unreachable" + assert msg == exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_config.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_config.py new file mode 100644 index 000000000..1ffdfbc02 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_config.py @@ -0,0 +1,254 @@ +# (c) 2021-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +''' unit tests ONTAP Ansible module: na_ontap_security_config ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + call_main, create_module, create_and_apply, expect_and_capture_ansible_exception, AnsibleFailJson, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_security_config \ + import NetAppOntapSecurityConfig as security_config_module, main as my_main # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'security_config_record': (200, { + "records": [{ + "is_fips_enabled": False, + "supported_protocols": ['TLSv1.3', 'TLSv1.2', 'TLSv1.1'], + "supported_cipher_suites": 'TLS_RSA_WITH_AES_128_CCM_8' + }], "num_records": 1 + }, None), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + + +security_config_info = { + 'num-records': 1, + 'attributes': { + 'security-config-info': { + "interface": 'ssl', + "is-fips-enabled": False, + "supported-protocols": ['TLSv1.2', 'TLSv1.1'], + "supported-ciphers": 'ALL:!LOW:!aNULL:!EXP:!eNULL:!3DES:!DES:!RC4' + } + }, +} + + +ZRR = zapi_responses({ + 'security_config_info': build_zapi_response(security_config_info) +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'never', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + security_config_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_error_get_security_config_info(): + register_responses([ + ('ZAPI', 'security-config-get', ZRR['error']) + ]) + module_args = { + "name": 'ssl', + "is_fips_enabled": False, + "supported_protocols": ['TLSv1.2', 'TLSv1.1'] + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error getting security config for interface" + assert msg in error + + +def test_get_security_config_info(): + register_responses([ + ('security-config-get', ZRR['security_config_info']) + ]) + security_obj = create_module(security_config_module, DEFAULT_ARGS) + result = security_obj.get_security_config() + assert result + + +def test_modify_security_config_fips(): + register_responses([ + ('ZAPI', 'security-config-get', ZRR['security_config_info']), + ('ZAPI', 'security-config-modify', ZRR['success']) + ]) + module_args = { + "is_fips_enabled": True, + "supported_protocols": ['TLSv1.3', 'TLSv1.2'], + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify_security_config_fips(): + register_responses([ + ('ZAPI', 'security-config-get', ZRR['security_config_info']), + ('ZAPI', 'security-config-modify', ZRR['error']) + ]) + module_args = { + "is_fips_enabled": True, + "supported_protocols": ['TLSv1.3', 'TLSv1.2'], + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert "Error modifying security config for interface" in error + + +def test_error_security_config(): + register_responses([ + ]) + module_args = { + "is_fips_enabled": True, + "supported_protocols": ['TLSv1.2', 'TLSv1.1', 'TLSv1'], + } + error = create_module(security_config_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'If fips is enabled then TLSv1 is not a supported protocol' in error + + +def test_error_security_config_supported_ciphers(): + register_responses([ + ]) + module_args = { + "is_fips_enabled": True, + "supported_ciphers": 'ALL:!LOW:!aNULL:!EXP:!eNULL:!3DES:!DES:!RC4', + } + error = create_module(security_config_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'If fips is enabled then supported ciphers should not be specified' in error + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always' +} + + +def test_rest_error_get(): + '''Test error rest get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/security', SRR['generic_error']), + ]) + module_args = { + "is_fips_enabled": False, + "supported_protocols": ['TLSv1.2', 'TLSv1.1'] + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + assert "Error on getting security config: calling: /security: got Expected error." in error + + +def test_rest_get_security_config(): + '''Test error rest get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/security', SRR['security_config_record']), + ]) + module_args = { + "is_fips_enabled": False, + "supported_protocols": ['TLSv1.2', 'TLSv1.1'] + } + security_obj = create_module(security_config_module, ARGS_REST, module_args) + result = security_obj.get_security_config_rest() + assert result + + +def test_rest_modify_security_config(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/security', SRR['security_config_record']), + ('PATCH', '/security', SRR['success']), + ]) + module_args = { + "is_fips_enabled": False, + "supported_protocols": ['TLSv1.3', 'TLSv1.2', 'TLSv1.1'], + "supported_cipher_suites": 'TLS_RSA_WITH_AES_128_CCM' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_rest_error_security_config(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = { + "is_fips_enabled": True, + "supported_protocols": ['TLSv1.2', 'TLSv1.1', 'TLSv1'], + "supported_cipher_suites": 'TLS_RSA_WITH_AES_128_CCM' + } + error = create_module(security_config_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'If fips is enabled then TLSv1 is not a supported protocol' in error + + +def test_rest_error_security_config_protocol(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = { + "is_fips_enabled": True, + "supported_protocols": ['TLSv1.2', 'TLSv1.1'], + "supported_cipher_suites": 'TLS_RSA_WITH_AES_128_CCM' + } + error = create_module(security_config_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'If fips is enabled then TLSv1.1 is not a supported protocol' in error + + +def test_rest_error_modify_security_config(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/security', SRR['security_config_record']), + ('PATCH', '/security', SRR['generic_error']), + ]) + module_args = { + "is_fips_enabled": True, + "supported_protocols": ['TLSv1.3', 'TLSv1.2'], + "supported_cipher_suites": 'TLS_RSA_WITH_AES_128_CCM' + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + assert "Error on modifying security config: calling: /security: got Expected error." in error + + +def test_rest_modify_security_config_fips(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', '/security', SRR['security_config_record']), + ('PATCH', '/security', SRR['success']), + ]) + module_args = { + "is_fips_enabled": True, + "supported_protocols": ['TLSv1.3', 'TLSv1.2'], + "supported_cipher_suites": 'TLS_RSA_WITH_AES_128_CCM' + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_ca_certificate.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_ca_certificate.py new file mode 100644 index 000000000..3728619eb --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_ca_certificate.py @@ -0,0 +1,140 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, expect_and_capture_ansible_exception, call_main, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, \ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_security_ipsec_ca_certificate \ + import NetAppOntapSecurityCACertificate as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'cert1', + 'use_rest': 'always' +} + + +SRR = rest_responses({ + 'ipsec_ca_svm_scope': (200, {"records": [{ + 'name': 'cert1', + 'svm': {'name': 'svm4'}, + 'uuid': '380a12f7' + }], "num_records": 1}, None), + 'ipsec_ca_cluster_scope': (200, {"records": [{ + 'name': 'cert2', + 'scope': 'cluster', + 'uuid': '878eaa35'}], "num_records": 1}, None), + 'error_ipsec_ca_not_exist': (404, None, {'code': 4, 'message': "entry doesn't exist"}), +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "name"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_create_security_ipsec_ca_certificate_svm(): + ''' create ipsec ca certificates in svm ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['ipsec_ca_svm_scope']), # get certificate uuid. + ('GET', 'security/ipsec/ca-certificates/380a12f7', SRR['error_ipsec_ca_not_exist']), # ipsec ca does not exist. + ('POST', 'security/ipsec/ca-certificates', SRR['success']), # create. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['ipsec_ca_svm_scope']), # get certificate uuid. + ('GET', 'security/ipsec/ca-certificates/380a12f7', SRR['ipsec_ca_svm_scope']), # ipsec ca does not exist. + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'svm': 'svm4'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'svm': 'svm4'})['changed'] + + +def test_create_security_ipsec_ca_certificate_cluster(): + ''' create ipsec ca certificates in cluster ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['ipsec_ca_cluster_scope']), + ('GET', 'security/ipsec/ca-certificates/878eaa35', SRR['error_ipsec_ca_not_exist']), + ('POST', 'security/ipsec/ca-certificates', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['ipsec_ca_cluster_scope']), + ('GET', 'security/ipsec/ca-certificates/878eaa35', SRR['ipsec_ca_cluster_scope']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'name': 'cert1'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'name': 'cert1'})['changed'] + + +def test_error_certificate_not_exist(): + ''' error if certificate not present ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['empty_records']), + # do not throw error if certificate not exist and state is absent. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['empty_records']) + ]) + error = "Error: certificate cert1 is not installed" + assert error in create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_delete_security_ipsec_ca_certificate(): + ''' test delete ipsec ca certificate ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['ipsec_ca_cluster_scope']), + ('GET', 'security/ipsec/ca-certificates/878eaa35', SRR['ipsec_ca_cluster_scope']), + ('DELETE', 'security/ipsec/ca-certificates/878eaa35', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['ipsec_ca_cluster_scope']), + ('GET', 'security/ipsec/ca-certificates/878eaa35', SRR['empty_records']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_all_methods_catch_exception(): + ''' test exception in get/create/modify/delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + # GET/POST/DELETE error. + ('GET', 'security/certificates', SRR['generic_error']), + ('GET', 'security/certificates', SRR['ipsec_ca_cluster_scope']), + ('GET', 'security/ipsec/ca-certificates/878eaa35', SRR['generic_error']), + ('POST', 'security/ipsec/ca-certificates', SRR['generic_error']), + ('DELETE', 'security/ipsec/ca-certificates/878eaa35', SRR['generic_error']) + ]) + ca_obj = create_module(my_module, DEFAULT_ARGS) + assert 'Error fetching uuid for certificate' in expect_and_capture_ansible_exception(ca_obj.get_certificate_uuid, 'fail')['msg'] + assert 'Error fetching security IPsec CA certificate' in expect_and_capture_ansible_exception(ca_obj.get_ipsec_ca_certificate, 'fail')['msg'] + assert 'Error adding security IPsec CA certificate' in expect_and_capture_ansible_exception(ca_obj.create_ipsec_ca_certificate, 'fail')['msg'] + assert 'Error deleting security IPsec CA certificate' in expect_and_capture_ansible_exception(ca_obj.delete_ipsec_ca_certificate, 'fail')['msg'] + + +def test_error_ontap9_9_1(): + ''' test module supported from 9.10.1 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']) + ]) + assert 'requires ONTAP 9.10.1 or later' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_config.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_config.py new file mode 100644 index 000000000..e4f7d2527 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_config.py @@ -0,0 +1,87 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, expect_and_capture_ansible_exception, call_main, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, \ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_security_ipsec_config \ + import NetAppOntapSecurityIPsecConfig as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' +} + + +SRR = rest_responses({ + 'ipsec_config': (200, {"records": [{"enabled": True, "replay_window": "64"}]}, None), + 'ipsec_config_1': (200, {"records": [{"enabled": False, "replay_window": "0"}]}, None) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_modify_security_ipsec_config(): + ''' create ipsec policy with certificates ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec', SRR['ipsec_config_1']), + ('PATCH', 'security/ipsec', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec', SRR['ipsec_config']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec', SRR['empty_records']), + ]) + args = { + "enabled": True, + "replay_window": 64 + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_all_methods_catch_exception(): + ''' test exception in get/create/modify/delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # GET/PATCH error. + ('GET', 'security/ipsec', SRR['generic_error']), + ('PATCH', 'security/ipsec', SRR['generic_error']) + ]) + sec_obj = create_module(my_module, DEFAULT_ARGS) + assert 'Error fetching security IPsec config' in expect_and_capture_ansible_exception(sec_obj.get_security_ipsec_config, 'fail')['msg'] + assert 'Error modifying security IPsec config' in expect_and_capture_ansible_exception(sec_obj.modify_security_ipsec_config, 'fail', {})['msg'] + + +def test_error_ontap97(): + ''' test module supported from 9.8 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']) + ]) + assert 'requires ONTAP 9.8.0 or later' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_policy.py new file mode 100644 index 000000000..b913ac03e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ipsec_policy.py @@ -0,0 +1,268 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, \ + create_and_apply, create_module, expect_and_capture_ansible_exception, call_main, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, \ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_security_ipsec_policy \ + import NetAppOntapSecurityIPsecPolicy as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'ipsec_policy', + 'use_rest': 'always', + 'local_endpoint': { + 'address': '10.23.43.23', + 'netmask': '24', + 'port': '201' + }, + 'remote_endpoint': { + 'address': '10.23.43.13', + 'netmask': '24' + }, + 'protocol': 'tcp' +} + + +def form_rest_response(args=None): + response = { + "uuid": "6c025f9b", + "name": "ipsec1", + "scope": "svm", + "svm": {"name": "ansibleSVM"}, + "local_endpoint": { + "address": "10.23.43.23", + "netmask": "24", + "port": "201-201" + }, + "remote_endpoint": { + "address": "10.23.43.13", + "netmask": "24", + "port": "0-0" + }, + "protocol": "tcp", + "local_identity": "ing", + "remote_identity": "ing", + "action": "discard", + "enabled": False, + "authentication_method": "none" + } + if args: + response.update(args) + return response + + +SRR = rest_responses({ + 'ipsec_auth_none': (200, {"records": [form_rest_response()], "num_records": 1}, None), + 'ipsec_auth_psk': (200, {"records": [form_rest_response({ + "action": "esp_transport", + "authentication_method": "psk" + })], "num_records": 1}, None), + 'ipsec_auth_pki': (200, {"records": [form_rest_response({ + "action": "esp_transport", + "authentication_method": "pki", + "certificate": {"name": "ca_cert"} + })], "num_records": 1}, None), + 'ipsec_modify': (200, {"records": [form_rest_response({ + "local_endpoint": {"address": "10.23.43.24", "netmask": "24"}, + "remote_endpoint": {"address": "10.23.43.14", "netmask": "24", "port": "200-200"}, + "protocol": "udp", + })], "num_records": 1}, None), + 'ipsec_ipv6': (200, {"records": [form_rest_response({ + "local_endpoint": {"address": "2402:940::45", "netmask": "64", "port": "120-120"}, + "remote_endpoint": {"address": "2402:940::55", "netmask": "64", "port": "200-200"}, + "protocol": "udp", + })], "num_records": 1}, None), + 'ipsec_ipv6_modify': (200, {"records": [form_rest_response({ + "local_endpoint": {"address": "2402:940::46", "netmask": "64", "port": "120-120"}, + "remote_endpoint": {"address": "2402:940::56", "netmask": "64", "port": "200-200"}, + "protocol": "udp", + })], "num_records": 1}, None) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "name"] + error = create_module(my_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_create_security_ipsec_policy_certificate(): + ''' create ipsec policy with certificates ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ipsec/policies', SRR['empty_records']), + ('POST', 'security/ipsec/policies', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ipsec/policies', SRR['ipsec_auth_pki']), + ]) + args = { + "action": "esp_transport", + "authentication_method": "pki", + "certificate": "ca_cert" + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_create_security_ipsec_policy_psk(): + ''' create ipsec policy with pre-shared keys ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ipsec/policies', SRR['empty_records']), + ('POST', 'security/ipsec/policies', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ipsec/policies', SRR['ipsec_auth_psk']), + ]) + args = { + "action": "esp_transport", + "authentication_method": "psk", + "secret_key": "QDFRTGJUOJDE4RFGDSDW" + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_create_security_ipsec_policy(): + ''' create ipsec policy without authentication method in 9.8 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec/policies', SRR['empty_records']), + ('POST', 'security/ipsec/policies', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec/policies', SRR['ipsec_auth_none']), + ]) + assert create_and_apply(my_module, DEFAULT_ARGS)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS)['changed'] + + +def test_modify_security_ipsec_policy(): + ''' modify ipsec policy ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec/policies', SRR['ipsec_auth_none']), + ('PATCH', 'security/ipsec/policies/6c025f9b', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec/policies', SRR['ipsec_modify']) + ]) + args = { + "local_endpoint": {"address": "10.23.43.24", "netmask": "255.255.255.0"}, + "remote_endpoint": {"address": "10.23.43.14", "netmask": "255.255.255.0", "port": "200"}, + "protocol": "udp" + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_warnings_raised(): + ''' test warnings raised ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + args = {"certificate": "new_Cert", "authentication_method": "pki", "action": "discard"} + create_module(my_module, DEFAULT_ARGS, args) + warning = "The IPsec action is discard" + print_warnings() + assert_warning_was_raised(warning, partial_match=True) + + args = {"secret_key": "AEDFGJTUSHNFGKGLFD", "authentication_method": "psk", "action": "bypass"} + create_module(my_module, DEFAULT_ARGS, args) + warning = "The IPsec action is bypass" + print_warnings() + assert_warning_was_raised(warning, partial_match=True) + + +def test_modify_security_ipsec_policy_ipv6(): + ''' test modify ipv6 address ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec/policies', SRR['ipsec_ipv6']), + ('PATCH', 'security/ipsec/policies/6c025f9b', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec/policies', SRR['ipsec_ipv6_modify']) + ]) + args = { + "local_endpoint": {"address": "2402:0940:000:000:00:00:0000:0046", "netmask": "64"}, + "remote_endpoint": {"address": "2402:0940:000:000:00:00:0000:0056", "netmask": "64", "port": "200"}, + "protocol": "17", + } + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + + +def test_delete_security_ipsec_policy(): + ''' test delete ipsec policy ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec/policies', SRR['ipsec_auth_none']), + ('DELETE', 'security/ipsec/policies/6c025f9b', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/ipsec/policies', SRR['empty_records']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_all_methods_catch_exception(): + ''' test exception in get/create/modify/delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # GET/POST/PATCH/DELETE error. + ('GET', 'security/ipsec/policies', SRR['generic_error']), + ('POST', 'security/ipsec/policies', SRR['generic_error']), + ('PATCH', 'security/ipsec/policies/6c025f9b', SRR['generic_error']), + ('DELETE', 'security/ipsec/policies/6c025f9b', SRR['generic_error']) + ]) + sec_obj = create_module(my_module, DEFAULT_ARGS) + sec_obj.uuid = '6c025f9b' + assert 'Error fetching security ipsec policy' in expect_and_capture_ansible_exception(sec_obj.get_security_ipsec_policy, 'fail')['msg'] + assert 'Error creating security ipsec policy' in expect_and_capture_ansible_exception(sec_obj.create_security_ipsec_policy, 'fail')['msg'] + assert 'Error modifying security ipsec policy' in expect_and_capture_ansible_exception(sec_obj.modify_security_ipsec_policy, 'fail', {})['msg'] + assert 'Error deleting security ipsec policy' in expect_and_capture_ansible_exception(sec_obj.delete_security_ipsec_policy, 'fail')['msg'] + + +def test_modify_error(): + ''' test modify error ''' + register_responses([ + # Error if try to modify certificate for auth_method none. + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ipsec/policies', SRR['ipsec_auth_none']), + # Error if try to modify action and authentication_method + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ipsec/policies', SRR['ipsec_auth_none']) + + ]) + args = {'certificate': 'cert_new'} + assert 'Error: cannot set certificate for IPsec policy' in create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + args = {'authentication_method': 'psk', 'action': 'esp_udp', 'secret_key': 'secretkey'} + assert 'Error: cannot modify options' in create_and_apply(my_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +def test_error_ontap97(): + ''' test module supported from 9.8 ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']) + ]) + assert 'requires ONTAP 9.8.0 or later' in call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_key_manager.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_key_manager.py new file mode 100644 index 000000000..38d18981f --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_key_manager.py @@ -0,0 +1,804 @@ +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_warning_was_raised, call_main, create_module, expect_and_capture_ansible_exception, patch_ansible, print_warnings + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_security_key_manager import\ + NetAppOntapSecurityKeyManager as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +security_key_info = { + 'attributes-list': { + 'key-manager-info': { + 'key-manager-ip-address': '0.1.2.3', + 'key-manager-server-status': 'available', + 'key-manager-tcp-port': '5696', + 'node-name': 'test_node' + } + } +} + +ZRR = zapi_responses({ + 'security_key_info': build_zapi_response(security_key_info, 1) +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never' + } + error = 'missing required arguments:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_get_nonexistent_key_manager(): + ''' Test if get_key_manager() returns None for non-existent key manager ''' + register_responses([ + ('ZAPI', 'security-key-manager-get-iter', ZRR['no_records']), + ]) + module_args = { + 'ip_address': '1.2.3.4', + 'use_rest': 'never' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + result = my_obj.get_key_manager() + assert result is None + + +def test_get_existing_key_manager(): + ''' Test if get_key_manager() returns details for existing key manager ''' + register_responses([ + ('ZAPI', 'security-key-manager-get-iter', ZRR['security_key_info']), + ]) + module_args = { + 'ip_address': '1.2.3.4', + 'use_rest': 'never' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + result = my_obj.get_key_manager() + assert result['ip_address'] == '0.1.2.3' + + +def test_successfully_add_key_manager(): + ''' Test successfully add key manager''' + register_responses([ + ('ZAPI', 'security-key-manager-setup', ZRR['success']), + ('ZAPI', 'security-key-manager-get-iter', ZRR['no_records']), + ('ZAPI', 'security-key-manager-add', ZRR['success']), + # idempotency + ('ZAPI', 'security-key-manager-setup', ZRR['success']), + ('ZAPI', 'security-key-manager-get-iter', ZRR['security_key_info']), + ]) + module_args = { + 'ip_address': '0.1.2.3', + 'use_rest': 'never' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify_key_manager(): + ''' Test successfully add key manager''' + register_responses([ + ('ZAPI', 'security-key-manager-setup', ZRR['success']), + ('ZAPI', 'security-key-manager-get-iter', ZRR['security_key_info']), + ]) + module_args = { + 'ip_address': '1.2.3.4', + 'use_rest': 'never' + } + error = "Error, cannot modify existing configuraton: modify is not supported with ZAPI, new values: {'ip_address': '1.2.3.4'}, current values:" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successfully_delete_key_manager(): + ''' Test successfully delete key manager''' + register_responses([ + ('ZAPI', 'security-key-manager-setup', ZRR['success']), + ('ZAPI', 'security-key-manager-get-iter', ZRR['security_key_info']), + ('ZAPI', 'security-key-manager-delete', ZRR['success']), + # idempotency + ('ZAPI', 'security-key-manager-setup', ZRR['success']), + ('ZAPI', 'security-key-manager-get-iter', ZRR['no_records']), + ]) + module_args = { + 'ip_address': '1.2.3.4', + 'state': 'absent', + 'use_rest': 'never', + 'node': 'some_node' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised('The option "node" is deprecated and should not be used.') + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + module_args = { + 'ip_address': '1.2.3.4', + 'use_rest': 'never', + 'node': 'some_node' + } + error = 'Error: the python NetApp-Lib module is required. Import error: None' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print_warnings() + assert_warning_was_raised('The option "node" is deprecated and should not be used.') + + +def test_error_handling(): + ''' test error handling on ZAPI calls ''' + register_responses([ + ('ZAPI', 'security-key-manager-setup', ZRR['error']), + ('ZAPI', 'security-key-manager-get-iter', ZRR['error']), + ('ZAPI', 'security-key-manager-add', ZRR['error']), + ('ZAPI', 'security-key-manager-delete', ZRR['error']), + + ]) + module_args = { + 'ip_address': '1.2.3.4', + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = zapi_error_message('Error setting up key manager') + assert error in expect_and_capture_ansible_exception(my_obj.key_manager_setup, 'fail')['msg'] + error = zapi_error_message('Error fetching key manager') + assert error in expect_and_capture_ansible_exception(my_obj.get_key_manager, 'fail')['msg'] + error = zapi_error_message('Error creating key manager') + assert error in expect_and_capture_ansible_exception(my_obj.create_key_manager, 'fail')['msg'] + error = zapi_error_message('Error deleting key manager') + assert error in expect_and_capture_ansible_exception(my_obj.delete_key_manager, 'fail')['msg'] + + +def test_rest_is_required(): + '''report error if external or onboard are used with ZAPI''' + register_responses([ + ]) + module_args = { + 'onboard': { + 'synchronize': True + }, + 'use_rest': 'never', + } + error = 'Error: REST is required for onboard option.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args = { + 'external': { + 'servers': ['0.1.2.3:5696'], + 'client_certificate': 'client_certificate', + 'server_ca_certificates': ['server_ca_certificate'] + }, + 'use_rest': 'never', + 'vserver': 'svm_name', + } + error = 'options.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'one_external_seckey_record': (200, { + 'records': [{ + 'uuid': 'a1b2c3', + 'external': { + 'servers': [{'server': '0.1.2.3:5696'}] + }}], + 'num_records': 1 + }, None), + 'one_external_seckey_record_2_servers': (200, { + 'records': [{ + 'uuid': 'a1b2c3', + 'external': { + 'servers': [ + {'server': '1.2.3.4:5696'}, + {'server': '0.1.2.3:5696'}] + }, + 'onboard': {'enabled': False}}], + 'num_records': 1 + }, None), + 'one_onboard_seckey_record': (200, { + 'records': [{ + 'uuid': 'a1b2c3', + 'onboard': { + 'enabled': True, + 'key_backup': "certificate", + }}], + 'num_records': 1 + }, None), + 'one_security_certificate_record': (200, { + 'records': [{'uuid': 'a1b2c3'}], + 'num_records': 1 + }, None), + 'error_duplicate': (400, None, {'message': 'New passphrase cannot be same as the old passphrase.'}), + 'error_incorrect': (400, None, {'message': 'Cluster-wide passphrase is incorrect.'}), + 'error_svm_not_found': (400, None, {'message': 'SVM "svm_name" does not exist'}), + 'error_already_present': (400, None, {'message': 'already has external key management configured'}), +}, False) + + +def test_successfully_add_key_manager_old_style_rest(): + ''' Test successfully add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['zero_records']), + ('POST', 'security/key-managers', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_external_seckey_record']), + ]) + module_args = { + 'ip_address': '0.1.2.3', + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_add_key_manager_external_rest(): + ''' Test successfully add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['zero_records']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ('POST', 'security/key-managers', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_external_seckey_record']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ]) + module_args = { + 'external': { + 'servers': ['0.1.2.3:5696'], + 'client_certificate': 'client_certificate', + 'server_ca_certificates': ['server_ca_certificate'] + }, + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_add_key_manager_external_rest_svm(): + ''' Test successfully add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['zero_records']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ('POST', 'security/key-managers', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_external_seckey_record']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ]) + module_args = { + 'external': { + 'servers': ['0.1.2.3:5696'], + 'client_certificate': 'client_certificate', + 'server_ca_certificates': ['server_ca_certificate'] + }, + 'vserver': 'svm_name', + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_add_key_manager_onboard_rest(): + ''' Test successfully add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['zero_records']), + ('POST', 'security/key-managers', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_duplicate']), + ]) + module_args = { + 'onboard': { + 'passphrase': 'passphrase_too_short', + 'from_passphrase': 'ignored on create', + }, + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_add_key_manager_onboard_svm_rest(): + ''' Test successfully add key manager''' + register_responses([ + ]) + module_args = { + 'onboard': { + 'passphrase': 'passphrase_too_short', + 'from_passphrase': 'ignored on create', + }, + 'vserver': 'svm_name', + 'use_rest': 'always' + } + error = 'parameters are mutually exclusive:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successfully_delete_key_manager_rest(): + ''' Test successfully add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('DELETE', 'security/key-managers/a1b2c3', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['zero_records']), + ]) + module_args = { + 'state': 'absent', + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_change_passphrase_onboard_key_manager_rest(): + ''' Test successfully add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_incorrect']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_duplicate']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + # both passphrases are incorrect + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_incorrect']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_incorrect']), + # unexpected success on check passphrase + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_duplicate']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + # unexpected success on check passphrase + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_incorrect']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + # unexpected success on check passphrase + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + # unexpected error on check passphrase + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['generic_error']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_duplicate']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + # unexpected error on check passphrase + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_incorrect']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['generic_error']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + # unexpected error on check passphrase + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['generic_error']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['generic_error']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_duplicate']), + ]) + module_args = { + 'onboard': { + 'passphrase': 'passphrase_too_short', + 'from_passphrase': 'passphrase_too_short' + }, + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + error = rest_error_message('Error: neither from_passphrase nor passphrase match installed passphrase', + 'security/key-managers/a1b2c3', + got="got {'message': 'Cluster-wide passphrase is incorrect.'}.") + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + # success + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # ignored error + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # idempotency + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_change_passphrase_and_sync_onboard_key_manager_rest(): + ''' Test successfully modify onboard key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_incorrect']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_duplicate']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + # idempotency - sync is always sent! + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['error_duplicate']), + ('PATCH', 'security/key-managers/a1b2c3', SRR['success']), + ]) + module_args = { + 'onboard': { + 'passphrase': 'passphrase_too_short', + 'from_passphrase': 'passphrase_too_short', + 'synchronize': True + }, + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # idempotency + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_change_external_key_manager_rest(): + ''' Test successfully add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_external_seckey_record_2_servers']), + ('DELETE', 'security/key-managers/a1b2c3/key-servers/1.2.3.4:5696', SRR['success']), + ('DELETE', 'security/key-managers/a1b2c3/key-servers/0.1.2.3:5696', SRR['success']), + ('POST', 'security/key-managers/a1b2c3/key-servers', SRR['success']), + ('POST', 'security/key-managers/a1b2c3/key-servers', SRR['success']), + # same servers but different order + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_external_seckey_record_2_servers']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_external_seckey_record_2_servers']), + ]) + module_args = { + 'external': { + 'servers': ['0.1.2.3:5697', '1.2.3.4:5697'] + }, + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # same servers but different order + module_args = { + 'external': { + 'servers': ['0.1.2.3:5696', '1.2.3.4:5696'] + }, + 'use_rest': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # idempotency + module_args = { + 'external': { + 'servers': ['1.2.3.4:5696', '0.1.2.3:5696'] + }, + 'use_rest': 'always' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_external_key_manager_rest(): + ''' Test error add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['generic_error']), + ('GET', 'security/certificates', SRR['generic_error']), + ('POST', 'security/key-managers', SRR['generic_error']), + ('PATCH', 'security/key-managers/123', SRR['generic_error']), + ('DELETE', 'security/key-managers/123', SRR['generic_error']), + ('POST', 'security/key-managers/123/key-servers', SRR['generic_error']), + ('DELETE', 'security/key-managers/123/key-servers/server_name', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = rest_error_message('Error fetching key manager info for cluster', 'security/key-managers') + assert error in expect_and_capture_ansible_exception(my_obj.get_key_manager, 'fail')['msg'] + error = rest_error_message('Error fetching security certificate info for name of type: type on cluster', 'security/certificates') + assert error in expect_and_capture_ansible_exception(my_obj.get_security_certificate_uuid_rest, 'fail', 'name', 'type')['msg'] + error = rest_error_message('Error creating key manager for cluster', 'security/key-managers') + assert error in expect_and_capture_ansible_exception(my_obj.create_key_manager_rest, 'fail')['msg'] + my_obj.uuid = '123' + error = rest_error_message('Error modifying key manager for cluster', 'security/key-managers/123') + assert error in expect_and_capture_ansible_exception(my_obj.modify_key_manager_rest, 'fail', {'onboard': {'xxxx': 'yyyy'}})['msg'] + error = rest_error_message('Error deleting key manager for cluster', 'security/key-managers/123') + assert error in expect_and_capture_ansible_exception(my_obj.delete_key_manager_rest, 'fail')['msg'] + error = rest_error_message('Error adding external key server server_name', 'security/key-managers/123/key-servers') + assert error in expect_and_capture_ansible_exception(my_obj.add_external_server_rest, 'fail', 'server_name')['msg'] + error = rest_error_message('Error removing external key server server_name', 'security/key-managers/123/key-servers/server_name') + assert error in expect_and_capture_ansible_exception(my_obj.remove_external_server_rest, 'fail', 'server_name')['msg'] + + +def test_get_security_certificate_uuid_rest_by_name_then_common_name(): + ''' Use name first, then common_name''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + # not found + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['zero_records']), + # with 9.7 or earlier, name is not supported + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ]) + module_args = { + 'use_rest': 'always', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_security_certificate_uuid_rest('name', 'type') is not None + assert_warning_was_raised('certificate name not found, retrying with common_name and type type.') + # not found, neither with name nor common_name + error = 'Error fetching security certificate info for name of type: type on cluster: not found.' + assert error in expect_and_capture_ansible_exception(my_obj.get_security_certificate_uuid_rest, 'fail', 'name', 'type')['msg'] + # 9.7 + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_security_certificate_uuid_rest('name', 'type') is not None + assert_warning_was_raised('name is not supported in 9.6 or 9.7, using common_name name and type type.') + + +def test_get_security_certificate_uuid_rest_by_name_then_common_name_svm(): + ''' With SVM, retry at cluster scope if not found or error at SVM scope ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + # not found + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['generic_error']), + ('GET', 'security/certificates', SRR['zero_records']), + # with 9.7 or earlier, name is not supported + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ]) + module_args = { + 'use_rest': 'always', + 'vserver': 'svm_name' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_security_certificate_uuid_rest('name', 'type') is not None + assert_warning_was_raised('certificate name not found, retrying with common_name and type type.') + # not found, neither with name nor common_name + error = 'Error fetching security certificate info for name of type: type on vserver: svm_name: not found.' + assert error in expect_and_capture_ansible_exception(my_obj.get_security_certificate_uuid_rest, 'fail', 'name', 'type')['msg'] + # 9.7 + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_security_certificate_uuid_rest('name', 'type') is not None + assert_warning_was_raised('name is not supported in 9.6 or 9.7, using common_name name and type type.') + + +def test_warn_when_onboard_exists_and_only_one_passphrase_present(): + ''' Warn if only one passphrase is present ''' + register_responses([ + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ]) + module_args = { + 'onboard': { + 'passphrase': 'passphrase_too_short', + }, + 'use_rest': 'always' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_warning_was_raised('passphrase is ignored') + module_args = { + 'onboard': { + 'from_passphrase': 'passphrase_too_short', + }, + 'use_rest': 'always' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert_warning_was_raised('from_passphrase is ignored') + + +def test_error_cannot_change_key_manager_type_rest(): + ''' Warn if only one passphrase is present ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_external_seckey_record']), + ]) + module_args = { + 'external': { + 'servers': ['0.1.2.3:5697', '1.2.3.4:5697'] + }, + 'use_rest': 'always' + } + error = 'Error, cannot modify existing configuraton: onboard key-manager is already installed, it needs to be deleted first.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args = { + 'onboard': { + 'from_passphrase': 'passphrase_too_short', + }, + 'use_rest': 'always' + } + error = 'Error, cannot modify existing configuraton: external key-manager is already installed, it needs to be deleted first.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_error_sync_repquires_passphrase_rest(): + ''' Warn if only one passphrase is present ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['one_onboard_seckey_record']), + ]) + module_args = { + 'onboard': { + 'synchronize': True + }, + 'use_rest': 'always' + } + error = 'Error: passphrase is required for synchronize.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_return_not_present_when_svm_not_found_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['error_svm_not_found']), + ]) + module_args = { + 'state': 'absent', + 'vserver': 'svm_name', + 'use_rest': 'always' + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_retry_on_create_error(dont_sleep): + """ when no key server is present, REST does not return a record """ + ''' Test successfully add key manager''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/key-managers', SRR['zero_records']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ('GET', 'security/certificates', SRR['one_security_certificate_record']), + ('POST', 'security/key-managers', SRR['error_already_present']), + ('DELETE', 'security/key-managers', SRR['success']), + # we only retry once, erroring out + ('POST', 'security/key-managers', SRR['error_already_present']), + + ]) + module_args = { + 'external': { + 'servers': ['0.1.2.3:5696'], + 'client_certificate': 'client_certificate', + 'server_ca_certificates': ['server_ca_certificate'] + }, + 'vserver': 'svm_name', + 'use_rest': 'always' + } + error = 'Error creating key manager for cluster:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_update_key_server_list(): + ''' Validate servers are added/removed ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # add/remove + ('DELETE', 'security/key-managers/123/key-servers/s1', SRR['success']), + ('DELETE', 'security/key-managers/123/key-servers/s3', SRR['success']), + ('POST', 'security/key-managers/123/key-servers', SRR['success']), + ('POST', 'security/key-managers/123/key-servers', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + } + # no requested change + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + current = { + 'external': { + 'servers': [ + {'server': 's1'}, + {'server': 's2'}, + {'server': 's3'}, + ] + } + } + # idempotent + assert my_obj.update_key_server_list(current) is None + my_obj.parameters['external'] = { + 'servers': [ + {'server': 's1'}, + {'server': 's2'}, + {'server': 's3'}, + ] + } + assert my_obj.update_key_server_list(current) is None + # delete/add + my_obj.parameters['external'] = { + 'servers': [ + {'server': 's4'}, + {'server': 's2'}, + {'server': 's5'}, + ] + } + my_obj.uuid = '123' + assert my_obj.update_key_server_list(current) is None diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ssh.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ssh.py new file mode 100644 index 000000000..f7723db63 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_security_ssh.py @@ -0,0 +1,164 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible, call_main +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_security_ssh import main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +SRR = rest_responses({ + 'ssh_security': (200, { + "records": [ + { + "ciphers": [ + "aes256_ctr", + "aes192_ctr", + "aes128_ctr" + ], + "max_authentication_retry_count": 0, + "svm": { + "name": "ansibleSVM", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "mac_algorithms": ["hmac_sha1", "hmac_sha2_512_etm"], + "key_exchange_algorithms": [ + "diffie_hellman_group_exchange_sha256", + "diffie_hellman_group14_sha1" + ], + }], + "num_records": 1 + }, None), + 'ssh_security_no_svm': (200, { + "records": [ + { + "ciphers": [ + "aes256_ctr", + + ], + }], + "num_records": 1 + }, None), +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', +} + + +def test_get_security_ssh_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ssh/svms', SRR['generic_error']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ssh', SRR['generic_error']) + ]) + module_args = {"vserver": "AnsibleSVM"} + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = 'calling: security/ssh/svms: got Expected error.' + assert msg in error + error = call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] + + +def test_modify_security_ssh_algorithms_rest(): + ''' test modify algorithms ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ssh/svms', SRR['ssh_security']), + ('PATCH', 'security/ssh/svms/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['empty_good']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ssh', SRR['ssh_security']), + ('PATCH', 'security/ssh', SRR['empty_good']), + ]) + module_args = { + "vserver": "AnsibleSVM", + "ciphers": ["aes256_ctr", "aes192_ctr"], + "mac_algorithms": ["hmac_sha1", "hmac_sha2_512_etm"], + "key_exchange_algorithms": ["diffie_hellman_group_exchange_sha256"], + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args.pop('vserver') + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_security_ssh_retry_rest(): + ''' test modify maximum retry count ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ssh/svms', SRR['ssh_security']), + ('PATCH', 'security/ssh/svms/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['empty_good']), + ]) + module_args = { + "vserver": "AnsibleSVM", + "max_authentication_retry_count": 2, + } + assert call_main(my_main, DEFAULT_ARGS, module_args) + + +def test_error_modify_security_ssh_rest(): + ''' test modify algorithms ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ssh/svms', SRR['ssh_security']), + ('PATCH', 'security/ssh/svms/02c9e252-41be-11e9-81d5-00a0986138f7', SRR['generic_error']), + ]) + module_args = { + "vserver": "AnsibleSVM", + "ciphers": ["aes256_ctr", "aes192_ctr"], + "max_authentication_retry_count": 2, + "mac_algorithms": ["hmac_sha1", "hmac_sha2_512_etm"], + "key_exchange_algorithms": ["diffie_hellman_group_exchange_sha256"], + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = 'calling: security/ssh/svms/02c9e252-41be-11e9-81d5-00a0986138f7: got Expected error.' + assert msg in error + + +def test_error_empty_security_ssh_rest(): + ''' Validation of input parameters ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + module_args = { + "ciphers": [] + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = 'Removing all SSH ciphers is not supported. SSH login would fail. ' + \ + 'There must be at least one ciphers associated with the SSH configuration.' + assert msg in error + + +def test_module_error_ontap_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + module_args = {'use_rest': 'always'} + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error: na_ontap_security_ssh only supports REST, and requires ONTAP 9.10.1 or later' in error + + +def test_module_error_no_svm_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/ssh/svms', SRR['ssh_security_no_svm']), + ]) + module_args = { + "vserver": "AnsibleSVM", + "ciphers": ["aes256_ctr", "aes192_ctr"] + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Error: no uuid found for the SVM' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_service_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_service_policy.py new file mode 100644 index 000000000..c11c44059 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_service_policy.py @@ -0,0 +1,402 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP service policy Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_no_warnings, expect_and_capture_ansible_exception, call_main, create_module, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_service_policy import NetAppOntapServicePolicy as my_module, main as my_main + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'name': 'sp123', +} + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'one_sp_record': (200, { + "records": [{ + 'name': 'sp123', + 'uuid': 'uuid123', + 'svm': dict(name='vserver'), + 'services': ['data_core'], + 'scope': 'svm', + 'ipspace': dict(name='ipspace') + }], + 'num_records': 1 + }, None), + 'two_sp_records': (200, { + "records": [ + { + 'name': 'sp123', + }, + { + 'name': 'sp124', + }], + 'num_records': 2 + }, None), +}, False) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + module_args = { + 'hostname': '' + } + error = 'missing required arguments: name' + assert error == call_main(my_main, module_args, fail=True)['msg'] + + +def test_ensure_get_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['one_sp_record']), + ]) + module_args = { + 'services': ['data_core'], + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is False + assert_no_warnings() + + +def test_ensure_create_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['zero_records']), + ('POST', 'network/ip/service-policies', SRR['empty_good']), + ]) + module_args = { + 'services': ['data_core'], + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + assert_no_warnings() + + +def test_ensure_create_called_cluster(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['zero_records']), + ('POST', 'network/ip/service-policies', SRR['empty_good']), + ]) + module_args = { + 'ipspace': 'ipspace', + 'services': ['data_core'] + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + assert_no_warnings() + + +def test_ensure_create_idempotent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['one_sp_record']), + ]) + module_args = { + 'services': ['data_core'], + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is False + assert_no_warnings() + + +def test_ensure_modify_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['one_sp_record']), + ('PATCH', 'network/ip/service-policies/uuid123', SRR['empty_good']), + ]) + module_args = { + 'services': ['data_nfs'], + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + assert_no_warnings() + + +def test_ensure_modify_called_no_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['one_sp_record']), + ('PATCH', 'network/ip/service-policies/uuid123', SRR['empty_good']), + ]) + module_args = { + 'services': ['no_service'], + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + assert_no_warnings() + + +def test_ensure_delete_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['one_sp_record']), + ('DELETE', 'network/ip/service-policies/uuid123', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent', + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is True + assert_no_warnings() + + +def test_ensure_delete_idempotent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['zero_records']), + ]) + module_args = { + 'state': 'absent', + 'vserver': 'vserver', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] is False + assert_no_warnings() + + +def test_negative_extra_record(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['two_sp_records']), + ]) + module_args = { + 'services': ['data_nfs'], + 'vserver': 'vserver', + } + error = 'Error in get_service_policy: calling: network/ip/service-policies: unexpected response' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_ipspace_required_1(): + module_args = { + 'services': ['data_nfs'], + 'vserver': None, + } + error = "vserver is None but all of the following are missing: ipspace" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_ipspace_required_2(): + module_args = { + 'scope': 'cluster', + 'services': ['data_nfs'], + 'vserver': None, + } + error = "scope is cluster but all of the following are missing: ipspace" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_ipspace_required_3(): + module_args = { + 'services': ['data_nfs'], + } + error = "one of the following is required: ipspace, vserver" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_vserver_required_1(): + module_args = { + 'scope': 'svm', + 'services': ['data_nfs'], + } + error = "one of the following is required: ipspace, vserver" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_vserver_required_2(): + module_args = { + 'ipspace': None, + 'scope': 'svm', + 'services': ['data_nfs'], + } + error = "scope is svm but all of the following are missing: vserver" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_vserver_required_3(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'ipspace': None, + 'scope': 'svm', + 'services': ['data_nfs'], + 'vserver': None, + } + error = 'Error: vserver cannot be None when "scope: svm" is specified.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_vserver_not_required(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'ipspace': None, + 'scope': 'cluster', + 'services': ['data_nfs'], + 'vserver': 'vserver', + } + error = 'Error: vserver cannot be set when "scope: cluster" is specified. Got: vserver' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_no_service_not_alone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'scope': 'svm', + 'services': ['data_nfs', 'no_service'], + 'vserver': 'vserver', + } + error = "Error: no other service can be present when no_service is specified." + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_no_service_not_alone_with_cluster_scope(): + module_args = { + 'ipspace': 'ipspace', + 'scope': 'cluster', + 'services': ['data_nfs', 'no_service'], + 'vserver': 'vserver', + } + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + error = "Error: no other service can be present when no_service is specified." + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_extra_arg_in_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['one_sp_record']), + ]) + module_args = { + 'ipspace': 'ipspace', + 'scope': 'cluster', + 'services': ['data_nfs'], + } + error = "Error: attributes not supported in modify: {'scope': 'cluster'}" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_empty_body_in_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'scope': 'svm', + 'services': ['data_nfs'], + 'vserver': 'vserver', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + current = dict(uuid='') + modify = {} + error = 'Error: nothing to change - modify called with: {}' + assert error in expect_and_capture_ansible_exception(my_obj.modify_service_policy, 'fail', current, modify)['msg'] + assert_no_warnings() + + +def test_negative_create_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['zero_records']), + ('POST', 'network/ip/service-policies', SRR['generic_error']), + ]) + module_args = { + 'scope': 'svm', + 'services': ['data_nfs'], + 'vserver': 'vserver', + } + error = rest_error_message('Error in create_service_policy', 'network/ip/service-policies') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_delete_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['one_sp_record']), + ('DELETE', 'network/ip/service-policies/uuid123', SRR['generic_error']), + ]) + module_args = { + 'state': 'absent', + 'vserver': 'vserver', + } + error = rest_error_message('Error in delete_service_policy', 'network/ip/service-policies/uuid123') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_modify_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'network/ip/service-policies', SRR['one_sp_record']), + ('PATCH', 'network/ip/service-policies/uuid123', SRR['generic_error']), + ]) + module_args = { + 'services': ['data_nfs'], + 'vserver': 'vserver', + } + error = rest_error_message('Error in modify_service_policy', 'network/ip/service-policies/uuid123') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + + +def test_negative_unknown_services(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'services': ['data_nfs9'], + 'vserver': 'vserver', + } + error = 'Error: unknown service: data_nfs9. New services may need to be added to "additional_services".' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert_no_warnings() + module_args = { + 'services': ['data_nfs9', 'data_cifs', 'dummy'], + 'vserver': 'vserver', + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + for needle in ['Error: unknown services:', 'data_nfs9', 'dummy']: + assert needle in error + assert 'data_cifs' not in error + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_service_processor_network.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_service_processor_network.py new file mode 100644 index 000000000..c8c249810 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_service_processor_network.py @@ -0,0 +1,296 @@ +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_service_processor_network \ + import NetAppOntapServiceProcessorNetwork as sp_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def mock_args(enable=False, use_rest=False): + data = { + 'node': 'test-vsim1', + 'is_enabled': enable, + 'address_type': 'ipv4', + 'hostname': 'host', + 'username': 'admin', + 'password': 'password', + 'use_rest': 'never' + } + if enable is True: + data['is_enabled'] = enable + data['ip_address'] = '1.1.1.1' + data['gateway_ip_address'] = '2.2.2.2' + data['netmask'] = '255.255.248.0' + data['dhcp'] = 'none' + if use_rest: + data['use_rest'] = 'always' + return data + + +sp_enabled_info = { + 'num-records': 1, + 'attributes-list': { + 'service-processor-network-info': { + 'node': 'test-vsim1', + 'is-enabled': 'true', + 'address-type': 'ipv4', + 'dhcp': 'v4', + 'gateway-ip-address': '2.2.2.2', + 'netmask': '255.255.248.0', + 'ip-address': '1.1.1.1', + 'setup-status': 'succeeded' + } + } +} + +sp_disabled_info = { + 'num-records': 1, + 'attributes-list': { + 'service-processor-network-info': { + 'node-name': 'test-vsim1', + 'is-enabled': 'false', + 'address-type': 'ipv4', + 'setup-status': 'not_setup' + } + } +} + +sp_status_info = { + 'num-records': 1, + 'attributes-list': { + 'service-processor-network-info': { + 'node-name': 'test-vsim1', + 'is-enabled': 'false', + 'address-type': 'ipv4', + 'setup-status': 'in_progress' + } + } +} + +ZRR = zapi_responses({ + 'sp_enabled_info': build_zapi_response(sp_enabled_info), + 'sp_disabled_info': build_zapi_response(sp_disabled_info), + 'sp_status_info': build_zapi_response(sp_status_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "node", "address_type"] + error = create_module(sp_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_modify_error_on_disabled_sp(): + ''' a more interesting test ''' + register_responses([ + ('service-processor-network-get-iter', ZRR['sp_disabled_info']) + ]) + error = 'Error: Cannot modify a service processor network if it is disabled in ZAPI' + assert error in create_and_apply(sp_module, mock_args(), {'ip_address': '1.1.1.1'}, 'error')['msg'] + + +def test_modify_error_on_disabe_dhcp_without_ip(): + ''' a more interesting test ''' + register_responses([ + ('service-processor-network-get-iter', ZRR['sp_enabled_info']) + ]) + error = 'Error: To disable dhcp, configure ip-address, netmask and gateway details manually.' + assert error in create_and_apply(sp_module, mock_args(enable=True), None, fail=True)['msg'] + + +def test_modify_error_of_params_disabled_false(): + ''' a more interesting test ''' + register_responses([ + ('service-processor-network-get-iter', ZRR['sp_enabled_info']) + ]) + error = 'Error: Cannot modify any other parameter for a service processor network if option "is_enabled" is set to false.' + assert error in create_and_apply(sp_module, mock_args(), {'ip_address': '2.1.1.1'}, 'error')['msg'] + + +def test_modify_sp(): + ''' a more interesting test ''' + register_responses([ + ('service-processor-network-get-iter', ZRR['sp_enabled_info']), + ('service-processor-network-modify', ZRR['success']) + ]) + assert create_and_apply(sp_module, mock_args(enable=True), {'ip_address': '3.3.3.3'})['changed'] + + +@patch('time.sleep') +def test_modify_sp_wait(sleep): + ''' a more interesting test ''' + register_responses([ + ('service-processor-network-get-iter', ZRR['sp_enabled_info']), + ('service-processor-network-modify', ZRR['success']), + ('service-processor-network-get-iter', ZRR['sp_enabled_info']) + ]) + args = {'ip_address': '3.3.3.3', 'wait_for_completion': True} + assert create_and_apply(sp_module, mock_args(enable=True), args)['changed'] + + +def test_non_existing_sp(): + register_responses([ + ('service-processor-network-get-iter', ZRR['no_records']) + ]) + error = 'Error No Service Processor for node: test-vsim1' + assert create_and_apply(sp_module, mock_args(), fail=True)['msg'] + + +@patch('time.sleep') +def test_wait_on_sp_status(sleep): + register_responses([ + ('service-processor-network-get-iter', ZRR['sp_enabled_info']), + ('service-processor-network-modify', ZRR['success']), + ('service-processor-network-get-iter', ZRR['sp_status_info']), + ('service-processor-network-get-iter', ZRR['sp_status_info']), + ('service-processor-network-get-iter', ZRR['sp_status_info']), + ('service-processor-network-get-iter', ZRR['sp_status_info']), + ('service-processor-network-get-iter', ZRR['sp_enabled_info']) + ]) + args = {'ip_address': '3.3.3.3', 'wait_for_completion': True} + assert create_and_apply(sp_module, mock_args(enable=True), args)['changed'] + + +def test_if_all_methods_catch_exception(): + ''' test error zapi - get/modify''' + register_responses([ + ('service-processor-network-get-iter', ZRR['error']), + ('service-processor-network-get-iter', ZRR['error']), + ('service-processor-network-modify', ZRR['error']) + ]) + sp_obj = create_module(sp_module, mock_args()) + + assert 'Error fetching service processor network info' in expect_and_capture_ansible_exception(sp_obj.get_service_processor_network, 'fail')['msg'] + assert 'Error fetching service processor network status' in expect_and_capture_ansible_exception(sp_obj.get_sp_network_status, 'fail')['msg'] + assert 'Error modifying service processor network' in expect_and_capture_ansible_exception(sp_obj.modify_service_processor_network, 'fail', {})['msg'] + + +SRR = rest_responses({ + 'sp_enabled_info': (200, {"records": [{ + 'name': 'ansdev-stor-1', + 'service_processor': { + 'dhcp_enabled': False, + 'firmware_version': '3.10', + 'ipv4_interface': { + 'address': '1.1.1.1', + 'gateway': '2.2.2.2', + 'netmask': '255.255.248.0' + }, + 'link_status': 'up', + 'state': 'online' + }, + 'uuid': '5dd7aed0'} + ]}, None), + 'sp_disabled_info': (200, {"records": [{ + 'name': 'ansdev-stor-1', + 'service_processor': { + 'firmware_version': '3.10', + 'link_status': 'up', + 'state': 'online' + }, + 'uuid': '5dd7aed0'} + ]}, None) +}) + + +def test_modify_sp_rest(): + ''' modify sp in rest ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster/nodes', SRR['sp_enabled_info']), + ('PATCH', 'cluster/nodes/5dd7aed0', SRR['success']) + ]) + assert create_and_apply(sp_module, mock_args(enable=True, use_rest=True), {'ip_address': '3.3.3.3'})['changed'] + + +def test_non_existing_sp_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster/nodes', SRR['empty_records']) + ]) + error = 'Error No Service Processor for node: test-vsim1' + assert create_and_apply(sp_module, mock_args(enable=True, use_rest=True), fail=True)['msg'] + + +def test_if_all_methods_catch_exception_rest(): + ''' test error zapi - get/modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster/nodes', SRR['generic_error']), + ('PATCH', 'cluster/nodes/5dd7aed0', SRR['generic_error']) + ]) + sp_obj = create_module(sp_module, mock_args(use_rest=True)) + sp_obj.uuid = '5dd7aed0' + assert 'Error fetching service processor network info' in expect_and_capture_ansible_exception(sp_obj.get_service_processor_network, 'fail')['msg'] + assert 'Error modifying service processor network' in expect_and_capture_ansible_exception(sp_obj.modify_service_processor_network, 'fail', {})['msg'] + + +def test_disable_sp_rest(): + ''' disable not supported in REST ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster/nodes', SRR['sp_enabled_info']) + ]) + error = 'Error: disable service processor network status not allowed in REST' + assert error in create_and_apply(sp_module, mock_args(enable=True, use_rest=True), {'is_enabled': False}, 'fail')['msg'] + + +def test_enable_sp_rest_without_ip_or_dhcp(): + ''' enable requires ip or dhcp in REST ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster/nodes', SRR['sp_disabled_info']) + ]) + error = 'Error: enable service processor network requires dhcp or ip_address,netmask,gateway details in REST.' + assert error in create_and_apply(sp_module, mock_args(use_rest=True), {'is_enabled': True}, 'fail')['msg'] + + +@patch('time.sleep') +def test_wait_on_sp_status_rest(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'cluster/nodes', SRR['sp_disabled_info']), + ('PATCH', 'cluster/nodes/5dd7aed0', SRR['success']), + ('GET', 'cluster/nodes', SRR['sp_disabled_info']), + ('GET', 'cluster/nodes', SRR['sp_disabled_info']), + ('GET', 'cluster/nodes', SRR['sp_enabled_info']) + ]) + args = {'ip_address': '1.1.1.1', 'wait_for_completion': True} + assert create_and_apply(sp_module, mock_args(enable=True, use_rest=True), args)['changed'] + + +def test_error_dhcp_for_address_type_ipv6(): + ''' dhcp cannot be disabled if manual interface options not set''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']) + ]) + error = 'Error: dhcp cannot be set for address_type: ipv6' + args = {'address_type': 'ipv6', 'dhcp': 'v4'} + assert error in create_module(sp_module, mock_args(use_rest=True), args, fail=True)['msg'] + + +def test_error_dhcp_enable_and_set_manual_options_rest(): + ''' dhcp enable and manual interface options set together''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']) + ]) + error = "Error: set dhcp v4 or all of 'ip_address, gateway_ip_address, netmask'." + args = {'dhcp': 'v4'} + assert error in create_module(sp_module, mock_args(use_rest=True, enable=True), args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snaplock_clock.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snaplock_clock.py new file mode 100644 index 000000000..6177f2a29 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snaplock_clock.py @@ -0,0 +1,228 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP fpolicy ext engine Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snaplock_clock \ + import NetAppOntapSnaplockClock as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'snaplock_clock_set': + xml = self.build_snaplock_clock_info_set() + elif self.type == 'snaplock_clock_not_set': + xml = self.build_snaplock_clock_info_not_set() + elif self.type == 'snaplock_clock_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_snaplock_clock_info_set(): + ''' build xml data for snaplock-get-node-compliance-clock ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'snaplock-node-compliance-clock': { + 'compliance-clock-info': { + 'formatted-snaplock-compliance-clock': 'Tue Mar 23 09:56:07 EDT 2021 -04:00' + } + } + } + xml.translate_struct(data) + return xml + + @staticmethod + def build_snaplock_clock_info_not_set(): + ''' build xml data for snaplock-get-node-compliance-clock ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'snaplock-node-compliance-clock': { + 'compliance-clock-info': { + 'formatted-snaplock-compliance-clock': 'ComplianceClock is not configured.' + } + } + } + xml.translate_struct(data) + return xml + + +def default_args(): + args = { + 'node': 'node1', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'snaplock_clock_set_record': (200, { + "records": [{ + 'node': 'node1', + 'time': 'Tue Mar 23 09:56:07 EDT 2021 -04:00' + }], + 'num_records': 1 + }, None), + 'snaplock_clock_not_set_record': (200, { + "records": [{ + 'node': 'node1', + 'time': 'ComplianceClock is not configured.' + }], + 'num_records': 1 + }, None) + +} + + +def get_snaplock_clock_mock_object(cx_type='zapi', kind=None): + snaplock_clock_obj = my_module() + if cx_type == 'zapi': + if kind is None: + snaplock_clock_obj.server = MockONTAPConnection() + else: + snaplock_clock_obj.server = MockONTAPConnection(kind=kind) + return snaplock_clock_obj + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_ensure_get_called(patch_ansible): + ''' test get_snaplock_clock for non initialized clock''' + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + print('starting') + my_obj = my_module() + print('use_rest:', my_obj.use_rest) + my_obj.server = MockONTAPConnection(kind='snaplock_clock_not_set') + assert my_obj.get_snaplock_node_compliance_clock is not None + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' test for missing arguments ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snaplock_clock.NetAppOntapSnaplockClock.set_snaplock_node_compliance_clock') +def test_successful_initialize(self, patch_ansible): + ''' Initializing snaplock_clock and test idempotency ''' + args = dict(default_args()) + args['use_rest'] = 'never' + args['feature_flags'] = {'no_cserver_ems': True} + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection(kind='snaplock_clock_not_set') + with patch.object(my_module, 'set_snaplock_node_compliance_clock', wraps=my_obj.set_snaplock_node_compliance_clock) as mock_create: + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert exc.value.args[0]['changed'] + mock_create.assert_called_with() + # test idempotency + args = dict(default_args()) + args['use_rest'] = 'never' + args['feature_flags'] = {'no_cserver_ems': True} + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('snaplock_clock_set') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Create: ' + repr(exc.value)) + assert not exc.value.args[0]['changed'] + + +def test_if_all_methods_catch_exception(patch_ansible): + args = dict(default_args()) + args['use_rest'] = 'never' + set_module_args(args) + my_obj = my_module() + my_obj.server = MockONTAPConnection('snaplock_clock_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.set_snaplock_node_compliance_clock() + assert 'Error setting snaplock compliance clock for node ' in exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_initialize(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Initialize snaplock clock ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['snaplock_clock_not_set_record'], # get + SRR['empty_good'], # post + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_initialize_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Initialize snaplock clock idempotent ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['snaplock_clock_set_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror.py new file mode 100644 index 000000000..9ba179279 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror.py @@ -0,0 +1,1894 @@ +''' unit tests ONTAP Ansible module: na_ontap_snapmirror ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_no_warnings, assert_warning_was_raised, expect_and_capture_ansible_exception, call_main, create_module, patch_ansible, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapmirror \ + import NetAppONTAPSnapmirror as my_module, main as my_main + +HAS_SF_COMMON = True +try: + from solidfire.common import ApiServerError +except ImportError: + HAS_SF_COMMON = False + +if not HAS_SF_COMMON: + pytestmark = pytest.mark.skip('skipping as missing required solidfire.common') + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +DEFAULT_ARGS = { + "hostname": "10.193.189.206", + "username": "admin", + "password": "netapp123", + "https": "yes", + "validate_certs": "no", + "state": "present", + "initialize": "True", + "relationship_state": "active", + "source_path": "svmsrc3:volsrc1", + "destination_path": "svmdst3:voldst1", + "relationship_type": "extended_data_protection" +} + + +def sm_rest_info(state, healthy, transfer_state=None, destination_path=DEFAULT_ARGS['destination_path']): + record = { + 'uuid': 'b5ee4571-5429-11ec-9779-005056b39a06', + 'destination': { + 'path': destination_path + }, + 'policy': { + 'name': 'MirrorAndVault' + }, + 'state': state, + 'healthy': healthy, + } + if transfer_state: + record['transfer'] = {'state': transfer_state} + if transfer_state == 'transferring': + record['transfer']['uuid'] = 'xfer_uuid' + if healthy is False: + record['unhealthy_reason'] = 'this is why the relationship is not healthy.' + record['transfer_schedule'] = {'name': 'abc'} + + return { + 'records': [record], + 'num_records': 1 + } + + +sm_policies = { + # We query only on the policy name, as it can be at the vserver or cluster scope. + # So we can have ghost records from other SVMs. + 'records': [ + { + 'type': 'sync', + 'svm': {'name': 'other'} + }, + { + 'type': 'async', + 'svm': {'name': 'svmdst3'} + }, + { + 'type': 'svm_invalid', + 'svm': {'name': 'bad_type'} + }, + { + 'type': 'system_invalid', + }, + ], + 'num_records': 4, +} + + +svm_peer_info = { + 'records': [{ + 'peer': { + 'svm': {'name': 'vserver'}, + 'cluster': {'name': 'cluster'}, + } + }] +} + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'sm_get_uninitialized': (200, sm_rest_info('uninitialized', True), None), + 'sm_get_uninitialized_xfering': (200, sm_rest_info('uninitialized', True, 'transferring'), None), + 'sm_get_mirrored': (200, sm_rest_info('snapmirrored', True, 'success'), None), + 'sm_get_restore': (200, sm_rest_info('snapmirrored', True, 'success', destination_path=DEFAULT_ARGS['source_path']), None), + 'sm_get_paused': (200, sm_rest_info('paused', True, 'success'), None), + 'sm_get_broken': (200, sm_rest_info('broken_off', True, 'success'), None), + 'sm_get_data_transferring': (200, sm_rest_info('transferring', True, 'transferring'), None), + 'sm_get_abort': (200, sm_rest_info('sm_get_abort', False, 'failed'), None), + 'sm_get_resync': (200, { + 'uuid': 'b5ee4571-5429-11ec-9779-005056b39a06', + 'description': 'PATCH /api/snapmirror/relationships/1c4467ca-5434-11ec-9779-005056b39a06', + 'state': 'success', + 'message': 'success', + 'code': 0, + }, None), + 'job_status': (201, { + 'job': { + 'uuid': '3a23a60e-542c-11ec-9779-005056b39a06', + '_links': { + 'self': { + 'href': '/api/cluster/jobs/3a23a60e-542c-11ec-9779-005056b39a06' + } + } + } + }, None), + 'sm_policies': (200, sm_policies, None), + 'svm_peer_info': (200, svm_peer_info, None), +}) + + +def sm_info(mirror_state, status, quiesce_status, relationship_type='extended_data_protection', source='ansible:volsrc1'): + + return { + 'num-records': 1, + 'status': quiesce_status, + 'attributes-list': { + 'snapmirror-info': { + 'mirror-state': mirror_state, + 'schedule': None, + 'source-location': source, + 'relationship-status': status, + 'policy': 'ansible_policy', + 'relationship-type': relationship_type, + 'max-transfer-rate': 10000, + 'identity-preserve': 'true', + 'last-transfer-error': 'last_transfer_error', + 'is-healthy': 'true', + 'unhealthy-reason': 'unhealthy_reason', + }, + 'snapmirror-destination-info': { + 'destination-location': 'ansible' + } + } + } + + +# we only test for existence, contents do not matter +volume_info = { + 'num-records': 1, +} + + +vserver_peer_info = { + 'num-records': 1, + 'attributes-list': { + 'vserver-peer-info': { + 'remote-vserver-name': 'svmsrc3', + 'peer-cluster': 'cluster', + } + } +} + + +ZRR = zapi_responses({ + 'sm_info': build_zapi_response(sm_info(None, 'idle', 'passed')), + 'sm_info_broken_off': build_zapi_response(sm_info('broken_off', 'idle', 'passed')), + 'sm_info_snapmirrored': build_zapi_response(sm_info('snapmirrored', 'idle', 'passed')), + 'sm_info_snapmirrored_from_element': build_zapi_response(sm_info('snapmirrored', 'idle', 'passed', source='10.10.10.11:/lun/1000')), + 'sm_info_snapmirrored_to_element': build_zapi_response(sm_info('snapmirrored', 'idle', 'passed', source='svmsrc3:volsrc1')), + 'sm_info_snapmirrored_load_sharing': build_zapi_response(sm_info('snapmirrored', 'idle', 'passed', 'load_sharing')), + 'sm_info_snapmirrored_vault': build_zapi_response(sm_info('snapmirrored', 'idle', 'passed', 'vault')), + 'sm_info_snapmirrored_quiesced': build_zapi_response(sm_info('snapmirrored', 'quiesced', 'passed')), + 'sm_info_uninitialized': build_zapi_response(sm_info('uninitialized', 'idle', 'passed')), + 'sm_info_uninitialized_load_sharing': build_zapi_response(sm_info('uninitialized', 'idle', 'passed', 'load_sharing')), + 'volume_info': build_zapi_response(volume_info), + 'vserver_peer_info': build_zapi_response(vserver_peer_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + msg = "missing required arguments: hostname" + assert create_module(my_module, {}, fail=True)['msg'] == msg + + +def test_module_fail_unsuuported_rest_options(): + ''' required arguments are reported as errors ''' + module_args = { + "use_rest": "never", + "create_destination": {"enabled": True}, + } + errors = [ + 'Error: using any of', + 'create_destination', + 'requires ONTAP 9.7 or later and REST must be enabled - using ZAPI.' + ] + for error in errors: + assert error in create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +if netapp_utils.has_netapp_lib(): + zapi_create_responses = [ + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), # ONTAP to ONTAP + ('ZAPI', 'snapmirror-create', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-initialize', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check status + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ] +else: + zapi_create_responses = [] + + +def test_negative_zapi_unsupported_options(): + ''' ZAPI unsupported options ''' + register_responses([ + ]) + module_args = { + "use_rest": "never", + "identity_preservation": "full" + } + msg = "Error: The option identity_preservation is supported only with REST." + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +@patch('time.sleep') +def test_successful_create_with_source(dont_sleep): + ''' creating snapmirror and testing idempotency ''' + # earlier versions of pythons don't support *zapi_create_responses + responses = list(zapi_create_responses) + responses.extend([ + # idempotency + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # ONTAP to ONTAP + ('ZAPI', 'vserver-peer-get-iter', ZRR['vserver_peer_info']), # validate source svm + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # ONTAP to ONTAP, check for update + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + register_responses(responses) + module_args = { + "use_rest": "never", + "source_hostname": "10.10.10.10", + "schedule": "abc", + "identity_preserve": True, + "relationship_type": "data_protection", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args.pop('schedule') + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_successful_create_with_peer(dont_sleep): + ''' creating snapmirror and testing idempotency ''' + register_responses(zapi_create_responses) + module_args = { + "use_rest": "never", + "peer_options": {"hostname": "10.10.10.10"}, + "schedule": "abc", + "identity_preserve": True, + "relationship_type": "data_protection", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_negative_break(dont_sleep): + ''' breaking snapmirror to test quiesce time-delay failure ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'vserver-peer-get-iter', ZRR['vserver_peer_info']), # validate source svm + ('ZAPI', 'snapmirror-quiesce', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # 5 retries + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ]) + module_args = { + "use_rest": "never", + "source_hostname": "10.10.10.10", + "relationship_state": "broken", + "relationship_type": "data_protection", + } + msg = "Taking a long time to quiesce SnapMirror relationship, try again later" + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +@patch('time.sleep') +def test_successful_break(dont_sleep): + ''' breaking snapmirror and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'vserver-peer-get-iter', ZRR['vserver_peer_info']), # validate source svm + ('ZAPI', 'snapmirror-quiesce', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_quiesced']), + ('ZAPI', 'snapmirror-break', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # idempotency + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_broken_off']), + ('ZAPI', 'vserver-peer-get-iter', ZRR['vserver_peer_info']), # validate source svm + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "source_hostname": "10.10.10.10", + "relationship_state": "broken", + "relationship_type": "data_protection", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_create_without_initialize(): + ''' creating snapmirror and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), # ONTAP to ONTAP + ('ZAPI', 'snapmirror-create', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # idempotency + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # ONTAP to ONTAP + ('ZAPI', 'vserver-peer-get-iter', ZRR['vserver_peer_info']), # validate source svm + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # ONTAP to ONTAP, check for update + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "source_hostname": "10.10.10.10", + "schedule": "abc", + "relationship_type": "data_protection", + "initialize": False, + "policy": 'ansible_policy', + "max_transfer_rate": 10000, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args.pop('schedule') + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_set_source_peer(): + module_args = { + 'connection_type': 'ontap_elementsw' + } + error = 'Error: peer_options are required to identify ONTAP cluster with connection_type: ontap_elementsw' + assert error in create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args = { + 'connection_type': 'elementsw_ontap' + } + error = 'Error: peer_options are required to identify SolidFire cluster with connection_type: elementsw_ontap' + assert error in create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.create_sf_connection') +def test_set_element_connection(mock_create_sf_cx): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'peer_options': {'hostname': 'any'} + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + elementsw_helper, elem = my_obj.set_element_connection('source') + assert elementsw_helper is not None + assert elem is not None + elementsw_helper, elem = my_obj.set_element_connection('destination') + assert elementsw_helper is not None + assert elem is not None + + +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapmirror.NetAppONTAPSnapmirror.set_element_connection') +def test_successful_element_ontap_create(connection, dont_sleep): + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), # element to ONTAP + ('ZAPI', 'snapmirror-create', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-initialize', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check status + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # idempotency + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_from_element']), # element to ONTAP + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # element to ONTAP, check for update + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + mock_elem, mock_helper = Mock(), Mock() + connection.return_value = mock_helper, mock_elem + mock_elem.get_cluster_info.return_value.cluster_info.svip = '10.10.10.11' + module_args = { + "use_rest": "never", + "source_hostname": "10.10.10.10", + "connection_type": "elementsw_ontap", + "schedule": "abc", + "source_path": "10.10.10.11:/lun/1000", + "relationship_type": "data_protection", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args.pop('schedule') + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapmirror.NetAppONTAPSnapmirror.set_element_connection') +def test_successful_ontap_element_create(connection, dont_sleep): + ''' check elementsw parameters for source ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # an existing relationship is required element to ONTAP + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), # ONTAP to element + ('ZAPI', 'snapmirror-create', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-initialize', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check status + # idempotency + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # an existing relationship is required element to ONTAP + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_to_element']), # ONTAP to element + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # ONTAP to element, check for update + ]) + mock_elem, mock_helper = Mock(), Mock() + connection.return_value = mock_helper, mock_elem + mock_elem.get_cluster_info.return_value.cluster_info.svip = '10.10.10.11' + module_args = { + "use_rest": "never", + "source_hostname": "10.10.10.10", + "connection_type": "ontap_elementsw", + "schedule": "abc", + "destination_path": "10.10.10.11:/lun/1000", + "relationship_type": "data_protection", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args.pop('schedule') + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_successful_delete(dont_sleep): + ''' deleting snapmirror and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'vserver-peer-get-iter', ZRR['vserver_peer_info']), # validate source svm + ('ZAPI', 'snapmirror-quiesce', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_quiesced']), + ('ZAPI', 'snapmirror-break', ZRR['success']), + ('ZAPI', 'snapmirror-get-destination-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-release', ZRR['success']), + ('ZAPI', 'snapmirror-destroy', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), # check health + # idempotency + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), # check health + ]) + module_args = { + "use_rest": "never", + "state": "absent", + "source_hostname": "10.10.10.10", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_successful_delete_without_source_hostname_check(dont_sleep): + ''' source cluster hostname is optional when source is unknown''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'vserver-peer-get-iter', ZRR['vserver_peer_info']), # validate source svm + ('ZAPI', 'snapmirror-quiesce', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_quiesced']), + ('ZAPI', 'snapmirror-break', ZRR['success']), + ('ZAPI', 'snapmirror-destroy', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), # check health + ]) + module_args = { + "use_rest": "never", + "state": "absent", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_successful_delete_with_error_on_break(dont_sleep): + ''' source cluster hostname is optional when source is unknown''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-quiesce', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_quiesced']), + ('ZAPI', 'snapmirror-break', ZRR['error']), + ('ZAPI', 'snapmirror-destroy', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "state": "absent", + "validate_source_path": False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised('Ignored error(s): Error breaking SnapMirror relationship: NetApp API failed. Reason - 12345:synthetic error for UT purpose') + + +@patch('time.sleep') +def test_negative_delete_error_with_error_on_break(dont_sleep): + ''' source cluster hostname is optional when source is unknown''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-quiesce', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_quiesced']), + ('ZAPI', 'snapmirror-break', ZRR['error']), + ('ZAPI', 'snapmirror-destroy', ZRR['error']), + ]) + module_args = { + "use_rest": "never", + "state": "absent", + "validate_source_path": False + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Previous error(s): Error breaking SnapMirror relationship: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + assert 'Error deleting SnapMirror:' in error + + +def test_negative_delete_with_destination_path_missing(): + ''' with misisng destination_path''' + register_responses([ + ]) + args = dict(DEFAULT_ARGS) + args.pop('destination_path') + module_args = { + "use_rest": "never", + "state": "absent", + "source_hostname": "source_host", + } + msg = "Missing parameters: Source path or Destination path" + assert call_main(my_main, args, module_args, fail=True)['msg'] == msg + + +def test_successful_delete_check_get_destination(): + register_responses([ + ('ZAPI', 'snapmirror-get-destination-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-destination-iter', ZRR['no_records']), + ]) + module_args = { + "use_rest": "never", + "state": "absent", + "source_hostname": "source_host", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.set_source_cluster_connection() is None + assert my_obj.get_destination() + assert my_obj.get_destination() is None + + +def test_snapmirror_release(): + register_responses([ + ('ZAPI', 'snapmirror-release', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_volume": "source_volume", + "source_vserver": "source_vserver", + "destination_volume": "destination_volume", + "destination_vserver": "destination_vserver", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.set_source_cluster_connection() is None + assert my_obj.snapmirror_release() is None + + +def test_snapmirror_resume(): + ''' resuming snapmirror ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_quiesced']), + ('ZAPI', 'snapmirror-resume', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # update reads mirror_state + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # idempotency test + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # update reads mirror_state + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "relationship_type": "data_protection", + "validate_source_path": False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_snapmirror_restore(): + ''' restore snapmirror ''' + register_responses([ + ('ZAPI', 'snapmirror-restore', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # idempotency test - TODO + ('ZAPI', 'snapmirror-restore', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "relationship_type": "restore", + "source_snapshot": "source_snapshot", + "clean_up_failure": True, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # TODO: should be idempotent! But we don't read the current state! + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_successful_abort(dont_sleep): + ''' aborting snapmirror and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-quiesce', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_quiesced']), + ('ZAPI', 'snapmirror-break', ZRR['success']), + ('ZAPI', 'snapmirror-destroy', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # idempotency test + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "state": "absent", + "validate_source_path": False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify(): + ''' modifying snapmirror and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-modify', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # update reads mirror_state + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # idempotency test + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # update reads mirror_state + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "relationship_type": "data_protection", + "policy": "ansible2", + "schedule": "abc2", + "max_transfer_rate": 2000, + "validate_source_path": False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args = { + "use_rest": "never", + "relationship_type": "data_protection", + "validate_source_path": False + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_successful_initialize(dont_sleep): + ''' initialize snapmirror and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_uninitialized']), + ('ZAPI', 'snapmirror-initialize', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check status + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # update reads mirror_state + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # 2nd run + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_uninitialized_load_sharing']), + ('ZAPI', 'snapmirror-initialize-ls-set', ZRR['success']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check status + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # update reads mirror_state + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "relationship_type": "data_protection", + "validate_source_path": False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args = { + "use_rest": "never", + "relationship_type": "load_sharing", + "validate_source_path": False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_update(): + ''' update snapmirror and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored']), # update reads mirror_state + ('ZAPI', 'snapmirror-update', ZRR['success']), # update + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + # 2nd run + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_load_sharing']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_load_sharing']), # update reads mirror_state + ('ZAPI', 'snapmirror-update-ls-set', ZRR['success']), # update + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info']), # check health + ]) + module_args = { + "use_rest": "never", + "relationship_type": "data_protection", + "validate_source_path": False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args = { + "use_rest": "never", + "relationship_type": "load_sharing", + "validate_source_path": False + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapmirror.NetAppONTAPSnapmirror.set_element_connection') +def test_elementsw_no_source_path(connection): + ''' elementsw_volume_exists ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['no_records']), + ]) + mock_elem, mock_helper = Mock(), Mock() + connection.return_value = mock_helper, mock_elem + mock_elem.get_cluster_info.return_value.cluster_info.svip = '10.11.12.13' + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_username": "source_user", + "connection_type": "ontap_elementsw", + "destination_path": "10.11.12.13:/lun/1234" + } + error = 'Error: creating an ONTAP to ElementSW snapmirror relationship requires an established SnapMirror relation from ElementSW to ONTAP cluster' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_elementsw_volume_exists(): + ''' elementsw_volume_exists ''' + mock_helper = Mock() + mock_helper.volume_id_exists.side_effect = [1000, None] + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_username": "source_user", + "source_path": "10.10.10.10:/lun/1000", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.check_if_elementsw_volume_exists('10.10.10.10:/lun/1000', mock_helper) is None + expect_and_capture_ansible_exception(my_obj.check_if_elementsw_volume_exists, 'fail', '10.10.10.11:/lun/1000', mock_helper) + mock_helper.volume_id_exists.side_effect = ApiServerError('function_name', {}) + error = 'Error fetching Volume details' + assert error in expect_and_capture_ansible_exception(my_obj.check_if_elementsw_volume_exists, 'fail', '1234', mock_helper)['msg'] + + +def test_elementsw_svip_exists(): + ''' svip_exists ''' + mock_elem = Mock() + mock_elem.get_cluster_info.return_value.cluster_info.svip = '10.10.10.10' + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_username": "source_user", + # "source_password": "source_password", + "source_path": "10.10.10.10:/lun/1000", + # "source_volume": "source_volume", + # "source_vserver": "source_vserver", + # "destination_volume": "destination_volume", + # "destination_vserver": "destination_vserver", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.validate_elementsw_svip('10.10.10.10:/lun/1000', mock_elem) is None + + +def test_elementsw_svip_exists_negative(): + ''' svip_exists negative testing''' + mock_elem = Mock() + mock_elem.get_cluster_info.return_value.cluster_info.svip = '10.10.10.10' + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_username": "source_user", + "source_path": "10.10.10.10:/lun/1000", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + expect_and_capture_ansible_exception(my_obj.validate_elementsw_svip, 'fail', '10.10.10.11:/lun/1000', mock_elem) + mock_elem.get_cluster_info.side_effect = ApiServerError('function_name', {}) + error = 'Error fetching SVIP' + assert error in expect_and_capture_ansible_exception(my_obj.validate_elementsw_svip, 'fail', 'svip', mock_elem)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapmirror.NetAppONTAPSnapmirror.set_element_connection') +def test_check_elementsw_params_source(connection): + ''' check elementsw parameters for source ''' + mock_elem, mock_helper = Mock(), Mock() + connection.return_value = mock_helper, mock_elem + mock_elem.get_cluster_info.return_value.cluster_info.svip = '10.10.10.10' + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_username": "source_user", + "source_path": "10.10.10.10:/lun/1000", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.check_elementsw_parameters('source') is None + + +def test_check_elementsw_params_negative(): + ''' check elementsw parameters for source negative testing ''' + args = dict(DEFAULT_ARGS) + del args['source_path'] + module_args = { + "use_rest": "never", + } + msg = 'Error: Missing required parameter source_path' + my_obj = create_module(my_module, args, module_args) + assert msg in expect_and_capture_ansible_exception(my_obj.check_elementsw_parameters, 'fail', 'source')['msg'] + + +def test_check_elementsw_params_invalid(): + ''' check elementsw parameters for source invalid testing ''' + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_volume": "source_volume", + "source_vserver": "source_vserver", + "destination_volume": "destination_volume", + "destination_vserver": "destination_vserver", + } + msg = 'Error: invalid source_path' + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert msg in expect_and_capture_ansible_exception(my_obj.check_elementsw_parameters, 'fail', 'source')['msg'] + + +def test_elementsw_source_path_format(): + ''' test element_source_path_format_matches ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['volume_info']), + ]) + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_volume": "source_volume", + "source_vserver": "source_vserver", + "destination_volume": "destination_volume", + "destination_vserver": "destination_vserver", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.check_if_remote_volume_exists() + assert my_obj.element_source_path_format_matches('1.1.1.1:dummy') is None + assert my_obj.element_source_path_format_matches('10.10.10.10:/lun/10') is not None + + +def test_remote_volume_exists(): + ''' test check_if_remote_volume_exists ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['volume_info']), + ]) + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_volume": "source_volume", + "source_vserver": "source_vserver", + "destination_volume": "destination_volume", + "destination_vserver": "destination_vserver", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.check_if_remote_volume_exists() + + +@patch('time.sleep') +def test_if_all_methods_catch_exception(dont_sleep): + module_args = { + "use_rest": "never", + "source_hostname": "source_host", + "source_volume": "source_volume", + "source_vserver": "source_vserver", + "destination_volume": "destination_volume", + "destination_vserver": "destination_vserver", + } + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_quiesced']), + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.source_server = my_obj.server # for get_destination + tests = [ + (my_obj.check_if_remote_volume_exists, [('volume-get-iter', 'error')], 'Error fetching source volume details source_volume:'), + (my_obj.get_destination, [('snapmirror-get-destination-iter', 'error')], 'Error fetching snapmirror destinations info:'), + (my_obj.get_svm_peer, [('vserver-peer-get-iter', 'error')], 'Error fetching vserver peer info:'), + (my_obj.snapmirror_abort, [('snapmirror-abort', 'error')], 'Error aborting SnapMirror relationship:'), + (my_obj.snapmirror_break, [('snapmirror-quiesce', 'success'), ('snapmirror-get-iter', 'sm_info_snapmirrored_quiesced'), ('snapmirror-break', 'error')], + 'Error breaking SnapMirror relationship:'), + (my_obj.snapmirror_create, [('volume-get-iter', 'success')], 'Source volume does not exist. Please specify a volume that exists'), + (my_obj.snapmirror_create, [('volume-get-iter', 'volume_info'), ('snapmirror-create', 'error')], 'Error creating SnapMirror'), + (my_obj.snapmirror_delete, [('snapmirror-destroy', 'error')], 'Error deleting SnapMirror:'), + (my_obj.snapmirror_get, [('snapmirror-get-iter', 'error')], 'Error fetching snapmirror info:'), + (my_obj.snapmirror_initialize, [('snapmirror-get-iter', 'sm_info'), ('snapmirror-initialize', 'error')], 'Error initializing SnapMirror:'), + (my_obj.snapmirror_modify, [('snapmirror-modify', 'error')], 'Error modifying SnapMirror schedule or policy:'), + (my_obj.snapmirror_quiesce, [('snapmirror-quiesce', 'error')], 'Error quiescing SnapMirror:'), + (my_obj.snapmirror_release, [('snapmirror-release', 'error')], 'Error releasing SnapMirror relationship:'), + (my_obj.snapmirror_resume, [('snapmirror-resume', 'error')], 'Error resuming SnapMirror relationship:'), + (my_obj.snapmirror_restore, [('snapmirror-restore', 'error')], 'Error restoring SnapMirror relationship:'), + (my_obj.snapmirror_resync, [('snapmirror-resync', 'error')], 'Error resyncing SnapMirror relationship:'), + (my_obj.snapmirror_update, [('snapmirror-update', 'error')], 'Error updating SnapMirror:'), + ] + for (function, zapis, error) in tests: + calls = [('ZAPI', zapi[0], ZRR[zapi[1]]) for zapi in zapis] + register_responses(calls) + if function in (my_obj.get_svm_peer,): + assert error in expect_and_capture_ansible_exception(function, 'fail', 's_svm', 'd_svm')['msg'] + elif function in (my_obj.snapmirror_update, my_obj.snapmirror_modify): + assert error in expect_and_capture_ansible_exception(function, 'fail', {})['msg'] + else: + assert error in expect_and_capture_ansible_exception(function, 'fail')['msg'] + + +@patch('time.sleep') +def test_successful_rest_create(dont_sleep): + ''' creating snapmirror and testing idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), + ('POST', 'snapmirror/relationships', SRR['success']), + ('GET', 'snapmirror/relationships', SRR['sm_get_uninitialized']), + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check initialized + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check health + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check health + ]) + module_args = { + "use_rest": "always", + "schedule": "abc", + "identity_preservation": "full" + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['update'] = False + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_rest_create(): + ''' creating snapmirror with unsupported REST options ''' + module_args = { + "use_rest": "always", + "identity_preserve": True, + "schedule": "abc", + "relationship_type": "data_protection", + } + msg = "REST API currently does not support 'identity_preserve, relationship_type: data_protection'" + assert create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_negative_rest_create_schedule_not_supported(): + ''' creating snapmirror with unsupported REST options ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + "use_rest": "always", + "schedule": "abc", + } + msg = "Error: Minimum version of ONTAP for schedule is (9, 11, 1). Current version: (9, 8, 0)."\ + " - With REST use the policy option to define a schedule." + assert create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_negative_rest_create_identity_preservation_not_supported(): + ''' creating snapmirror with unsupported REST options ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + "use_rest": "always", + "identity_preservation": "full", + } + msg = "Error: Minimum version of ONTAP for identity_preservation is (9, 11, 1). Current version: (9, 8, 0)." + error = create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error == msg + + +def test_negative_rest_get_error(): + ''' creating snapmirror with API error ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + } + msg = "Error getting SnapMirror svmdst3:voldst1: calling: snapmirror/relationships: got Expected error." + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_negative_rest_create_error(): + ''' creating snapmirror with API error ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), + ('POST', 'snapmirror/relationships', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + } + msg = "Error creating SnapMirror: calling: snapmirror/relationships: got Expected error." + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +@patch('time.sleep') +def test_rest_snapmirror_initialize(dont_sleep): + ''' snapmirror initialize testing ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_uninitialized_xfering']), + ('GET', 'snapmirror/relationships', SRR['sm_get_uninitialized_xfering']), + ('GET', 'snapmirror/relationships', SRR['sm_get_uninitialized']), + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # Inside SM init patch response + ('GET', 'snapmirror/relationships', SRR['sm_get_data_transferring']), # get to check status after initialize + ('GET', 'snapmirror/relationships', SRR['sm_get_data_transferring']), # get to check status after initialize + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # get to check status after initialize + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check for update + ('POST', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06/transfers', SRR['success']), # update + ('GET', 'snapmirror/relationships', SRR['sm_get_uninitialized']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_snapmirror_update(): + ''' snapmirror initialize testing ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # first sm_get + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply update calls again sm_get + ('POST', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06/transfers', SRR['success']), # sm update + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_sm_break_success_no_data_transfer(dont_sleep): + ''' testing snapmirror break when no_data are transferring ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply first sm_get with no data transfer + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # SM quiesce response to pause + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # sm quiesce api fn calls again sm_get + # sm quiesce validate the state which calls sm_get + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + # sm quiesce validate the state which calls sm_get after wait + ('GET', 'snapmirror/relationships', SRR['sm_get_paused']), + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm break response + ('GET', 'snapmirror/relationships', SRR['sm_get_paused']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + "relationship_state": "broken", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_sm_break_success_no_data_transfer_idempotency(): + ''' testing snapmirror break when no_data are transferring idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_broken']), # apply first sm_get with no data transfer + ('GET', 'snapmirror/relationships', SRR['sm_get_broken']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + "relationship_state": "broken", + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_sm_break_fails_if_uninit(): + ''' testing snapmirror break fails if sm state uninitialized ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # apply first sm_get with state uninitialized + ('GET', 'snapmirror/relationships', SRR['sm_get_uninitialized']), + ]) + module_args = { + "use_rest": "always", + "relationship_state": "broken", + } + msg = "SnapMirror relationship cannot be broken if mirror state is uninitialized" + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_sm_break_fails_if_load_sharing_or_vault(): + ''' testing snapmirror break fails for load_sharing or vault types ''' + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_load_sharing']), + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored_vault']), + ]) + module_args = { + "use_rest": "never", + "relationship_state": "broken", + "relationship_type": "load_sharing", + "validate_source_path": False + } + msg = "SnapMirror break is not allowed in a load_sharing or vault relationship" + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + module_args['relationship_type'] = 'vault' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +@patch('time.sleep') +def test_rest_snapmirror_quiesce_fail_when_state_not_paused(dont_sleep): + ''' testing snapmirror break when no_data are transferring ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply first sm_get with no data transfer + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # SM quiesce response + # SM quiesce validate the state which calls sm_get after wait + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # first fail + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # second fail + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # third fail + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # fourth fail + ]) + module_args = { + "use_rest": "always", + "relationship_state": "broken", + "validate_source_path": False + } + msg = "Taking a long time to quiesce SnapMirror relationship, try again later" + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_snapmirror_break_fails_if_data_is_transferring(): + ''' testing snapmirror break when no_data are transferring ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # apply first sm_get with data transfer + ('GET', 'snapmirror/relationships', SRR['sm_get_data_transferring']), + ]) + module_args = { + "use_rest": "always", + "relationship_state": "broken", + } + msg = "snapmirror data are transferring" + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +@patch('time.sleep') +def test_rest_resync_when_state_is_broken(dont_sleep): + ''' resync when snapmirror state is broken and relationship_state active ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_broken']), # apply first sm_get with state broken_off + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm resync response + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check for idle + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_resume_when_state_quiesced(): + ''' resync when snapmirror state is broken and relationship_state active ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_paused']), # apply first sm_get with state quiesced + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm resync response + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # sm update calls sm_get + ('POST', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06/transfers', SRR['success']), # sm update response + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_snapmirror_delete(dont_sleep): + ''' snapmirror delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply first sm_get with no data transfer + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm quiesce response + # sm quiesce validate the state which calls sm_get + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + # sm quiesce validate the state which calls sm_get after wait with 0 iter + ('GET', 'snapmirror/relationships', SRR['sm_get_paused']), + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm break response + ('DELETE', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm delete response + ('GET', 'snapmirror/relationships', SRR['zero_records']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + "state": "absent", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_snapmirror_delete_with_error_on_break(dont_sleep): + ''' snapmirror delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply first sm_get with no data transfer + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm quiesce response + # sm quiesce validate the state which calls sm_get + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + # sm quiesce validate the state which calls sm_get after wait with 0 iter + ('GET', 'snapmirror/relationships', SRR['sm_get_paused']), + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['generic_error']), # sm break response + ('DELETE', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm delete response + ('GET', 'snapmirror/relationships', SRR['zero_records']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + "state": "absent", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised("Ignored error(s): Error patching SnapMirror: {'state': 'broken_off'}: " + "calling: snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06: got Expected error.") + + +@patch('time.sleep') +def test_rest_snapmirror_delete_with_error_on_break_and_delete(dont_sleep): + ''' snapmirror delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply first sm_get with no data transfer + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm quiesce response + # sm quiesce validate the state which calls sm_get + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + # sm quiesce validate the state which calls sm_get after wait with 0 iter + ('GET', 'snapmirror/relationships', SRR['sm_get_paused']), + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['generic_error']), # sm break response + ('DELETE', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['generic_error']), # sm delete response + ]) + module_args = { + "use_rest": "always", + "state": "absent", + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print_warnings() + assert "Previous error(s): Error patching SnapMirror: {'state': 'broken_off'}" in error + assert "Error deleting SnapMirror: calling: snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06: got Expected error" in error + + +@patch('time.sleep') +def test_rest_snapmirror_delete_calls_abort(dont_sleep): + ''' snapmirror delete calls abort when transfer state is in transferring''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # apply first sm_get with data transfer + ('GET', 'snapmirror/relationships', SRR['sm_get_data_transferring']), + # abort + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06/transfers/xfer_uuid', SRR['empty_good']), + ('GET', 'snapmirror/relationships', SRR['sm_get_abort']), # wait_for_status calls again sm_get + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm quiesce response + # sm quiesce validate the state which calls sm_get + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + # sm quiesce validate the state which calls sm_get after wait with 0 iter + ('GET', 'snapmirror/relationships', SRR['sm_get_paused']), + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm break response + ('DELETE', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm delete response + ('GET', 'snapmirror/relationships', SRR['zero_records']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + "state": "absent", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_snapmirror_modify(): + ''' snapmirror modify''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply first sm_get + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm modify response + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # sm update calls sm_get to check mirror state + ('POST', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06/transfers', SRR['success']), # sm update response + ('GET', 'snapmirror/relationships', SRR['zero_records']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + "policy": "Asynchronous", + "schedule": "abcdef", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_snapmirror_modify_warning(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply first sm_get + ('PATCH', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06', SRR['success']), # sm modify response + ]) + module_args = { + "use_rest": "always", + "policy": "Asynchronous", + "schedule": "abcdef", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.snapmirror_mod_init_resync_break_quiesce_resume_rest(modify=module_args) is None + print_warnings() + assert_warning_was_raised('Unexpected key in modify: use_rest, value: always') + + +def test_rest_snapmirror_restore(): + ''' snapmirror restore ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # apply first sm_get + ('POST', 'snapmirror/relationships', SRR['success']), # first post response + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # After first post call to get relationship uuid + ('POST', 'snapmirror/relationships/b5ee4571-5429-11ec-9779-005056b39a06/transfers', SRR['success']), # second post response + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + "relationship_type": "restore", + "source_snapshot": "source_snapshot", + "clean_up_failure": False, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_snapmirror_create_and_initialize_not_found(): + ''' snapmirror restore ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), # apply first sm_get + ('GET', 'snapmirror/policies', SRR['zero_records']), # policy not found + ]) + module_args = { + "use_rest": "always", + "create_destination": {"enabled": True}, + "policy": "sm_policy" + } + error = 'Error: cannot find policy sm_policy for vserver svmdst3' + assert error == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_error_snapmirror_create_and_initialize_bad_type(): + ''' snapmirror restore ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), # apply first sm_get + ('GET', 'snapmirror/policies', SRR['sm_policies']), # policy + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), # apply first sm_get + ('GET', 'snapmirror/policies', SRR['sm_policies']), # policy + ]) + module_args = { + "use_rest": "always", + "create_destination": {"enabled": True}, + "policy": "sm_policy", + "destination_vserver": "bad_type", + "source_vserver": "any" + } + error = 'Error: unexpected type: svm_invalid for policy sm_policy for vserver bad_type' + assert error == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['destination_vserver'] = 'cluster_scope_only' + error = 'Error: unexpected type: system_invalid for policy sm_policy for vserver cluster_scope_only' + assert error == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_errors(): + ''' generic REST errors ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # set_initialization_state + ('GET', 'snapmirror/policies', SRR['generic_error']), + # snapmirror_restore_rest + ('POST', 'snapmirror/relationships', SRR['generic_error']), + # snapmirror_restore_rest + ('POST', 'snapmirror/relationships', SRR['success']), + ('POST', 'snapmirror/relationships/1234/transfers', SRR['generic_error']), + # snapmirror_mod_init_resync_break_quiesce_resume_rest + ('PATCH', 'snapmirror/relationships/1234', SRR['generic_error']), + # snapmirror_update_rest + ('POST', 'snapmirror/relationships/1234/transfers', SRR['generic_error']), + # snapmirror_abort_rest + ('PATCH', 'snapmirror/relationships/1234/transfers/5678', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + "policy": "policy" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = rest_error_message("Error fetching SnapMirror policy", 'snapmirror/policies') + assert error in expect_and_capture_ansible_exception(my_obj.set_initialization_state, 'fail')['msg'] + my_obj.parameters['uuid'] = '1234' + my_obj.parameters['transfer_uuid'] = '5678' + error = rest_error_message("Error restoring SnapMirror", 'snapmirror/relationships') + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_restore_rest, 'fail')['msg'] + error = rest_error_message("Error restoring SnapMirror Transfer", 'snapmirror/relationships/1234/transfers') + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_restore_rest, 'fail')['msg'] + my_obj.na_helper.changed = True + assert my_obj.snapmirror_mod_init_resync_break_quiesce_resume_rest() is None + assert not my_obj.na_helper.changed + error = rest_error_message("Error patching SnapMirror: {'state': 'broken_off'}", 'snapmirror/relationships/1234') + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_mod_init_resync_break_quiesce_resume_rest, 'fail', 'broken_off')['msg'] + error = rest_error_message('Error updating SnapMirror relationship', 'snapmirror/relationships/1234/transfers') + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_update_rest, 'fail')['msg'] + error = rest_error_message('Error aborting SnapMirror', 'snapmirror/relationships/1234/transfers/5678') + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_abort_rest, 'fail')['msg'] + + +def test_rest_error_no_uuid(): + ''' snapmirror restore ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + # snapmirror_restore_rest + ('POST', 'snapmirror/relationships', SRR['success']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), + # snapmirror_mod_init_resync_break_quiesce_resume_rest + ('GET', 'snapmirror/relationships', SRR['zero_records']), + # snapmirror_update_rest + ('GET', 'snapmirror/relationships', SRR['zero_records']), + # others, no call + ]) + module_args = { + "use_rest": "always", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = 'Error restoring SnapMirror: unable to get UUID for the SnapMirror relationship.' + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_restore_rest, 'fail')['msg'] + error = 'Error in updating SnapMirror relationship: unable to get UUID for the SnapMirror relationship.' + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_mod_init_resync_break_quiesce_resume_rest, 'fail')['msg'] + error = 'Error in updating SnapMirror relationship: unable to get UUID for the SnapMirror relationship.' + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_update_rest, 'fail')['msg'] + error = 'Error in aborting SnapMirror: unable to get either uuid: None or transfer_uuid: None.' + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_abort_rest, 'fail')['msg'] + error = 'Error in deleting SnapMirror: None, unable to get UUID for the SnapMirror relationship.' + assert error in expect_and_capture_ansible_exception(my_obj.snapmirror_delete_rest, 'fail')['msg'] + + +@patch('time.sleep') +def test_rest_snapmirror_create_and_initialize(dont_sleep): + ''' snapmirror restore ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), # apply first sm_get + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'storage/volumes', SRR['one_record']), + ('GET', 'snapmirror/policies', SRR['sm_policies']), # policy + ('POST', 'snapmirror/relationships', SRR['success']), # first post response + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check status + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), # check_health calls sm_get + ]) + module_args = { + "use_rest": "always", + "create_destination": {"enabled": True}, + "policy": "sm_policy", + # force a call to check_if_remote_volume_exists + "peer_options": {"hostname": "10.10.10.10"}, + "source_volume": "source_volume", + "source_vserver": "source_vserver", + "destination_volume": "destination_volume", + "destination_vserver": "svmdst3" + + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_set_new_style(): + # validate the old options are set properly using new endpoints + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + args = dict(DEFAULT_ARGS) + args.pop('source_path') + args.pop('destination_path') + module_args = { + "use_rest": "always", + "source_endpoint": { + "cluster": "source_cluster", + "consistency_group_volumes": "source_consistency_group_volumes", + "path": "source_path", + "svm": "source_svm", + }, + "destination_endpoint": { + "cluster": "destination_cluster", + "consistency_group_volumes": "destination_consistency_group_volumes", + "path": "destination_path", + "svm": "destination_svm", + }, + } + my_obj = create_module(my_module, args, module_args) + assert my_obj.set_new_style() is None + assert my_obj.new_style + assert my_obj.parameters['destination_vserver'] == 'destination_svm' + assert my_obj.set_initialization_state() == 'in_sync' + + +def test_negative_set_new_style(): + # validate the old options are set properly using new endpoints + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster', SRR['is_rest_97']), + ]) + args = dict(DEFAULT_ARGS) + args.pop('source_path') + args.pop('destination_path') + module_args = { + "use_rest": "always", + "source_endpoint": { + "cluster": "source_cluster", + "consistency_group_volumes": "source_consistency_group_volumes", + "path": "source_path", + "svm": "source_svm", + }, + "destination_endpoint": { + "cluster": "destination_cluster", + "consistency_group_volumes": "destination_consistency_group_volumes", + "path": "destination_path", + "svm": "destination_svm", + }, + } + # errors on source_endpoint + my_obj = create_module(my_module, args, module_args) + error = expect_and_capture_ansible_exception(my_obj.set_new_style, 'fail')['msg'] + assert "Error: using any of ['cluster', 'ipspace'] requires ONTAP 9.7 or later and REST must be enabled" in error + assert "ONTAP version: 9.6.0 - using REST" in error + my_obj = create_module(my_module, args, module_args) + error = expect_and_capture_ansible_exception(my_obj.set_new_style, 'fail')['msg'] + assert "Error: using consistency_group_volumes requires ONTAP 9.8 or later and REST must be enabled" in error + assert "ONTAP version: 9.7.0 - using REST" in error + # errors on destination_endpoint + module_args['source_endpoint'].pop('cluster') + my_obj = create_module(my_module, args, module_args) + error = expect_and_capture_ansible_exception(my_obj.set_new_style, 'fail')['msg'] + assert "Error: using any of ['cluster', 'ipspace'] requires ONTAP 9.7 or later and REST must be enabled" in error + assert "ONTAP version: 9.6.0 - using REST" in error + module_args['source_endpoint'].pop('consistency_group_volumes') + my_obj = create_module(my_module, args, module_args) + error = expect_and_capture_ansible_exception(my_obj.set_new_style, 'fail')['msg'] + assert "Error: using consistency_group_volumes requires ONTAP 9.8 or later and REST must be enabled" in error + assert "ONTAP version: 9.7.0 - using REST" in error + module_args.pop('source_endpoint') + module_args.pop('destination_endpoint') + my_obj = create_module(my_module, args, module_args) + error = expect_and_capture_ansible_exception(my_obj.set_new_style, 'fail')['msg'] + assert error == 'Missing parameters: Source endpoint or Destination endpoint' + + +def test_check_parameters_new_style(): + # validate the old options are set properly using new endpoints + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + args = dict(DEFAULT_ARGS) + args.pop('source_path') + args.pop('destination_path') + module_args = { + "use_rest": "always", + "source_endpoint": { + "cluster": "source_cluster", + "consistency_group_volumes": "source_consistency_group_volumes", + "path": "source_path", + "svm": "source_svm", + }, + "destination_endpoint": { + "cluster": "destination_cluster", + "consistency_group_volumes": "destination_consistency_group_volumes", + "path": "destination_path", + "svm": "destination_svm", + }, + } + my_obj = create_module(my_module, args, module_args) + assert my_obj.check_parameters() is None + assert my_obj.new_style + assert my_obj.parameters['destination_vserver'] == 'destination_svm' + + +def test_negative_check_parameters_new_style(): + # validate version checks + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_97']), + ]) + args = dict(DEFAULT_ARGS) + args.pop('source_path') + args.pop('destination_path') + module_args = { + "use_rest": "always", + "source_endpoint": { + "cluster": "source_cluster", + "consistency_group_volumes": "source_consistency_group_volumes", + "path": "source_path", + "svm": "source_svm", + }, + "destination_endpoint": { + "cluster": "destination_cluster", + "consistency_group_volumes": "destination_consistency_group_volumes", + "path": "destination_path", + "svm": "destination_svm", + }, + "create_destination": {"enabled": True} + } + # errors on source_endpoint + error = 'Minimum version of ONTAP for create_destination is (9, 7).' + assert error in create_module(my_module, args, module_args, fail=True)['msg'] + my_obj = create_module(my_module, args, module_args) + error = expect_and_capture_ansible_exception(my_obj.check_parameters, 'fail')['msg'] + assert "Error: using consistency_group_volumes requires ONTAP 9.8 or later and REST must be enabled" in error + assert "ONTAP version: 9.7.0 - using REST" in error + module_args['destination_endpoint'].pop('path') + error = create_module(my_module, args, module_args, fail=True)['msg'] + assert "missing required arguments: path found in destination_endpoint" in error + + +def test_check_parameters_old_style(): + # validate the old options are set properly using new endpoints + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_96']), + ]) + # using paths + module_args = { + "use_rest": "always", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.check_parameters() is None + assert not my_obj.new_style + # using volume and vserver, paths are constructed + args = dict(DEFAULT_ARGS) + args.pop('source_path') + args.pop('destination_path') + module_args = { + "use_rest": "always", + "source_volume": "source_vol", + "source_vserver": "source_svm", + "destination_volume": "dest_vol", + "destination_vserver": "dest_svm", + } + my_obj = create_module(my_module, args, module_args) + assert my_obj.check_parameters() is None + assert not my_obj.new_style + assert my_obj.parameters['source_path'] == "source_svm:source_vol" + assert my_obj.parameters['destination_path'] == "dest_svm:dest_vol" + # vserver DR + module_args = { + "use_rest": "always", + "source_vserver": "source_svm", + "destination_vserver": "dest_svm", + } + my_obj = create_module(my_module, args, module_args) + assert my_obj.check_parameters() is None + assert not my_obj.new_style + assert my_obj.parameters['source_path'] == "source_svm:" + assert my_obj.parameters['destination_path'] == "dest_svm:" + body, dummy = my_obj.get_create_body() + assert body["source"] == {"path": "source_svm:"} + module_args = { + "use_rest": "always", + "source_volume": "source_vol", + "source_vserver": "source_svm", + "destination_volume": "dest_vol", + "destination_vserver": "dest_svm", + } + my_obj = create_module(my_module, args, module_args) + my_obj.parameters.pop("source_vserver") + error = 'Missing parameters: source vserver or destination vserver or both' + assert error in expect_and_capture_ansible_exception(my_obj.check_parameters, 'fail')['msg'] + + +def test_validate_source_path(): + # validate source path when vserver local name is different + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peers', SRR['zero_records']), + ('GET', 'svm/peers', SRR['svm_peer_info']), + ('GET', 'svm/peers', SRR['svm_peer_info']), + # error + ('GET', 'svm/peers', SRR['generic_error']), + # warnings + ]) + # using paths + module_args = { + "use_rest": "always", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + current = None + assert my_obj.validate_source_path(current) is None + current = {} + assert my_obj.validate_source_path(current) is None + current = {'source_path': 'svmsrc3:volsrc1'} + assert my_obj.validate_source_path(current) is None + current = {'source_path': 'svmsrc3:volsrc1'} + assert my_obj.validate_source_path(current) is None + current = {'source_path': 'vserver:volume'} + error = 'Error: another relationship is present for the same destination with source_path: "vserver:volume" '\ + '(vserver:volume on cluster cluster). Desired: svmsrc3:volsrc1 on None' + assert error in expect_and_capture_ansible_exception(my_obj.validate_source_path, 'fail', current)['msg'] + current = {'source_path': 'vserver:volume1'} + my_obj.parameters['connection_type'] = 'other' + error = 'Error: another relationship is present for the same destination with source_path: "vserver:volume1".'\ + ' Desired: svmsrc3:volsrc1 on None' + assert error in expect_and_capture_ansible_exception(my_obj.validate_source_path, 'fail', current)['msg'] + my_obj.parameters['connection_type'] = 'ontap_ontap' + current = {'source_path': 'vserver:volume'} + error = rest_error_message('Error retrieving SVM peer', 'svm/peers') + assert error in expect_and_capture_ansible_exception(my_obj.validate_source_path, 'fail', current)['msg'] + current = {'source_path': 'vserver/volume'} + assert my_obj.validate_source_path(current) is None + assert_warning_was_raised('Unexpected source path: vserver/volume, skipping validation.') + my_obj.parameters['destination_endpoint'] = {'path': 'vserver/volume'} + current = {'source_path': 'vserver:volume'} + assert my_obj.validate_source_path(current) is None + assert_warning_was_raised('Unexpected destination path: vserver/volume, skipping validation.') + + +@patch('time.sleep') +def test_wait_for_idle_status(dont_sleep): + # validate wait time and time-out + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), + ('GET', 'snapmirror/relationships', SRR['sm_get_mirrored']), + # time-out + ('GET', 'snapmirror/relationships', SRR['zero_records']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), + ]) + # using paths + module_args = { + "use_rest": "always", + "transferring_time_out": 0, + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.wait_for_idle_status() is None + assert my_obj.wait_for_idle_status() is not None + module_args = { + "use_rest": "always", + "transferring_time_out": 60, + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.wait_for_idle_status() is not None + assert my_obj.wait_for_idle_status() is None + assert_warning_was_raised('SnapMirror relationship is still transferring after 60 seconds.') + + +def test_dp_to_xdp(): + # with ZAPI, DP is transformed to XDP to match ONTAP behavior + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored']), + ]) + # using paths + module_args = { + "use_rest": "never", + "relationship_type": 'data_protection', + "validate_source_path": False + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_actions() is not None + assert my_obj.parameters['relationship_type'] == 'extended_data_protection' + + +def test_cannot_change_rtype(): + # with ZAPI, can't change relationship_type + register_responses([ + ('ZAPI', 'snapmirror-get-iter', ZRR['sm_info_snapmirrored']), + ]) + # using paths + module_args = { + "use_rest": "never", + "relationship_type": 'load_sharing', + "validate_source_path": False + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = 'Error: cannot modify relationship_type from extended_data_protection to load_sharing.' + assert error in expect_and_capture_ansible_exception(my_obj.get_actions, 'fail', )['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + module_args = { + 'use_rest': 'never', + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_check_health(): + # validate source path when vserver local name is different + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'snapmirror/relationships', SRR['zero_records']), + ('GET', 'snapmirror/relationships', SRR['sm_get_abort']), + ]) + module_args = { + "use_rest": "always", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.check_health() is None + assert_no_warnings() + assert my_obj.check_health() is None + assert_warning_was_raised('SnapMirror relationship exists but is not healthy. ' + 'Unhealthy reason: this is why the relationship is not healthy. ' + 'Last transfer error: this is why the relationship is not healthy.') + + +def test_negative_check_if_remote_volume_exists_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/volumes', SRR['zero_records']), + ('GET', 'storage/volumes', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = 'REST is not supported on Source' + assert error in expect_and_capture_ansible_exception(my_obj.check_if_remote_volume_exists_rest, 'fail')['msg'] + my_obj.src_use_rest = True + assert not my_obj.check_if_remote_volume_exists_rest() + my_obj.parameters['peer_options'] = {} + netapp_utils.setup_host_options_from_module_params(my_obj.parameters['peer_options'], my_obj.module, netapp_utils.na_ontap_host_argument_spec_peer().keys()) + my_obj.parameters['source_volume'] = 'volume' + my_obj.parameters['source_vserver'] = 'vserver' + assert my_obj.set_source_cluster_connection() is None + assert not my_obj.check_if_remote_volume_exists_rest() + error = rest_error_message('Error fetching source volume', 'storage/volumes') + assert error in expect_and_capture_ansible_exception(my_obj.check_if_remote_volume_exists_rest, 'fail')['msg'] + + +def test_snapmirror_release_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + module_args = { + "use_rest": "always", + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.snapmirror_release() is None + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_negative_set_source_cluster_connection(mock_netapp_lib): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + module_args = { + "use_rest": "never", + "source_volume": "source_volume", + "source_vserver": "source_vserver", + "destination_volume": "destination_volume", + "destination_vserver": "destination_vserver", + "relationship_type": "vault", + "peer_options": { + "use_rest": "always", + "hostname": "source_host", + } + } + mock_netapp_lib.side_effect = [True, False] + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "REST API currently does not support 'relationship_type: vault'" + assert error in expect_and_capture_ansible_exception(my_obj.set_source_cluster_connection, 'fail')['msg'] + my_obj.parameters['peer_options']['use_rest'] = 'auto' + error = "Error: the python NetApp-Lib module is required. Import error: None" + assert error in expect_and_capture_ansible_exception(my_obj.set_source_cluster_connection, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror_policy.py new file mode 100644 index 000000000..23a1e9c64 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapmirror_policy.py @@ -0,0 +1,1269 @@ +# (c) 2019-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_snapmirror_policy ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_module, patch_ansible, expect_and_capture_ansible_exception, create_and_apply + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapmirror_policy import NetAppOntapSnapMirrorPolicy as my_module, main as my_main + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'success': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_snapmirror_policy_async': (200, { + 'svm': {'name': 'ansible'}, + 'name': 'ansible', + 'uuid': 'abcdef12-3456-7890-abcd-ef1234567890', + 'comment': 'created by ansible', + 'type': 'async', + 'snapmirror_label': [], + 'keep': [], + 'schedule': [], + 'prefix': [], + 'network_compression_enabled': True, + 'identity_preservation': 'exclude_network_config' + }, None), + 'get_snapmirror_policy_async_with_options': (200, { + 'svm': {'name': 'ansible'}, + 'name': 'ansible', + 'uuid': 'abcdef12-3456-7890-abcd-ef1234567890', + 'comment': 'created by ansible', + 'type': 'async', + 'snapmirror_label': [], + 'keep': [], + 'schedule': [], + 'prefix': [], + 'copy_latest_source_snapshot': True, + 'network_compression_enabled': True, + 'identity_preservation': 'exclude_network_config' + }, None), + 'get_snapmirror_policy_sync': (200, { + 'svm': {'name': 'ansible'}, + 'name': 'ansible', + 'uuid': 'abcdef12-3456-7890-abcd-ef1234567890', + 'comment': 'created by ansible', + 'type': 'sync', + 'snapmirror_label': [], + 'keep': [], + 'schedule': [], + 'prefix': [], + 'network_compression_enabled': False + }, None), + 'get_snapmirror_policy_async_with_rules': (200, { + 'svm': {'name': 'ansible'}, + 'name': 'ansible', + 'uuid': 'abcdef12-3456-7890-abcd-ef1234567890', + 'comment': 'created by ansible', + 'type': 'async', + 'retention': [ + { + 'label': 'daily', + 'count': 7, + 'creation_schedule': {'name': ''}, + 'prefix': '', + }, + { + 'label': 'weekly', + 'count': 5, + 'creation_schedule': {'name': 'weekly'}, + 'prefix': 'weekly', + }, + { + 'label': 'monthly', + 'count': 12, + 'creation_schedule': {'name': 'monthly'}, + 'prefix': 'monthly', + }, + ], + 'network_compression_enabled': False + }, None), + 'get_snapmirror_policy_async_with_rules_dash': (200, { + 'svm': {'name': 'ansible'}, + 'name': 'ansible', + 'uuid': 'abcdef12-3456-7890-abcd-ef1234567890', + 'comment': 'created by ansible', + 'type': 'async', + 'retention': [ + { + 'label': 'daily', + 'count': 7, + 'creation_schedule': {'name': ''}, + 'prefix': '', + }, + { + 'label': 'weekly', + 'count': 5, + 'creation_schedule': {'name': 'weekly'}, + 'prefix': 'weekly', + }, + { + 'label': 'monthly', + 'count': 12, + 'creation_schedule': {'name': '-'}, + 'prefix': '-', + }, + ], + 'network_compression_enabled': False + }, None), + 'get_snapmirror_policy_async_with_create_snapshot_on_source': (200, { + 'svm': {'name': 'ansible'}, + 'name': 'ansible', + 'uuid': 'abcdef12-3456-7890-abcd-ef1234567890', + 'comment': 'created by ansible', + 'type': 'async', + 'retention': [ + { + 'label': 'daily', + 'count': 7, + 'creation_schedule': {'name': ''}, + 'prefix': '', + }, + ], + 'create_snapshot_on_source': False, + 'is_network_compression_enabled': True, + 'transfer_schedule': {'name': 'yearly'}, + }, None), + 'get_snapmirror_policy_sync_with_sync_type': (200, { + 'svm': {'name': 'ansible'}, + 'name': 'ansible', + 'uuid': 'abcdef12-3456-7890-abcd-ef1234567890', + 'comment': 'created by ansible', + 'type': 'sync', + 'sync_type': 'automated_failover', + # does not make sense, but does not hurt + 'copy_all_source_snapshots': False + }, None), +}) + + +snapmirror_policy_info = { + 'comment': 'created by ansible', + 'policy-name': 'ansible', + 'type': 'async_mirror', + 'tries': '8', + 'transfer-priority': 'normal', + 'restart': 'always', + 'is-network-compression-enabled': 'false', + 'ignore-atime': 'false', + 'vserver-name': 'ansible', + 'common-snapshot-schedule': 'monthly' +} + +snapmirror_policy_rules = { + 'snapmirror-policy-rules': [ + {'info': { + 'snapmirror-label': 'daily', + 'keep': 7, + 'schedule': '', + 'prefix': '', + }}, + {'info': { + 'snapmirror-label': 'weekly', + 'keep': 5, + 'schedule': 'weekly', + 'prefix': 'weekly', + }}, + {'info': { + 'snapmirror-label': 'monthly', + 'keep': 12, + 'schedule': 'monthly', + 'prefix': 'monthly', + }}, + {'info': { + 'snapmirror-label': 'sm_created', + 'keep': 12, + 'schedule': 'monthly', + 'prefix': 'monthly', + }}, + ] +} + + +def get_snapmirror_policy_info(with_rules=False): + info = dict(snapmirror_policy_info) + if with_rules: + info.update(snapmirror_policy_rules) + return {'attributes-list': {'snapmirror-policy-info': info}} + + +ZRR = zapi_responses({ + 'snapmirror-policy-info': build_zapi_response(get_snapmirror_policy_info()), + 'snapmirror-policy-info-with-rules': build_zapi_response(get_snapmirror_policy_info(True)), + 'error_13001': build_zapi_error(13001, 'policy not found'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'use_rest': 'use_rest', + 'policy_name': 'ansible', + 'vserver': 'ansible', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + args = dict(DEFAULT_ARGS) + args.pop('policy_name') + error = 'missing required arguments: policy_name' + assert error in call_main(my_main, args, fail=True)['msg'] + + +def test_ensure_get_called(): + ''' test get_snapmirror_policy for non-existent snapmirror policy''' + register_responses([ + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_snapmirror_policy() is None + + +def test_ensure_get_called_existing(): + ''' test get_snapmirror_policy for existing snapmirror policy''' + register_responses([ + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['snapmirror-policy-info']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_snapmirror_policy() + + +def test_successful_create(): + ''' creating snapmirror policy without rules and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['no_records']), + ('ZAPI', 'snapmirror-policy-create', ZRR['success']), + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['snapmirror-policy-info']), + ]) + module_args = { + 'use_rest': 'never', + 'transfer_priority': 'normal' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_create_with_rest(): + ''' creating snapmirror policy without rules via REST and testing idempotency ''' + register_responses([ + # default is async + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('POST', 'snapmirror/policies', SRR['success']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async']), + # explicitly async + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('POST', 'snapmirror/policies', SRR['success']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async_with_options']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async_with_options']), + # sync + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('POST', 'snapmirror/policies', SRR['success']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_sync']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_sync']), + ]) + module_args = { + 'use_rest': 'always', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['policy_type'] = 'async_mirror' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['policy_type'] = 'sync_mirror' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_create_with_rules(): + ''' creating snapmirror policy with rules and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['error_13001']), + ('ZAPI', 'snapmirror-policy-create', ZRR['success']), + ('ZAPI', 'snapmirror-policy-add-rule', ZRR['success']), + ('ZAPI', 'snapmirror-policy-add-rule', ZRR['success']), + ('ZAPI', 'snapmirror-policy-add-rule', ZRR['success']), + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['snapmirror-policy-info-with-rules']), + ]) + module_args = { + 'use_rest': 'never', + 'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [7, 5, 12], + 'schedule': ['', 'weekly', 'monthly'], + 'prefix': ['', 'weekly', 'monthly'] + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_create_with_rules_via_rest(): + ''' creating snapmirror policy with rules via rest and testing idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('POST', 'snapmirror/policies', SRR['success']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async']), + ('PATCH', 'snapmirror/policies/abcdef12-3456-7890-abcd-ef1234567890', SRR['success']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async_with_rules']), + ]) + module_args = { + 'use_rest': 'always', + 'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [7, 5, 12], + 'schedule': ['', 'weekly', 'monthly'], + 'prefix': ['', 'weekly', 'monthly'] + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_delete(): + ''' deleting snapmirror policy and testing idempotency ''' + register_responses([ + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['snapmirror-policy-info']), + ('ZAPI', 'snapmirror-policy-delete', ZRR['success']), + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + 'state': 'absent' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_delete_with_rest(): + ''' deleting snapmirror policy via REST and testing idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async_with_rules_dash']), + ('DELETE', 'snapmirror/policies/abcdef12-3456-7890-abcd-ef1234567890', SRR['success']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async_with_rules']), + ('DELETE', 'snapmirror/policies/abcdef12-3456-7890-abcd-ef1234567890', SRR['success']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ]) + module_args = { + 'state': 'absent', + 'use_rest': 'always', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify(): + ''' modifying snapmirror policy without rules. idempotency was tested in create ''' + register_responses([ + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['snapmirror-policy-info']), + ('ZAPI', 'snapmirror-policy-modify', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'comment': 'old comment', + 'ignore_atime': True, + 'is_network_compression_enabled': True, + 'owner': 'cluster_admin', + 'restart': 'default', + 'tries': '7'} + + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify_with_rest(): + ''' modifying snapmirror policy without rules via REST. Idempotency was tested in create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async']), + ('PATCH', 'snapmirror/policies/abcdef12-3456-7890-abcd-ef1234567890', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'comment': 'old comment', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify_with_rules(): + ''' modifying snapmirror policy with rules. Idempotency was tested in create ''' + register_responses([ + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['snapmirror-policy-info']), + ('ZAPI', 'snapmirror-policy-add-rule', ZRR['success']), + ('ZAPI', 'snapmirror-policy-add-rule', ZRR['success']), + ('ZAPI', 'snapmirror-policy-add-rule', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [7, 5, 12], + 'schedule': ['', 'weekly', 'monthly'], + 'prefix': ['', 'weekly', 'monthly'] + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify_with_rules_via_rest(): + ''' modifying snapmirror policy with rules via rest. Idempotency was tested in create ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async']), + ('PATCH', 'snapmirror/policies/abcdef12-3456-7890-abcd-ef1234567890', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [7, 5, 12], + 'schedule': ['', 'weekly', 'monthly'], + 'prefix': ['', 'weekly', 'monthly'] + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ZAPI', 'snapmirror-policy-get-iter', ZRR['error']), + ('ZAPI', 'snapmirror-policy-create', ZRR['error']), + ('ZAPI', 'snapmirror-policy-delete', ZRR['error']), + ('ZAPI', 'snapmirror-policy-modify', ZRR['error']), + ('ZAPI', 'snapmirror-policy-remove-rule', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'common_snapshot_schedule': 'sched', + 'policy_type': 'sync_mirror', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = zapi_error_message('Error getting snapmirror policy ansible') + assert error in expect_and_capture_ansible_exception(my_obj.get_snapmirror_policy, 'fail')['msg'] + error = zapi_error_message('Error creating snapmirror policy ansible') + assert error in expect_and_capture_ansible_exception(my_obj.create_snapmirror_policy, 'fail')['msg'] + error = zapi_error_message('Error deleting snapmirror policy ansible') + assert error in expect_and_capture_ansible_exception(my_obj.delete_snapmirror_policy, 'fail')['msg'] + error = zapi_error_message('Error modifying snapmirror policy ansible') + assert error in expect_and_capture_ansible_exception(my_obj.modify_snapmirror_policy, 'fail')['msg'] + module_args = { + 'use_rest': 'never', + 'common_snapshot_schedule': 'sched', + 'policy_type': 'sync_mirror', + 'snapmirror_label': ['lbl1'], + 'keep': [24], + } + current = { + 'snapmirror_label': ['lbl2'], + 'keep': [24], + 'prefix': [''], + 'schedule': ['weekly'], + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = zapi_error_message('Error modifying snapmirror policy rule ansible') + assert error in expect_and_capture_ansible_exception(my_obj.modify_snapmirror_policy_rules, 'fail', current)['msg'] + + +def test_if_all_methods_catch_exception_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['generic_error']), + ('POST', 'snapmirror/policies', SRR['generic_error']), + ('DELETE', 'snapmirror/policies/uuid', SRR['generic_error']), + ('PATCH', 'snapmirror/policies/uuid', SRR['generic_error']), + # modifying rules + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('PATCH', 'snapmirror/policies/uuid', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + 'policy_type': 'sync_mirror', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = rest_error_message('Error getting snapmirror policy', 'snapmirror/policies') + assert error in expect_and_capture_ansible_exception(my_obj.get_snapmirror_policy_rest, 'fail')['msg'] + error = rest_error_message('Error creating snapmirror policy', 'snapmirror/policies') + assert error in expect_and_capture_ansible_exception(my_obj.create_snapmirror_policy, 'fail')['msg'] + error = rest_error_message('Error deleting snapmirror policy', 'snapmirror/policies/uuid') + assert error in expect_and_capture_ansible_exception(my_obj.delete_snapmirror_policy, 'fail', 'uuid')['msg'] + error = rest_error_message('Error modifying snapmirror policy', 'snapmirror/policies/uuid') + assert error in expect_and_capture_ansible_exception(my_obj.modify_snapmirror_policy, 'fail', 'uuid', {'key': 'value'})['msg'] + module_args = { + 'use_rest': 'always', + 'policy_type': 'sync_mirror', + 'snapmirror_label': ['lbl1'], + 'keep': [24], + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = rest_error_message('Error modifying snapmirror policy rules', 'snapmirror/policies/uuid') + assert error in expect_and_capture_ansible_exception(my_obj.modify_snapmirror_policy_rules, 'fail', None, 'uuid')['msg'] + + +def test_create_snapmirror_policy_retention_obj_for_rest(): + ''' test create_snapmirror_policy_retention_obj_for_rest ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + # Test no rules + assert my_obj.create_snapmirror_policy_retention_obj_for_rest() == [] + + # Test one rule + rules = [{'snapmirror_label': 'daily', 'keep': 7}] + retention_obj = [{'label': 'daily', 'count': '7'}] + assert my_obj.create_snapmirror_policy_retention_obj_for_rest(rules) == retention_obj + + # Test two rules, with a prefix + rules = [{'snapmirror_label': 'daily', 'keep': 7}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly'}] + retention_obj = [{'label': 'daily', 'count': '7'}, + {'label': 'weekly', 'count': '5', 'prefix': 'weekly'}] + assert my_obj.create_snapmirror_policy_retention_obj_for_rest(rules) == retention_obj + + # Test three rules, with a prefix & schedule + rules = [{'snapmirror_label': 'daily', 'keep': 7}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly_sv'}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly_sv', 'schedule': 'monthly'}] + retention_obj = [{'label': 'daily', 'count': '7'}, + {'label': 'weekly', 'count': '5', 'prefix': 'weekly_sv'}, + {'label': 'monthly', 'count': '12', 'prefix': 'monthly_sv', 'creation_schedule': {'name': 'monthly'}}] + assert my_obj.create_snapmirror_policy_retention_obj_for_rest(rules) == retention_obj + + +def test_identify_snapmirror_policy_rules_with_schedule(): + ''' test identify_snapmirror_policy_rules_with_schedule ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + # Test no rules + assert my_obj.identify_snapmirror_policy_rules_with_schedule() == ([], []) + + # Test one non-schedule rule identified + rules = [{'snapmirror_label': 'daily', 'keep': 7}] + schedule_rules = [] + non_schedule_rules = [{'snapmirror_label': 'daily', 'keep': 7}] + assert my_obj.identify_snapmirror_policy_rules_with_schedule(rules) == (schedule_rules, non_schedule_rules) + + # Test one schedule and two non-schedule rules identified + rules = [{'snapmirror_label': 'daily', 'keep': 7}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly_sv'}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly_sv', 'schedule': 'monthly'}] + schedule_rules = [{'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly_sv', 'schedule': 'monthly'}] + non_schedule_rules = [{'snapmirror_label': 'daily', 'keep': 7}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly_sv'}] + assert my_obj.identify_snapmirror_policy_rules_with_schedule(rules) == (schedule_rules, non_schedule_rules) + + # Test three schedule & zero non-schedule rules identified + rules = [{'snapmirror_label': 'daily', 'keep': 7, 'schedule': 'daily'}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly_sv', 'schedule': 'weekly'}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly_sv', 'schedule': 'monthly'}] + schedule_rules = [{'snapmirror_label': 'daily', 'keep': 7, 'schedule': 'daily'}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly_sv', 'schedule': 'weekly'}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly_sv', 'schedule': 'monthly'}] + non_schedule_rules = [] + assert my_obj.identify_snapmirror_policy_rules_with_schedule(rules) == (schedule_rules, non_schedule_rules) + + +def test_identify_new_snapmirror_policy_rules(): + ''' test identify_new_snapmirror_policy_rules ''' + register_responses([ + ]) + + # Test with no rules in parameters. new_rules should always be []. + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + current = None + new_rules = [] + assert my_obj.identify_new_snapmirror_policy_rules(current) == new_rules + + current = {'snapmirror_label': ['daily'], 'keep': [7], 'prefix': [''], 'schedule': ['']} + new_rules = [] + assert my_obj.identify_new_snapmirror_policy_rules(current) == new_rules + + # Test with rules in parameters. + module_args = { + 'use_rest': 'never', + 'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [7, 5, 12], + 'schedule': ['', 'weekly', 'monthly'], + 'prefix': ['', 'weekly', 'monthly'] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + # Test three new rules identified when no rules currently exist + current = None + new_rules = [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': '', 'schedule': ''}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly', 'schedule': 'weekly'}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly', 'schedule': 'monthly'}] + assert my_obj.identify_new_snapmirror_policy_rules(current) == new_rules + + # Test two new rules identified and one rule already exists + current = {'snapmirror_label': ['daily'], 'keep': [7], 'prefix': [''], 'schedule': ['']} + new_rules = [{'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly', 'schedule': 'weekly'}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly', 'schedule': 'monthly'}] + assert my_obj.identify_new_snapmirror_policy_rules(current) == new_rules + + # Test one new rule identified and two rules already exist + current = {'snapmirror_label': ['daily', 'monthly'], + 'keep': [7, 12], + 'prefix': ['', 'monthly'], + 'schedule': ['', 'monthly']} + new_rules = [{'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly', 'schedule': 'weekly'}] + assert my_obj.identify_new_snapmirror_policy_rules(current) == new_rules + + # Test no new rules identified as all rules already exist + current = {'snapmirror_label': ['daily', 'monthly', 'weekly'], + 'keep': [7, 12, 5], + 'prefix': ['', 'monthly', 'weekly'], + 'schedule': ['', 'monthly', 'weekly']} + new_rules = [] + assert my_obj.identify_new_snapmirror_policy_rules(current) == new_rules + + +def test_identify_obsolete_snapmirror_policy_rules(): + ''' test identify_obsolete_snapmirror_policy_rules ''' + register_responses([ + ]) + + # Test with no rules in parameters. obsolete_rules should always be []. + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + current = None + obsolete_rules = [] + assert my_obj.identify_obsolete_snapmirror_policy_rules(current) == obsolete_rules + + current = {'snapmirror_label': ['daily'], 'keep': [7], 'prefix': [''], 'schedule': ['']} + obsolete_rules = [] + assert my_obj.identify_obsolete_snapmirror_policy_rules(current) == obsolete_rules + + # Test removing all rules. obsolete_rules should equal current. + module_args = { + 'use_rest': 'never', + 'snapmirror_label': [] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + current = {'snapmirror_label': ['monthly', 'weekly', 'hourly', 'daily', 'yearly'], + 'keep': [12, 5, 24, 7, 7], + 'prefix': ['monthly', 'weekly', '', '', 'yearly'], + 'schedule': ['monthly', 'weekly', '', '', 'yearly']} + obsolete_rules = [{'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly', 'schedule': 'monthly'}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly', 'schedule': 'weekly'}, + {'snapmirror_label': 'hourly', 'keep': 24, 'prefix': '', 'schedule': ''}, + {'snapmirror_label': 'daily', 'keep': 7, 'prefix': '', 'schedule': ''}, + {'snapmirror_label': 'yearly', 'keep': 7, 'prefix': 'yearly', 'schedule': 'yearly'}] + assert my_obj.identify_obsolete_snapmirror_policy_rules(current) == obsolete_rules + + # Test with rules in parameters. + module_args = { + 'use_rest': 'never', + 'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [7, 5, 12], + 'schedule': ['', 'weekly', 'monthly'], + 'prefix': ['', 'weekly', 'monthly'] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + # Test no rules exist, thus no obsolete rules + current = None + obsolete_rules = [] + assert my_obj.identify_obsolete_snapmirror_policy_rules(current) == obsolete_rules + + # Test new rules and one obsolete rule identified + current = {'snapmirror_label': ['hourly'], 'keep': [24], 'prefix': [''], 'schedule': ['']} + obsolete_rules = [{'snapmirror_label': 'hourly', 'keep': 24, 'prefix': '', 'schedule': ''}] + assert my_obj.identify_obsolete_snapmirror_policy_rules(current) == obsolete_rules + + # Test new rules, with one retained and one obsolete rule identified + current = {'snapmirror_label': ['hourly', 'daily'], + 'keep': [24, 7], + 'prefix': ['', ''], + 'schedule': ['', '']} + obsolete_rules = [{'snapmirror_label': 'hourly', 'keep': 24, 'prefix': '', 'schedule': ''}] + assert my_obj.identify_obsolete_snapmirror_policy_rules(current) == obsolete_rules + + # Test new rules and two obsolete rules identified + current = {'snapmirror_label': ['monthly', 'weekly', 'hourly', 'daily', 'yearly'], + 'keep': [12, 5, 24, 7, 7], + 'prefix': ['monthly', 'weekly', '', '', 'yearly'], + 'schedule': ['monthly', 'weekly', '', '', 'yearly']} + obsolete_rules = [{'snapmirror_label': 'hourly', 'keep': 24, 'prefix': '', 'schedule': ''}, + {'snapmirror_label': 'yearly', 'keep': 7, 'prefix': 'yearly', 'schedule': 'yearly'}] + assert my_obj.identify_obsolete_snapmirror_policy_rules(current) == obsolete_rules + + +def test_identify_modified_snapmirror_policy_rules(): + ''' test identify_modified_snapmirror_policy_rules ''' + register_responses([ + + ]) + + # Test with no rules in parameters. modified_rules & unmodified_rules should always be []. + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + current = None + modified_rules, unmodified_rules = [], [] + assert my_obj.identify_modified_snapmirror_policy_rules(current), (modified_rules == unmodified_rules) + + current = {'snapmirror_label': ['daily'], 'keep': [14], 'prefix': ['daily'], 'schedule': ['daily']} + modified_rules, unmodified_rules = [], [] + assert my_obj.identify_modified_snapmirror_policy_rules(current), (modified_rules == unmodified_rules) + + # Test removing all rules. modified_rules & unmodified_rules should be []. + module_args = { + 'use_rest': 'never', + 'snapmirror_label': [] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + current = {'snapmirror_label': ['monthly', 'weekly', 'hourly', 'daily', 'yearly'], + 'keep': [12, 5, 24, 7, 7], + 'prefix': ['monthly', 'weekly', '', '', 'yearly'], + 'schedule': ['monthly', 'weekly', '', '', 'yearly']} + modified_rules, unmodified_rules = [], [] + assert my_obj.identify_modified_snapmirror_policy_rules(current), (modified_rules == unmodified_rules) + + # Test with rules in parameters. + module_args = { + 'use_rest': 'never', + 'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [7, 5, 12], + 'schedule': ['', 'weekly', 'monthly'], + 'prefix': ['', 'weekly', 'monthly'] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + + # Test no rules exist, thus no modified & unmodified rules + current = None + modified_rules, unmodified_rules = [], [] + assert my_obj.identify_modified_snapmirror_policy_rules(current), (modified_rules == unmodified_rules) + + # Test new rules don't exist, thus no modified & unmodified rules + current = {'snapmirror_label': ['hourly'], 'keep': [24], 'prefix': [''], 'schedule': ['']} + modified_rules, unmodified_rules = [], [] + assert my_obj.identify_modified_snapmirror_policy_rules(current), (modified_rules == unmodified_rules) + + # Test daily & monthly modified, weekly unmodified + current = {'snapmirror_label': ['hourly', 'daily', 'weekly', 'monthly'], + 'keep': [24, 14, 5, 6], + 'prefix': ['', 'daily', 'weekly', 'monthly'], + 'schedule': ['', 'daily', 'weekly', 'monthly']} + modified_rules = [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': '', 'schedule': ''}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly', 'schedule': 'monthly'}] + unmodified_rules = [{'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly', 'schedule': 'weekly'}] + assert my_obj.identify_modified_snapmirror_policy_rules(current), (modified_rules == unmodified_rules) + + # Test all rules modified + current = {'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [14, 10, 6], + 'prefix': ['', '', ''], + 'schedule': ['daily', 'weekly', 'monthly']} + modified_rules = [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': '', 'schedule': ''}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly', 'schedule': 'weekly'}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly', 'schedule': 'monthly'}] + unmodified_rules = [] + assert my_obj.identify_modified_snapmirror_policy_rules(current), (modified_rules == unmodified_rules) + + # Test all rules unmodified + current = {'snapmirror_label': ['daily', 'weekly', 'monthly'], + 'keep': [7, 5, 12], + 'prefix': ['', 'weekly', 'monthly'], + 'schedule': ['', 'weekly', 'monthly']} + modified_rules = [] + unmodified_rules = [{'snapmirror_label': 'daily', 'keep': 7, 'prefix': '', 'schedule': ''}, + {'snapmirror_label': 'weekly', 'keep': 5, 'prefix': 'weekly', 'schedule': 'weekly'}, + {'snapmirror_label': 'monthly', 'keep': 12, 'prefix': 'monthly', 'schedule': 'monthly'}] + assert my_obj.identify_modified_snapmirror_policy_rules(current), (modified_rules == unmodified_rules) + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.HAS_NETAPP_LIB', False) +def test_module_fail_when_netapp_lib_missing(): + ''' required lib missing ''' + module_args = { + 'use_rest': 'never', + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_validate_parameters(): + ''' test test_validate_parameters ''' + register_responses([ + ]) + + args = dict(DEFAULT_ARGS) + args.pop('vserver') + module_args = { + 'use_rest': 'never', + } + error = 'Error: vserver is a required parameter when using ZAPI.' + assert error in create_module(my_module, args, module_args, fail=True)['msg'] + + module_args = { + 'use_rest': 'never', + 'snapmirror_label': list(range(11)), + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = 'Error: A SnapMirror Policy can have up to a maximum of' + assert error in expect_and_capture_ansible_exception(my_obj.validate_parameters, 'fail')['msg'] + + module_args = { + 'use_rest': 'never', + 'snapmirror_label': list(range(10)), + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "Error: Missing 'keep' parameter. When specifying the 'snapmirror_label' parameter, the 'keep' parameter must also be supplied" + assert error in expect_and_capture_ansible_exception(my_obj.validate_parameters, 'fail')['msg'] + + module_args = { + 'use_rest': 'never', + 'snapmirror_label': list(range(10)), + 'keep': list(range(9)), + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "Error: Each 'snapmirror_label' value must have an accompanying 'keep' value" + assert error in expect_and_capture_ansible_exception(my_obj.validate_parameters, 'fail')['msg'] + + module_args = { + 'use_rest': 'never', + 'snapmirror_label': list(range(10)), + 'keep': list(range(11)), + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "Error: Each 'keep' value must have an accompanying 'snapmirror_label' value" + assert error in expect_and_capture_ansible_exception(my_obj.validate_parameters, 'fail')['msg'] + + module_args = { + 'use_rest': 'never', + 'keep': list(range(11)), + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "Error: Missing 'snapmirror_label' parameter. When specifying the 'keep' parameter, the 'snapmirror_label' parameter must also be supplied" + assert error in expect_and_capture_ansible_exception(my_obj.validate_parameters, 'fail')['msg'] + + module_args = { + 'use_rest': 'never', + 'snapmirror_label': list(range(10)), + 'keep': list(range(10)), + 'prefix': list(range(10)), + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "Error: Missing 'schedule' parameter. When specifying the 'prefix' parameter, the 'schedule' parameter must also be supplied" + assert error in expect_and_capture_ansible_exception(my_obj.validate_parameters, 'fail')['msg'] + + module_args = { + 'use_rest': 'never', + 'identity_preservation': 'full', + } + error = 'Error: identity_preservation option is not supported with ZAPI. It can only be used with REST.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + module_args = { + 'use_rest': 'never', + 'copy_all_source_snapshots': True, + } + error = 'Error: copy_all_source_snapshots option is not supported with ZAPI. It can only be used with REST.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_validate_parameters_rest(): + ''' test test_validate_parameters ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('POST', 'snapmirror/policies', SRR['success']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async']), + # copy_all_source_snapshots + ('GET', 'cluster', SRR['is_rest_9_10_1']), + # copy_latest_source_snapshot + ('GET', 'cluster', SRR['is_rest_9_11_1']), + # create_snapshot_on_source + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ]) + + module_args = { + 'use_rest': 'always', + 'policy_type': 'sync_mirror', + 'is_network_compression_enabled': True + } + error = 'Error: input parameter network_compression_enabled is not valid for SnapMirror policy type sync' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + module_args = { + 'use_rest': 'always', + 'policy_type': 'sync_mirror', + 'identity_preservation': 'full' + } + error = 'Error: identity_preservation is only supported with async (async) policy_type, got: sync' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + module_args = { + 'use_rest': 'always', + 'policy_type': 'async_mirror', + 'is_network_compression_enabled': True, + 'identity_preservation': 'full' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + module_args = { + 'use_rest': 'always', + 'policy_type': 'async_mirror', + 'copy_all_source_snapshots': False, + } + error = 'Error: the property copy_all_source_snapshots can only be set to true when present' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + module_args = { + 'use_rest': 'always', + 'policy_type': 'async_mirror', + 'copy_latest_source_snapshot': False, + } + error = 'Error: the property copy_latest_source_snapshot can only be set to true when present' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + module_args = { + 'use_rest': 'always', + 'policy_type': 'vault', + 'create_snapshot_on_source': True, + } + error = 'Error: the property create_snapshot_on_source can only be set to false when present' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_errors_in_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('POST', 'snapmirror/policies', SRR['success']), + ('GET', 'snapmirror/policies', SRR['zero_records']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_sync']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async']), + ]) + module_args = { + 'use_rest': 'always', + } + error = 'Error: policy ansible not present after create.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + # change in policy type + module_args = { + 'use_rest': 'always', + 'policy_type': 'async_mirror', + } + error = 'Error: The policy property policy_type cannot be modified from sync to async' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args = { + 'use_rest': 'always', + 'policy_type': 'sync_mirror', + } + error = 'Error: The policy property policy_type cannot be modified from async to sync' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_errors_in_create_with_copy_snapshots(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = { + 'use_rest': 'always', + 'copy_all_source_snapshots': True, + 'policy_type': 'sync_mirror' + } + msg = 'Error: option copy_all_source_snapshots is not supported with policy type sync_mirror.' + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg in error + + +def test_errors_in_create_with_copy_latest_snapshots(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ]) + module_args = { + 'use_rest': 'always', + 'copy_latest_source_snapshot': True, + 'policy_type': 'async', + 'snapmirror_label': ["daily", "weekly"], + } + msg = 'Error: Retention properties cannot be specified along with copy_all_source_snapshots or copy_latest_source_snapshot properties' + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg in error + + +def test_errors_in_create_snapshot_on_source(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ]) + module_args = { + 'use_rest': 'always', + 'create_snapshot_on_source': False, + 'policy_type': 'sync_mirror', + 'snapmirror_label': ["daily", "weekly"], + 'keep': ["7", "2"], + } + msg = 'Error: option create_snapshot_on_source is not supported with policy type sync_mirror.' + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg in error + + module_args = { + 'use_rest': 'always', + 'create_snapshot_on_source': False, + 'policy_type': 'async', + 'snapmirror_label': ["daily", "weekly"], + } + msg = 'Error: The properties snapmirror_label and keep must be specified with' + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg in error + + +def test_async_create_snapshot_on_source(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_12_1']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['empty_records']), + ('POST', 'snapmirror/policies', SRR['success']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_async_with_create_snapshot_on_source']), + ]) + module_args = { + 'use_rest': 'always', + 'create_snapshot_on_source': False, + 'policy_type': 'vault', + 'snapmirror_label': ["daily", "weekly"], + 'keep': ["7", "2"], + 'prefix': ["p1", "p2"], + 'schedule': ["daily", "weekly"], + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_get_snapmirror_policy_sync_with_sync_type(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'snapmirror/policies', SRR['empty_records']), + ('POST', 'snapmirror/policies', SRR['success']), + ('GET', 'snapmirror/policies', SRR['get_snapmirror_policy_sync_with_sync_type']), + ]) + module_args = { + 'use_rest': 'always', + 'policy_type': 'sync_mirror', + 'sync_type': 'automated_failover' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_set_scope(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['zero_records']), + # first test + ('GET', 'svm/svms', SRR['zero_records']), + ('GET', 'svm/svms', SRR['one_vserver_record']), + ('GET', 'svm/svms', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + # vserver not found + assert my_obj.set_scope() == 'cluster' + # vserver found + assert my_obj.set_scope() == 'svm' + # API error + error = rest_error_message('Error getting vserver ansible info', 'svm/svms') + assert error in expect_and_capture_ansible_exception(my_obj.set_scope, 'fail')['msg'] + # no vserver + my_obj.parameters.pop('vserver') + assert my_obj.set_scope() == 'cluster' + + +def check_mapping(my_obj, policy_type, expected_policy_type, copy_latest_source_snapshot, copy_all_source_snapshots, create_snapshot_on_source, retention): + my_obj.parameters['policy_type'] = policy_type + if copy_latest_source_snapshot is None: + my_obj.parameters.pop('copy_latest_source_snapshot', None) + else: + my_obj.parameters['copy_latest_source_snapshot'] = copy_latest_source_snapshot + if copy_all_source_snapshots is None: + my_obj.parameters.pop('copy_all_source_snapshots', None) + else: + my_obj.parameters['copy_all_source_snapshots'] = copy_all_source_snapshots + if create_snapshot_on_source is None: + my_obj.parameters.pop('create_snapshot_on_source', None) + else: + my_obj.parameters['create_snapshot_on_source'] = create_snapshot_on_source + if retention is None: + my_obj.parameters.pop('snapmirror_label', None) + my_obj.parameters.pop('keep', None) + my_obj.parameters.pop('prefix', None) + my_obj.parameters.pop('schedule', None) + else: + for key, value in retention.items(): + my_obj.parameters[key] = value + my_obj.validate_policy_type() + assert my_obj.parameters['policy_type'] == expected_policy_type + + +def check_options(my_obj, copy_latest_source_snapshot, copy_all_source_snapshots, create_snapshot_on_source): + if copy_latest_source_snapshot is None: + assert 'copy_latest_source_snapshot' not in my_obj.parameters + else: + assert my_obj.parameters['copy_latest_source_snapshot'] == copy_latest_source_snapshot + if copy_all_source_snapshots is None: + assert 'copy_all_source_snapshots' not in my_obj.parameters + else: + assert my_obj.parameters['copy_all_source_snapshots'] == copy_all_source_snapshots + if create_snapshot_on_source is None: + assert 'create_snapshot_on_source' not in my_obj.parameters + else: + assert my_obj.parameters['create_snapshot_on_source'] == create_snapshot_on_source + + +def test_validate_policy_type(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['zero_records']), + # first test + ]) + module_args = { + 'use_rest': 'always', + } + retention = { + 'snapmirror_label': ["daily", "weekly"], + 'keep': ["7", "2"] + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + check_mapping(my_obj, 'async', 'async', None, None, None, None) + check_options(my_obj, None, None, None) + check_mapping(my_obj, 'mirror_vault', 'async', None, None, None, None) + check_options(my_obj, None, None, None) + check_mapping(my_obj, 'vault', 'async', None, None, None, retention) + check_options(my_obj, None, None, False) + check_mapping(my_obj, 'async_mirror', 'async', None, None, None, None) + check_options(my_obj, True, None, None) + check_mapping(my_obj, 'sync', 'sync', None, None, None, None) + check_options(my_obj, None, None, None) + check_mapping(my_obj, 'sync_mirror', 'sync', None, None, None, None) + check_options(my_obj, None, None, None) + check_mapping(my_obj, 'strict_sync_mirror', 'sync', None, None, None, None) + check_options(my_obj, None, None, None) + + my_obj.parameters['policy_type'] = 'async' + my_obj.parameters['sync_type'] = 'strict_sync' + error = "Error: 'sync_type' is only applicable for sync policy_type" + assert error in expect_and_capture_ansible_exception(my_obj.validate_policy_type, 'fail')['msg'] + + module_args = { + 'use_rest': 'never', + 'policy_type': 'sync' + } + error = 'Error: The policy types async and sync are not supported in ZAPI.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_build_body_for_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['zero_records']), + # first test + ]) + module_args = { + 'use_rest': 'always', + 'snapmirror_label': ["daily", "weekly"], + 'keep': ["7", "2"], + 'copy_all_source_snapshots': True + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + body = my_obj.build_body_for_create() + assert 'copy_all_source_snapshots' in body + + +def test_modify_snapmirror_policy_rules_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'svm/svms', SRR['zero_records']), + # first test + ]) + module_args = { + 'use_rest': 'always', + 'snapmirror_label': ["daily", "weekly"], + 'keep': ["7", "2"], + 'copy_all_source_snapshots': True + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.modify_snapmirror_policy_rules_rest('uuid', [], ['umod'], [], []) is None diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot.py new file mode 100644 index 000000000..f7c49eaad --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot.py @@ -0,0 +1,363 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_nvme_snapshot''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_and_apply, create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot \ + import NetAppOntapSnapshot as my_module, main as my_main + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +SRR = rest_responses({ + 'volume_uuid': (200, + {'records': [{"uuid": "test_uuid"}], 'num_records': 1}, None, + ), + 'snapshot_record': (200, + {'records': [{"volume": {"uuid": "d9cd4ec5-c96d-11eb-9271-005056b3ef5a", + "name": "ansible_vol"}, + "uuid": "343b5227-8c6b-4e79-a133-304bbf7537ce", + "svm": {"uuid": "b663d6f0-c96d-11eb-9271-005056b3ef5a", + "name": "ansible"}, + "name": "ss1", + "create_time": "2021-06-10T17:24:41-04:00", + "comment": "123", + "expiry_time": "2022-02-04T14:00:00-05:00", + "snapmirror_label": "321", }], 'num_records': 1}, None), + 'create_response': (200, {'job': {'uuid': 'd0b3eefe-cd59-11eb-a170-005056b338cd', + '_links': { + 'self': {'href': '/api/cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd'}}}}, + None), + 'job_response': (200, {'uuid': 'e43a40db-cd61-11eb-a170-005056b338cd', + 'description': 'PATCH /api/storage/volumes/d9cd4ec5-c96d-11eb-9271-005056b3ef5a/' + 'snapshots/da995362-cd61-11eb-a170-005056b338cd', + 'state': 'success', + 'message': 'success', + 'code': 0, + 'start_time': '2021-06-14T18:43:08-04:00', + 'end_time': '2021-06-14T18:43:08-04:00', + 'svm': {'name': 'ansible', 'uuid': 'b663d6f0-c96d-11eb-9271-005056b3ef5a', + '_links': {'self': {'href': '/api/svm/svms/b663d6f0-c96d-11eb-9271-005056b3ef5a'}}}, + '_links': {'self': {'href': '/api/cluster/jobs/e43a40db-cd61-11eb-a170-005056b338cd'}}}, + None) +}, allow_override=False) + + +snapshot_info = { + 'num-records': 1, + 'attributes-list': { + 'snapshot-info': { + 'comment': 'new comment', + 'name': 'ansible', + 'snapmirror-label': 'label12' + } + } +} + +ZRR = zapi_responses({ + 'get_snapshot': build_zapi_response(snapshot_info) +}) + + +DEFAULT_ARGS = { + 'state': 'present', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'comment': 'test comment', + 'snapshot': 'test_snapshot', + 'snapmirror_label': 'test_label', + 'volume': 'test_vol' +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + error = create_module(my_module, fail=True)['msg'] + assert 'missing required arguments:' in error + for arg in ('hostname', 'snapshot', 'volume', 'vserver'): + assert arg in error + + +def test_ensure_get_called(): + ''' test get_snapshot() for non-existent snapshot''' + register_responses([ + ('snapshot-get-iter', ZRR['empty']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_snapshot() is None + + +def test_ensure_get_called_existing(): + ''' test get_snapshot() for existing snapshot''' + register_responses([ + ('snapshot-get-iter', ZRR['get_snapshot']), + ]) + module_args = { + 'use_rest': 'never', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_snapshot() + + +def test_successful_create(): + ''' creating snapshot and testing idempotency ''' + register_responses([ + ('snapshot-get-iter', ZRR['empty']), + ('snapshot-create', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'async_bool': True + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify(): + ''' modifying snapshot and testing idempotency ''' + register_responses([ + ('snapshot-get-iter', ZRR['get_snapshot']), + ('snapshot-modify-iter', ZRR['success']), + ('snapshot-get-iter', ZRR['get_snapshot']), + ]) + module_args = { + 'use_rest': 'never', + 'comment': 'adding comment', + 'snapmirror_label': 'label22', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + module_args = { + 'use_rest': 'never', + 'comment': 'new comment', + 'snapmirror_label': 'label12', + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_rename(): + ''' modifying snapshot and testing idempotency ''' + register_responses([ + ('snapshot-get-iter', ZRR['empty']), + ('snapshot-get-iter', ZRR['get_snapshot']), + ('snapshot-rename', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'from_name': 'from_snapshot', + 'comment': 'new comment', + 'snapmirror_label': 'label12', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_delete(): + ''' deleting snapshot and testing idempotency ''' + register_responses([ + ('snapshot-get-iter', ZRR['get_snapshot']), + ('snapshot-delete', ZRR['success']), + ('snapshot-get-iter', ZRR['empty']), + ]) + module_args = { + 'use_rest': 'never', + 'state': 'absent', + 'ignore_owners': True, + 'snapshot_instance_uuid': 'uuid', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('snapshot-get-iter', ZRR['error']), + ('snapshot-create', ZRR['error']), + ('snapshot-delete', ZRR['error']), + ('snapshot-modify-iter', ZRR['error']), + ('snapshot-rename', ZRR['error']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), # get version + ('GET', 'storage/volumes/None/snapshots', SRR['generic_error']), + ('POST', 'storage/volumes/None/snapshots', SRR['generic_error']), + ('DELETE', 'storage/volumes/None/snapshots/None', SRR['generic_error']), + ('PATCH', 'storage/volumes/None/snapshots/None', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']) + ]) + module_args = { + 'use_rest': 'never', + 'from_name': 'from_snapshot'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'Error fetching snapshot' in expect_and_capture_ansible_exception(my_obj.get_snapshot, 'fail')['msg'] + assert 'Error creating snapshot test_snapshot:' in expect_and_capture_ansible_exception(my_obj.create_snapshot, 'fail')['msg'] + assert 'Error deleting snapshot test_snapshot:' in expect_and_capture_ansible_exception(my_obj.delete_snapshot, 'fail')['msg'] + assert 'Error modifying snapshot test_snapshot:' in expect_and_capture_ansible_exception(my_obj.modify_snapshot, 'fail')['msg'] + assert 'Error renaming snapshot from_snapshot to test_snapshot:' in expect_and_capture_ansible_exception(my_obj.rename_snapshot, 'fail')['msg'] + module_args = {'use_rest': 'always'} + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'Error fetching snapshot' in expect_and_capture_ansible_exception(my_obj.get_snapshot, 'fail')['msg'] + assert 'Error when creating snapshot:' in expect_and_capture_ansible_exception(my_obj.create_snapshot, 'fail')['msg'] + assert 'Error when deleting snapshot:' in expect_and_capture_ansible_exception(my_obj.delete_snapshot, 'fail')['msg'] + assert 'Error when modifying snapshot:' in expect_and_capture_ansible_exception(my_obj.modify_snapshot, 'fail')['msg'] + assert 'Error getting volume info:' in expect_and_capture_ansible_exception(my_obj.get_volume_uuid, 'fail')['msg'] + + +def test_module_fail_rest_ONTAP96(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) # get version + ]) + module_args = {'use_rest': 'always'} + msg = 'Error: Minimum version of ONTAP for snapmirror_label is (9, 7)' + assert msg in create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_successfully_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['empty_records']), + ('POST', 'storage/volumes/test_uuid/snapshots', SRR['create_response']), + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_response']), + ]) + module_args = { + 'use_rest': 'always', + 'expiry_time': 'expiry' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_create_no_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['empty_records']), + ]) + module_args = {'use_rest': 'always'} + msg = 'Error: volume test_vol not found for vserver vserver.' + assert msg == create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_successfully_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['snapshot_record']), + ('PATCH', 'storage/volumes/test_uuid/snapshots/343b5227-8c6b-4e79-a133-304bbf7537ce', SRR['create_response']), # modify + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_response']), + ]) + module_args = { + 'use_rest': 'always', + 'comment': 'new comment', + 'expiry_time': 'expiry' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_rename(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['empty_records']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['snapshot_record']), + ('PATCH', 'storage/volumes/test_uuid/snapshots/343b5227-8c6b-4e79-a133-304bbf7537ce', SRR['create_response']), # modify + ('GET', 'cluster/jobs/d0b3eefe-cd59-11eb-a170-005056b338cd', SRR['job_response']), + ]) + module_args = { + 'use_rest': 'always', + 'from_name': 'old_snapshot'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_rename_from_not_found(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['empty_records']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['empty_records']), + ]) + module_args = { + 'use_rest': 'always', + 'from_name': 'old_snapshot'} + msg = 'Error renaming snapshot: test_snapshot - no snapshot with from_name: old_snapshot.' + assert msg == create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_successfully_delete(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['snapshot_record']), + ('DELETE', 'storage/volumes/test_uuid/snapshots/343b5227-8c6b-4e79-a133-304bbf7537ce', SRR['success']), + ]) + module_args = { + 'use_rest': 'always', + 'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_delete(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['snapshot_record']), + ('DELETE', 'storage/volumes/test_uuid/snapshots/343b5227-8c6b-4e79-a133-304bbf7537ce', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + 'state': 'absent'} + msg = 'Error when deleting snapshot: calling: storage/volumes/test_uuid/snapshots/343b5227-8c6b-4e79-a133-304bbf7537ce: got Expected error.' + assert msg == create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_call_main(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/volumes', SRR['volume_uuid']), + ('GET', 'storage/volumes/test_uuid/snapshots', SRR['snapshot_record']), + ('DELETE', 'storage/volumes/test_uuid/snapshots/343b5227-8c6b-4e79-a133-304bbf7537ce', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + 'state': 'absent'} + msg = 'Error when deleting snapshot: calling: storage/volumes/test_uuid/snapshots/343b5227-8c6b-4e79-a133-304bbf7537ce: got Expected error.' + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_unsupported_options(): + module_args = { + 'use_rest': 'always', + 'ignore_owners': True} + error = "REST API currently does not support 'ignore_owners'" + assert error == create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args = { + 'use_rest': 'never', + 'expiry_time': 'any'} + error = "expiry_time is currently only supported with REST on Ontap 9.6 or higher" + assert error == create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_missing_netapp_lib(mock_has_netapp_lib): + module_args = { + 'use_rest': 'never', + } + mock_has_netapp_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg == create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy.py new file mode 100644 index 000000000..84d928f19 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy.py @@ -0,0 +1,658 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_snapshot_policy''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy \ + import NetAppOntapSnapshotPolicy as my_module + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'policy': + xml = self.build_snapshot_policy_info() + elif self.type == 'snapshot_policy_info_policy_disabled': + xml = self.build_snapshot_policy_info_policy_disabled() + elif self.type == 'snapshot_policy_info_comment_modified': + xml = self.build_snapshot_policy_info_comment_modified() + elif self.type == 'snapshot_policy_info_schedules_added': + xml = self.build_snapshot_policy_info_schedules_added() + elif self.type == 'snapshot_policy_info_schedules_deleted': + xml = self.build_snapshot_policy_info_schedules_deleted() + elif self.type == 'snapshot_policy_info_modified_schedule_counts': + xml = self.build_snapshot_policy_info_modified_schedule_counts() + elif self.type == 'policy_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + def asup_log_for_cserver(self): + ''' mock autosupport log''' + return None + + @staticmethod + def build_snapshot_policy_info(): + ''' build xml data for snapshot-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': { + 'snapshot-policy-info': { + 'comment': 'new comment', + 'enabled': 'true', + 'policy': 'ansible', + 'snapshot-policy-schedules': { + 'snapshot-schedule-info': { + 'count': 100, + 'schedule': 'hourly', + 'prefix': 'hourly', + 'snapmirror-label': '' + } + }, + 'vserver-name': 'hostname' + } + }} + xml.translate_struct(data) + return xml + + @staticmethod + def build_snapshot_policy_info_comment_modified(): + ''' build xml data for snapshot-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': { + 'snapshot-policy-info': { + 'comment': 'modified comment', + 'enabled': 'true', + 'policy': 'ansible', + 'snapshot-policy-schedules': { + 'snapshot-schedule-info': { + 'count': 100, + 'schedule': 'hourly', + 'prefix': 'hourly', + 'snapmirror-label': '' + } + }, + 'vserver-name': 'hostname' + } + }} + xml.translate_struct(data) + return xml + + @staticmethod + def build_snapshot_policy_info_policy_disabled(): + ''' build xml data for snapshot-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': { + 'snapshot-policy-info': { + 'comment': 'new comment', + 'enabled': 'false', + 'policy': 'ansible', + 'snapshot-policy-schedules': { + 'snapshot-schedule-info': { + 'count': 100, + 'schedule': 'hourly', + 'prefix': 'hourly', + 'snapmirror-label': '' + } + }, + 'vserver-name': 'hostname' + } + }} + xml.translate_struct(data) + return xml + + @staticmethod + def build_snapshot_policy_info_schedules_added(): + ''' build xml data for snapshot-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': { + 'snapshot-policy-info': { + 'comment': 'new comment', + 'enabled': 'true', + 'policy': 'ansible', + 'snapshot-policy-schedules': [ + { + 'snapshot-schedule-info': { + 'count': 100, + 'schedule': 'hourly', + 'prefix': 'hourly', + 'snapmirror-label': '' + } + }, + { + 'snapshot-schedule-info': { + 'count': 5, + 'schedule': 'daily', + 'prefix': 'daily', + 'snapmirror-label': 'daily' + } + }, + { + 'snapshot-schedule-info': { + 'count': 10, + 'schedule': 'weekly', + 'prefix': 'weekly', + 'snapmirror-label': '' + } + } + ], + 'vserver-name': 'hostname' + } + }} + xml.translate_struct(data) + return xml + + @staticmethod + def build_snapshot_policy_info_schedules_deleted(): + ''' build xml data for snapshot-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': { + 'snapshot-policy-info': { + 'comment': 'new comment', + 'enabled': 'true', + 'policy': 'ansible', + 'snapshot-policy-schedules': [ + { + 'snapshot-schedule-info': { + 'schedule': 'daily', + 'prefix': 'daily', + 'count': 5, + 'snapmirror-label': 'daily' + } + } + ], + 'vserver-name': 'hostname' + } + }} + xml.translate_struct(data) + return xml + + @staticmethod + def build_snapshot_policy_info_modified_schedule_counts(): + ''' build xml data for snapshot-policy-info ''' + xml = netapp_utils.zapi.NaElement('xml') + data = {'num-records': 1, + 'attributes-list': { + 'snapshot-policy-info': { + 'comment': 'new comment', + 'enabled': 'true', + 'policy': 'ansible', + 'snapshot-policy-schedules': [ + { + 'snapshot-schedule-info': { + 'count': 10, + 'schedule': 'hourly', + 'prefix': 'hourly', + 'snapmirror-label': '' + } + }, + { + 'snapshot-schedule-info': { + 'count': 50, + 'schedule': 'daily', + 'prefix': 'daily', + 'snapmirror-label': 'daily' + } + }, + { + 'snapshot-schedule-info': { + 'count': 100, + 'schedule': 'weekly', + 'prefix': 'weekly', + 'snapmirror-label': '' + } + } + ], + 'vserver-name': 'hostname' + } + }} + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self): + if self.onbox: + hostname = '10.10.10.10' + username = 'admin' + password = '1234' + name = 'ansible' + enabled = True + count = 100 + schedule = 'hourly' + prefix = 'hourly' + comment = 'new comment' + else: + hostname = 'hostname' + username = 'username' + password = 'password' + name = 'ansible' + enabled = True + count = 100 + schedule = 'hourly' + prefix = 'hourly' + comment = 'new comment' + return dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'name': name, + 'enabled': enabled, + 'count': count, + 'schedule': schedule, + 'prefix': prefix, + 'comment': comment, + 'use_rest': 'never' + }) + + def set_default_current(self): + default_args = self.set_default_args() + return dict({ + 'name': default_args['name'], + 'enabled': default_args['enabled'], + 'count': [default_args['count']], + 'schedule': [default_args['schedule']], + 'snapmirror_label': [''], + 'prefix': [default_args['prefix']], + 'comment': default_args['comment'], + 'vserver': default_args['hostname'] + }) + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called(self): + ''' test get_snapshot_policy() for non-existent snapshot policy''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = self.server + assert my_obj.get_snapshot_policy() is None + + def test_ensure_get_called_existing(self): + ''' test get_snapshot_policy() for existing snapshot policy''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.server = MockONTAPConnection(kind='policy') + assert my_obj.get_snapshot_policy() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy.NetAppOntapSnapshotPolicy.create_snapshot_policy') + def test_successful_create(self, create_snapshot): + ''' creating snapshot policy and testing idempotency ''' + set_module_args(self.set_default_args()) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + create_snapshot.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy.NetAppOntapSnapshotPolicy.modify_snapshot_policy') + def test_successful_modify_comment(self, modify_snapshot): + ''' modifying snapshot policy comment and testing idempotency ''' + data = self.set_default_args() + data['comment'] = 'modified comment' + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + current = self.set_default_current() + modify_snapshot.assert_called_with(current) + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('snapshot_policy_info_comment_modified') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy.NetAppOntapSnapshotPolicy.modify_snapshot_policy') + def test_successful_disable_policy(self, modify_snapshot): + ''' disabling snapshot policy and testing idempotency ''' + data = self.set_default_args() + data['enabled'] = False + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + current = self.set_default_current() + modify_snapshot.assert_called_with(current) + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('snapshot_policy_info_policy_disabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy.NetAppOntapSnapshotPolicy.modify_snapshot_policy') + def test_successful_enable_policy(self, modify_snapshot): + ''' enabling snapshot policy and testing idempotency ''' + data = self.set_default_args() + data['enabled'] = True + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('snapshot_policy_info_policy_disabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + current = self.set_default_current() + current['enabled'] = False + modify_snapshot.assert_called_with(current) + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy.NetAppOntapSnapshotPolicy.modify_snapshot_policy') + def test_successful_modify_schedules_add(self, modify_snapshot): + ''' adding snapshot policy schedules and testing idempotency ''' + data = self.set_default_args() + data['schedule'] = ['hourly', 'daily', 'weekly'] + data['prefix'] = ['hourly', 'daily', 'weekly'] + data['count'] = [100, 5, 10] + data['snapmirror_label'] = ['', 'daily', ''] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + current = self.set_default_current() + modify_snapshot.assert_called_with(current) + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('snapshot_policy_info_schedules_added') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy.NetAppOntapSnapshotPolicy.modify_snapshot_policy') + def test_successful_modify_schedules_delete(self, modify_snapshot): + ''' deleting snapshot policy schedules and testing idempotency ''' + data = self.set_default_args() + data['schedule'] = ['daily'] + data['prefix'] = ['daily'] + data['count'] = [5] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + current = self.set_default_current() + modify_snapshot.assert_called_with(current) + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('snapshot_policy_info_schedules_deleted') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy.NetAppOntapSnapshotPolicy.modify_snapshot_policy') + def test_successful_modify_schedules(self, modify_snapshot): + ''' modifying snapshot policy schedule counts and testing idempotency ''' + data = self.set_default_args() + data['schedule'] = ['hourly', 'daily', 'weekly'] + data['count'] = [10, 50, 100] + data['prefix'] = ['hourly', 'daily', 'weekly'] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + current = self.set_default_current() + modify_snapshot.assert_called_with(current) + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('snapshot_policy_info_modified_schedule_counts') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy.NetAppOntapSnapshotPolicy.delete_snapshot_policy') + def test_successful_delete(self, delete_snapshot): + ''' deleting snapshot policy and testing idempotency ''' + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('policy') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + delete_snapshot.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_valid_schedule_count(self): + ''' validate when schedule has same number of elements ''' + data = self.set_default_args() + data['schedule'] = ['hourly', 'daily', 'weekly', 'monthly', '5min'] + data['prefix'] = ['hourly', 'daily', 'weekly', 'monthly', '5min'] + data['count'] = [1, 2, 3, 4, 5] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + my_obj.create_snapshot_policy() + create_xml = my_obj.server.xml_in + assert data['count'][2] == int(create_xml['count3']) + assert data['schedule'][4] == create_xml['schedule5'] + + def test_valid_schedule_count_with_snapmirror_labels(self): + ''' validate when schedule has same number of elements with snapmirror labels ''' + data = self.set_default_args() + data['schedule'] = ['hourly', 'daily', 'weekly', 'monthly', '5min'] + data['prefix'] = ['hourly', 'daily', 'weekly', 'monthly', '5min'] + data['count'] = [1, 2, 3, 4, 5] + data['snapmirror_label'] = ['hourly', 'daily', 'weekly', 'monthly', '5min'] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + my_obj.create_snapshot_policy() + create_xml = my_obj.server.xml_in + assert data['count'][2] == int(create_xml['count3']) + assert data['schedule'][4] == create_xml['schedule5'] + assert data['snapmirror_label'][3] == create_xml['snapmirror-label4'] + + def test_invalid_params(self): + ''' validate error when schedule does not have same number of elements ''' + data = self.set_default_args() + data['schedule'] = ['s1', 's2'] + data['prefix'] = ['s1', 's2'] + data['count'] = [1, 2, 3] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_snapshot_policy() + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + assert exc.value.args[0]['msg'] == msg + + def test_invalid_schedule_count(self): + ''' validate error when schedule has more than 5 elements ''' + data = self.set_default_args() + data['schedule'] = ['s1', 's2', 's3', 's4', 's5', 's6'] + data['count'] = [1, 2, 3, 4, 5, 6] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_snapshot_policy() + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + assert exc.value.args[0]['msg'] == msg + + def test_invalid_schedule_count_less_than_one(self): + ''' validate error when schedule has less than 1 element ''' + data = self.set_default_args() + data['schedule'] = [] + data['count'] = [] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_snapshot_policy() + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + assert exc.value.args[0]['msg'] == msg + + def test_invalid_schedule_count_is_none(self): + ''' validate error when schedule is None ''' + data = self.set_default_args() + data['schedule'] = None + data['count'] = None + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_snapshot_policy() + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + assert exc.value.args[0]['msg'] == msg + + def test_invalid_schedule_count_with_snapmirror_labels(self): + ''' validate error when schedule with snapmirror labels does not have same number of elements ''' + data = self.set_default_args() + data['schedule'] = ['s1', 's2', 's3'] + data['count'] = [1, 2, 3] + data['snapmirror_label'] = ['sm1', 'sm2'] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_snapshot_policy() + msg = 'Error: Each Snapshot Policy schedule must have an accompanying SnapMirror Label' + assert exc.value.args[0]['msg'] == msg + + def test_invalid_schedule_count_with_prefixes(self): + ''' validate error when schedule with prefixes does not have same number of elements ''' + data = self.set_default_args() + data['schedule'] = ['s1', 's2', 's3'] + data['count'] = [1, 2, 3] + data['prefix'] = ['s1', 's2'] + set_module_args(data) + my_obj = my_module() + my_obj.asup_log_for_cserver = Mock(return_value=None) + if not self.onbox: + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_snapshot_policy() + msg = 'Error: Each Snapshot Policy schedule must have an accompanying prefix' + assert exc.value.args[0]['msg'] == msg + + def test_if_all_methods_catch_exception(self): + module_args = {} + module_args.update(self.set_default_args()) + set_module_args(module_args) + my_obj = my_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('policy_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.create_snapshot_policy() + assert 'Error creating snapshot policy ansible:' in exc.value.args[0]['msg'] + with pytest.raises(AnsibleFailJson) as exc: + my_obj.delete_snapshot_policy() + assert 'Error deleting snapshot policy ansible:' in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy_rest.py new file mode 100644 index 000000000..b79507759 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snapshot_policy_rest.py @@ -0,0 +1,481 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_snapshot_policy """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snapshot_policy \ + import NetAppOntapSnapshotPolicy as my_module + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +SRR = rest_responses({ + 'snapshot_record': (200, {"records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "comment": "modified comment", + "enabled": True, + "name": "policy_name", + "copies": [ + { + "count": 10, + "schedule": { + "name": "hourly" + }, + "prefix": 'hourly', + "snapmirror_label": '' + }, + { + "count": 30, + "schedule": { + "name": "weekly" + }, + "prefix": 'weekly', + "snapmirror_label": '' + } + ], + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + } + ], + "num_records": 1 + }, None), + 'schedule_record': (200, {"records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "comment": "modified comment", + "enabled": 'true', + "name": "policy_name", + "count": 10, + "prefix": "hourly", + "snapmirror_label": '', + "schedule": { + "name": "hourly", + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa" + }, + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + }, + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "ansibleSVM" + }, + "comment": "modified comment", + "enabled": 'true', + "name": "policy_name", + "count": 30, + "prefix": "weekly", + "snapmirror_label": '', + "schedule": { + "name": "weekly", + "uuid": "671aa46e-11ad-11ec-a267-005056b30dsa" + }, + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + } + ], "num_records": 2}, None), +}) + + +ARGS_REST = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'policy_name', + 'vserver': 'ansibleSVM', + 'enabled': True, + 'count': [10, 30], + 'schedule': "hourly,weekly", + 'comment': 'modified comment', + 'use_rest': 'always' +} + +ARGS_REST_no_SVM = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'policy_name', + 'enabled': True, + 'count': [10, 30], + 'schedule': "hourly,weekly", + 'comment': 'modified comment', + 'use_rest': 'always' +} + + +def test_error_get_snapshot_policy_rest(): + ''' Test get error with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on fetching snapshot policy:' in error + + +def test_error_get_snapshot_schedule_rest(): + ''' Test get error with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('PATCH', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_good']), + ('GET', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['generic_error']) + ]) + module_args = { + 'enabled': False, + 'comment': 'testing policy', + 'name': 'policy2' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on fetching snapshot schedule:' in error + + +def test_module_error_ontap_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + module_args = {'use_rest': 'always'} + msg = create_module(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error: REST requires ONTAP 9.8 or later for snapshot schedules.' == msg + + +def test_create_snapshot_polciy_rest(): + ''' Test create with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['empty_records']), + ('POST', 'storage/snapshot-policies', SRR['empty_good']), + ]) + assert create_and_apply(my_module, ARGS_REST) + + +def test_create_snapshot_polciy_with_snapmirror_label_rest(): + ''' Test create with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['empty_records']), + ('POST', 'storage/snapshot-policies', SRR['empty_good']), + ]) + module_args = { + "snapmirror_label": ['hourly', 'weekly'] + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_create_snapshot_polciy_with_prefix_rest(): + ''' Test create with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['empty_records']), + ('POST', 'storage/snapshot-policies', SRR['empty_good']), + ]) + module_args = { + "prefix": ['', ''] + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_error_create_snapshot_polciy_rest(): + ''' Test error create with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['empty_records']), + ('POST', 'storage/snapshot-policies', SRR['generic_error']), + ]) + error = create_and_apply(my_module, ARGS_REST, fail=True)['msg'] + assert 'Error on creating snapshot policy:' in error + + +def test_delete_snapshot_policy_rest(): + ''' Test delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('DELETE', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_error_delete_snapshot_policy_rest(): + ''' Test error delete with rest API''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('DELETE', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['generic_error']), + ]) + module_args = { + 'state': 'absent' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on deleting snapshot policy:' in error + + +def test_modify_snapshot_policy_rest(): + ''' Test modify comment, rename and disable policy with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('PATCH', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_good']), + ('GET', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['schedule_record']) + ]) + module_args = { + 'enabled': False, + 'comment': 'testing policy', + 'name': 'policy2' + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_error_modify_snapshot_policy_rest(): + ''' Neagtive test - modify snapshot policy with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('PATCH', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['generic_error']), + ]) + module_args = { + 'enabled': 'no' + } + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert 'Error on modifying snapshot policy:' in error + + +def test_modify_snapshot_schedule_rest(): + ''' Test modify snapshot schedule and disable policy with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('PATCH', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412', SRR['empty_good']), + ('GET', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['schedule_record']), + ('PATCH', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules/671aa46e-11ad-11ec-a267-005056b30dsa', SRR['empty_good']) + ]) + module_args = { + "enabled": False, + "count": ['10', '20'], + "schedule": ['hourly', 'weekly'] + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_modify_snapshot_schedule_count_label_rest(): + ''' Test modify snapmirror_label and count with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('GET', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['schedule_record']), + ('PATCH', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules/671aa46e-11ad-11ec-a267-005056b30dsa', SRR['empty_good']) + ]) + module_args = { + "snapmirror_label": ['', 'weekly'], + "count": [10, 20], + "schedule": ['hourly', 'weekly'] + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_modify_snapshot_schedule_count_rest(): + ''' Test modify snapshot count with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('GET', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['schedule_record']), + ('PATCH', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules/671aa46e-11ad-11ec-a267-005056b30dsa', SRR['empty_good']) + ]) + module_args = { + "count": "10,40", + "schedule": ['hourly', 'weekly'], + "snapmirror_label": ['', ''] + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_modify_snapshot_count_rest(): + ''' Test modify snapshot count, snapmirror_label and prefix with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('GET', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['schedule_record']), + ('PATCH', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']) + ]) + module_args = { + "count": "20,30", + "schedule": ['hourly', 'weekly'], + "snapmirror_label": ['hourly', ''], + "prefix": ['', 'weekly'] + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_delete_snapshot_schedule_rest(): + ''' Test delete snapshot schedule with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('GET', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['schedule_record']), + ('DELETE', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']) + ]) + module_args = { + "count": 30, + "schedule": ['weekly'] + } + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_delete_all_snapshot_schedule_rest(): + ''' Validate deleting all snapshot schedule with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']) + ]) + module_args = { + "count": [], + "schedule": [] + } + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert msg in error + + +def test_add_snapshot_schedule_rest(): + ''' Test modify by adding schedule to a snapshot with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']), + ('GET', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['schedule_record']), + ('POST', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['empty_good']), + ('POST', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['success']), + ('POST', 'storage/snapshot-policies/1cd8a442-86d1-11e0-ae1c-123478563412/schedules', SRR['success']) + ]) + module_args = { + "count": "10,30,20,1,2", + "schedule": ['hourly', 'weekly', 'daily', 'monthly', '5min'], + "snapmirror_label": ['', '', '', '', '']} + assert create_and_apply(my_module, ARGS_REST, module_args) + + +def test_add_max_snapshot_schedule_rest(): + ''' Test modify by adding more than maximum number of schedule to a snapshot with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'storage/snapshot-policies', SRR['snapshot_record']) + ]) + module_args = { + "count": "10,30,20,1,2,3", + "schedule": ['hourly', 'weekly', 'daily', 'monthly', '5min', '10min'], + "snapmirror_label": ['', '', '', '', '', '']} + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + error = create_and_apply(my_module, ARGS_REST, module_args, fail=True)['msg'] + assert msg in error + + +def test_invalid_count_rest(): + ''' Test invalid count for a schedule with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + current = { + 'schedule': 'weekly', + 'count': []} + my_module_object = create_module(my_module, ARGS_REST, current) + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + assert msg in expect_and_capture_ansible_exception(my_module_object.validate_parameters, 'fail')['msg'] + + +def test_validate_schedule_count_with_snapmirror_labels_rest(): + ''' validate when schedule has same number of elements with snapmirror labels with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + current = { + 'schedule': ['hourly', 'daily', 'weekly', 'monthly', '5min'], + 'snapmirror_label': ['', '', ''], + 'count': [1, 2, 3, 4, 5]} + my_module_object = create_module(my_module, ARGS_REST, current) + msg = "Error: Each Snapshot Policy schedule must have an accompanying SnapMirror Label" + assert msg in expect_and_capture_ansible_exception(my_module_object.validate_parameters, 'fail')['msg'] + + +def test_validate_schedule_count_with_prefix_rest(): + ''' validate when schedule has same number of elements with prefix with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + current = { + 'schedule': ['hourly', 'daily', 'weekly', 'monthly', '5min'], + 'prefix': ['hourly', 'daily', 'weekly'], + 'count': [1, 2, 3, 4, 5]} + my_module_object = create_module(my_module, ARGS_REST, current) + msg = "Error: Each Snapshot Policy schedule must have an accompanying prefix" + assert msg in expect_and_capture_ansible_exception(my_module_object.validate_parameters, 'fail')['msg'] + + +def test_validate_schedule_count_max_rest(): + ''' Validate maximum number of snapshot schedule and count with REST API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + current = { + 'schedule': ['hourly', 'daily', 'weekly', 'monthly', '5min', '10min'], + 'count': [1, 2, 3, 4, 5, 6]} + my_module_object = create_module(my_module, ARGS_REST, current) + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + assert msg in expect_and_capture_ansible_exception(my_module_object.validate_parameters, 'fail')['msg'] + + +def test_invalid_count_number_rest(): + ''' validate when schedule has same number of elements with count with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + current = { + 'schedule': ['hourly', 'daily', 'weekly'], + 'count': [1, 2, 3, 4, 5, 6] + } + my_module_object = create_module(my_module, ARGS_REST, current) + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + assert msg in expect_and_capture_ansible_exception(my_module_object.validate_parameters, 'fail')['msg'] + + +def test_invalid_schedule_count_rest(): + ''' validate invalid number of schedule and count with rest API ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + current = { + 'schedule': [], + 'count': []} + my_module_object = create_module(my_module, ARGS_REST, current) + msg = 'Error: A Snapshot policy must have at least 1 ' \ + 'schedule and can have up to a maximum of 5 schedules, with a count ' \ + 'representing the maximum number of Snapshot copies for each schedule' + assert msg in expect_and_capture_ansible_exception(my_module_object.validate_parameters, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp.py new file mode 100644 index 000000000..24d8c5da4 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp.py @@ -0,0 +1,158 @@ +# (c) 2018-2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP snmp Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import assert_no_warnings, set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snmp \ + import NetAppONTAPSnmp as my_module, main as uut_main # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def default_args(): + args = { + 'state': 'present', + 'hostname': '10.10.10.10', + 'username': 'admin', + 'https': 'true', + 'validate_certs': 'false', + 'password': 'password', + 'use_rest': 'always' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'community_user_record': (200, { + 'records': [{ + "name": "snmpv3user2", + "authentication_method": "community", + 'engine_id': "80000315058e02057c0fb8e911bc9f005056bb942e" + }], + 'num_records': 1 + }, None), + 'snmp_user_record': (200, { + 'records': [{ + "name": "snmpv3user3", + "authentication_method": "usm", + 'engine_id': "80000315058e02057c0fb8e911bc9f005056bb942e" + }], + 'num_records': 1 + }, None), +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args(dict(hostname='')) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + msg = 'missing required arguments: community_name' + assert msg == exc.value.args[0]['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_get_community_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['community_name'] = 'snmpv3user2' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['community_user_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_create_community_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['community_name'] = 'snmpv3user2' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['empty_good'], # create + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_community_called(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['community_name'] = 'snmpv3user2' + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['community_user_record'], # get + SRR['community_user_record'], + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is True + assert_no_warnings() + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_ensure_delete_community_idempotent(mock_request, patch_ansible): + ''' test get''' + args = dict(default_args()) + args['community_name'] = 'snmpv3user2' + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest_9_8'], # get version + SRR['zero_record'], # get + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + print('Info: %s' % exc.value.args[0]) + assert exc.value.args[0]['changed'] is False + assert_no_warnings() diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp_traphosts.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp_traphosts.py new file mode 100644 index 000000000..43b9624bb --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_snmp_traphosts.py @@ -0,0 +1,153 @@ +# (c) 2020-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_snmp_traphosts """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_snmp_traphosts \ + import NetAppONTAPSnmpTraphosts as traphost_module # module under test + +# REST API canned responses when mocking send_request +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'snmp_record': ( + 200, + { + "records": [ + { + "host": "example.com", + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'host': 'example.com' +} + + +def test_rest_error_get(): + '''Test error rest get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'support/snmp/traphosts', SRR['generic_error']), + ]) + error = create_and_apply(traphost_module, ARGS_REST, fail=True)['msg'] + msg = "Error on fetching snmp traphosts info:" + assert msg in error + + +def test_rest_create(): + '''Test create snmp traphost''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'support/snmp/traphosts', SRR['empty_records']), + ('POST', 'support/snmp/traphosts', SRR['empty_good']), + ]) + assert create_and_apply(traphost_module, ARGS_REST) + + +def test_rest_error_create(): + '''Test error create snmp traphost''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'support/snmp/traphosts', SRR['empty_records']), + ('POST', 'support/snmp/traphosts', SRR['generic_error']), + ]) + error = create_and_apply(traphost_module, ARGS_REST, fail=True)['msg'] + msg = "Error creating traphost:" + assert msg in error + + +def test_rest_delete(): + '''Test delete snmp traphost''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'support/snmp/traphosts', SRR['snmp_record']), + ('DELETE', 'support/snmp/traphosts/example.com', SRR['empty_good']), + ]) + module_args = { + 'state': 'absent' + } + assert create_and_apply(traphost_module, ARGS_REST, module_args) + + +def test_rest_error_delete(): + '''Test error delete snmp traphost''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'support/snmp/traphosts', SRR['snmp_record']), + ('DELETE', 'support/snmp/traphosts/example.com', SRR['generic_error']), + ]) + module_args = { + 'state': 'absent' + } + error = create_and_apply(traphost_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error deleting traphost:" + assert msg in error + + +def test_create_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'support/snmp/traphosts', SRR['snmp_record']) + ]) + module_args = { + 'state': 'present' + } + assert not create_and_apply(traphost_module, ARGS_REST, module_args)['changed'] + + +def test_delete_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'support/snmp/traphosts', SRR['empty_records']) + ]) + module_args = { + 'state': 'absent' + } + assert not create_and_apply(traphost_module, ARGS_REST, module_args)['changed'] + + +def test_ontap_version_rest(): + ''' Test ONTAP version ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_96']), + ]) + module_args = {'use_rest': 'always'} + error = create_module(traphost_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error: na_ontap_snmp_traphosts only supports REST, and requires ONTAP 9.7.0 or later." + assert msg in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_software_update.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_software_update.py new file mode 100644 index 000000000..40bf3e851 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_software_update.py @@ -0,0 +1,1124 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_software_update ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_warning_was_raised, expect_and_capture_ansible_exception, call_main, create_module, create_and_apply, patch_ansible, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import JOB_GET_API, rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, build_zapi_response, zapi_error_message, zapi_responses + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_software_update \ + import NetAppONTAPSoftwareUpdate as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def cluster_image_info(mixed=False): + version1 = 'Fattire__9.3.0' + version2 = version1 + if mixed: + version2 += '.1' + return { + 'num-records': 1, + # composite response, attributes-list for cluster-image-get-iter and attributes for cluster-image-get + 'attributes-list': [ + {'cluster-image-info': { + 'node-id': 'node4test', + 'current-version': version1}}, + {'cluster-image-info': { + 'node-id': 'node4test', + 'current-version': version2}}, + ], + 'attributes': { + 'cluster-image-info': { + 'node-id': 'node4test', + 'current-version': version1 + }}, + } + + +def software_update_info(status): + if status == 'async_pkg_get_phase_complete': + overall_status = 'completed' + elif status == 'async_pkg_get_phase_running': + overall_status = 'in_progress' + else: + overall_status = status + + return { + 'num-records': 1, + # 'attributes-list': {'cluster-image-info': {'node-id': node}}, + 'progress-status': status, + 'progress-details': 'some_details', + 'failure-reason': 'failure_reason', + 'attributes': { + 'ndu-progress-info': { + 'overall-status': overall_status, + 'completed-node-count': '0', + 'validation-reports': [{ + 'validation-report-info': { + 'one_check': 'one', + 'two_check': 'two' + }}]}}, + } + + +cluster_image_validation_report_list = { + 'cluster-image-validation-report-list': [ + {'cluster-image-validation-report-list-info': { + 'required-action': { + 'required-action-info': { + 'action': 'some_action', + 'advice': 'some_advice', + 'error': 'some_error', + } + }, + 'ndu-check': 'ndu_ck', + 'ndu-status': 'ndu_st', + }}, + {'cluster-image-validation-report-list-info': { + 'required-action': { + 'required-action-info': { + 'action': 'other_action', + 'advice': 'other_advice', + 'error': 'other_error', + } + }, + 'ndu-check': 'ndu_ck', + 'ndu-status': 'ndu_st', + }}, + ], +} + + +cluster_image_package_local_info = { + 'attributes-list': [ + {'cluster-image-package-local-info': { + 'package-version': 'Fattire__9.3.0', + + }}, + {'cluster-image-package-local-info': { + 'package-version': 'Fattire__9.3.1', + + }}, + ], +} + + +ZRR = zapi_responses({ + 'cluster_image_info': build_zapi_response(cluster_image_info()), + 'cluster_image_info_mixed': build_zapi_response(cluster_image_info(True)), + 'software_update_info_running': build_zapi_response(software_update_info('async_pkg_get_phase_running')), + 'software_update_info_complete': build_zapi_response(software_update_info('async_pkg_get_phase_complete')), + 'software_update_info_error': build_zapi_response(software_update_info('error')), + 'cluster_image_validation_report_list': build_zapi_response(cluster_image_validation_report_list), + 'cluster_image_package_local_info': build_zapi_response(cluster_image_package_local_info, 2), + 'error_18408': build_zapi_error(18408, 'pkg exists!') +}) + + +def cluster_software_node_info(mixed=False): + version1 = 'Fattire__9.3.0' + version2 = 'GEN_MAJ_min_2' if mixed else version1 + return { + 'nodes': [ + {'name': 'node1', 'version': version1}, + {'name': 'node2', 'version': version2}, + ] + } + + +def cluster_software_state_info(state): + # state: in_progress, completed, ... + return { + 'state': state + } + + +cluster_software_validation_results = { + "validation_results": [{ + "action": { + "message": "Use NFS hard mounts, if possible." + }, + "issue": { + "message": "Cluster HA is not configured in the cluster." + }, + "status": "warning", + "update_check": "nfs_mounts" + }], +} + + +def cluster_software_download_info(state): + return { + "message": "message", + "state": state, + } + + +SRR = rest_responses({ + 'cluster_software_node_info': (200, cluster_software_node_info(), None), + 'cluster_software_node_info_mixed': (200, cluster_software_node_info(True), None), + 'cluster_software_validation_results': (200, cluster_software_validation_results, None), + 'cluster_software_state_completed': (200, cluster_software_state_info('completed'), None), + 'cluster_software_state_in_progress': (200, cluster_software_state_info('in_progress'), None), + 'cluster_software_state_in_error': (200, cluster_software_state_info('in_error'), None), + 'cluster_software_download_state_success': (200, cluster_software_download_info('success'), None), + 'cluster_software_download_state_running': (200, cluster_software_download_info('running'), None), + 'cluster_software_package_info_ft': (200, {'records': [{'version': 'Fattire__9.3.0'}]}, None), + 'cluster_software_package_info_pte': (200, {'records': [{'version': 'PlinyTheElder'}]}, None), + 'error_image_already_exists': (200, {}, 'Package image with the same name already exists'), + 'error_download_in_progress': (200, {}, 'Software get operation already in progress'), +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'package_version': 'Fattire__9.3.0', + 'package_url': 'abc.com', + 'https': 'true', + 'stabilize_minutes': 10 +} + + +@patch('time.sleep') +def test_ensure_apply_for_update_called(dont_sleep): + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-update', ZRR['success']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-package-delete', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_ensure_apply_for_update_called_node(dont_sleep): + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-update', ZRR['success']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-package-delete', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + "nodes": ["node_abc"], + "package_version": "PlinyTheElder", + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_ensure_apply_for_update_called_idempotent(dont_sleep): + # image already installed + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + + ]) + module_args = { + "use_rest": "never", + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_ensure_apply_for_update_called_idempotent_node(dont_sleep): + # image already installed + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get', ZRR['cluster_image_info']), + + ]) + module_args = { + "use_rest": "never", + "nodes": ["node_abc"], + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_ensure_apply_for_update_called_with_validation(dont_sleep): + # for validation before update + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-validate', ZRR['success']), + ('ZAPI', 'cluster-image-update', ZRR['success']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-package-delete', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + "validate_after_download": True, + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_negative_download_error(dont_sleep): + ''' downloading software - error while downloading the image - first request ''' + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['error']), + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + } + error = zapi_error_message('Error downloading cluster image package for abc.com') + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_negative_download_progress_error(dont_sleep): + ''' downloading software - error while downloading the image - progress error ''' + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_error']), + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + } + error = 'Error downloading package: failure_reason' + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_negative_download_progress_error_no_status(dont_sleep): + ''' downloading software - error while downloading the image - progress error ''' + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['success']), # retrying if status cannot be found + ('ZAPI', 'cluster-image-get-download-progress', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_error']), + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + } + error = 'Error downloading package: failure_reason' + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_negative_download_progress_error_fetching_status(dont_sleep): + ''' downloading software - error while downloading the image - progress error ''' + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['error']), + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + } + error = zapi_error_message('Error fetching cluster image package download progress for abc.com') + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_negative_update_error_zapi(dont_sleep): + ''' updating software - error while updating the image ''' + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-update', ZRR['error']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['error']), # additional error details + ('ZAPI', 'cluster-image-validate', ZRR['error']), # additional error details + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + } + error = zapi_error_message('Error updating cluster image for PlinyTheElder') + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_negative_update_error(dont_sleep): + ''' updating software - error while updating the image ''' + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-update', ZRR['success']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['software_update_info_error']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['software_update_info_error']), + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + } + error = 'Error updating image using ZAPI: overall_status: error.' + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_negative_update_error_timeout(dont_sleep): + ''' updating software - error while updating the image ''' + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_running']), + ('ZAPI', 'cluster-image-get-download-progress', ZRR['software_update_info_complete']), + ('ZAPI', 'cluster-image-update', ZRR['success']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['software_update_info_error']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['software_update_info_running']), + ]) + module_args = { + "use_rest": "never", + "package_version": "PlinyTheElder", + } + error = 'Timeout error updating image using ZAPI: overall_status: in_progress. Should the timeout value be increased?'\ + ' Current value is 1800 seconds. The software update continues in background.' + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + module_args = { + "use_rest": "never" + } + assert 'Error: the python NetApp-Lib module is required. Import error: None' == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_fail_with_http(): + args = dict(DEFAULT_ARGS) + args.pop('https') + assert 'Error: https parameter must be True' == call_main(my_main, args, fail=True)['msg'] + + +def test_is_update_required(): + ''' update is required if nodes have different images, or version does not match ''' + register_responses([ + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info_mixed']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info_mixed']), + ]) + module_args = { + "use_rest": "never" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert not my_obj.is_update_required() + assert my_obj.is_update_required() + my_obj.parameters["package_version"] = "PlinyTheElder" + assert my_obj.is_update_required() + assert my_obj.is_update_required() + + +def test_cluster_image_validate(): + ''' check error, then check that reports are read correctly ''' + register_responses([ + ('ZAPI', 'cluster-image-validate', ZRR['error']), + ('ZAPI', 'cluster-image-validate', ZRR['cluster_image_validation_report_list']), + ]) + module_args = { + "use_rest": "never" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.cluster_image_validate() == zapi_error_message('Error running cluster image validate') + reports = my_obj.cluster_image_validate() + assert 'required_action' in reports[0] + assert 'action' in reports[0]['required_action'] + assert reports[0]['required_action']['action'] == 'some_action' + assert reports[1]['required_action']['action'] == 'other_action' + + +def test_cluster_image_zapi_errors(): + ''' ZAPi error on delete ''' + register_responses([ + ('ZAPI', 'cluster-image-get-iter', ZRR['error']), + ('ZAPI', 'cluster-image-get', ZRR['error']), + ('ZAPI', 'cluster-image-package-delete', ZRR['error']), + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['error']), + ]) + module_args = { + "use_rest": "never" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert expect_and_capture_ansible_exception(my_obj.cluster_image_get_versions, 'fail')['msg'] ==\ + zapi_error_message('Error fetching cluster image details: Fattire__9.3.0') + assert expect_and_capture_ansible_exception(my_obj.cluster_image_get_for_node, 'fail', 'node')['msg'] ==\ + zapi_error_message('Error fetching cluster image details for node') + assert expect_and_capture_ansible_exception(my_obj.cluster_image_package_delete, 'fail')['msg'] ==\ + zapi_error_message('Error deleting cluster image package for Fattire__9.3.0') + assert expect_and_capture_ansible_exception(my_obj.cluster_image_packages_get_zapi, 'fail')['msg'] ==\ + zapi_error_message('Error getting list of local packages') + + +def test_cluster_image_get_for_node_none_none(): + ''' empty response on get ''' + register_responses([ + ('ZAPI', 'cluster-image-get', ZRR['success']), + ]) + module_args = { + "use_rest": "never" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.cluster_image_get_for_node('node') == (None, None) + + +def test_cluster_image_package_download(): + ''' ZAPI error on download - package already exists''' + register_responses([ + ('ZAPI', 'cluster-image-package-download', ZRR['error']), + ('ZAPI', 'cluster-image-package-download', ZRR['error_18408']), + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['cluster_image_package_local_info']), + ('ZAPI', 'cluster-image-package-download', ZRR['success']), + ]) + module_args = { + "use_rest": "never" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert expect_and_capture_ansible_exception(my_obj.cluster_image_package_download, 'fail')['msg'] ==\ + zapi_error_message('Error downloading cluster image package for abc.com') + assert my_obj.cluster_image_package_download() + assert not my_obj.cluster_image_package_download() + + +def test_cluster_image_update_progress_get_error(): + ''' ZAPI error on progress get ''' + register_responses([ + ('ZAPI', 'cluster-image-update-progress-info', ZRR['error']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['error']), + ('ZAPI', 'cluster-image-update-progress-info', ZRR['error']), + ]) + module_args = { + "use_rest": "never" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert expect_and_capture_ansible_exception(my_obj.cluster_image_update_progress_get, 'fail', ignore_connection_error=False)['msg'] ==\ + zapi_error_message('Error fetching cluster image update progress details') + assert my_obj.cluster_image_update_progress_get() == {} + assert my_obj.cluster_image_update_progress_get(ignore_connection_error=True) == {} + + +def test_delete_package_zapi(): + # deleting a package + register_responses([ + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['cluster_image_package_local_info']), + ('ZAPI', 'cluster-image-package-delete', ZRR['success']), + # idempotency + ('ZAPI', 'cluster-image-package-local-get-iter', ZRR['no_records']), + ]) + module_args = { + "use_rest": "never", + "state": "absent", + "package_version": "Fattire__9.3.0", + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +# REST tests + +@patch('time.sleep') +def test_rest_ensure_apply_for_update_called(dont_sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success_with_job_uuid']), + ('GET', JOB_GET_API, SRR['job_generic_response_running']), + ('GET', JOB_GET_API, SRR['job_generic_response_running']), + ('GET', JOB_GET_API, SRR['generic_error']), + ('GET', JOB_GET_API, SRR['job_generic_response_success']), + ('PATCH', 'cluster/software', SRR['success_with_job_uuid']), + ('GET', JOB_GET_API, SRR['job_generic_response_running']), + ('GET', JOB_GET_API, SRR['generic_error']), + ('GET', JOB_GET_API, SRR['job_generic_response_running']), + ('GET', JOB_GET_API, SRR['job_generic_response_success']), + ('GET', 'cluster/software', SRR['cluster_software_state_in_progress']), + ('GET', 'cluster/software', SRR['cluster_software_state_in_progress']), + ('GET', 'cluster/software', SRR['cluster_software_state_in_progress']), + ('GET', 'cluster/software', SRR['cluster_software_state_completed']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + ('DELETE', 'cluster/software/packages/PlinyTheElder', SRR['success_with_job_uuid']), + ('GET', JOB_GET_API, SRR['job_generic_response_running']), + ('GET', JOB_GET_API, SRR['job_generic_response_running']), + ('GET', JOB_GET_API, SRR['job_generic_response_success']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_ensure_apply_for_update_called_idempotent(dont_sleep): + # image already installed + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + + ]) + module_args = { + "use_rest": "always", + } + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_ensure_apply_for_update_called_with_validation(dont_sleep): + # for validation before update + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success']), + ('PATCH', 'cluster/software', SRR['success']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + ('PATCH', 'cluster/software', SRR['success']), + ('GET', 'cluster/software', SRR['cluster_software_state_in_progress']), + ('GET', 'cluster/software', SRR['cluster_software_state_in_progress']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['cluster_software_state_in_progress']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['cluster_software_state_completed']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + ('DELETE', 'cluster/software/packages/PlinyTheElder', SRR['success']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "validate_after_download": True, + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_download_idempotent_package_already_exist_pre(dont_sleep): + ''' downloading software - package already present before attempting download ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['cluster_software_package_info_pte']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "download_only": True, + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_download_idempotent_package_already_exist_post(dont_sleep): + ''' downloading software - package already present when attempting download ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['error_image_already_exists']), + ('GET', 'cluster/software/packages', SRR['cluster_software_package_info_pte']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "download_only": True, + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_download_already_in_progress(dont_sleep): + ''' downloading software - package already present when attempting download ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['error_download_in_progress']), + ('GET', 'cluster/software/download', SRR['cluster_software_download_state_running']), + ('GET', 'cluster/software/download', SRR['generic_error']), + ('GET', 'cluster/software/download', SRR['generic_error']), + ('GET', 'cluster/software/download', SRR['cluster_software_download_state_running']), + ('GET', 'cluster/software/download', SRR['cluster_software_download_state_success']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "download_only": True, + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_negative_download_package_already_exist(dont_sleep): + ''' downloading software - error while downloading the image - first request ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['error_image_already_exists']), + ('GET', 'cluster/software/packages', SRR['cluster_software_package_info_ft']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "download_only": True, + } + error = 'Error: another package with the same file name exists: found: Fattire__9.3.0' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_rest_negative_download_error(dont_sleep): + ''' downloading software - error while downloading the image - first request ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + } + error = rest_error_message('Error downloading software', 'cluster/software/download', ' - current versions:') + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_rest_negative_download_progress_error(dont_sleep): + ''' downloading software - error while downloading the image - progress error ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success_with_job_uuid']), + ('GET', JOB_GET_API, SRR['job_generic_response_running']), + ('GET', JOB_GET_API, SRR['job_generic_response_running']), + ('GET', JOB_GET_API, SRR['job_generic_response_failure']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + } + error = 'Error downloading software: job reported error: job reported failure, received' + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_rest_negative_update_error_sync(dont_sleep): + ''' updating software - error while updating the image ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success']), + ('PATCH', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + # second error on validate results + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success']), + ('PATCH', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + } + error = rest_error_message('Error updating software', 'cluster/software') + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + assert 'validation results:' in msg + assert "'issue': {'message': 'Cluster HA is not configured in the cluster.'}" in msg + # seconnd error on validate results + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + assert 'validation results:' in msg + assert 'validation results: Error fetching software information for validation_results:' in msg + + +@patch('time.sleep') +def test_rest_negative_update_error_waiting_for_state(dont_sleep): + ''' updating software - error while updating the image ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success']), + ('PATCH', 'cluster/software', SRR['success']), + ('GET', 'cluster/software', SRR['cluster_software_state_in_progress']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + # over 20 consecutive errors + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success']), + ('PATCH', 'cluster/software', SRR['success']), + ('GET', 'cluster/software', SRR['cluster_software_state_in_progress']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "timeout": 240 + } + error = rest_error_message('Error: unable to read image update state, using timeout 240. ' + 'Last error: Error fetching software information for state', 'cluster/software') + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + assert 'All errors:' in msg + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "timeout": 1800 + } + # stop after 20 errors + error = rest_error_message('Error: unable to read image update state, using timeout 1800. ' + 'Last error: Error fetching software information for state', 'cluster/software') + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + assert 'All errors:' in msg + + +@patch('time.sleep') +def test_rest_negative_update_error_job_errors(dont_sleep): + ''' updating software - error while updating the image ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success']), + ('PATCH', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + # second error on validate results + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('POST', 'cluster/software/download', SRR['success']), + ('PATCH', 'cluster/software', SRR['generic_error']), + ('GET', 'cluster/software', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + } + error = rest_error_message('Error updating software', 'cluster/software') + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + assert 'validation results:' in msg + assert "'issue': {'message': 'Cluster HA is not configured in the cluster.'}" in msg + # seconnd error on validate results + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + assert 'validation results:' in msg + assert 'validation results: Error fetching software information for validation_results:' in msg + + +def test_rest_is_update_required(): + ''' update is required if nodes have different images, or version does not match ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('GET', 'cluster/software', SRR['cluster_software_node_info_mixed']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('GET', 'cluster/software', SRR['cluster_software_node_info_mixed']), + ]) + module_args = { + "use_rest": "always" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert not my_obj.is_update_required() + assert my_obj.is_update_required() + my_obj.parameters["package_version"] = "PlinyTheElder" + assert my_obj.is_update_required() + assert my_obj.is_update_required() + + +@patch('time.sleep') +def test_rest_cluster_image_validate(dont_sleep): + ''' check error, then check that reports are read correctly ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('PATCH', 'cluster/software', SRR['generic_error']), + ('PATCH', 'cluster/software', SRR['success']), + ('GET', 'cluster/software', SRR['zero_records']), # retried as validation_results is not present - empty record + ('GET', 'cluster/software', SRR['cluster_software_node_info']), # retried as validation_results is not present - other keys + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + ]) + module_args = { + "use_rest": "always" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.cluster_image_validate() == rest_error_message('Error validating software', 'cluster/software') + reports = my_obj.cluster_image_validate() + assert 'action' in reports[0] + assert 'issue' in reports[0] + + +def test_rest_cluster_image_errors(): + ''' REST error on get and delete ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software', SRR['generic_error']), + ('DELETE', 'cluster/software/packages/Fattire__9.3.0', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert expect_and_capture_ansible_exception(my_obj.cluster_image_get_versions, 'fail')['msg'] ==\ + rest_error_message('Error fetching software information for nodes', 'cluster/software') + assert expect_and_capture_ansible_exception(my_obj.cluster_image_package_delete, 'fail')['msg'] ==\ + rest_error_message('Error deleting cluster software package for Fattire__9.3.0', 'cluster/software/packages/Fattire__9.3.0') + + +def test_rest_cluster_image_get_for_node_versions(): + ''' getting nodes versions ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ('GET', 'cluster/software', SRR['cluster_software_node_info']), + ]) + module_args = { + "use_rest": "always" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.cluster_image_get_rest('versions') == [('node1', 'Fattire__9.3.0'), ('node2', 'Fattire__9.3.0')] + my_obj.parameters['nodes'] = ['node1'] + assert my_obj.cluster_image_get_rest('versions') == [('node1', 'Fattire__9.3.0')] + my_obj.parameters['nodes'] = ['node2'] + assert my_obj.cluster_image_get_rest('versions') == [('node2', 'Fattire__9.3.0')] + my_obj.parameters['nodes'] = ['node2', 'node3'] + error = 'Error: node not found in cluster: node3.' + assert expect_and_capture_ansible_exception(my_obj.cluster_image_get_rest, 'fail', 'versions')['msg'] == error + my_obj.parameters['nodes'] = ['node4', 'node3'] + error = 'Error: nodes not found in cluster: node4, node3.' + assert expect_and_capture_ansible_exception(my_obj.cluster_image_get_rest, 'fail', 'versions')['msg'] == error + + +def test_rest_negative_cluster_image_get_for_node_versions(): + ''' getting nodes versions ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software', SRR['zero_records']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + ]) + module_args = { + "use_rest": "always" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "Error fetching software information for nodes: no record calling cluster/software" + assert error in expect_and_capture_ansible_exception(my_obj.cluster_image_get_rest, 'fail', 'versions')['msg'] + error = "Unexpected results for what: versions, record: {'validation_results':" + assert error in expect_and_capture_ansible_exception(my_obj.cluster_image_get_rest, 'fail', 'versions')['msg'] + + +def test_rest_cluster_image_package_download(): + ''' download error, download error indicating package exists, successful download ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('POST', 'cluster/software/download', SRR['generic_error']), + ('POST', 'cluster/software/download', SRR['error_image_already_exists']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ('POST', 'cluster/software/download', SRR['success']), + ]) + module_args = { + "use_rest": "always" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = rest_error_message('Error downloading software', 'cluster/software/download', " - current versions: ['not available with force_update']") + assert error in expect_and_capture_ansible_exception(my_obj.download_software_rest, 'fail')['msg'] + error = 'Error: ONTAP reported package already exists, but no package found: ' + assert error in expect_and_capture_ansible_exception(my_obj.download_software_rest, 'fail')['msg'] + assert not my_obj.download_software_rest() + + +def test_rest_post_update_tasks(): + ''' validate success and error messages ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + ('DELETE', 'cluster/software/packages/Fattire__9.3.0', SRR['success']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + ('GET', 'cluster/software', SRR['cluster_software_validation_results']), + ]) + module_args = { + "use_rest": "always" + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.post_update_tasks_rest('completed') == cluster_software_validation_results['validation_results'] + # time out + error = 'Timeout error updating image using REST: state: in_progress.' + assert error in expect_and_capture_ansible_exception(my_obj.post_update_tasks_rest, 'fail', 'in_progress')['msg'] + # other state + error = 'Error updating image using REST: state: error_state.' + assert error in expect_and_capture_ansible_exception(my_obj.post_update_tasks_rest, 'fail', 'error_state')['msg'] + + +def test_rest_delete_package(): + ''' deleting package ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['cluster_software_package_info_pte']), + ('DELETE', 'cluster/software/packages/PlinyTheElder', SRR['success']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['cluster_software_package_info_ft']), + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "state": "absent", + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_negative_delete_package(): + ''' deleting package ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['generic_error']), + # idempotency + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster/software/packages', SRR['cluster_software_package_info_pte']), + ('DELETE', 'cluster/software/packages/PlinyTheElder', SRR['generic_error']) + ]) + module_args = { + "use_rest": "always", + "package_version": "PlinyTheElder", + "state": "absent", + } + error = rest_error_message('Error: unable to fetch local package list', 'cluster/software/packages') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + error = rest_error_message('Error deleting cluster software package for PlinyTheElder', 'cluster/software/packages/PlinyTheElder') + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_partially_supported_options(): + ''' validate success and error messages ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + "use_rest": "always", + } + error = 'Minimum version of ONTAP for stabilize_minutes is (9, 8)' + assert error in create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert create_module(my_module, DEFAULT_ARGS, module_args) + module_args = { + "use_rest": "always", + "nodes": "node1" + } + error = 'Minimum version of ONTAP for nodes is (9, 9)' + assert error in create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args = { + "use_rest": "auto", + "nodes": "node1" + } + assert create_module(my_module, DEFAULT_ARGS, module_args) + print_warnings + assert_warning_was_raised('Falling back to ZAPI because of unsupported option(s) or option value(s) "nodes" in REST require (9, 9)') + + +def test_missing_arg(): + args = dict(DEFAULT_ARGS) + args.pop('package_url') + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster/software/packages', SRR['zero_records']), + ]) + module_args = { + "use_rest": "always", + } + error = 'Error: packague_url is a required parameter to download the software package.' + assert error in call_main(my_main, args, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_storage_auto_giveback.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_storage_auto_giveback.py new file mode 100644 index 000000000..3c6d345c1 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_storage_auto_giveback.py @@ -0,0 +1,320 @@ +''' unit tests ONTAP Ansible module: na_ontap_storage_auto_giveback ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch, Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_auto_giveback \ + import NetAppOntapStorageAutoGiveback as storage_auto_giveback_module # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'storage_auto_giveback_enabled_record': (200, { + 'num_records': 1, + 'records': [{ + 'node': 'node1', + 'auto_giveback': True, + 'auto_giveback_after_panic': True + }] + }, None), + 'storage_auto_giveback_disabled_record': (200, { + 'num_records': 1, + "records": [{ + 'node': 'node1', + 'auto_giveback': False, + 'auto_giveback_after_panic': False + }] + }, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'auto_giveback_enabled': + xml = self.build_storage_auto_giveback_enabled_info() + elif self.type == 'auto_giveback_disabled': + xml = self.build_storage_auto_giveback_disabled_info() + elif self.type == 'auto_giveback_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_storage_auto_giveback_enabled_info(): + ''' build xml data for cf-get-iter ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'num-records': 1, + 'attributes-list': { + 'storage-failover-info': { + 'sfo-node-info': { + 'node-related-info': { + 'node': 'node1' + } + }, + 'sfo-options-info': { + 'options-related-info': { + 'auto-giveback-enabled': 'true', + 'sfo-giveback-options-info': { + 'giveback-options': { + 'auto-giveback-after-panic-enabled': 'true' + } + } + } + } + } + } + } + + xml.translate_struct(data) + return xml + + @staticmethod + def build_storage_auto_giveback_disabled_info(): + ''' build xml data for cf-get-iter ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'num-records': 1, + 'attributes-list': { + 'storage-failover-info': { + 'sfo-node-info': { + 'node-related-info': { + 'node': 'node1' + } + }, + 'sfo-options-info': { + 'options-related-info': { + 'auto-giveback-enabled': 'false', + 'sfo-giveback-options-info': { + 'giveback-options': { + 'auto-giveback-after-panic-enabled': 'false' + } + } + } + } + } + } + } + + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self, use_rest=None): + if self.onbox: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + name = 'node1' + auto_giveback_enabled = True + auto_giveback_after_panic_enabled = True + else: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + name = 'node1' + auto_giveback_enabled = True + auto_giveback_after_panic_enabled = True + + args = dict({ + 'hostname': hostname, + 'username': username, + 'password': password, + 'name': name, + 'auto_giveback_enabled': auto_giveback_enabled, + 'auto_giveback_after_panic_enabled': auto_giveback_after_panic_enabled + }) + + if use_rest is not None: + args['use_rest'] = use_rest + + return args + + @staticmethod + def get_storage_auto_giveback_mock_object(cx_type='zapi', kind=None): + storage_auto_giveback_obj = storage_auto_giveback_module() + if cx_type == 'zapi': + if kind is None: + storage_auto_giveback_obj.server = MockONTAPConnection() + else: + storage_auto_giveback_obj.server = MockONTAPConnection(kind=kind) + return storage_auto_giveback_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + storage_auto_giveback_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called_existing(self): + ''' test get_storage_auto_giveback for existing config ''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = storage_auto_giveback_module() + my_obj.server = MockONTAPConnection(kind='auto_giveback_enabled') + assert my_obj.get_storage_auto_giveback() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_auto_giveback.NetAppOntapStorageAutoGiveback.modify_storage_auto_giveback') + def test_successful_enable(self, modify_storage_auto_giveback): + ''' enable storage_auto_giveback and testing idempotency ''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = storage_auto_giveback_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('auto_giveback_disabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + modify_storage_auto_giveback.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = storage_auto_giveback_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('auto_giveback_enabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_auto_giveback.NetAppOntapStorageAutoGiveback.modify_storage_auto_giveback') + def test_successful_disable(self, modify_storage_auto_giveback): + ''' disable storage_auto_giveback and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['auto_giveback_enabled'] = False + data['auto_giveback_after_panic_enabled'] = False + set_module_args(data) + my_obj = storage_auto_giveback_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('auto_giveback_enabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + # modify_storage_auto_giveback.assert_called_with() + # to reset na_helper from remembering the previous 'changed' value + data = self.set_default_args(use_rest='Never') + data['auto_giveback_enabled'] = False + data['auto_giveback_after_panic_enabled'] = False + set_module_args(data) + my_obj = storage_auto_giveback_module() + my_obj.ems_log_event = Mock(return_value=None) + if not self.onbox: + my_obj.server = MockONTAPConnection('auto_giveback_disabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + data = self.set_default_args(use_rest='Never') + set_module_args(data) + my_obj = storage_auto_giveback_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('auto_giveback_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_storage_auto_giveback() + assert 'Error modifying auto giveback' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_storage_auto_giveback_mock_object(cx_type='rest').apply() + assert SRR['generic_error'][2] in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_enabled_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['storage_auto_giveback_disabled_record'], # get + SRR['empty_good'], # patch + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_storage_auto_giveback_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_enabled_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['storage_auto_giveback_enabled_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_storage_auto_giveback_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_disabled_rest(self, mock_request): + data = self.set_default_args() + data['auto_giveback_enabled'] = False + data['auto_giveback_after_panic_enabled'] = False + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['storage_auto_giveback_enabled_record'], # get + SRR['empty_good'], # patch + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_storage_auto_giveback_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_disabled_rest(self, mock_request): + data = self.set_default_args() + data['auto_giveback_enabled'] = False + data['auto_giveback_after_panic_enabled'] = False + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['storage_auto_giveback_disabled_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_storage_auto_giveback_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_storage_failover.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_storage_failover.py new file mode 100644 index 000000000..aa0b7703e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_storage_failover.py @@ -0,0 +1,350 @@ +''' unit tests ONTAP Ansible module: na_ontap_storage_failover ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_failover \ + import NetAppOntapStorageFailover as storage_failover_module # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'no_records': (200, {'records': []}, None), + 'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'storage_failover_enabled_record': (200, { + 'num_records': 1, + 'records': [{ + 'name': 'node1', + 'uuid': '56ab5d21-312a-11e8-9166-9d4fc452db4e', + 'ha': { + 'enabled': True + } + }] + }, None), + 'storage_failover_disabled_record': (200, { + 'num_records': 1, + "records": [{ + 'name': 'node1', + 'uuid': '56ab5d21-312a-11e8-9166-9d4fc452db4e', + 'ha': { + 'enabled': False + } + }] + }, None), + 'no_ha_record': (200, { + 'num_records': 1, + "records": [{ + 'name': 'node1', + 'uuid': '56ab5d21-312a-11e8-9166-9d4fc452db4e', + }] + }, None) +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None): + ''' save arguments ''' + self.type = kind + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'storage_failover_enabled': + xml = self.build_storage_failover_enabled_info() + elif self.type == 'storage_failover_disabled': + xml = self.build_storage_failover_disabled_info() + elif self.type == 'storage_failover_fail': + raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test") + self.xml_out = xml + return xml + + @staticmethod + def build_storage_failover_enabled_info(): + ''' build xml data for cf-status ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'is-enabled': 'true' + } + + xml.translate_struct(data) + return xml + + @staticmethod + def build_storage_failover_disabled_info(): + ''' build xml data for cf-status ''' + xml = netapp_utils.zapi.NaElement('xml') + data = { + 'is-enabled': 'false' + } + + xml.translate_struct(data) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + self.onbox = False + + def set_default_args(self, use_rest=None): + if self.onbox: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + node_name = 'node1' + else: + hostname = '10.10.10.10' + username = 'username' + password = 'password' + node_name = 'node1' + + args = dict({ + 'state': 'present', + 'hostname': hostname, + 'username': username, + 'password': password, + 'node_name': node_name + }) + + if use_rest is not None: + args['use_rest'] = use_rest + + return args + + @staticmethod + def get_storage_failover_mock_object(cx_type='zapi', kind=None): + storage_failover_obj = storage_failover_module() + if cx_type == 'zapi': + if kind is None: + storage_failover_obj.server = MockONTAPConnection() + else: + storage_failover_obj.server = MockONTAPConnection(kind=kind) + return storage_failover_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + storage_failover_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_get_called_existing(self): + ''' test get_storage_failover for existing config ''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = storage_failover_module() + my_obj.server = MockONTAPConnection(kind='storage_failover_enabled') + assert my_obj.get_storage_failover() + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_failover.NetAppOntapStorageFailover.modify_storage_failover') + def test_successful_enable(self, modify_storage_failover): + ''' enable storage_failover and testing idempotency ''' + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = storage_failover_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('storage_failover_disabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + modify_storage_failover.assert_called_with({'is_enabled': False}) + # to reset na_helper from remembering the previous 'changed' value + set_module_args(self.set_default_args(use_rest='Never')) + my_obj = storage_failover_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('storage_failover_enabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_storage_failover.NetAppOntapStorageFailover.modify_storage_failover') + def test_successful_disable(self, modify_storage_failover): + ''' disable storage_failover and testing idempotency ''' + data = self.set_default_args(use_rest='Never') + data['state'] = 'absent' + set_module_args(data) + my_obj = storage_failover_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('storage_failover_enabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + modify_storage_failover.assert_called_with({'is_enabled': True}) + # to reset na_helper from remembering the previous 'changed' value + my_obj = storage_failover_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('storage_failover_disabled') + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert not exc.value.args[0]['changed'] + + def test_if_all_methods_catch_exception(self): + data = self.set_default_args(use_rest='Never') + set_module_args(data) + my_obj = storage_failover_module() + if not self.onbox: + my_obj.server = MockONTAPConnection('storage_failover_fail') + with pytest.raises(AnsibleFailJson) as exc: + my_obj.modify_storage_failover(self.get_storage_failover_mock_object()) + assert 'Error modifying storage failover' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') + def test_negative_no_netapp_lib(self, mock_request): + data = self.set_default_args(use_rest='Never') + set_module_args(data) + mock_request.return_value = False + with pytest.raises(AnsibleFailJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert 'Error: the python NetApp-Lib module is required.' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert SRR['generic_error'][2] in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_enabled_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['storage_failover_disabled_record'], # get + SRR['empty_good'], # patch + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_enabled_rest(self, mock_request): + data = self.set_default_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['storage_failover_enabled_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_successful_disabled_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['storage_failover_enabled_record'], # get + SRR['empty_good'], # patch + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_idempotent_disabled_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['storage_failover_disabled_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_negative_no_ha_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'present' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_ha_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert 'HA is not available on node: node1' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_negative_node_not_found_rest(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_records'], + SRR['storage_failover_disabled_record'], # get + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert 'REST API did not return failover details for node' in exc.value.args[0]['msg'] + assert 'current nodes: node1' in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_negative_node_not_found_rest_no_names(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_records'], + SRR['no_records'], # get all nodes + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert 'REST API did not return failover details for node' in exc.value.args[0]['msg'] + assert 'current nodes: node1' not in exc.value.args[0]['msg'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_negative_node_not_found_rest_error_on_get_nodes(self, mock_request): + data = self.set_default_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['no_records'], + SRR['generic_error'], # get all nodes + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_storage_failover_mock_object(cx_type='rest').apply() + assert 'REST API did not return failover details for node' in exc.value.args[0]['msg'] + assert 'current nodes: node1' not in exc.value.args[0]['msg'] + assert 'failed to get list of nodes' in exc.value.args[0]['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_svm.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_svm.py new file mode 100644 index 000000000..d18d32a57 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_svm.py @@ -0,0 +1,1251 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + assert_warning_was_raised, call_main, clear_warnings, create_and_apply, create_module, expect_and_capture_ansible_exception, patch_ansible, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_svm \ + import NetAppOntapSVM as svm_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +# REST API canned responses when mocking send_request + +svm_info = { + "uuid": "09e9fd5e-8ebd-11e9-b162-005056b39fe7", + "name": "test_svm", + "state": "running", + "subtype": "default", + "language": "c.utf_8", + "aggregates": [{"name": "aggr_1", + "uuid": "850dd65b-8811-4611-ac8c-6f6240475ff9"}, + {"name": "aggr_2", + "uuid": "850dd65b-8811-4611-ac8c-6f6240475ff9"}], + "comment": "new comment", + "ipspace": {"name": "ansible_ipspace", + "uuid": "2b760d31-8dfd-11e9-b162-005056b39fe7"}, + "snapshot_policy": {"uuid": "3b611707-8dfd-11e9-b162-005056b39fe7", + "name": "old_snapshot_policy"}, + "nfs": {"enabled": True, "allowed": True}, + "cifs": {"enabled": False}, + "iscsi": {"enabled": False}, + "fcp": {"enabled": False}, + "nvme": {"enabled": False}, + 'max_volumes': 3333 +} + +svm_info_cert1 = dict(svm_info) +svm_info_cert1['certificate'] = {'name': 'cert_1', 'uuid': 'cert_uuid_1'} +svm_info_cert2 = dict(svm_info) +svm_info_cert2['certificate'] = {'name': 'cert_2', 'uuid': 'cert_uuid_2'} + +SRR = rest_responses({ + 'svm_record': (200, {'records': [svm_info]}, None), + 'svm_record_cert1': (200, {'records': [svm_info_cert1]}, None), + 'svm_record_cert2': (200, {'records': [svm_info_cert2]}, None), + 'svm_record_ap': (200, + {'records': [{"name": "test_svm", + "state": "running", + "aggregates": [{"name": "aggr_1", + "uuid": "850dd65b-8811-4611-ac8c-6f6240475ff9"}, + {"name": "aggr_2", + "uuid": "850dd65b-8811-4611-ac8c-6f6240475ff9"}], + "ipspace": {"name": "ansible_ipspace", + "uuid": "2b760d31-8dfd-11e9-b162-005056b39fe7"}, + "snapshot_policy": {"uuid": "3b611707-8dfd-11e9-b162-005056b39fe7", + "name": "old_snapshot_policy"}, + "nfs": {"enabled": False}, + "cifs": {"enabled": True, "allowed": True}, + "iscsi": {"enabled": True, "allowed": True}, + "fcp": {"enabled": False}, + "nvme": {"enabled": False}, + "language": "de.utf_8", + "uuid": "svm_uuid" + }]}, None), + 'cli_record': (200, + {'records': [{"max_volumes": 100, "allowed_protocols": ['nfs', 'iscsi']}]}, None), + 'certificate_record_1': (200, + {'records': [{"name": "cert_1", + "uuid": "cert_uuid_1"}]}, None), + 'certificate_record_2': (200, + {'records': [{"name": "cert_2", + "uuid": "cert_uuid_2"}]}, None), + 'svm_web_record_1': (200, { + 'records': [{ + 'certificate': { + "uuid": "cert_uuid_1" + }, + 'client_enabled': False, + 'ocsp_enabled': False, + }]}, None), + 'svm_web_record_2': (200, { + 'records': [{ + 'certificate': { + "uuid": "cert_uuid_2" + }, + 'client_enabled': True, + 'ocsp_enabled': True, + }]}, None) +}, False) + +DEFAULT_ARGS = { + 'name': 'test_svm', + 'aggr_list': 'aggr_1,aggr_2', + 'ipspace': 'ansible_ipspace', + 'comment': 'new comment', + 'subtype': 'default', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' +} + +vserver_info = { + 'num-records': 1, + 'attributes-list': { + 'vserver-info': { + 'vserver-name': 'test_svm', + 'ipspace': 'ansible_ipspace', + 'root-volume': 'ansible_vol', + 'root-volume-aggregate': 'ansible_aggr', + 'language': 'c.utf_8', + 'comment': 'new comment', + 'snapshot-policy': 'old_snapshot_policy', + 'vserver-subtype': 'default', + 'allowed-protocols': [{'protocol': 'nfs'}, {'protocol': 'cifs'}], + 'aggr-list': [{'aggr-name': 'aggr_1'}, {'aggr-name': 'aggr_2'}], + }}} + + +ZRR = zapi_responses({ + 'svm_record': build_zapi_response(vserver_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + error = create_module(svm_module, {}, fail=True)['msg'] + assert 'missing required arguments:' in error + assert 'hostname' in error + assert 'name' in error + + +def test_error_missing_name(): + ''' Test if create throws an error if name is not specified''' + register_responses([ + ]) + args = dict(DEFAULT_ARGS) + args.pop('name') + assert create_module(svm_module, args, fail=True)['msg'] == 'missing required arguments: name' + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_error_missing_netapp_lib(mock_has_netapp_lib): + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ]) + mock_has_netapp_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg == create_module(svm_module, DEFAULT_ARGS, fail=True)['msg'] + + +def test_successful_create_zapi(): + '''Test successful create''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-create', ZRR['success']), + ('ZAPI', 'vserver-modify', ZRR['success']), + ]) + assert create_and_apply(svm_module, DEFAULT_ARGS)['changed'] + + +def test_create_idempotency(): + '''Test API create''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ]) + assert not create_and_apply(svm_module, DEFAULT_ARGS)['changed'] + + +def test_create_error(): + '''Test successful create''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-create', ZRR['error']), + ]) + msg = 'Error provisioning SVM test_svm: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert create_and_apply(svm_module, DEFAULT_ARGS, fail=True)['msg'] == msg + + +def test_successful_delete(): + '''Test successful delete''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-destroy', ZRR['success']), + ]) + _modify_options_with_expected_change('state', 'absent') + + +def test_error_delete(): + '''Test delete with ZAPI error + ''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-destroy', ZRR['error']), + ]) + module_args = { + 'state': 'absent', + } + msg = 'Error deleting SVM test_svm: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_delete_idempotency(): + '''Test delete idempotency ''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ]) + module_args = { + 'state': 'absent', + } + assert not create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_init(): + '''Validate that: + admin_state is ignored with ZAPI + language is set to lower case for C.UTF-8 + ''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ]) + module_args = { + 'admin_state': 'running', + 'language': 'C.uTf-8' + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + assert my_obj.parameters['language'] == 'c.utf_8' + assert_warning_was_raised('admin_state is ignored when ZAPI is used.') + + +def test_init_error(): + '''Validate that: + unallowed protocol raises an error + services is not supported with ZAPI + ''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('GET', 'cluster', SRR['is_zapi']), + ('GET', 'cluster', SRR['is_rest_96']), + ]) + module_args = { + 'allowed_protocols': 'dummy,humpty,dumpty,cifs,nfs', + } + error = create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert 'Unexpected value dummy in allowed_protocols.' in error + assert 'Unexpected value humpty in allowed_protocols.' in error + assert 'Unexpected value dumpty in allowed_protocols.' in error + assert 'cifs' not in error + assert 'nfs' not in error + + module_args = { + 'services': {}, + } + error = create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error == 'using services requires ONTAP 9.6 or later and REST must be enabled - Unreachable - using ZAPI.' + module_args = { + 'services': {'ndmp': {'allowed': True}}, + } + error = create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error == 'using ndmp requires ONTAP 9.7 or later and REST must be enabled - ONTAP version: 9.6.0 - using REST.' + + +def test_successful_rename(): + '''Test successful rename''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-rename', ZRR['success']), + ]) + module_args = { + 'from_name': 'test_svm', + 'name': 'test_new_svm', + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_rename_no_from(): + '''Test error rename''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ]) + module_args = { + 'from_name': 'test_svm', + 'name': 'test_new_svm', + } + msg = 'Error renaming SVM test_new_svm: no SVM with from_name test_svm.' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_error_rename_zapi(): + '''Test error rename''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-rename', ZRR['error']), + ]) + module_args = { + 'from_name': 'test_svm', + 'name': 'test_new_svm', + } + msg = 'Error renaming SVM test_svm: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_successful_modify_language(): + '''Test successful modify language''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-modify', ZRR['success']), + ]) + _modify_options_with_expected_change('language', 'c') + + +def test_error_modify_language(): + '''Test error modify language''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-modify', ZRR['error']), + ]) + module_args = { + 'language': 'c', + } + msg = 'Error modifying SVM test_svm: NetApp API failed. Reason - 12345:synthetic error for UT purpose' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_error_modify_fixed_properties(): + '''Test error modifying a fixed property''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ]) + module_args = { + 'ipspace': 'new', + } + msg = 'Error modifying SVM test_svm: cannot modify ipspace - current: ansible_ipspace - desired: new.' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + module_args = { + 'ipspace': 'new', + 'root_volume': 'new_root' + } + msg = 'Error modifying SVM test_svm: cannot modify root_volume - current: ansible_vol - desired: new_root, '\ + 'ipspace - current: ansible_ipspace - desired: new.' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_successful_modify_snapshot_policy(): + '''Test successful modify language''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-modify', ZRR['success']), + ]) + _modify_options_with_expected_change( + 'snapshot_policy', 'new_snapshot_policy' + ) + + +def test_successful_modify_allowed_protocols(): + '''Test successful modify allowed protocols''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-modify', ZRR['success']), + ]) + _modify_options_with_expected_change( + 'allowed_protocols', 'nvme,fcp' + ) + + +def test_successful_modify_aggr_list(): + '''Test successful modify aggr-list''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-modify', ZRR['success']), + ]) + _modify_options_with_expected_change( + 'aggr_list', 'aggr_3,aggr_4' + ) + + +def test_successful_modify_aggr_list_star(): + '''Test successful modify aggr-list''' + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ('ZAPI', 'vserver-get-iter', ZRR['svm_record']), + ('ZAPI', 'vserver-modify', ZRR['success']), + ]) + module_args = { + 'aggr_list': '*' + } + results = create_and_apply(svm_module, DEFAULT_ARGS, module_args) + assert results['changed'] + assert_warning_was_raised("na_ontap_svm: changed always 'True' when aggr_list is '*'.") + + +def _modify_options_with_expected_change(arg0, arg1): + module_args = { + arg0: arg1, + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['generic_error']), + ]) + module_args = { + 'root_volume': 'whatever', + 'aggr_list': '*', + 'ignore_rest_unsupported_options': 'true', + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == 'calling: svm/svms: got Expected error.' + + +def test_rest_error_unsupported_parm(): + register_responses([ + ]) + module_args = { + 'root_volume': 'not_supported_by_rest', + 'use_rest': 'always', + } + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == "REST API currently does not support 'root_volume'" + + +def test_rest_successfully_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['success']), + ]) + assert create_and_apply(svm_module, DEFAULT_ARGS)['changed'] + + +def test_rest_error_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['generic_error']), + ]) + msg = 'Error in create: calling: svm/svms: got Expected error.' + assert create_and_apply(svm_module, DEFAULT_ARGS, fail=True)['msg'] == msg + + +def test_rest_create_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ]) + module_args = { + 'root_volume': 'whatever', + 'aggr_list': '*', + 'ignore_rest_unsupported_options': 'true', + } + assert not create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successful_delete(): + '''Test successful delete''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('DELETE', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['success']), + ]) + module_args = { + 'state': 'absent', + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_delete(): + '''Test error delete''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('DELETE', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['generic_error']), + ]) + module_args = { + 'state': 'absent', + } + msg = 'Error in delete: calling: svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7: got Expected error.' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_error_delete_no_svm(): + '''Test error delete''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ]) + my_obj = create_module(svm_module, DEFAULT_ARGS) + msg = 'Internal error, expecting SVM object in delete' + assert expect_and_capture_ansible_exception(my_obj.delete_vserver, 'fail')['msg'] == msg + + +def test_rest_delete_idempotency(): + '''Test delete idempotency''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['zero_records']), + ]) + module_args = { + 'state': 'absent', + } + assert not create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successful_rename(): + '''Test successful rename''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['zero_records']), + ('GET', 'svm/svms', SRR['svm_record']), + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['success']), + ]) + module_args = { + 'from_name': 'test_svm', + 'name': 'test_new_svm', + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successful_modify_language(): + '''Test successful modify language''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['success']), + ]) + module_args = { + 'language': 'c', + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successful_get(): + '''Test successful get''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'svm/svms', SRR['svm_record_ap']), + ]) + module_args = { + 'admin_state': 'running', + 'language': 'c' + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + current = my_obj.get_vserver() + print(current) + assert current['services']['nfs']['allowed'] + assert not current['services']['cifs']['enabled'] + current = my_obj.get_vserver() + print(current) + assert not current['services']['nfs']['enabled'] + assert current['services']['cifs']['allowed'] + assert current['services']['iscsi']['allowed'] + + +def test_rest_successfully_create_ignore_zapi_option(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['success']), + ]) + module_args = { + 'root_volume': 'whatever', + 'aggr_list': '*', + 'ignore_rest_unsupported_options': 'true', + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_create_with_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['success']), + ]) + module_args = { + 'services': {'nfs': {'allowed': True, 'enabled': True}, 'fcp': {'allowed': True, 'enabled': True}} + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_modify_with_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['success']), + ('POST', 'protocols/san/fcp/services', SRR['success']), + ]) + module_args = { + 'admin_state': 'stopped', + 'services': {'nfs': {'allowed': True, 'enabled': True}, 'fcp': {'allowed': True, 'enabled': True}} + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_enable_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('POST', 'protocols/san/fcp/services', SRR['success']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + modify = {'services': {'nfs': {'allowed': True}, 'fcp': {'enabled': True}}} + current = {'services': {'nfs': {'allowed': True}}, 'uuid': 'uuid'} + assert my_obj.modify_services(modify, current) is None + + +def test_rest_successfully_reenable_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('PATCH', 'protocols/san/fcp/services/uuid', SRR['success']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + modify = {'services': {'nfs': {'allowed': True}, 'fcp': {'enabled': True}}} + fcp_dict = {'_links': {'self': {'href': 'fcp_link'}}} + current = {'services': {'nfs': {'allowed': True}}, 'uuid': 'uuid', 'fcp': fcp_dict} + assert my_obj.modify_services(modify, current) is None + + +def test_rest_negative_enable_service(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + modify = {'services': {'nfs': {'allowed': True}, 'bad_value': {'enabled': True}}, 'name': 'new_name'} + current = {'services': {'nfs': {'allowed': True}}, 'uuid': 'uuid'} + error = expect_and_capture_ansible_exception(my_obj.modify_services, 'fail', modify, current)['msg'] + assert error == 'Internal error, unexpecting service: bad_value.' + + +def test_rest_negative_modify_services(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('POST', 'protocols/san/fcp/services', SRR['generic_error']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + modify = {'services': {'nfs': {'allowed': True}, 'fcp': {'enabled': True}}, 'name': 'new_name'} + current = {'services': {'nfs': {'allowed': True}}, 'uuid': 'uuid'} + error = expect_and_capture_ansible_exception(my_obj.modify_services, 'fail', modify, current)['msg'] + assert error == 'Error in modify service for fcp: calling: protocols/san/fcp/services: got Expected error.' + + +def test_rest_negative_modify_current_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + modify = {'enabled_protocols': ['nfs', 'fcp']} + current = None + error = expect_and_capture_ansible_exception(my_obj.modify_vserver, 'fail', modify, current)['msg'] + assert error == 'Internal error, expecting SVM object in modify.' + + +def test_rest_negative_modify_modify_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + modify = {} + current = {'enabled_protocols': ['nfs'], 'disabled_protocols': ['fcp', 'iscsi', 'nvme'], 'uuid': 'uuid'} + error = expect_and_capture_ansible_exception(my_obj.modify_vserver, 'fail', modify, current)['msg'] + assert error == 'Internal error, expecting something to modify in modify.' + + +def test_rest_negative_modify_error_1(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('PATCH', 'svm/svms/uuid', SRR['generic_error']), # rename + ]) + module_args = { + 'admin_state': 'running', + 'language': 'klingon', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + modify = {'enabled_protocols': ['nfs', 'fcp'], 'name': 'new_name', 'language': 'klingon'} + current = {'enabled_protocols': ['nfs'], 'disabled_protocols': ['fcp', 'iscsi', 'nvme'], 'uuid': 'uuid'} + error = expect_and_capture_ansible_exception(my_obj.modify_vserver, 'fail', modify, current)['msg'] + assert error == 'Error in rename: calling: svm/svms/uuid: got Expected error.' + + +def test_rest_negative_modify_error_2(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('PATCH', 'svm/svms/uuid', SRR['success']), # rename + ('PATCH', 'svm/svms/uuid', SRR['generic_error']), # modify + ]) + module_args = { + 'admin_state': 'running', + 'language': 'klingon', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + modify = {'enabled_protocols': ['nfs', 'fcp'], 'name': 'new_name', 'language': 'klingon'} + current = {'enabled_protocols': ['nfs'], 'disabled_protocols': ['fcp', 'iscsi', 'nvme'], 'uuid': 'uuid'} + error = expect_and_capture_ansible_exception(my_obj.modify_vserver, 'fail', modify, current)['msg'] + assert error == 'Error in modify: calling: svm/svms/uuid: got Expected error.' + + +def test_rest_successfully_get_older_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'private/cli/vserver', SRR['cli_record']), # get protocols + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + assert not create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_add_protocols_on_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['success']), + ('PATCH', 'private/cli/vserver/add-protocols', SRR['success']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_add_remove_protocols_on_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'private/cli/vserver', SRR['cli_record']), # get protocols + ('PATCH', 'private/cli/vserver/add-protocols', SRR['success']), + ('PATCH', 'private/cli/vserver/remove-protocols', SRR['success']) + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}, 'iscsi': {'allowed': False}, 'fcp': {'allowed': True}} + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_add_remove_protocols_on_modify_old_style(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'private/cli/vserver', SRR['cli_record']), # get protocols + ('PATCH', 'private/cli/vserver/add-protocols', SRR['success']), + ('PATCH', 'private/cli/vserver/remove-protocols', SRR['success']) + ]) + module_args = { + 'admin_state': 'running', + 'allowed_protocols': ['nfs', 'fcp'] + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_validate_int_or_string_as_int(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + assert create_module(svm_module, DEFAULT_ARGS, module_args).validate_int_or_string('10', 'whatever') is None + + +def test_validate_int_or_string_as_str(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + assert create_module(svm_module, DEFAULT_ARGS, module_args).validate_int_or_string('whatever', 'whatever') is None + + +def test_negative_validate_int_or_string(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ]) + module_args = { + 'admin_state': 'running', + 'services': {'nfs': {'allowed': True, 'enabled': True}} + } + astring = 'testme' + error = expect_and_capture_ansible_exception(create_module(svm_module, DEFAULT_ARGS, module_args).validate_int_or_string, 'fail', '10a', astring)['msg'] + assert "expecting int value or '%s'" % astring in error + + +def test_rest_successfully_modify_with_admin_state(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['success']) # change admin_state + ]) + module_args = {'admin_state': 'stopped'} + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_successfully_modify_with_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['success']), + ('GET', 'svm/svms', SRR['svm_record']), + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['success']) # change admin_state + ]) + module_args = {'admin_state': 'stopped'} + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + +# Tests for web services - 4 cases +# ZAPI: not supported +# REST < 9.8: not supported +# REST 9.8, 9.9. 9.10.0: only certificate is supported, using deprecated certificate fields in svs/svms +# REST >= 9.10.1: all options are supported, using svm/svms/uuid/web + + +def test_web_services_error_zapi(): + register_responses([ + ('GET', 'cluster', SRR['is_zapi']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = 'using web requires ONTAP 9.8 or later and REST must be enabled - Unreachable - using ZAPI.' + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_error_9_7_5(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_7_5']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = 'using web requires ONTAP 9.8 or later and REST must be enabled - ONTAP version: 9.7.5 - using REST.' + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_error_9_8_0(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + msg = "using ('client_enabled', 'ocsp_enabled') requires ONTAP 9.10.1 or later and REST must be enabled - ONTAP version: 9.8.0 - using REST." + module_args = {'web': {'certificate': 'cert_name', 'client_enabled': True}} + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + module_args = {'web': {'certificate': 'cert_name', 'ocsp_enabled': True}} + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_8_0_none_set(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'private/cli/vserver', SRR['cli_record']), # get protocols + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['success']) # change certificate + ]) + module_args = {'web': {'certificate': 'cert_name'}} + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_web_services_modify_certificate_9_8_0_other_set(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'svm/svms', SRR['svm_record_cert2']), + ('GET', 'private/cli/vserver', SRR['cli_record']), # get protocols + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['success']) # change certificate + ]) + module_args = {'web': {'certificate': 'cert_name'}} + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_web_services_modify_certificate_9_8_0_idempotent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'svm/svms', SRR['svm_record_cert1']), + ('GET', 'private/cli/vserver', SRR['cli_record']), # get protocols + ]) + module_args = {'web': {'certificate': 'cert_name'}} + assert not create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_web_services_modify_certificate_9_8_0_error_not_found(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = "Error certificate not found: {'name': 'cert_name'}. Current certificates with type=server: ['cert_1']" + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_8_0_error_api1(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/certificates', SRR['generic_error']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = "Error retrieving certificate {'name': 'cert_name'}: calling: security/certificates: got Expected error." + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_8_0_error_api2(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['generic_error']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = "Error retrieving certificates: calling: security/certificates: got Expected error." + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_10_1_none_set(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web', SRR['zero_records']), + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web', SRR['success']) # change certificate + ]) + module_args = {'web': {'certificate': 'cert_name'}} + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_web_services_modify_certificate_9_10_1_other_set(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web', SRR['svm_web_record_2']), + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web', SRR['success']) # change certificate + ]) + module_args = {'web': {'certificate': 'cert_name'}} + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_web_services_modify_certificate_9_10_1_idempotent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web', SRR['svm_web_record_1']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + assert not create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_web_services_modify_certificate_9_10_1_error_not_found(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['zero_records']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = "Error certificate not found: {'name': 'cert_name'}. Current certificates with type=server: ['cert_1']" + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + msg = "Error certificate not found: {'name': 'cert_name'}. Current certificates with type=server: []" + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_10_1_error_api1(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['generic_error']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = "Error retrieving certificate {'name': 'cert_name'}: calling: security/certificates: got Expected error." + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_10_1_error_api2(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['zero_records']), + ('GET', 'security/certificates', SRR['generic_error']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = "Error retrieving certificates: calling: security/certificates: got Expected error." + assert create_module(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_10_1_error_api3(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web', SRR['generic_error']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = 'Error retrieving web info: calling: svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web: got Expected error.' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_10_1_error_api4(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ('GET', 'svm/svms', SRR['svm_record']), + ('GET', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web', SRR['svm_web_record_2']), + ('PATCH', 'svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web', SRR['generic_error']) # change certificate + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = "Error in modify web service for {'certificate': {'uuid': 'cert_uuid_1'}}: "\ + "calling: svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web: got Expected error." + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_web_services_modify_certificate_9_10_1_warning(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/certificates', SRR['certificate_record_1']), + ]) + module_args = {'web': {'certificate': 'cert_name'}} + msg = "Error in modify web service for {'certificate': {'uuid': 'cert_uuid_1'}}: "\ + "calling: svm/svms/09e9fd5e-8ebd-11e9-b162-005056b39fe7/web: got Expected error." + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + assert my_obj.modify_web_services({}, {'uuid': 'uuid'}) is None + assert_warning_was_raised('Nothing to change: {}') + clear_warnings() + assert my_obj.modify_web_services({'certificate': {'name': 'whatever'}}, {'uuid': 'uuid'}) is None + assert_warning_was_raised("Nothing to change: {'certificate': {}}") + clear_warnings() + assert my_obj.modify_web_services({'certificate': {}}, {'uuid': 'uuid'}) is None + assert_warning_was_raised("Nothing to change: {'certificate': {}}") + + +def test_rest_cli_max_volumes_get(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['svm_record_ap']), + ('GET', 'private/cli/vserver', SRR['cli_record']), + ]) + module_args = { + 'max_volumes': 3333, + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + record = my_obj.get_vserver() + assert 'name' in SRR['svm_record_ap'][1]['records'][0] + assert 'max_volumes' not in SRR['svm_record_ap'][1]['records'][0] + assert 'max_volumes' in record + + +def test_rest_cli_max_volumes_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['success']), + ('PATCH', 'private/cli/vserver', SRR['success']), + ]) + module_args = { + 'max_volumes': 3333, + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_cli_max_volumes_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['svm_record_ap']), + ('GET', 'private/cli/vserver', SRR['cli_record']), + ('PATCH', 'private/cli/vserver', SRR['success']), + ]) + module_args = { + 'max_volumes': 3333, + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_rest_cli_max_volumes_get(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['svm_record_ap']), + ('GET', 'private/cli/vserver', SRR['generic_error']), + ]) + module_args = { + 'max_volumes': 3333, + } + msg = 'Error getting vserver info: calling: private/cli/vserver: got Expected error. - None' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_error_rest_cli_max_volumes_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['svm_record_ap']), + ('GET', 'private/cli/vserver', SRR['cli_record']), + ('PATCH', 'private/cli/vserver', SRR['generic_error']), + ]) + module_args = { + 'max_volumes': 3333, + } + msg = 'Error updating max_volumes: calling: private/cli/vserver: got Expected error. - None' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_cli_add_remove_protocols_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['success']), + ('PATCH', 'private/cli/vserver/add-protocols', SRR['success']), + ('PATCH', 'private/cli/vserver/remove-protocols', SRR['success']), + ]) + module_args = { + 'allowed_protocols': 'nfs,cifs', + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_rest_cli_add_protocols_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['zero_records']), + ('POST', 'svm/svms', SRR['success']), + ('PATCH', 'private/cli/vserver/add-protocols', SRR['generic_error']), + ]) + module_args = { + 'allowed_protocols': 'nfs,cifs', + } + msg = 'Error adding protocols: calling: private/cli/vserver/add-protocols: got Expected error. - None' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_cli_remove_protocols_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['svm_record_ap']), + ('GET', 'private/cli/vserver', SRR['cli_record']), + ('PATCH', 'private/cli/vserver/remove-protocols', SRR['success']), + ]) + module_args = { + 'allowed_protocols': 'nfs,cifs', + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_rest_cli_remove_protocols_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['svm_record_ap']), + ('GET', 'private/cli/vserver', SRR['cli_record']), + ('PATCH', 'private/cli/vserver/remove-protocols', SRR['generic_error']), + ]) + module_args = { + 'allowed_protocols': 'nfs,cifs', + } + msg = 'Error removing protocols: calling: private/cli/vserver/remove-protocols: got Expected error. - None' + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == msg + + +def test_add_parameter_to_dict(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'name': 'svm', + 'ipspace': 'ipspace', + 'max_volumes': 3333, + } + my_obj = create_module(svm_module, DEFAULT_ARGS, module_args) + test_dict = {} + my_obj.add_parameter_to_dict(test_dict, 'name', None) + my_obj.add_parameter_to_dict(test_dict, 'ipspace', 'ipspace_key') + my_obj.add_parameter_to_dict(test_dict, 'max_volumes', None, True) + print(test_dict) + assert test_dict['name'] == 'svm' + assert test_dict['ipspace_key'] == 'ipspace' + assert test_dict['max_volumes'] == '3333' + + +def test_rest_language_match(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'svm/svms', SRR['svm_record_ap']), + ('GET', 'private/cli/vserver', SRR['cli_record']), + ('PATCH', 'svm/svms/svm_uuid', SRR['success']), + ]) + module_args = { + 'language': 'de.UTF-8' + } + assert create_and_apply(svm_module, DEFAULT_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised( + 'Attempting to change language from ONTAP value de.utf_8 to de.UTF-8. Use de.utf_8 to suppress this warning and maintain idempotency.') diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_template.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_template.py new file mode 100644 index 000000000..b548739a8 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_template.py @@ -0,0 +1,86 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_cg_snapshot \ + import NetAppONTAPCGSnapshot as my_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, parm1=None): + ''' save arguments ''' + self.type = kind + self.parm1 = parm1 + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'vserver': + xml = self.build_vserver_info(self.parm1) + self.xml_out = xml + return xml + + @staticmethod + def build_vserver_info(vserver): + ''' build xml data for vserser-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = netapp_utils.zapi.NaElement('attributes-list') + attributes.add_node_with_children('vserver-info', + **{'vserver-name': vserver}) + xml.add_child_elem(attributes) + # print(xml.to_string()) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.server = MockONTAPConnection() + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_ensure_command_called(self): + ''' a more interesting test ''' +# TODO: change argument names/values + set_module_args({ + 'vserver': 'vserver', + 'volumes': 'volumes', + 'snapshot': 'snapshot', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + }) + my_obj = my_module() + my_obj.server = self.server + with pytest.raises(AnsibleFailJson) as exc: + # It may not be a good idea to start with apply + # More atomic methods can be easier to mock + # Hint: start with get methods, as they are called first + my_obj.apply() +# TODO: change message, and maybe test contents + msg = 'Error fetching CG ID for CG commit snapshot' + assert exc.value.args[0]['msg'] == msg diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ucadapter.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ucadapter.py new file mode 100644 index 000000000..5f1f502c1 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_ucadapter.py @@ -0,0 +1,173 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_ucadapter ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_ucadapter \ + import NetAppOntapadapter as ucadapter_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + +DEFAULT_ARGS = { + 'hostname': '10.0.0.0', + 'username': 'user', + 'password': 'pass', + 'node_name': 'node1', + 'adapter_name': '0f', + 'mode': 'fc', + 'type': 'target', + 'use_rest': 'never' +} + +ucm_info_mode_fc = { + 'attributes': { + 'uc-adapter-info': { + 'mode': 'fc', + 'pending-mode': 'abc', + 'type': 'target', + 'pending-type': 'intitiator', + 'status': 'up', + } + } +} + +ucm_info_mode_cna = { + 'attributes': { + 'uc-adapter-info': { + 'mode': 'cna', + 'pending-mode': 'cna', + 'type': 'target', + 'pending-type': 'intitiator', + 'status': 'up', + } + } +} + + +ZRR = zapi_responses({ + 'ucm_info': build_zapi_response(ucm_info_mode_fc), + 'ucm_info_cna': build_zapi_response(ucm_info_mode_cna) +}) + + +SRR = rest_responses({ + 'ucm_info': (200, {"records": [{ + 'current_mode': 'fc', + 'current_type': 'target', + 'status_admin': 'up' + }], "num_records": 1}, None), + 'ucm_info_cna': (200, {"records": [{ + 'current_mode': 'cna', + 'current_type': 'target', + 'status_admin': 'up' + }], "num_records": 1}, None), + 'fc_adapter_info': (200, {"records": [{ + 'uuid': 'abcdef' + }], "num_records": 1}, None) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "node_name", "adapter_name"] + error = create_module(ucadapter_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_ensure_ucadapter_get_called(): + ''' fetching ucadapter details ''' + register_responses([ + ('ucm-adapter-get', ZRR['empty']) + ]) + ucm_obj = create_module(ucadapter_module, DEFAULT_ARGS) + assert ucm_obj.get_adapter() is None + + +def test_change_mode_from_cna_to_fc(): + ''' configuring ucadaptor and checking idempotency ''' + register_responses([ + ('ucm-adapter-get', ZRR['ucm_info_cna']), + ('fcp-adapter-config-down', ZRR['success']), + ('ucm-adapter-modify', ZRR['success']), + ('fcp-adapter-config-up', ZRR['success']), + ('ucm-adapter-get', ZRR['ucm_info_cna']) + ]) + assert create_and_apply(ucadapter_module, DEFAULT_ARGS)['changed'] + args = {'mode': 'cna'} + assert not create_and_apply(ucadapter_module, DEFAULT_ARGS, args)['changed'] + + +def test_change_mode_from_fc_to_cna(): + register_responses([ + ('ucm-adapter-get', ZRR['ucm_info']), + ('fcp-adapter-config-down', ZRR['success']), + ('ucm-adapter-modify', ZRR['success']), + ('fcp-adapter-config-up', ZRR['success']), + ]) + args = {'mode': 'cna'} + assert create_and_apply(ucadapter_module, DEFAULT_ARGS, args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ucm-adapter-get', ZRR['error']), + ('ucm-adapter-modify', ZRR['error']), + ('fcp-adapter-config-down', ZRR['error']), + ('fcp-adapter-config-up', ZRR['error']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'network/fc/ports', SRR['generic_error']), + ('GET', 'private/cli/ucadmin', SRR['generic_error']), + ('PATCH', 'private/cli/ucadmin', SRR['generic_error']), + ('PATCH', 'network/fc/ports/abcdef', SRR['generic_error']), + ('PATCH', 'network/fc/ports/abcdef', SRR['generic_error']), + ('GET', 'network/fc/ports', SRR['empty_records']) + ]) + ucm_obj = create_module(ucadapter_module, DEFAULT_ARGS) + assert 'Error fetching ucadapter' in expect_and_capture_ansible_exception(ucm_obj.get_adapter, 'fail')['msg'] + assert 'Error modifying adapter' in expect_and_capture_ansible_exception(ucm_obj.modify_adapter, 'fail')['msg'] + assert 'Error trying to down' in expect_and_capture_ansible_exception(ucm_obj.online_or_offline_adapter, 'fail', 'down', '0f')['msg'] + assert 'Error trying to up' in expect_and_capture_ansible_exception(ucm_obj.online_or_offline_adapter, 'fail', 'up', '0f')['msg'] + + ucm_obj = create_module(ucadapter_module, DEFAULT_ARGS, {'use_rest': 'always'}) + ucm_obj.adapters_uuids = {'0f': 'abcdef'} + assert 'Error fetching adapter 0f uuid' in expect_and_capture_ansible_exception(ucm_obj.get_adapter_uuid, 'fail', '0f')['msg'] + assert 'Error fetching ucadapter' in expect_and_capture_ansible_exception(ucm_obj.get_adapter, 'fail')['msg'] + assert 'Error modifying adapter' in expect_and_capture_ansible_exception(ucm_obj.modify_adapter, 'fail')['msg'] + assert 'Error trying to down' in expect_and_capture_ansible_exception(ucm_obj.online_or_offline_adapter, 'fail', 'down', '0f')['msg'] + assert 'Error trying to up' in expect_and_capture_ansible_exception(ucm_obj.online_or_offline_adapter, 'fail', 'up', '0f')['msg'] + assert 'Error: Adapter(s) 0f not exist' in expect_and_capture_ansible_exception(ucm_obj.get_adapters_uuids, 'fail')['msg'] + + +def test_change_mode_from_cna_to_fc_rest(): + ''' configuring ucadaptor ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'private/cli/ucadmin', SRR['ucm_info_cna']), + ('GET', 'network/fc/ports', SRR['fc_adapter_info']), + ('PATCH', 'network/fc/ports/abcdef', SRR['success']), + ('PATCH', 'private/cli/ucadmin', SRR['success']), + ('PATCH', 'network/fc/ports/abcdef', SRR['success']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'private/cli/ucadmin', SRR['ucm_info_cna']) + ]) + assert create_and_apply(ucadapter_module, DEFAULT_ARGS, {'use_rest': 'always'})['changed'] + args = {'mode': 'cna', 'use_rest': 'always'} + assert not create_and_apply(ucadapter_module, DEFAULT_ARGS, args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_unix_group.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_unix_group.py new file mode 100644 index 000000000..a29779e5c --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_unix_group.py @@ -0,0 +1,545 @@ +# (c) 2019-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_unix_group \ + import NetAppOntapUnixGroup as group_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'user_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "vserver" + }, + "name": "user_group", + "id": 1, + "users": [{"name": "user1"}, {"name": "user2"}], + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + } + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + +unix_group_info = { + 'num-records': 1, + 'attributes-list': { + 'unix-group-info': { + 'group-name': 'user_group', + 'group-id': '1', + 'users': [{'unix-user-name': {'user-name': 'user1'}}] + } + } +} + + +ZRR = zapi_responses({ + 'unix_group_info': build_zapi_response(unix_group_info) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'name': 'user_group', + 'id': '1', + 'use_rest': 'never', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + group_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get_nonexistent_user_group(): + ''' Test if get_unix_group returns None for non-existent group ''' + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['empty']) + ]) + user_obj = create_module(group_module, DEFAULT_ARGS) + result = user_obj.get_unix_group() + assert result is None + + +def test_get_user_group(): + ''' Test if get_unix_group returns unix group ''' + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']) + ]) + user_obj = create_module(group_module, DEFAULT_ARGS) + result = user_obj.get_unix_group() + assert result + + +def test_get_error_existent_user_group(): + ''' Test if get_unix_user returns existent user group ''' + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['error']) + ]) + group_module_object = create_module(group_module, DEFAULT_ARGS) + msg = "Error getting UNIX group" + assert msg in expect_and_capture_ansible_exception(group_module_object.get_unix_group, 'fail')['msg'] + + +def test_create_unix_group_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['empty']), + ('name-mapping-unix-group-create', ZRR['success']), + ]) + module_args = { + 'name': 'user_group', + 'id': '1' + } + assert create_and_apply(group_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_unix_group_with_user_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['empty']), + ('name-mapping-unix-group-create', ZRR['success']), + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-add-user', ZRR['success']) + ]) + module_args = { + 'name': 'user_group', + 'id': '1', + 'users': ['user1', 'user2'] + } + assert create_and_apply(group_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_unix_user_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['empty']), + ('name-mapping-unix-group-create', ZRR['error']), + ]) + module_args = { + 'name': 'user_group', + 'id': '1', + 'users': ['user1', 'user2'] + } + error = create_and_apply(group_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error creating UNIX group" + assert msg in error + + +def test_delete_unix_group_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-destroy', ZRR['success']), + ]) + module_args = { + 'name': 'user_group', + 'state': 'absent' + } + assert create_and_apply(group_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_remove_unix_group_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-destroy', ZRR['error']), + ]) + module_args = { + 'name': 'user_group', + 'state': 'absent' + } + error = create_and_apply(group_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error removing UNIX group" + assert msg in error + + +def test_create_idempotent(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']) + ]) + module_args = { + 'state': 'present', + 'name': 'user_group', + 'id': '1', + } + assert not create_and_apply(group_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_idempotent(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['empty']) + ]) + module_args = { + 'state': 'absent', + 'name': 'user_group', + } + assert not create_and_apply(group_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_unix_group_id_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-modify', ZRR['success']), + ]) + module_args = { + 'name': 'user_group', + 'id': '2' + } + assert create_and_apply(group_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify_unix_group_id_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-modify', ZRR['error']), + ]) + module_args = { + 'name': 'user_group', + 'id': '2' + } + error = create_and_apply(group_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error modifying UNIX group" + assert msg in error + + +def test_add_unix_group_user_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-add-user', ZRR['success']) + ]) + module_args = { + 'name': 'user_group', + 'users': ['user1', 'user2'] + } + assert create_and_apply(group_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_add_unix_group_user_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-add-user', ZRR['error']) + ]) + module_args = { + 'name': 'user_group', + 'users': ['user1', 'user2'] + } + error = create_and_apply(group_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error adding user" + assert msg in error + + +def test_delete_unix_group_user_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-delete-user', ZRR['success']) + ]) + module_args = { + 'name': 'user_group', + 'users': '' + } + assert create_and_apply(group_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_delete_unix_group_user_zapi(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-get-iter', ZRR['unix_group_info']), + ('name-mapping-unix-group-delete-user', ZRR['error']) + ]) + module_args = { + 'name': 'user_group', + 'users': '' + } + error = create_and_apply(group_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error deleting user" + assert msg in error + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('name-mapping-unix-group-get-iter', ZRR['error']), + ('name-mapping-unix-group-create', ZRR['error']), + ('name-mapping-unix-group-destroy', ZRR['error']), + ('name-mapping-unix-group-modify', ZRR['error']), + ]) + module_args = {'use_rest': 'never', 'name': 'user_group'} + my_obj = create_module(group_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.get_unix_group, 'fail')['msg'] + assert 'Error getting UNIX group user_group: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.create_unix_group, 'fail')['msg'] + assert 'Error creating UNIX group user_group: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.delete_unix_group, 'fail')['msg'] + assert 'Error removing UNIX group user_group: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.modify_unix_group, 'fail', 'name-mapping-unix-group-modify')['msg'] + assert 'Error modifying UNIX group user_group: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'vserver', + 'name': 'user_group', + 'id': '1' +} + + +def test_get_nonexistent_user_group_rest(): + ''' Test if get_unix_user returns None for non-existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['empty_records']), + ]) + user_obj = create_module(group_module, ARGS_REST) + result = user_obj.get_unix_group_rest() + assert result is None + + +def test_get_existent_user_group_rest(): + ''' Test if get_unix_user returns existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ]) + user_obj = create_module(group_module, ARGS_REST) + result = user_obj.get_unix_group_rest() + assert result + + +def test_get_error_existent_user_group_rest(): + ''' Test if get_unix_user returns existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['generic_error']), + ]) + error = create_and_apply(group_module, ARGS_REST, fail=True)['msg'] + msg = "Error getting UNIX group:" + assert msg in error + + +def test_ontap_version_rest(): + ''' Test ONTAP version ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ]) + module_args = {'use_rest': 'always'} + error = create_module(group_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error: REST requires ONTAP 9.9.1 or later for UNIX group APIs." + assert msg in error + + +def test_create_unix_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['empty_records']), + ('POST', 'name-services/unix-groups', SRR['empty_good']), + ]) + module_args = { + 'name': 'user_group', + 'id': 1, + } + assert create_and_apply(group_module, ARGS_REST, module_args)['changed'] + + +def test_create_unix_group_with_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['empty_records']), + ('POST', 'name-services/unix-groups', SRR['empty_good']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('POST', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group/users', SRR['empty_records']) + ]) + module_args = { + 'name': 'user_group', + 'id': 1, + 'users': ['user1', 'user2', 'user3'] + } + assert create_and_apply(group_module, ARGS_REST, module_args)['changed'] + + +def test_error_create_unix_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['empty_records']), + ('POST', 'name-services/unix-groups', SRR['generic_error']), + ]) + module_args = { + 'name': 'user_group', + 'id': 1, + } + error = create_and_apply(group_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error creating UNIX group:" + assert msg in error + + +def test_delete_unix_group_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('DELETE', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group', SRR['empty_good']), + ]) + module_args = { + 'name': 'user_group', + 'state': 'absent' + } + assert create_and_apply(group_module, ARGS_REST, module_args)['changed'] + + +def test_error_remove_unix_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('DELETE', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group', SRR['generic_error']), + ]) + module_args = { + 'name': 'user_group', + 'state': 'absent' + } + error = create_and_apply(group_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error deleting UNIX group:" + assert msg in error + + +def test_modify_unix_group_id_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('PATCH', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group', SRR['empty_good']) + ]) + module_args = { + 'name': 'user_group', + 'id': '2' + } + assert create_and_apply(group_module, ARGS_REST, module_args)['changed'] + + +def test_error_modify_unix_group_id_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('PATCH', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group', SRR['generic_error']) + ]) + module_args = { + 'name': 'user_group', + 'id': '2' + } + error = create_and_apply(group_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on modifying UNIX group:" + assert msg in error + + +def test_create_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ]) + module_args = { + 'state': 'present', + 'name': 'user_group', + 'id': '1', + } + assert not create_and_apply(group_module, ARGS_REST, module_args)['changed'] + + +def test_delete_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['empty_records']), + ]) + module_args = { + 'state': 'absent', + 'name': 'user_group' + } + assert not create_and_apply(group_module, ARGS_REST, module_args)['changed'] + + +def test_add_unix_group_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('POST', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group/users', SRR['empty_records']) + ]) + module_args = { + 'name': 'user_group', + 'users': ['user1', 'user2', 'user3'] + } + assert create_and_apply(group_module, ARGS_REST, module_args)['changed'] + + +def test_error_add_unix_group_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('POST', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group/users', SRR['generic_error']) + ]) + module_args = { + 'name': 'user_group', + 'users': ['user1', 'user2', 'user3'] + } + error = create_and_apply(group_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error Adding user to UNIX group:" + assert msg in error + + +def test_delete_unix_group_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('DELETE', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group/users/user2', SRR['empty_records']) + ]) + module_args = { + 'users': ["user1"] + } + assert create_and_apply(group_module, ARGS_REST, module_args)['changed'] + + +def test_error_delete_unix_group_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_1']), + ('GET', 'name-services/unix-groups', SRR['user_record']), + ('DELETE', 'name-services/unix-groups/671aa46e-11ad-11ec-a267-005056b30cfa/user_group/users/user2', SRR['generic_error']) + ]) + module_args = { + 'users': ["user1"] + } + error = create_and_apply(group_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error removing user from UNIX group:" + assert msg in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_unix_user.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_unix_user.py new file mode 100644 index 000000000..1f6fc0847 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_unix_user.py @@ -0,0 +1,465 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_unix_user \ + import NetAppOntapUnixUser as user_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'user_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "vserver" + }, + "name": "user", + "primary_gid": 2, + "id": 1, + "full_name": "test_user", + "target": { + "name": "20:05:00:50:56:b3:0c:fa" + } + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + +unix_user_info = { + 'num-records': 1, + 'attributes-list': { + 'unix-user-info': { + 'name': 'user', + 'user-id': '1', + 'group-id': 2, + 'full-name': 'test_user'} + } +} + +ZRR = zapi_responses({ + 'unix_user_info': build_zapi_response(unix_user_info) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'name': 'user', + 'group_id': 2, + 'id': '1', + 'full_name': 'test_user', + 'use_rest': 'never', +} + + +DEFAULT_NO_USER = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'name': 'no_user', + 'group_id': '2', + 'id': '1', + 'full_name': 'test_user', + 'use_rest': 'never', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + user_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get_nonexistent_user(): + ''' Test if get_unix_user returns None for non-existent user ''' + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['empty']) + ]) + user_obj = create_module(user_module, DEFAULT_NO_USER) + result = user_obj.get_unix_user() + assert result is None + + +def test_get_existent_user(): + ''' Test if get_unix_user returns existent user ''' + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['unix_user_info']) + ]) + user_obj = create_module(user_module, DEFAULT_ARGS) + result = user_obj.get_unix_user() + assert result + + +def test_get_error_existent_user(): + ''' Test if get_unix_user returns existent user ''' + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['error']) + ]) + user_module_object = create_module(user_module, DEFAULT_ARGS) + msg = "Error getting UNIX user" + assert msg in expect_and_capture_ansible_exception(user_module_object.get_unix_user, 'fail')['msg'] + + +def test_create_unix_user_zapi(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['empty']), + ('name-mapping-unix-user-create', ZRR['success']), + ]) + module_args = { + 'name': 'user', + 'group_id': '2', + 'id': '1', + 'full_name': 'test_user', + } + assert create_and_apply(user_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_create_unix_user_zapi(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['empty']), + ('name-mapping-unix-user-create', ZRR['error']), + ]) + module_args = { + 'name': 'user4', + 'group_id': '4', + 'id': '4', + 'full_name': 'test_user4', + } + error = create_and_apply(user_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error creating UNIX user" + assert msg in error + + +def test_delete_unix_user_zapi(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['unix_user_info']), + ('name-mapping-unix-user-destroy', ZRR['success']), + ]) + module_args = { + 'name': 'user', + 'group_id': '2', + 'id': '1', + 'full_name': 'test_user', + 'state': 'absent' + } + assert create_and_apply(user_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_remove_unix_user_zapi(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['unix_user_info']), + ('name-mapping-unix-user-destroy', ZRR['error']), + ]) + module_args = { + 'name': 'user', + 'group_id': '2', + 'id': '1', + 'full_name': 'test_user', + 'state': 'absent' + } + error = create_and_apply(user_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error removing UNIX user" + assert msg in error + + +def test_modify_unix_user_id_zapi(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['unix_user_info']), + ('name-mapping-unix-user-modify', ZRR['success']), + ]) + module_args = { + 'group_id': '3', + 'id': '2' + } + assert create_and_apply(user_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_unix_user_full_name_zapi(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['unix_user_info']), + ('name-mapping-unix-user-modify', ZRR['success']), + ]) + module_args = { + 'full_name': 'test_user1' + } + assert create_and_apply(user_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify_unix_user_full_name_zapi(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['unix_user_info']), + ('name-mapping-unix-user-modify', ZRR['error']), + ]) + module_args = { + 'full_name': 'test_user1' + } + error = create_and_apply(user_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + msg = "Error modifying UNIX user" + assert msg in error + + +def test_create_idempotent(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['unix_user_info']) + ]) + module_args = { + 'state': 'present', + 'name': 'user', + 'group_id': 2, + 'id': '1', + 'full_name': 'test_user', + } + assert not create_and_apply(user_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_idempotent(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['empty']) + ]) + module_args = { + 'state': 'absent' + } + assert not create_and_apply(user_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('name-mapping-unix-user-get-iter', ZRR['error']), + ('name-mapping-unix-user-create', ZRR['error']), + ('name-mapping-unix-user-destroy', ZRR['error']), + ('name-mapping-unix-user-modify', ZRR['error']) + ]) + module_args = {'id': 5} + my_obj = create_module(user_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.get_unix_user, 'fail')['msg'] + assert 'Error getting UNIX user user: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.create_unix_user, 'fail')['msg'] + assert 'Error creating UNIX user user: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.delete_unix_user, 'fail')['msg'] + assert 'Error removing UNIX user user: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.modify_unix_user, 'fail', 'name-mapping-unix-user-modify')['msg'] + assert 'Error modifying UNIX user user: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'vserver', + 'name': 'user', + 'primary_gid': 2, + 'id': 1, + 'full_name': 'test_user' +} + +REST_NO_USER = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'vserver', + 'name': 'user5', + 'primary_gid': 2, + 'id': 1, + 'full_name': 'test_user' +} + + +def test_get_nonexistent_user_rest_rest(): + ''' Test if get_unix_user returns None for non-existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['empty_records']), + ]) + user_obj = create_module(user_module, REST_NO_USER) + result = user_obj.get_unix_user_rest() + assert result is None + + +def test_get_existent_user_rest(): + ''' Test if get_unix_user returns existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['user_record']), + ]) + user_obj = create_module(user_module, ARGS_REST) + result = user_obj.get_unix_user_rest() + assert result + + +def test_get_error_existent_user_rest(): + ''' Test if get_unix_user returns existent user ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['generic_error']), + ]) + error = create_and_apply(user_module, ARGS_REST, fail=True)['msg'] + msg = "Error on getting unix-user info:" + assert msg in error + + +def test_create_unix_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['empty_records']), + ('POST', 'name-services/unix-users', SRR['empty_good']), + ]) + module_args = { + 'name': 'user', + 'primary_gid': 2, + 'id': 1, + 'full_name': 'test_user', + } + assert create_and_apply(user_module, ARGS_REST, module_args)['changed'] + + +def test_error_create_unix_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['empty_records']), + ('POST', 'name-services/unix-users', SRR['generic_error']), + ]) + module_args = { + 'name': 'user4', + 'primary_gid': 4, + 'id': 4, + 'full_name': 'test_user4', + } + error = create_and_apply(user_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on creating unix-user:" + assert msg in error + + +def test_delete_unix_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['user_record']), + ('DELETE', 'name-services/unix-users/671aa46e-11ad-11ec-a267-005056b30cfa/user', SRR['empty_good']), + ]) + module_args = { + 'name': 'user', + 'group_id': '2', + 'id': '1', + 'full_name': 'test_user', + 'state': 'absent' + } + assert create_and_apply(user_module, ARGS_REST, module_args)['changed'] + + +def test_error_remove_unix_user_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['user_record']), + ('DELETE', 'name-services/unix-users/671aa46e-11ad-11ec-a267-005056b30cfa/user', SRR['generic_error']) + ]) + module_args = { + 'name': 'user', + 'id': '1', + 'state': 'absent' + } + error = create_and_apply(user_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on deleting unix-user" + assert msg in error + + +def test_modify_unix_user_id_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['user_record']), + ('PATCH', 'name-services/unix-users/671aa46e-11ad-11ec-a267-005056b30cfa/user', SRR['empty_good']) + ]) + module_args = { + 'name': 'user', + 'group_id': '3', + 'id': '2' + } + assert create_and_apply(user_module, ARGS_REST, module_args)['changed'] + + +def test_modify_unix_user_full_name_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['user_record']), + ('PATCH', 'name-services/unix-users/671aa46e-11ad-11ec-a267-005056b30cfa/user', SRR['empty_good']) + ]) + module_args = { + 'name': 'user', + 'full_name': 'test_user1' + } + assert create_and_apply(user_module, ARGS_REST, module_args)['changed'] + + +def test_error_modify_unix_user_full_name_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['user_record']), + ('PATCH', 'name-services/unix-users/671aa46e-11ad-11ec-a267-005056b30cfa/user', SRR['generic_error']) + ]) + module_args = { + 'name': 'user', + 'full_name': 'test_user1' + } + error = create_and_apply(user_module, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on modifying unix-user:" + assert msg in error + + +def test_create_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['user_record']), + ]) + module_args = { + 'state': 'present', + 'name': 'user', + 'group_id': 2, + 'id': '1', + 'full_name': 'test_user', + } + assert not create_and_apply(user_module, ARGS_REST, module_args)['changed'] + + +def test_delete_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'name-services/unix-users', SRR['empty_records']), + ]) + module_args = { + 'state': 'absent' + } + assert not create_and_apply(user_module, ARGS_REST, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user.py new file mode 100644 index 000000000..4b3294798 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user.py @@ -0,0 +1,744 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_user ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_error_message, rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_module, expect_and_capture_ansible_exception, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_user import NetAppOntapUser as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'repeated_password': (400, None, {'message': "New password must be different than the old password."}), + 'get_uuid': (200, {'owner': {'uuid': 'ansible'}}, None), + 'get_user_rest': (200, + {'num_records': 1, + 'records': [{'owner': {'uuid': 'ansible_vserver'}, + 'name': 'abcd'}]}, None), + 'get_user_rest_multiple': (200, + {'num_records': 2, + 'records': [{'owner': {'uuid': 'ansible_vserver'}, + 'name': 'abcd'}, + {}]}, None), + 'get_user_details_rest': (200, + {'role': {'name': 'vsadmin'}, + 'applications': [{'application': 'http'}], + 'locked': False}, None), + 'get_user_details_rest_no_pwd': (200, # locked is absent if no password was set + {'role': {'name': 'vsadmin'}, + 'applications': [{'application': 'http'}], + }, None) +}, True) + + +def login_info(locked, role_name, apps): + attributes_list = [] + for app in apps: + if app in ('console', 'service-processor'): + attributes_list.append( + {'security-login-account-info': { + 'is-locked': locked, 'role-name': role_name, 'application': app, 'authentication-method': 'password'}} + ) + if app in ('ssh',): + attributes_list.append( + {'security-login-account-info': { + 'is-locked': locked, 'role-name': role_name, 'application': 'ssh', 'authentication-method': 'publickey', + 'second-authentication-method': 'password'}}, + ) + if app in ('http',): + attributes_list.extend([ + {'security-login-account-info': { + 'is-locked': locked, 'role-name': role_name, 'application': 'http', 'authentication-method': 'password'}}, + ]) + return { + 'num-records': len(attributes_list), + 'attributes-list': attributes_list + } + + +ZRR = zapi_responses({ + 'login_locked_user': build_zapi_response(login_info("true", 'user', ['console', 'ssh'])), + 'login_unlocked_user': build_zapi_response(login_info("False", 'user', ['console', 'ssh'])), + 'login_unlocked_user_http': build_zapi_response(login_info("False", 'user', ['http'])), + 'login_unlocked_user_service_processor': build_zapi_response(login_info("False", 'user', ['service-processor'])), + 'user_not_found': build_zapi_error('16034', "This exception should not be seen"), + 'internal_error': build_zapi_error('13114', "Forcing an internal error"), + 'reused_password': build_zapi_error('13214', "New password must be different than last 6 passwords."), +}, True) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'user_name', + 'vserver': 'vserver', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + } + print('Info: %s' % call_main(my_main, {}, module_args, fail=True)['msg']) + + +def test_module_fail_when_vserver_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'user_name', + } + assert 'Error: vserver is required' in call_main(my_main, {}, module_args, fail=True)['msg'] + + +def test_ensure_user_get_called(): + ''' a more interesting test ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'role_name': 'test', + 'applications': 'http', + 'authentication_method': 'password', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + # app = dict(application='testapp', authentication_methods=['testam']) + user_info = my_obj.get_user() + print('Info: test_user_get: %s' % repr(user_info)) + assert user_info is None + + +def test_ensure_user_get_called_not_found(): + ''' a more interesting test ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['user_not_found']), + ]) + module_args = { + 'use_rest': 'never', + 'role_name': 'test', + 'applications': 'http', + 'authentication_method': 'password', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + # app = dict(application='testapp', authentication_methods=['testam']) + user_info = my_obj.get_user() + print('Info: test_user_get: %s' % repr(user_info)) + assert user_info is None + + +def test_ensure_user_apply_called(): + ''' creating user and checking idempotency ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['success']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user_http']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'create', + 'role_name': 'user', + 'applications': 'http', + 'authentication_method': 'password', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_sp_apply_called(): + ''' creating user with service_processor application and idempotency ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user_service_processor']), + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user_service_processor']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'create', + 'role_name': 'user', + 'applications': 'service-processor', + 'authentication_method': 'password', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['applications'] = 'service_processor' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_apply_for_delete_called(): + ''' deleting user and checking idempotency ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ]) + module_args = { + "use_rest": "never", + "state": "absent", + 'name': 'create', + 'role_name': 'user', + 'applications': 'console', + 'authentication_method': 'password', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_lock_called(): + ''' changing user_lock to True and checking idempotency''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-lock', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + "lock_user": False, + 'name': 'create', + 'role_name': 'user', + 'applications': 'console', + 'authentication_method': 'password', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['lock_user'] = 'true' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_unlock_called(): + ''' changing user_lock to False and checking idempotency''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_locked_user']), + ('ZAPI', 'security-login-get-iter', ZRR['login_locked_user']), + ('ZAPI', 'security-login-unlock', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + "lock_user": True, + 'name': 'create', + 'role_name': 'user', + 'applications': 'console', + 'authentication_method': 'password', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['lock_user'] = 'false' + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_set_password_called(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-modify-password', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'create', + 'role_name': 'user', + 'applications': 'console', + 'authentication_method': 'password', + 'set_password': '123456', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_set_password_internal_error(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-modify-password', ZRR['internal_error']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'create', + 'role_name': 'user', + 'applications': 'console', + 'authentication_method': 'password', + 'set_password': '123456', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert not my_obj.change_password() + + +def test_set_password_reused(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-modify-password', ZRR['reused_password']) + ]) + module_args = { + 'use_rest': 'never', + 'name': 'create', + 'role_name': 'user', + 'applications': 'console', + 'authentication_method': 'password', + 'set_password': '123456', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert not my_obj.change_password() + + +def test_ensure_user_role_update_called(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-modify', ZRR['success']), + ('ZAPI', 'security-login-modify-password', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'create', + 'role_name': 'test123', + 'applications': 'console', + 'authentication_method': 'password', + 'set_password': '123456', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_role_update_additional_application_called(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-modify-password', ZRR['success']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'create', + 'role_name': 'test123', + 'applications': 'http', + 'authentication_method': 'password', + 'set_password': '123456', + 'replace_existing_apps_and_methods': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['error']), + ('ZAPI', 'security-login-create', ZRR['error']), + ('ZAPI', 'security-login-lock', ZRR['error']), + ('ZAPI', 'security-login-unlock', ZRR['error']), + ('ZAPI', 'security-login-delete', ZRR['error']), + ('ZAPI', 'security-login-modify-password', ZRR['error']), + ('ZAPI', 'security-login-modify', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never', + 'role_name': 'test', + 'applications': 'console', + 'authentication_method': 'password', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + app = dict(application='console', authentication_methods=['password']) + assert zapi_error_message('Error getting user user_name') in expect_and_capture_ansible_exception(my_obj.get_user, 'fail')['msg'] + assert zapi_error_message('Error creating user user_name') in expect_and_capture_ansible_exception(my_obj.create_user, 'fail', app)['msg'] + assert zapi_error_message('Error locking user user_name') in expect_and_capture_ansible_exception(my_obj.lock_given_user, 'fail')['msg'] + assert zapi_error_message('Error unlocking user user_name') in expect_and_capture_ansible_exception(my_obj.unlock_given_user, 'fail')['msg'] + assert zapi_error_message('Error removing user user_name') in expect_and_capture_ansible_exception(my_obj.delete_user, 'fail', app)['msg'] + assert zapi_error_message('Error setting password for user user_name') in expect_and_capture_ansible_exception(my_obj.change_password, 'fail')['msg'] + assert zapi_error_message('Error modifying user user_name') in expect_and_capture_ansible_exception(my_obj.modify_user, 'fail', app, ['password'])['msg'] + err_msg = 'vserver is required with ZAPI' + assert err_msg in create_module(my_module, DEFAULT_ARGS, {'use_rest': 'never', 'svm': None}, fail=True)['msg'] + + +def test_create_user_with_usm_auth(): + ''' switching back to ZAPI ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ('ZAPI', 'security-login-create', ZRR['success']), + ]) + module_args = { + 'use_rest': 'auto', + 'applications': 'snmp', + 'authentication_method': 'usm', + 'name': 'create', + 'role_name': 'test123', + 'set_password': '123456', + 'remote_switch_ipaddress': '12.34.56.78', + 'authentication_password': 'auth_pwd', + 'authentication_protocol': 'md5', + 'privacy_password': 'auth_pwd', + 'privacy_protocol': 'des', + 'engine_id': 'engine_123', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_error_applications_snmp(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + module_args = { + 'use_rest': 'always', + 'applications': 'snmp', + 'authentication_method': 'usm', + 'name': 'create', + 'role_name': 'test123', + 'set_password': '123456', + } + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == "snmp as application is not supported in REST." + + +def test_ensure_user_get_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'applications': ['http', 'ontapi'], + 'authentication_method': 'password', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_user_rest() is not None + + +def test_ensure_create_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['zero_records']), + ('POST', 'security/accounts', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'applications': ['http', 'ontapi'], + 'authentication_method': 'password', + 'set_password': 'xfjjttjwll`1', + 'lock_user': True + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_create_cluster_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['zero_records']), + ('POST', 'security/accounts', SRR['empty_good']), + ]) + module_args = { + "hostname": "hostname", + "username": "username", + "password": "password", + "name": "user_name", + "use_rest": "always", + 'role_name': 'vsadmin', + 'applications': ['http', 'ontapi'], + 'authentication_method': 'password', + 'set_password': 'xfjjttjwll`1', + 'lock_user': True + } + assert call_main(my_main, module_args)['changed'] + + +def test_ensure_delete_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('DELETE', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'state': 'absent', + 'role_name': 'vsadmin', + 'applications': ['http', 'ontapi'], + 'authentication_method': 'password', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_modify_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application': 'ssh', + 'authentication_method': 'password', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_lock_unlock_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'applications': 'http', + 'authentication_method': 'password', + 'lock_user': True, + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_change_password_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ]) + module_args = { + 'set_password': 'newvalue', + 'use_rest': 'always', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_change_password_user_rest_check_mode(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ]) + module_args = { + 'set_password': 'newvalue', + 'use_rest': 'always', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.module.check_mode = True + assert expect_and_capture_ansible_exception(my_obj.apply, 'exit')['changed'] + + +def test_existing_password(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['repeated_password']), # password + ]) + module_args = { + 'set_password': 'newvalue', + 'use_rest': 'always', + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_rest_unsupported_property(): + register_responses([ + ]) + module_args = { + 'privacy_password': 'value', + 'use_rest': 'always', + } + msg = "REST API currently does not support 'privacy_password'" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_negative_zapi_missing_netapp_lib(mock_has): + register_responses([ + ]) + mock_has.return_value = False + module_args = { + 'use_rest': 'never', + } + msg = "Error: the python NetApp-Lib module is required. Import error: None" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_zapi_missing_apps(): + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + } + msg = "application_dicts or application_strs is a required parameter with ZAPI" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rest_error_on_get_user(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + } + msg = "Error while fetching user info: Expected error" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rest_error_on_get_user_multiple(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest_multiple']), + ]) + module_args = { + 'use_rest': 'always', + } + msg = "Error while fetching user info, found multiple entries:" + assert msg in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rest_error_on_get_user_details(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + } + msg = "Error while fetching user details: Expected error" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rest_error_on_delete(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('DELETE', 'security/accounts/ansible_vserver/abcd', SRR['generic_error']), + ]) + module_args = { + "use_rest": "always", + 'state': 'absent', + 'role_name': 'vsadmin', + } + msg = "Error while deleting user: Expected error" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rest_error_on_unlocking(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['generic_error']), + ]) + module_args = { + 'use_rest': 'always', + 'lock_user': True + } + msg = "Error while locking/unlocking user: Expected error" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rest_error_on_unlocking_no_password(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest_no_pwd']), + ]) + module_args = { + 'use_rest': 'always', + 'lock_user': True + } + msg = "Error: cannot modify lock state if password is not set." + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rest_error_on_changing_password(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['generic_error']), + ]) + module_args = { + 'set_password': '12345', + 'use_rest': 'always', + } + msg = "Error while updating user password: Expected error" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_rest_error_on_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['generic_error']), + ]) + module_args = { + 'role_name': 'vsadmin2', + 'use_rest': 'always', + 'applications': ['http', 'ontapi'], + 'authentication_method': 'password', + } + msg = "Error while modifying user details: Expected error" + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_rest_unlocking_with_password(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest_no_pwd']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['success']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['success']), + ]) + module_args = { + 'set_password': 'ansnssnajj12%', + 'use_rest': 'always', + 'lock_user': True + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_create_validations(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['zero_records']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['zero_records']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['zero_records']), + ]) + module_args = { + 'use_rest': 'always', + } + msg = 'Error: missing required parameters for create: role_name and: application_dicts or application_strs.' + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['role_name'] = 'role' + msg = 'Error: missing required parameter for create: application_dicts or application_strs.' + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args.pop('role_name') + module_args['applications'] = 'http' + module_args['authentication_method'] = 'password' + msg = 'Error: missing required parameter for create: role_name.' + assert msg == call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_dicts.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_dicts.py new file mode 100644 index 000000000..a4181d54d --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_dicts.py @@ -0,0 +1,589 @@ +# (c) 2018 - 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_user ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, print_requests, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_error_message, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + expect_and_capture_ansible_exception, call_main, create_module, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_user import NetAppOntapUser as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'invalid_value_error': (400, None, {'message': "invalid value service_processor"}), + 'get_user_rest': (200, + {'num_records': 1, + 'records': [{'owner': {'uuid': 'ansible_vserver'}, + 'name': 'abcd'}]}, None), + 'get_user_details_rest': (200, + {'role': {'name': 'vsadmin'}, + 'applications': [{'application': 'http', 'authentication-method': 'password', 'second_authentication_method': 'none'}], + 'locked': False}, None) +}) + + +def login_info(locked, role_name, apps): + attributes_list = [] + for app in apps: + if app in ('console', 'service-processor',): + attributes_list.append( + {'security-login-account-info': { + 'is-locked': locked, 'role-name': role_name, 'application': app, 'authentication-method': 'password'}} + ) + if app in ('ssh',): + attributes_list.append( + {'security-login-account-info': { + 'is-locked': locked, 'role-name': role_name, 'application': 'ssh', 'authentication-method': 'publickey', + 'second-authentication-method': 'password'}}, + ) + if app in ('http',): + attributes_list.extend([ + {'security-login-account-info': { + 'is-locked': locked, 'role-name': role_name, 'application': 'http', 'authentication-method': 'password'}}, + {'security-login-account-info': { + 'is-locked': locked, 'role-name': role_name, 'application': 'http', 'authentication-method': 'saml'}}, + ]) + return { + 'num-records': len(attributes_list), + 'attributes-list': attributes_list + } + + +ZRR = zapi_responses({ + 'login_locked_user': build_zapi_response(login_info("true", 'user', ['console', 'ssh'])), + 'login_unlocked_user': build_zapi_response(login_info("False", 'user', ['console', 'ssh'])), + 'login_unlocked_user_console': build_zapi_response(login_info("False", 'user', ['console'])), + 'login_unlocked_user_service_processor': build_zapi_response(login_info("False", 'user', ['service-processor'])), + 'login_unlocked_user_ssh': build_zapi_response(login_info("False", 'user', ['ssh'])), + 'login_unlocked_user_http': build_zapi_response(login_info("False", 'user', ['http'])) +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'user_name', + 'vserver': 'vserver', +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + "use_rest": "never" + } + print('Info: %s' % call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg']) + + +def test_module_fail_when_application_name_is_repeated(): + ''' required arguments are reported as errors ''' + register_responses([ + ]) + module_args = { + "use_rest": "never", + "application_dicts": [ + {'application': 'ssh', 'authentication_methods': ['cert']}, + {'application': 'ssh', 'authentication_methods': ['password']}] + } + error = 'Error: repeated application name: ssh. Group all authentication methods under a single entry.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_ensure_user_get_called(): + ''' a more interesting test ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user_http']), + ]) + module_args = { + "use_rest": "never", + 'role_name': 'test', + 'applications': 'console', + 'authentication_method': 'password', + 'replace_existing_apps_and_methods': 'always' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + user_info = my_obj.get_user() + print('Info: test_user_get: %s' % repr(user_info)) + assert 'saml' in user_info['applications'][0]['authentication_methods'] + + +def test_ensure_user_apply_called_replace(): + ''' creating user and checking idempotency ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'user', + 'applications': 'console', + 'authentication_method': 'password', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_apply_called_using_dict(): + ''' creating user and checking idempotency ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user_ssh']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'user', + 'application_dicts': [{ + 'application': 'ssh', + 'authentication_methods': ['publickey'], + 'second_authentication_method': 'password' + }] + } + + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + # BUG: SSH is not idempotent with SSH and replace_existing_apps_and_methods == 'auto' + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_apply_called_add(): + ''' creating user and checking idempotency ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-modify', ZRR['success']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user_console']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'user', + 'application_dicts': + [dict(application='console', authentication_methods=['password'])], + 'replace_existing_apps_and_methods': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_sp_apply_called(): + ''' creating user with service_processor application and idempotency ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user_service_processor']), + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user_service_processor']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'user', + 'application_dicts': + [dict(application='service-processor', authentication_methods=['password'])], + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['application_dicts'] = [dict(application='service_processor', authentication_methods=['password'])] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_apply_for_delete_called(): + ''' deleting user and checking idempotency ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-get-iter', ZRR['no_records']), + ]) + module_args = { + "use_rest": "never", + "state": "absent", + 'name': 'create', + 'role_name': 'user', + 'application_dicts': + [dict(application='console', authentication_methods=['password'])], + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_lock_called(): + ''' changing user_lock to True and checking idempotency''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-lock', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + "lock_user": False, + 'name': 'create', + 'role_name': 'user', + 'application_dicts': [ + dict(application='console', authentication_methods=['password']), + dict(application='ssh', authentication_methods=['publickey'], second_authentication_method='password') + ], + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['lock_user'] = True + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_unlock_called(): + ''' changing user_lock to False and checking idempotency''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_locked_user']), + ('ZAPI', 'security-login-get-iter', ZRR['login_locked_user']), + ('ZAPI', 'security-login-unlock', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + "lock_user": True, + 'name': 'create', + 'role_name': 'user', + 'application_dicts': [ + dict(application='console', authentication_methods=['password']), + dict(application='ssh', authentication_methods=['publickey'], second_authentication_method='password') + ], + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + module_args['lock_user'] = False + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_set_password_called(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-modify-password', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'user', + 'application_dicts': [ + dict(application='console', authentication_methods=['password']), + dict(application='ssh', authentication_methods=['publickey'], second_authentication_method='password') + ], + 'set_password': '123456', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_role_update_called(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-modify', ZRR['success']), + ('ZAPI', 'security-login-modify', ZRR['success']), + ('ZAPI', 'security-login-modify-password', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'test123', + 'application_dicts': [ + dict(application='console', authentication_methods=['password']), + dict(application='ssh', authentication_methods=['publickey'], second_authentication_method='password') + ], + 'set_password': '123456', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_role_update_additional_application_called(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-modify-password', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'test123', + 'application_dicts': + [dict(application='http', authentication_methods=['password'])], + 'set_password': '123456', + 'replace_existing_apps_and_methods': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_user_role_update_additional_method_called(): + ''' set password ''' + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['login_unlocked_user']), + ('ZAPI', 'security-login-create', ZRR['success']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-delete', ZRR['success']), + ('ZAPI', 'security-login-modify-password', ZRR['success']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'test123', + 'application_dicts': + [dict(application='console', authentication_methods=['domain'])], + 'set_password': '123456', + 'replace_existing_apps_and_methods': 'always' + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ZAPI', 'security-login-get-iter', ZRR['error']), + ('ZAPI', 'security-login-create', ZRR['error']), + ('ZAPI', 'security-login-lock', ZRR['error']), + ('ZAPI', 'security-login-unlock', ZRR['error']), + ('ZAPI', 'security-login-delete', ZRR['error']), + ('ZAPI', 'security-login-modify-password', ZRR['error']), + ('ZAPI', 'security-login-modify', ZRR['error']), + ]) + module_args = { + "use_rest": "never", + 'name': 'create', + 'role_name': 'test123', + 'application_dicts': + [dict(application='console', authentication_methods=['password'])], + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + app = dict(application='console', authentication_methods=['password']) + assert zapi_error_message('Error getting user create') in expect_and_capture_ansible_exception(my_obj.get_user, 'fail')['msg'] + assert zapi_error_message('Error creating user create') in expect_and_capture_ansible_exception(my_obj.create_user, 'fail', app)['msg'] + assert zapi_error_message('Error locking user create') in expect_and_capture_ansible_exception(my_obj.lock_given_user, 'fail')['msg'] + assert zapi_error_message('Error unlocking user create') in expect_and_capture_ansible_exception(my_obj.unlock_given_user, 'fail')['msg'] + assert zapi_error_message('Error removing user create') in expect_and_capture_ansible_exception(my_obj.delete_user, 'fail', app)['msg'] + assert zapi_error_message('Error setting password for user create') in expect_and_capture_ansible_exception(my_obj.change_password, 'fail')['msg'] + assert zapi_error_message('Error modifying user create') in expect_and_capture_ansible_exception(my_obj.modify_user, 'fail', app, ['password'])['msg'] + + +def test_rest_error_applications_snmp(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['get_user_rest']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'test123', + 'application_dicts': + [dict(application='snmp', authentication_methods=['usm'])], + 'set_password': '123456', + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == "snmp as application is not supported in REST." + + +def test_ensure_user_get_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application_dicts': + [dict(application='http', authentication_methods=['password']), + dict(application='ontapi', authentication_methods=['password'])], + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert my_obj.get_user_rest() is not None + + +def test_ensure_create_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['zero_records']), + ('POST', 'security/accounts', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application_dicts': + [dict(application='http', authentication_methods=['password']), + dict(application='ontapi', authentication_methods=['password'])], + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_delete_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('DELETE', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'state': 'absent', + 'role_name': 'vsadmin', + 'application_dicts': + [dict(application='http', authentication_methods=['password']), + dict(application='ontapi', authentication_methods=['password'])], + 'vserver': None + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_modify_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application_dicts': [dict(application='service_processor', authentication_methods=['usm'])] + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_lock_unlock_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application_dicts': + [dict(application='http', authentication_methods=['password'])], + 'lock_user': True, + } + print_requests() + # TODO: a single PATCH should be enough ? + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_change_password_user_rest_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['get_user_rest']), + ('GET', 'security/accounts/ansible_vserver/abcd', SRR['get_user_details_rest']), + ('PATCH', 'security/accounts/ansible_vserver/abcd', SRR['empty_good']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application_dicts': + [dict(application='http', authentication_methods=['password'])], + 'password': 'newvalue', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_sp_retry(): + """simulate error in create_user_rest and retry""" + register_responses([ + # retry followed by error + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['zero_records']), + ('POST', 'security/accounts', SRR['invalid_value_error']), + ('POST', 'security/accounts', SRR['generic_error']), + # retry followed by success + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'security/accounts', SRR['zero_records']), + ('POST', 'security/accounts', SRR['invalid_value_error']), + ('POST', 'security/accounts', SRR['success']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application_dicts': [ + dict(application='service_processor', authentication_methods=['usm']) + ] + } + assert 'invalid value' in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + module_args['application_dicts'] = [dict(application='service-processor', authentication_methods=['usm'])] + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_validate_application(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application_dicts': + [dict(application='http', authentication_methods=['password'])], + 'password': 'newvalue', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'second_authentication_method' in my_obj.parameters['applications'][0] + my_obj.parameters['applications'][0].pop('second_authentication_method') + my_obj.validate_applications() + assert 'second_authentication_method' in my_obj.parameters['applications'][0] + assert my_obj.parameters['applications'][0]['second_authentication_method'] is None + + +def test_sp_transform(): + current = {'applications': []} + sp_app_u = 'service_processor' + sp_app_d = 'service-processor' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ]) + # 1. no change using underscore + module_args = { + "use_rest": "always", + 'role_name': 'vsadmin', + 'application_dicts': [ + {'application': sp_app_u, 'authentication_methods': ['password']} + ], + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.change_sp_application([]) + sp_apps = [application['application'] for application in my_obj.parameters['applications'] if application['application'].startswith('service')] + assert sp_apps == [sp_app_u] + # 2. change underscore -> dash + my_obj.change_sp_application([{'application': sp_app_d}]) + sp_apps = [application['application'] for application in my_obj.parameters['applications'] if application['application'].startswith('service')] + assert sp_apps == [sp_app_d] + # 3. no change using dash + module_args['application_dicts'] = [{'application': sp_app_d, 'authentication_methods': ['password']}] + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + my_obj.change_sp_application([]) + sp_apps = [application['application'] for application in my_obj.parameters['applications'] if application['application'].startswith('service')] + assert sp_apps == [sp_app_d] + # 4. change dash -> underscore + my_obj.change_sp_application([{'application': sp_app_u}]) + sp_apps = [application['application'] for application in my_obj.parameters['applications'] if application['application'].startswith('service')] + assert sp_apps == [sp_app_u] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_role.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_role.py new file mode 100644 index 000000000..9fafd8a68 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_role.py @@ -0,0 +1,139 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_user_role \ + import NetAppOntapUserRole as role_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def build_role_info(access_level='all'): + return { + 'num-records': 1, + 'attributes-list': { + 'security-login-role-info': { + 'access-level': access_level, + 'command-directory-name': 'volume', + 'role-name': 'testrole', + 'role-query': 'show', + 'vserver': 'ansible' + } + } + } + + +ZRR = zapi_responses({ + 'build_role_info': build_zapi_response(build_role_info()), + 'build_role_modified': build_zapi_response(build_role_info('none')) +}) + +DEFAULT_ARGS = { + 'name': 'testrole', + 'vserver': 'ansible', + 'command_directory_name': 'volume', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'https': 'False', + 'use_rest': 'never' +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "name"] + error = create_module(role_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_get_nonexistent_policy(): + ''' Test if get_role returns None for non-existent role ''' + register_responses([ + ('ZAPI', 'security-login-role-get-iter', ZRR['empty']), + ]) + my_obj = create_module(role_module, DEFAULT_ARGS) + assert my_obj.get_role() is None + + +def test_get_existing_role(): + ''' Test if get_role returns details for existing role ''' + register_responses([ + ('ZAPI', 'security-login-role-get-iter', ZRR['build_role_info']), + ]) + my_obj = create_module(role_module, DEFAULT_ARGS) + current = my_obj.get_role() + assert current['name'] == DEFAULT_ARGS['name'] + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'security-login-role-get-iter', ZRR['empty']), + ('ZAPI', 'security-login-role-create', ZRR['success']), + # idempotency check + ('ZAPI', 'security-login-role-get-iter', ZRR['build_role_info']), + ]) + assert create_and_apply(role_module, DEFAULT_ARGS)['changed'] + assert not create_and_apply(role_module, DEFAULT_ARGS)['changed'] + + +def test_successful_modify(): + ''' Test successful modify ''' + register_responses([ + ('ZAPI', 'security-login-role-get-iter', ZRR['build_role_info']), + ('ZAPI', 'security-login-role-modify', ZRR['success']), + # idempotency check + ('ZAPI', 'security-login-role-get-iter', ZRR['build_role_modified']), + ]) + assert create_and_apply(role_module, DEFAULT_ARGS, {'access_level': 'none'})['changed'] + assert not create_and_apply(role_module, DEFAULT_ARGS, {'access_level': 'none'})['changed'] + + +def test_successful_delete(): + ''' Test delete existing role ''' + register_responses([ + ('ZAPI', 'security-login-role-get-iter', ZRR['build_role_info']), + ('ZAPI', 'security-login-role-delete', ZRR['success']), + # idempotency check + ('ZAPI', 'security-login-role-get-iter', ZRR['empty']), + ]) + assert create_and_apply(role_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + assert not create_and_apply(role_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('ZAPI', 'security-login-role-get-iter', ZRR['error']), + ('ZAPI', 'security-login-role-create', ZRR['error']), + ('ZAPI', 'security-login-role-modify', ZRR['error']), + ('ZAPI', 'security-login-role-delete', ZRR['error']) + ]) + my_obj = create_module(role_module, DEFAULT_ARGS) + assert 'Error getting role' in expect_and_capture_ansible_exception(my_obj.get_role, 'fail')['msg'] + assert 'Error creating role' in expect_and_capture_ansible_exception(my_obj.create_role, 'fail')['msg'] + assert 'Error modifying role' in expect_and_capture_ansible_exception(my_obj.modify_role, 'fail', {})['msg'] + assert 'Error removing role' in expect_and_capture_ansible_exception(my_obj.delete_role, 'fail')['msg'] + + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['command_directory_name'] + assert 'Error: command_directory_name is required' in create_module(role_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['vserver'] + assert 'Error: vserver is required' in create_module(role_module, DEFAULT_ARGS_COPY, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_role_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_role_rest.py new file mode 100644 index 000000000..b6e1e0b95 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_user_role_rest.py @@ -0,0 +1,647 @@ +# (c) 2022-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_user_role \ + import NetAppOntapUserRole as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'user_role_9_10': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [ + { + "access": "readonly", + "path": "/api/storage/volumes" + } + ], + "name": "admin", + "scope": "cluster" + }, None), + 'user_role_9_11_command': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [ + { + "path": "job schedule interval", + 'query': "-days <1 -hours >12" + }, { + 'path': 'DEFAULT', + 'access': 'none', + "_links": { + "self": { + "href": "/api/resourcelink" + }} + } + ], + "name": "admin", + "scope": "cluster" + }, None), + 'user_role_9_10_two_paths': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [ + { + "access": "readonly", + "path": "/api/storage/volumes" + }, + { + "access": "readonly", + "path": "/api/cluster/jobs", + } + ], + "name": "admin", + "scope": "cluster" + }, None), + 'user_role_9_10_two_paths_modified': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [ + {"access": "readonly", "path": "/api/storage/volumes"}, + {"access": "readonly", "path": "/api/cluster/jobs"} + ], + "name": "admin", + "scope": "cluster" + }, None), + 'user_role_9_11': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [ + { + "access": "readonly", + "path": "/api/cluster/jobs", + } + ], + "name": "admin", + "scope": "cluster" + }, None), + 'user_role_cluster_jobs_all': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [{"access": "all", "path": "/api/cluster/jobs"}], + "name": "admin", + "scope": "cluster" + }, None), + 'user_role_privileges': (200, { + "records": [ + { + "access": "readonly", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "path": "/api/cluster/jobs", + } + ], + }, None), + 'user_role_privileges_command': (200, { + "records": [ + { + "access": "all", + 'query': "-days <1 -hours >12", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "path": "job schedule interval", + } + ], + }, None), + 'user_role_privileges_two_paths': (200, { + "records": [ + { + "access": "readonly", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "path": "/api/cluster/jobs", + }, { + "access": "readonly", + "_links": { + "self": { + "href": "/api/resourcelink" + } + }, + "path": "/api/storage/volumes", + } + ], + }, None), + 'user_role_volume': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [ + { + "access": "readonly", + "path": "volume create" + }, + { + "access": "readonly", + "path": "volume modify", + }, + { + "access": "readonly", + "path": "volume show", + } + ], + "name": "admin", + }, None), + 'user_role_vserver': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [{"access": "readonly", "path": "vserver show"}], + "name": "admin", + }, None), + 'user_role_volume_privileges': (200, { + "records": [ + {"access": "readonly", "path": "volume create"}, + {"access": "readonly", "path": "volume modify"} + ], + }, None), + 'user_role_privileges_schedule': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [{"access": "all", "path": "job schedule interval", "query": "-days <1 -hours >12"}], + "name": "admin", + }, None), + 'user_role_privileges_schedule_modify': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [{"access": "all", "path": "job schedule interval", "query": "-days <1 -hours >8"}], + "name": "admin", + }, None), + 'user_role_volume_with_query': (200, { + "owner": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "privileges": [{"access": "readonly", "path": "/api/storage/volumes", "query": "-vserver vs1|vs2|vs3 -destination-aggregate aggr1|aggr2"}], + "name": "admin", + "scope": "cluster" + }, None), + "error_4": (409, None, {'code': 4, 'message': "entry doesn't exist, 'target': 'path'"}), +}) + +PRIVILEGES_SINGLE_WITH_QUERY = [{ + "path": "job schedule interval", + 'query': "-days <1 -hours >12" +}] + +PRIVILEGES_PATH_ONLY = [{ + "path": "/api/cluster/jobs" +}] + +PRIVILEGES_2_PATH_ONLY = [{ + "path": "/api/cluster/jobs" +}, { + "path": "/api/storage/volumes" +}] + +PRIVILEGES = [{ + 'path': '/api/storage/volumes', + 'access': 'readonly' +}] + +PRIVILEGES_911 = [{ + 'path': '/api/storage/volumes', + 'access': 'readonly', +}] + +PRIVILEGES_MODIFY = [{ + 'path': '/api/cluster/jobs', + 'access': 'all' +}] + +PRIVILEGES_COMMAND_MODIFY = [{ + 'path': 'job schedule interval', + 'query': "-days <1 -hours >8" +}] + +PRIVILEGES_MODIFY_911 = [{ + 'path': '/api/cluster/jobs', + 'access': 'all', +}] + +PRIVILEGES_MODIFY_NEW_PATH = [{ + 'path': '/api/cluster/jobs', + 'access': 'all' +}, { + "path": "/api/storage/volumes", + "access": 'all' +}] + +PRIVILEGES_MODIFY_NEW_PATH_9_11 = [{ + 'path': '/api/cluster/jobs', + 'access': 'all', +}, { + "path": "/api/storage/volumes", + "access": 'all', +}] + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'name': 'admin', + 'vserver': 'svm1' +} + + +def test_privileges_query_in_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = {'privileges': PRIVILEGES_SINGLE_WITH_QUERY, + 'use_rest': 'always'} + my_module_object = create_module(my_module, DEFAULT_ARGS, module_args, fail=True) + msg = 'Minimum version of ONTAP for privileges.query is (9, 11, 1)' + assert msg in my_module_object['msg'] + + +def test_get_user_role_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_role() is None + + +def test_get_user_role_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['generic_error']) + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error getting role admin: calling: security/roles: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_role, 'fail')['msg'] + + +def test_get_user_role(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['user_role_9_10']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_role() is not None + + +def test_get_user_role_9_11(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['user_role_9_11']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_role() is not None + + +def test_create_user_role_9_10_new_format(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['empty_records']), + ('POST', 'security/roles', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_9_10']) + ]) + module_args = {'privileges': PRIVILEGES} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_user_role_9_11_new_format(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['empty_records']), + ('POST', 'security/roles', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_9_10']) + ]) + module_args = {'privileges': PRIVILEGES_911} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_user_role_9_11_new_format_query(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['empty_records']), + ('POST', 'security/roles', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_privileges_schedule']) + ]) + module_args = {'privileges': PRIVILEGES_SINGLE_WITH_QUERY} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_user_role_9_10_new_format_path_only(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['empty_records']), + ('POST', 'security/roles', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_9_11']) + ]) + module_args = {'privileges': PRIVILEGES_PATH_ONLY} + print(module_args) + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_user_role_9_10_new_format_2_path_only(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['empty_records']), + ('POST', 'security/roles', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_9_10_two_paths']) + ]) + module_args = {'privileges': PRIVILEGES_2_PATH_ONLY} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_user_role_9_10_old_format(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['empty_records']), + ('POST', 'security/roles', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_9_10']) + ]) + module_args = {'command_directory_name': "/api/storage/volumes", + 'access_level': 'readonly'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_user_role_9_11_old_format_with_query(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['empty_records']), + ('POST', 'security/roles', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_volume_with_query']) + ]) + module_args = {'command_directory_name': "/api/storage/volumes", + 'access_level': 'readonly', + 'query': "-vserver vs1|vs2|vs3 -destination-aggregate aggr1|aggr2"} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_create_user_role_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('POST', 'security/roles', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['privileges'] = PRIVILEGES + error = expect_and_capture_ansible_exception(my_obj.create_role, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error creating role admin: calling: security/roles: got Expected error.' == error + + +def test_delete_user_role(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['user_role_9_10']), + ('DELETE', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin', SRR['empty_good']), + ('GET', 'security/roles', SRR['empty_records']) + ]) + module_args = {'state': 'absent', + 'command_directory_name': "/api/storage/volumes", + 'access_level': 'readonly'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_user_role_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('DELETE', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['privileges'] = PRIVILEGES + my_obj.parameters['state'] = 'absent' + my_obj.owner_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + error = expect_and_capture_ansible_exception(my_obj.delete_role, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error deleting role admin: calling: security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin: got Expected error.' == error + + +def test_modify_user_role_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['user_role_9_10']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_privileges']), + ('PATCH', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fcluster%2Fjobs', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_cluster_jobs_all']) + ]) + module_args = {'privileges': PRIVILEGES_MODIFY} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_user_role_command_9_10(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['user_role_9_11_command']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_privileges_command']), + ('PATCH', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/job schedule interval', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_privileges_schedule_modify']) + ]) + module_args = {'privileges': PRIVILEGES_COMMAND_MODIFY} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_remove_user_role_9_10(): + # This test will modify cluster/job, and delete storage/volumes + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['user_role_9_10_two_paths']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_privileges_two_paths']), + ('PATCH', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fcluster%2Fjobs', SRR['empty_good']), + ('DELETE', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fstorage%2Fvolumes', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_cluster_jobs_all']) + ]) + module_args = {'privileges': PRIVILEGES_MODIFY} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_user_role_9_11(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['user_role_9_11']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_privileges']), + ('PATCH', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fcluster%2Fjobs', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_cluster_jobs_all']) + ]) + module_args = {'privileges': PRIVILEGES_MODIFY_911} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_user_role_create_new_privilege(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles', SRR['user_role_9_10']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_privileges']), + ('PATCH', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fcluster%2Fjobs', SRR['empty_good']), # First path + ('POST', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['empty_good']), # Second path + ('GET', 'security/roles', SRR['user_role_9_10_two_paths_modified']) + ]) + module_args = {'privileges': PRIVILEGES_MODIFY_NEW_PATH} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_user_role_create_new_privilege_9_11(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['user_role_9_11']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_privileges']), + ('PATCH', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fcluster%2Fjobs', SRR['empty_good']), # First path + ('POST', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['empty_good']), # Second path + ('GET', 'security/roles', SRR['empty_records']) + ]) + module_args = {'privileges': PRIVILEGES_MODIFY_NEW_PATH_9_11} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_remove_user_role_error(): + # This test will modify cluster/job, and delete storage/volumes + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('DELETE', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fstorage%2Fvolumes', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['name'] = 'admin' + my_obj.owner_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + error = expect_and_capture_ansible_exception(my_obj.delete_role_privilege, 'fail', '/api/storage/volumes')['msg'] + print('Info: %s' % error) + assert 'Error deleting role privileges admin: calling: security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fstorage%2Fvolumes: '\ + 'got Expected error.' == error + + +def test_get_user_role_privileges_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['name'] = 'admin' + my_obj.owner_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + error = expect_and_capture_ansible_exception(my_obj.get_role_privileges_rest, 'fail')['msg'] + print('Info: %s' % error) + assert 'Error getting role privileges for role admin: calling: security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges: '\ + 'got Expected error.' == error + + +def test_create_user_role_privileges_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('POST', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['name'] = 'admin' + my_obj.owner_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + error = expect_and_capture_ansible_exception(my_obj.create_role_privilege, 'fail', PRIVILEGES[0])['msg'] + print('Info: %s' % error) + assert 'Error creating role privilege /api/storage/volumes: calling: security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges: '\ + 'got Expected error.' == error + + +def test_modify_user_role_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_privileges']), + ('PATCH', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fcluster%2Fjobs', SRR['generic_error']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.parameters['privileges'] = PRIVILEGES_MODIFY + my_obj.owner_uuid = '02c9e252-41be-11e9-81d5-00a0986138f7' + current = {'privileges': PRIVILEGES_MODIFY} + error = expect_and_capture_ansible_exception(my_obj.modify_role, 'fail', current)['msg'] + print('Info: %s' % error) + assert 'Error modifying privileges for path %2Fapi%2Fcluster%2Fjobs: calling: '\ + 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/%2Fapi%2Fcluster%2Fjobs: '\ + 'got Expected error.' == error + + +def test_command_directory_present_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + assert 'Error: either path or command_directory_name is required' in create_and_apply(my_module, DEFAULT_ARGS, fail=True)['msg'] + + +def test_warnings_additional_commands_added_after_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['empty_records']), + ('POST', 'security/roles', SRR['empty_good']), + ('GET', 'security/roles', SRR['user_role_volume']) + ]) + args = {'privileges': [{'path': 'volume create', 'access': 'all'}]} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert_warning_was_raised("Create operation also affected additional related commands", partial_match=True) + + +def test_warnings_create_required_after_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['user_role_volume']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_volume_privileges']), + ('DELETE', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/volume modify', SRR['empty_good']), + ('GET', 'security/roles', SRR['empty_records']), + ]) + args = {'privileges': [{'path': 'volume create', 'access': 'readonly'}]} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert_warning_was_raised("Create role is required", partial_match=True) + + +def test_warnings_modify_required_after_original_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'security/roles', SRR['user_role_volume']), + ('GET', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges', SRR['user_role_volume_privileges']), + ('DELETE', 'security/roles/02c9e252-41be-11e9-81d5-00a0986138f7/admin/privileges/volume modify', SRR['error_4']), + ('GET', 'security/roles', SRR['user_role_vserver']), + ]) + args = {'privileges': [{'path': 'volume create', 'access': 'readonly'}]} + assert create_and_apply(my_module, DEFAULT_ARGS, args)['changed'] + assert_warning_was_raised("modify is required, desired", partial_match=True) + + +def test_error_with_legacy_commands_9_10_1(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']) + ]) + args = {'privileges': [{'path': 'volume create', 'access': 'readonly'}]} + assert "Error: Invalid URI ['volume create']" in create_module(my_module, DEFAULT_ARGS, args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume.py new file mode 100644 index 000000000..3161ead04 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume.py @@ -0,0 +1,2011 @@ +# (c) 2018-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import copy +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + assert_warning_was_raised, call_main, create_module, create_and_apply, expect_and_capture_ansible_exception, patch_ansible, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import \ + get_mock_record, patch_request_and_invoke, print_requests, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_error, build_zapi_response, zapi_error_message, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume \ + import NetAppOntapVolume as vol_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), +} + +MOCK_VOL = { + 'name': 'test_vol', + 'aggregate': 'test_aggr', + 'junction_path': '/test', + 'vserver': 'test_vserver', + 'size': 20971520, + 'unix_permissions': '755', + 'user_id': 100, + 'group_id': 1000, + 'snapshot_policy': 'default', + 'qos_policy_group': 'performance', + 'qos_adaptive_policy_group': 'performance', + 'percent_snapshot_space': 60, + 'language': 'en', + 'vserver_dr_protection': 'unprotected', + 'uuid': 'UUID' +} + + +def volume_info(style, vol_details=None, remove_keys=None, encrypt='false'): + if not vol_details: + vol_details = MOCK_VOL + info = copy.deepcopy({ + 'num-records': 1, + 'attributes-list': { + 'volume-attributes': { + 'encrypt': encrypt, + 'volume-id-attributes': { + 'aggr-list': vol_details['aggregate'], + 'containing-aggregate-name': vol_details['aggregate'], + 'flexgroup-uuid': 'uuid', + 'junction-path': vol_details['junction_path'], + 'style-extended': style, + 'type': 'rw' + }, + 'volume-comp-aggr-attributes': { + 'tiering-policy': 'snapshot-only' + }, + 'volume-language-attributes': { + 'language-code': 'en' + }, + 'volume-export-attributes': { + 'policy': 'default' + }, + 'volume-performance-attributes': { + 'is-atime-update-enabled': 'true' + }, + 'volume-state-attributes': { + 'state': "online", + 'is-nvfail-enabled': 'true' + }, + 'volume-inode-attributes': { + 'files-total': '2000', + }, + 'volume-space-attributes': { + 'space-guarantee': 'none', + 'size': vol_details['size'], + 'percentage-snapshot-reserve': vol_details['percent_snapshot_space'], + 'space-slo': 'thick' + }, + 'volume-snapshot-attributes': { + 'snapshot-policy': vol_details['snapshot_policy'] + }, + 'volume-security-attributes': { + 'volume-security-unix-attributes': { + 'permissions': vol_details['unix_permissions'], + 'group-id': vol_details['group_id'], + 'user-id': vol_details['user_id'] + }, + 'style': 'unix', + }, + 'volume-vserver-dr-protection-attributes': { + 'vserver-dr-protection': vol_details['vserver_dr_protection'], + }, + 'volume-qos-attributes': { + 'policy-group-name': vol_details['qos_policy_group'], + 'adaptive-policy-group-name': vol_details['qos_adaptive_policy_group'] + }, + 'volume-snapshot-autodelete-attributes': { + 'commitment': 'try', + 'is-autodelete-enabled': 'true', + } + } + } + }) + if remove_keys: + for key in remove_keys: + if key == 'is_online': + del info['attributes-list']['volume-attributes']['volume-state-attributes']['state'] + else: + raise KeyError('unexpected key %s' % key) + return info + + +def vol_encryption_conversion_status(status): + return { + 'num-records': 1, + 'attributes-list': { + 'volume-encryption-conversion-info': { + 'status': status + } + } + } + + +def vol_move_status(status): + return { + 'num-records': 1, + 'attributes-list': { + 'volume-move-info': { + 'state': status, + 'details': 'some info' + } + } + } + + +def job_info(state, error): + return { + 'num-records': 1, + 'attributes': { + 'job-info': { + 'job-state': state, + 'job-progress': 'progress', + 'job-completion': error, + } + } + } + + +def results_info(status): + return { + 'result-status': status, + 'result-jobid': 'job12345', + } + + +def modify_async_results_info(status, error=None): + list_name = 'failure-list' if error else 'success-list' + info = { + list_name: { + 'volume-modify-iter-async-info': { + 'status': status, + 'jobid': '1234' + } + } + } + if error: + info[list_name]['volume-modify-iter-async-info']['error-message'] = error + return info + + +def sis_info(): + return { + 'num-records': 1, + 'attributes-list': { + 'sis-status-info': { + 'policy': 'test', + 'is-compression-enabled': 'true', + 'sis-status-completion': 'false', + } + } + } + + +ZRR = zapi_responses({ + 'get_flexgroup': build_zapi_response(volume_info('flexgroup')), + 'get_flexvol': build_zapi_response(volume_info('flexvol')), + 'get_flexvol_encrypted': build_zapi_response(volume_info('flexvol', encrypt='true')), + 'get_flexvol_no_online_key': build_zapi_response(volume_info('flexvol', remove_keys=['is_online'])), + 'job_failure': build_zapi_response(job_info('failure', 'failure')), + 'job_other': build_zapi_response(job_info('other', 'other_error')), + 'job_running': build_zapi_response(job_info('running', None)), + 'job_success': build_zapi_response(job_info('success', None)), + 'job_time_out': build_zapi_response(job_info('running', 'time_out')), + 'job_no_completion': build_zapi_response(job_info('failure', None)), + 'async_results': build_zapi_response(results_info('in_progress')), + 'failed_results': build_zapi_response(results_info('failed')), + 'modify_async_result_success': build_zapi_response(modify_async_results_info('in_progress')), + 'modify_async_result_failure': build_zapi_response(modify_async_results_info('failure', 'error_in_modify')), + 'vol_encryption_conversion_status_running': build_zapi_response(vol_encryption_conversion_status('running')), + 'vol_encryption_conversion_status_idle': build_zapi_response(vol_encryption_conversion_status('Not currently going on.')), + 'vol_encryption_conversion_status_error': build_zapi_response(vol_encryption_conversion_status('other')), + 'vol_move_status_running': build_zapi_response(vol_move_status('healthy')), + 'vol_move_status_idle': build_zapi_response(vol_move_status('done')), + 'vol_move_status_error': build_zapi_response(vol_move_status('failed')), + 'insufficient_privileges': build_zapi_error(12346, 'Insufficient privileges: user USERID does not have read access to this resource'), + 'get_sis_info': build_zapi_response(sis_info()), + 'error_15661': build_zapi_error(15661, 'force job not found error'), + 'error_tiering_94': build_zapi_error(94, 'volume-comp-aggr-attributes') +}) + + +MINIMUM_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': 'test_vol', + 'vserver': 'test_vserver', + 'use_rest': 'never' +} + + +DEFAULT_ARGS = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': 'test_vol', + 'vserver': 'test_vserver', + 'policy': 'default', + 'language': 'en', + 'is_online': True, + 'unix_permissions': '---rwxr-xr-x', + 'user_id': 100, + 'group_id': 1000, + 'snapshot_policy': 'default', + 'qos_policy_group': 'performance', + 'qos_adaptive_policy_group': 'performance', + 'size': 20, + 'size_unit': 'mb', + 'junction_path': '/test', + 'percent_snapshot_space': 60, + 'type': 'rw', + 'nvfail_enabled': True, + 'space_slo': 'thick', + 'use_rest': 'never' +} + + +ZAPI_ERROR = 'NetApp API failed. Reason - 12345:synthetic error for UT purpose' + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + error = create_module(vol_module, {}, fail=True) + print('Info: %s' % error['msg']) + assert 'missing required arguments:' in error['msg'] + + +def test_get_nonexistent_volume(): + ''' Test if get_volume returns None for non-existent volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['success']), + ]) + assert create_module(vol_module, DEFAULT_ARGS).get_volume() is None + + +def test_get_error(): + ''' Test if get_volume handles error ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['error']), + ]) + error = 'Error fetching volume test_vol : %s' % ZAPI_ERROR + assert expect_and_capture_ansible_exception(create_module(vol_module, DEFAULT_ARGS).get_volume, 'fail')['msg'] == error + + +def test_get_existing_volume(): + ''' Test if get_volume returns details for existing volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + volume_info = create_module(vol_module, DEFAULT_ARGS).get_volume() + assert volume_info is not None + assert 'aggregate_name' in volume_info + + +def test_create_error_missing_param(): + ''' Test if create throws an error if aggregate_name is not specified''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ]) + module_args = { + 'size': 20, + 'encrypt': True, + } + msg = 'Error provisioning volume test_vol: aggregate_name is required' + assert msg == create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-create', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + module_args = { + 'aggregate_name': MOCK_VOL['aggregate'], + 'size': 20, + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_successful_create_with_completion(dont_sleep): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-create', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), # wait for online + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + module_args = { + 'aggregate_name': MOCK_VOL['aggregate'], + 'size': 20, + 'wait_for_completion': True + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_error_timeout_create_with_completion(dont_sleep): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-create', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ]) + module_args = { + 'aggregate_name': MOCK_VOL['aggregate'], + 'size': 20, + 'time_out': 42, + 'wait_for_completion': True + } + error = "Error waiting for volume test_vol to come online: ['Timeout after 42 seconds']" + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +@patch('time.sleep') +def test_error_timeout_keyerror_create_with_completion(dont_sleep): + ''' Test successful create ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-create', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol_no_online_key']), # wait for online + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-get-iter', ZRR['no_records']), # wait for online + ]) + module_args = { + 'aggregate_name': MOCK_VOL['aggregate'], + 'size': 20, + 'time_out': 42, + 'wait_for_completion': True + } + error_py3x = '''Error waiting for volume test_vol to come online: ["KeyError('is_online')", 'Timeout after 42 seconds']''' + error_py27 = '''Error waiting for volume test_vol to come online: ["KeyError('is_online',)", 'Timeout after 42 seconds']''' + error = create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('error', error) + assert error == error_py3x or error == error_py27 + + +def test_error_create(): + ''' Test error on create ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-create', ZRR['error']), + ]) + module_args = { + 'aggregate_name': MOCK_VOL['aggregate'], + 'size': 20, + 'encrypt': True, + } + error = 'Error provisioning volume test_vol of size 20971520: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_create_idempotency(): + ''' Test create idempotency ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + assert not create_and_apply(vol_module, DEFAULT_ARGS)['changed'] + + +def test_successful_delete(): + ''' Test delete existing volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-destroy', ZRR['success']), + ]) + module_args = { + 'state': 'absent', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_delete(): + ''' Test delete existing volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-destroy', ZRR['error']), + ('ZAPI', 'volume-destroy', ZRR['error']), + ]) + module_args = { + 'state': 'absent', + } + error = 'Error deleting volume test_vol:' + msg = create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + error = 'volume delete failed with unmount-and-offline option: %s' % ZAPI_ERROR + assert error in msg + error = 'volume delete failed without unmount-and-offline option: %s' % ZAPI_ERROR + assert error in msg + + +def test_error_delete_async(): + ''' Test delete existing volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexgroup']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-unmount', ZRR['error']), + ('ZAPI', 'volume-offline-async', ZRR['error']), + ('ZAPI', 'volume-destroy-async', ZRR['error']), + ]) + module_args = { + 'state': 'absent', + + } + error = 'Error deleting volume test_vol: %s' % ZAPI_ERROR + msg = create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + error = 'Error unmounting volume test_vol: %s' % ZAPI_ERROR + assert error in msg + error = 'Error changing the state of volume test_vol to offline: %s' % ZAPI_ERROR + assert error in msg + + +def test_delete_idempotency(): + ''' Test delete idempotency ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ]) + module_args = { + 'state': 'absent', + } + assert not create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify_size(): + ''' Test successful modify size ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-size', ZRR['success']), + ]) + module_args = { + 'size': 200, + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<new-size>209715200', 2) + + +def test_modify_idempotency(): + ''' Test modify idempotency ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + assert not create_and_apply(vol_module, DEFAULT_ARGS)['changed'] + + +def test_modify_error(): + ''' Test modify idempotency ''' + register_responses([ + ('ZAPI', 'volume-modify-iter', ZRR['error']), + ]) + msg = 'Error modifying volume test_vol: %s' % ZAPI_ERROR + assert msg == expect_and_capture_ansible_exception(create_module(vol_module, DEFAULT_ARGS).volume_modify_attributes, 'fail', {})['msg'] + + +def test_mount_volume(): + ''' Test mount volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-mount', ZRR['success']), + ]) + module_args = { + 'junction_path': '/test123', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_mount_volume(): + ''' Test mount volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-mount', ZRR['error']), + ]) + module_args = { + 'junction_path': '/test123', + } + error = 'Error mounting volume test_vol on path /test123: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_unmount_volume(): + ''' Test unmount volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-unmount', ZRR['success']), + ]) + module_args = { + 'junction_path': '', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_unmount_volume(): + ''' Test unmount volume ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-unmount', ZRR['error']), + ]) + module_args = { + 'junction_path': '', + } + error = 'Error unmounting volume test_vol: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_successful_modify_space(): + ''' Test successful modify space ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + args = dict(DEFAULT_ARGS) + del args['space_slo'] + module_args = { + 'space_guarantee': 'volume', + } + assert create_and_apply(vol_module, args, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<space-guarantee>volume', 2) + + +def test_successful_modify_unix_permissions(): + ''' Test successful modify unix_permissions ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = { + 'unix_permissions': '---rw-r-xr-x', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<permissions>---rw-r-xr-x', 2) + + +def test_successful_modify_volume_security_style(): + ''' Test successful modify volume_security_style ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = { + 'volume_security_style': 'mixed', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<style>mixed</style>', 2) + + +def test_successful_modify_max_files_and_encrypt(): + ''' Test successful modify unix_permissions ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ('ZAPI', 'volume-encryption-conversion-start', ZRR['success']), + ]) + module_args = { + 'encrypt': True, + 'max_files': '3000', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<files-total>3000', 2) + + +def test_successful_modify_snapshot_policy(): + ''' Test successful modify snapshot_policy ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = { + 'snapshot_policy': 'default-1weekly', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<snapshot-policy>default-1weekly', 2) + + +def test_successful_modify_efficiency_policy(): + ''' Test successful modify efficiency_policy ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'sis-enable', ZRR['success']), + ('ZAPI', 'sis-set-config', ZRR['success']), + ]) + module_args = { + 'efficiency_policy': 'test', + 'inline_compression': True + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<policy-name>test', 3) + + +def test_successful_modify_efficiency_policy_idempotent(): + ''' Test successful modify efficiency_policy ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['get_sis_info']), + ]) + module_args = { + 'efficiency_policy': 'test', + 'compression': True + } + assert not create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify_efficiency_policy_async(): + ''' Test successful modify efficiency_policy ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexgroup']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'sis-enable-async', ZRR['success']), + ('ZAPI', 'sis-set-config-async', ZRR['success']), + ]) + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'efficiency_policy': 'test', + 'compression': True, + 'wait_for_completion': True, + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<policy-name>test', 3) + + +def test_error_set_efficiency_policy(): + register_responses([ + ('ZAPI', 'sis-enable', ZRR['error']), + ]) + module_args = {'efficiency_policy': 'test_policy'} + msg = 'Error enable efficiency on volume test_vol: %s' % ZAPI_ERROR + assert msg == expect_and_capture_ansible_exception(create_module(vol_module, MINIMUM_ARGS, module_args).set_efficiency_config, 'fail')['msg'] + + +def test_error_modify_efficiency_policy(): + ''' Test error modify efficiency_policy ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'sis-enable', ZRR['success']), + ('ZAPI', 'sis-set-config', ZRR['error']), + ]) + module_args = { + 'efficiency_policy': 'test', + } + error = 'Error setting up efficiency attributes on volume test_vol: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_error_set_efficiency_policy_async(): + register_responses([ + ('ZAPI', 'sis-enable-async', ZRR['error']), + ]) + module_args = {'efficiency_policy': 'test_policy'} + msg = 'Error enable efficiency on volume test_vol: %s' % ZAPI_ERROR + assert msg == expect_and_capture_ansible_exception(create_module(vol_module, MINIMUM_ARGS, module_args).set_efficiency_config_async, 'fail')['msg'] + + +def test_error_modify_efficiency_policy_async(): + ''' Test error modify efficiency_policy ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexgroup']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'sis-enable-async', ZRR['success']), + ('ZAPI', 'sis-set-config-async', ZRR['error']), + ]) + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'efficiency_policy': 'test', + } + error = 'Error setting up efficiency attributes on volume test_vol: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_successful_modify_percent_snapshot_space(): + ''' Test successful modify percent_snapshot_space ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = { + 'percent_snapshot_space': 90, + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<percentage-snapshot-reserve>90', 2) + + +def test_successful_modify_qos_policy_group(): + ''' Test successful modify qos_policy_group ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = { + 'qos_policy_group': 'extreme', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<policy-group-name>extreme', 2) + + +def test_successful_modify_qos_adaptive_policy_group(): + ''' Test successful modify qos_adaptive_policy_group ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = { + 'qos_adaptive_policy_group': 'extreme', + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<adaptive-policy-group-name>extreme', 2) + + +def test_successful_move(): + ''' Test successful modify aggregate ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-move-start', ZRR['success']), + ('ZAPI', 'volume-move-get-iter', ZRR['vol_move_status_idle']), + ]) + module_args = { + 'aggregate_name': 'different_aggr', + 'cutover_action': 'abort_on_failure', + 'encrypt': True, + 'wait_for_completion': True + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_unencrypt_volume(): + ''' Test successful modify aggregate ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol_encrypted']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-move-start', ZRR['success']), + ('ZAPI', 'volume-move-get-iter', ZRR['vol_move_status_idle']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol_encrypted']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-move-start', ZRR['success']), + ('ZAPI', 'volume-move-get-iter', ZRR['vol_move_status_idle']), + ]) + # without aggregate + module_args = { + 'encrypt': False, + 'wait_for_completion': True + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + # with aggregate. + module_args['aggregate_name'] = 'test_aggr' + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_move(): + ''' Test error modify aggregate ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-move-start', ZRR['error']), + ]) + module_args = { + 'aggregate_name': 'different_aggr', + } + error = 'Error moving volume test_vol: %s - Retry failed with REST error: False' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def setup_rename(is_isinfinite=None): + module_args = { + 'from_name': MOCK_VOL['name'], + 'name': 'new_name', + 'time_out': 20 + } + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'uuid': MOCK_VOL['uuid'], + 'vserver': MOCK_VOL['vserver'], + } + if is_isinfinite is not None: + module_args['is_infinite'] = is_isinfinite + current['is_infinite'] = is_isinfinite + return module_args, current + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_rename(get_volume): + ''' Test successful rename volume ''' + register_responses([ + ('ZAPI', 'volume-rename', ZRR['success']), + ]) + module_args, current = setup_rename() + get_volume.side_effect = [ + None, + current + ] + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_error_rename(get_volume): + ''' Test error rename volume ''' + register_responses([ + ('ZAPI', 'volume-rename', ZRR['error']), + ]) + module_args, current = setup_rename() + get_volume.side_effect = [ + None, + current + ] + error = 'Error renaming volume new_name: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_error_rename_no_from(get_volume): + ''' Test error rename volume ''' + register_responses([ + ]) + module_args, current = setup_rename() + get_volume.side_effect = [ + None, + None + ] + error = 'Error renaming volume: cannot find %s' % MOCK_VOL['name'] + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_rename_async(get_volume): + ''' Test successful rename volume ''' + register_responses([ + ('ZAPI', 'volume-rename-async', ZRR['success']), + ]) + module_args, current = setup_rename(is_isinfinite=True) + get_volume.side_effect = [ + None, + current + ] + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_helper(): + register_responses([ + ('ZAPI', 'volume-unmount', ZRR['success']), + ('ZAPI', 'volume-offline', ZRR['success']), + ]) + module_args = {'is_online': False} + modify = {'is_online': False} + assert create_module(vol_module, DEFAULT_ARGS, module_args).take_modify_actions(modify) is None + + +def test_compare_chmod_value_true_1(): + module_args = {'unix_permissions': '------------'} + current = { + 'unix_permissions': '0' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_compare_chmod_value_true_2(): + module_args = {'unix_permissions': '---rwxrwxrwx'} + current = { + 'unix_permissions': '777' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_compare_chmod_value_true_3(): + module_args = {'unix_permissions': '---rwxr-xr-x'} + current = { + 'unix_permissions': '755' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_compare_chmod_value_true_4(): + module_args = {'unix_permissions': '755'} + current = { + 'unix_permissions': '755' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_compare_chmod_value_false_1(): + module_args = {'unix_permissions': '---rwxrwxrwx'} + current = { + 'unix_permissions': '0' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert not vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_compare_chmod_value_false_2(): + module_args = {'unix_permissions': '---rwxrwxrwx'} + current = None + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert not vol_obj.na_helper.compare_chmod_value(current, module_args['unix_permissions']) + + +def test_compare_chmod_value_invalid_input_1(): + module_args = {'unix_permissions': '---xwrxwrxwr'} + current = { + 'unix_permissions': '777' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert not vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_compare_chmod_value_invalid_input_2(): + module_args = {'unix_permissions': '---rwx-wx--a'} + current = { + 'unix_permissions': '0' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert not vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_compare_chmod_value_invalid_input_3(): + module_args = {'unix_permissions': '---'} + current = { + 'unix_permissions': '0' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert not vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_compare_chmod_value_invalid_input_4(): + module_args = {'unix_permissions': 'rwx---rwxrwx'} + current = { + 'unix_permissions': '0' + } + vol_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert not vol_obj.na_helper.compare_chmod_value(current['unix_permissions'], module_args['unix_permissions']) + + +def test_successful_create_flex_group_manually(): + ''' Test successful create flexGroup manually ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['empty']), + ('ZAPI', 'volume-create-async', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexgroup']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter-async', ZRR['modify_async_result_success']), + ('ZAPI', 'job-get', ZRR['job_success']), + ]) + args = copy.deepcopy(DEFAULT_ARGS) + del args['space_slo'] + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'space_guarantee': 'file', + 'time_out': 20 + } + assert create_and_apply(vol_module, args, module_args)['changed'] + + +def test_error_create_flex_group_manually(): + ''' Test error create flexGroup manually ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['empty']), + ('ZAPI', 'volume-create-async', ZRR['error']), + ]) + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'time_out': 20 + } + error = 'Error provisioning volume test_vol of size 20971520: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_partial_error_create_flex_group_manually(): + ''' Test error create flexGroup manually ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('ZAPI', 'volume-get-iter', ZRR['empty']), + ('ZAPI', 'volume-create-async', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexgroup']), + ('ZAPI', 'sis-get-iter', ZRR['insufficient_privileges']), # ignored but raises a warning + ('ZAPI', 'volume-modify-iter-async', ZRR['modify_async_result_failure']), + ]) + args = copy.deepcopy(DEFAULT_ARGS) + del args['space_slo'] + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'space_guarantee': 'file', + 'time_out': 20, + 'use_rest': 'auto' + } + error = 'Volume created with success, with missing attributes: Error modifying volume test_vol: error_in_modify' + assert create_and_apply(vol_module, args, module_args, fail=True)['msg'] == error + print_warnings() + assert_warning_was_raised('cannot read volume efficiency options (as expected when running as vserver): ' + 'NetApp API failed. Reason - 12346:Insufficient privileges: user USERID does not have read access to this resource') + + +def test_successful_create_flex_group_auto_provision(): + ''' Test successful create flexGroup auto provision ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['empty']), + ('ZAPI', 'volume-create-async', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexgroup']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + module_args = { + 'auto_provision_as': 'flexgroup', + 'time_out': 20 + } + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_delete_flex_group(get_volume): + ''' Test successful delete flexGroup ''' + register_responses([ + ('ZAPI', 'volume-unmount', ZRR['success']), + ('ZAPI', 'volume-offline-async', ZRR['job_success']), + ('ZAPI', 'volume-destroy-async', ZRR['job_success']), + ]) + module_args = { + 'state': 'absent', + } + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '755', + 'is_online': True, + 'uuid': 'uuid' + } + get_volume.return_value = current + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +def setup_resize(): + module_args = { + 'size': 400, + 'size_unit': 'mb' + } + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'size': 20971520, + 'unix_permissions': '755', + 'uuid': '1234' + } + return module_args, current + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_resize_flex_group(get_volume): + ''' Test successful reszie flexGroup ''' + register_responses([ + ('ZAPI', 'volume-size-async', ZRR['job_success']), + ]) + module_args, current = setup_resize() + get_volume.return_value = current + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_error_resize_flex_group(get_volume): + ''' Test error reszie flexGroup ''' + register_responses([ + ('ZAPI', 'volume-size-async', ZRR['error']), + ]) + module_args, current = setup_resize() + get_volume.return_value = current + error = 'Error re-sizing volume test_vol: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.check_job_status') +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_modify_unix_permissions_flex_group(get_volume, check_job_status): + ''' Test successful modify unix permissions flexGroup ''' + register_responses([ + ('ZAPI', 'volume-modify-iter-async', ZRR['modify_async_result_success']), + ]) + module_args = { + 'unix_permissions': '---rw-r-xr-x', + 'time_out': 20 + } + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '777', + 'uuid': '1234' + } + get_volume.return_value = current + check_job_status.return_value = None + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_modify_unix_permissions_flex_group_0_time_out(get_volume): + ''' Test successful modify unix permissions flexGroup ''' + register_responses([ + ('ZAPI', 'volume-modify-iter-async', ZRR['modify_async_result_success']), + ]) + module_args = { + 'unix_permissions': '---rw-r-xr-x', + 'time_out': 0 + } + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '777', + 'uuid': '1234' + } + get_volume.return_value = current + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_modify_unix_permissions_flex_group_0_missing_result(get_volume): + ''' Test successful modify unix permissions flexGroup ''' + register_responses([ + ('ZAPI', 'volume-modify-iter-async', ZRR['job_running']), # bad response + ]) + module_args = { + 'unix_permissions': '---rw-r-xr-x', + 'time_out': 0 + } + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '777', + 'uuid': '1234' + } + get_volume.return_value = current + # check_job_status.side_effect = ['job_error'] + error = create_and_apply(vol_module, DEFAULT_ARGS, module_args, 'fail') + assert error['msg'].startswith('Unexpected error when modifying volume: result is:') + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.check_job_status') +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_error_modify_unix_permissions_flex_group(get_volume, check_job_status): + ''' Test error modify unix permissions flexGroup ''' + register_responses([ + ('ZAPI', 'volume-modify-iter-async', ZRR['modify_async_result_success']), + ]) + module_args = { + 'unix_permissions': '---rw-r-xr-x', + 'time_out': 20 + } + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '777', + 'uuid': '1234' + } + get_volume.return_value = current + check_job_status.side_effect = ['job_error'] + error = create_and_apply(vol_module, DEFAULT_ARGS, module_args, 'fail') + assert error['msg'] == 'Error when modifying volume: job_error' + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_failure_modify_unix_permissions_flex_group(get_volume): + ''' Test failure modify unix permissions flexGroup ''' + register_responses([ + ('ZAPI', 'volume-modify-iter-async', ZRR['modify_async_result_failure']), + ]) + module_args = { + 'unix_permissions': '---rw-r-xr-x', + 'time_out': 20 + } + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'unix_permissions': '777', + 'uuid': '1234' + } + get_volume.return_value = current + error = create_and_apply(vol_module, DEFAULT_ARGS, module_args, 'fail') + assert error['msg'] == 'Error modifying volume test_vol: error_in_modify' + + +def setup_offline_state(): + module_args = {'is_online': False} + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'is_online': True, + 'junction_path': '/test', + 'unix_permissions': '755', + 'uuid': '1234' + } + return module_args, current + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_offline_state_flex_group(get_volume): + ''' Test successful offline flexGroup state ''' + register_responses([ + ('ZAPI', 'volume-unmount', ZRR['success']), + ('ZAPI', 'volume-offline-async', ZRR['async_results']), + ('ZAPI', 'job-get', ZRR['job_success']), + ]) + module_args, current = setup_offline_state() + get_volume.return_value = current + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_error_offline_state_flex_group(get_volume): + ''' Test error offline flexGroup state ''' + register_responses([ + ('ZAPI', 'volume-unmount', ZRR['success']), + ('ZAPI', 'volume-offline-async', ZRR['error']), + ]) + module_args, current = setup_offline_state() + get_volume.return_value = current + error = 'Error changing the state of volume test_vol to offline: %s' % ZAPI_ERROR + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_error_unmounting_offline_state_flex_group(get_volume): + ''' Test error offline flexGroup state ''' + register_responses([ + ('ZAPI', 'volume-unmount', ZRR['error']), + ('ZAPI', 'volume-offline-async', ZRR['error']), + ]) + module_args, current = setup_offline_state() + get_volume.return_value = current + error = 'Error changing the state of volume test_vol to offline: %s' % ZAPI_ERROR + msg = create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert error in msg + errpr = 'Error unmounting volume test_vol: %s' % ZAPI_ERROR + assert error in msg + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_online_state_flex_group(get_volume): + ''' Test successful online flexGroup state ''' + register_responses([ + ('ZAPI', 'volume-online-async', ZRR['async_results']), + ('ZAPI', 'job-get', ZRR['job_success']), + ('ZAPI', 'volume-modify-iter-async', ZRR['modify_async_result_success']), + ('ZAPI', 'job-get', ZRR['job_success']), + ('ZAPI', 'volume-mount', ZRR['success']), + ]) + current = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'style_extended': 'flexgroup', + 'is_online': False, + 'junction_path': 'anything', + 'unix_permissions': '755', + 'uuid': '1234' + } + get_volume.return_value = current + assert create_and_apply(vol_module, DEFAULT_ARGS)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<group-id>', 2) + assert get_mock_record().is_text_in_zapi_request('<user-id>', 2) + assert get_mock_record().is_text_in_zapi_request('<percentage-snapshot-reserve>', 2) + assert get_mock_record().is_text_in_zapi_request('<junction-path>/test</junction-path>', 4) + + +def test_check_job_status_error(): + ''' Test check job status error ''' + register_responses([ + ('ZAPI', 'job-get', ZRR['error']), + ]) + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'time_out': 0 + } + error = 'Error fetching job info: %s' % ZAPI_ERROR + assert expect_and_capture_ansible_exception(create_module(vol_module, MINIMUM_ARGS, module_args).check_job_status, 'fail', '123')['msg'] == error + + +@patch('time.sleep') +def test_check_job_status_not_found(skip_sleep): + ''' Test check job status error ''' + register_responses([ + ('ZAPI', 'job-get', ZRR['error_15661']), + ('ZAPI', 'vserver-get-iter', ZRR['no_records']), + ('ZAPI', 'job-get', ZRR['error_15661']), + ]) + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'time_out': 50 + } + error = 'cannot locate job with id: 123' + assert create_module(vol_module, MINIMUM_ARGS, module_args).check_job_status('123') == error + + +@patch('time.sleep') +def test_check_job_status_failure(skip_sleep): + ''' Test check job status error ''' + register_responses([ + ('ZAPI', 'job-get', ZRR['job_running']), + ('ZAPI', 'job-get', ZRR['job_running']), + ('ZAPI', 'job-get', ZRR['job_failure']), + ('ZAPI', 'job-get', ZRR['job_running']), + ('ZAPI', 'job-get', ZRR['job_running']), + ('ZAPI', 'job-get', ZRR['job_no_completion']), + ]) + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'time_out': 20 + } + msg = 'failure' + assert msg == create_module(vol_module, MINIMUM_ARGS, module_args).check_job_status('123') + msg = 'progress' + assert msg == create_module(vol_module, MINIMUM_ARGS, module_args).check_job_status('123') + + +def test_check_job_status_time_out_is_0(): + ''' Test check job status time out is 0''' + register_responses([ + ('ZAPI', 'job-get', ZRR['job_time_out']), + ]) + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'time_out': 0 + } + msg = 'job completion exceeded expected timer of: 0 seconds' + assert msg == create_module(vol_module, MINIMUM_ARGS, module_args).check_job_status('123') + + +def test_check_job_status_unexpected(): + ''' Test check job status unexpected state ''' + register_responses([ + ('ZAPI', 'job-get', ZRR['job_other']), + ]) + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'time_out': 20 + } + msg = 'Unexpected job status in:' + assert msg in expect_and_capture_ansible_exception(create_module(vol_module, MINIMUM_ARGS, module_args).check_job_status, 'fail', '123')['msg'] + + +def test_successful_modify_tiering_policy(): + ''' Test successful modify tiering policy ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = {'tiering_policy': 'auto'} + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<tiering-policy>auto</tiering-policy>', 2) + + +def test_error_modify_tiering_policy(): + ''' Test successful modify tiering policy ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['error']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['error_tiering_94']), + ]) + module_args = {'tiering_policy': 'auto'} + error = zapi_error_message('Error modifying volume test_vol') + assert error in create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + error = zapi_error_message('Error modifying volume test_vol', 94, 'volume-comp-aggr-attributes', '. Added info: tiering option requires 9.4 or later.') + assert error in create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_successful_modify_vserver_dr_protection(): + ''' Test successful modify vserver_dr_protection ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = {'vserver_dr_protection': 'protected'} + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<vserver-dr-protection>protected</vserver-dr-protection>', 2) + + +def test_successful_group_id(): + ''' Test successful modify group_id ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = {'group_id': 1001} + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<group-id>1001</group-id>', 2) + + +def test_successful_modify_user_id(): + ''' Test successful modify user_id ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ]) + module_args = {'user_id': 101} + assert create_and_apply(vol_module, DEFAULT_ARGS, module_args)['changed'] + print_requests() + assert get_mock_record().is_text_in_zapi_request('<user-id>101</user-id>', 2) + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume.NetAppOntapVolume.get_volume') +def test_successful_modify_snapshot_auto_delete(get_volume): + ''' Test successful modify unix permissions flexGroup ''' + register_responses([ + # One ZAPI call for each option! + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['success']), + ]) + module_args = { + 'snapshot_auto_delete': { + 'delete_order': 'oldest_first', 'destroy_list': 'lun_clone,vol_clone', + 'target_free_space': 20, 'prefix': 'test', 'commitment': 'try', + 'state': 'on', 'trigger': 'snap_reserve', 'defer_delete': 'scheduled'}} + current = { + 'name': MOCK_VOL['name'], + 'vserver': MOCK_VOL['vserver'], + 'snapshot_auto_delete': { + 'delete_order': 'newest_first', 'destroy_list': 'lun_clone,vol_clone', + 'target_free_space': 30, 'prefix': 'test', 'commitment': 'try', + 'state': 'on', 'trigger': 'snap_reserve', 'defer_delete': 'scheduled'}, + 'uuid': '1234' + } + get_volume.return_value = current + assert create_and_apply(vol_module, MINIMUM_ARGS, module_args)['changed'] + + +def test_error_modify_snapshot_auto_delete(): + register_responses([ + ('ZAPI', 'snapshot-autodelete-set-option', ZRR['error']), + ]) + module_args = {'snapshot_auto_delete': { + 'delete_order': 'oldest_first', 'destroy_list': 'lun_clone,vol_clone', + 'target_free_space': 20, 'prefix': 'test', 'commitment': 'try', + 'state': 'on', 'trigger': 'snap_reserve', 'defer_delete': 'scheduled'}} + msg = 'Error setting snapshot auto delete options for volume test_vol: %s' % ZAPI_ERROR + assert msg == expect_and_capture_ansible_exception(create_module(vol_module, MINIMUM_ARGS, module_args).set_snapshot_auto_delete, 'fail')['msg'] + + +def test_successful_volume_rehost(): + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-rehost', ZRR['success']), + ]) + module_args = { + 'from_vserver': 'source_vserver', + 'auto_remap_luns': False, + } + assert create_and_apply(vol_module, MINIMUM_ARGS, module_args)['changed'] + + +def test_error_volume_rehost(): + register_responses([ + ('ZAPI', 'volume-rehost', ZRR['error']), + ]) + module_args = { + 'from_vserver': 'source_vserver', + 'force_unmap_luns': False, + } + msg = 'Error rehosting volume test_vol: %s' % ZAPI_ERROR + assert msg == expect_and_capture_ansible_exception(create_module(vol_module, MINIMUM_ARGS, module_args).rehost_volume, 'fail')['msg'] + + +def test_successful_volume_restore(): + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'snapshot-restore-volume', ZRR['success']), + ]) + module_args = { + 'snapshot_restore': 'snapshot_copy', + 'force_restore': True, + 'preserve_lun_ids': True + } + assert create_and_apply(vol_module, MINIMUM_ARGS, module_args)['changed'] + + +def test_error_volume_restore(): + register_responses([ + ('ZAPI', 'snapshot-restore-volume', ZRR['error']), + ]) + module_args = {'snapshot_restore': 'snapshot_copy'} + msg = 'Error restoring volume test_vol: %s' % ZAPI_ERROR + assert msg == expect_and_capture_ansible_exception(create_module(vol_module, MINIMUM_ARGS, module_args).snapshot_restore_volume, 'fail')['msg'] + + +def test_error_modify_flexvol_to_flexgroup(): + ''' Test successful modify vserver_dr_protection ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + module_args = {'auto_provision_as': 'flexgroup'} + msg = 'Error: changing a volume from one backend to another is not allowed. Current: flexvol, desired: flexgroup.' + assert msg == create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_error_modify_flexgroup_to_flexvol(): + ''' Changing the style from flexgroup to flexvol is not allowed ''' + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexgroup']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + module_args = {'aggregate_name': 'nothing'} + msg = 'Error: aggregate_name option cannot be used with FlexGroups.' + assert msg == create_and_apply(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_error_snaplock_not_supported_with_zapi(): + ''' Test successful modify vserver_dr_protection ''' + module_args = {'snaplock': {'retention': {'default': 'P30TM'}}} + msg = 'Error: snaplock option is not supported with ZAPI. It can only be used with REST. use_rest: never.' + assert msg == create_module(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_wait_for_task_completion_no_records(): + register_responses([ + ('ZAPI', 'results', ZRR['no_records']), + ]) + # using response to build a request + zapi_iter, valid = build_zapi_response({'fake-iter': 'any'}) + my_obj = create_module(vol_module, DEFAULT_ARGS) + assert my_obj.wait_for_task_completion(zapi_iter, lambda: True) is None + + +def test_wait_for_task_completion_one_response(): + register_responses([ + ('ZAPI', 'results', ZRR['one_record_no_data']), + ]) + # using response to build a request + zapi_iter, valid = build_zapi_response({'fake-iter': 'any'}) + my_obj = create_module(vol_module, DEFAULT_ARGS) + assert my_obj.wait_for_task_completion(zapi_iter, lambda x: False) is None + + +@patch('time.sleep') +def test_wait_for_task_completion_loop(skip_sleep): + register_responses([ + ('ZAPI', 'results', ZRR['one_record_no_data']), + ('ZAPI', 'results', ZRR['one_record_no_data']), + ('ZAPI', 'results', ZRR['one_record_no_data']), + ]) + + def check_state(x): + check_state.counter += 1 + # True continues the wait loop + # False exits the loop + return (True, True, False)[check_state.counter - 1] + + check_state.counter = 0 + + # using response to build a request + zapi_iter, valid = build_zapi_response({'fake-iter': 'any'}) + my_obj = create_module(vol_module, DEFAULT_ARGS) + assert my_obj.wait_for_task_completion(zapi_iter, check_state) is None + + +@patch('time.sleep') +def test_wait_for_task_completion_loop_with_recoverable_error(skip_sleep): + register_responses([ + ('ZAPI', 'results', ZRR['one_record_no_data']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['one_record_no_data']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['one_record_no_data']), + ]) + + def check_state(x): + check_state.counter += 1 + return (True, True, False)[check_state.counter - 1] + + check_state.counter = 0 + + # using response to build a request + zapi_iter, valid = build_zapi_response({'fake-iter': 'any'}) + my_obj = create_module(vol_module, DEFAULT_ARGS) + assert my_obj.wait_for_task_completion(zapi_iter, check_state) is None + + +@patch('time.sleep') +def test_wait_for_task_completion_loop_with_non_recoverable_error(skip_sleep): + register_responses([ + ('ZAPI', 'results', ZRR['one_record_no_data']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['one_record_no_data']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ('ZAPI', 'results', ZRR['error']), + ]) + + # using response to build a request + zapi_iter, valid = build_zapi_response({'fake-iter': 'any'}) + my_obj = create_module(vol_module, DEFAULT_ARGS) + assert str(my_obj.wait_for_task_completion(zapi_iter, lambda x: True)) == ZAPI_ERROR + + +@patch('time.sleep') +def test_start_encryption_conversion(skip_sleep): + register_responses([ + ('ZAPI', 'volume-encryption-conversion-start', ZRR['success']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_running']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_running']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_idle']), + ]) + module_args = { + 'wait_for_completion': True, + 'max_wait_time': 120 + } + my_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert my_obj.start_encryption_conversion(True) is None + + +@patch('time.sleep') +def test_error_on_wait_for_start_encryption_conversion(skip_sleep): + register_responses([ + ('ZAPI', 'volume-encryption-conversion-start', ZRR['success']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_running']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_running']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ]) + module_args = { + 'wait_for_completion': True, + 'max_wait_time': 280 + } + my_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + error = expect_and_capture_ansible_exception(my_obj.start_encryption_conversion, 'fail', True)['msg'] + assert error == 'Error getting volume encryption_conversion status: %s' % ZAPI_ERROR + + +def test_error_start_encryption_conversion(): + register_responses([ + ('ZAPI', 'volume-encryption-conversion-start', ZRR['error']), + ]) + module_args = { + 'wait_for_completion': True + } + my_obj = create_module(vol_module, DEFAULT_ARGS, module_args) + error = expect_and_capture_ansible_exception(my_obj.start_encryption_conversion, 'fail', True)['msg'] + assert error == 'Error enabling encryption for volume test_vol: %s' % ZAPI_ERROR + + +@patch('time.sleep') +def test_wait_for_volume_encryption_conversion_with_non_recoverable_error(skip_sleep): + register_responses([ + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_running']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_running']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ]) + my_obj = create_module(vol_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.wait_for_volume_encryption_conversion, 'fail')['msg'] + assert error == 'Error getting volume encryption_conversion status: %s' % ZAPI_ERROR + + +@patch('time.sleep') +def test_wait_for_volume_encryption_conversion(skip_sleep): + register_responses([ + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_running']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['error']), + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_idle']), + ]) + my_obj = create_module(vol_module, DEFAULT_ARGS) + assert my_obj.wait_for_volume_encryption_conversion() is None + + +def test_wait_for_volume_encryption_conversion_bad_status(): + register_responses([ + ('ZAPI', 'volume-encryption-conversion-get-iter', ZRR['vol_encryption_conversion_status_error']), + ]) + my_obj = create_module(vol_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.wait_for_volume_encryption_conversion, 'fail')['msg'] + assert error == 'Error converting encryption for volume test_vol: other' + + +@patch('time.sleep') +def test_wait_for_volume_move_with_non_recoverable_error(skip_sleep): + register_responses([ + ('ZAPI', 'volume-move-get-iter', ZRR['vol_move_status_running']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['vol_move_status_running']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ]) + my_obj = create_module(vol_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.wait_for_volume_move, 'fail')['msg'] + assert error == 'Error getting volume move status: %s' % ZAPI_ERROR + + +@patch('time.sleep') +def test_wait_for_volume_move(skip_sleep): + register_responses([ + ('ZAPI', 'volume-move-get-iter', ZRR['vol_move_status_running']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['error']), + ('ZAPI', 'volume-move-get-iter', ZRR['vol_move_status_idle']), + ]) + my_obj = create_module(vol_module, DEFAULT_ARGS) + assert my_obj.wait_for_volume_move() is None + + +def test_wait_for_volume_move_bad_status(): + register_responses([ + ('ZAPI', 'volume-move-get-iter', ZRR['vol_move_status_error']), + ]) + my_obj = create_module(vol_module, DEFAULT_ARGS) + error = expect_and_capture_ansible_exception(my_obj.wait_for_volume_move, 'fail')['msg'] + assert error == 'Error moving volume test_vol: some info' + + +def test_error_validate_snapshot_auto_delete(): + module_args = { + 'snapshot_auto_delete': { + 'commitment': 'whatever', + 'unknown': 'unexpected option' + } + } + error = "snapshot_auto_delete option 'unknown' is not valid." + assert create_module(vol_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_get_snapshot_auto_delete_attributes(): + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexgroup']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + result = create_module(vol_module, DEFAULT_ARGS).get_volume() + assert 'snapshot_auto_delete' in result + assert 'is_autodelete_enabled' not in result['snapshot_auto_delete'] + assert result['snapshot_auto_delete']['state'] == 'on' + + +def test_error_on_get_efficiency_info(): + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['error']), + ]) + error = 'Error fetching efficiency policy for volume test_vol: %s' % ZAPI_ERROR + assert call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] == error + + +def test_create_volume_from_main(): + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-create', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-modify-iter', ZRR['success']), + ('ZAPI', 'volume-unmount', ZRR['success']), + ('ZAPI', 'volume-offline', ZRR['success']) + ]) + args = dict(DEFAULT_ARGS) + del args['space_slo'] + module_args = { + 'aggregate_name': MOCK_VOL['aggregate'], + 'comment': 'some comment', + 'is_online': False, + 'space_guarantee': 'file', + 'tiering_policy': 'snapshot-only', + 'volume_security_style': 'unix', + 'vserver_dr_protection': 'unprotected', + } + assert call_main(my_main, args, module_args)['changed'] + + +def test_error_create_volume_change_in_type(): + register_responses([ + ('ZAPI', 'volume-get-iter', ZRR['no_records']), + ('ZAPI', 'volume-create', ZRR['success']), + ('ZAPI', 'volume-get-iter', ZRR['get_flexvol']), + ('ZAPI', 'sis-get-iter', ZRR['no_records']), + ]) + args = dict(DEFAULT_ARGS) + module_args = { + 'aggregate_name': MOCK_VOL['aggregate'], + 'type': 'dp', + } + error = 'Error: volume type was not set properly at creation time. Current: rw, desired: dp.' + assert call_main(my_main, args, module_args, fail=True)['msg'] == error + + +def test_create_volume_attribute(): + obj = create_module(vol_module, DEFAULT_ARGS) + # str + obj.parameters['option_name'] = 'my_option' + parent = netapp_utils.zapi.NaElement('results') + obj.create_volume_attribute(None, parent, 'zapi_name', 'option_name') + print(parent.to_string()) + assert parent['zapi_name'] == 'my_option' + # int - fail, unless converted + obj.parameters['option_name'] = 123 + expect_and_capture_ansible_exception(obj.create_volume_attribute, TypeError, None, parent, 'zapi_name', 'option_name') + parent = netapp_utils.zapi.NaElement('results') + obj.create_volume_attribute(None, parent, 'zapi_name', 'option_name', int) + assert parent['zapi_name'] == '123' + # boolmodify_volume_efficiency_config + obj.parameters['option_name'] = False + parent = netapp_utils.zapi.NaElement('results') + obj.create_volume_attribute(None, parent, 'zapi_name', 'option_name', bool) + assert parent['zapi_name'] == 'false' + # parent->attrs->attr + # create child + parent = netapp_utils.zapi.NaElement('results') + obj.create_volume_attribute('child', parent, 'zapi_name', 'option_name', bool) + assert parent['child']['zapi_name'] == 'false' + # use existing child in parent + obj.create_volume_attribute('child', parent, 'zapi_name2', 'option_name', bool) + assert parent['child']['zapi_name2'] == 'false' + # pass child + parent = netapp_utils.zapi.NaElement('results') + child = netapp_utils.zapi.NaElement('child') + obj.create_volume_attribute(child, parent, 'zapi_name', 'option_name', bool) + assert parent['child']['zapi_name'] == 'false' + + +def test_check_invoke_result(): + register_responses([ + # 3rd run + ('ZAPI', 'job-get', ZRR['job_success']), + # 3th run + ('ZAPI', 'job-get', ZRR['job_failure']), + ]) + module_args = { + 'time_out': 0 + } + obj = create_module(vol_module, DEFAULT_ARGS, module_args) + # 1 - operation failed immediately + error = 'Operation failed when testing volume.' + assert error in expect_and_capture_ansible_exception(obj.check_invoke_result, 'fail', ZRR['failed_results'][0], 'testing')['msg'] + # 2 - operation in progress - exit immediately as time_out is 0 + assert obj.check_invoke_result(ZRR['async_results'][0], 'testing') is None + module_args = { + 'time_out': 10 + } + # 3 - operation in progress - job reported success + obj = create_module(vol_module, DEFAULT_ARGS, module_args) + assert obj.check_invoke_result(ZRR['async_results'][0], 'testing') is None + # 4 - operation in progress - job reported a failure + obj = create_module(vol_module, DEFAULT_ARGS, module_args) + error = 'Error when testing volume: failure' + assert error in expect_and_capture_ansible_exception(obj.check_invoke_result, 'fail', ZRR['async_results'][0], 'testing')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_autosize.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_autosize.py new file mode 100644 index 000000000..662d95bfe --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_autosize.py @@ -0,0 +1,367 @@ +# (c) 2019, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_volume_autosize ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import call_main, patch_ansible, create_module, create_and_apply +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume_autosize \ + import NetAppOntapVolumeAutosize as autosize_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_uuid': (200, {'records': [{'uuid': 'testuuid'}]}, None), + 'get_autosize': (200, + {'uuid': 'testuuid', + 'name': 'testname', + 'autosize': {"maximum": 10737418240, + "minimum": 22020096, + "grow_threshold": 99, + "shrink_threshold": 40, + "mode": "grow" + } + }, None), + 'get_autosize_empty': (200, { + 'uuid': 'testuuid', + 'name': 'testname', + 'autosize': {} + }, None) +}) + + +MOCK_AUTOSIZE = { + 'grow_threshold_percent': 99, + 'maximum_size': '10g', + 'minimum_size': '21m', + 'increment_size': '10m', + 'mode': 'grow', + 'shrink_threshold_percent': 40, + 'vserver': 'test_vserver', + 'volume': 'test_volume' +} + + +autosize_info = { + 'grow-threshold-percent': MOCK_AUTOSIZE['grow_threshold_percent'], + 'maximum-size': '10485760', + 'minimum-size': '21504', + 'increment-size': '10240', + 'mode': MOCK_AUTOSIZE['mode'], + 'shrink-threshold-percent': MOCK_AUTOSIZE['shrink_threshold_percent'] +} + + +ZRR = zapi_responses({ + 'get_autosize': build_zapi_response(autosize_info) +}) + + +DEFAULT_ARGS = { + 'vserver': MOCK_AUTOSIZE['vserver'], + 'volume': MOCK_AUTOSIZE['volume'], + 'grow_threshold_percent': MOCK_AUTOSIZE['grow_threshold_percent'], + 'maximum_size': MOCK_AUTOSIZE['maximum_size'], + 'minimum_size': MOCK_AUTOSIZE['minimum_size'], + 'mode': MOCK_AUTOSIZE['mode'], + 'shrink_threshold_percent': MOCK_AUTOSIZE['shrink_threshold_percent'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + args = dict(DEFAULT_ARGS) + args.pop('vserver') + error = 'missing required arguments: vserver' + assert create_module(autosize_module, args, fail=True)['msg'] == error + + +def test_idempotent_modify(): + register_responses([ + ('ZAPI', 'volume-autosize-get', ZRR['get_autosize']), + ]) + module_args = { + 'use_rest': 'never' + } + assert not create_and_apply(autosize_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_modify(): + register_responses([ + ('ZAPI', 'volume-autosize-get', ZRR['get_autosize']), + ('ZAPI', 'volume-autosize-set', ZRR['success']), + ]) + module_args = { + 'increment_size': MOCK_AUTOSIZE['increment_size'], + 'maximum_size': '11g', + 'use_rest': 'never' + } + assert create_and_apply(autosize_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_zapi__create_get_volume_return_no_data(): + module_args = { + 'use_rest': 'never' + } + my_obj = create_module(autosize_module, DEFAULT_ARGS, module_args) + assert my_obj._create_get_volume_return(build_zapi_response({'unsupported_key': 'value'})[0]) is None + + +def test_error_get(): + register_responses([ + ('ZAPI', 'volume-autosize-get', ZRR['error']), + ]) + module_args = { + 'use_rest': 'never' + } + error = 'Error fetching volume autosize info for test_volume: NetApp API failed. Reason - 12345:synthetic error for UT purpose.' + assert create_and_apply(autosize_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_error_modify(): + register_responses([ + ('ZAPI', 'volume-autosize-get', ZRR['get_autosize']), + ('ZAPI', 'volume-autosize-set', ZRR['error']), + ]) + module_args = { + 'increment_size': MOCK_AUTOSIZE['increment_size'], + 'maximum_size': '11g', + 'use_rest': 'never' + } + error = 'Error modifying volume autosize for test_volume: NetApp API failed. Reason - 12345:synthetic error for UT purpose.' + assert create_and_apply(autosize_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_successful_reset(): + register_responses([ + ('ZAPI', 'volume-autosize-get', ZRR['get_autosize']), + ('ZAPI', 'volume-autosize-set', ZRR['success']), + ]) + args = dict(DEFAULT_ARGS) + for arg in ('maximum_size', 'minimum_size', 'grow_threshold_percent', 'shrink_threshold_percent', 'mode'): + # remove args that are eclusive with reset + args.pop(arg) + module_args = { + 'reset': True, + 'use_rest': 'never' + } + assert create_and_apply(autosize_module, args, module_args)['changed'] + + +def test_rest_error_volume_not_found(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['zero_records']), + ]) + error = 'Error fetching volume autosize info for test_volume: volume not found for vserver test_vserver.' + assert create_and_apply(autosize_module, DEFAULT_ARGS, fail=True)['msg'] == error + + +def test_rest_error_get(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['generic_error']), + ]) + module_args = { + 'maximum_size': '11g' + } + error = 'Error fetching volume autosize info for test_volume: calling: storage/volumes: got Expected error.' + assert create_and_apply(autosize_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_rest_error_patch(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize']), + ('PATCH', 'storage/volumes/testuuid', SRR['generic_error']), + ]) + module_args = { + 'maximum_size': '11g' + } + error = 'Error modifying volume autosize for test_volume: calling: storage/volumes/testuuid: got Expected error.' + assert create_and_apply(autosize_module, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_rest_successful_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize']), + ('PATCH', 'storage/volumes/testuuid', SRR['success']), + ]) + module_args = { + 'maximum_size': '11g' + } + assert create_and_apply(autosize_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_rest_idempotent_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize']), + ]) + assert not create_and_apply(autosize_module, DEFAULT_ARGS)['changed'] + + +def test_rest_idempotent_modify_no_attributes(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize_empty']), + ]) + module_args = { + 'maximum_size': '11g' + } + assert not create_and_apply(autosize_module, DEFAULT_ARGS)['changed'] + + +def test_rest__create_get_volume_return_no_data(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(autosize_module, DEFAULT_ARGS) + assert my_obj._create_get_volume_return({'unsupported_key': 'value'}) == {'uuid': None} + + +def test_rest_modify_no_data(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(autosize_module, DEFAULT_ARGS) + # remove all attributes + for arg in ('maximum_size', 'minimum_size', 'grow_threshold_percent', 'shrink_threshold_percent', 'mode'): + my_obj.parameters.pop(arg) + assert my_obj.modify_volume_autosize('uuid') is None + + +def test_rest_convert_to_bytes(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(autosize_module, DEFAULT_ARGS) + + module_args = { + 'minimum_size': '11k' + } + assert my_obj.convert_to_byte('minimum_size', module_args) == 11 * 1024 + + module_args = { + 'minimum_size': '11g' + } + assert my_obj.convert_to_byte('minimum_size', module_args) == 11 * 1024 * 1024 * 1024 + + +def test_rest_convert_to_kb(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + my_obj = create_module(autosize_module, DEFAULT_ARGS) + + module_args = { + 'minimum_size': '11k' + } + assert my_obj.convert_to_kb('minimum_size', module_args) == 11 + + module_args = { + 'minimum_size': '11g' + } + assert my_obj.convert_to_kb('minimum_size', module_args) == 11 * 1024 * 1024 + + +def test_rest_invalid_values(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize']) + ]) + module_args = { + 'minimum_size': '11kb' + } + error = 'minimum_size must end with a k, m, g or t, found b in 11kb.' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + module_args = { + 'minimum_size': '11kk' + } + error = 'minimum_size must start with a number, found 11k in 11kk.' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + module_args = { + 'minimum_size': '' + } + error = "minimum_size must start with a number, and must end with a k, m, g or t, found ''." + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + module_args = { + 'minimum_size': 10 + } + error = 'minimum_size must end with a k, m, g or t, found 0 in 10.' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + +def test_rest_unsupported_parameters(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_autosize']) + ]) + module_args = { + 'increment_size': '11k' + } + error = 'Rest API does not support increment size, please switch to ZAPI' + assert call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] == error + + # reset is not supported - when set to True + module_args = { + 'reset': True + } + args = dict(DEFAULT_ARGS) + for arg in ('maximum_size', 'minimum_size', 'grow_threshold_percent', 'shrink_threshold_percent', 'mode'): + # remove args that are eclusive with reset + args.pop(arg) + error = 'Rest API does not support reset, please switch to ZAPI' + assert call_main(my_main, args, module_args, fail=True)['msg'] == error + + # reset is ignored when False + module_args = { + 'reset': False + } + assert not call_main(my_main, args, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_missing_netapp_lib(mock_has_netapp_lib): + module_args = { + 'use_rest': 'never', + } + mock_has_netapp_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg == create_module(autosize_module, DEFAULT_ARGS, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_clone.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_clone.py new file mode 100644 index 000000000..f68401348 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_clone.py @@ -0,0 +1,210 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_volume_clone''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + call_main, create_and_apply, create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import zapi_responses, build_zapi_response, build_zapi_error + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume_clone \ + import NetAppONTAPVolumeClone as my_module, main as my_main + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +clone_info = { + 'attributes': { + 'volume-clone-info': { + 'volume': 'ansible', + 'parent-volume': 'ansible'}}} + +clone_info_split_in_progress = { + 'attributes': { + 'volume-clone-info': { + 'volume': 'ansible', + 'parent-volume': 'ansible', + 'block-percentage-complete': 20, + 'blocks-scanned': 56676, + 'blocks-updated': 54588}}} + +ZRR = zapi_responses({ + 'clone_info': build_zapi_response(clone_info, 1), + 'clone_info_split_in_progress': build_zapi_response(clone_info_split_in_progress, 1), + 'error_no_clone': build_zapi_error(15661, 'flexclone not found.') +}) + +DEFAULT_ARGS = { + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'vserver': 'ansible', + 'volume': 'ansible', + 'parent_volume': 'ansible', + 'split': None, + 'use_rest': 'never' +} + + +def test_module_fail_when_required_args_missing(): + ''' test required arguments are reported as errors ''' + msg = create_module(my_module, fail=True)['msg'] + print('Info: %s' % msg) + + +def test_ensure_get_called(): + ''' test get_volume_clone() for non-existent volume clone''' + register_responses([ + ('volume-clone-get', ZRR['empty']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + assert my_obj.get_volume_clone() is None + + +def test_ensure_get_called_existing(): + ''' test get_volume_clone() for existing volume clone''' + register_responses([ + ('volume-clone-get', ZRR['clone_info']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + current = {'split': False} + assert my_obj.get_volume_clone() == current + + +def test_ensure_get_called_no_clone_error(): + ''' test get_volume_clone() for existing volume clone''' + register_responses([ + ('volume-clone-get', ZRR['error_no_clone']) + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + current = {'split': False} + assert my_obj.get_volume_clone() is None + + +def test_successful_create(): + ''' test creating volume_clone without split and testing idempotency ''' + register_responses([ + ('volume-clone-get', ZRR['empty']), + ('volume-clone-create', ZRR['success']), + ('volume-clone-get', ZRR['clone_info']), + ]) + module_args = { + 'parent_snapshot': 'abc', + 'volume_type': 'dp', + 'qos_policy_group_name': 'abc', + 'junction_path': 'abc', + 'uid': '1', + 'gid': '1' + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_create_with_split(): + ''' test creating volume_clone with split and testing idempotency ''' + register_responses([ + # first test, create and split + ('volume-clone-get', ZRR['empty']), + ('volume-clone-create', ZRR['success']), + ('volume-clone-split-start', ZRR['success']), + # second test, clone already exists but is not split + ('volume-clone-get', ZRR['clone_info']), + ('volume-clone-split-start', ZRR['success']), + # third test, clone already exists, split already in progress + ('volume-clone-get', ZRR['clone_info_split_in_progress']), + ]) + module_args = { + 'parent_snapshot': 'abc', + 'volume_type': 'dp', + 'qos_policy_group_name': 'abc', + 'junction_path': 'abc', + 'uid': '1', + 'gid': '1', + 'split': True + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successful_create_with_parent_vserver(): + ''' test creating volume_clone with split and testing idempotency ''' + register_responses([ + # first test, create and split + ('volume-clone-get', ZRR['empty']), + ('volume-clone-create', ZRR['success']), + ('volume-clone-split-start', ZRR['success']), + # second test, clone already exists but is not split + ('volume-clone-get', ZRR['clone_info']), + ('volume-clone-split-start', ZRR['success']), + # third test, clone already exists, split already in progress + ('volume-clone-get', ZRR['clone_info_split_in_progress']), + ]) + module_args = { + 'parent_snapshot': 'abc', + 'parent_vserver': 'abc', + 'volume_type': 'dp', + 'qos_policy_group_name': 'abc', + 'space_reserve': 'volume', + 'split': True + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_vserver_cluster_options_give_error(): + module_args = { + 'parent_snapshot': 'abc', + 'parent_vserver': 'abc', + 'volume_type': 'dp', + 'qos_policy_group_name': 'abc', + 'junction_path': 'abc', + 'uid': '1', + 'gid': '1' + } + msg = create_module(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert "parameters are mutually exclusive: " in msg + print('Info: %s' % msg) + + +def test_if_all_methods_catch_exception(): + ''' test if all methods catch exception ''' + register_responses([ + ('volume-clone-get', ZRR['error']), + ('volume-clone-create', ZRR['error']), + ('volume-clone-split-start', ZRR['error']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + msg = expect_and_capture_ansible_exception(my_obj.get_volume_clone, 'fail')['msg'] + assert 'Error fetching volume clone information ' in msg + msg = expect_and_capture_ansible_exception(my_obj.create_volume_clone, 'fail')['msg'] + assert 'Error creating volume clone: ' in msg + msg = expect_and_capture_ansible_exception(my_obj.start_volume_clone_split, 'fail')['msg'] + assert 'Error starting volume clone split: ' in msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_missing_netapp_lib(mock_has_netapp_lib): + ''' test error when netapp_lib is missing ''' + mock_has_netapp_lib.return_value = False + msg = create_module(my_module, DEFAULT_ARGS, fail=True)['msg'] + assert 'Error: the python NetApp-Lib module is required. Import error: None' == msg + + +def test_main(): + ''' validate call to main() ''' + register_responses([ + ('volume-clone-get', ZRR['empty']), + ('volume-clone-create', ZRR['success']), + ]) + assert call_main(my_main, DEFAULT_ARGS)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_clone_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_clone_rest.py new file mode 100644 index 000000000..ba0767d42 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_clone_rest.py @@ -0,0 +1,244 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import copy +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume_clone \ + import NetAppONTAPVolumeClone as my_module # module under test + +# needed for get and modify/delete as they still use ZAPI +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request + +clone_info = { + "clone": { + "is_flexclone": True, + "parent_snapshot": { + "name": "clone_ansibleVolume12_.2022-01-25_211704.0" + }, + "parent_svm": { + "name": "ansibleSVM" + }, + "parent_volume": { + "name": "ansibleVolume12" + } + }, + "name": "ansibleVolume12_clone", + "nas": { + "gid": 0, + "uid": 0 + }, + "svm": { + "name": "ansibleSVM" + }, + "uuid": "2458688d-7e24-11ec-a267-005056b30cfa" +} + +clone_info_no_uuid = dict(clone_info) +clone_info_no_uuid.pop('uuid') +clone_info_not_a_clone = copy.deepcopy(clone_info) +clone_info_not_a_clone['clone']['is_flexclone'] = False + +SRR = rest_responses({ + 'volume_clone': ( + 200, + {'records': [ + clone_info, + ]}, None + ), + 'volume_clone_no_uuid': ( + 200, + {'records': [ + clone_info_no_uuid, + ]}, None + ), + 'volume_clone_not_a_clone': ( + 200, + {'records': [ + clone_info_not_a_clone, + ]}, None + ), + 'two_records': ( + 200, + {'records': [ + clone_info, + clone_info_no_uuid, + ]}, None + ) +}) + + +DEFAULT_ARGS = { + 'vserver': 'ansibleSVM', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'name': 'clone_of_parent_volume', + 'parent_volume': 'parent_volume' +} + + +def test_successfully_create_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('POST', 'storage/volumes', SRR['volume_clone']), + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {})['changed'] + + +def test_error_getting_volume_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['generic_error']), + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error getting volume clone clone_of_parent_volume: calling: storage/volumes: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_volume_clone_rest, 'fail')['msg'] + + +def test_error_creating_volume_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'storage/volumes', SRR['generic_error']), + ]) + my_module_object = create_module(my_module, DEFAULT_ARGS) + msg = 'Error creating volume clone clone_of_parent_volume: calling: storage/volumes: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_module_object.create_volume_clone_rest, 'fail')['msg'] + + +def test_error_space_reserve_volume_clone(): + error = create_module(my_module, fail=True)['msg'] + print('Info: %s' % error) + assert 'missing required arguments:' in error + assert 'name' in error + + +def test_successfully_create_with_optional_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('POST', 'storage/volumes', SRR['volume_clone']), + ]) + module_args = { + 'qos_policy_group_name': 'test_policy_name', + 'parent_snapshot': 'test_snapshot', + 'volume_type': 'rw', + 'junction_path': '/test_junction_path', + 'uid': 10, + 'gid': 20, + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_create_with_parent_vserver_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('POST', 'storage/volumes', SRR['volume_clone']), + ]) + module_args = { + 'qos_policy_group_name': 'test_policy_name', + 'parent_snapshot': 'test_snapshot', + 'volume_type': 'rw', + 'parent_vserver': 'test_vserver', + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_create_and_split_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('POST', 'storage/volumes', SRR['volume_clone']), + ('PATCH', 'storage/volumes/2458688d-7e24-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = {'split': True} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_create_no_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('POST', 'storage/volumes', SRR['empty_records']), + ]) + module_args = {'split': True} + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg == 'Error starting volume clone split clone_of_parent_volume: clone UUID is not set' + + +def test_negative_create_no_uuid_in_response(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('POST', 'storage/volumes', SRR['volume_clone_no_uuid']), + ]) + module_args = {'split': True} + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg.startswith('Error: failed to parse create clone response: uuid key not present in') + + +def test_negative_create_bad_response(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('POST', 'storage/volumes', SRR['two_records']), + ]) + module_args = {'split': True} + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg.startswith('Error: failed to parse create clone response: calling: storage/volumes: unexpected response ') + + +def test_successfully_split_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['volume_clone']), + ('PATCH', 'storage/volumes/2458688d-7e24-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = {'split': True} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_split_volume_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('PATCH', 'storage/volumes/2458688d-7e24-11ec-a267-005056b30cfa', SRR['generic_error']), + ]) + my_obj = create_module(my_module, DEFAULT_ARGS) + my_obj.uuid = '2458688d-7e24-11ec-a267-005056b30cfa' + my_obj.parameters['split'] = True + msg = "Error starting volume clone split clone_of_parent_volume: calling: storage/volumes/2458688d-7e24-11ec-a267-005056b30cfa: got Expected error." + assert msg == expect_and_capture_ansible_exception(my_obj.start_volume_clone_split_rest, 'fail')['msg'] + + +def test_volume_not_a_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['volume_clone_not_a_clone']), + ]) + module_args = {'split': True} + assert not create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_volume_not_a_clone(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['volume_clone_not_a_clone']), + ]) + module_args = {'split': False} + msg = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + assert msg == 'Error: a volume clone_of_parent_volume which is not a FlexClone already exists, and split not requested.' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_efficiency.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_efficiency.py new file mode 100644 index 000000000..104cc8e51 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_efficiency.py @@ -0,0 +1,346 @@ +''' unit tests ONTAP Ansible module: na_ontap_volume_efficiency ''' +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume_efficiency \ + import NetAppOntapVolumeEfficiency as volume_efficiency_module, main # module under test + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +DEFAULT_ARGS = { + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'vserver': 'vs1', + 'path': '/vol/volTest', + 'policy': 'auto', + 'use_rest': 'never', + 'enable_compression': True, + 'enable_inline_compression': True, + 'enable_cross_volume_inline_dedupe': True, + 'enable_inline_dedupe': True, + 'enable_data_compaction': True, + 'enable_cross_volume_background_dedupe': True +} + +DEFAULT_ARGS_REST = { + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'vserver': 'vs1', + 'path': '/vol/volTest', + 'policy': 'auto', + 'use_rest': 'always' +} + + +def return_vol_info(state='enabled', status='idle', policy='auto'): + return { + 'num-records': 1, + 'attributes-list': { + 'sis-status-info': { + 'path': '/vol/volTest', + 'state': state, + 'schedule': None, + 'status': status, + 'policy': policy, + 'is-inline-compression-enabled': 'true', + 'is-compression-enabled': 'true', + 'is-inline-dedupe-enabled': 'true', + 'is-data-compaction-enabled': 'true', + 'is-cross-volume-inline-dedupe-enabled': 'true', + 'is-cross-volume-background-dedupe-enabled': 'true' + } + } + } + + +ZRR = zapi_responses({ + 'vol_eff_info': build_zapi_response(return_vol_info()), + 'vol_eff_info_disabled': build_zapi_response(return_vol_info(state='disabled')), + 'vol_eff_info_running': build_zapi_response(return_vol_info(status='running')), + 'vol_eff_info_policy': build_zapi_response(return_vol_info(policy='default')) +}) + + +def return_vol_info_rest(state='enabled', status='idle', policy='auto', compaction='inline'): + return { + "records": [{ + "uuid": "25311eff", + "name": "test_e", + "efficiency": { + "compression": "both", + "storage_efficiency_mode": "default", + "dedupe": "both", + "cross_volume_dedupe": "both", + "compaction": compaction, + "schedule": "-", + "volume_path": "/vol/test_e", + "state": state, + "op_state": status, + "type": "regular", + "progress": "Idle for 02:06:26", + "last_op_begin": "Mon Jan 02 00:10:00 2023", + "last_op_end": "Mon Jan 02 00:10:00 2023", + "last_op_size": 0, + "last_op_state": "Success", + "policy": {"name": policy} + } + }], + "num_records": 1 + } + + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'volume_efficiency_info': (200, return_vol_info_rest(), None), + 'volume_efficiency_status_running': (200, return_vol_info_rest(status='active'), None), + 'volume_efficiency_disabled': (200, return_vol_info_rest(state='disabled'), None), + 'volume_efficiency_modify': (200, return_vol_info_rest(compaction='none'), None), + "unauthorized": (403, None, {'code': 6, 'message': 'Unexpected argument "storage_efficiency_mode".'}), + "unexpected_arg": (403, None, {'code': 6, 'message': "not authorized for that command"}) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "vserver"] + error = create_module(volume_efficiency_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['path'] + assert 'one of the following is required: path, volume_name' in create_module(volume_efficiency_module, DEFAULT_ARGS_COPY, fail=True)['msg'] + + +def test_ensure_get_called_existing(): + ''' test get_volume_efficiency for existing config ''' + register_responses([ + ('sis-get-iter', ZRR['vol_eff_info']) + ]) + my_obj = create_module(volume_efficiency_module, DEFAULT_ARGS) + assert my_obj.get_volume_efficiency() + + +def test_successful_enable(): + ''' enable volume_efficiency and testing idempotency ''' + register_responses([ + ('sis-get-iter', ZRR['vol_eff_info_disabled']), + ('sis-enable', ZRR['success']), + ('sis-get-iter', ZRR['vol_eff_info']), + # idempotency check + ('sis-get-iter', ZRR['vol_eff_info']), + + ]) + DEFAULT_ARGS_COPY = DEFAULT_ARGS.copy() + del DEFAULT_ARGS_COPY['path'] + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_COPY, {'volume_name': 'volTest'})['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS)['changed'] + + +def test_successful_disable(): + ''' disable volume_efficiency and testing idempotency ''' + register_responses([ + ('sis-get-iter', ZRR['vol_eff_info']), + ('sis-disable', ZRR['success']), + # idempotency check + ('sis-get-iter', ZRR['vol_eff_info_disabled']), + + ]) + args = { + 'state': 'absent', + 'use_rest': 'never' + } + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, args)['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_successful_modify(): + ''' modifying volume_efficiency config and testing idempotency ''' + register_responses([ + ('sis-get-iter', ZRR['vol_eff_info']), + ('sis-set-config', ZRR['success']), + # idempotency check + ('sis-get-iter', ZRR['vol_eff_info_policy']), + + ]) + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS, {'policy': 'default'})['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS, {'policy': 'default'})['changed'] + + +def test_successful_start(): + ''' start volume_efficiency and testing idempotency ''' + register_responses([ + ('sis-get-iter', ZRR['vol_eff_info']), + ('sis-start', ZRR['success']), + # idempotency check + ('sis-get-iter', ZRR['vol_eff_info_running']), + + ]) + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS, {'volume_efficiency': 'start'})['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS, {'volume_efficiency': 'start'})['changed'] + + +def test_successful_stop(): + ''' stop volume_efficiency and testing idempotency ''' + register_responses([ + ('sis-get-iter', ZRR['vol_eff_info_running']), + ('sis-stop', ZRR['success']), + # idempotency check + ('sis-get-iter', ZRR['vol_eff_info']), + + ]) + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS, {'volume_efficiency': 'stop'})['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS, {'volume_efficiency': 'stop'})['changed'] + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('sis-get-iter', ZRR['error']), + ('sis-set-config', ZRR['error']), + ('sis-start', ZRR['error']), + ('sis-stop', ZRR['error']), + ('sis-enable', ZRR['error']), + ('sis-disable', ZRR['error']), + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('PATCH', 'storage/volumes', SRR['generic_error']), + ('PATCH', 'storage/volumes', SRR['unauthorized']), + ('PATCH', 'storage/volumes', SRR['unexpected_arg']) + ]) + vol_eff_obj = create_module(volume_efficiency_module, DEFAULT_ARGS) + assert 'Error getting volume efficiency' in expect_and_capture_ansible_exception(vol_eff_obj.get_volume_efficiency, 'fail')['msg'] + assert 'Error modifying storage efficiency' in expect_and_capture_ansible_exception(vol_eff_obj.modify_volume_efficiency, 'fail', {})['msg'] + assert 'Error starting storage efficiency' in expect_and_capture_ansible_exception(vol_eff_obj.start_volume_efficiency, 'fail')['msg'] + assert 'Error stopping storage efficiency' in expect_and_capture_ansible_exception(vol_eff_obj.stop_volume_efficiency, 'fail')['msg'] + assert 'Error enabling storage efficiency' in expect_and_capture_ansible_exception(vol_eff_obj.enable_volume_efficiency, 'fail')['msg'] + assert 'Error disabling storage efficiency' in expect_and_capture_ansible_exception(vol_eff_obj.disable_volume_efficiency, 'fail')['msg'] + + args = {'state': 'absent', 'enable_compression': True} + modify = {'enabled': 'disabled'} + vol_eff_obj = create_module(volume_efficiency_module, DEFAULT_ARGS_REST, args) + assert 'Error getting volume efficiency' in expect_and_capture_ansible_exception(vol_eff_obj.get_volume_efficiency, 'fail')['msg'] + assert 'Error in volume/efficiency patch' in expect_and_capture_ansible_exception(vol_eff_obj.modify_volume_efficiency, 'fail', {'arg': 1})['msg'] + assert 'cannot modify storage_efficiency' in expect_and_capture_ansible_exception(vol_eff_obj.modify_volume_efficiency, 'fail', {'arg': 1})['msg'] + assert 'user is not authorized' in expect_and_capture_ansible_exception(vol_eff_obj.modify_volume_efficiency, 'fail', {'arg': 1})['msg'] + # Error: cannot set compression keys: ['enable_compression'] + assert 'when volume efficiency already disabled' in expect_and_capture_ansible_exception(vol_eff_obj.validate_efficiency_compression, 'fail', {})['msg'] + assert 'when trying to disable volume' in expect_and_capture_ansible_exception(vol_eff_obj.validate_efficiency_compression, 'fail', modify)['msg'] + + +def test_successful_enable_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_disabled']), + ('PATCH', 'storage/volumes/25311eff', SRR['success']), + # idempotency check + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_info']), + ]) + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, {'use_rest': 'always'})['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, {'use_rest': 'always'})['changed'] + + +def test_successful_disable_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_info']), + ('PATCH', 'storage/volumes/25311eff', SRR['success']), + # idempotency check + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_disabled']), + ]) + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, {'state': 'absent'})['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, {'state': 'absent'})['changed'] + + +def test_successful_modify_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_info']), + ('PATCH', 'storage/volumes/25311eff', SRR['success']), + # idempotency check + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_modify']), + ]) + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, {'enable_data_compaction': False})['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, {'enable_data_compaction': False})['changed'] + + +def test_successful_enable_vol_efficiency_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_disabled']), + ('PATCH', 'storage/volumes/25311eff', SRR['success']), + # idempotency check + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_info']), + ]) + DEFAULT_ARGS_REST_COPY = DEFAULT_ARGS_REST.copy() + del DEFAULT_ARGS_REST_COPY['path'] + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST_COPY, {'volume_name': 'vol1'})['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST)['changed'] + + +def test_successful_start_rest_all_options(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_info']), + ('PATCH', 'storage/volumes/25311eff', SRR['success']), + # idempotency check + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_status_running']), + ]) + args = { + 'volume_efficiency': 'start', + 'start_ve_scan_old_data': True + } + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, args)['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_successful_stop_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_status_running']), + ('PATCH', 'storage/volumes/25311eff', SRR['success']), + # idempotency check + ('GET', 'cluster', SRR['is_rest_9_11_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_info']), + ]) + args = {'volume_efficiency': 'stop'} + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, args)['changed'] + assert not create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_negative_modify_rest_se_mode_no_version(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + error = 'Error: Minimum version of ONTAP for storage_efficiency_mode is (9, 10, 1)' + assert error in create_module(volume_efficiency_module, DEFAULT_ARGS_REST, {'storage_efficiency_mode': 'default'}, fail=True)['msg'] + error = 'Error: cannot set storage_efficiency_mode in ZAPI' + assert error in create_module(volume_efficiency_module, DEFAULT_ARGS, {'storage_efficiency_mode': 'default'}, fail=True)['msg'] + + +def test_modify_rest_se_mode(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['volume_efficiency_info']), + ('PATCH', 'storage/volumes/25311eff', SRR['success']) + ]) + assert create_and_apply(volume_efficiency_module, DEFAULT_ARGS_REST, {'storage_efficiency_mode': 'efficient'})['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_rest.py new file mode 100644 index 000000000..47525beec --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_rest.py @@ -0,0 +1,1440 @@ +# (c) 2020-2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import copy +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + assert_no_warnings, assert_warning_was_raised, print_warnings, call_main, create_and_apply,\ + create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume \ + import NetAppOntapVolume as volume_module, main as my_main # module under test + +# needed for get and modify/delete as they still use ZAPI +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +volume_info = { + "uuid": "7882901a-1aef-11ec-a267-005056b30cfa", + "comment": "carchi8py", + "name": "test_svm", + "state": "online", + "style": "flexvol", + "tiering": { + "policy": "backup", + "min_cooling_days": 0 + }, + "type": "rw", + "aggregates": [ + { + "name": "aggr1", + "uuid": "aggr1_uuid" + } + ], + "encryption": { + "enabled": True + }, + "efficiency": { + "compression": "none", + "policy": { + "name": "-" + } + }, + "files": { + "maximum": 2000 + }, + "nas": { + "gid": 0, + "security_style": "unix", + "uid": 0, + "unix_permissions": 654, + "path": '/this/path', + "export_policy": { + "name": "default" + } + }, + "snapshot_policy": { + "name": "default", + "uuid": "0a42a3d9-0c29-11ec-a267-005056b30cfa" + }, + "space": { + "logical_space": { + "enforcement": False, + "reporting": False, + }, + "size": 10737418240, + "snapshot": { + "reserve_percent": 5 + } + }, + "guarantee": { + "type": "volume" + }, + "snaplock": { + "type": "non_snaplock" + }, + "analytics": { + "state": "on" + } +} + +volume_info_mount = copy.deepcopy(volume_info) +volume_info_mount['nas']['path'] = '' +del volume_info_mount['nas']['path'] +volume_info_encrypt_off = copy.deepcopy(volume_info) +volume_info_encrypt_off['encryption']['enabled'] = False +volume_info_sl_enterprise = copy.deepcopy(volume_info) +volume_info_sl_enterprise['snaplock']['type'] = 'enterprise' +volume_info_sl_enterprise['snaplock']['retention'] = {'default': 'P30Y'} +volume_analytics_disabled = copy.deepcopy(volume_info) +volume_analytics_disabled['analytics']['state'] = 'off' +volume_analytics_initializing = copy.deepcopy(volume_info) +volume_analytics_initializing['analytics']['state'] = 'initializing' +volume_info_offline = copy.deepcopy(volume_info) +volume_info_offline['state'] = 'offline' +volume_info_tags = copy.deepcopy(volume_info) +volume_info_tags['_tags'] = ["team:csi", "environment:test"] + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_rest_96': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy_9_6_0')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'no_record': (200, {'num_records': 0, 'records': []}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + # Volume + 'get_volume': (200, {'records': [volume_info]}, None), + 'get_volume_sl_enterprise': (200, {'records': [volume_info_sl_enterprise]}, None), + 'get_volume_mount': (200, {'records': [volume_info_mount]}, None), + 'get_volume_encrypt_off': (200, {'records': [volume_info_encrypt_off]}, None), + # module specific responses + 'nas_app_record': (200, + {'records': [{"uuid": "09e9fd5e-8ebd-11e9-b162-005056b39fe7", + "name": "test_app", + "nas": { + "application_components": [{'xxx': 1}], + }}]}, None), + 'nas_app_record_by_uuid': (200, + {"uuid": "09e9fd5e-8ebd-11e9-b162-005056b39fe7", + "name": "test_app", + "nas": { + "application_components": [{'xxx': 1}], + "flexcache": { + "origin": {'svm': {'name': 'org_name'}} + } + }}, None), + 'get_aggr_one_object_store': (200, + {'records': ['one']}, None), + 'get_aggr_two_object_stores': (200, + {'records': ['two']}, None), + 'move_state_replicating': (200, {'movement': {'state': 'replicating'}}, None), + 'move_state_success': (200, {'movement': {'state': 'success'}}, None), + 'encrypting': (200, {'encryption': {'status': {'message': 'initializing'}}}, None), + 'encrypted': (200, {'encryption': {'state': 'encrypted'}}, None), + 'analytics_off': (200, {'records': [volume_analytics_disabled]}, None), + 'analytics_initializing': (200, {'records': [volume_analytics_initializing]}, None), + 'one_svm_record': (200, {'records': [{'uuid': 'svm_uuid'}]}, None), + 'volume_info_offline': (200, {'records': [volume_info_offline]}, None), + 'volume_info_tags': (200, {'records': [volume_info_tags]}, None) +}) + +DEFAULT_APP_ARGS = { + 'name': 'test_svm', + 'vserver': 'ansibleSVM', + 'nas_application_template': dict( + tiering=None + ), + # 'aggregate_name': 'whatever', # not used for create when using REST application/applications + 'size': 10, + 'size_unit': 'gb', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always' +} + +DEFAULT_VOLUME_ARGS = { + 'name': 'test_svm', + 'vserver': 'ansibleSVM', + 'aggregate_name': 'aggr1', + 'size': 10, + 'size_unit': 'gb', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always' +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + exc = create_module(volume_module, fail=True) + print('Info: %s' % exc['msg']) + assert 'missing required arguments:' in exc['msg'] + + +def test_fail_if_aggr_is_set(): + module_args = {'aggregate_name': 'should_fail'} + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + error = 'Conflict: aggregate_name is not supported when application template is enabled. Found: aggregate_name: should_fail' + assert create_module(volume_module, DEFAULT_APP_ARGS, module_args, fail=True)['msg'] == error + + +def test_missing_size(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # GET volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ]) + data = dict(DEFAULT_APP_ARGS) + data.pop('size') + error = 'Error: "size" is required to create nas application.' + assert create_and_apply(volume_module, data, fail=True)['msg'] == error + + +def test_mismatched_tiering_policies(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + module_args = { + 'tiering_policy': 'none', + 'nas_application_template': {'tiering': {'policy': 'auto'}} + } + error = 'Conflict: if tiering_policy and nas_application_template tiering policy are both set, they must match.'\ + ' Found "none" and "auto".' + assert create_module(volume_module, DEFAULT_APP_ARGS, module_args, fail=True)['msg'] == error + + +def test_rest_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # GET volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ('POST', 'application/applications', SRR['generic_error']), # POST application/applications + ]) + error = 'Error in create_nas_application: calling: application/applications: got %s.' % SRR['generic_error'][2] + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, fail=True)['msg'] == error + + +def test_rest_successfully_created(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ('POST', 'application/applications', SRR['empty_good']), # POST application/applications + ('GET', 'storage/volumes', SRR['get_volume']), + ]) + assert create_and_apply(volume_module, DEFAULT_APP_ARGS)['changed'] + + +def test_rest_create_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + + ]) + assert not create_and_apply(volume_module, DEFAULT_APP_ARGS)['changed'] + + +def test_rest_successfully_created_with_modify(): + ''' since language is not supported in application, the module is expected to: + 1. create the volume using application REST API + 2. immediately modify the volume to update options which are not available in the nas template. + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['empty_good']), # set unix_permissions + ]) + module_args = { + 'language': 'fr', + 'unix_permissions': '---rw-r-xr-x' + } + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + + +def test_rest_successfully_resized(): + ''' make sure resize if using RESP API if sizing_method is present + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['empty_good']), # PATCH storage/volumes + ]) + module_args = { + 'sizing_method': 'add_new_resources', + 'size': 20737418240 + } + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + + +def test_rest_volume_create_modify_tags(): + ''' volume create, modify with tags + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'storage/volumes', SRR['no_record']), + ('GET', 'svm/svms', SRR['one_svm_record']), + ('POST', 'storage/volumes', SRR['success']), + ('GET', 'storage/volumes', SRR['volume_info_tags']), + # idempotent check + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'storage/volumes', SRR['volume_info_tags']), + # modify tags + ('GET', 'cluster', SRR['is_rest_9_13_1']), + ('GET', 'storage/volumes', SRR['volume_info_tags']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['success']), + ]) + module_args = {'tags': ["team:csi", "environment:test"]} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + assert not create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + module_args = {'tags': ["team:csi"]} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_deleted(): + ''' delete volume using REST - no app + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['empty_good']), # PATCH storage/volumes - unmount + ('DELETE', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['empty_good']), # DELETE storage/volumes + ]) + module_args = {'state': 'absent'} + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + assert_no_warnings() + + +def test_rest_successfully_deleted_with_warning(): + ''' delete volume using REST - no app - unmount failed + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # PATCH storage/volumes - unmount + ('DELETE', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['empty_good']), # DELETE storage/volumes + ]) + module_args = {'state': 'absent'} + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + print_warnings() + assert_warning_was_raised('Volume was successfully deleted though unmount failed with: calling: ' + 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error.') + + +def test_rest_successfully_deleted_with_app(): + ''' delete app + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['empty_good']), # PATCH storage/volumes - unmount + ('DELETE', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['empty_good']), # DELETE storage/volumes + ]) + module_args = {'state': 'absent'} + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + + +def test_rest_successfully_move_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Move volume + ]) + module_args = {'aggregate_name': 'aggr2'} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_move_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Move volume + ]) + module_args = {'aggregate_name': 'aggr2'} + msg = "Error moving volume test_svm: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_error_rehost_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['zero_records']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ]) + module_args = {'from_vserver': 'svm_orig'} + msg = "Error: ONTAP REST API does not support Rehosting Volumes" + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_volume_unmount_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Mount Volume + ]) + module_args = {'junction_path': ''} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_volume_unmount_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Mount Volume + ]) + module_args = {'junction_path': ''} + msg = 'Error unmounting volume test_svm with path "": calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error.' + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_volume_mount_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_mount']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Mount Volume + ]) + module_args = {'junction_path': '/this/path'} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_volume_mount_do_nothing_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_mount']), # Get Volume + ]) + module_args = {'junction_path': ''} + assert not create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_volume_mount_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_mount']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Mount Volume + ]) + module_args = {'junction_path': '/this/path'} + msg = 'Error mounting volume test_svm with path "/this/path": calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error.' + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_change_volume_state(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['success']), # Move volume + ]) + module_args = {'is_online': False} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_change_volume_state(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Move volume + ]) + module_args = {'is_online': False} + msg = "Error changing state of volume test_svm: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_modify_attributes(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Modify + ]) + module_args = { + 'space_guarantee': 'volume', + 'percent_snapshot_space': 10, + 'snapshot_policy': 'default2', + 'export_policy': 'default2', + 'group_id': 5, + 'user_id': 5, + 'volume_security_style': 'mixed', + 'comment': 'carchi8py was here', + 'tiering_minimum_cooling_days': 10, + 'logical_space_enforcement': True, + 'logical_space_reporting': True + } + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_modify_attributes(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Modify + ]) + module_args = { + 'space_guarantee': 'volume', + 'percent_snapshot_space': 10, + 'snapshot_policy': 'default2', + 'export_policy': 'default2', + 'group_id': 5, + 'user_id': 5, + 'volume_security_style': 'mixed', + 'comment': 'carchi8py was here', + } + msg = "Error modifying volume test_svm: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_create_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + ]) + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS)['changed'] + + +def test_rest_error_get_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['generic_error']), # Get Volume + ]) + msg = "calling: storage/volumes: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, fail=True)['msg'] == msg + + +def test_rest_error_create_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['generic_error']), # Create Volume + ]) + msg = "Error creating volume test_svm: calling: storage/volumes: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, fail=True)['msg'] == msg + + +def test_rest_successfully_create_volume_with_options(): + module_args = { + 'space_guarantee': 'volume', + 'percent_snapshot_space': 5, + 'snapshot_policy': 'default', + 'export_policy': 'default', + 'group_id': 0, + 'user_id': 0, + 'volume_security_style': 'unix', + 'comment': 'carchi8py', + 'type': 'RW', + 'language': 'en', + 'encrypt': True, + 'junction_path': '/this/path', + 'tiering_policy': 'backup', + 'tiering_minimum_cooling_days': 10, + } + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + # TODO - force a patch after create + # ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # modify Volume + ]) + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS)['changed'] + + +def test_rest_successfully_snapshot_restore_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Modify Snapshot restore + ]) + module_args = {'snapshot_restore': 'snapshot_copy'} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_snapshot_restore_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Modify Snapshot restore + ]) + module_args = {'snapshot_restore': 'snapshot_copy'} + msg = "Error restoring snapshot snapshot_copy in volume test_svm: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_error_snapshot_restore_volume_no_parent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['zero_records']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ]) + module_args = {'snapshot_restore': 'snapshot_copy'} + msg = "Error restoring volume: cannot find parent: test_svm" + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_rename_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume name + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume from + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume from + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Patch + ]) + module_args = { + 'from_name': 'test_svm', + 'name': 'new_name' + } + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_rename_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume name + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume from + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume from + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Patch + ]) + module_args = { + 'from_name': 'test_svm', + 'name': 'new_name' + } + msg = "Error changing name of volume new_name: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_error_resizing_volume(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), # Get Volume name + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Resize volume + ]) + module_args = { + 'sizing_method': 'add_new_resources', + 'size': 20737418240 + } + msg = "Error resizing volume test_svm: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_create_volume_with_unix_permissions(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # add unix permissions + ]) + module_args = {'unix_permissions': '---rw-r-xr-x'} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_create_volume_with_qos_policy(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Set policy name + ]) + module_args = {'qos_policy_group': 'policy-name'} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_create_volume_with_qos_adaptive_policy_group(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Set policy name + ]) + module_args = {'qos_adaptive_policy_group': 'policy-name'} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_create_volume_with_qos_adaptive_policy_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + module_args = { + 'qos_adaptive_policy_group': 'policy-name', + 'qos_policy_group': 'policy-name' + } + msg = "Error: With Rest API qos_policy_group and qos_adaptive_policy_group are now the same thing, and cannot be set at the same time" + assert create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_create_volume_with_tiering_policy(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Set Tiering_policy + ]) + module_args = {'tiering_policy': 'all'} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_create_volume_encrypt(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Set Encryption + ]) + module_args = {'encrypt': False} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +@patch('time.sleep') +def test_rest_successfully_modify_volume_encrypt(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_encrypt_off']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # Set Encryption + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_encrypt_off']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), + ('GET', 'storage/volumes', SRR['encrypting']), + ('GET', 'storage/volumes', SRR['encrypting']), + ('GET', 'storage/volumes', SRR['encrypting']), + ('GET', 'storage/volumes', SRR['encrypted']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_encrypt_off']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']) + ]) + module_args = {'encrypt': True} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + module_args = {'encrypt': True, 'wait_for_completion': True} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + error = 'Error getting volume encryption_conversion status' + assert error in create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] + error = 'unencrypting volume is only supported when moving the volume to another aggregate in REST' + assert error in create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, {'encrypt': False}, fail=True)['msg'] + + +def test_rest_error_modify_volume_encrypt(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_encrypt_off']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Set Encryption + ]) + module_args = {'encrypt': True} + msg = "Error enabling encryption for volume test_svm: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_successfully_modify_volume_compression(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_encrypt_off']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # compression + ]) + module_args = { + 'efficiency_policy': 'test', + 'compression': True + } + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_successfully_modify_volume_inline_compression(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_encrypt_off']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # compression + ]) + module_args = { + 'efficiency_policy': 'test', + 'inline_compression': True + } + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_modify_volume_efficiency_policy(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_encrypt_off']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Set Encryption + ]) + module_args = {'efficiency_policy': 'test'} + msg = "Error setting efficiency for volume test_svm: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_error_volume_compression_both(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume_encrypt_off']), # Get Volume + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['generic_error']), # Set Encryption + ]) + module_args = { + 'compression': True, + 'inline_compression': True + } + msg = "Error setting efficiency for volume test_svm: calling: storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa: got Expected error." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_error_modify_volume_efficiency_policy_with_ontap_96(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + module_args = {'efficiency_policy': 'test'} + msg = "Error: Minimum version of ONTAP for efficiency_policy is (9, 7)." + assert msg in create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] + + +def test_rest_error_modify_volume_tiering_minimum_cooling_days_98(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + module_args = {'tiering_minimum_cooling_days': 2} + msg = "Error: Minimum version of ONTAP for tiering_minimum_cooling_days is (9, 8)." + assert msg in create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] + + +def test_rest_successfully_created_with_logical_space(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), # GET svm + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + ]) + module_args = { + 'logical_space_enforcement': False, + 'logical_space_reporting': False + } + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_rest_error_modify_backend_fabricpool(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), + ('GET', 'storage/aggregates/aggr1_uuid/cloud-stores', SRR['no_record']), # get_aggr_object_stores + ]) + module_args = { + 'nas_application_template': {'tiering': {'control': 'required'}}, + 'feature_flags': {'warn_or_fail_on_fabricpool_backend_change': 'fail'} + } + + msg = "Error: changing a volume from one backend to another is not allowed. Current tiering control: disallowed, desired: required." + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args, fail=True)['msg'] == msg + + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), + ('GET', 'application/applications', SRR['no_record']), # TODO: modify + ]) + module_args['feature_flags'] = {'warn_or_fail_on_fabricpool_backend_change': 'invalid'} + assert not create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + print_warnings() + warning = "Unexpected value 'invalid' for warn_or_fail_on_fabricpool_backend_change, expecting: None, 'ignore', 'fail', 'warn'" + assert_warning_was_raised(warning) + + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), + ('GET', 'storage/aggregates/aggr1_uuid/cloud-stores', SRR['no_record']), # get_aggr_object_stores + ('GET', 'application/applications', SRR['no_record']), # TODO: modify + ]) + module_args['feature_flags'] = {'warn_or_fail_on_fabricpool_backend_change': 'warn'} + assert not create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + warning = "Ignored %s" % msg + print_warnings() + assert_warning_was_raised(warning) + + +def test_rest_negative_modify_backend_fabricpool(): + ''' fail to get aggregate object store''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), + ('GET', 'storage/aggregates/aggr1_uuid/cloud-stores', SRR['generic_error']), + ]) + module_args = { + 'nas_application_template': {'tiering': {'control': 'required'}}, + 'feature_flags': {'warn_or_fail_on_fabricpool_backend_change': 'fail'} + } + msg = "Error getting object store for aggregate: aggr1: calling: storage/aggregates/aggr1_uuid/cloud-stores: got Expected error." + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args, fail=True)['msg'] == msg + + +def test_rest_tiering_control(): + ''' The volume is supported by one or more aggregates + If all aggregates are associated with one or more object stores, the volume has a FabricPool backend. + If all aggregates are not associated with one or more object stores, the volume meets the 'disallowed' criteria. + ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/aggregates/uuid1/cloud-stores', SRR['no_record']), # get_aggr_object_stores aggr1 + ('GET', 'storage/aggregates/uuid2/cloud-stores', SRR['no_record']), # get_aggr_object_stores aggr2 + ('GET', 'storage/aggregates/uuid1/cloud-stores', SRR['get_aggr_one_object_store']), # get_aggr_object_stores aggr1 + ('GET', 'storage/aggregates/uuid2/cloud-stores', SRR['no_record']), # get_aggr_object_stores aggr2 + ('GET', 'storage/aggregates/uuid1/cloud-stores', SRR['get_aggr_two_object_stores']), # get_aggr_object_stores aggr1 + ('GET', 'storage/aggregates/uuid2/cloud-stores', SRR['get_aggr_one_object_store']), # get_aggr_object_stores aggr2 + ]) + module_args = { + 'nas_application_template': {'tiering': {'control': 'required'}}, + 'feature_flags': {'warn_or_fail_on_fabricpool_backend_change': 'fail'} + } + current = {'aggregates': [{'name': 'aggr1', 'uuid': 'uuid1'}, {'name': 'aggr2', 'uuid': 'uuid2'}]} + vol_object = create_module(volume_module, DEFAULT_APP_ARGS, module_args) + result = vol_object.tiering_control(current) + assert result == 'disallowed' + result = vol_object.tiering_control(current) + assert result == 'best_effort' + result = vol_object.tiering_control(current) + assert result == 'required' + current = {'aggregates': []} + result = vol_object.tiering_control(current) + assert result is None + + +def test_error_snaplock_volume_create_sl_type_not_changed(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('GET', 'svm/svms', SRR['one_svm_record']), + ('POST', 'storage/volumes', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['get_volume']), + ]) + module_args = {'snaplock': {'type': 'enterprise'}} + error = 'Error: volume snaplock type was not set properly at creation time. Current: non_snaplock, desired: enterprise.' + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == error + + +def test_error_snaplock_volume_create_sl_type_not_supported(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('GET', 'svm/svms', SRR['one_svm_record']), + ]) + module_args = {'snaplock': {'type': 'enterprise'}} + error = 'Error: using snaplock type requires ONTAP 9.10.1 or later and REST must be enabled - ONTAP version: 9.6.0 - using REST.' + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == error + + +def test_error_snaplock_volume_create_sl_options_not_supported_when_non_snaplock(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('GET', 'svm/svms', SRR['one_svm_record']), + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('GET', 'svm/svms', SRR['one_svm_record']), + ]) + module_args = {'snaplock': { + 'type': 'non_snaplock', + 'retention': {'default': 'P30Y'} + }} + error = "Error: snaplock options are not supported for non_snaplock volume, found: {'retention': {'default': 'P30Y'}}." + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == error + + # 'non_snaplock' is the default too + module_args = {'snaplock': { + 'retention': {'default': 'P30Y'} + }} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == error + + +def test_snaplock_volume_create(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['empty_records']), + ('GET', 'svm/svms', SRR['one_svm_record']), + ('POST', 'storage/volumes', SRR['empty_records']), + ('GET', 'storage/volumes', SRR['get_volume_sl_enterprise']), + ]) + module_args = {'snaplock': {'type': 'enterprise', 'retention': {'maximum': 'P5D'}}} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_error_snaplock_volume_modify_type(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['get_volume_sl_enterprise']), + ]) + module_args = {'snaplock': {'type': 'compliance'}} + error = 'Error: changing a volume snaplock type after creation is not allowed. Current: enterprise, desired: compliance.' + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == error + + +def test_snaplock_volume_modify_other_options(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['get_volume_sl_enterprise']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['success']), + ]) + module_args = {'snaplock': { + 'retention': {'default': 'P20Y'} + }} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_snaplock_volume_modify_other_options_idempotent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['get_volume_sl_enterprise']), + ]) + module_args = {'snaplock': { + 'retention': {'default': 'P30Y'} + }} + assert not create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +def test_max_files_volume_modify(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['get_volume_sl_enterprise']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['success']), + ]) + module_args = {'max_files': 3000} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, module_args)['changed'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_use_zapi_and_netapp_lib_missing(mock_has_netapp_lib): + """ZAPI requires netapp_lib""" + register_responses([ + ]) + mock_has_netapp_lib.return_value = False + module_args = {'use_rest': 'never'} + error = 'Error: the python NetApp-Lib module is required. Import error: None' + assert create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == error + + +def test_fallback_to_zapi_and_nas_application_is_used(): + """fallback to ZAPI when use_rest: auto and some ZAPI only options are used""" + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = {'use_rest': 'auto', 'cutover_action': 'wait', 'nas_application_template': {'storage_service': 'value'}} + error = "Error: nas_application_template requires REST support. use_rest: auto. "\ + "Conflict because of unsupported option(s) or option value(s) in REST: ['cutover_action']." + assert create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == error + assert_warning_was_raised("Falling back to ZAPI because of unsupported option(s) or option value(s) in REST: ['cutover_action']") + + +def test_fallback_to_zapi_and_rest_option_is_used(): + """fallback to ZAPI when use_rest: auto and some ZAPI only options are used""" + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = {'use_rest': 'auto', 'cutover_action': 'wait', 'sizing_method': 'use_existing_resources'} + error = "Error: sizing_method option is not supported with ZAPI. It can only be used with REST. use_rest: auto. "\ + "Conflict because of unsupported option(s) or option value(s) in REST: ['cutover_action']." + assert create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args, fail=True)['msg'] == error + assert_warning_was_raised("Falling back to ZAPI because of unsupported option(s) or option value(s) in REST: ['cutover_action']") + + +def test_error_conflict_export_policy_and_nfs_access(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = { + 'export_policy': 'auto', + 'nas_application_template': { + 'tiering': None, + 'nfs_access': [{'access': 'ro'}] + }, + 'tiering_policy': 'backup' + } + error = 'Conflict: export_policy option and nfs_access suboption in nas_application_template are mutually exclusive.' + assert create_module(volume_module, DEFAULT_APP_ARGS, module_args, fail=True)['msg'] == error + + +def test_create_nas_app_nfs_access(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ('POST', 'application/applications', SRR['empty_good']), # POST application/applications + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['get_volume']), + ]) + module_args = { + 'nas_application_template': { + 'exclude_aggregates': ['aggr_ex'], + 'nfs_access': [{'access': 'ro'}], + 'tiering': None, + }, + 'snapshot_policy': 'snspol' + } + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + + +def test_create_nas_app_tiering_object_store(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ('POST', 'application/applications', SRR['empty_good']), # POST application/applications + ('GET', 'storage/volumes', SRR['get_volume']), + ('GET', 'storage/aggregates/aggr1_uuid/cloud-stores', SRR['get_aggr_one_object_store']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['get_volume']), + ]) + module_args = { + 'nas_application_template': { + 'flexcache': { + 'dr_cache': True, + 'origin_component_name': 'ocn', + 'origin_svm_name': 'osn', + }, + 'storage_service': 'extreme', + 'tiering': { + 'control': 'required', + 'object_stores': ['obs1'] + }, + }, + 'export_policy': 'exppol', + 'qos_policy_group': 'qospol', + 'snapshot_policy': 'snspol' + } + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + + +def test_create_nas_app_tiering_policy_flexcache(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ('POST', 'application/applications', SRR['empty_good']), # POST application/applications + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['get_volume']), + ]) + module_args = { + 'nas_application_template': { + 'flexcache': { + 'dr_cache': True, + 'origin_component_name': 'ocn', + 'origin_svm_name': 'osn', + }, + 'storage_service': 'extreme', + }, + 'qos_policy_group': 'qospol', + 'snapshot_policy': 'snspol', + 'tiering_policy': 'snapshot-only', + } + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + + +def test_create_nas_app_tiering_flexcache(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), + ('GET', 'application/applications', SRR['no_record']), # GET application/applications + ('POST', 'application/applications', SRR['empty_good']), # POST application/applications + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['get_volume']), + ]) + module_args = { + 'nas_application_template': { + 'flexcache': { + 'dr_cache': True, + 'origin_component_name': 'ocn', + 'origin_svm_name': 'osn', + }, + 'storage_service': 'extreme', + 'tiering': { + 'control': 'best_effort' + }, + }, + 'qos_policy_group': 'qospol', + 'snapshot_policy': 'snspol' + } + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + + +def test_version_error_nas_app(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ]) + module_args = { + 'nas_application_template': { + 'flexcache': { + 'dr_cache': True, + 'origin_component_name': 'ocn', + 'origin_svm_name': 'osn', + }, + }, + } + error = 'Error: using nas_application_template requires ONTAP 9.7 or later and REST must be enabled - ONTAP version: 9.6.0.' + assert create_module(volume_module, DEFAULT_APP_ARGS, module_args, fail=True)['msg'] == error + + +def test_version_error_nas_app_dr_cache(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + module_args = { + 'nas_application_template': { + 'flexcache': { + 'dr_cache': True, + 'origin_component_name': 'ocn', + 'origin_svm_name': 'osn', + }, + }, + } + error = 'Error: using flexcache: dr_cache requires ONTAP 9.9 or later and REST must be enabled - ONTAP version: 9.8.0.' + assert create_module(volume_module, DEFAULT_APP_ARGS, module_args, fail=True)['msg'] == error + + +def test_error_volume_rest_patch(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + my_obj = create_module(volume_module, DEFAULT_APP_ARGS) + my_obj.parameters['uuid'] = None + error = 'Could not read UUID for volume test_svm in patch.' + assert expect_and_capture_ansible_exception(my_obj.volume_rest_patch, 'fail', {})['msg'] == error + + +def test_error_volume_rest_delete(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + my_obj = create_module(volume_module, DEFAULT_APP_ARGS) + my_obj.parameters['uuid'] = None + error = 'Could not read UUID for volume test_svm in delete.' + assert expect_and_capture_ansible_exception(my_obj.rest_delete_volume, 'fail', '')['msg'] == error + + +def test_error_modify_app_not_supported_no_volume_but_app(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['no_record']), + ('GET', 'svm/svms', SRR['one_svm_record']), + ('GET', 'application/applications', SRR['nas_app_record']), + ('GET', 'application/applications/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['nas_app_record_by_uuid']), + ]) + module_args = {} + # TODO: we need to handle this error case with a better error mssage + error = \ + 'Error in create_nas_application: function create_application should not be called when application uuid is set: 09e9fd5e-8ebd-11e9-b162-005056b39fe7.' + assert create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args, fail=True)['msg'] == error + + +def test_warning_modify_app_not_supported(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'storage/volumes', SRR['get_volume']), + ('GET', 'application/applications', SRR['nas_app_record']), + ('GET', 'application/applications/09e9fd5e-8ebd-11e9-b162-005056b39fe7', SRR['nas_app_record_by_uuid']), + ]) + module_args = { + 'nas_application_template': { + 'flexcache': { + 'dr_cache': True, + 'origin_component_name': 'ocn', + 'origin_svm_name': 'osn', + }, + }, + } + assert not create_and_apply(volume_module, DEFAULT_APP_ARGS, module_args)['changed'] + assert_warning_was_raised("Modifying an app is not supported at present: ignoring: {'flexcache': {'origin': {'svm': {'name': 'osn'}}}}") + + +def test_create_flexgroup_volume_from_main(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), # Get Volume + ('GET', 'svm/svms', SRR['one_svm_record']), + ('POST', 'storage/volumes', SRR['no_record']), # Create Volume + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # eff policy + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # modify + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['no_record']), # offline + ]) + args = copy.deepcopy(DEFAULT_VOLUME_ARGS) + del args['aggregate_name'] + module_args = { + 'aggr_list': 'aggr_0,aggr_1', + 'aggr_list_multiplier': 2, + 'comment': 'some comment', + 'compression': False, + 'efficiency_policy': 'effpol', + 'export_policy': 'exppol', + 'group_id': 1001, + 'junction_path': '/this/path', + 'inline_compression': False, + 'is_online': False, + 'language': 'us', + 'percent_snapshot_space': 10, + 'snapshot_policy': 'snspol', + 'space_guarantee': 'file', + 'tiering_minimum_cooling_days': 30, + 'tiering_policy': 'snapshot-only', + 'type': 'rw', + 'user_id': 123, + 'volume_security_style': 'unix', + } + assert call_main(my_main, args, module_args)['changed'] + + +def test_get_volume_style(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ]) + args = copy.deepcopy(DEFAULT_VOLUME_ARGS) + del args['aggregate_name'] + module_args = { + 'auto_provision_as': 'flexgroup', + } + my_obj = create_module(volume_module, args, module_args) + assert my_obj.get_volume_style(None) == 'flexgroup' + assert my_obj.parameters.get('aggr_list_multiplier') == 1 + + +def test_move_volume_with_rest_passthrough(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('PATCH', 'private/cli/volume/move/start', SRR['success']), + ('PATCH', 'private/cli/volume/move/start', SRR['generic_error']), + ]) + module_args = { + 'aggregate_name': 'aggr2' + } + obj = create_module(volume_module, DEFAULT_VOLUME_ARGS, module_args) + error = obj.move_volume_with_rest_passthrough(True) + assert error is None + error = obj.move_volume_with_rest_passthrough(True) + assert 'Expected error' in error + + +def test_ignore_small_change(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + obj = create_module(volume_module, DEFAULT_VOLUME_ARGS) + obj.parameters['attribute'] = 51 + assert obj.ignore_small_change({'attribute': 50}, 'attribute', .5) is None + assert obj.parameters['attribute'] == 51 + assert_no_warnings() + obj.parameters['attribute'] = 50.2 + assert obj.ignore_small_change({'attribute': 50}, 'attribute', .5) is None + assert obj.parameters['attribute'] == 50 + print_warnings() + assert_warning_was_raised('resize request for attribute ignored: 0.4% is below the threshold: 0.5%') + + +def test_set_efficiency_rest_empty_body(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ]) + obj = create_module(volume_module, DEFAULT_VOLUME_ARGS) + # no action + assert obj.set_efficiency_rest() is None + + +@patch('time.sleep') +def test_volume_move_rest(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'storage/volumes', SRR['get_volume_mount']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['success']), + ('GET', 'storage/volumes', SRR['move_state_replicating']), + ('GET', 'storage/volumes', SRR['move_state_success']), + # error when trying to get volume status + ('GET', 'cluster', SRR['is_rest_9_8_0']), + ('GET', 'storage/volumes', SRR['get_volume_mount']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['success']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']), + ('GET', 'storage/volumes', SRR['generic_error']) + ]) + args = {'aggregate_name': 'aggr2', 'wait_for_completion': True, 'max_wait_time': 280} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, args)['changed'] + error = "Error getting volume move status" + assert error in create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, args, fail=True)['msg'] + + +def test_analytics_option(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['no_record']), + ('GET', 'svm/svms', SRR['one_svm_record']), + ('POST', 'storage/volumes', SRR['success']), + ('GET', 'storage/volumes', SRR['get_volume']), + # idempotency check + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), + # Disable analytics + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['get_volume']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['success']), + # Enable analytics + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['analytics_off']), + ('PATCH', 'storage/volumes/7882901a-1aef-11ec-a267-005056b30cfa', SRR['success']), + # Try to Enable analytics which is initializing(no change required.) + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['analytics_initializing']) + ]) + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, {'analytics': 'on'})['changed'] + assert not create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, {'analytics': 'on'})['changed'] + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, {'analytics': 'off'})['changed'] + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, {'analytics': 'on'})['changed'] + assert not create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, {'analytics': 'on'})['changed'] + + +def test_warn_rest_modify(): + """ Test skip snapshot_restore and modify when volume is offline """ + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'storage/volumes', SRR['volume_info_offline']) + ]) + args = {'is_online': False, 'junction_path': '/test', 'use_rest': 'always', 'snapshot_restore': 'restore1'} + assert create_and_apply(volume_module, DEFAULT_VOLUME_ARGS, args)['changed'] is False + assert_warning_was_raised("Cannot perform action(s): ['snapshot_restore'] and modify: ['junction_path']", partial_match=True) diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_snaplock.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_snaplock.py new file mode 100644 index 000000000..0c836233f --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_volume_snaplock.py @@ -0,0 +1,131 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" unit tests for Ansible module: na_ontap_volume_snaplock """ + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume_snaplock \ + import NetAppOntapVolumeSnaplock as snaplock_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.type = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.type == 'snaplock': + xml = self.build_snaplock_info(self.params) + elif self.type == 'zapi_error': + error = netapp_utils.zapi.NaApiError('test', 'error') + raise error + self.xml_out = xml + return xml + + @staticmethod + def build_snaplock_info(data): + ''' build xml data for vserser-info ''' + xml = netapp_utils.zapi.NaElement('xml') + attributes = {'snaplock-attrs': { + 'snaplock-attrs-info': { + 'autocommit-period': data['autocommit_period'], + 'default-retention-period': data['default_retention_period'], + 'maximum-retention-period': data['maximum_retention_period'], + 'minimum-retention-period': data['minimum_retention_period'], + 'is-volume-append-mode-enabled': data['is_volume_append_mode_enabled'] + } + }} + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' a group of related Unit Tests ''' + + def setUp(self): + self.mock_snaplock = { + 'autocommit_period': '10days', + 'default_retention_period': '1years', + 'maximum_retention_period': '2years', + 'minimum_retention_period': '6months', + 'is_volume_append_mode_enabled': 'false' + } + + def mock_args(self): + return { + 'name': 'test_volume', + 'autocommit_period': self.mock_snaplock['autocommit_period'], + 'default_retention_period': self.mock_snaplock['default_retention_period'], + 'maximum_retention_period': self.mock_snaplock['maximum_retention_period'], + 'minimum_retention_period': self.mock_snaplock['minimum_retention_period'], + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'test_vserver' + } + + def get_snaplock_mock_object(self, kind=None): + """ + Helper method to return an na_ontap_volume_snaplock object + :param kind: passes this param to MockONTAPConnection() + :return: na_ontap_volume_snaplock object + """ + snaplock_obj = snaplock_module() + if kind is None: + snaplock_obj.server = MockONTAPConnection() + else: + snaplock_obj.server = MockONTAPConnection(kind=kind, data=self.mock_snaplock) + return snaplock_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + snaplock_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_existing_snaplock(self): + set_module_args(self.mock_args()) + result = self.get_snaplock_mock_object(kind='snaplock').get_volume_snaplock_attrs() + assert result['autocommit_period'] == self.mock_snaplock['autocommit_period'] + assert result['default_retention_period'] == self.mock_snaplock['default_retention_period'] + assert result['is_volume_append_mode_enabled'] is False + assert result['maximum_retention_period'] == self.mock_snaplock['maximum_retention_period'] + + def test_modify_snaplock(self): + data = self.mock_args() + data['maximum_retention_period'] = '5years' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_snaplock_mock_object('snaplock').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_volume_snaplock.NetAppOntapVolumeSnaplock.get_volume_snaplock_attrs') + def test_modify_snaplock_error(self, get_volume_snaplock_attrs): + data = self.mock_args() + data['maximum_retention_period'] = '5years' + set_module_args(data) + get_volume_snaplock_attrs.side_effect = [self.mock_snaplock] + with pytest.raises(AnsibleFailJson) as exc: + self.get_snaplock_mock_object('zapi_error').apply() + assert exc.value.args[0]['msg'] == 'Error setting snaplock attributes for volume test_volume : NetApp API failed. Reason - test:error' diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan.py new file mode 100644 index 000000000..924865507 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan.py @@ -0,0 +1,200 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_vscan''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vscan \ + import NetAppOntapVscan as vscan_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') +HAS_NETAPP_ZAPI_MSG = "pip install netapp_lib is required" + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Ooops, the UT needs one more SRR response"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'enabled': (200, {'records': [{'enabled': True, 'svm': {'uuid': 'testuuid'}}]}, None), + 'disabled': (200, {'records': [{'enabled': False, 'svm': {'uuid': 'testuuid'}}]}, None), +} + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'enable': + xml = self.build_vscan_status_info(self.params) + self.xml_out = xml + return xml + + @staticmethod + def build_vscan_status_info(status): + xml = netapp_utils.zapi.NaElement('xml') + attributes = {'num-records': 1, + 'attributes-list': {'vscan-status-info': {'is-vscan-enabled': status}}} + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_job_schedule ''' + + def mock_args(self): + return { + 'enable': False, + 'vserver': 'vserver', + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_vscan_mock_object(self, cx_type='zapi', kind=None, status=None): + vscan_obj = vscan_module() + if cx_type == 'zapi': + if kind is None: + vscan_obj.server = MockONTAPConnection() + else: + vscan_obj.server = MockONTAPConnection(kind=kind, data=status) + # For rest, mocking is achieved through side_effect + return vscan_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + vscan_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_successfully_enable(self): + data = self.mock_args() + data['enable'] = True + data['use_rest'] = 'never' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_vscan_mock_object('zapi', 'enable', 'false').apply() + assert exc.value.args[0]['changed'] + + def test_idempotently_enable(self): + data = self.mock_args() + data['enable'] = True + data['use_rest'] = 'never' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_vscan_mock_object('zapi', 'enable', 'true').apply() + assert not exc.value.args[0]['changed'] + + def test_successfully_disable(self): + data = self.mock_args() + data['enable'] = False + data['use_rest'] = 'never' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_vscan_mock_object('zapi', 'enable', 'true').apply() + assert exc.value.args[0]['changed'] + + def test_idempotently_disable(self): + data = self.mock_args() + data['enable'] = False + data['use_rest'] = 'never' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_vscan_mock_object('zapi', 'enable', 'false').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_error(self, mock_request): + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_vscan_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['msg'] == SRR['generic_error'][2] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successly_enable(self, mock_request): + data = self.mock_args() + data['enable'] = True + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['disabled'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_vscan_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_idempotently_enable(self, mock_request): + data = self.mock_args() + data['enable'] = True + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['enabled'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_vscan_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successly_disable(self, mock_request): + data = self.mock_args() + data['enable'] = False + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['enabled'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_vscan_mock_object(cx_type='rest').apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_idempotently_disable(self, mock_request): + data = self.mock_args() + data['enable'] = False + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['disabled'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_vscan_mock_object(cx_type='rest').apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_access_policy.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_access_policy.py new file mode 100644 index 000000000..d5228c1cc --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_access_policy.py @@ -0,0 +1,348 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_vscan_scanner_pool ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import patch_ansible,\ + create_module, create_and_apply, expect_and_capture_ansible_exception +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vscan_on_access_policy \ + import NetAppOntapVscanOnAccessPolicy as policy_module # module under test +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +DEFAULT_ARGS = { + 'state': 'present', + 'vserver': 'test_vserver', + 'policy_name': 'test_carchi', + 'max_file_size': 2147483648 + 1, # 2GB + 1 + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' +} + + +vscan_info = { + 'num-records': 1, + 'attributes-list': { + 'vscan-on-access-policy-info': { + 'policy-name': 'test_carchi', + 'vserver': 'test_vserver', + 'max-file-size': 2147483648 + 1, + 'is-scan-mandatory': 'false', + 'scan-files-with-no-ext': 'true', + 'is-policy-enabled': 'true', + 'file-ext-to-include': ['py'] + } + } +} + + +ZRR = zapi_responses({ + 'vscan_info': build_zapi_response(vscan_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + # with python 2.6, dictionaries are not ordered + fragments = ["missing required arguments:", "hostname", "policy_name", "vserver"] + error = create_module(policy_module, {}, fail=True)['msg'] + for fragment in fragments: + assert fragment in error + + +def test_get_nonexistent_policy(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['empty']) + ]) + policy_obj = create_module(policy_module, DEFAULT_ARGS) + result = policy_obj.get_on_access_policy() + assert result is None + + +def test_get_existing_scanner(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['vscan_info']) + ]) + policy_obj = create_module(policy_module, DEFAULT_ARGS) + result = policy_obj.get_on_access_policy() + assert result + + +def test_successfully_create(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['empty']), + ('vscan-on-access-policy-create', ZRR['success']) + ]) + assert create_and_apply(policy_module, DEFAULT_ARGS)['changed'] + + +def test_create_idempotency(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['vscan_info']) + ]) + assert create_and_apply(policy_module, DEFAULT_ARGS)['changed'] is False + + +def test_successfully_delete(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['vscan_info']), + ('vscan-on-access-policy-delete', ZRR['success']) + ]) + assert create_and_apply(policy_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] + + +def test_delete_idempotency(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['empty']) + ]) + assert create_and_apply(policy_module, DEFAULT_ARGS, {'state': 'absent'})['changed'] is False + + +def test_successfully_create_and_enable_policy(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['empty']), + ('vscan-on-access-policy-create', ZRR['success']), + ('vscan-on-access-policy-status-modify', ZRR['success']) + ]) + args = {'policy_status': True} + assert create_and_apply(policy_module, DEFAULT_ARGS, args)['changed'] + + +def test_disable_policy_and_delete(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['vscan_info']), + ('vscan-on-access-policy-status-modify', ZRR['success']), + ('vscan-on-access-policy-delete', ZRR['success']) + ]) + args = {'policy_status': False, 'state': 'absent'} + assert create_and_apply(policy_module, DEFAULT_ARGS, args)['changed'] + + +def test_modify_policy(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['vscan_info']), + ('vscan-on-access-policy-modify', ZRR['success']) + ]) + args = {'max_file_size': 2147483650} + assert create_and_apply(policy_module, DEFAULT_ARGS, args)['changed'] + + +def test_modify_files_to_incluse_empty_error(): + args = {'file_ext_to_include': []} + msg = 'Error: The value for file_ext_include cannot be empty' + assert msg in create_module(policy_module, DEFAULT_ARGS, args, fail=True)['msg'] + + +def module_error_disable_policy(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['vscan_info']), + ('vscan-on-access-policy-status-modify', ZRR['error']) + ]) + args = {'policy_status': False} + error = create_and_apply(policy_module, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error modifying status Vscan on Access Policy' in error + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('vscan-on-access-policy-get-iter', ZRR['error']), + ('vscan-on-access-policy-create', ZRR['error']), + ('vscan-on-access-policy-modify', ZRR['error']), + ('vscan-on-access-policy-delete', ZRR['error']), + ]) + + policy_obj = create_module(policy_module, DEFAULT_ARGS) + + error = expect_and_capture_ansible_exception(policy_obj.get_on_access_policy, 'fail')['msg'] + assert 'Error searching Vscan on Access Policy' in error + + error = expect_and_capture_ansible_exception(policy_obj.create_on_access_policy, 'fail')['msg'] + assert 'Error creating Vscan on Access Policy' in error + + error = expect_and_capture_ansible_exception(policy_obj.modify_on_access_policy, 'fail')['msg'] + assert 'Error Modifying Vscan on Access Policy' in error + + error = expect_and_capture_ansible_exception(policy_obj.delete_on_access_policy, 'fail')['msg'] + assert 'Error Deleting Vscan on Access Policy' in error + + +DEFAULT_ARGS_REST = { + "policy_name": "custom_CIFS", + "policy_status": True, + "file_ext_to_exclude": ["exe", "yml", "py"], + "file_ext_to_include": ['txt', 'json'], + "scan_readonly_volumes": True, + "only_execute_access": False, + "is_scan_mandatory": True, + "paths_to_exclude": ['\folder1', '\folder2'], + "scan_files_with_no_ext": True, + "max_file_size": 2147483648, + "vserver": "vscan-test", + "hostname": "test", + "username": "test_user", + "password": "test_pass", + "use_rest": "always" +} + + +SRR = rest_responses({ + 'vscan_on_access_policy': (200, {"records": [ + { + "svm": {"name": "vscan-test"}, + "name": "custom_CIFS", + "enabled": True, + "mandatory": True, + "scope": { + "max_file_size": 2147483648, + "exclude_paths": ["\folder1", "\folder2"], + "include_extensions": ["txt", "json"], + "exclude_extensions": ["exe", "yml", "py"], + "scan_without_extension": True, + "scan_readonly_volumes": True, + "only_execute_access": False + } + } + ], "num_records": 1}, None), + 'svm_uuid': (200, {"records": [ + { + 'uuid': 'e3cb5c7f-cd20' + }], "num_records": 1}, None) +}) + + +def test_successfully_create_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies', SRR['empty_records']), + ('POST', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies', SRR['success']) + ]) + assert create_and_apply(policy_module, DEFAULT_ARGS_REST)['changed'] + + +def test_successfully_create_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies', SRR['vscan_on_access_policy']) + ]) + assert create_and_apply(policy_module, DEFAULT_ARGS_REST)['changed'] is False + + +def test_modify_policy_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies', SRR['vscan_on_access_policy']), + ('PATCH', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies/custom_CIFS', SRR['success']) + ]) + args = { + "policy_status": False, + "file_ext_to_exclude": ['yml'], + "file_ext_to_include": ['json'], + "scan_readonly_volumes": False, + "only_execute_access": True, + "is_scan_mandatory": False, + "paths_to_exclude": ['\folder1'], + "scan_files_with_no_ext": False, + "max_file_size": 2147483649 + } + assert create_and_apply(policy_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_disable_and_delete_policy_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies', SRR['vscan_on_access_policy']), + ('PATCH', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies/custom_CIFS', SRR['success']), + ('DELETE', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies/custom_CIFS', SRR['success']) + ]) + args = { + 'state': 'absent', + 'policy_status': False + } + assert create_and_apply(policy_module, DEFAULT_ARGS_REST, args)['changed'] + + +def test_delete_idempotent(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies', SRR['empty_records']) + ]) + args = { + 'state': 'absent' + } + assert create_and_apply(policy_module, DEFAULT_ARGS_REST, args)['changed'] is False + + +def test_get_vserver_not_found(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['empty_records']) + ]) + msg = 'Error vserver vscan-test does not exist or is not a data vserver.' + assert msg in create_and_apply(policy_module, DEFAULT_ARGS_REST, fail=True)['msg'] + + +def test_invalid_option_error_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']) + ]) + args = {'paths_to_exclude': [""]} + msg = 'Error: Invalid value specified for option(s)' + assert msg in create_module(policy_module, DEFAULT_ARGS_REST, args, fail=True)['msg'] + + +def test_get_error_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['svm_uuid']), + ('GET', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies', SRR['generic_error']) + ]) + msg = 'Error searching Vscan on Access Policy' + assert msg in create_and_apply(policy_module, DEFAULT_ARGS_REST, fail=True)['msg'] + + +def test_if_all_methods_catch_exception_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/svms', SRR['generic_error']), + ('POST', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies', SRR['generic_error']), + ('PATCH', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies/custom_CIFS', SRR['generic_error']), + ('DELETE', 'protocols/vscan/e3cb5c7f-cd20/on-access-policies/custom_CIFS', SRR['generic_error']) + ]) + + policy_obj = create_module(policy_module, DEFAULT_ARGS_REST) + policy_obj.svm_uuid = "e3cb5c7f-cd20" + + msg = 'calling: svm/svms: got Expected error.' + assert msg in expect_and_capture_ansible_exception(policy_obj.get_svm_uuid, 'fail')['msg'] + + msg = 'Error creating Vscan on Access Policy' + assert msg in expect_and_capture_ansible_exception(policy_obj.create_on_access_policy_rest, 'fail')['msg'] + + msg = 'Error Modifying Vscan on Access Policy' + assert msg in expect_and_capture_ansible_exception(policy_obj.modify_on_access_policy_rest, 'fail', {"policy_status": False})['msg'] + + msg = 'Error Deleting Vscan on Access Policy' + assert msg in expect_and_capture_ansible_exception(policy_obj.delete_on_access_policy_rest, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_demand_task.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_demand_task.py new file mode 100644 index 000000000..8060cef9a --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_demand_task.py @@ -0,0 +1,135 @@ +''' unit tests for Ansible module: na_ontap_vscan_on_demand_task ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vscan_on_demand_task \ + import NetAppOntapVscanOnDemandTask as onDemand_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'task': + xml = self.build_onDemand_pool_info(self.params) + self.xml_out = xml + return xml + + @staticmethod + def build_onDemand_pool_info(onDemand_details): + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'vscan-on-demand-task-info': { + 'task-name': onDemand_details['task_name'], + 'report-directory': onDemand_details['report_directory'], + 'scan-paths': { + 'string': onDemand_details['scan_paths'] + } + } + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_job_schedule ''' + + def setUp(self): + self.mock_onDemand = { + 'state': 'present', + 'vserver': 'test_vserver', + 'report_directory': '/', + 'task_name': '/', + 'scan_paths': '/' + } + + def mock_args(self): + return { + 'state': self.mock_onDemand['state'], + 'vserver': self.mock_onDemand['vserver'], + 'report_directory': self.mock_onDemand['report_directory'], + 'task_name': self.mock_onDemand['task_name'], + 'scan_paths': self.mock_onDemand['scan_paths'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'never' + } + + def get_demand_mock_object(self, kind=None): + scanner_obj = onDemand_module() + scanner_obj.asup_log_for_cserver = Mock(return_value=None) + if kind is None: + scanner_obj.server = MockONTAPConnection() + else: + scanner_obj.server = MockONTAPConnection(kind='task', data=self.mock_onDemand) + return scanner_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + onDemand_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_demand_task(self): + set_module_args(self.mock_args()) + result = self.get_demand_mock_object().get_demand_task() + assert not result + + def test_get_existing_demand_task(self): + set_module_args(self.mock_args()) + result = self.get_demand_mock_object('task').get_demand_task() + assert result + + def test_successfully_create(self): + set_module_args(self.mock_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_demand_mock_object().apply() + assert exc.value.args[0]['changed'] + + def test_create_idempotency(self): + set_module_args(self.mock_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_demand_mock_object('task').apply() + assert not exc.value.args[0]['changed'] + + def test_successfully_delete(self): + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_demand_mock_object('task').apply() + assert exc.value.args[0]['changed'] + + def test_delete_idempotency(self): + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_demand_mock_object().apply() + assert not exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_demand_task_rest.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_demand_task_rest.py new file mode 100644 index 000000000..0630bdff7 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_on_demand_task_rest.py @@ -0,0 +1,184 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import get_mock_record, \ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vscan_on_demand_task \ + import NetAppOntapVscanOnDemandTask as my_module, main as my_main # module under test + +# needed for get and modify/delete as they still use ZAPI +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + 'on_demand_task': (200, {"records": [ + { + "log_path": "/vol0/report_dir", + "scan_paths": [ + "/vol1/", + "/vol2/cifs/" + ], + "name": "task-1", + "svm": { + "name": "svm1", + "uuid": "02c9e252-41be-11e9-81d5-00a0986138f7" + }, + "scope": { + "exclude_paths": [ + "/vol1/cold-files/", + "/vol1/cifs/names" + ], + "scan_without_extension": True, + "include_extensions": [ + "vmdk", + "mp*" + ], + "exclude_extensions": [ + "mp3", + "mp4" + ], + "max_file_size": "10737418240" + }, + "schedule": { + "name": "weekly", + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412" + } + } + ]}, None), + 'svm_info': (200, { + "uuid": "1cd8a442-86d1-11e0-ae1c-123478563412", + "name": "svm1", + }, None), +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'svm1', + 'use_rest': 'always', + 'task_name': 'carchi8pytask', + 'scan_paths': ['/vol/vol1/'], + 'report_directory': '/', +} + + +def test_get_svm_uuid(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['svm_info']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + assert my_obj.get_svm_uuid() is None + + +def test_get_svm_uuid_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['generic_error']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + msg = 'Error fetching svm uuid: calling: svm/svms: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_obj.get_svm_uuid, 'fail')['msg'] + + +def test_get_vscan_on_demand_task_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies', SRR['empty_records']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + my_obj.svm_uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + assert my_obj.get_demand_task_rest() is None + + +def test_get_vscan_on_demand_task_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies', SRR['generic_error']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + my_obj.svm_uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + msg = 'Error fetching on demand task carchi8pytask: calling: protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_obj.get_demand_task_rest, 'fail')['msg'] + + +def test_create_vscan_on_demand_task(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['svm_info']), + ('GET', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies', SRR['empty_records']), + ('POST', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies', SRR['empty_good']) + ]) + assert create_and_apply(my_module, DEFAULT_ARGS, {})['changed'] + + +def test_create_vscan_on_demand_task_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('POST', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies', SRR['generic_error']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + my_obj.svm_uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + msg = 'Error creating on demand task carchi8pytask: calling: protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_obj.create_demand_task_rest, 'fail')['msg'] + + +def test_create_vscan_on_demand_task_with_all_options(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['svm_info']), + ('GET', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies', SRR['empty_records']), + ('POST', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies', SRR['empty_good']) + ]) + module_args = {'file_ext_to_exclude': ['mp3', 'mp4'], + 'file_ext_to_include': ['vmdk', 'mp*'], + 'max_file_size': '10737418240', + 'paths_to_exclude': ['/vol1/cold-files/', '/vol1/cifs/names'], + 'schedule': 'weekly'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_vscan_on_demand_task(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('GET', 'svm/svms', SRR['svm_info']), + ('GET', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies', SRR['on_demand_task']), + ('DELETE', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies/carchi8pytask', SRR['empty_good']) + ]) + module_args = {'state': 'absent'} + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_delete_vscan_on_demand_task_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest']), + ('DELETE', 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies/carchi8pytask', SRR['generic_error']) + ]) + set_module_args(DEFAULT_ARGS) + my_obj = my_module() + my_obj.svm_uuid = '1cd8a442-86d1-11e0-ae1c-123478563412' + msg = 'Error deleting on demand task carchi8pytask: calling: ' + \ + 'protocols/vscan/1cd8a442-86d1-11e0-ae1c-123478563412/on-demand-policies/carchi8pytask: got Expected error.' + assert msg in expect_and_capture_ansible_exception(my_obj.delete_demand_task_rest, 'fail')['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_scanner_pool.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_scanner_pool.py new file mode 100644 index 000000000..b80e01e82 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vscan_scanner_pool.py @@ -0,0 +1,154 @@ +# (c) 2018, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_vscan_scanner_pool ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import Mock +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vscan_scanner_pool \ + import NetAppOntapVscanScannerPool as scanner_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +class MockONTAPConnection(object): + ''' mock server connection to ONTAP host ''' + + def __init__(self, kind=None, data=None): + ''' save arguments ''' + self.kind = kind + self.params = data + self.xml_in = None + self.xml_out = None + + def invoke_successfully(self, xml, enable_tunneling): # pylint: disable=unused-argument + ''' mock invoke_successfully returning xml data ''' + self.xml_in = xml + if self.kind == 'scanner': + xml = self.build_scanner_pool_info(self.params) + self.xml_out = xml + return xml + + @staticmethod + def build_scanner_pool_info(sanner_details): + xml = netapp_utils.zapi.NaElement('xml') + attributes = { + 'num-records': 1, + 'attributes-list': { + 'vscan-scanner-pool-info': { + 'scanner-pool': sanner_details['scanner_pool'], + 'scanner-policy': sanner_details['scanner_policy'], + 'hostnames': [ + {'hostname': sanner_details['hostnames'][0]}, + {'hostname': sanner_details['hostnames'][1]} + ], + 'privileged-users': [ + {"privileged-user": sanner_details['privileged_users'][0]}, + {"privileged-user": sanner_details['privileged_users'][1]} + ] + } + } + } + xml.translate_struct(attributes) + return xml + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_job_schedule ''' + + def setUp(self): + self.mock_scanner = { + 'state': 'present', + 'scanner_pool': 'test_pool', + 'vserver': 'test_vserver', + 'hostnames': ['host1', 'host2'], + 'privileged_users': ['domain\\admin', 'domain\\carchi8py'], + 'scanner_policy': 'primary' + } + + def mock_args(self): + return { + 'state': self.mock_scanner['state'], + 'scanner_pool': self.mock_scanner['scanner_pool'], + 'vserver': self.mock_scanner['vserver'], + 'hostnames': self.mock_scanner['hostnames'], + 'privileged_users': self.mock_scanner['privileged_users'], + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'scanner_policy': self.mock_scanner['scanner_policy'] + } + + def get_scanner_mock_object(self, kind=None): + scanner_obj = scanner_module() + scanner_obj.asup_log_for_cserver = Mock(return_value=None) + if kind is None: + scanner_obj.server = MockONTAPConnection() + else: + scanner_obj.server = MockONTAPConnection(kind='scanner', data=self.mock_scanner) + return scanner_obj + + def test_module_fail_when_required_args_missing(self): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + scanner_module() + print('Info: %s' % exc.value.args[0]['msg']) + + def test_get_nonexistent_scanner(self): + ''' Test if get_scanner_pool returns None for non-existent job ''' + set_module_args(self.mock_args()) + result = self.get_scanner_mock_object().get_scanner_pool() + assert not result + + def test_get_existing_scanner(self): + ''' Test if get_scanner_pool returns None for non-existent job ''' + set_module_args(self.mock_args()) + result = self.get_scanner_mock_object('scanner').get_scanner_pool() + assert result + + def test_successfully_create(self): + set_module_args(self.mock_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_scanner_mock_object().apply() + assert exc.value.args[0]['changed'] + + def test_create_idempotency(self): + set_module_args(self.mock_args()) + with pytest.raises(AnsibleExitJson) as exc: + self.get_scanner_mock_object('scanner').apply() + assert not exc.value.args[0]['changed'] + + def test_successfully_delete(self): + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_scanner_mock_object('scanner').apply() + assert exc.value.args[0]['changed'] + + def test_delete_idempotency(self): + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_scanner_mock_object().apply() + assert not exc.value.args[0]['changed'] + + def test_successfully_modify(self): + data = self.mock_args() + data['hostnames'] = "host1" + set_module_args(data) + with pytest.raises(AnsibleExitJson) as exc: + self.get_scanner_mock_object('scanner').apply() + assert exc.value.args[0]['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_audit.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_audit.py new file mode 100644 index 000000000..9a4ec6f91 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_audit.py @@ -0,0 +1,354 @@ +# (c) 2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_vserver_audit ''' + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, call_main, create_module, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vserver_audit \ + import NetAppONTAPVserverAudit as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +# REST API canned responses when mocking send_request +SRR = rest_responses({ + # module specific responses + 'audit_record': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "vserver" + }, + "enabled": True, + "events": { + "authorization_policy": True, + "cap_staging": True, + "cifs_logon_logoff": False, + "file_operations": False, + "file_share": True, + "security_group": True, + "user_account": True + }, + "log_path": "/", + "log": { + "format": "xml", + "retention": {"count": 4}, + "rotation": {"size": 1048576} + }, + "guarantee": False + } + ], + "num_records": 1 + }, None + ), + 'audit_record_modified': ( + 200, + { + "records": [ + { + "svm": { + "uuid": "671aa46e-11ad-11ec-a267-005056b30cfa", + "name": "vserver" + }, + "enabled": False, + "events": { + "authorization_policy": True, + "cap_staging": True, + "cifs_logon_logoff": False, + "file_operations": False, + "file_share": True, + "security_group": True, + "user_account": True + }, + "log_path": "/", + "log": { + "format": "xml", + "retention": {"count": 4}, + "rotation": {"size": 1048576} + }, + "guarantee": False + } + ], + "num_records": 1 + }, None + ), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + +ARGS_REST = { + 'hostname': 'test', + 'username': 'test_user', + 'password': 'test_pass!', + 'use_rest': 'always', + 'vserver': 'vserver', +} + + +def test_get_nonexistent_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['empty_records']), + ]) + audit_obj = create_module(my_module, ARGS_REST) + result = audit_obj.get_vserver_audit_configuration_rest() + assert result is None + + +def test_get_existent_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['audit_record']), + ]) + audit_obj = create_module(my_module, ARGS_REST) + result = audit_obj.get_vserver_audit_configuration_rest() + assert result + + +def test_error_get_existent_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['generic_error']), + ]) + error = call_main(my_main, ARGS_REST, fail=True)['msg'] + msg = "Error on fetching vserver audit configuration" + assert msg in error + + +def test_create_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['empty_records']), + ('POST', 'protocols/audit', SRR['empty_good']), + ]) + module_args = { + "enabled": False, + "events": { + "authorization_policy": False, + "cap_staging": False, + "cifs_logon_logoff": True, + "file_operations": True, + "file_share": False, + "security_group": False, + "user_account": False + }, + "log_path": "/", + "log": { + "format": "xml", + "retention": {"count": 4}, + "rotation": {"size": 1048576} + } + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_create_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['empty_records']), + ('POST', 'protocols/audit', SRR['generic_error']), + ]) + module_args = { + "enabled": False, + "events": { + "authorization_policy": False, + "cap_staging": False, + "cifs_logon_logoff": True, + "file_operations": True, + "file_share": False, + "security_group": False, + "user_account": False + }, + "log_path": "/", + "log": { + "format": "xml", + "retention": {"count": 4}, + "rotation": {"size": 1048576} + }, + "guarantee": False + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on creating vserver audit configuration" + assert msg in error + + +def test_modify_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['audit_record']), + ('PATCH', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + "enabled": True, + "events": { + "authorization_policy": True, + "cap_staging": True, + "cifs_logon_logoff": False, + "file_operations": False, + "file_share": True, + "security_group": True, + "user_account": True + }, + "log_path": "/tmp", + "log": { + "format": "evtx", + "retention": {"count": 5}, + "rotation": {"size": 10485760} + } + + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_enable_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['audit_record']), + ('PATCH', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + "enabled": False + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_modify_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['audit_record']), + ('PATCH', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']), + ]) + module_args = { + "enabled": True, + "events": { + "authorization_policy": True, + "cap_staging": True, + "cifs_logon_logoff": False, + "file_operations": False, + "file_share": True, + "security_group": True, + "user_account": True + }, + "log_path": "/tmp", + "log": { + "format": "evtx", + "retention": {"count": 5}, + "rotation": {"size": 10485760} + } + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on modifying vserver audit configuration" + assert msg in error + + +def test_error_enabling_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['audit_record']), + ('PATCH', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ('PATCH', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + "enabled": False, + "events": { + "authorization_policy": False, + "cap_staging": False, + "cifs_logon_logoff": False, + "file_operations": False, + "file_share": True, + "security_group": True, + "user_account": True + }, + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_error_disabling_events_audit_config_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ]) + module_args = { + "events": { + "authorization_policy": False, + "cap_staging": False, + "cifs_logon_logoff": False, + "file_operations": False, + "file_share": False, + "security_group": False, + "user_account": False + }, + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "At least one event should be enabled" + assert msg in error + + +@patch('time.sleep') +def test_delete_audit_config_rest(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['audit_record']), + ('PATCH', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ('GET', 'protocols/audit', SRR['audit_record_modified']), + ('DELETE', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ]) + module_args = { + "state": "absent" + } + assert call_main(my_main, ARGS_REST, module_args)['changed'] + + +@patch('time.sleep') +def test_error_delete_audit_config_rest(sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['audit_record']), + ('PATCH', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['empty_good']), + ('GET', 'protocols/audit', SRR['audit_record_modified']), + ('DELETE', 'protocols/audit/671aa46e-11ad-11ec-a267-005056b30cfa', SRR['generic_error']), + ]) + module_args = { + "state": "absent" + } + error = call_main(my_main, ARGS_REST, module_args, fail=True)['msg'] + msg = "Error on deleting vserver audit configuration" + assert msg in error + + +def test_create_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['audit_record']), + ]) + module_args = { + 'state': 'present' + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] + + +def test_delete_idempotent_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_10_1']), + ('GET', 'protocols/audit', SRR['empty_records']) + ]) + module_args = { + 'state': 'absent' + } + assert not call_main(my_main, ARGS_REST, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_cifs_security.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_cifs_security.py new file mode 100644 index 000000000..6cb823d40 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_cifs_security.py @@ -0,0 +1,111 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, AnsibleFailJson +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vserver_cifs_security \ + import NetAppONTAPCifsSecurity as cifs_security_module # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +cifs_security_info = { + 'num-records': 1, + 'attributes-list': { + 'cifs-security': { + 'is_aes_encryption_enabled': False, + 'lm_compatibility_level': 'krb', + 'kerberos_clock_skew': 20 + } + } +} + +ZRR = zapi_responses({ + 'cifs_security_info': build_zapi_response(cifs_security_info) +}) + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'vserver', + 'use_rest': 'never', + 'is_aes_encryption_enabled': False, + 'lm_compatibility_level': 'krb', + 'kerberos_clock_skew': 20 +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + cifs_security_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_get(): + register_responses([ + ('cifs-security-get-iter', ZRR['cifs_security_info']) + ]) + cifs_obj = create_module(cifs_security_module, DEFAULT_ARGS) + result = cifs_obj.cifs_security_get_iter() + assert result + + +def test_modify_int_option(): + register_responses([ + ('cifs-security-get-iter', ZRR['cifs_security_info']), + ('cifs-security-modify', ZRR['success']), + ]) + module_args = { + 'kerberos_clock_skew': 15 + } + assert create_and_apply(cifs_security_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_modify_bool_option(): + register_responses([ + ('cifs-security-get-iter', ZRR['cifs_security_info']), + ('cifs-security-modify', ZRR['success']), + ]) + module_args = { + 'is_aes_encryption_enabled': True + } + assert create_and_apply(cifs_security_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify_bool_option(): + register_responses([ + ('cifs-security-get-iter', ZRR['cifs_security_info']), + ('cifs-security-modify', ZRR['error']), + ]) + module_args = { + 'is_aes_encryption_enabled': True + } + error = create_and_apply(cifs_security_module, DEFAULT_ARGS, fail=True)['msg'] + assert 'Error modifying cifs security' in error + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('cifs-security-modify', ZRR['error']) + ]) + module_args = {'use_rest': 'never', 'is_aes_encryption_enabled': True} + current = {} + my_obj = create_module(cifs_security_module, DEFAULT_ARGS, module_args) + + error = expect_and_capture_ansible_exception(my_obj.cifs_security_modify, 'fail', current)['msg'] + assert 'Error modifying cifs security on vserver: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_peer.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_peer.py new file mode 100644 index 000000000..2af8a151f --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_peer.py @@ -0,0 +1,440 @@ +# (c) 2018-2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import call_main, set_module_args,\ + AnsibleFailJson, patch_ansible, create_module, create_and_apply, expect_and_capture_ansible_exception, assert_warning_was_raised, print_warnings +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke,\ + register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vserver_peer \ + import NetAppONTAPVserverPeer as vserver_peer, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +DEFAULT_ARGS = { + 'vserver': 'test', + 'peer_vserver': 'test_peer', + 'peer_cluster': 'test_cluster_peer', + 'local_name_for_peer': 'peer_name', + 'local_name_for_source': 'source_name', + 'applications': ['snapmirror'], + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'feature_flags': {'no_cserver_ems': True}, + 'use_rest': 'never' +} + +vserver_peer_info = { + 'num-records': 1, + 'attributes-list': { + 'vserver-peer-info': { + 'remote-vserver-name': 'test_peer', + 'vserver': 'test', + 'peer-vserver': 'test_peer', + 'peer-state': 'peered' + } + } +} + +cluster_info = { + 'attributes': { + 'cluster-identity-info': {'cluster-name': 'test_cluster_peer'} + } +} + +ZRR = zapi_responses({ + 'vserver_peer_info': build_zapi_response(vserver_peer_info), + 'cluster_info': build_zapi_response(cluster_info) +}) + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_obj = vserver_peer() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_successful_create(): + ''' Test successful create ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['empty']), + ('vserver-peer-create', ZRR['success']), + ('vserver-peer-get-iter', ZRR['vserver_peer_info']), + ('vserver-peer-accept', ZRR['success']) + ]) + args = {'dest_hostname': 'test_destination'} + assert create_and_apply(vserver_peer, DEFAULT_ARGS, args)['changed'] + + +def test_successful_create_new_style(): + ''' Test successful create ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['empty']), + ('vserver-peer-create', ZRR['success']), + ('vserver-peer-get-iter', ZRR['vserver_peer_info']), + ('vserver-peer-accept', ZRR['success']) + ]) + default_args = DEFAULT_ARGS + # test without local name + del default_args['local_name_for_peer'] + del default_args['local_name_for_source'] + args = {'peer_options': {'hostname': 'test_destination'}} + assert create_and_apply(vserver_peer, default_args, args)['changed'] + + +def test_create_idempotency(): + ''' Test create idempotency ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['vserver_peer_info']) + ]) + args = {'peer_options': {'hostname': 'test_destination'}} + assert create_and_apply(vserver_peer, DEFAULT_ARGS, args)['changed'] is False + + +def test_successful_delete(): + ''' Test successful delete peer ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['vserver_peer_info']), + ('vserver-peer-delete', ZRR['success']) + ]) + args = { + 'peer_options': {'hostname': 'test_destination'}, + 'state': 'absent' + } + assert create_and_apply(vserver_peer, DEFAULT_ARGS, args)['changed'] + + +def test_delete_idempotency(): + ''' Test delete idempotency ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['empty']) + ]) + args = {'dest_hostname': 'test_destination', 'state': 'absent'} + assert create_and_apply(vserver_peer, DEFAULT_ARGS, args)['changed'] is False + + +def test_helper_vserver_peer_get_iter(): + ''' Test vserver_peer_get_iter method ''' + args = {'dest_hostname': 'test_destination'} + obj = create_module(vserver_peer, DEFAULT_ARGS, args) + result = obj.vserver_peer_get_iter('source') + print(result.to_string(pretty=True)) + assert result['query'] is not None + assert result['query']['vserver-peer-info'] is not None + info = result['query']['vserver-peer-info'] + assert info['vserver'] == DEFAULT_ARGS['vserver'] + assert info['remote-vserver-name'] == DEFAULT_ARGS['peer_vserver'] + + +def test_dest_hostname_absent(): + my_obj = create_module(vserver_peer, DEFAULT_ARGS) + assert my_obj.parameters['hostname'] == my_obj.parameters['dest_hostname'] + + +def test_get_packet(): + ''' Test vserver_peer_get method ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['vserver_peer_info']) + ]) + args = {'dest_hostname': 'test_destination'} + obj = create_module(vserver_peer, DEFAULT_ARGS, args) + result = obj.vserver_peer_get() + assert 'vserver' in result.keys() + assert 'peer_vserver' in result.keys() + assert 'peer_state' in result.keys() + + +def test_error_on_missing_params_create(): + ''' Test error thrown from vserver_peer_create ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['empty']) + ]) + default_args = DEFAULT_ARGS.copy() + del default_args['applications'] + args = {'dest_hostname': 'test_destination'} + msg = create_and_apply(vserver_peer, default_args, args, fail=True)['msg'] + assert 'applications parameter is missing' in msg + + +def test_get_peer_cluster_called(): + ''' Test get_peer_cluster_name called if peer_cluster is missing ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['empty']), + ('cluster-identity-get', ZRR['cluster_info']), + ('vserver-peer-create', ZRR['success']), + ('vserver-peer-get-iter', ZRR['vserver_peer_info']), + ('vserver-peer-accept', ZRR['success']) + ]) + default_args = DEFAULT_ARGS.copy() + del default_args['peer_cluster'] + args = {'dest_hostname': 'test_destination'} + assert create_and_apply(vserver_peer, default_args, args)['changed'] + + +def test_get_peer_cluster_packet(): + ''' Test get_peer_cluster_name xml packet ''' + register_responses([ + ('cluster-identity-get', ZRR['cluster_info']) + ]) + args = {'dest_hostname': 'test_destination'} + obj = create_module(vserver_peer, DEFAULT_ARGS, args) + result = obj.get_peer_cluster_name() + assert result == DEFAULT_ARGS['peer_cluster'] + + +def test_error_on_first_ZAPI_call(): + ''' Test error thrown from vserver_peer_get ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['error']) + ]) + args = {'dest_hostname': 'test_destination'} + msg = create_and_apply(vserver_peer, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error fetching vserver peer' in msg + + +def test_error_create_new_style(): + ''' Test error in create - peer not visible ''' + register_responses([ + ('vserver-peer-get-iter', ZRR['empty']), + ('vserver-peer-create', ZRR['success']), + ('vserver-peer-get-iter', ZRR['empty']) + ]) + args = {'peer_options': {'hostname': 'test_destination'}} + msg = create_and_apply(vserver_peer, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error retrieving vserver peer information while accepting' in msg + + +def test_if_all_methods_catch_exception(): + register_responses([ + ('vserver-peer-delete', ZRR['error']), + ('cluster-identity-get', ZRR['error']), + ('vserver-peer-create', ZRR['error']) + ]) + args = {'dest_hostname': 'test_destination'} + my_obj = create_module(vserver_peer, DEFAULT_ARGS, args) + + error = expect_and_capture_ansible_exception(my_obj.vserver_peer_delete, 'fail', current={'local_peer_vserver': 'test_peer'})['msg'] + assert 'Error deleting vserver peer test: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.get_peer_cluster_name, 'fail')['msg'] + assert 'Error fetching peer cluster name for peer vserver test_peer: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + error = expect_and_capture_ansible_exception(my_obj.vserver_peer_create, 'fail')['msg'] + assert 'Error creating vserver peer test: NetApp API failed. Reason - 12345:synthetic error for UT purpose' in error + + +def test_error_in_vserver_accept(): + register_responses([ + ('vserver-peer-get-iter', ZRR['empty']), + ('vserver-peer-create', ZRR['success']), + ('vserver-peer-get-iter', ZRR['vserver_peer_info']), + ('vserver-peer-accept', ZRR['error']) + ]) + args = {'dest_hostname': 'test_destination'} + msg = create_and_apply(vserver_peer, DEFAULT_ARGS, args, fail=True)['msg'] + assert 'Error accepting vserver peer test_peer: NetApp API failed. Reason - 12345:synthetic error for UT purpose' == msg + + +DEFAULT_ARGS_REST = { + "hostname": "10.193.177.97", + "username": "admin", + "password": "netapp123", + "https": "yes", + "validate_certs": "no", + "use_rest": "always", + "state": "present", + "dest_hostname": "0.0.0.0", + "vserver": "svmsrc3", + "peer_vserver": "svmdst3", + "applications": ['snapmirror'] +} + + +SRR = rest_responses({ + 'vserver_peer_info': (200, { + "records": [{ + "vserver": "svmsrc1", + "peer_vserver": "svmdst1", + "name": "svmdst1", + "state": "peered", + "local_peer_vserver_uuid": "545d2562-2fca-11ec-8016-005056b3f5d5" + }], + 'num_records': 1 + }, None), + 'cluster_info': (200, {"name": "mohanontap98cluster"}, None), + 'job_info': (200, { + "job": { + "uuid": "d78811c1-aebc-11ec-b4de-005056b30cfa", + "_links": {"self": {"href": "/api/cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa"}} + }}, None), + 'job_not_found': (404, "", {"message": "entry doesn't exist", "code": "4", "target": "uuid"}) +}) + + +def test_ensure_get_server_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['vserver_peer_info']) + ]) + assert create_and_apply(vserver_peer, DEFAULT_ARGS_REST)['changed'] is False + + +def test_ensure_create_server_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['empty_records']), + ('POST', 'svm/peers', SRR['success']), + ('GET', 'svm/peers', SRR['vserver_peer_info']), + ('PATCH', 'svm/peers', SRR['success']) + ]) + assert create_and_apply(vserver_peer, DEFAULT_ARGS_REST, {'peer_cluster': 'peer_cluster'})['changed'] + + +def test_ensure_delete_server_called(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['vserver_peer_info']), + ('DELETE', 'svm/peers', SRR['success']) + ]) + assert create_and_apply(vserver_peer, DEFAULT_ARGS_REST, {'state': 'absent'})['changed'] + + +def test_create_vserver_peer_without_cluster_name_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['empty_records']), + ('GET', 'cluster', SRR['cluster_info']), + ('POST', 'svm/peers', SRR['success']), + ('GET', 'svm/peers', SRR['vserver_peer_info']), + ('PATCH', 'svm/peers', SRR['success']) + ]) + assert create_and_apply(vserver_peer, DEFAULT_ARGS_REST)['changed'] + + +def test_create_vserver_peer_with_local_name_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['empty_records']), + ('GET', 'cluster', SRR['cluster_info']), + ('POST', 'svm/peers', SRR['success']), + ('GET', 'svm/peers', SRR['vserver_peer_info']), + ('PATCH', 'svm/peers', SRR['success']) + ]) + args = { + 'local_name_for_peer': 'peer', + 'local_name_for_source': 'source' + } + assert create_and_apply(vserver_peer, DEFAULT_ARGS_REST, args)['changed'] + + +def test_error_in_vserver_accept_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['empty_records']), + ('GET', 'cluster', SRR['cluster_info']), + ('POST', 'svm/peers', SRR['success']), + ('GET', 'svm/peers', SRR['vserver_peer_info']), + ('PATCH', 'svm/peers', SRR['generic_error']) + ]) + msg = create_and_apply(vserver_peer, DEFAULT_ARGS_REST, fail=True)['msg'] + assert 'Error accepting vserver peer relationship on svmdst3: calling: svm/peers: got Expected error.' == msg + + +def test_error_in_vserver_get_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['generic_error']) + ]) + msg = create_and_apply(vserver_peer, DEFAULT_ARGS_REST, fail=True)['msg'] + assert 'Error fetching vserver peer svmsrc3: calling: svm/peers: got Expected error.' == msg + + +def test_error_in_vserver_delete_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['vserver_peer_info']), + ('DELETE', 'svm/peers', SRR['generic_error']) + ]) + msg = create_and_apply(vserver_peer, DEFAULT_ARGS_REST, {'state': 'absent'}, fail=True)['msg'] + assert 'Error deleting vserver peer relationship on svmsrc3: calling: svm/peers: got Expected error.' == msg + + +def test_error_in_peer_cluster_get_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['empty_records']), + ('GET', 'cluster', SRR['generic_error']) + ]) + msg = create_and_apply(vserver_peer, DEFAULT_ARGS_REST, fail=True)['msg'] + assert 'Error fetching peer cluster name for peer vserver svmdst3: calling: cluster: got Expected error.' == msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_missing_netapp_lib(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + msg = 'Error: the python NetApp-Lib module is required. Import error: None' + assert msg == create_module(vserver_peer, DEFAULT_ARGS, fail=True)['msg'] + + +@patch('time.sleep') +def test_job_error_in_vserver_delete_rest(dont_sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['vserver_peer_info']), + ('DELETE', 'svm/peers', SRR['job_info']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']) + ]) + assert create_and_apply(vserver_peer, DEFAULT_ARGS_REST, {'state': 'absent'})['changed'] + print_warnings() + assert_warning_was_raised('Ignoring job status, assuming success - Issue #45.') + + +@patch('time.sleep') +def test_job_error_in_vserver_create_rest(dont_sleep): + register_responses([ + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'cluster', SRR['is_rest_9_9_0']), + ('GET', 'svm/peers', SRR['empty_records']), + ('GET', 'cluster', SRR['empty_records']), + ('POST', 'svm/peers', SRR['job_info']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'cluster/jobs/d78811c1-aebc-11ec-b4de-005056b30cfa', SRR['job_not_found']), + ('GET', 'svm/peers', SRR['empty_records']), + ]) + assert call_main(my_main, DEFAULT_ARGS_REST, fail=True)['msg'] == 'Error reading vserver peer information on peer svmdst3' + print_warnings() + assert_warning_was_raised('Ignoring job status, assuming success - Issue #45.') diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_peer_permissions.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_peer_permissions.py new file mode 100644 index 000000000..b9d5af9b4 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_vserver_peer_permissions.py @@ -0,0 +1,226 @@ +# (c) 2023, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import pytest +import sys + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args, \ + patch_ansible, create_and_apply, create_module, expect_and_capture_ansible_exception, call_main +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import patch_request_and_invoke, register_responses, get_mock_record +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_vserver_peer_permissions \ + import NetAppONTAPVserverPeerPermissions as my_module, main as my_main # module under test + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + +SRR = rest_responses({ + 'peer_record': (200, { + "records": [ + { + "svm": {"name": "ansibleSVM", "uuid": "e3cb5c7fcd20"}, + "cluster_peer": {"name": "test912-2", "uuid": "1e3cb5c7fcd20"}, + "applications": ['snapmirror', 'flexcache'], + }], + "num_records": 1 + }, None), + "no_record": ( + 200, + {"num_records": 0}, + None) +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + 'applications': ['snapmirror'], +} + + +def test_error_validate_vserver_name_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + module_args = { + 'vserver': '*', + 'cluster_peer': 'test912-2' + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'As svm name * represents all svms and created by default, please provide a specific SVM name' + assert msg in error + + +def test_error_validate_vserver_apps_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']) + ]) + module_args = { + 'state': 'present', + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + 'applications': [''] + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Applications field cannot be empty, at least one application must be specified' + assert msg in error + + +def test_get_vserver_peer_permission_rest_none(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['empty_records']) + ]) + module_args = { + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + result = my_obj.get_vserver_peer_permission_rest() + assert result is None + + +def test_get_vserver_peer_permission_rest_error(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['generic_error']) + ]) + module_args = { + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + } + my_module_object = create_module(my_module, DEFAULT_ARGS, module_args) + msg = 'Error on fetching vserver peer permissions' + assert msg in expect_and_capture_ansible_exception(my_module_object.get_vserver_peer_permission_rest, 'fail')['msg'] + + +def test_create_vserver_peer_permission_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['empty_records']), + ('POST', 'svm/peer-permissions', SRR['empty_good']) + ]) + module_args = { + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + 'applications': ['snapmirror'] + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_vserver_peer_permission_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['empty_records']), + ('POST', 'svm/peer-permissions', SRR['generic_error']) + ]) + module_args = { + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + 'applications': ['snapmirror'] + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error on creating vserver peer permissions' + assert msg in error + + +def test_modify_vserver_peer_permission_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['peer_record']), + ('PATCH', 'svm/peer-permissions/1e3cb5c7fcd20/e3cb5c7fcd20', SRR['empty_good']) + ]) + module_args = { + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + 'applications': ['snapmirror'] + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_modify_vserver_peer_permission_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['peer_record']), + ('PATCH', 'svm/peer-permissions/1e3cb5c7fcd20/e3cb5c7fcd20', SRR['generic_error']) + ]) + module_args = { + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + 'applications': ['snapmirror'] + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error on modifying vserver peer permissions' + assert msg in error + + +def test_delete_vserver_peer_permission_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['peer_record']), + ('DELETE', 'svm/peer-permissions/1e3cb5c7fcd20/e3cb5c7fcd20', SRR['empty_good']) + ]) + module_args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_error_delete_vserver_peer_permission_rest(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['peer_record']), + ('DELETE', 'svm/peer-permissions/1e3cb5c7fcd20/e3cb5c7fcd20', SRR['generic_error']) + ]) + module_args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2' + } + error = call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + print('Info: %s' % error) + msg = 'Error on deleting vserver peer permissions' + assert msg in error + + +def test_successfully_vserver_peer_permission_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['peer_record']), + ]) + module_args = { + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + 'applications': ['snapmirror', 'flexcache'] + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_successfully_delete_vserver_peer_permission_rest_idempotency(): + register_responses([ + ('GET', 'cluster', SRR['is_rest_96']), + ('GET', 'svm/peer-permissions', SRR['empty_records']), + ]) + module_args = { + 'state': 'absent', + 'vserver': 'ansibleSVM', + 'cluster_peer': 'test912-2', + 'applications': ['snapmirror', 'flexcache'] + } + assert not call_main(my_main, DEFAULT_ARGS, module_args)['changed'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_wait_for_condition.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_wait_for_condition.py new file mode 100644 index 000000000..b851a9842 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_wait_for_condition.py @@ -0,0 +1,485 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test template for ONTAP Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + call_main, create_module, expect_and_capture_ansible_exception, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.rest_factory import rest_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_wait_for_condition \ + import NetAppONTAPWFC as my_module, main as my_main + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') + + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def sp_image_update_progress_info(in_progress=True): + return { + 'attributes': { + 'service-processor-image-update-progress-info': { + 'is-in-progress': 'true' if in_progress else 'false', + } + } + } + + +def sp_info(version): + return { + 'attributes': { + 'service-processor--info': { + 'firmware-version': version, + } + } + } + + +ZRR = zapi_responses({ + 'sp_info_3_09': build_zapi_response(sp_info('3.09'), 1), + 'sp_info_3_10': build_zapi_response(sp_info('3.10'), 1), + 'sp_image_update_progress_info_in_progress': build_zapi_response(sp_image_update_progress_info(True), 1), + 'sp_image_update_progress_info_idle': build_zapi_response(sp_image_update_progress_info(False), 1), +}) + + +SRR = rest_responses({ + 'one_record_home_node': (200, {'records': [ + {'name': 'node2_abc_if', + 'uuid': '54321', + 'enabled': True, + 'location': {'home_port': {'name': 'e0c'}, 'home_node': {'name': 'node2'}, 'node': {'name': 'node2'}, 'port': {'name': 'e0c'}} + }]}, None), + 'one_record_vserver': (200, {'records': [{ + 'name': 'abc_if', + 'uuid': '54321', + 'svm': {'name': 'vserver', 'uuid': 'svm_uuid'}, + 'data_protocol': ['nfs'], + 'enabled': True, + 'ip': {'address': '10.11.12.13', 'netmask': '255.192.0.0'}, + 'location': { + 'home_port': {'name': 'e0c'}, + 'home_node': {'name': 'node2'}, + 'node': {'name': 'node2'}, + 'port': {'name': 'e0c'}, + 'auto_revert': True, + 'failover': True + }, + 'service_policy': {'name': 'data-mgmt'} + }]}, None), + 'two_records': (200, {'records': [{'name': 'node2_abc_if'}, {'name': 'node2_abc_if'}]}, None), + 'error_precluster': (500, None, {'message': 'are available in precluster.'}), + 'cluster_identity': (200, {'location': 'Oz', 'name': 'abc'}, None), + 'node_309_online': (200, {'records': [ + {'service_processor': {'firmware_version': '3.09', 'state': 'online'}} + ]}, None), + 'node_309_updating': (200, {'records': [ + {'service_processor': {'firmware_version': '3.09', 'state': 'updating'}} + ]}, None), + 'node_310_online': (200, {'records': [ + {'service_processor': {'firmware_version': '3.10', 'state': 'online'}} + ]}, None), + 'snapmirror_relationship': (200, {'records': [ + {'state': 'snapmirrored'} + ]}, None), +}, False) + +DEFAULT_ARGS = { + 'hostname': '10.10.10.10', + 'username': 'admin', + 'password': 'password', + 'attributes': { + 'node': 'node1', + 'expected_version': '3.10' + } +} + + +def test_module_fail_when_required_args_missing(): + ''' required arguments are reported as errors ''' + module_args = { + 'use_rest': 'never' + } + error = create_module(my_module, module_args, fail=True)['msg'] + assert 'missing required arguments:' in error + assert 'name' in error + assert 'conditions' in error + + +@patch('time.sleep') +def test_rest_successful_wait_for_sp_upgrade(dont_sleep): + ''' Test successful sp_upgrade check ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_updating']), + ]) + module_args = { + 'use_rest': 'always', + 'name': 'sp_upgrade', + 'conditions': 'is_in_progress', + } + results = call_main(my_main, DEFAULT_ARGS, module_args) + assert results['msg'] == 'matched condition: is_in_progress' + assert results['states'] == 'online*2,updating' + assert results['last_state'] == 'updating' + + +@patch('time.sleep') +def test_rest_successful_wait_for_snapmirror_relationship(dont_sleep): + ''' Test successful snapmirror_relationship check ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'snapmirror/relationships', SRR['snapmirror_relationship']), + ]) + module_args = { + 'use_rest': 'always', + 'name': 'snapmirror_relationship', + 'conditions': 'transfer_state', + 'attributes': { + 'destination_path': 'path', + 'expected_transfer_state': 'idle' + } + } + results = call_main(my_main, DEFAULT_ARGS, module_args) + assert results['msg'] == 'matched condition: transfer_state' + # these are generated from dictionaries keys, and sequence is not guaranteed with python 3.5 + assert results['states'] in ['snapmirrored,idle', 'idle'] + assert results['last_state'] == 'idle' + + +@patch('time.sleep') +def test_rest_successful_wait_for_sp_version(dont_sleep): + ''' Test successful sp_version check ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_updating']), + ('GET', 'cluster/nodes', SRR['generic_error']), + ('GET', 'cluster/nodes', SRR['zero_records']), + ('GET', 'cluster/nodes', SRR['node_309_updating']), + ('GET', 'cluster/nodes', SRR['node_310_online']), + ]) + module_args = { + 'use_rest': 'always', + 'name': 'sp_version', + 'conditions': 'firmware_version', + } + results = call_main(my_main, DEFAULT_ARGS, module_args) + assert results['msg'] == 'matched condition: firmware_version' + assert results['states'] == '3.09*4,3.10' + assert results['last_state'] == '3.10' + + +@patch('time.sleep') +def test_rest_successful_wait_for_sp_version_not_matched(dont_sleep): + ''' Test successful sp_version check ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_updating']), + ('GET', 'cluster/nodes', SRR['generic_error']), + ('GET', 'cluster/nodes', SRR['zero_records']), + ('GET', 'cluster/nodes', SRR['node_309_updating']), + ('GET', 'cluster/nodes', SRR['node_310_online']), + ]) + module_args = { + 'use_rest': 'always', + 'name': 'sp_version', + 'conditions': ['firmware_version'], + 'state': 'absent', + 'attributes': { + 'node': 'node1', + 'expected_version': '3.09' + } + } + results = call_main(my_main, DEFAULT_ARGS, module_args) + assert results['msg'] == 'conditions not matched' + assert results['states'] == '3.09*4,3.10' + assert results['last_state'] == '3.10' + + +@patch('time.sleep') +def test_rest_negative_wait_for_sp_version_error(dont_sleep): + ''' Test negative sp_version check ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster/nodes', SRR['zero_records']), + ('GET', 'cluster/nodes', SRR['zero_records']), + ('GET', 'cluster/nodes', SRR['zero_records']), + ]) + module_args = { + 'use_rest': 'always', + 'name': 'sp_version', + 'conditions': 'firmware_version', + } + error = 'Error: no record for node:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_rest_negative_wait_for_sp_version_timeout(dont_sleep): + ''' Test negative sp_version check ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ('GET', 'cluster/nodes', SRR['node_309_online']), + ]) + module_args = { + 'use_rest': 'always', + 'name': 'sp_version', + 'conditions': 'firmware_version', + 'timeout': 40, + 'polling_interval': 12, + } + error = 'Error: timeout waiting for condition: firmware_version==3.10.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_zapi_successful_wait_for_sp_upgrade(dont_sleep): + ''' Test successful sp_upgrade check ''' + register_responses([ + ('ZAPI', 'service-processor-image-update-progress-get', ZRR['sp_image_update_progress_info_idle']), + ('ZAPI', 'service-processor-image-update-progress-get', ZRR['sp_image_update_progress_info_idle']), + ('ZAPI', 'service-processor-image-update-progress-get', ZRR['sp_image_update_progress_info_in_progress']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'sp_upgrade', + 'conditions': 'is_in_progress', + } + results = call_main(my_main, DEFAULT_ARGS, module_args) + assert results['msg'] == 'matched condition: is_in_progress' + assert results['states'] == 'false*2,true' + assert results['last_state'] == 'true' + + +@patch('time.sleep') +def test_zapi_successful_wait_for_sp_version(dont_sleep): + ''' Test successful sp_version check ''' + register_responses([ + ('ZAPI', 'service-processor-get', ZRR['sp_info_3_09']), + ('ZAPI', 'service-processor-get', ZRR['error']), + ('ZAPI', 'service-processor-get', ZRR['sp_info_3_09']), + ('ZAPI', 'service-processor-get', ZRR['sp_info_3_10']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'sp_version', + 'conditions': 'firmware_version', + } + results = call_main(my_main, DEFAULT_ARGS, module_args) + assert results['msg'] == 'matched condition: firmware_version' + assert results['states'] == '3.09*2,3.10' + assert results['last_state'] == '3.10' + + +def test_zapi_negative_wait_for_snapmirror_relationship_error(): + ''' Test negative snapmirror_relationship check ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + 'name': 'snapmirror_relationship', + 'conditions': 'state', + 'attributes': { + 'destination_path': 'path', + 'expected_state': 'snapmirrored' + } + } + error = 'Error: event snapmirror_relationship is not supported with ZAPI. It requires REST.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_zapi_negative_wait_for_sp_version_error(dont_sleep): + ''' Test negative sp_version check ''' + register_responses([ + ('ZAPI', 'service-processor-get', ZRR['no_records']), + ('ZAPI', 'service-processor-get', ZRR['no_records']), + ('ZAPI', 'service-processor-get', ZRR['no_records']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'sp_version', + 'conditions': 'firmware_version', + } + error = 'Error: Cannot find element with name: firmware-version in results:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +@patch('time.sleep') +def test_zapi_negative_wait_for_sp_version_timeout(dont_sleep): + ''' Test negative sp_version check ''' + register_responses([ + ('ZAPI', 'service-processor-get', ZRR['sp_info_3_09']), + ('ZAPI', 'service-processor-get', ZRR['error']), + ('ZAPI', 'service-processor-get', ZRR['sp_info_3_09']), + ('ZAPI', 'service-processor-get', ZRR['sp_info_3_09']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'sp_version', + 'conditions': 'firmware_version', + 'timeout': 30, + 'polling_interval': 9, + } + error = 'Error: timeout waiting for condition: firmware_version==3.10.' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_invalid_name(): + ''' Test that name is valid ''' + register_responses([ + ]) + module_args = { + 'use_rest': 'never', + 'name': 'some_name', + 'conditions': 'firmware_version', + } + error = 'value of name must be one of:' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['use_rest'] = 'always' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_validate_resource(): + ''' KeyError on unexpected name ''' + module_args = { + 'use_rest': 'never', + 'name': 'sp_version', + 'conditions': 'firmware_version', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'some_name' in expect_and_capture_ansible_exception(my_obj.validate_resource, KeyError, 'some_name') + module_args['use_rest'] = 'always' + assert 'some_name' in expect_and_capture_ansible_exception(my_obj.validate_resource, KeyError, 'some_name') + + +def test_negative_build_zapi(): + ''' KeyError on unexpected name ''' + module_args = { + 'use_rest': 'never', + 'name': 'sp_version', + 'conditions': 'firmware_version', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'some_name' in expect_and_capture_ansible_exception(my_obj.build_zapi, KeyError, 'some_name') + + +def test_negative_build_rest_api_kwargs(): + ''' KeyError on unexpected name ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'name': 'sp_version', + 'conditions': 'firmware_version', + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + assert 'some_name' in expect_and_capture_ansible_exception(my_obj.build_rest_api_kwargs, KeyError, 'some_name') + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_wait_for_condition.NetAppONTAPWFC.get_record_rest') +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_wait_for_condition.NetAppONTAPWFC.extract_condition') +def test_get_condition_other(mock_extract_condition, mock_get_record_rest): + ''' condition not found, non expected condition ignored ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'always', + 'name': 'sp_version', + 'conditions': 'firmware_version', + 'state': 'absent' + } + my_obj = create_module(my_module, DEFAULT_ARGS, module_args) + condition = 'other_condition' + mock_get_record_rest.return_value = None, None + mock_extract_condition.side_effect = [ + (None, None), + (condition, None), + ] + assert my_obj.get_condition('name', 'dummy') == ('conditions not matched', None) + assert my_obj.get_condition('name', 'dummy') == ('conditions not matched: found other condition: %s' % condition, None) + + +def test_invalid_condition(): + ''' Test that condition is valid ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'sp_upgrade', + 'conditions': [ + 'firmware_version', + 'some_condition' + ] + } + error = 'firmware_version is not valid for resource name: sp_upgrade' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + module_args['use_rest'] = 'always' + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +# def test_invalid_attributes(): +def test_missing_attribute(): + ''' Test that required attributes are present ''' + register_responses([ + ('GET', 'cluster', SRR['is_rest_97']), + ]) + module_args = { + 'use_rest': 'never', + 'name': 'sp_version', + 'conditions': [ + 'firmware_version', + ] + } + args = dict(DEFAULT_ARGS) + del args['attributes'] + error = 'name is sp_version but all of the following are missing: attributes' + assert error in call_main(my_main, args, module_args, fail=True)['msg'] + module_args['use_rest'] = 'always' + assert error in call_main(my_main, args, module_args, fail=True)['msg'] + module_args['use_rest'] = 'never' + args['attributes'] = {'node': 'node1'} + error = 'Error: attributes: expected_version is required for resource name: sp_version' + assert error in call_main(my_main, args, module_args, fail=True)['msg'] + module_args['use_rest'] = 'always' + assert error in call_main(my_main, args, module_args, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_negative_missing_netapp_lib(mock_netapp_lib): + ''' create cluster ''' + module_args = { + 'use_rest': 'never', + 'name': 'sp_version', + 'conditions': 'firmware_version', + } + mock_netapp_lib.return_value = False + error = "the python NetApp-Lib module is required" + assert error in call_main(my_main, DEFAULT_ARGS, module_args, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_wwpn_alias.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_wwpn_alias.py new file mode 100644 index 000000000..1ceece18c --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_wwpn_alias.py @@ -0,0 +1,192 @@ +# (c) 2020, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests for Ansible module: na_ontap_wwpn_alias ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest + +from ansible_collections.netapp.ontap.tests.unit.compat import unittest +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_wwpn_alias \ + import NetAppOntapWwpnAlias as alias_module # module under test + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, {}, None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + # module specific responses + 'get_alias': ( + 200, + {"records": [{ + "svm": { + "uuid": "uuid", + "name": "svm"}, + "alias": "host1", + "wwpn": "01:02:03:04:0a:0b:0c:0d"}], + "num_records": 1}, None), + 'get_svm_uuid': ( + 200, + {"records": [{ + "uuid": "test_uuid" + }]}, None), + "no_record": ( + 200, + {"num_records": 0}, + None) +} + + +class TestMyModule(unittest.TestCase): + ''' Unit tests for na_ontap_wwpn_alias ''' + + def setUp(self): + self.mock_alias = { + 'name': 'host1', + 'vserver': 'test_vserver' + } + + def mock_args(self): + return { + 'vserver': self.mock_alias['vserver'], + 'name': self.mock_alias['name'], + "wwpn": "01:02:03:04:0a:0b:0c:0d", + 'hostname': 'test_host', + 'username': 'test_user', + 'password': 'test_pass!' + } + + def get_alias_mock_object(self): + alias_obj = alias_module() + return alias_obj + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_successful_create(self, mock_request): + '''Test successful rest create''' + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_svm_uuid'], + SRR['no_record'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_create_idempotency(self, mock_request): + '''Test rest create idempotency''' + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_svm_uuid'], + SRR['get_alias'], + SRR['no_record'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert not exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_create_error(self, mock_request): + '''Test rest create error''' + data = self.mock_args() + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_svm_uuid'], + SRR['no_record'], + SRR['generic_error'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['msg'] == "Error on creating wwpn alias: Expected error." + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_modify(self, mock_request): + '''Test rest modify error''' + data = self.mock_args() + data['wwpn'] = "01:02:03:04:0a:0b:0c:0e" + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_svm_uuid'], + SRR['get_alias'], + SRR['empty_good'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleExitJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['changed'] + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_modify_error_delete(self, mock_request): + '''Test rest modify error''' + data = self.mock_args() + data['wwpn'] = "01:02:03:04:0a:0b:0c:0e" + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_svm_uuid'], + SRR['get_alias'], + SRR['generic_error'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['msg'] == "Error on modifying wwpn alias when trying to delete alias: Expected error." + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_modify_error_create(self, mock_request): + '''Test rest modify error''' + data = self.mock_args() + data['wwpn'] = "01:02:03:04:0a:0b:0c:0e" + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_svm_uuid'], + SRR['get_alias'], + SRR['empty_good'], + SRR['generic_error'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['msg'] == "Error on modifying wwpn alias when trying to re-create alias: Expected error." + + @patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') + def test_rest_delete_error(self, mock_request): + '''Test rest delete error''' + data = self.mock_args() + data['state'] = 'absent' + set_module_args(data) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['get_svm_uuid'], + SRR['get_alias'], + SRR['generic_error'], + SRR['empty_good'], + SRR['end_of_sequence'] + ] + with pytest.raises(AnsibleFailJson) as exc: + self.get_alias_mock_object().apply() + assert exc.value.args[0]['msg'] == "Error on deleting wwpn alias: Expected error." diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_zapit.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_zapit.py new file mode 100644 index 000000000..fa64e1f88 --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_na_ontap_zapit.py @@ -0,0 +1,255 @@ +# (c) 2022, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit tests ONTAP Ansible module: na_ontap_zapit ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import\ + expect_and_capture_ansible_exception, call_main, create_module, create_and_apply, patch_ansible +from ansible_collections.netapp.ontap.tests.unit.framework.mock_rest_and_zapi_requests import\ + patch_request_and_invoke, register_responses +from ansible_collections.netapp.ontap.tests.unit.framework.zapi_factory import build_zapi_response, zapi_responses + +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_zapit \ + import NetAppONTAPZapi as my_module, main as my_main # module under test + +if not netapp_utils.has_netapp_lib(): + pytestmark = pytest.mark.skip('skipping as missing required netapp_lib') + + +def cluster_image_info(): + version = 'Fattire__9.3.0' + return { + 'num-records': 1, + # composite response, attributes-list for cluster-image-get-iter and attributes for cluster-image-get + 'attributes-list': [ + {'cluster-image-info': { + 'node-id': 'node4test', + 'current-version': version}}, + {'cluster-image-info': { + 'node-id': 'node4test', + 'current-version': version}}, + ], + 'attributes': { + 'cluster-image-info': { + 'node-id': 'node4test', + 'current-version': version + }}, + } + + +def build_zapi_error_custom(errno, reason, results='results'): + ''' build an XML response + errno as int + reason as str + ''' + if not netapp_utils.has_netapp_lib(): + return 'build_zapi_error: netapp-lib is missing', 'invalid' + if results != 'results': + return (netapp_utils.zapi.NaElement(results), 'valid') + xml = {} + if errno is not None: + xml['errorno'] = errno + if reason is not None: + xml['reason'] = reason + response = netapp_utils.zapi.NaElement('results') + if xml: + response.translate_struct(xml) + return (response, 'valid') + + +ZRR = zapi_responses({ + 'cluster_image_info': build_zapi_response(cluster_image_info()), + 'error_no_errno': build_zapi_error_custom(None, 'some reason'), + 'error_no_reason': build_zapi_error_custom(18408, None), + 'error_no_results': build_zapi_error_custom(None, None, 'no_results') +}) + + +DEFAULT_ARGS = { + 'hostname': 'hostname', + 'username': 'username', + 'password': 'password', + 'zapi': {'cluster-image-get-iter': None} +} + + +def test_ensure_zapi_called_cluster(): + register_responses([ + + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ]) + module_args = { + "use_rest": "never", + } + assert create_and_apply(my_module, DEFAULT_ARGS, module_args)['changed'] + + +def test_ensure_zapi_called_vserver(): + register_responses([ + ('ZAPI', 'cluster-image-get-iter', ZRR['cluster_image_info']), + ]) + module_args = { + "use_rest": "never", + "vserver": "vserver", + "zapi": {'cluster-image-get-iter': {'attributes': None}} + } + assert call_main(my_main, DEFAULT_ARGS, module_args)['changed'] + + +def test_negative_zapi_called_attributes(): + register_responses([ + ('ZAPI', 'cluster-image-get-iter', ZRR['error']), + ]) + module_args = { + "use_rest": "never", + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + assert exception['msg'] == 'ZAPI failure: check errno and reason.' + assert exception['errno'] == '12345' + assert exception['reason'] == 'synthetic error for UT purpose' + + +def test_negative_zapi_called_element_no_errno(): + register_responses([ + ('ZAPI', 'cluster-image-get-iter', ZRR['error_no_errno']), + ]) + module_args = { + "use_rest": "never", + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + assert exception['msg'] == 'ZAPI failure: check errno and reason.' + assert exception['errno'] == 'ESTATUSFAILED' + assert exception['reason'] == 'some reason' + + +def test_negative_zapi_called_element_no_reason(): + register_responses([ + ('ZAPI', 'cluster-image-get-iter', ZRR['error_no_reason']), + ]) + module_args = { + "use_rest": "never", + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + assert exception['msg'] == 'ZAPI failure: check errno and reason.' + assert exception['errno'] == '18408' + assert exception['reason'] == 'Execution failure with unknown reason.' + + +def test_negative_zapi_unexpected_error(): + register_responses([ + ('ZAPI', 'cluster-image-get-iter', (netapp_utils.zapi.NaApiError(), 'valid')), + ]) + module_args = { + "use_rest": "never", + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + assert exception['msg'] == "Error running zapi cluster-image-get-iter: NetApp API failed. Reason - unknown:unknown" + + +def test_negative_two_zapis(): + register_responses([ + ]) + module_args = { + "use_rest": "never", + "zapi": {"1": 1, "2": 2} + } + exception = create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True) + assert 'A single ZAPI can be called at a time, received: ' in exception['msg'] + + +def test_negative_bad_zapi_type(): + register_responses([ + ]) + module_args = { + "use_rest": "never", + } + obj = create_module(my_module, DEFAULT_ARGS, module_args) + obj.zapi = "1" + error = 'A directory entry is expected, eg: system-get-version: , received: 1' + assert expect_and_capture_ansible_exception(obj.run_zapi, 'fail')['msg'] == error + obj.zapi = [3, 1] + error = 'A directory entry is expected, eg: system-get-version: , received: [3, 1]' + assert expect_and_capture_ansible_exception(obj.run_zapi, 'fail')['msg'] == error + + +# python 2.7 does not have bytes but str +BYTES_MARKER_BEGIN = "b'" if sys.version_info >= (3, 0) else '' +BYTES_MARKER_END = "'" if sys.version_info >= (3, 0) else '' +BYTES_TYPE = 'bytes' if sys.version_info >= (3, 0) else 'str' + + +def test_negative_zapi_called_element_no_results(): + register_responses([ + ('ZAPI', 'cluster-image-get-iter', ZRR['error_no_results']), + ]) + module_args = { + "use_rest": "never", + } + error = "Error running zapi, no results field: %s<no_results/>" % BYTES_MARKER_BEGIN + assert error in create_and_apply(my_module, DEFAULT_ARGS, module_args, fail=True)['msg'] + + +def test_negative_bad_zapi_response_to_string(): + module_args = { + "use_rest": "never", + } + obj = create_module(my_module, DEFAULT_ARGS, module_args) + error = "Error running zapi in to_string: '%s' object has no attribute 'to_string'" % BYTES_TYPE + assert expect_and_capture_ansible_exception(obj.jsonify_and_parse_output, 'fail', b'<bad_xml')['msg'] == error + + +class xml_mock: + def __init__(self, data, action=None): + self.data = data + self.action = action + + def to_string(self): + if self.action == 'raise_on_to_string': + raise ValueError(self.data) + return self.data + + +def test_negative_bad_zapi_response_bad_xml(): + module_args = { + "use_rest": "never", + } + obj = create_module(my_module, DEFAULT_ARGS, module_args) + xml = xml_mock(b'<bad_xml') + error = "Error running zapi in xmltodict: %s<bad_xml%s: unclosed token" % (BYTES_MARKER_BEGIN, BYTES_MARKER_END) + assert error in expect_and_capture_ansible_exception(obj.jsonify_and_parse_output, 'fail', xml)['msg'] + + +def test_negative_bad_zapi_response_bad_json(): + # TODO: need to find some valid XML that cannot be converted in JSON. Is it possible? + module_args = { + "use_rest": "never", + } + obj = create_module(my_module, DEFAULT_ARGS, module_args) + xml = xml_mock(b'<bad_json><elemX-1>elem_value</elemX-1><elem-2>elem_value</elem-2></bad_json>') + error = "Error running zapi, no results field" + assert error in expect_and_capture_ansible_exception(obj.jsonify_and_parse_output, 'fail', xml)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.has_netapp_lib') +def test_fail_netapp_lib_error(mock_has_netapp_lib): + mock_has_netapp_lib.return_value = False + assert 'Error: the python NetApp-Lib module is required. Import error: None' == call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_zapit.HAS_JSON', False) +def test_fail_netapp_lib_error(): + assert 'the python json module is required' == call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] + + +@patch('ansible_collections.netapp.ontap.plugins.modules.na_ontap_zapit.HAS_XMLTODICT', False) +def test_fail_netapp_lib_error(): + assert 'the python xmltodict module is required' == call_main(my_main, DEFAULT_ARGS, fail=True)['msg'] diff --git a/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_ontap_fdspt.py b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_ontap_fdspt.py new file mode 100644 index 000000000..80512768e --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/plugins/modules/test_ontap_fdspt.py @@ -0,0 +1,164 @@ +# (c) 2021, NetApp, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +''' unit test for ONTAP na_ontap_fdspt Ansible module ''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type +import pytest +import sys + +from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch +import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils +# pylint: disable=unused-import +from ansible_collections.netapp.ontap.tests.unit.plugins.module_utils.ansible_mocks import set_module_args,\ + AnsibleFailJson, AnsibleExitJson, patch_ansible + +from ansible_collections.netapp.ontap.plugins.modules.na_ontap_fdspt \ + import NetAppOntapFDSPT as my_module # module under test + + +if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): + pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not available') + + +def default_args(): + args = { + 'name': 'policy1', + 'vserver': 'vserver1', + 'hostname': '10.10.10.10', + 'username': 'username', + 'password': 'password', + 'use_rest': 'always', + 'ntfs_mode': 'ignore', + 'security_type': 'ntfs', + 'path': '/' + } + return args + + +# REST API canned responses when mocking send_request +SRR = { + # common responses + 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), + 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), + 'is_zapi': (400, {}, "Unreachable"), + 'empty_good': (200, {}, None), + 'zero_record': (200, dict(records=[], num_records=0), None), + 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), + 'end_of_sequence': (500, None, "Unexpected call to send_request"), + 'generic_error': (400, None, "Expected error"), + 'policy_task_record': ( + 200, { + 'records': [{ + 'vserver': 'vserver1', + 'policy_name': 'policy1', + 'index_num': 1, + 'path': '/', + 'security_type': 'ntfs', + 'ntfs_mode': 'ignore', + 'access_control': 'file_directory'}], + 'num_records': 1}, + None), +} + + +def test_module_fail_when_required_args_missing(patch_ansible): + ''' required arguments are reported as errors ''' + with pytest.raises(AnsibleFailJson) as exc: + set_module_args({}) + my_module() + print('Info: %s' % exc.value.args[0]['msg']) + + +def test_rest_missing_arguments(patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' test missing arguements ''' + args = dict(default_args()) + del args['hostname'] + set_module_args(args) + with pytest.raises(AnsibleFailJson) as exc: + my_module() + msg = 'missing required arguments: hostname' + assert exc.value.args[0]['msg'] == msg + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_create(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Create security policies''' + args = dict(default_args()) + args['name'] = 'new_policy_task' + print(args) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['zero_record'], + SRR['empty_good'], # create + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_remove(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' remove Security policies ''' + args = dict(default_args()) + args['state'] = 'absent' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['policy_task_record'], + SRR['empty_good'], # delete + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 3 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_modify(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' remove Security policies ''' + args = dict(default_args()) + args['state'] = 'present' + args['name'] = 'policy1' + args['ntfs_mode'] = 'replace' + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['policy_task_record'], + SRR['empty_good'], # delete + SRR['empty_good'], # add + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is True + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 4 + + +@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') +def test_rest_no_action(mock_request, patch_ansible): # pylint: disable=redefined-outer-name,unused-argument + ''' Idempotent test ''' + args = dict(default_args()) + set_module_args(args) + mock_request.side_effect = [ + SRR['is_rest'], + SRR['policy_task_record'], + SRR['end_of_sequence'] + ] + my_obj = my_module() + with pytest.raises(AnsibleExitJson) as exc: + my_obj.apply() + assert exc.value.args[0]['changed'] is False + print(mock_request.mock_calls) + assert len(mock_request.mock_calls) == 2 diff --git a/ansible_collections/netapp/ontap/tests/unit/requirements.txt b/ansible_collections/netapp/ontap/tests/unit/requirements.txt new file mode 100644 index 000000000..290e4346d --- /dev/null +++ b/ansible_collections/netapp/ontap/tests/unit/requirements.txt @@ -0,0 +1,7 @@ +ipaddress ; python_version >= '2.7' +isodate ; python_version >= '2.7' +netapp-lib ; python_version >= '2.7' +requests ; python_version >= '2.7' +six ; python_version >= '2.7' +solidfire-sdk-python ; python_version >= '2.7' +xmltodict ; python_version >= '2.7' |