From d1efc8e19e3e1350f5f80da1a8f03e2b5c03206e Mon Sep 17 00:00:00 2001 From: Barry Peddycord III Date: Mon, 20 Feb 2017 22:21:34 -0500 Subject: [PATCH] New Networking Module: NCLU (#21101) * Adding Cumulus NCLU module * Delete incorrect testing folder * wrong import location for nclu test * another wrong import location for nclu test * unittest library doesn't support 'skip', removing 'real' nclu tests * Don't need stringio since I'm not doing real tests * got rid of unnecessary shebang in test_nclu * version set to 1.0 * Documentation fixes --- lib/ansible/modules/network/cumulus/nclu.py | 198 +++++++++++++++ .../units/modules/network/cumulus/__init__.py | 0 .../modules/network/cumulus/test_nclu.py | 226 ++++++++++++++++++ 3 files changed, 424 insertions(+) create mode 100644 lib/ansible/modules/network/cumulus/nclu.py create mode 100644 test/units/modules/network/cumulus/__init__.py create mode 100644 test/units/modules/network/cumulus/test_nclu.py diff --git a/lib/ansible/modules/network/cumulus/nclu.py b/lib/ansible/modules/network/cumulus/nclu.py new file mode 100644 index 0000000000..90310561e9 --- /dev/null +++ b/lib/ansible/modules/network/cumulus/nclu.py @@ -0,0 +1,198 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016-2017, Cumulus Networks +# +# 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 . + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} +DOCUMENTATION = ''' +--- +module: nclu +version_added: "2.3" +author: "Cumulus Networks" +short_description: Configure network interfaces using NCLU +description: + - Interface to the Network Command Line Utility, developed to make it easier + to configure operating systems running ifupdown2 and Quagga, such as + Cumulus Linux. Command documentation is available at + U(https://docs.cumulusnetworks.com/display/DOCS/Network+Command+Line+Utility) +options: + commands: + description: + - A list of strings containing the net commands to run. Mutually + exclusive with I(template). + template: + description: + - A single, multi-line string with jinja2 formatting. This string + will be broken by lines, and each line will be run through net. + Mutually exclusive with I(commands). + commit: + description: + - When true, performs a 'net commit' at the end of the block. + Mutually exclusive with I(atomic). + default: false + abort: + description: + - Boolean. When true, perform a 'net abort' before the block. + This cleans out any uncommitted changes in the buffer. + Mutually exclusive with I(atomic). + default: false + atomic: + description: + - When true, equivalent to both I(commit) and I(abort) being true. + Mutually exclusive with I(commit) and I(atomic). + default: false + description: + description: + - Commit description that will be recorded to the commit log if + I(commit) or I(atomic) are true. + default: "Ansible-originated commit" +''' + +EXAMPLES = ''' + +- name: Add two interfaces without committing any changes + nclu: + commands: + - add int swp1 + - add int swp2 + +- name: Add 48 interfaces and commit the change. + nclu: + template: | + {% for iface in range(1,49) %} + add int swp{{i}} + {% endfor %} + commit: true + description: "Ansible - add swps1-48" + +- name: Atomically add an interface + nclu: + commands: + - add int swp1 + atomic: true + description: "Ansible - add swp1" +''' + +RETURN = ''' +changed: + description: whether the interface was changed + returned: changed + type: bool + sample: True +msg: + description: human-readable report of success or failure + returned: always + type: string + sample: "interface bond0 config updated" +''' + + +def command_helper(module, command, errmsg=None): + """Run a command, catch any nclu errors""" + (_rc, output, _err) = module.run_command("/usr/bin/net %s"%command) + if _rc or 'ERROR' in output or 'ERROR' in _err: + module.fail_json(msg=errmsg or output) + return str(output) + + +def check_pending(module): + """Check the pending diff of the nclu buffer.""" + pending = command_helper(module, "pending", "Error in pending config. You may want to view `net pending` on this target.") + + delimeter1 = "net add/del commands since the last 'net commit'" + color1 = '\x1b[94m' + if delimeter1 in pending: + pending = pending.split(delimeter1)[0] + pending = pending.replace('\x1b[94m', '') + return pending.strip() + + +def run_nclu(module, command_list, command_string, commit, atomic, abort, description): + _changed = False + + commands = [] + if command_list: + commands = command_list + elif command_string: + commands = command_string.splitlines() + + do_commit = False + do_abort = abort + if commit or atomic: + do_commit = True + if atomic: + do_abort = True + + if do_abort: + command_helper(module, "abort") + + # First, look at the staged commands. + before = check_pending(module) + # Run all of the the net commands + output_lines = [] + for line in commands: + output_lines += [command_helper(module, line.strip(), "Failed on line %s"%line)] + output = "\n".join(output_lines) + + # If pending changes changed, report a change. + after = check_pending(module) + if before == after: + _changed = False + else: + _changed = True + + # Do the commit. + if do_commit: + result = command_helper(module, "commit description '%s'"%description) + if "commit ignored" in result: + _changed = False + command_helper(module, "abort") + elif command_helper(module, "show commit last") == "": + _changed = False + + return _changed, output + + +def main(testing=False): + module = AnsibleModule(argument_spec=dict( + commands = dict(required=False, type='list'), + template = dict(required=False, type='str'), + description = dict(required=False, type='str', default="Ansible-originated commit"), + abort = dict(required=False, type='bool', default=False), + commit = dict(required=False, type='bool', default=False), + atomic = dict(required=False, type='bool', default=False)), + mutually_exclusive=[('commands', 'template'), + ('commit', 'atomic'), + ('abort', 'atomic')] + ) + command_list = module.params.get('commands', None) + command_string = module.params.get('template', None) + commit = module.params.get('commit') + atomic = module.params.get('atomic') + abort = module.params.get('abort') + description = module.params.get('description') + + _changed, output = run_nclu(module, command_list, command_string, commit, atomic, abort, description) + if not testing: + module.exit_json(changed=_changed, msg=output) + elif testing: + return {"changed": _changed, "msg": output} + +# import module snippets +from ansible.module_utils.basic import AnsibleModule +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/cumulus/__init__.py b/test/units/modules/network/cumulus/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/units/modules/network/cumulus/test_nclu.py b/test/units/modules/network/cumulus/test_nclu.py new file mode 100644 index 0000000000..3585786c14 --- /dev/null +++ b/test/units/modules/network/cumulus/test_nclu.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- + +# (c) 2016, Cumulus Networks +# +# 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 . + +import unittest +from ansible.modules.network.cumulus import nclu + +import sys +import time +from ansible.module_utils.basic import * + + + +class FakeModule(object): + """Fake NCLU module to check the logic of the ansible module. + + We have two sets of tests: fake and real. Real tests only run if + NCLU is installed on the testing machine (it should be a Cumulus VX + VM or something like that). + + Fake tests are used to test the logic of the ansible module proper - that + the right things are done when certain feedback is received. + + Real tests are used to test regressions against versions of NCLU. This + FakeModule mimics the output that is used for screenscraping. If the real + output differs, the real tests will catch that. + + To prepare a VX: + sudo apt-get update + sudo apt-get install python-setuptools git gcc python-dev libssl-dev + sudo easy_install pip + sudo pip install ansible nose coverage + # git the module and cd to the directory + nosetests --with-coverage --cover-package=nclu --cover-erase --cover-branches + + If a real test fails, it means that there is a risk of a version split, and + that changing the module will break for old versions of NCLU if not careful. + """ + + def __init__(self, **kwargs): + self.reset() + + def exit_json(self, **kwargs): + self.exit_code = kwargs + + def fail_json(self, **kwargs): + self.fail_code = kwargs + + def run_command(self, command): + """Run an NCLU command""" + + self.command_history.append(command) + if command == "/usr/bin/net pending": + return (0, self.pending, "") + elif command == "/usr/bin/net abort": + self.pending = "" + return (0, "", "") + elif command.startswith("/usr/bin/net commit"): + if self.pending: + self.last_commit = self.pending + self.pending = "" + return (0, "", "") + else: + return (0, "commit ignored...there were no pending changes", "") + elif command == "/usr/bin/net show commit last": + return (0, self.last_commit, "") + else: + self.pending += command + return self.mocks.get(command, (0, "", "")) + + def mock_output(self, command, _rc, output, _err): + """Prepare a command to mock certain output""" + + self.mocks[command] = (_rc, output, _err) + + def reset(self): + self.params = {} + self.exit_code = {} + self.fail_code = {} + self.command_history = [] + self.mocks = {} + self.pending = "" + self.last_commit = "" + + +def skipUnlessNcluInstalled(original_function): + if os.path.isfile('/usr/bin/net'): + return original_function + else: + return unittest.skip('only run if nclu is installed') + + +class TestNclu(unittest.TestCase): + + def test_command_helper(self): + module = FakeModule() + module.mock_output("/usr/bin/net add int swp1", 0, "", "") + + result = nclu.command_helper(module, 'add int swp1', 'error out') + self.assertEqual(module.command_history[-1], "/usr/bin/net add int swp1") + self.assertEqual(result, "") + + def test_command_helper_error_code(self): + module = FakeModule() + module.mock_output("/usr/bin/net fake fail command", 1, "", "") + + result = nclu.command_helper(module, 'fake fail command', 'error out') + self.assertEqual(module.fail_code, {'msg': "error out"}) + + def test_command_helper_error_msg(self): + module = FakeModule() + module.mock_output("/usr/bin/net fake fail command", 0, + "ERROR: Command not found", "") + + result = nclu.command_helper(module, 'fake fail command', 'error out') + self.assertEqual(module.fail_code, {'msg': "error out"}) + + def test_command_helper_no_error_msg(self): + module = FakeModule() + module.mock_output("/usr/bin/net fake fail command", 0, + "ERROR: Command not found", "") + + result = nclu.command_helper(module, 'fake fail command') + self.assertEqual(module.fail_code, {'msg': "ERROR: Command not found"}) + + def test_empty_run(self): + module = FakeModule() + changed, output = nclu.run_nclu(module, None, None, False, False, False, "") + self.assertEqual(module.command_history, ['/usr/bin/net pending', + '/usr/bin/net pending']) + self.assertEqual(module.fail_code, {}) + self.assertEqual(changed, False) + + def test_command_list(self): + module = FakeModule() + changed, output = nclu.run_nclu(module, ['add int swp1', 'add int swp2'], + None, False, False, False, "") + + self.assertEqual(module.command_history, ['/usr/bin/net pending', + '/usr/bin/net add int swp1', + '/usr/bin/net add int swp2', + '/usr/bin/net pending']) + self.assertNotEqual(len(module.pending), 0) + self.assertEqual(module.fail_code, {}) + self.assertEqual(changed, True) + + def test_command_list_commit(self): + module = FakeModule() + changed, output = nclu.run_nclu(module, + ['add int swp1', 'add int swp2'], + None, True, False, False, "committed") + + self.assertEqual(module.command_history, ['/usr/bin/net pending', + '/usr/bin/net add int swp1', + '/usr/bin/net add int swp2', + '/usr/bin/net pending', + "/usr/bin/net commit description 'committed'", + '/usr/bin/net show commit last']) + self.assertEqual(len(module.pending), 0) + self.assertEqual(module.fail_code, {}) + self.assertEqual(changed, True) + + + def test_command_atomic(self): + module = FakeModule() + changed, output = nclu.run_nclu(module, + ['add int swp1', 'add int swp2'], + None, False, True, False, "atomically") + + self.assertEqual(module.command_history, ['/usr/bin/net abort', + '/usr/bin/net pending', + '/usr/bin/net add int swp1', + '/usr/bin/net add int swp2', + '/usr/bin/net pending', + "/usr/bin/net commit description 'atomically'", + '/usr/bin/net show commit last']) + self.assertEqual(len(module.pending), 0) + self.assertEqual(module.fail_code, {}) + self.assertEqual(changed, True) + + def test_command_abort_first(self): + module = FakeModule() + module.pending = "dirty" + nclu.run_nclu(module, None, None, False, False, True, "") + + self.assertEqual(len(module.pending), 0) + + def test_command_template_commit(self): + module = FakeModule() + changed, output = nclu.run_nclu(module, None, + " add int swp1\n add int swp2", + True, False, False, "committed") + + self.assertEqual(module.command_history, ['/usr/bin/net pending', + '/usr/bin/net add int swp1', + '/usr/bin/net add int swp2', + '/usr/bin/net pending', + "/usr/bin/net commit description 'committed'", + '/usr/bin/net show commit last']) + self.assertEqual(len(module.pending), 0) + self.assertEqual(module.fail_code, {}) + self.assertEqual(changed, True) + + def test_commit_ignored(self): + module = FakeModule() + changed, output = nclu.run_nclu(module, None, None, True, False, False, "ignore me") + + self.assertEqual(module.command_history, ['/usr/bin/net pending', + '/usr/bin/net pending', + "/usr/bin/net commit description 'ignore me'", + '/usr/bin/net abort']) + self.assertEqual(len(module.pending), 0) + self.assertEqual(module.fail_code, {}) + self.assertEqual(changed, False)