adds new module net_command for network devices (#19468)

* new module net_command for sending a command to a network device
* adds unit test cases for module
* only works with connection=network_cli
This commit is contained in:
Peter Sprygada 2016-12-19 11:21:37 -05:00 committed by GitHub
parent b77ab1a6c9
commit 2a5a2773c8
6 changed files with 244 additions and 4 deletions

View file

@ -373,8 +373,8 @@ COLOR_DIFF_LINES = get_config(p, 'colors', 'diff_lines', 'ANSIBLE_COLOR_DIFF_LI
DIFF_CONTEXT = get_config(p, 'diff', 'context', 'ANSIBLE_DIFF_CONTEXT', 3, value_type='integer') DIFF_CONTEXT = get_config(p, 'diff', 'context', 'ANSIBLE_DIFF_CONTEXT', 3, value_type='integer')
# non-configurable things # non-configurable things
MODULE_REQUIRE_ARGS = ['command', 'win_command', 'shell', 'win_shell', 'raw', 'script'] MODULE_REQUIRE_ARGS = ['command', 'win_command', 'net_command', 'shell', 'win_shell', 'raw', 'script']
MODULE_NO_JSON = ['command', 'win_command', 'shell', 'win_shell', 'raw'] MODULE_NO_JSON = ['command', 'win_command', 'net_command', 'shell', 'win_shell', 'raw']
DEFAULT_BECOME_PASS = None DEFAULT_BECOME_PASS = None
DEFAULT_PASSWORD_CHARS = to_text(ascii_letters + digits + ".,:-_", errors='strict') # characters included in auto-generated passwords DEFAULT_PASSWORD_CHARS = to_text(ascii_letters + digits + ".,:-_", errors='strict') # characters included in auto-generated passwords
DEFAULT_SUDO_PASS = None DEFAULT_SUDO_PASS = None

View file

@ -0,0 +1,131 @@
#!/usr/bin/python
#
# 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': 'core',
'version': '1.0'
}
DOCUMENTATION = """
---
module: net_command
version_added: "2.3"
author: "Peter Sprygada (@privateip)"
short_description: Executes a common on a remote network device
description:
- This module will take the command and execute it on the remote
device in a CLI shell. The command will outout will be returned
via the stdout return key. If an error is detected, the command
will return the error via the stderr key.
options:
free_form:
description:
- A free form command to run on the remote host. There is no
parameter actually named 'free_form'. See the examples .
required: true
notes:
- This module requires setting the Ansible connection type to network_cli
- This module will always set the changed return key to C(True)
"""
EXAMPLES = """
- name: execute show version
net_command: show version
- name: run a series of commmands
net_command: "{{ item }}"
with_items:
- show interfaces
- show ip route
- show version
"""
RETURN = """
rc:
description: The command return code (0 means success)
returned: always
type: int
sample: 0
stdout:
description: The command standard output
returned: always
type: string
sample: "Hostname: ios01\nFQDN: ios01.example.net"
stderr:
description: The command standard error
returned: always
type: string
sample: "shw hostname\r\n% Invalid input\r\nios01>"
stdout_lines:
description: The command standard output split in lines
returned: always
type: list
sample: ["Hostname: ios01", "FQDN: ios01.example.net"]
start:
description: The time the job started
returned: always
type: str
sample: "2016-11-16 10:38:15.126146"
end:
description: The time the job ended
returned: always
type: str
sample: "2016-11-16 10:38:25.595612"
delta:
description: The time elapsed to perform all operations
returned: always
type: str
sample: "0:00:10.469466"
"""
from ansible.module_utils.local import LocalAnsibleModule
def main():
""" main entry point for module execution
"""
argument_spec = dict(
_raw_params=dict()
)
module = LocalAnsibleModule(argument_spec=argument_spec,
supports_check_mode=False)
if str(module.params['_raw_params']).strip() == '':
module.fail_json(rc=256, msg='no command given')
result = {'changed': True}
rc, out, err = module.exec_command(module.params['_raw_params'])
try:
out = module.from_json(out)
except ValueError:
if out:
out = str(out).strip()
result['stdout_lines'] = out.split('\n')
result.update({
'rc': rc,
'stdout': out,
'stderr': str(err).strip()
})
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -31,6 +31,7 @@ from ansible.template import Templar
RAW_PARAM_MODULES = ([ RAW_PARAM_MODULES = ([
'command', 'command',
'win_command', 'win_command',
'net_command',
'shell', 'shell',
'win_shell', 'win_shell',
'script', 'script',
@ -164,7 +165,7 @@ class ModuleArgsParser:
# only internal variables can start with an underscore, so # only internal variables can start with an underscore, so
# we don't allow users to set them directy in arguments # we don't allow users to set them directy in arguments
if args and action not in ('command', 'win_command', 'shell', 'win_shell', 'script', 'raw'): if args and action not in ('command', 'net_command', 'win_command', 'shell', 'win_shell', 'script', 'raw'):
for arg in args: for arg in args:
arg = to_text(arg) arg = to_text(arg)
if arg.startswith('_ansible_'): if arg.startswith('_ansible_'):
@ -195,7 +196,7 @@ class ModuleArgsParser:
args = thing args = thing
elif isinstance(thing, string_types): elif isinstance(thing, string_types):
# form is like: local_action: copy src=a dest=b ... pretty common # form is like: local_action: copy src=a dest=b ... pretty common
check_raw = action in ('command', 'win_command', 'shell', 'win_shell', 'script', 'raw') check_raw = action in ('command', 'net_command', 'win_command', 'shell', 'win_shell', 'script', 'raw')
args = parse_kv(thing, check_raw=check_raw) args = parse_kv(thing, check_raw=check_raw)
elif thing is None: elif thing is None:
# this can happen with modules which take no params, like ping: # this can happen with modules which take no params, like ping:

View file

View file

@ -0,0 +1,108 @@
#!/usr/bin/env python
#
# (c) 2016 Red Hat Inc.
#
# 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
import os
import json
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch, MagicMock
from ansible.errors import AnsibleModuleExit
from ansible.modules.network.basics import net_command
from ansible.module_utils import basic
from ansible.module_utils.local import LocalAnsibleModule
from ansible.module_utils._text import to_bytes
def set_module_args(args):
args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
basic._ANSIBLE_ARGS = to_bytes(args)
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
def load_fixture(name):
path = os.path.join(fixture_path, name)
if path in fixture_data:
return fixture_data[path]
with open(path) as f:
data = f.read()
try:
data = json.loads(data)
except:
pass
fixture_data[path] = data
return data
class TestNetCommandModule(unittest.TestCase):
def execute_module(self, command_response=None, failed=False, changed=True):
if not command_response:
command_response = (256, '', 'no command response provided in test case')
with patch.object(LocalAnsibleModule, 'exec_command') as mock_exec_command:
mock_exec_command.return_value = command_response
with self.assertRaises(AnsibleModuleExit) as exc:
net_command.main()
result = exc.exception.result
if failed:
self.assertTrue(result.get('failed'), result)
else:
self.assertEqual(result.get('changed'), changed, result)
return result
def test_net_command_string(self):
"""
Test for all keys in the response
"""
set_module_args({'_raw_params': 'show version'})
result = self.execute_module((0, 'ok', ''))
for key in ['rc', 'stdout', 'stderr', 'stdout_lines']:
self.assertIn(key, result)
def test_net_command_json(self):
"""
The stdout_lines key should not be present when the return
string is a json data structure
"""
set_module_args({'_raw_params': 'show version'})
result = self.execute_module((0, '{"key": "value"}', ''))
for key in ['rc', 'stdout', 'stderr']:
self.assertIn(key, result)
self.assertNotIn('stdout_lines', result)
def test_net_command_missing_command(self):
"""
Test failure on missing command
"""
set_module_args({'_raw_params': ''})
self.execute_module(failed=True)