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
This commit is contained in:
Barry Peddycord III 2017-02-20 22:21:34 -05:00 committed by Peter Sprygada
parent 1bdffbd7ea
commit d1efc8e19e
3 changed files with 424 additions and 0 deletions

View file

@ -0,0 +1,198 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2016-2017, Cumulus Networks <ce-ceng@cumulusnetworks.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/>.
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()

View file

@ -0,0 +1,226 @@
# -*- coding: utf-8 -*-
# (c) 2016, Cumulus Networks <ce-ceng@cumulusnetworks.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/>.
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)