Modules to manage ICX devices (#58969)

* new module

* new terminal

* new terminal

* new cliconf

* cliconf

* icx cliconf

* icx_cliconf

* icx test units module

* icx units module

* icx banner unit test

* PR changes resolved

* changes resolved

* Changes Resolved

* check_running_config changes resolved

* added notes

* removed icx rst

* new commit

* new changes

* deleted icx rst

* icx .rst

* modified platform_index.rst

* modified platform_index.rst

* changes resolved

* PR comments resolved

* Update platform_index.rst

PR comment resolved
This commit is contained in:
sushma-alethea 2019-07-31 20:01:41 +05:30 committed by Ganesh Nalawade
parent 58a53fe0eb
commit f2cb44633a
13 changed files with 959 additions and 0 deletions

1
.github/BOTMETA.yml vendored
View file

@ -334,6 +334,7 @@ files:
maintainers: $team_iosxr maintainers: $team_iosxr
$modules/network/ironware/: paulquack $modules/network/ironware/: paulquack
$modules/network/junos/: Qalthos ganeshrn $modules/network/junos/: Qalthos ganeshrn
$modules/network/icx/: sushma-alethea
$modules/network/layer2/: $team_networking $modules/network/layer2/: $team_networking
$modules/network/layer3/: $team_networking $modules/network/layer3/: $team_networking
$modules/network/meraki/: &meraki $modules/network/meraki/: &meraki

View file

@ -0,0 +1,69 @@
.. _icx_platform_options:
***************************************
ICX Platform Options
***************************************
ICX supports Enable Mode (Privilege Escalation). This page offers details on how to use Enable Mode on ICX in Ansible.
.. contents:: Topics
Connections Available
================================================================================
+---------------------------+-----------------------------------------------+
|.. | CLI |
+===========================+===============================================+
| **Protocol** | SSH |
+---------------------------+-----------------------------------------------+
| | **Credentials** | | uses SSH keys / SSH-agent if present |
| | | | accepts ``-u myuser -k`` if using password |
+---------------------------+-----------------------------------------------+
| **Indirect Access** | via a bastion (jump host) |
+---------------------------+-----------------------------------------------+
| | **Connection Settings** | | ``ansible_connection: network_cli`` |
| | | | |
| | | | |
+---------------------------+-----------------------------------------------+
| | **Enable Mode** | | supported - use ``ansible_become: yes`` |
| | (Privilege Escalation) | | with ``ansible_become_method: enable`` |
| | | | and ``ansible_become_password:`` |
+---------------------------+-----------------------------------------------+
| **Returned Data Format** | ``stdout[0].`` |
+---------------------------+-----------------------------------------------+
Using CLI in Ansible
====================
Example CLI ``group_vars/icx.yml``
----------------------------------
.. code-block:: yaml
ansible_connection: network_cli
ansible_network_os: icx
ansible_user: myuser
ansible_password: !vault...
ansible_become: yes
ansible_become_method: enable
ansible_become_password: !vault...
ansible_ssh_common_args: '-o ProxyCommand="ssh -W %h:%p -q bastion01"'
- If you are using SSH keys (including an ssh-agent) you can remove the ``ansible_password`` configuration.
- If you are accessing your host directly (not through a bastion/jump host) you can remove the ``ansible_ssh_common_args`` configuration.
- If you are accessing your host through a bastion/jump host, you cannot include your SSH password in the ``ProxyCommand`` directive. To prevent secrets from leaking out (for example in ``ps`` output), SSH does not support providing passwords via environment variables.
Example CLI Task
----------------
.. code-block:: yaml
- name: Backup current switch config (icx)
icx_config:
backup: yes
register: backup_icx_location
when: ansible_network_os == 'icx'
.. include:: shared_snippets/SSH_warning.txt

View file

@ -17,6 +17,7 @@ Some Ansible Network platforms support multiple connection types, privilege esca
platform_enos platform_enos
platform_eos platform_eos
platform_exos platform_exos
platform_icx
platform_ios platform_ios
platform_ironware platform_ironware
platform_junos platform_junos
@ -80,6 +81,8 @@ Settings by Platform
+-------------------+-------------------------+-------------+---------+---------+----------+ +-------------------+-------------------------+-------------+---------+---------+----------+
| Pluribus Netvisor | ``netvisor`` | ✓ | | | | | Pluribus Netvisor | ``netvisor`` | ✓ | | | |
+-------------------+-------------------------+-------------+---------+---------+----------+ +-------------------+-------------------------+-------------+---------+---------+----------+
| Ruckus ICX* | ``icx`` | ✓ | | | |
+-------------------+-------------------------+-------------+---------+---------+----------+
| VyOS* | ``vyos`` | ✓ | | | ✓ | | VyOS* | ``vyos`` | ✓ | | | ✓ |
+-------------------+-------------------------+-------------+---------+---------+----------+ +-------------------+-------------------------+-------------+---------+---------+----------+
| OS that supports | ``<network-os>`` | | ✓ | | ✓ | | OS that supports | ``<network-os>`` | | ✓ | | ✓ |

View file

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2019, Ansible Project
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
from ansible.module_utils._text import to_text
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.connection import Connection, ConnectionError
_DEVICE_CONFIGS = {}
def get_connection(module):
return Connection(module._socket_path)
def load_config(module, commands):
connection = get_connection(module)
try:
resp = connection.edit_config(candidate=commands)
return resp.get('response')
except ConnectionError as exc:
module.fail_json(msg=to_text(exc))
def run_commands(module, commands, check_rc=True):
connection = get_connection(module)
try:
return connection.run_commands(commands=commands, check_rc=check_rc)
except ConnectionError as exc:
module.fail_json(msg=to_text(exc))
def exec_scp(module, command):
connection = Connection(module._socket_path)
return connection.scp(**command)
def get_config(module, flags=None, compare=None):
flag_str = ' '.join(to_list(flags))
try:
return _DEVICE_CONFIGS[flag_str]
except KeyError:
connection = get_connection(module)
try:
out = connection.get_config(flags=flags, compare=compare)
except ConnectionError as exc:
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
cfg = to_text(out, errors='surrogate_then_replace').strip()
_DEVICE_CONFIGS[flag_str] = cfg
return cfg
def check_args(module, warnings):
pass
def get_defaults_flag(module):
connection = get_connection(module)
try:
out = connection.get_defaults_flag()
except ConnectionError as exc:
module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
return to_text(out, errors='surrogate_then_replace').strip()

View file

@ -0,0 +1,216 @@
#!/usr/bin/python
# Copyright: Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = """
---
module: icx_banner
version_added: "2.9"
author: "Ruckus Wireless (@Commscope)"
short_description: Manage multiline banners on Ruckus ICX 7000 series switches
description:
- This will configure both login and motd banners on remote
ruckus ICX 7000 series switches. It allows playbooks to add or remove
banner text from the active running configuration.
notes:
- Tested against ICX 10.1
options:
banner:
description:
- Specifies which banner should be configured on the remote device.
type: str
required: true
choices: ['motd', 'exec', 'incoming']
text:
description:
- The banner text that should be
present in the remote device running configuration.
This argument accepts a multiline string, with no empty lines.
type: str
state:
description:
- Specifies whether or not the configuration is
present in the current devices active running configuration.
type: str
default: present
choices: ['present', 'absent']
enterkey:
description:
- Specifies whether or not the motd configuration should accept
the require-enter-key
type: bool
default: no
check_running_config:
description:
- Check running configuration. This can be set as environment variable.
Module will use environment variable value(default:True), unless it is overriden,
by specifying it as module parameter.
type: bool
default: yes
"""
EXAMPLES = """
- name: configure the motd banner
icx_banner:
banner: motd
text: |
this is my motd banner
that contains a multiline
string
state: present
- name: remove the motd banner
icx_banner:
banner: motd
state: absent
- name: configure require-enter-key for motd
icx_banner:
banner: motd
enterkey: True
- name: remove require-enter-key for motd
icx_banner:
banner: motd
enterkey: False
"""
RETURN = """
commands:
description: The list of configuration mode commands to send to the device
returned: always
type: list
sample:
- banner motd
- this is my motd banner
- that contains a multiline
- string
"""
import re
from ansible.module_utils._text import to_text
from ansible.module_utils.connection import exec_command
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.network.icx.icx import load_config, get_config
from ansible.module_utils.connection import Connection, ConnectionError
def map_obj_to_commands(updates, module):
commands = list()
state = module.params['state']
want, have = updates
if module.params['banner'] != 'motd' and module.params['enterkey']:
module.fail_json(msg=module.params['banner'] + " banner can have text only, got enterkey")
if state == 'absent':
if 'text' in have.keys() and have['text']:
commands.append('no banner %s' % module.params['banner'])
if(module.params['enterkey'] is False):
commands.append('no banner %s require-enter-key' % module.params['banner'])
elif state == 'present':
if module.params['text'] is None and module.params['enterkey'] is None:
module.fail_json(msg=module.params['banner'] + " one of the following is required: text, enterkey:only if motd")
if module.params["banner"] == "motd" and want['enterkey'] != have['enterkey']:
if(module.params['enterkey']):
commands.append('banner %s require-enter-key' % module.params['banner'])
if want['text'] and (want['text'] != have.get('text')):
module.params["enterkey"] = None
banner_cmd = 'banner %s' % module.params['banner']
banner_cmd += ' $\n'
banner_cmd += module.params['text'].strip()
banner_cmd += '\n$'
commands.append(banner_cmd)
return commands
def map_config_to_obj(module):
compare = module.params.get('check_running_config')
obj = {'banner': module.params['banner'], 'state': 'absent', 'enterkey': False}
exec_command(module, 'skip')
output_text = ''
output_re = ''
out = get_config(module, flags=['| begin banner %s'
% module.params['banner']], compare=module.params['check_running_config'])
if out:
try:
output_re = re.search(r'banner %s( require-enter-key)' % module.params['banner'], out, re.S).group(0)
obj['enterkey'] = True
except BaseException:
pass
try:
output_text = re.search(r'banner %s (\$([^\$])+\$){1}' % module.params['banner'], out, re.S).group(1).strip('$\n')
except BaseException:
pass
else:
output_text = None
if output_text:
obj['text'] = output_text
obj['state'] = 'present'
if module.params['check_running_config'] is False:
obj = {'banner': module.params['banner'], 'state': 'absent', 'enterkey': False, 'text': 'JUNK'}
return obj
def map_params_to_obj(module):
text = module.params['text']
if text:
text = str(text).strip()
return {
'banner': module.params['banner'],
'text': text,
'state': module.params['state'],
'enterkey': module.params['enterkey']
}
def main():
"""entry point for module execution
"""
argument_spec = dict(
banner=dict(required=True, choices=['motd', 'exec', 'incoming']),
text=dict(),
enterkey=dict(type='bool'),
state=dict(default='present', choices=['present', 'absent']),
check_running_config=dict(default=True, type='bool', fallback=(env_fallback, ['ANSIBLE_CHECK_ICX_RUNNING_CONFIG']))
)
required_one_of = [['text', 'enterkey', 'state']]
module = AnsibleModule(argument_spec=argument_spec,
required_one_of=required_one_of,
supports_check_mode=True)
warnings = list()
results = {'changed': False}
want = map_params_to_obj(module)
have = map_config_to_obj(module)
commands = map_obj_to_commands((want, have), module)
results['commands'] = commands
if commands:
if not module.check_mode:
response = load_config(module, commands)
results['changed'] = True
module.exit_json(**results)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,315 @@
# Copyright: (c) 2019, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
---
author: Ruckus Wireless (@Commscope)
cliconf: icx
short_description: Use icx cliconf to run command on Ruckus ICX platform
description:
- This icx plugin provides low level abstraction APIs for
sending and receiving CLI commands from Ruckus ICX network devices.
version_added: "2.9"
"""
import re
import time
import json
import os
from itertools import chain
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils._text import to_text
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.module_utils.network.common.utils import to_list
from ansible.plugins.cliconf import CliconfBase, enable_mode
from ansible.module_utils.common._collections_compat import Mapping
class Cliconf(CliconfBase):
@enable_mode
def get_config(self, source='running', flags=None, format=None, compare=None):
if source not in ('running', 'startup'):
raise ValueError("fetching configuration from %s is not supported" % source)
if format:
raise ValueError("'format' value %s is not supported for get_config" % format)
if not flags:
flags = []
if compare is False:
return ''
else:
if source == 'running':
cmd = 'show running-config '
else:
cmd = 'show configuration '
cmd += ' '.join(to_list(flags))
cmd = cmd.strip()
return self.send_command(cmd)
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
"""
Generate diff between candidate and running configuration. If the
remote host supports onbox diff capabilities ie. supports_onbox_diff in that case
candidate and running configurations are not required to be passed as argument.
In case if onbox diff capability is not supported candidate argument is mandatory
and running argument is optional.
:param candidate: The configuration which is expected to be present on remote host.
:param running: The base configuration which is used to generate diff.
:param diff_match: Instructs how to match the candidate configuration with current device configuration
Valid values are 'line', 'strict', 'exact', 'none'.
'line' - commands are matched line by line
'strict' - command lines are matched with respect to position
'exact' - command lines must be an equal match
'none' - will not compare the candidate configuration with the running configuration
:param diff_ignore_lines: Use this argument to specify one or more lines that should be
ignored during the diff. This is used for lines in the configuration
that are automatically updated by the system. This argument takes
a list of regular expressions or exact line matches.
:param path: The ordered set of parents that uniquely identify the section or hierarchy
the commands should be checked against. If the parents argument
is omitted, the commands are checked against the set of top
level or global commands.
:param diff_replace: Instructs on the way to perform the configuration on the device.
If the replace argument is set to I(line) then the modified lines are
pushed to the device in configuration mode. If the replace argument is
set to I(block) then the entire command block is pushed to the device in
configuration mode if any line is not correct.
:return: Configuration diff in json format.
{
'config_diff': '',
'banner_diff': {}
}
"""
diff = {}
device_operations = self.get_device_operations()
option_values = self.get_option_values()
if candidate is None and device_operations['supports_generate_diff']:
raise ValueError("candidate configuration is required to generate diff")
if diff_match not in option_values['diff_match']:
raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
if diff_replace not in option_values['diff_replace']:
raise ValueError("'replace' value %s in invalid, valid values are %s" % (diff_replace, ', '.join(option_values['diff_replace'])))
# prepare candidate configuration
candidate_obj = NetworkConfig(indent=1)
want_src, want_banners = self._extract_banners(candidate)
candidate_obj.load(want_src)
if running and diff_match != 'none':
# running configuration
have_src, have_banners = self._extract_banners(running)
running_obj = NetworkConfig(indent=1, contents=have_src, ignore_lines=diff_ignore_lines)
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
else:
configdiffobjs = candidate_obj.items
have_banners = {}
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
banners = self._diff_banners(want_banners, have_banners)
diff['banner_diff'] = banners if banners else {}
return diff
@enable_mode
def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
resp = {}
operations = self.get_device_operations()
self.check_edit_config_capability(operations, candidate, commit, replace, comment)
results = []
requests = []
if commit:
prompt = self._connection.get_prompt()
if (b'(config-if' in prompt) or (b'(config' in prompt) or (b'(config-lag-if' in prompt):
self.send_command('end')
self.send_command('configure terminal')
for line in to_list(candidate):
if not isinstance(line, Mapping):
line = {'command': line}
cmd = line['command']
if cmd != 'end' and cmd[0] != '!':
results.append(self.send_command(**line))
requests.append(cmd)
self.send_command('end')
else:
raise ValueError('check mode is not supported')
resp['request'] = requests
resp['response'] = results
return resp
def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None, check_all=False):
if not command:
raise ValueError('must provide value of command to execute')
if output:
raise ValueError("'output' value %s is not supported for get" % output)
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all)
def scp(self, command=None, scp_user=None, scp_pass=None):
if not command:
raise ValueError('must provide value of command to execute')
prompt = ["User name:", "Password:"]
if(scp_pass is None):
answer = [scp_user, self._connection._play_context.password]
else:
answer = [scp_user, scp_pass]
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=False, check_all=True)
def get_device_info(self):
device_info = {}
device_info['network_os'] = 'icx'
reply = self.get(command='show version')
data = to_text(reply, errors='surrogate_or_strict').strip()
match = re.search(r'Version (\S+)', data)
if match:
device_info['network_os_version'] = match.group(1).strip(',')
match = re.search(r'^Cisco (.+) \(revision', data, re.M)
if match:
device_info['network_os_model'] = match.group(1)
match = re.search(r'^(.+) uptime', data, re.M)
if match:
device_info['network_os_hostname'] = match.group(1)
return device_info
def get_device_operations(self):
return {
'supports_diff_replace': True,
'supports_commit': False,
'supports_rollback': False,
'supports_defaults': True,
'supports_onbox_diff': False,
'supports_commit_comment': False,
'supports_multiline_delimiter': True,
'supports_diff_match': True,
'supports_diff_ignore_lines': True,
'supports_generate_diff': True,
'supports_replace': False
}
def get_option_values(self):
return {
'format': ['text'],
'diff_match': ['line', 'strict', 'exact', 'none'],
'diff_replace': ['line', 'block'],
'output': []
}
def get_capabilities(self):
result = dict()
result['rpc'] = self.get_base_rpc() + ['edit_banner', 'get_diff', 'run_commands', 'get_defaults_flag']
result['network_api'] = 'cliconf'
result['device_operations'] = self.get_device_operations()
result.update(self.get_option_values())
return json.dumps(result)
def edit_banner(self, candidate=None, multiline_delimiter="@", commit=True):
"""
Edit banner on remote device
:param banners: Banners to be loaded in json format
:param multiline_delimiter: Line delimiter for banner
:param commit: Boolean value that indicates if the device candidate
configuration should be pushed in the running configuration or discarded.
:param diff: Boolean flag to indicate if configuration that is applied on remote host should
generated and returned in response or not
:return: Returns response of executing the configuration command received
from remote host
"""
resp = {}
banners_obj = json.loads(candidate)
results = []
requests = []
if commit:
for key, value in iteritems(banners_obj):
key += ' %s' % multiline_delimiter
self.send_command('config terminal', sendonly=True)
for cmd in [key, value, multiline_delimiter]:
obj = {'command': cmd, 'sendonly': True}
results.append(self.send_command(**obj))
requests.append(cmd)
self.send_command('end', sendonly=True)
time.sleep(0.1)
results.append(self.send_command('\n'))
requests.append('\n')
resp['request'] = requests
resp['response'] = results
return resp
def run_commands(self, commands=None, check_rc=True):
if commands is None:
raise ValueError("'commands' value is required")
responses = list()
for cmd in to_list(commands):
if not isinstance(cmd, Mapping):
cmd = {'command': cmd}
output = cmd.pop('output', None)
if output:
raise ValueError("'output' value %s is not supported for run_commands" % output)
try:
out = self.send_command(**cmd)
except AnsibleConnectionFailure as e:
if check_rc:
raise
out = getattr(e, 'err', to_text(e))
responses.append(out)
return responses
def _extract_banners(self, config):
banners = {}
banner_cmds = re.findall(r'^banner (\w+)', config, re.M)
for cmd in banner_cmds:
regex = r'banner %s \$(.+?)(?=\$)' % cmd
match = re.search(regex, config, re.S)
if match:
key = 'banner %s' % cmd
banners[key] = match.group(1).strip()
for cmd in banner_cmds:
regex = r'banner %s \$(.+?)(?=\$)' % cmd
match = re.search(regex, config, re.S)
if match:
config = config.replace(str(match.group(1)), '')
config = re.sub(r'banner \w+ \$\$', '!! banner removed', config)
return config, banners
def _diff_banners(self, want, have):
candidate = {}
for key, value in iteritems(want):
if value != have.get(key):
candidate[key] = value
return candidate

View file

@ -0,0 +1,81 @@
# Copyright: (c) 2019, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
from ansible.plugins.terminal import TerminalBase
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils._text import to_text, to_bytes
import json
class TerminalModule(TerminalBase):
terminal_stdout_re = [
re.compile(br"[\r\n]?[\w\+\-\.:\/\[\]]+(?:\([^\)]+\)){0,3}(?:[>#]) ?$")
]
terminal_stderr_re = [
re.compile(br"% ?Error"),
re.compile(br"% ?Bad secret"),
re.compile(br"[\r\n%] Bad passwords"),
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.compile(br"'[^']' +returned error code: ?\d+"),
re.compile(br"Bad mask", re.I),
re.compile(br"% ?(\S+) ?overlaps with ?(\S+)", re.I),
re.compile(br"[%\S] ?Error: ?[\s]+", re.I),
re.compile(br"[%\S] ?Informational: ?[\s]+", re.I),
re.compile(br"Command authorization failed"),
re.compile(br"Error - *"),
re.compile(br"Error - Incorrect username or password."),
re.compile(br"Invalid input"),
re.compile(br"Already a http operation is in progress"),
re.compile(br"Flash access in progress. Please try later"),
re.compile(br"Error: .*"),
re.compile(br"^Error: .*", re.I),
re.compile(br"^Ambiguous input"),
re.compile(br"Errno")
]
def on_open_shell(self):
pass
def __del__(self):
try:
self.close()
except AnsibleConnectionFailure:
raise AnsibleConnectionFailure('unable to set terminal parameters')
def on_become(self, passwd=None):
if self._get_prompt().endswith(b'#'):
return
cmd = {u'command': u'enable'}
cmd[u'prompt'] = to_text(r"[\r\n](?:Local_)?[Pp]assword: ?$", errors='surrogate_or_strict')
cmd[u'answer'] = passwd
cmd[u'prompt_retry_check'] = True
try:
self._exec_cli_command(to_bytes(json.dumps(cmd), errors='surrogate_or_strict'))
prompt = self._get_prompt()
if prompt is None or not prompt.endswith(b'#'):
raise AnsibleConnectionFailure('failed to elevate privilege to enable mode still at prompt [%s]' % prompt)
except AnsibleConnectionFailure as e:
prompt = self._get_prompt()
raise AnsibleConnectionFailure('unable to elevate privilege to enable mode, at prompt [%s] with error: %s' % (prompt, e.message))
def on_unbecome(self):
prompt = self._get_prompt()
if prompt is None:
return
if b'(config' in prompt:
self._exec_cli_command(b'exit')
elif prompt.endswith(b'#'):
self._exec_cli_command(b'exit')

View file

@ -0,0 +1,16 @@
banner motd require-enter-key
banner motd $
welcome
new user
$
!
interface ethernet 1/1/1
port-name port name
disable
speed-duplex 10-full
inline power power-limit 7000
!
interface ethernet 1/1/2
speed-duplex 10-full
inline power power-limit 3000
!

View file

@ -0,0 +1,93 @@
# Copyright: (c) 2019, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
fixture_data = {}
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 Exception:
pass
fixture_data[path] = data
return data
class TestICXModule(ModuleTestCase):
ENV_ICX_USE_DIFF = True
def set_running_config(self):
self.ENV_ICX_USE_DIFF = self.get_running_config()
def get_running_config(self, compare=None):
if compare is not None:
diff = compare
elif os.environ.get('ANSIBLE_CHECK_ICX_RUNNING_CONFIG') is not None:
if os.environ.get('ANSIBLE_CHECK_ICX_RUNNING_CONFIG') == 'False':
diff = False
else:
diff = True
else:
diff = True
return diff
def execute_module(self, failed=False, changed=False, commands=None, sort=True, defaults=False, fields=None):
self.load_fixtures(commands)
if failed:
result = self.failed()
self.assertTrue(result['failed'], result)
else:
result = self.changed(changed)
self.assertEqual(result['changed'], changed, result)
if commands is not None:
if sort:
self.assertEqual(sorted(commands), sorted(result['commands']))
else:
self.assertEqual(commands, result['commands'], result['commands'])
if fields is not None:
for key in fields:
if fields.get(key) is not None:
self.assertEqual(fields.get(key), result.get(key))
return result
def failed(self):
with self.assertRaises(AnsibleFailJson) as exc:
self.module.main()
result = exc.exception.args[0]
self.assertTrue(result['failed'], result)
return result
def changed(self, changed=False):
with self.assertRaises(AnsibleExitJson) as exc:
self.module.main()
result = exc.exception.args[0]
self.assertEqual(result['changed'], changed, result)
return result
def load_fixtures(self, commands=None):
pass

View file

@ -0,0 +1,96 @@
# Copyright: (c) 2019, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from units.compat.mock import patch
from ansible.modules.network.icx import icx_banner
from units.modules.utils import set_module_args
from .icx_module import TestICXModule, load_fixture
class TestICXBannerModule(TestICXModule):
module = icx_banner
def setUp(self):
super(TestICXBannerModule, self).setUp()
self.mock_exec_command = patch('ansible.modules.network.icx.icx_banner.exec_command')
self.exec_command = self.mock_exec_command.start()
self.mock_load_config = patch('ansible.modules.network.icx.icx_banner.load_config')
self.load_config = self.mock_load_config.start()
self.mock_get_config = patch('ansible.modules.network.icx.icx_banner.get_config')
self.get_config = self.mock_get_config.start()
self.set_running_config()
def tearDown(self):
super(TestICXBannerModule, self).tearDown()
self.mock_exec_command.stop()
self.mock_load_config.stop()
self.mock_get_config.stop()
def load_fixtures(self, commands=None):
compares = None
def load_file(*args, **kwargs):
module = args
for arg in args:
if arg.params['check_running_config'] is True:
return load_fixture('icx_banner_show_banner.txt').strip()
else:
return ''
self.exec_command.return_value = (0, '', None)
self.get_config.side_effect = load_file
self.load_config.return_value = dict(diff=None, session='session')
def test_icx_banner_create(self):
if not self.ENV_ICX_USE_DIFF:
set_module_args(dict(banner='motd', text='welcome\nnew user'))
commands = ['banner motd $\nwelcome\nnew user\n$']
self.execute_module(changed=True, commands=commands)
else:
for banner_type in ('motd', 'exec', 'incoming'):
set_module_args(dict(banner=banner_type, text='test\nbanner\nstring'))
commands = ['banner {0} $\ntest\nbanner\nstring\n$'.format(banner_type)]
self.execute_module(changed=True, commands=commands)
def test_icx_banner_remove(self):
set_module_args(dict(banner='motd', state='absent'))
if not self.ENV_ICX_USE_DIFF:
commands = ['no banner motd']
self.execute_module(changed=True, commands=commands)
else:
commands = ['no banner motd']
self.execute_module(changed=True, commands=commands)
def test_icx_banner_motd_enter_set(self):
set_module_args(dict(banner='motd', enterkey=True))
if not self.ENV_ICX_USE_DIFF:
commands = ['banner motd require-enter-key']
self.execute_module(changed=True, commands=commands)
else:
self.execute_module(changed=False)
def test_icx_banner_motd_enter_remove(self):
set_module_args(dict(banner='motd', state='absent', enterkey=False))
if not self.ENV_ICX_USE_DIFF:
commands = ['no banner motd', 'no banner motd require-enter-key']
self.execute_module(changed=True, commands=commands)
else:
commands = ['no banner motd', 'no banner motd require-enter-key']
self.execute_module(changed=True, commands=commands)
def test_icx_banner_remove_compare(self):
set_module_args(dict(banner='incoming', state='absent', check_running_config='True'))
if self.get_running_config(compare=True):
if not self.ENV_ICX_USE_DIFF:
commands = []
self.execute_module(changed=False, commands=commands)
else:
commands = []
self.execute_module()