Adding a cli transport option for the bigip_command module. (#30391)

* Adding a cli transport option for the bigip_command module.

* Fixing keyerror when using other f5 modules. Adding version_added for new option in bigip_command.

* Removing local connection check because the F5 tasks can be delegated to any host that has the libraries for REST.

* Using the network_common load_provider.

* Adding unit test to cover cli transport and updating previous unit test to ensure cli was not called.
pull/30662/merge
James Mighion 7 years ago committed by Tim Rupp
parent 6d16739926
commit 50052b3d70

@ -1293,7 +1293,7 @@ MERGE_MULTIPLE_CLI_TAGS:
version_added: "2.3" version_added: "2.3"
NETWORK_GROUP_MODULES: NETWORK_GROUP_MODULES:
name: Network module families 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' description: 'TODO: write it'
env: [{name: NETWORK_GROUP_MODULES}] env: [{name: NETWORK_GROUP_MODULES}]
ini: ini:

@ -134,6 +134,28 @@ def fq_list_names(partition, list_names):
return map(lambda x: fq_name(partition, x), 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 # New style
from abc import ABCMeta, abstractproperty from abc import ABCMeta, abstractproperty
@ -154,6 +176,9 @@ except ImportError:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems, with_metaclass 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( F5_COMMON_ARGS = dict(
@ -232,6 +257,7 @@ class AnsibleF5Client(object):
self.check_mode = self.module.check_mode self.check_mode = self.module.check_mode
self._connect_params = self._get_connect_params() self._connect_params = self._get_connect_params()
if 'transport' not in self.module.params or self.module.params['transport'] != 'cli':
try: try:
self.api = self._get_mgmt_root( self.api = self._get_mgmt_root(
f5_product_name, **self._connect_params f5_product_name, **self._connect_params

@ -64,6 +64,17 @@ options:
conditional, the interval indicates how to long to wait before conditional, the interval indicates how to long to wait before
trying the command again. trying the command again.
default: 1 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: notes:
- Requires the f5-sdk Python package on the host. This is as easy as pip - Requires the f5-sdk Python package on the host. This is as easy as pip
install f5-sdk. 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 HAS_F5SDK
from ansible.module_utils.f5_utils import F5ModuleError from ansible.module_utils.f5_utils import F5ModuleError
from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError 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.netcli import FailedConditionsError
from ansible.module_utils.six import string_types from ansible.module_utils.six import string_types
from ansible.module_utils.netcli import Conditional from ansible.module_utils.netcli import Conditional
@ -179,6 +191,7 @@ class Parameters(AnsibleF5Parameters):
@property @property
def commands(self): def commands(self):
commands = deque(self._values['commands']) commands = deque(self._values['commands'])
if self._values['transport'] != 'cli':
commands.appendleft( commands.appendleft(
'tmsh modify cli preference pager disabled' 'tmsh modify cli preference pager disabled'
) )
@ -208,9 +221,11 @@ class ModuleManager(object):
def _is_valid_mode(self, cmd): def _is_valid_mode(self, cmd):
valid_configs = [ valid_configs = [
'tmsh list', 'tmsh show', 'list', 'show',
'tmsh modify cli preference pager disabled' '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): if any(cmd.startswith(x) for x in valid_configs):
return True return True
return False return False
@ -241,12 +256,16 @@ class ModuleManager(object):
return return
while retries > 0: while retries > 0:
if self.client.module.params['transport'] == 'cli':
responses = run_commands(self.client.module, self.want.commands)
else:
responses = self.execute_on_device(commands) responses = self.execute_on_device(commands)
for item in list(conditionals): for item in list(conditionals):
if item(responses): if item(responses):
if self.want.match == 'any': if self.want.match == 'any':
return item conditionals = list()
break
conditionals.remove(item) conditionals.remove(item)
if not conditionals: if not conditionals:
@ -280,7 +299,7 @@ class ModuleManager(object):
commands = transform(commands) commands = transform(commands)
for index, item in enumerate(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( warnings.append(
'Using "write" commands is not idempotent. You should use ' 'Using "write" commands is not idempotent. You should use '
'a module that is specifically made for that. If such a ' 'a module that is specifically made for that. If such a '
@ -329,15 +348,17 @@ class ArgumentSpec(object):
interval=dict( interval=dict(
default=1, default=1,
type='int' type='int'
),
transport=dict(
type='str',
default='rest',
choices=['cli', 'rest']
) )
) )
self.f5_product_name = 'bigip' self.f5_product_name = 'bigip'
def main(): def main():
if not HAS_F5SDK:
raise F5ModuleError("The python f5-sdk module is required")
spec = ArgumentSpec() spec = ArgumentSpec()
client = AnsibleF5Client( client = AnsibleF5Client(
@ -346,6 +367,9 @@ def main():
f5_product_name=spec.f5_product_name 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: try:
mm = ModuleManager(client) mm = ModuleManager(client)
results = mm.exec_module() results = mm.exec_module()

@ -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 <http://www.gnu.org/licenses/>.
#
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

@ -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 <http://www.gnu.org/licenses/>.
#
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')

@ -67,6 +67,7 @@ def load_fixture(name):
class TestParameters(unittest.TestCase): class TestParameters(unittest.TestCase):
def test_module_parameters(self): def test_module_parameters(self):
args = dict( args = dict(
commands=[ commands=[
@ -85,6 +86,10 @@ class TestParameters(unittest.TestCase):
class TestManager(unittest.TestCase): class TestManager(unittest.TestCase):
def setUp(self): 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() self.spec = ArgumentSpec()
def test_run_single_command(self, *args): def test_run_single_command(self, *args):
@ -104,9 +109,28 @@ class TestManager(unittest.TestCase):
) )
mm = ModuleManager(client) 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() results = mm.exec_module()
assert results['changed'] is True 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)

Loading…
Cancel
Save