diff --git a/lib/ansible/module_utils/network/nso/nso.py b/lib/ansible/module_utils/network/nso/nso.py index d6eae6533f8..170c4ce19f4 100644 --- a/lib/ansible/module_utils/network/nso/nso.py +++ b/lib/ansible/module_utils/network/nso/nso.py @@ -25,6 +25,13 @@ from ansible.module_utils.urls import open_url import json import re +try: + unicode + HAVE_UNICODE = True +except NameError: + unicode = str + HAVE_UNICODE = False + nso_argument_spec = dict( url=dict(required=True), @@ -484,3 +491,66 @@ def verify_version(client): (version[1] == 4 and (len(version) < 3 or version[2] < 3))): raise ModuleFailException( 'unsupported NSO version {0}, only 4.4.3 or later is supported'.format(version_str)) + + +def normalize_value(expected_value, value, key): + if value is None: + return None + if isinstance(expected_value, bool): + return value == 'true' + if isinstance(expected_value, int): + try: + return int(value) + except TypeError: + raise ModuleFailException( + 'returned value {0} for {1} is not a valid integer'.format( + key, value)) + if isinstance(expected_value, float): + try: + return float(value) + except TypeError: + raise ModuleFailException( + 'returned value {0} for {1} is not a valid float'.format( + key, value)) + if isinstance(expected_value, (list, tuple)): + if not isinstance(value, (list, tuple)): + raise ModuleFailException( + 'returned value {0} for {1} is not a list'.format(value, key)) + if len(expected_value) != len(value): + raise ModuleFailException( + 'list length mismatch for {0}'.format(key)) + + normalized_value = [] + for i in range(len(expected_value)): + normalized_value.append( + normalize_value(expected_value[i], value[i], '{0}[{1}]'.format(key, i))) + return normalized_value + + if isinstance(expected_value, dict): + if not isinstance(value, dict): + raise ModuleFailException( + 'returned value {0} for {1} is not a dict'.format(value, key)) + if len(expected_value) != len(value): + raise ModuleFailException( + 'dict length mismatch for {0}'.format(key)) + + normalized_value = {} + for k in expected_value.keys(): + n_k = normalize_value(k, k, '{0}[{1}]'.format(key, k)) + if n_k not in value: + raise ModuleFailException('missing {0} in value'.format(n_k)) + normalized_value[n_k] = normalize_value(expected_value[k], value[k], '{0}[{1}]'.format(key, k)) + return normalized_value + + if HAVE_UNICODE: + if isinstance(expected_value, unicode) and isinstance(value, str): + return value.decode('utf-8') + if isinstance(expected_value, str) and isinstance(value, unicode): + return value.encode('utf-8') + else: + if hasattr(expected_value, 'encode') and hasattr(value, 'decode'): + return value.decode('utf-8') + if hasattr(expected_value, 'decode') and hasattr(value, 'encode'): + return value.encode('utf-8') + + return value diff --git a/lib/ansible/modules/network/nso/nso_action.py b/lib/ansible/modules/network/nso/nso_action.py new file mode 100644 index 00000000000..93abfccfcd7 --- /dev/null +++ b/lib/ansible/modules/network/nso/nso_action.py @@ -0,0 +1,182 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 . +# + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: nso_action +extends_documentation_fragment: nso +short_description: Executes Cisco NSO actions and verifies output. +description: + - This module provices support for executing Cisco NSO actions and then + verifying that the output is as expected. +author: "Claes Nästén (@cnasten)" +options: + path: + description: Path to NSO action. + required: true + input: + description: > + NSO action parameters. + output_required: + description: > + Required output parameters. + output_invalid: + description: > + List of result parameter names that will cause the task to fail if they + are present. + validate_strict: + description: > + If set to true, the task will fail if any output parameters not in + output_required is present in the output. +version_added: "2.5" +''' + +EXAMPLES = ''' +- name: Sync NSO device + nso_config: + url: http://localhost:8080/jsonrpc + username: username + password: password + path: /ncs:devices/device{ce0}/sync-from + output_required: + result: true +''' + +RETURN = ''' +output: + description: Action output + returned: success + type: dict + sample: + result: true +''' + +from ansible.module_utils.network.nso.nso import connect, verify_version, nso_argument_spec +from ansible.module_utils.network.nso.nso import normalize_value +from ansible.module_utils.network.nso.nso import ModuleFailException, NsoException +from ansible.module_utils.basic import AnsibleModule + + +class NsoAction(object): + def __init__(self, check_mode, client, + path, input, + output_required, output_invalid, validate_strict): + self._check_mode = check_mode + self._client = client + self._path = path + self._input = input + self._output_required = output_required + self._output_invalid = output_invalid + self._validate_strict = validate_strict + + def main(self): + schema = self._client.get_schema(path=self._path) + if schema['data']['kind'] != 'action': + raise ModuleFailException('{0} is not an action'.format(self._path)) + + input_schema = [c for c in schema['data']['children'] + if c.get('is_action_input', False)] + + for key, value in self._input.items(): + child = next((c for c in input_schema if c['name'] == key), None) + if child is None: + raise ModuleFailException('no parameter {0}'.format(key)) + + # implement type validation in the future + + if self._check_mode: + return {} + else: + return self._run_and_verify() + + def _run_and_verify(self): + output = self._client.run_action(None, self._path, self._input) + for key, value in self._output_required.items(): + if key not in output: + raise ModuleFailException('{0} not in result'.format(key)) + + n_value = normalize_value(value, output[key], key) + if value != n_value: + msg = '{0} value mismatch. expected {1} got {2}'.format( + key, value, n_value) + raise ModuleFailException(msg) + + for key in self._output_invalid.keys(): + if key in output: + raise ModuleFailException('{0} not allowed in result'.format(key)) + + if self._validate_strict: + for name in output.keys(): + if name not in self._output_required: + raise ModuleFailException('{0} not allowed in result'.format(name)) + + return output + + +def main(): + argument_spec = dict( + path=dict(required=True), + input=dict(required=False, type='dict', default={}), + output_required=dict(required=False, type='dict', default={}), + output_invalid=dict(required=False, type='dict', default={}), + validate_strict=dict(required=False, type='bool', default=False) + ) + argument_spec.update(nso_argument_spec) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + p = module.params + + client = connect(p) + nso_action = NsoAction( + module.check_mode, client, + p['path'], + p['input'], + p['output_required'], + p['output_invalid'], + p['validate_strict']) + try: + verify_version(client) + + output = nso_action.main() + client.logout() + module.exit_json(changed=True, output=output) + except NsoException as ex: + client.logout() + module.fail_json(msg=ex.message) + except ModuleFailException as ex: + client.logout() + module.fail_json(msg=ex.message) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/utils/module_docs_fragments/nso.py b/lib/ansible/utils/module_docs_fragments/nso.py index c262c133d6b..3ce8d236d6f 100644 --- a/lib/ansible/utils/module_docs_fragments/nso.py +++ b/lib/ansible/utils/module_docs_fragments/nso.py @@ -34,4 +34,6 @@ options: password: description: NSO password required: true +requirements: + - Cisco NSO version 4.4.3 or higher ''' diff --git a/test/units/modules/network/nso/fixtures/complex_schema.json b/test/units/modules/network/nso/fixtures/complex_schema.json new file mode 100644 index 00000000000..49a1176557e --- /dev/null +++ b/test/units/modules/network/nso/fixtures/complex_schema.json @@ -0,0 +1,212 @@ +{ + "meta": { + "prefix": "ansible", + "namespace": "http://example.com/ansible", + "types": { + }, + "keypath": "/ansible:action/complex" + }, + "data": { + "kind": "action", + "mandatory": true, + "name": "complex", + "qname": "ansible:complex", + "access": { + "read": false, + "create": false, + "execute": true, + "update": false, + "delete": false + }, + "children": [ + { + "kind": "leaf", + "is_action_input": true, + "name": "number", + "qname": "ansible:number", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "primitive": true, + "name": "uint8" + } + }, + { + "kind": "container", + "is_action_input": true, + "mandatory": true, + "name": "ansible", + "qname": "ansible:ansible", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "children": [ + { + "kind": "choice", + "cases": [ + { + "kind": "case", + "name": "version", + "children": [ + { + "kind": "leaf", + "is_action_input": true, + "name": "version", + "qname": "ansible:version", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + } + } + ] + }, + { + "kind": "case", + "name": "release", + "children": [ + { + "kind": "container", + "is_action_input": true, + "mandatory": true, + "name": "release", + "qname": "ansible:release", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "children": [ + { + "kind": "leaf", + "is_action_input": true, + "name": "major", + "qname": "ansible:major", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "primitive": true, + "name": "uint8" + } + }, + { + "kind": "leaf", + "is_action_input": true, + "name": "minor", + "qname": "ansible:minor", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "primitive": true, + "name": "uint8" + } + } + ] + } + ] + } + ], + "name": "version-releae-choice" + } + ] + }, + { + "kind": "choice", + "cases": [ + { + "kind": "case", + "name": "version", + "children": [ + { + "kind": "list", + "min_elements": 0, + "name": "version", + "max_elements": "unbounded", + "qname": "ansible:version", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "mandatory": true, + "children": [ + { + "kind": "leaf", + "name": "name", + "qname": "ansible:name", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ], + "is_action_output": true + } + ] + }, + { + "kind": "case", + "name": "release", + "children": [ + { + "kind": "leaf", + "name": "release", + "qname": "ansible:release", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ] + } + ], + "name": "version-release-choice" + } + ] + } +} diff --git a/test/units/modules/network/nso/fixtures/description_schema.json b/test/units/modules/network/nso/fixtures/description_schema.json new file mode 100644 index 00000000000..2680a484add --- /dev/null +++ b/test/units/modules/network/nso/fixtures/description_schema.json @@ -0,0 +1,28 @@ +{ + "meta": { + "prefix": "ncs", + "namespace": "http://tail-f.com/ns/ncs", + "types": { + }, + "keypath": "/ncs:devices/device{ce0}/description" + }, + "data": { + "info": { + "string": "Free form textual description" + }, + "kind": "leaf", + "name": "description", + "qname": "ncs:description", + "access": { + "read": true, + "create": true, + "execute": false, + "update": true, + "delete": true + }, + "type": { + "primitive": true, + "name": "string" + } + } +} diff --git a/test/units/modules/network/nso/fixtures/sync_from_schema.json b/test/units/modules/network/nso/fixtures/sync_from_schema.json new file mode 100644 index 00000000000..dc2206d4992 --- /dev/null +++ b/test/units/modules/network/nso/fixtures/sync_from_schema.json @@ -0,0 +1,178 @@ +{ + "meta": { + "prefix": "ncs", + "namespace": "http://tail-f.com/ns/ncs", + "types": { + "http://tail-f.com/ns/ncs:outformat2": [ + { + "name": "http://tail-f.com/ns/ncs:outformat2", + "enumeration": [ + { + "info": "NCS CLI curly bracket format.", + "label": "cli" + }, + { + "info": "NETCONF XML edit-config format, i.e., the edit-config that\nwould be applied locally (at NCS) to get a config\nthat is equal to that of the managed device.", + "label": "xml" + } + ] + }, + { + "name": "string" + } + ] + }, + "keypath": "/ncs:devices/device{ce0}/sync-from" + }, + "data": { + "info": { + "string": "Synchronize the config by pulling from the device" + }, + "kind": "action", + "mandatory": true, + "name": "sync-from", + "qname": "ncs:sync-from", + "access": { + "read": false, + "create": false, + "execute": true, + "update": false, + "delete": false + }, + "children": [ + { + "kind": "container", + "is_action_input": true, + "name": "dry-run", + "presence": true, + "qname": "ncs:dry-run", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "children": [ + { + "info": { + "string": "Report what would be done towards CDB, without\nactually doing anything." + }, + "kind": "leaf", + "is_action_input": true, + "name": "outformat", + "qname": "ncs:outformat", + "access": { + "read": false, + "create": false, + "execute": false, + "update": true, + "delete": false + }, + "type": { + "namespace": "http://tail-f.com/ns/ncs", + "name": "outformat2" + } + } + ] + }, + { + "kind": "choice", + "cases": [ + { + "kind": "case", + "name": "result", + "children": [ + { + "kind": "leaf", + "name": "result", + "qname": "ncs:result", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "boolean" + }, + "is_action_output": true + } + ] + }, + { + "kind": "case", + "name": "result-xml", + "children": [ + { + "kind": "leaf", + "name": "result-xml", + "is_cli_preformatted": true, + "qname": "ncs:result-xml", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ] + }, + { + "kind": "case", + "name": "cli", + "children": [ + { + "kind": "leaf", + "name": "cli", + "is_cli_preformatted": true, + "qname": "ncs:cli", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ] + } + ], + "name": "outformat" + }, + { + "info": { + "string": "If present, contains additional information about the result." + }, + "kind": "leaf", + "name": "info", + "qname": "ncs:info", + "access": { + "read": false, + "create": false, + "execute": false, + "update": false, + "delete": false + }, + "type": { + "primitive": true, + "name": "string" + }, + "is_action_output": true + } + ] + } +} diff --git a/test/units/modules/network/nso/nso_module.py b/test/units/modules/network/nso/nso_module.py index 43974903103..44ce5d50498 100644 --- a/test/units/modules/network/nso/nso_module.py +++ b/test/units/modules/network/nso/nso_module.py @@ -20,13 +20,21 @@ from __future__ import (absolute_import, division, print_function) import os import json -from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') fixture_data = {} +def set_module_args(args): + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + def load_fixture(name): path = os.path.join(fixture_path, name) if path not in fixture_data: @@ -72,7 +80,15 @@ def mock_call(calls, url, data=None, headers=None, method=None): return result -class TestNsoModule(ModuleTestCase): +class AnsibleExitJson(Exception): + pass + + +class AnsibleFailJson(Exception): + pass + + +class TestNsoModule(unittest.TestCase): def execute_module(self, failed=False, changed=False, **kwargs): if failed: @@ -88,16 +104,27 @@ class TestNsoModule(ModuleTestCase): return result def failed(self): - with self.assertRaises(AnsibleFailJson) as exc: - self.module.main() + def fail_json(*args, **kwargs): + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + with patch.object(basic.AnsibleModule, 'fail_json', fail_json): + with self.assertRaises(AnsibleFailJson) as exc: + self.module.main() result = exc.exception.args[0] self.assertTrue(result['failed'], result) return result def changed(self, changed=False): - with self.assertRaises(AnsibleExitJson) as exc: - self.module.main() + def exit_json(*args, **kwargs): + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + with patch.object(basic.AnsibleModule, 'exit_json', exit_json): + with self.assertRaises(AnsibleExitJson) as exc: + self.module.main() result = exc.exception.args[0] self.assertEqual(result['changed'], changed, result) diff --git a/test/units/modules/network/nso/test_nso_action.py b/test/units/modules/network/nso/test_nso_action.py new file mode 100644 index 00000000000..b890b6bc1ea --- /dev/null +++ b/test/units/modules/network/nso/test_nso_action.py @@ -0,0 +1,159 @@ +# +# Copyright (c) 2017 Cisco and/or its affiliates. +# +# 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 . + +from __future__ import (absolute_import, division, print_function) + +import json + +from ansible.compat.tests.mock import patch +from ansible.modules.network.nso import nso_action +from . import nso_module +from .nso_module import MockResponse + + +class TestNsoAction(nso_module.TestNsoModule): + module = nso_action + + @patch('ansible.module_utils.network.nso.nso.open_url') + def test_nso_action_missing(self, open_url_mock): + action_input = {} + path = '/ncs:devices/device{ce0}/missing' + calls = [ + MockResponse('login', {}, 200, '{}', {'set-cookie': 'id'}), + MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5.0"}'), + MockResponse('new_trans', {'mode': 'read'}, 200, '{"result": {"th": 1}}'), + MockResponse('get_schema', {'path': path}, 200, '{"error": {"data": {"param": "path"}, "type": "rpc.method.invalid_params"}}'), + MockResponse('logout', {}, 200, '{"result": {}}'), + ] + open_url_mock.side_effect = lambda *args, **kwargs: nso_module.mock_call(calls, *args, **kwargs) + + nso_module.set_module_args({ + 'username': 'user', 'password': 'password', + 'url': 'http://localhost:8080/jsonrpc', + 'path': path, + 'input': action_input + }) + self.execute_module(failed=True, msg='NSO get_schema invalid params. path = /ncs:devices/device{ce0}/missing') + + self.assertEqual(0, len(calls)) + + @patch('ansible.module_utils.network.nso.nso.open_url') + def test_nso_action_not_action(self, open_url_mock): + action_input = {} + path = '/ncs:devices/device{ce0}/description' + schema = nso_module.load_fixture('description_schema.json') + calls = [ + MockResponse('login', {}, 200, '{}', {'set-cookie': 'id'}), + MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5.0"}'), + MockResponse('new_trans', {'mode': 'read'}, 200, '{"result": {"th": 1}}'), + MockResponse('get_schema', {'path': path}, 200, '{"result": %s}' % (json.dumps(schema, ))), + MockResponse('logout', {}, 200, '{"result": {}}'), + ] + open_url_mock.side_effect = lambda *args, **kwargs: nso_module.mock_call(calls, *args, **kwargs) + + nso_module.set_module_args({ + 'username': 'user', 'password': 'password', + 'url': 'http://localhost:8080/jsonrpc', + 'path': path, + 'input': action_input + }) + self.execute_module(failed=True, msg='/ncs:devices/device{ce0}/description is not an action') + + self.assertEqual(0, len(calls)) + + @patch('ansible.module_utils.network.nso.nso.open_url') + def test_nso_action_ok(self, open_url_mock): + action_input = {} + path = '/ncs:devices/device{ce0}/sync-from' + output = {"result": True} + schema = nso_module.load_fixture('sync_from_schema.json') + calls = [ + MockResponse('login', {}, 200, '{}', {'set-cookie': 'id'}), + MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5.0"}'), + MockResponse('new_trans', {'mode': 'read'}, 200, '{"result": {"th": 1}}'), + MockResponse('get_schema', {'path': path}, 200, '{"result": %s}' % (json.dumps(schema, ))), + MockResponse('run_action', {'path': path, 'params': action_input}, 200, '{"result": {"result": true}}'), + MockResponse('logout', {}, 200, '{"result": {}}'), + ] + open_url_mock.side_effect = lambda *args, **kwargs: nso_module.mock_call(calls, *args, **kwargs) + + nso_module.set_module_args({ + 'username': 'user', 'password': 'password', + 'url': 'http://localhost:8080/jsonrpc', + 'path': path, + 'input': action_input + }) + self.execute_module(changed=True, output=output) + + self.assertEqual(0, len(calls)) + + @patch('ansible.module_utils.network.nso.nso.open_url') + def test_nso_action_validate_ok(self, open_url_mock): + action_input = {} + path = '/test:action' + output = {'version': [{'name': 'v1'}, {'name': 'v2'}]} + schema = nso_module.load_fixture('complex_schema.json') + calls = [ + MockResponse('login', {}, 200, '{}', {'set-cookie': 'id'}), + MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5.0"}'), + MockResponse('new_trans', {'mode': 'read'}, 200, '{"result": {"th": 1}}'), + MockResponse('get_schema', {'path': path}, 200, '{"result": %s}' % (json.dumps(schema, ))), + MockResponse('run_action', {'path': path, 'params': action_input}, 200, + '{"result": {"version": [{"name": "v1"}, {"name": "v2"}]}}'), + MockResponse('logout', {}, 200, '{"result": {}}'), + ] + open_url_mock.side_effect = lambda *args, **kwargs: nso_module.mock_call(calls, *args, **kwargs) + + nso_module.set_module_args({ + 'username': 'user', 'password': 'password', + 'url': 'http://localhost:8080/jsonrpc', + 'path': path, + 'input': action_input, + 'output_required': output + }) + self.execute_module(changed=True, output=output) + + self.assertEqual(0, len(calls)) + + @patch('ansible.module_utils.network.nso.nso.open_url') + def test_nso_action_validate_failed(self, open_url_mock): + action_input = {} + path = '/test:action' + output_mismatch = {'version': [{'name': 'v1'}, {'name': 'v3'}]} + schema = nso_module.load_fixture('complex_schema.json') + calls = [ + MockResponse('login', {}, 200, '{}', {'set-cookie': 'id'}), + MockResponse('get_system_setting', {'operation': 'version'}, 200, '{"result": "4.5.0"}'), + MockResponse('new_trans', {'mode': 'read'}, 200, '{"result": {"th": 1}}'), + MockResponse('get_schema', {'path': path}, 200, '{"result": %s}' % (json.dumps(schema, ))), + MockResponse('run_action', {'path': path, 'params': action_input}, 200, + '{"result": {"version": [{"name": "v1"}, {"name": "v2"}]}}'), + MockResponse('logout', {}, 200, '{"result": {}}'), + ] + open_url_mock.side_effect = lambda *args, **kwargs: nso_module.mock_call(calls, *args, **kwargs) + + nso_module.set_module_args({ + 'username': 'user', 'password': 'password', + 'url': 'http://localhost:8080/jsonrpc', + 'path': path, + 'input': action_input, + 'output_required': output_mismatch + }) + self.execute_module(failed=True, msg="version value mismatch. expected [{'name': 'v1'}, {'name': 'v3'}] got [{'name': 'v1'}, {'name': 'v2'}]") + + self.assertEqual(0, len(calls)) diff --git a/test/units/modules/network/nso/test_nso_config.py b/test/units/modules/network/nso/test_nso_config.py index 6795769ecd5..e75eea017e5 100644 --- a/test/units/modules/network/nso/test_nso_config.py +++ b/test/units/modules/network/nso/test_nso_config.py @@ -51,8 +51,7 @@ class TestNsoConfig(nso_module.TestNsoModule): 'username': 'user', 'password': 'password', 'url': 'http://localhost:8080/jsonrpc', 'data': data }) - with self.assertRaises(AnsibleFailJson): - self.execute_module(changed=False, changes=[], diffs=[]) + self.execute_module(failed=True) self.assertEqual(0, len(calls))