mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
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.
This commit is contained in:
parent
6d16739926
commit
50052b3d70
6 changed files with 225 additions and 22 deletions
|
@ -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:
|
||||
|
|
|
@ -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,6 +257,7 @@ class AnsibleF5Client(object):
|
|||
self.check_mode = self.module.check_mode
|
||||
self._connect_params = self._get_connect_params()
|
||||
|
||||
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
|
||||
|
|
|
@ -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,6 +191,7 @@ class Parameters(AnsibleF5Parameters):
|
|||
@property
|
||||
def commands(self):
|
||||
commands = deque(self._values['commands'])
|
||||
if self._values['transport'] != 'cli':
|
||||
commands.appendleft(
|
||||
'tmsh modify cli preference pager disabled'
|
||||
)
|
||||
|
@ -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:
|
||||
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()
|
||||
|
|
77
lib/ansible/plugins/action/bigip.py
Normal file
77
lib/ansible/plugins/action/bigip.py
Normal file
|
@ -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
|
52
lib/ansible/plugins/terminal/bigip.py
Normal file
52
lib/ansible/plugins/terminal/bigip.py
Normal file
|
@ -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):
|
||||
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue