mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-08-23 06:21:43 -07: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"
|
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,12 +257,13 @@ 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()
|
||||||
|
|
||||||
try:
|
if 'transport' not in self.module.params or self.module.params['transport'] != 'cli':
|
||||||
self.api = self._get_mgmt_root(
|
try:
|
||||||
f5_product_name, **self._connect_params
|
self.api = self._get_mgmt_root(
|
||||||
)
|
f5_product_name, **self._connect_params
|
||||||
except iControlUnexpectedHTTPError as exc:
|
)
|
||||||
self.fail(str(exc))
|
except iControlUnexpectedHTTPError as exc:
|
||||||
|
self.fail(str(exc))
|
||||||
|
|
||||||
def fail(self, msg):
|
def fail(self, msg):
|
||||||
self.module.fail_json(msg=msg)
|
self.module.fail_json(msg=msg)
|
||||||
|
|
|
@ -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,10 +191,11 @@ class Parameters(AnsibleF5Parameters):
|
||||||
@property
|
@property
|
||||||
def commands(self):
|
def commands(self):
|
||||||
commands = deque(self._values['commands'])
|
commands = deque(self._values['commands'])
|
||||||
commands.appendleft(
|
if self._values['transport'] != 'cli':
|
||||||
'tmsh modify cli preference pager disabled'
|
commands.appendleft(
|
||||||
)
|
'tmsh modify cli preference pager disabled'
|
||||||
commands = map(self._ensure_tmsh_prefix, list(commands))
|
)
|
||||||
|
commands = map(self._ensure_tmsh_prefix, list(commands))
|
||||||
return list(commands)
|
return list(commands)
|
||||||
|
|
||||||
def _ensure_tmsh_prefix(self, cmd):
|
def _ensure_tmsh_prefix(self, cmd):
|
||||||
|
@ -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:
|
||||||
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):
|
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()
|
||||||
|
|
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):
|
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…
Add table
Add a link
Reference in a new issue