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:
parent
58a53fe0eb
commit
f2cb44633a
13 changed files with 959 additions and 0 deletions
1
.github/BOTMETA.yml
vendored
1
.github/BOTMETA.yml
vendored
|
@ -334,6 +334,7 @@ files:
|
|||
maintainers: $team_iosxr
|
||||
$modules/network/ironware/: paulquack
|
||||
$modules/network/junos/: Qalthos ganeshrn
|
||||
$modules/network/icx/: sushma-alethea
|
||||
$modules/network/layer2/: $team_networking
|
||||
$modules/network/layer3/: $team_networking
|
||||
$modules/network/meraki/: &meraki
|
||||
|
|
69
docs/docsite/rst/network/user_guide/platform_icx.rst
Normal file
69
docs/docsite/rst/network/user_guide/platform_icx.rst
Normal 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
|
|
@ -17,6 +17,7 @@ Some Ansible Network platforms support multiple connection types, privilege esca
|
|||
platform_enos
|
||||
platform_eos
|
||||
platform_exos
|
||||
platform_icx
|
||||
platform_ios
|
||||
platform_ironware
|
||||
platform_junos
|
||||
|
@ -80,6 +81,8 @@ Settings by Platform
|
|||
+-------------------+-------------------------+-------------+---------+---------+----------+
|
||||
| Pluribus Netvisor | ``netvisor`` | ✓ | | | |
|
||||
+-------------------+-------------------------+-------------+---------+---------+----------+
|
||||
| Ruckus ICX* | ``icx`` | ✓ | | | |
|
||||
+-------------------+-------------------------+-------------+---------+---------+----------+
|
||||
| VyOS* | ``vyos`` | ✓ | | | ✓ |
|
||||
+-------------------+-------------------------+-------------+---------+---------+----------+
|
||||
| OS that supports | ``<network-os>`` | | ✓ | | ✓ |
|
||||
|
|
0
lib/ansible/module_utils/network/icx/__init__.py
Normal file
0
lib/ansible/module_utils/network/icx/__init__.py
Normal file
69
lib/ansible/module_utils/network/icx/icx.py
Normal file
69
lib/ansible/module_utils/network/icx/icx.py
Normal 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()
|
0
lib/ansible/modules/network/icx/__init__.py
Normal file
0
lib/ansible/modules/network/icx/__init__.py
Normal file
216
lib/ansible/modules/network/icx/icx_banner.py
Normal file
216
lib/ansible/modules/network/icx/icx_banner.py
Normal 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()
|
315
lib/ansible/plugins/cliconf/icx.py
Normal file
315
lib/ansible/plugins/cliconf/icx.py
Normal 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
|
81
lib/ansible/plugins/terminal/icx.py
Normal file
81
lib/ansible/plugins/terminal/icx.py
Normal 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')
|
0
test/units/modules/network/icx/__init__.py
Normal file
0
test/units/modules/network/icx/__init__.py
Normal 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
|
||||
!
|
93
test/units/modules/network/icx/icx_module.py
Normal file
93
test/units/modules/network/icx/icx_module.py
Normal 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
|
96
test/units/modules/network/icx/test_icx_banner.py
Normal file
96
test/units/modules/network/icx/test_icx_banner.py
Normal 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()
|
Loading…
Reference in a new issue