diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml
index 9e6ad2de23..0ace566641 100644
--- a/lib/ansible/config/base.yml
+++ b/lib/ansible/config/base.yml
@@ -1293,7 +1293,7 @@ MERGE_MULTIPLE_CLI_TAGS:
version_added: "2.3"
NETWORK_GROUP_MODULES:
name: Network module families
- default: [eos, nxos, ios, iosxr, junos, enos, ce, vyos, sros, dellos9, dellos10, dellos6, asa, aruba, aireos]
+ default: [eos, nxos, ios, iosxr, junos, enos, ce, vyos, sros, dellos9, dellos10, dellos6, asa, aruba, aireos, bigip]
description: 'TODO: write it'
env: [{name: NETWORK_GROUP_MODULES}]
ini:
diff --git a/lib/ansible/module_utils/f5_utils.py b/lib/ansible/module_utils/f5_utils.py
index 7b7aeaeba8..4f3f0d1bac 100644
--- a/lib/ansible/module_utils/f5_utils.py
+++ b/lib/ansible/module_utils/f5_utils.py
@@ -134,6 +134,28 @@ def fq_list_names(partition, list_names):
return map(lambda x: fq_name(partition, x), list_names)
+def to_commands(module, commands):
+ spec = {
+ 'command': dict(key=True),
+ 'prompt': dict(),
+ 'answer': dict()
+ }
+ transform = ComplexList(spec, module)
+ return transform(commands)
+
+
+def run_commands(module, commands, check_rc=True):
+ responses = list()
+ commands = to_commands(module, to_list(commands))
+ for cmd in commands:
+ cmd = module.jsonify(cmd)
+ rc, out, err = exec_command(module, cmd)
+ if check_rc and rc != 0:
+ module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), rc=rc)
+ responses.append(to_text(out, errors='surrogate_then_replace'))
+ return responses
+
+
# New style
from abc import ABCMeta, abstractproperty
@@ -154,6 +176,9 @@ except ImportError:
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems, with_metaclass
+from ansible.module_utils.network_common import to_list, ComplexList
+from ansible.module_utils.connection import exec_command
+from ansible.module_utils._text import to_text
F5_COMMON_ARGS = dict(
@@ -232,12 +257,13 @@ class AnsibleF5Client(object):
self.check_mode = self.module.check_mode
self._connect_params = self._get_connect_params()
- try:
- self.api = self._get_mgmt_root(
- f5_product_name, **self._connect_params
- )
- except iControlUnexpectedHTTPError as exc:
- self.fail(str(exc))
+ if 'transport' not in self.module.params or self.module.params['transport'] != 'cli':
+ try:
+ self.api = self._get_mgmt_root(
+ f5_product_name, **self._connect_params
+ )
+ except iControlUnexpectedHTTPError as exc:
+ self.fail(str(exc))
def fail(self, msg):
self.module.fail_json(msg=msg)
diff --git a/lib/ansible/modules/network/f5/bigip_command.py b/lib/ansible/modules/network/f5/bigip_command.py
index b24362de09..a601f4ba61 100644
--- a/lib/ansible/modules/network/f5/bigip_command.py
+++ b/lib/ansible/modules/network/f5/bigip_command.py
@@ -64,6 +64,17 @@ options:
conditional, the interval indicates how to long to wait before
trying the command again.
default: 1
+ transport:
+ description:
+ - Configures the transport connection to use when connecting to the
+ remote device. The transport argument supports connectivity to the
+ device over cli (ssh) or rest.
+ required: true
+ choices:
+ - rest
+ - cli
+ default: rest
+ version_added: "2.5"
notes:
- Requires the f5-sdk Python package on the host. This is as easy as pip
install f5-sdk.
@@ -158,6 +169,7 @@ from ansible.module_utils.f5_utils import AnsibleF5Parameters
from ansible.module_utils.f5_utils import HAS_F5SDK
from ansible.module_utils.f5_utils import F5ModuleError
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError
+from ansible.module_utils.f5_utils import run_commands
from ansible.module_utils.netcli import FailedConditionsError
from ansible.module_utils.six import string_types
from ansible.module_utils.netcli import Conditional
@@ -179,10 +191,11 @@ class Parameters(AnsibleF5Parameters):
@property
def commands(self):
commands = deque(self._values['commands'])
- commands.appendleft(
- 'tmsh modify cli preference pager disabled'
- )
- commands = map(self._ensure_tmsh_prefix, list(commands))
+ if self._values['transport'] != 'cli':
+ commands.appendleft(
+ 'tmsh modify cli preference pager disabled'
+ )
+ commands = map(self._ensure_tmsh_prefix, list(commands))
return list(commands)
def _ensure_tmsh_prefix(self, cmd):
@@ -208,9 +221,11 @@ class ModuleManager(object):
def _is_valid_mode(self, cmd):
valid_configs = [
- 'tmsh list', 'tmsh show',
- 'tmsh modify cli preference pager disabled'
+ 'list', 'show',
+ 'modify cli preference pager disabled'
]
+ if self.client.module.params['transport'] != 'cli':
+ valid_configs = list(map(self.want._ensure_tmsh_prefix, valid_configs))
if any(cmd.startswith(x) for x in valid_configs):
return True
return False
@@ -241,12 +256,16 @@ class ModuleManager(object):
return
while retries > 0:
- responses = self.execute_on_device(commands)
+ if self.client.module.params['transport'] == 'cli':
+ responses = run_commands(self.client.module, self.want.commands)
+ else:
+ responses = self.execute_on_device(commands)
for item in list(conditionals):
if item(responses):
if self.want.match == 'any':
- return item
+ conditionals = list()
+ break
conditionals.remove(item)
if not conditionals:
@@ -280,7 +299,7 @@ class ModuleManager(object):
commands = transform(commands)
for index, item in enumerate(commands):
- if not self._is_valid_mode(item['command']):
+ if not self._is_valid_mode(item['command']) and self.client.module.params['transport'] != 'cli':
warnings.append(
'Using "write" commands is not idempotent. You should use '
'a module that is specifically made for that. If such a '
@@ -329,15 +348,17 @@ class ArgumentSpec(object):
interval=dict(
default=1,
type='int'
+ ),
+ transport=dict(
+ type='str',
+ default='rest',
+ choices=['cli', 'rest']
)
)
self.f5_product_name = 'bigip'
def main():
- if not HAS_F5SDK:
- raise F5ModuleError("The python f5-sdk module is required")
-
spec = ArgumentSpec()
client = AnsibleF5Client(
@@ -346,6 +367,9 @@ def main():
f5_product_name=spec.f5_product_name
)
+ if client.module.params['transport'] != 'cli' and not HAS_F5SDK:
+ raise F5ModuleError("The python f5-sdk module is required to use the rest api")
+
try:
mm = ModuleManager(client)
results = mm.exec_module()
diff --git a/lib/ansible/plugins/action/bigip.py b/lib/ansible/plugins/action/bigip.py
new file mode 100644
index 0000000000..0933751f22
--- /dev/null
+++ b/lib/ansible/plugins/action/bigip.py
@@ -0,0 +1,77 @@
+#
+# (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 .
+#
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import copy
+
+from ansible import constants as C
+from ansible.module_utils.f5_utils import F5_COMMON_ARGS
+from ansible.module_utils.network_common import load_provider
+from ansible.plugins.action.normal import ActionModule as _ActionModule
+
+try:
+ from __main__ import display
+except ImportError:
+ from ansible.utils.display import Display
+ display = Display()
+
+
+class ActionModule(_ActionModule):
+
+ def run(self, tmp=None, task_vars=None):
+ transport = self._task.args.get('transport', 'rest')
+
+ display.vvvv('connection transport is %s' % transport, self._play_context.remote_addr)
+
+ if transport == 'cli':
+ provider = load_provider(F5_COMMON_ARGS, self._task.args)
+ self._task.args.pop('provider', None)
+ pc = copy.deepcopy(self._play_context)
+ pc.connection = 'network_cli'
+ pc.network_os = 'bigip'
+ pc.remote_addr = provider.get('server', self._play_context.remote_addr)
+ pc.port = int(provider['server_port'] or self._play_context.port or 22)
+ pc.remote_user = provider.get('user', self._play_context.connection_user)
+ pc.password = provider.get('password', self._play_context.password)
+ pc.timeout = int(provider.get('timeout', C.PERSISTENT_COMMAND_TIMEOUT))
+
+ display.vvv('using connection plugin %s' % pc.connection, pc.remote_addr)
+ connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin)
+
+ socket_path = connection.run()
+ display.vvvv('socket_path: %s' % socket_path, pc.remote_addr)
+ if not socket_path:
+ return {'failed': True,
+ 'msg': 'unable to open shell. Please see: ' +
+ 'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'}
+
+ # make sure we are in the right cli context which should be
+ # enable mode and not config mode
+ rc, out, err = connection.exec_command('prompt()')
+ while '(config' in str(out):
+ display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr)
+ connection.exec_command('exit')
+ rc, out, err = connection.exec_command('prompt()')
+
+ task_vars['ansible_socket'] = socket_path
+
+ result = super(ActionModule, self).run(tmp, task_vars)
+ return result
diff --git a/lib/ansible/plugins/terminal/bigip.py b/lib/ansible/plugins/terminal/bigip.py
new file mode 100644
index 0000000000..0da27fda41
--- /dev/null
+++ b/lib/ansible/plugins/terminal/bigip.py
@@ -0,0 +1,52 @@
+#
+# (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 .
+#
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+
+from ansible.plugins.terminal import TerminalBase
+from ansible.errors import AnsibleConnectionFailure
+
+
+class TerminalModule(TerminalBase):
+
+ terminal_stdout_re = [
+ re.compile(br"[\r\n]?(?:\([^\)]+\)){,5}(?:>|#) ?$"),
+ re.compile(br"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:>|#) ?$"),
+ re.compile(br"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$")
+ ]
+
+ terminal_stderr_re = [
+ re.compile(br"% ?Error"),
+ re.compile(br"% User not present"),
+ re.compile(br"% ?Bad secret"),
+ re.compile(br"invalid input", re.I),
+ re.compile(br"(?:incomplete|ambiguous) command", re.I),
+ re.compile(br"connection timed out", re.I),
+ re.compile(br"[^\r\n]+ not found", re.I),
+ re.compile(br"'[^']' +returned error code: ?\d+"),
+ re.compile(br"[^\r\n]\/bin\/(?:ba)?sh")
+ ]
+
+ def on_open_shell(self):
+ try:
+ self._exec_cli_command(b'modify cli preference display-threshold 0 pager disabled')
+ except AnsibleConnectionFailure:
+ raise AnsibleConnectionFailure('unable to set terminal parameters')
diff --git a/test/units/modules/network/f5/test_bigip_command.py b/test/units/modules/network/f5/test_bigip_command.py
index 778acdc7f2..b3009fda80 100644
--- a/test/units/modules/network/f5/test_bigip_command.py
+++ b/test/units/modules/network/f5/test_bigip_command.py
@@ -67,6 +67,7 @@ def load_fixture(name):
class TestParameters(unittest.TestCase):
+
def test_module_parameters(self):
args = dict(
commands=[
@@ -85,6 +86,10 @@ class TestParameters(unittest.TestCase):
class TestManager(unittest.TestCase):
def setUp(self):
+ self.mock_run_commands = patch('ansible.modules.network.f5.bigip_command.run_commands')
+ self.run_commands = self.mock_run_commands.start()
+ self.mock_execute_on_device = patch('ansible.modules.network.f5.bigip_command.ModuleManager.execute_on_device')
+ self.execute_on_device = self.mock_execute_on_device.start()
self.spec = ArgumentSpec()
def test_run_single_command(self, *args):
@@ -104,9 +109,28 @@ class TestManager(unittest.TestCase):
)
mm = ModuleManager(client)
- # Override methods to force specific logic in the module to happen
- mm.execute_on_device = Mock(return_value='foo')
-
results = mm.exec_module()
assert results['changed'] is True
+ self.assertEqual(self.run_commands.call_count, 0)
+ self.assertEqual(self.execute_on_device.call_count, 1)
+
+ def test_cli_command(self, *args):
+ set_module_args(dict(
+ commands=[
+ "show sys version"
+ ],
+ server='localhost',
+ user='admin',
+ password='password',
+ transport='cli'
+ ))
+ client = AnsibleF5Client(
+ argument_spec=self.spec.argument_spec,
+ supports_check_mode=self.spec.supports_check_mode,
+ f5_product_name=self.spec.f5_product_name
+ )
+ mm = ModuleManager(client)
+ results = mm.exec_module()
+ self.assertEqual(self.run_commands.call_count, 1)
+ self.assertEqual(self.execute_on_device.call_count, 0)