mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Refactor ios cliconf plugin and ios_config module (#39695)
* Refactor ios cliconf plugin and ios_config module * Refactor ios cliconf plugin to support generic network_config module * Refactor ios_config module to work with cliconf api's * Enable command and response logging in cliconf pulgin * cliconf api documentation * Fix unit test and other minor changes * Doc update * Fix CI failure * Add default flag related changes * Minor changes * redact input command logging by default
This commit is contained in:
parent
9abc3dbec4
commit
ba4b12358c
8 changed files with 473 additions and 182 deletions
|
@ -135,10 +135,12 @@ def dumps(objects, output='block', comments=False):
|
|||
items = _obj_to_block(objects)
|
||||
elif output == 'commands':
|
||||
items = _obj_to_text(objects)
|
||||
elif output == 'raw':
|
||||
items = _obj_to_raw(objects)
|
||||
else:
|
||||
raise TypeError('unknown value supplied for keyword output')
|
||||
|
||||
if output != 'commands':
|
||||
if output == 'block':
|
||||
if comments:
|
||||
for index, item in enumerate(items):
|
||||
nextitem = index + 1
|
||||
|
|
|
@ -114,7 +114,7 @@ def get_config(module, flags=None):
|
|||
return _DEVICE_CONFIGS[flag_str]
|
||||
except KeyError:
|
||||
connection = get_connection(module)
|
||||
out = connection.get_config(flags=flags)
|
||||
out = connection.get_config(filter=flags)
|
||||
cfg = to_text(out, errors='surrogate_then_replace').strip()
|
||||
_DEVICE_CONFIGS[flag_str] = cfg
|
||||
return cfg
|
||||
|
|
|
@ -291,17 +291,14 @@ backup_path:
|
|||
type: string
|
||||
sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34
|
||||
"""
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
|
||||
from ansible.module_utils.network.ios.ios import run_commands, get_config, load_config
|
||||
from ansible.module_utils.network.ios.ios import get_defaults_flag
|
||||
from ansible.module_utils.network.ios.ios import run_commands, get_config
|
||||
from ansible.module_utils.network.ios.ios import get_defaults_flag, get_connection
|
||||
from ansible.module_utils.network.ios.ios import ios_argument_spec
|
||||
from ansible.module_utils.network.ios.ios import check_args as ios_check_args
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.network.common.parsing import Conditional
|
||||
from ansible.module_utils.network.common.config import NetworkConfig, dumps
|
||||
from ansible.module_utils.six import iteritems
|
||||
|
||||
|
||||
def check_args(module, warnings):
|
||||
|
@ -312,70 +309,29 @@ def check_args(module, warnings):
|
|||
'single character')
|
||||
|
||||
|
||||
def extract_banners(config):
|
||||
banners = {}
|
||||
banner_cmds = re.findall(r'^banner (\w+)', config, re.M)
|
||||
for cmd in banner_cmds:
|
||||
regex = r'banner %s \^C(.+?)(?=\^C)' % cmd
|
||||
match = re.search(regex, config, re.S)
|
||||
if match:
|
||||
key = 'banner %s' % cmd
|
||||
banners[key] = match.group(1).strip()
|
||||
def get_candidate_config(module):
|
||||
candidate = ''
|
||||
if module.params['src']:
|
||||
candidate = module.params['src']
|
||||
|
||||
for cmd in banner_cmds:
|
||||
regex = r'banner %s \^C(.+?)(?=\^C)' % cmd
|
||||
match = re.search(regex, config, re.S)
|
||||
if match:
|
||||
config = config.replace(str(match.group(1)), '')
|
||||
elif module.params['lines']:
|
||||
candidate_obj = NetworkConfig(indent=1)
|
||||
parents = module.params['parents'] or list()
|
||||
candidate_obj.add(module.params['lines'], parents=parents)
|
||||
candidate = dumps(candidate_obj, 'raw')
|
||||
|
||||
config = re.sub(r'banner \w+ \^C\^C', '!! banner removed', config)
|
||||
return (config, banners)
|
||||
|
||||
|
||||
def diff_banners(want, have):
|
||||
candidate = {}
|
||||
for key, value in iteritems(want):
|
||||
if value != have.get(key):
|
||||
candidate[key] = value
|
||||
return candidate
|
||||
|
||||
|
||||
def load_banners(module, banners):
|
||||
delimiter = module.params['multiline_delimiter']
|
||||
for key, value in iteritems(banners):
|
||||
key += ' %s' % delimiter
|
||||
for cmd in ['config terminal', key, value, delimiter, 'end']:
|
||||
obj = {'command': cmd, 'sendonly': True}
|
||||
run_commands(module, [cmd])
|
||||
time.sleep(0.1)
|
||||
run_commands(module, ['\n'])
|
||||
|
||||
|
||||
def get_running_config(module, current_config=None, flags=None):
|
||||
contents = module.params['running_config']
|
||||
|
||||
if not contents:
|
||||
running = module.params['running_config']
|
||||
if not running:
|
||||
if not module.params['defaults'] and current_config:
|
||||
contents, banners = extract_banners(current_config.config_text)
|
||||
running = current_config
|
||||
else:
|
||||
contents = get_config(module, flags=flags)
|
||||
contents, banners = extract_banners(contents)
|
||||
return NetworkConfig(indent=1, contents=contents), banners
|
||||
running = get_config(module, flags=flags)
|
||||
|
||||
|
||||
def get_candidate(module):
|
||||
candidate = NetworkConfig(indent=1)
|
||||
banners = {}
|
||||
|
||||
if module.params['src']:
|
||||
src, banners = extract_banners(module.params['src'])
|
||||
candidate.load(src)
|
||||
|
||||
elif module.params['lines']:
|
||||
parents = module.params['parents'] or list()
|
||||
candidate.add(module.params['lines'], parents=parents)
|
||||
|
||||
return candidate, banners
|
||||
return running
|
||||
|
||||
|
||||
def save_config(module, result):
|
||||
|
@ -445,7 +401,9 @@ def main():
|
|||
result['warnings'] = warnings
|
||||
|
||||
config = None
|
||||
contents = None
|
||||
flags = get_defaults_flag(module) if module.params['defaults'] else []
|
||||
connection = get_connection(module)
|
||||
|
||||
if module.params['backup'] or (module._diff and module.params['diff_against'] == 'running'):
|
||||
contents = get_config(module, flags=flags)
|
||||
|
@ -458,20 +416,16 @@ def main():
|
|||
replace = module.params['replace']
|
||||
path = module.params['parents']
|
||||
|
||||
candidate, want_banners = get_candidate(module)
|
||||
candidate = get_candidate_config(module)
|
||||
running = get_running_config(module, contents, flags=flags)
|
||||
|
||||
if match != 'none':
|
||||
config, have_banners = get_running_config(module, config, flags=flags)
|
||||
path = module.params['parents']
|
||||
configobjs = candidate.difference(config, path=path, match=match, replace=replace)
|
||||
else:
|
||||
configobjs = candidate.items
|
||||
have_banners = {}
|
||||
response = connection.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=None, path=path, replace=replace)
|
||||
diff = json.loads(response)
|
||||
config_diff = diff['config_diff']
|
||||
banner_diff = diff['banner_diff']
|
||||
|
||||
banners = diff_banners(want_banners, have_banners)
|
||||
|
||||
if configobjs or banners:
|
||||
commands = dumps(configobjs, 'commands').split('\n')
|
||||
if config_diff or banner_diff:
|
||||
commands = config_diff.split('\n')
|
||||
|
||||
if module.params['before']:
|
||||
commands[:0] = module.params['before']
|
||||
|
@ -481,15 +435,15 @@ def main():
|
|||
|
||||
result['commands'] = commands
|
||||
result['updates'] = commands
|
||||
result['banners'] = banners
|
||||
result['banners'] = banner_diff
|
||||
|
||||
# send the configuration commands to the device and merge
|
||||
# them with the current running config
|
||||
if not module.check_mode:
|
||||
if commands:
|
||||
load_config(module, commands)
|
||||
if banners:
|
||||
load_banners(module, banners)
|
||||
connection.edit_config(commands)
|
||||
if banner_diff:
|
||||
connection.edit_banner(json.dumps(banner_diff), multiline_delimiter=module.params['multiline_delimiter'])
|
||||
|
||||
result['changed'] = True
|
||||
|
||||
|
@ -556,6 +510,5 @@ def main():
|
|||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import signal
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from functools import wraps
|
||||
|
||||
|
@ -83,8 +81,12 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
|||
conn.edit_config(['hostname test', 'netconf ssh'])
|
||||
"""
|
||||
|
||||
__rpc__ = ['get_config', 'edit_config', 'get_capabilities', 'get', 'enable_response_logging', 'disable_response_logging']
|
||||
|
||||
def __init__(self, connection):
|
||||
self._connection = connection
|
||||
self.history = list()
|
||||
self.response_logging = False
|
||||
|
||||
def _alarm_handler(self, signum, frame):
|
||||
"""Alarm handler raised in case of command timeout """
|
||||
|
@ -92,94 +94,208 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
|||
self.close()
|
||||
|
||||
def send_command(self, command, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False):
|
||||
"""Executes a cli command and returns the results
|
||||
This method will execute the CLI command on the connection and return
|
||||
the results to the caller. The command output will be returned as a
|
||||
string
|
||||
"""Executes a command over the device connection
|
||||
|
||||
This method will execute a command over the device connection and
|
||||
return the results to the caller. This method will also perform
|
||||
logging of any commands based on the `nolog` argument.
|
||||
|
||||
:param command: The command to send over the connection to the device
|
||||
:param prompt: A regex pattern to evalue the expected prompt from the command
|
||||
:param answer: The answer to respond with if the prompt is matched.
|
||||
:param sendonly: Bool value that will send the command but not wait for a result.
|
||||
:param newline: Bool value that will append the newline character to the command
|
||||
:param prompt_retry_check: Bool value for trying to detect more prompts
|
||||
|
||||
:returns: The output from the device after executing the command
|
||||
"""
|
||||
kwargs = {'command': to_bytes(command), 'sendonly': sendonly,
|
||||
'newline': newline, 'prompt_retry_check': prompt_retry_check}
|
||||
kwargs = {
|
||||
'command': to_bytes(command),
|
||||
'sendonly': sendonly,
|
||||
'newline': newline,
|
||||
'prompt_retry_check': prompt_retry_check
|
||||
}
|
||||
|
||||
if prompt is not None:
|
||||
kwargs['prompt'] = to_bytes(prompt)
|
||||
if answer is not None:
|
||||
kwargs['answer'] = to_bytes(answer)
|
||||
|
||||
resp = self._connection.send(**kwargs)
|
||||
|
||||
if not self.response_logging:
|
||||
self.history.append(('*****', '*****'))
|
||||
else:
|
||||
self.history.append((kwargs['command'], resp))
|
||||
|
||||
return resp
|
||||
|
||||
def get_base_rpc(self):
|
||||
"""Returns list of base rpc method supported by remote device"""
|
||||
return ['get_config', 'edit_config', 'get_capabilities', 'get']
|
||||
return self.__rpc__
|
||||
|
||||
def get_history(self):
|
||||
""" Returns the history file for all commands
|
||||
|
||||
This will return a log of all the commands that have been sent to
|
||||
the device and all of the output received. By default, all commands
|
||||
and output will be redacted unless explicitly configured otherwise.
|
||||
|
||||
:return: An ordered list of command, output pairs
|
||||
"""
|
||||
return self.history
|
||||
|
||||
def reset_history(self):
|
||||
""" Resets the history of run commands
|
||||
:return: None
|
||||
"""
|
||||
self.history = list()
|
||||
|
||||
def enable_response_logging(self):
|
||||
"""Enable logging command response"""
|
||||
self.response_logging = True
|
||||
|
||||
def disable_response_logging(self):
|
||||
"""Disable logging command response"""
|
||||
self.response_logging = False
|
||||
|
||||
@abstractmethod
|
||||
def get_config(self, source='running', format='text'):
|
||||
def get_config(self, source='running', filter=None, format='text'):
|
||||
"""Retrieves the specified configuration from the device
|
||||
|
||||
This method will retrieve the configuration specified by source and
|
||||
return it to the caller as a string. Subsequent calls to this method
|
||||
will retrieve a new configuration from the device
|
||||
:args:
|
||||
arg[0] source: Datastore from which configuration should be retrieved eg: running/candidate/startup. (optional)
|
||||
default is running.
|
||||
arg[1] format: Output format in which configuration is retrieved
|
||||
Note: Specified datastore should be supported by remote device.
|
||||
:kwargs:
|
||||
Keywords supported
|
||||
:command: the command string to execute
|
||||
:source: Datastore from which configuration should be retrieved
|
||||
:format: Output format in which configuration is retrieved
|
||||
:returns: Returns output received from remote device as byte string
|
||||
|
||||
:param source: The configuration source to return from the device.
|
||||
This argument accepts either `running` or `startup` as valid values.
|
||||
|
||||
:param filter: For devices that support configuration filtering, this
|
||||
keyword argument is used to filter the returned configuration.
|
||||
The use of this keyword argument is device dependent adn will be
|
||||
silently ignored on devices that do not support it.
|
||||
|
||||
:param format: For devices that support fetching different configuration
|
||||
format, this keyword argument is used to specify the format in which
|
||||
configuration is to be retrieved.
|
||||
|
||||
:return: The device configuration as specified by the source argument.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def edit_config(self, commands=None):
|
||||
"""Loads the specified commands into the remote device
|
||||
This method will load the commands into the remote device. This
|
||||
method will make sure the device is in the proper context before
|
||||
send the commands (eg config mode)
|
||||
:args:
|
||||
arg[0] command: List of configuration commands
|
||||
:kwargs:
|
||||
Keywords supported
|
||||
:command: the command string to execute
|
||||
:returns: Returns output received from remote device as byte string
|
||||
def edit_config(self, candidate, check_mode=False, replace=None):
|
||||
"""Loads the candidate configuration into the network device
|
||||
|
||||
This method will load the specified candidate config into the device
|
||||
and merge with the current configuration unless replace is set to
|
||||
True. If the device does not support config replace an errors
|
||||
is returned.
|
||||
|
||||
:param candidate: The configuration to load into the device and merge
|
||||
with the current running configuration
|
||||
|
||||
:param check_mode: Boolean value that indicates if the device candidate
|
||||
configuration should be pushed in the running configuration or discarded.
|
||||
|
||||
:param replace: Specifies the way in which provided config value should replace
|
||||
the configuration running on the remote device. If the device
|
||||
doesn't support config replace, an error is return.
|
||||
|
||||
:return: Returns response of executing the configuration command received
|
||||
from remote host
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True):
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False, newline=True):
|
||||
"""Execute specified command on remote device
|
||||
This method will retrieve the specified data and
|
||||
return it to the caller as a string.
|
||||
:args:
|
||||
command: command in string format to be executed on remote device
|
||||
prompt: the expected prompt generated by executing command.
|
||||
This can be a string or a list of strings (optional)
|
||||
answer: the string to respond to the prompt with (optional)
|
||||
sendonly: bool to disable waiting for response, default is false (optional)
|
||||
:returns: Returns output received from remote device as byte string
|
||||
:param command: command in string format to be executed on remote device
|
||||
:param prompt: the expected prompt generated by executing command, this can
|
||||
be a string or a list of strings
|
||||
:param answer: the string to respond to the prompt with
|
||||
:param sendonly: bool to disable waiting for response, default is false
|
||||
:param newline: bool to indicate if newline should be added at end of answer or not
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_capabilities(self):
|
||||
"""Retrieves device information and supported
|
||||
rpc methods by device platform and return result
|
||||
as a string
|
||||
:returns: Returns output received from remote device as byte string
|
||||
"""Returns the basic capabilities of the network device
|
||||
This method will provide some basic facts about the device and
|
||||
what capabilities it has to modify the configuration. The minimum
|
||||
return from this method takes the following format.
|
||||
eg:
|
||||
{
|
||||
|
||||
'rpc': [list of supported rpcs],
|
||||
'network_api': <str>, # the name of the transport
|
||||
'device_info': {
|
||||
'network_os': <str>,
|
||||
'network_os_version': <str>,
|
||||
'network_os_model': <str>,
|
||||
'network_os_hostname': <str>,
|
||||
'network_os_image': <str>,
|
||||
'network_os_platform': <str>,
|
||||
},
|
||||
'device_operations': {
|
||||
'supports_replace': <bool>, # identify if config should be merged or replaced is supported
|
||||
'supports_commit': <bool>, # identify if commit is supported by device or not
|
||||
'supports_rollback': <bool>, # identify if rollback is supported or not
|
||||
'supports_defaults': <bool>, # identify if fetching running config with default is supported
|
||||
'supports_commit_comment': <bool>, # identify if adding comment to commit is supported of not
|
||||
'supports_onbox_diff: <bool>, # identify if on box diff capability is supported or not
|
||||
'supports_generate_diff: <bool>, # identify if diff capability is supported within plugin
|
||||
'supports_multiline_delimiter: <bool>, # identify if multiline demiliter is supported within config
|
||||
'support_match: <bool>, # identify if match is supported
|
||||
'support_diff_ignore_lines: <bool>, # identify if ignore line in diff is supported
|
||||
}
|
||||
'format': [list of supported configuration format],
|
||||
'match': ['line', 'strict', 'exact', 'none'],
|
||||
'replace': ['line', 'block', 'config'],
|
||||
}
|
||||
:return: capability as json string
|
||||
"""
|
||||
pass
|
||||
|
||||
def commit(self, comment=None):
|
||||
"""Commit configuration changes"""
|
||||
"""Commit configuration changes
|
||||
|
||||
This method will perform the commit operation on a previously loaded
|
||||
candidate configuration that was loaded using `edit_config()`. If
|
||||
there is a candidate configuration, it will be committed to the
|
||||
active configuration. If there is not a candidate configuration, this
|
||||
method should just silently return.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
return self._connection.method_not_found("commit is not supported by network_os %s" % self._play_context.network_os)
|
||||
|
||||
def discard_changes(self):
|
||||
"Discard changes in candidate datastore"
|
||||
"""Discard candidate configuration
|
||||
|
||||
This method will discard the current candidate configuration if one
|
||||
is present. If there is no candidate configuration currently loaded,
|
||||
then this method should just silently return
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
return self._connection.method_not_found("discard_changes is not supported by network_os %s" % self._play_context.network_os)
|
||||
|
||||
def copy_file(self, source=None, destination=None, proto='scp', timeout=30):
|
||||
"""Copies file over scp/sftp to remote device"""
|
||||
"""Copies file over scp/sftp to remote device
|
||||
|
||||
:param source: Source file path
|
||||
:param destination: Destination file path on remote device
|
||||
:param proto: Protocol to be used for file transfer,
|
||||
supported protocol: scp and sftp
|
||||
:param timeout: Specifies the wait time to receive response from
|
||||
remote host before triggering timeout exception
|
||||
:return: None
|
||||
"""
|
||||
ssh = self._connection.paramiko_conn._connect_uncached()
|
||||
if proto == 'scp':
|
||||
if not HAS_SCP:
|
||||
|
@ -191,6 +307,15 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
|
|||
sftp.put(source, destination)
|
||||
|
||||
def get_file(self, source=None, destination=None, proto='scp', timeout=30):
|
||||
"""Fetch file over scp/sftp from remote device
|
||||
:param source: Source file path
|
||||
:param destination: Destination file path
|
||||
:param proto: Protocol to be used for file transfer,
|
||||
supported protocol: scp and sftp
|
||||
:param timeout: Specifies the wait time to receive response from
|
||||
remote host before triggering timeout exception
|
||||
:return: None
|
||||
"""
|
||||
"""Fetch file over scp/sftp from remote device"""
|
||||
ssh = self._connection.paramiko_conn._connect_uncached()
|
||||
if proto == 'scp':
|
||||
|
|
|
@ -19,18 +19,134 @@
|
|||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import collections
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', filter=None, format='text'):
|
||||
if source not in ('running', 'startup'):
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
|
||||
if not filter:
|
||||
filter = []
|
||||
if source == 'running':
|
||||
cmd = 'show running-config '
|
||||
else:
|
||||
cmd = 'show startup-config '
|
||||
|
||||
cmd += ' '.join(to_list(filter))
|
||||
cmd = cmd.strip()
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, 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 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 on the remote device
|
||||
: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 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()
|
||||
|
||||
if candidate is None and not device_operations['supports_onbox_diff']:
|
||||
raise ValueError('candidate configuration is required to generate diff')
|
||||
|
||||
# prepare candidate configuration
|
||||
candidate_obj = NetworkConfig(indent=1)
|
||||
want_src, want_banners = self._extract_banners(candidate)
|
||||
candidate_obj.load(want_src)
|
||||
|
||||
if running and 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=match, replace=replace)
|
||||
|
||||
else:
|
||||
configdiffobjs = candidate_obj.items
|
||||
have_banners = {}
|
||||
|
||||
configdiff = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
|
||||
diff['config_diff'] = configdiff if configdiffobjs else {}
|
||||
|
||||
banners = self._diff_banners(want_banners, have_banners)
|
||||
|
||||
diff['banner_diff'] = banners if banners else {}
|
||||
return json.dumps(diff)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, candidate, check_mode=False, replace=None):
|
||||
|
||||
if not candidate:
|
||||
raise ValueError('must provide a candidate config to load')
|
||||
|
||||
if check_mode not in (True, False):
|
||||
raise ValueError('`check_mode` must be a bool, got %s' % check_mode)
|
||||
|
||||
device_operations = self.get_device_operations()
|
||||
options = self.get_options()
|
||||
if replace and replace not in options['replace']:
|
||||
raise ValueError('`replace` value %s in invalid, valid values are %s' % (replace, options['replace']))
|
||||
|
||||
results = []
|
||||
if not check_mode:
|
||||
for line in chain(['configure terminal'], to_list(candidate)):
|
||||
if line != 'end' and line[0] != '!':
|
||||
if not isinstance(line, collections.Mapping):
|
||||
line = {'command': line}
|
||||
|
||||
results.append(self.send_command(**line))
|
||||
|
||||
results.append(self.send_command('end'))
|
||||
|
||||
return results[1:-1]
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False):
|
||||
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
|
@ -52,48 +168,82 @@ class Cliconf(CliconfBase):
|
|||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text', flags=None):
|
||||
if source not in ('running', 'startup'):
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
def get_device_operations(self):
|
||||
return {
|
||||
'supports_replace': True,
|
||||
'supports_commit': False,
|
||||
'supports_rollback': False,
|
||||
'supports_defaults': True,
|
||||
'supports_onbox_diff': False,
|
||||
'supports_commit_comment': False,
|
||||
'supports_multiline_delimiter': False,
|
||||
'support_match': True,
|
||||
'support_diff_ignore_lines': True,
|
||||
'supports_generate_diff': True,
|
||||
}
|
||||
|
||||
if not flags:
|
||||
flags = []
|
||||
|
||||
if source == 'running':
|
||||
cmd = 'show running-config '
|
||||
else:
|
||||
cmd = 'show startup-config '
|
||||
|
||||
cmd += ' '.join(to_list(flags))
|
||||
cmd = cmd.strip()
|
||||
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
results = []
|
||||
for cmd in chain(['configure terminal'], to_list(command), ['end']):
|
||||
if isinstance(cmd, dict):
|
||||
command = cmd['command']
|
||||
prompt = cmd['prompt']
|
||||
answer = cmd['answer']
|
||||
newline = cmd.get('newline', True)
|
||||
else:
|
||||
command = cmd
|
||||
prompt = None
|
||||
answer = None
|
||||
newline = True
|
||||
|
||||
results.append(self.send_command(command, prompt, answer, False, newline))
|
||||
return results[1:-1]
|
||||
|
||||
def get(self, command, prompt=None, answer=None, sendonly=False):
|
||||
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
|
||||
def get_options(self):
|
||||
return {
|
||||
'format': ['text'],
|
||||
'match': ['line', 'strict', 'exact', 'none'],
|
||||
'replace': ['line', 'block']
|
||||
}
|
||||
|
||||
def get_capabilities(self):
|
||||
result = {}
|
||||
result['rpc'] = self.get_base_rpc()
|
||||
result = dict()
|
||||
result['rpc'] = self.get_base_rpc() + ['edit_banner']
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
result['device_operations'] = self.get_device_operations()
|
||||
result.update(self.get_options())
|
||||
return json.dumps(result)
|
||||
|
||||
def edit_banner(self, banners, multiline_delimiter="@", check_mode=False):
|
||||
"""
|
||||
Edit banner on remote device
|
||||
:param banners: Banners to be loaded in json format
|
||||
:param multiline_delimiter: Line delimiter for banner
|
||||
:param check_mode: Boolean value that indicates if the device candidate
|
||||
configuration should be pushed in the running configuration or discarded.
|
||||
:return: Returns response of executing the configuration command received
|
||||
from remote host
|
||||
"""
|
||||
banners_obj = json.loads(banners)
|
||||
results = []
|
||||
if not check_mode:
|
||||
for key, value in iteritems(banners_obj):
|
||||
key += ' %s' % multiline_delimiter
|
||||
for cmd in ['config terminal', key, value, multiline_delimiter, 'end']:
|
||||
obj = {'command': cmd, 'sendonly': True}
|
||||
results.append(self.send_command(**obj))
|
||||
|
||||
time.sleep(0.1)
|
||||
results.append(self.send_command('\n'))
|
||||
|
||||
return results[1:-1]
|
||||
|
||||
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 \^C(.+?)(?=\^C)' % 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 \^C(.+?)(?=\^C)' % cmd
|
||||
match = re.search(regex, config, re.S)
|
||||
if match:
|
||||
config = config.replace(str(match.group(1)), '')
|
||||
|
||||
config = re.sub(r'banner \w+ \^C\^C', '!! 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
|
||||
|
|
|
@ -282,6 +282,10 @@ class Connection(ConnectionBase):
|
|||
messages.append('deauthorizing connection')
|
||||
|
||||
self._play_context = play_context
|
||||
|
||||
self.reset_history()
|
||||
self.disable_response_logging()
|
||||
|
||||
return messages
|
||||
|
||||
def _connect(self):
|
||||
|
|
|
@ -20,8 +20,9 @@
|
|||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.compat.tests.mock import patch
|
||||
from ansible.compat.tests.mock import patch, MagicMock
|
||||
from ansible.modules.network.ios import ios_config
|
||||
from ansible.plugins.cliconf.ios import Cliconf
|
||||
from units.modules.utils import set_module_args
|
||||
from .ios_module import TestIosModule, load_fixture
|
||||
|
||||
|
@ -36,31 +37,39 @@ class TestIosConfigModule(TestIosModule):
|
|||
self.mock_get_config = patch('ansible.modules.network.ios.ios_config.get_config')
|
||||
self.get_config = self.mock_get_config.start()
|
||||
|
||||
self.mock_load_config = patch('ansible.modules.network.ios.ios_config.load_config')
|
||||
self.load_config = self.mock_load_config.start()
|
||||
self.mock_get_connection = patch('ansible.modules.network.ios.ios_config.get_connection')
|
||||
self.get_connection = self.mock_get_connection.start()
|
||||
|
||||
self.conn = self.get_connection()
|
||||
self.conn.edit_config = MagicMock()
|
||||
|
||||
self.mock_run_commands = patch('ansible.modules.network.ios.ios_config.run_commands')
|
||||
self.run_commands = self.mock_run_commands.start()
|
||||
|
||||
self.cliconf_obj = Cliconf(MagicMock())
|
||||
self.running_config = load_fixture('ios_config_config.cfg')
|
||||
|
||||
def tearDown(self):
|
||||
super(TestIosConfigModule, self).tearDown()
|
||||
self.mock_get_config.stop()
|
||||
self.mock_load_config.stop()
|
||||
self.mock_run_commands.stop()
|
||||
self.mock_get_connection.stop()
|
||||
|
||||
def load_fixtures(self, commands=None):
|
||||
config_file = 'ios_config_config.cfg'
|
||||
self.get_config.return_value = load_fixture(config_file)
|
||||
self.load_config.return_value = None
|
||||
self.get_connection.edit_config.return_value = None
|
||||
|
||||
def test_ios_config_unchanged(self):
|
||||
src = load_fixture('ios_config_config.cfg')
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, src))
|
||||
set_module_args(dict(src=src))
|
||||
self.execute_module()
|
||||
|
||||
def test_ios_config_src(self):
|
||||
src = load_fixture('ios_config_src.cfg')
|
||||
set_module_args(dict(src=src))
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, self.running_config))
|
||||
commands = ['hostname foo', 'interface GigabitEthernet0/0',
|
||||
'no ip address']
|
||||
self.execute_module(changed=True, commands=commands)
|
||||
|
@ -76,7 +85,7 @@ class TestIosConfigModule(TestIosModule):
|
|||
self.execute_module(changed=True)
|
||||
self.assertEqual(self.run_commands.call_count, 1)
|
||||
self.assertEqual(self.get_config.call_count, 0)
|
||||
self.assertEqual(self.load_config.call_count, 0)
|
||||
self.assertEqual(self.conn.edit_config.call_count, 0)
|
||||
args = self.run_commands.call_args[0][1]
|
||||
self.assertIn('copy running-config startup-config\r', args)
|
||||
|
||||
|
@ -84,10 +93,11 @@ class TestIosConfigModule(TestIosModule):
|
|||
src = load_fixture('ios_config_src.cfg')
|
||||
set_module_args(dict(src=src, save_when='changed'))
|
||||
commands = ['hostname foo', 'interface GigabitEthernet0/0', 'no ip address']
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, self.running_config))
|
||||
self.execute_module(changed=True, commands=commands)
|
||||
self.assertEqual(self.run_commands.call_count, 1)
|
||||
self.assertEqual(self.get_config.call_count, 1)
|
||||
self.assertEqual(self.load_config.call_count, 1)
|
||||
self.assertEqual(self.conn.edit_config.call_count, 1)
|
||||
args = self.run_commands.call_args[0][1]
|
||||
self.assertIn('copy running-config startup-config\r', args)
|
||||
|
||||
|
@ -96,7 +106,7 @@ class TestIosConfigModule(TestIosModule):
|
|||
self.execute_module(changed=False)
|
||||
self.assertEqual(self.run_commands.call_count, 0)
|
||||
self.assertEqual(self.get_config.call_count, 0)
|
||||
self.assertEqual(self.load_config.call_count, 0)
|
||||
self.assertEqual(self.conn.edit_config.call_count, 0)
|
||||
|
||||
def test_ios_config_save(self):
|
||||
self.run_commands.return_value = "hostname foo"
|
||||
|
@ -104,39 +114,57 @@ class TestIosConfigModule(TestIosModule):
|
|||
self.execute_module(changed=True)
|
||||
self.assertEqual(self.run_commands.call_count, 1)
|
||||
self.assertEqual(self.get_config.call_count, 0)
|
||||
self.assertEqual(self.load_config.call_count, 0)
|
||||
self.assertEqual(self.conn.edit_config.call_count, 0)
|
||||
args = self.run_commands.call_args[0][1]
|
||||
self.assertIn('copy running-config startup-config\r', args)
|
||||
|
||||
def test_ios_config_lines_wo_parents(self):
|
||||
set_module_args(dict(lines=['hostname foo']))
|
||||
lines = ['hostname foo']
|
||||
set_module_args(dict(lines=lines))
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
|
||||
commands = ['hostname foo']
|
||||
self.execute_module(changed=True, commands=commands)
|
||||
|
||||
def test_ios_config_lines_w_parents(self):
|
||||
set_module_args(dict(lines=['shutdown'], parents=['interface GigabitEthernet0/0']))
|
||||
lines = ['shutdown']
|
||||
parents = ['interface GigabitEthernet0/0']
|
||||
set_module_args(dict(lines=lines, parents=parents))
|
||||
module = MagicMock()
|
||||
module.params = {'lines': lines, 'parents': parents, 'src': None}
|
||||
candidate_config = ios_config.get_candidate_config(module)
|
||||
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config))
|
||||
|
||||
commands = ['interface GigabitEthernet0/0', 'shutdown']
|
||||
self.execute_module(changed=True, commands=commands)
|
||||
|
||||
def test_ios_config_before(self):
|
||||
set_module_args(dict(lines=['hostname foo'], before=['test1', 'test2']))
|
||||
lines = ['hostname foo']
|
||||
set_module_args(dict(lines=lines, before=['test1', 'test2']))
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
|
||||
commands = ['test1', 'test2', 'hostname foo']
|
||||
self.execute_module(changed=True, commands=commands, sort=False)
|
||||
|
||||
def test_ios_config_after(self):
|
||||
set_module_args(dict(lines=['hostname foo'], after=['test1', 'test2']))
|
||||
lines = ['hostname foo']
|
||||
set_module_args(dict(lines=lines, after=['test1', 'test2']))
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
|
||||
commands = ['hostname foo', 'test1', 'test2']
|
||||
self.execute_module(changed=True, commands=commands, sort=False)
|
||||
|
||||
def test_ios_config_before_after_no_change(self):
|
||||
set_module_args(dict(lines=['hostname router'],
|
||||
lines = ['hostname router']
|
||||
set_module_args(dict(lines=lines,
|
||||
before=['test1', 'test2'],
|
||||
after=['test3', 'test4']))
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
|
||||
self.execute_module()
|
||||
|
||||
def test_ios_config_config(self):
|
||||
config = 'hostname localhost'
|
||||
set_module_args(dict(lines=['hostname router'], config=config))
|
||||
lines = ['hostname router']
|
||||
set_module_args(dict(lines=lines, config=config))
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), config))
|
||||
commands = ['hostname router']
|
||||
self.execute_module(changed=True, commands=commands)
|
||||
|
||||
|
@ -144,18 +172,32 @@ class TestIosConfigModule(TestIosModule):
|
|||
lines = ['description test string', 'test string']
|
||||
parents = ['interface GigabitEthernet0/0']
|
||||
set_module_args(dict(lines=lines, replace='block', parents=parents))
|
||||
|
||||
module = MagicMock()
|
||||
module.params = {'lines': lines, 'parents': parents, 'src': None}
|
||||
candidate_config = ios_config.get_candidate_config(module)
|
||||
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, replace='block', path=parents))
|
||||
|
||||
commands = parents + lines
|
||||
self.execute_module(changed=True, commands=commands)
|
||||
|
||||
def test_ios_config_match_none(self):
|
||||
lines = ['hostname router']
|
||||
set_module_args(dict(lines=lines, match='none'))
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config, match='none'))
|
||||
self.execute_module(changed=True, commands=lines)
|
||||
|
||||
def test_ios_config_match_none(self):
|
||||
lines = ['ip address 1.2.3.4 255.255.255.0', 'description test string']
|
||||
parents = ['interface GigabitEthernet0/0']
|
||||
set_module_args(dict(lines=lines, parents=parents, match='none'))
|
||||
|
||||
module = MagicMock()
|
||||
module.params = {'lines': lines, 'parents': parents, 'src': None}
|
||||
candidate_config = ios_config.get_candidate_config(module)
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, match='none', path=parents))
|
||||
|
||||
commands = parents + lines
|
||||
self.execute_module(changed=True, commands=commands, sort=False)
|
||||
|
||||
|
@ -164,6 +206,12 @@ class TestIosConfigModule(TestIosModule):
|
|||
'shutdown']
|
||||
parents = ['interface GigabitEthernet0/0']
|
||||
set_module_args(dict(lines=lines, parents=parents, match='strict'))
|
||||
|
||||
module = MagicMock()
|
||||
module.params = {'lines': lines, 'parents': parents, 'src': None}
|
||||
candidate_config = ios_config.get_candidate_config(module)
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, match='strict', path=parents))
|
||||
|
||||
commands = parents + ['shutdown']
|
||||
self.execute_module(changed=True, commands=commands, sort=False)
|
||||
|
||||
|
@ -172,6 +220,12 @@ class TestIosConfigModule(TestIosModule):
|
|||
'shutdown']
|
||||
parents = ['interface GigabitEthernet0/0']
|
||||
set_module_args(dict(lines=lines, parents=parents, match='exact'))
|
||||
|
||||
module = MagicMock()
|
||||
module.params = {'lines': lines, 'parents': parents, 'src': None}
|
||||
candidate_config = ios_config.get_candidate_config(module)
|
||||
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, match='exact', path=parents))
|
||||
|
||||
commands = parents + lines
|
||||
self.execute_module(changed=True, commands=commands, sort=False)
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ class TestPluginCLIConfSLXOS(unittest.TestCase):
|
|||
self._mock_connection = MagicMock()
|
||||
self._mock_connection.send.side_effect = _connection_side_effect
|
||||
self._cliconf = slxos.Cliconf(self._mock_connection)
|
||||
self.maxDiff = None
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
@ -125,7 +126,9 @@ class TestPluginCLIConfSLXOS(unittest.TestCase):
|
|||
'get_config',
|
||||
'edit_config',
|
||||
'get_capabilities',
|
||||
'get'
|
||||
'get',
|
||||
'enable_response_logging',
|
||||
'disable_response_logging'
|
||||
],
|
||||
'device_info': {
|
||||
'network_os_model': 'BR-SLX9140',
|
||||
|
|
Loading…
Reference in a new issue