diff --git a/lib/ansible/module_utils/vyos2.py b/lib/ansible/module_utils/vyos2.py
new file mode 100644
index 00000000000..63d772ad028
--- /dev/null
+++ b/lib/ansible/module_utils/vyos2.py
@@ -0,0 +1,88 @@
+# 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.
+#
+# (c) 2016 Red Hat Inc.
+#
+# 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.
+#
+
+_DEVICE_CONFIGS = {}
+
+def get_config(module, target='commands'):
+ cmd = ' '.join(['show configuration', target])
+
+ try:
+ return _DEVICE_CONFIGS[cmd]
+ except KeyError:
+ rc, out, err = module.exec_command(cmd)
+ if rc != 0:
+ module.fail_json(msg='unable to retrieve current config', stderr=err)
+ cfg = str(out).strip()
+ _DEVICE_CONFIGS[cmd] = cfg
+ return cfg
+
+def run_commands(module, commands, check_rc=True):
+ assert isinstance(commands, list), 'commands must be a list'
+ responses = list()
+
+ for cmd in commands:
+ rc, out, err = module.exec_command(cmd)
+ if check_rc and rc != 0:
+ module.fail_json(msg=err, rc=rc)
+ responses.append(out)
+ return responses
+
+def load_config(module, commands, commit=False, comment=None, save=False):
+ assert isinstance(commands, list), 'commands must be a list'
+ commands.insert(0, 'configure')
+
+ for cmd in commands:
+ rc, out, err = module.exec_command(cmd, check_rc=False)
+ if rc != 0:
+ # discard any changes in case of failure
+ module.exec_command('exit discard')
+ module.fail_json(msg='configuration failed')
+
+ diff = None
+ if module._diff:
+ rc, out, err = module.exec_command('compare')
+ if not out.startswith('No changes'):
+ rc, out, err = module.exec_command('show')
+ diff = str(out).strip()
+
+ if commit:
+ cmd = 'commit'
+ if comment:
+ cmd += ' comment "%s"' % comment
+ module.exec_command(cmd)
+
+ if save:
+ module.exec_command(cmd)
+
+ if not commit:
+ module.exec_command('exit discard')
+ else:
+ module.exec_command('exit')
+
+ if diff:
+ return diff
diff --git a/lib/ansible/modules/network/vyos/vyos_command.py b/lib/ansible/modules/network/vyos/vyos_command.py
index 14180e305d7..5133f917f62 100644
--- a/lib/ansible/modules/network/vyos/vyos_command.py
+++ b/lib/ansible/modules/network/vyos/vyos_command.py
@@ -16,15 +16,17 @@
# along with Ansible. If not, see .
#
-ANSIBLE_METADATA = {'status': ['preview'],
- 'supported_by': 'community',
- 'version': '1.0'}
+ANSIBLE_METADATA = {
+ 'status': ['preview'],
+ 'supported_by': 'community',
+ 'version': '1.0',
+}
DOCUMENTATION = """
---
module: vyos_command
version_added: "2.2"
-author: "Peter Sprygada (@privateip)"
+author: "Nathaniel Case (@qalthos)"
short_description: Run one or more commands on VyOS devices
description:
- The command module allows running one or more commands on remote
@@ -36,7 +38,6 @@ description:
use a custom pager that can cause this module to hang. If the
value of the environment variable C(ANSIBLE_VYOS_TERMINAL_LENGTH)
is not set, the default number of 10000 is used.
-extends_documentation_fragment: vyos
options:
commands:
description:
@@ -70,7 +71,7 @@ options:
retries:
description:
- Specifies the number of retries a command should be tried
- before it is considered failed. The command is run on the
+ before it is considered failed. The command is run on the
target device every retry and evaluated against the I(wait_for)
conditionals.
required: false
@@ -78,8 +79,8 @@ options:
interval:
description:
- Configures the interval in seconds to wait between I(retries)
- of the command. If the command does not pass the specified
- conditional, the interval indicates how to long to wait before
+ of the command. If the command does not pass the specified
+ conditions, the interval indicates how long to wait before
trying the command again.
required: false
default: 1
@@ -90,30 +91,22 @@ notes:
"""
EXAMPLES = """
-# Note: examples below use the following provider dict to handle
-# transport and authentication to the node.
-vars:
- cli:
- host: "{{ inventory_hostname }}"
- username: vyos
- password: vyos
- transport: cli
+tasks:
+ - name: show configuration on ethernet devices eth0 and eth1
+ vyos_command:
+ commands:
+ - show interfaces ethernet {{ item }}
+ with_items:
+ - eth0
+ - eth1
-- vyos_command:
- commands:
- - show interfaces ethernet {{ item }}
- provider: "{{ cli }}"
- with_items:
- - eth0
- - eth1
-
-- vyos_command:
- commands:
- - show version
- - show hardware cpu
- wait_for:
- - "result[0] contains 'VyOS 1.1.7'"
- provider: "{{ cli }}"
+ - name: run multiple commands and check if version output contains specific version string
+ vyos_command:
+ commands:
+ - show version
+ - show hardware cpu
+ wait_for:
+ - "result[0] contains 'VyOS 1.1.7'"
"""
RETURN = """
@@ -128,7 +121,7 @@ stdout_lines:
type: list
sample: [['...', '...'], ['...'], ['...']]
failed_conditions:
- description: The conditionals that failed
+ description: The conditionals that have failed
returned: failed
type: list
sample: ['...', '...']
@@ -137,33 +130,61 @@ warnings:
returned: always
type: list
sample: ['...', '...']
+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"
"""
-import ansible.module_utils.vyos
-from ansible.module_utils.basic import get_exception
-from ansible.module_utils.netcli import CommandRunner
-from ansible.module_utils.netcli import AddCommandError, FailedConditionsError
-from ansible.module_utils.network import NetworkModule, NetworkError
+import time
+
+from ansible.module_utils.local import LocalAnsibleModule
+from ansible.module_utils.netcli import Conditional
+from ansible.module_utils.network_common import ComplexList
from ansible.module_utils.six import string_types
+from ansible.module_utils.vyos2 import run_commands
VALID_KEYS = ['command', 'output', 'prompt', 'response']
+
def to_lines(stdout):
for item in stdout:
if isinstance(item, string_types):
item = str(item).split('\n')
yield item
-def parse_commands(module):
- for cmd in module.params['commands']:
- if isinstance(cmd, string_types):
- cmd = dict(command=cmd, output=None)
- elif 'command' not in cmd:
- module.fail_json(msg='command keyword argument is required')
- elif cmd.get('output') not in [None, 'text']:
- module.fail_json(msg='invalid output specified for command')
- elif not set(cmd.keys()).issubset(VALID_KEYS):
- module.fail_json(msg='unknown keyword specified')
- yield cmd
+
+def parse_commands(module, warnings):
+ command = ComplexList(dict(
+ command=dict(key=True),
+ prompt=dict(),
+ response=dict(),
+ ))
+ commands = command(module.params['commands'])
+
+ for index, cmd in enumerate(commands):
+ if module.check_mode and not cmd['command'].startswith('show'):
+ warnings.append('only show commands are supported when using '
+ 'check mode, not executing `%s`' % cmd['command'])
+ else:
+ if cmd['command'].startswith('conf'):
+ module.fail_json(msg='vyos_command does not support running '
+ 'config mode commands. Please use '
+ 'vyos_config instead')
+ commands[index] = module.jsonify(cmd)
+
+ return commands
+
def main():
spec = dict(
@@ -177,63 +198,48 @@ def main():
interval=dict(default=1, type='int')
)
- module = NetworkModule(argument_spec=spec,
- connect_on_load=False,
- supports_check_mode=True)
+ module = LocalAnsibleModule(argument_spec=spec, supports_check_mode=True)
- commands = list(parse_commands(module))
- conditionals = module.params['wait_for'] or list()
warnings = list()
+ commands = parse_commands(module, warnings)
- runner = CommandRunner(module)
+ wait_for = module.params['wait_for'] or list()
+ conditionals = [Conditional(c) for c in wait_for]
- for cmd in commands:
- if module.check_mode and not cmd['command'].startswith('show'):
- warnings.append('only show commands are supported when using '
- 'check mode, not executing `%s`' % cmd['command'])
- else:
- if cmd['command'].startswith('conf'):
- module.fail_json(msg='vyos_command does not support running '
- 'config mode commands. Please use '
- 'vyos_config instead')
- try:
- runner.add_command(**cmd)
- except AddCommandError:
- exc = get_exception()
- warnings.append('duplicate command detected: %s' % cmd)
+ retries = module.params['retries']
+ interval = module.params['interval']
+ match = module.params['match']
- for item in conditionals:
- runner.add_conditional(item)
+ for _ in range(retries):
+ responses = run_commands(module, commands)
- runner.retries = module.params['retries']
- runner.interval = module.params['interval']
- runner.match = module.params['match']
+ for item in conditionals:
+ if item(responses):
+ if match == 'any':
+ conditionals = list()
+ break
+ conditionals.remove(item)
- try:
- runner.run()
- except FailedConditionsError:
- exc = get_exception()
- module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions)
- except NetworkError:
- exc = get_exception()
- module.fail_json(msg=str(exc))
+ if not conditionals:
+ break
- result = dict(changed=False, stdout=list())
+ time.sleep(interval)
- for cmd in commands:
- try:
- output = runner.get_command(cmd['command'])
- except ValueError:
- output = 'command not executed due to check_mode, see warnings'
- result['stdout'].append(output)
+ if conditionals:
+ failed_conditions = [item.raw for item in conditionals]
+ msg = 'One or more conditional statements have not been satisfied'
+ module.fail_json(msg=msg, falied_conditions=failed_conditions)
- result['warnings'] = warnings
- result['stdout_lines'] = list(to_lines(result['stdout']))
+ result = {
+ 'changed': False,
+ 'stdout': responses,
+ 'warnings': warnings,
+ 'stdout_lines': list(to_lines(responses)),
+ }
module.exit_json(**result)
if __name__ == '__main__':
main()
-
diff --git a/lib/ansible/plugins/terminal/vyos.py b/lib/ansible/plugins/terminal/vyos.py
index 309f06a7ac1..91c707471c3 100644
--- a/lib/ansible/plugins/terminal/vyos.py
+++ b/lib/ansible/plugins/terminal/vyos.py
@@ -19,6 +19,7 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
+import os
import re
from ansible.plugins.terminal import TerminalBase
@@ -38,9 +39,12 @@ class TerminalModule(TerminalBase):
re.compile(r"\n\s+Set failed"),
]
+ terminal_length = os.getenv('ANSIBLE_VYOS_TERMINAL_LENGTH', 10000)
+
def on_open_shell(self):
try:
self._exec_cli_command('set terminal length 0')
+ self._exec_cli_command('set terminal length %s' % self.terminal_length)
except AnsibleConnectionFailure:
raise AnsibleConnectionFailure('unable to set terminal parameters')
diff --git a/test/units/modules/network/vyos/fixtures/show_version b/test/units/modules/network/vyos/fixtures/show_version
new file mode 100644
index 00000000000..a015d554eb5
--- /dev/null
+++ b/test/units/modules/network/vyos/fixtures/show_version
@@ -0,0 +1,14 @@
+Version: VyOS 1.1.7
+Description: VyOS 1.1.7 (helium)
+Copyright: 2016 VyOS maintainers and contributors
+Built by: maintainers@vyos.net
+Built on: Wed Feb 17 09:57:31 UTC 2016
+Build ID: 1602170957-4459750
+System type: x86 64-bit
+Boot via: image
+Hypervisor: VMware
+HW model: VMware Virtual Platform
+HW S/N: VMware-42 3c 26 25 44 c5 0a 91-cf 2c 97 2b fe 9b 25 be
+HW UUID: 423C2625-44C5-0A91-CF2C-972BFE9B25BE
+Uptime: 01:08:20 up 52 days, 2:13, 1 user, load average: 0.00, 0.01, 0.05
+
diff --git a/test/units/modules/network/vyos/test_vyos_command.py b/test/units/modules/network/vyos/test_vyos_command.py
new file mode 100644
index 00000000000..0e987d3247a
--- /dev/null
+++ b/test/units/modules/network/vyos/test_vyos_command.py
@@ -0,0 +1,145 @@
+# (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 .
+
+# 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.vyos import vyos_command
+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 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 TestVyosCommandModule(unittest.TestCase):
+
+ def setUp(self):
+ self.mock_run_commands = patch('ansible.modules.network.vyos.vyos_command.run_commands')
+ self.run_commands = self.mock_run_commands.start()
+
+ def tearDown(self):
+ self.mock_run_commands.stop()
+
+ def execute_module(self, failed=False, changed=False):
+
+ def load_from_file(*args, **kwargs):
+ module, commands = args
+ output = list()
+
+ for item in commands:
+ try:
+ obj = json.loads(item)
+ command = obj['command']
+ except ValueError:
+ command = item
+ filename = str(command).replace(' ', '_')
+ output.append(load_fixture(filename))
+ return output
+
+ self.run_commands.side_effect = load_from_file
+
+ with self.assertRaises(AnsibleModuleExit) as exc:
+ vyos_command.main()
+
+ result = exc.exception.result
+
+ if failed:
+ self.assertTrue(result.get('failed'))
+ else:
+ self.assertEqual(result.get('changed'), changed, result)
+
+ return result
+
+ def test_vyos_command_simple(self):
+ set_module_args(dict(commands=['show version']))
+ result = self.execute_module()
+ self.assertEqual(len(result['stdout']), 1)
+ self.assertTrue(result['stdout'][0].startswith('Version: VyOS'))
+
+ def test_vyos_command_multiple(self):
+ set_module_args(dict(commands=['show version', 'show version']))
+ result = self.execute_module()
+ self.assertEqual(len(result['stdout']), 2)
+ self.assertTrue(result['stdout'][0].startswith('Version: VyOS'))
+
+ def test_vyos_command_wait_for(self):
+ wait_for = 'result[0] contains "VyOS maintainers"'
+ set_module_args(dict(commands=['show version'], wait_for=wait_for))
+ self.execute_module()
+
+ def test_vyos_command_wait_for_fails(self):
+ wait_for = 'result[0] contains "test string"'
+ set_module_args(dict(commands=['show version'], wait_for=wait_for))
+ self.execute_module(failed=True)
+ self.assertEqual(self.run_commands.call_count, 10)
+
+ def test_vyos_command_retries(self):
+ wait_for = 'result[0] contains "test string"'
+ set_module_args(dict(commands=['show version'], wait_for=wait_for, retries=2))
+ self.execute_module(failed=True)
+ self.assertEqual(self.run_commands.call_count, 2)
+
+ def test_vyos_command_match_any(self):
+ wait_for = ['result[0] contains "VyOS maintainers"',
+ 'result[0] contains "test string"']
+ set_module_args(dict(commands=['show version'], wait_for=wait_for, match='any'))
+ self.execute_module()
+
+ def test_vyos_command_match_all(self):
+ wait_for = ['result[0] contains "VyOS maintainers"',
+ 'result[0] contains "maintainers@vyos.net"']
+ set_module_args(dict(commands=['show version'], wait_for=wait_for, match='all'))
+ self.execute_module()
+
+ def test_vyos_command_match_all_failure(self):
+ wait_for = ['result[0] contains "VyOS maintainers"',
+ 'result[0] contains "test string"']
+ commands = ['show version', 'show version']
+ set_module_args(dict(commands=commands, wait_for=wait_for, match='all'))
+ self.execute_module(failed=True)