1
0
Fork 0
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:
Ganesh Nalawade 2018-06-06 11:12:45 +05:30 committed by GitHub
parent 9abc3dbec4
commit ba4b12358c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 473 additions and 182 deletions

View file

@ -135,10 +135,12 @@ def dumps(objects, output='block', comments=False):
items = _obj_to_block(objects) items = _obj_to_block(objects)
elif output == 'commands': elif output == 'commands':
items = _obj_to_text(objects) items = _obj_to_text(objects)
elif output == 'raw':
items = _obj_to_raw(objects)
else: else:
raise TypeError('unknown value supplied for keyword output') raise TypeError('unknown value supplied for keyword output')
if output != 'commands': if output == 'block':
if comments: if comments:
for index, item in enumerate(items): for index, item in enumerate(items):
nextitem = index + 1 nextitem = index + 1

View file

@ -114,7 +114,7 @@ def get_config(module, flags=None):
return _DEVICE_CONFIGS[flag_str] return _DEVICE_CONFIGS[flag_str]
except KeyError: except KeyError:
connection = get_connection(module) 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() cfg = to_text(out, errors='surrogate_then_replace').strip()
_DEVICE_CONFIGS[flag_str] = cfg _DEVICE_CONFIGS[flag_str] = cfg
return cfg return cfg

View file

@ -291,17 +291,14 @@ backup_path:
type: string type: string
sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34 sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34
""" """
import re import json
import time
from ansible.module_utils.network.ios.ios import run_commands, get_config, load_config from ansible.module_utils.network.ios.ios import run_commands, get_config
from ansible.module_utils.network.ios.ios import get_defaults_flag 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 ios_argument_spec
from ansible.module_utils.network.ios.ios import check_args as ios_check_args 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.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.network.common.config import NetworkConfig, dumps
from ansible.module_utils.six import iteritems
def check_args(module, warnings): def check_args(module, warnings):
@ -312,70 +309,29 @@ def check_args(module, warnings):
'single character') 'single character')
def extract_banners(config): def get_candidate_config(module):
banners = {} candidate = ''
banner_cmds = re.findall(r'^banner (\w+)', config, re.M) if module.params['src']:
for cmd in banner_cmds: candidate = module.params['src']
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: elif module.params['lines']:
regex = r'banner %s \^C(.+?)(?=\^C)' % cmd candidate_obj = NetworkConfig(indent=1)
match = re.search(regex, config, re.S) parents = module.params['parents'] or list()
if match: candidate_obj.add(module.params['lines'], parents=parents)
config = config.replace(str(match.group(1)), '') 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 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): def get_running_config(module, current_config=None, flags=None):
contents = module.params['running_config'] running = module.params['running_config']
if not running:
if not contents:
if not module.params['defaults'] and current_config: if not module.params['defaults'] and current_config:
contents, banners = extract_banners(current_config.config_text) running = current_config
else: else:
contents = get_config(module, flags=flags) running = get_config(module, flags=flags)
contents, banners = extract_banners(contents)
return NetworkConfig(indent=1, contents=contents), banners
return running
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
def save_config(module, result): def save_config(module, result):
@ -445,7 +401,9 @@ def main():
result['warnings'] = warnings result['warnings'] = warnings
config = None config = None
contents = None
flags = get_defaults_flag(module) if module.params['defaults'] else [] 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'): if module.params['backup'] or (module._diff and module.params['diff_against'] == 'running'):
contents = get_config(module, flags=flags) contents = get_config(module, flags=flags)
@ -458,20 +416,16 @@ def main():
replace = module.params['replace'] replace = module.params['replace']
path = module.params['parents'] 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': response = connection.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=None, path=path, replace=replace)
config, have_banners = get_running_config(module, config, flags=flags) diff = json.loads(response)
path = module.params['parents'] config_diff = diff['config_diff']
configobjs = candidate.difference(config, path=path, match=match, replace=replace) banner_diff = diff['banner_diff']
else:
configobjs = candidate.items
have_banners = {}
banners = diff_banners(want_banners, have_banners) if config_diff or banner_diff:
commands = config_diff.split('\n')
if configobjs or banners:
commands = dumps(configobjs, 'commands').split('\n')
if module.params['before']: if module.params['before']:
commands[:0] = module.params['before'] commands[:0] = module.params['before']
@ -481,15 +435,15 @@ def main():
result['commands'] = commands result['commands'] = commands
result['updates'] = commands result['updates'] = commands
result['banners'] = banners result['banners'] = banner_diff
# send the configuration commands to the device and merge # send the configuration commands to the device and merge
# them with the current running config # them with the current running config
if not module.check_mode: if not module.check_mode:
if commands: if commands:
load_config(module, commands) connection.edit_config(commands)
if banners: if banner_diff:
load_banners(module, banners) connection.edit_banner(json.dumps(banner_diff), multiline_delimiter=module.params['multiline_delimiter'])
result['changed'] = True result['changed'] = True
@ -556,6 +510,5 @@ def main():
module.exit_json(**result) module.exit_json(**result)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -19,8 +19,6 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import signal
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from functools import wraps from functools import wraps
@ -83,8 +81,12 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
conn.edit_config(['hostname test', 'netconf ssh']) 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): def __init__(self, connection):
self._connection = connection self._connection = connection
self.history = list()
self.response_logging = False
def _alarm_handler(self, signum, frame): def _alarm_handler(self, signum, frame):
"""Alarm handler raised in case of command timeout """ """Alarm handler raised in case of command timeout """
@ -92,94 +94,208 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
self.close() self.close()
def send_command(self, command, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False): 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 """Executes a command over the device connection
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 This method will execute a command over the device connection and
string 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, kwargs = {
'newline': newline, 'prompt_retry_check': prompt_retry_check} 'command': to_bytes(command),
'sendonly': sendonly,
'newline': newline,
'prompt_retry_check': prompt_retry_check
}
if prompt is not None: if prompt is not None:
kwargs['prompt'] = to_bytes(prompt) kwargs['prompt'] = to_bytes(prompt)
if answer is not None: if answer is not None:
kwargs['answer'] = to_bytes(answer) kwargs['answer'] = to_bytes(answer)
resp = self._connection.send(**kwargs) resp = self._connection.send(**kwargs)
if not self.response_logging:
self.history.append(('*****', '*****'))
else:
self.history.append((kwargs['command'], resp))
return resp return resp
def get_base_rpc(self): def get_base_rpc(self):
"""Returns list of base rpc method supported by remote device""" """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 @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 """Retrieves the specified configuration from the device
This method will retrieve the configuration specified by source and This method will retrieve the configuration specified by source and
return it to the caller as a string. Subsequent calls to this method return it to the caller as a string. Subsequent calls to this method
will retrieve a new configuration from the device will retrieve a new configuration from the device
:args:
arg[0] source: Datastore from which configuration should be retrieved eg: running/candidate/startup. (optional) :param source: The configuration source to return from the device.
default is running. This argument accepts either `running` or `startup` as valid values.
arg[1] format: Output format in which configuration is retrieved
Note: Specified datastore should be supported by remote device. :param filter: For devices that support configuration filtering, this
:kwargs: keyword argument is used to filter the returned configuration.
Keywords supported The use of this keyword argument is device dependent adn will be
:command: the command string to execute silently ignored on devices that do not support it.
:source: Datastore from which configuration should be retrieved
:format: Output format in which configuration is retrieved :param format: For devices that support fetching different configuration
:returns: Returns output received from remote device as byte string 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 pass
@abstractmethod @abstractmethod
def edit_config(self, commands=None): def edit_config(self, candidate, check_mode=False, replace=None):
"""Loads the specified commands into the remote device """Loads the candidate configuration into the network device
This method will load the commands into the remote device. This
method will make sure the device is in the proper context before This method will load the specified candidate config into the device
send the commands (eg config mode) and merge with the current configuration unless replace is set to
:args: True. If the device does not support config replace an errors
arg[0] command: List of configuration commands is returned.
:kwargs:
Keywords supported :param candidate: The configuration to load into the device and merge
:command: the command string to execute with the current running configuration
:returns: Returns output received from remote device as byte string
: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 pass
@abstractmethod @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 """Execute specified command on remote device
This method will retrieve the specified data and This method will retrieve the specified data and
return it to the caller as a string. return it to the caller as a string.
:args: :param command: command in string format to be executed on remote device
command: command in string format to be executed on remote device :param prompt: the expected prompt generated by executing command, this can
prompt: the expected prompt generated by executing command. be a string or a list of strings
This can be a string or a list of strings (optional) :param answer: the string to respond to the prompt with
answer: the string to respond to the prompt with (optional) :param sendonly: bool to disable waiting for response, default is false
sendonly: bool to disable waiting for response, default is false (optional) :param newline: bool to indicate if newline should be added at end of answer or not
:returns: Returns output received from remote device as byte string :return:
""" """
pass pass
@abstractmethod @abstractmethod
def get_capabilities(self): def get_capabilities(self):
"""Retrieves device information and supported """Returns the basic capabilities of the network device
rpc methods by device platform and return result This method will provide some basic facts about the device and
as a string what capabilities it has to modify the configuration. The minimum
:returns: Returns output received from remote device as byte string 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 pass
def commit(self, comment=None): 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) return self._connection.method_not_found("commit is not supported by network_os %s" % self._play_context.network_os)
def discard_changes(self): 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) 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): 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() ssh = self._connection.paramiko_conn._connect_uncached()
if proto == 'scp': if proto == 'scp':
if not HAS_SCP: if not HAS_SCP:
@ -191,6 +307,15 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
sftp.put(source, destination) sftp.put(source, destination)
def get_file(self, source=None, destination=None, proto='scp', timeout=30): 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""" """Fetch file over scp/sftp from remote device"""
ssh = self._connection.paramiko_conn._connect_uncached() ssh = self._connection.paramiko_conn._connect_uncached()
if proto == 'scp': if proto == 'scp':

View file

@ -19,18 +19,134 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import collections
import re import re
import time
import json import json
from itertools import chain from itertools import chain
from ansible.module_utils._text import to_text 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.module_utils.network.common.utils import to_list
from ansible.plugins.cliconf import CliconfBase, enable_mode from ansible.plugins.cliconf import CliconfBase, enable_mode
class Cliconf(CliconfBase): 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): def get_device_info(self):
device_info = {} device_info = {}
@ -52,48 +168,82 @@ class Cliconf(CliconfBase):
return device_info return device_info
@enable_mode def get_device_operations(self):
def get_config(self, source='running', format='text', flags=None): return {
if source not in ('running', 'startup'): 'supports_replace': True,
return self.invalid_params("fetching configuration from %s is not supported" % source) '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: def get_options(self):
flags = [] return {
'format': ['text'],
if source == 'running': 'match': ['line', 'strict', 'exact', 'none'],
cmd = 'show running-config ' 'replace': ['line', 'block']
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_capabilities(self): def get_capabilities(self):
result = {} result = dict()
result['rpc'] = self.get_base_rpc() result['rpc'] = self.get_base_rpc() + ['edit_banner']
result['network_api'] = 'cliconf' result['network_api'] = 'cliconf'
result['device_info'] = self.get_device_info() result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations()
result.update(self.get_options())
return json.dumps(result) 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

View file

@ -282,6 +282,10 @@ class Connection(ConnectionBase):
messages.append('deauthorizing connection') messages.append('deauthorizing connection')
self._play_context = play_context self._play_context = play_context
self.reset_history()
self.disable_response_logging()
return messages return messages
def _connect(self): def _connect(self):

View file

@ -20,8 +20,9 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __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.modules.network.ios import ios_config
from ansible.plugins.cliconf.ios import Cliconf
from units.modules.utils import set_module_args from units.modules.utils import set_module_args
from .ios_module import TestIosModule, load_fixture 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.mock_get_config = patch('ansible.modules.network.ios.ios_config.get_config')
self.get_config = self.mock_get_config.start() self.get_config = self.mock_get_config.start()
self.mock_load_config = patch('ansible.modules.network.ios.ios_config.load_config') self.mock_get_connection = patch('ansible.modules.network.ios.ios_config.get_connection')
self.load_config = self.mock_load_config.start() 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.mock_run_commands = patch('ansible.modules.network.ios.ios_config.run_commands')
self.run_commands = self.mock_run_commands.start() 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): def tearDown(self):
super(TestIosConfigModule, self).tearDown() super(TestIosConfigModule, self).tearDown()
self.mock_get_config.stop() self.mock_get_config.stop()
self.mock_load_config.stop()
self.mock_run_commands.stop() self.mock_run_commands.stop()
self.mock_get_connection.stop()
def load_fixtures(self, commands=None): def load_fixtures(self, commands=None):
config_file = 'ios_config_config.cfg' config_file = 'ios_config_config.cfg'
self.get_config.return_value = load_fixture(config_file) 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): def test_ios_config_unchanged(self):
src = load_fixture('ios_config_config.cfg') 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)) set_module_args(dict(src=src))
self.execute_module() self.execute_module()
def test_ios_config_src(self): def test_ios_config_src(self):
src = load_fixture('ios_config_src.cfg') src = load_fixture('ios_config_src.cfg')
set_module_args(dict(src=src)) 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', commands = ['hostname foo', 'interface GigabitEthernet0/0',
'no ip address'] 'no ip address']
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
@ -76,7 +85,7 @@ class TestIosConfigModule(TestIosModule):
self.execute_module(changed=True) self.execute_module(changed=True)
self.assertEqual(self.run_commands.call_count, 1) self.assertEqual(self.run_commands.call_count, 1)
self.assertEqual(self.get_config.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)
args = self.run_commands.call_args[0][1] args = self.run_commands.call_args[0][1]
self.assertIn('copy running-config startup-config\r', args) self.assertIn('copy running-config startup-config\r', args)
@ -84,10 +93,11 @@ class TestIosConfigModule(TestIosModule):
src = load_fixture('ios_config_src.cfg') src = load_fixture('ios_config_src.cfg')
set_module_args(dict(src=src, save_when='changed')) set_module_args(dict(src=src, save_when='changed'))
commands = ['hostname foo', 'interface GigabitEthernet0/0', 'no ip address'] 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.execute_module(changed=True, commands=commands)
self.assertEqual(self.run_commands.call_count, 1) self.assertEqual(self.run_commands.call_count, 1)
self.assertEqual(self.get_config.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] args = self.run_commands.call_args[0][1]
self.assertIn('copy running-config startup-config\r', args) self.assertIn('copy running-config startup-config\r', args)
@ -96,7 +106,7 @@ class TestIosConfigModule(TestIosModule):
self.execute_module(changed=False) self.execute_module(changed=False)
self.assertEqual(self.run_commands.call_count, 0) self.assertEqual(self.run_commands.call_count, 0)
self.assertEqual(self.get_config.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): def test_ios_config_save(self):
self.run_commands.return_value = "hostname foo" self.run_commands.return_value = "hostname foo"
@ -104,39 +114,57 @@ class TestIosConfigModule(TestIosModule):
self.execute_module(changed=True) self.execute_module(changed=True)
self.assertEqual(self.run_commands.call_count, 1) self.assertEqual(self.run_commands.call_count, 1)
self.assertEqual(self.get_config.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)
args = self.run_commands.call_args[0][1] args = self.run_commands.call_args[0][1]
self.assertIn('copy running-config startup-config\r', args) self.assertIn('copy running-config startup-config\r', args)
def test_ios_config_lines_wo_parents(self): 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'] commands = ['hostname foo']
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
def test_ios_config_lines_w_parents(self): 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'] commands = ['interface GigabitEthernet0/0', 'shutdown']
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
def test_ios_config_before(self): 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'] commands = ['test1', 'test2', 'hostname foo']
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)
def test_ios_config_after(self): 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'] commands = ['hostname foo', 'test1', 'test2']
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)
def test_ios_config_before_after_no_change(self): 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'], before=['test1', 'test2'],
after=['test3', 'test4'])) after=['test3', 'test4']))
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
self.execute_module() self.execute_module()
def test_ios_config_config(self): def test_ios_config_config(self):
config = 'hostname localhost' 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'] commands = ['hostname router']
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
@ -144,18 +172,32 @@ class TestIosConfigModule(TestIosModule):
lines = ['description test string', 'test string'] lines = ['description test string', 'test string']
parents = ['interface GigabitEthernet0/0'] parents = ['interface GigabitEthernet0/0']
set_module_args(dict(lines=lines, replace='block', parents=parents)) 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 commands = parents + lines
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
def test_ios_config_match_none(self): def test_ios_config_match_none(self):
lines = ['hostname router'] lines = ['hostname router']
set_module_args(dict(lines=lines, match='none')) 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) self.execute_module(changed=True, commands=lines)
def test_ios_config_match_none(self): def test_ios_config_match_none(self):
lines = ['ip address 1.2.3.4 255.255.255.0', 'description test string'] lines = ['ip address 1.2.3.4 255.255.255.0', 'description test string']
parents = ['interface GigabitEthernet0/0'] parents = ['interface GigabitEthernet0/0']
set_module_args(dict(lines=lines, parents=parents, match='none')) 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 commands = parents + lines
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)
@ -164,6 +206,12 @@ class TestIosConfigModule(TestIosModule):
'shutdown'] 'shutdown']
parents = ['interface GigabitEthernet0/0'] parents = ['interface GigabitEthernet0/0']
set_module_args(dict(lines=lines, parents=parents, match='strict')) 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'] commands = parents + ['shutdown']
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)
@ -172,6 +220,12 @@ class TestIosConfigModule(TestIosModule):
'shutdown'] 'shutdown']
parents = ['interface GigabitEthernet0/0'] parents = ['interface GigabitEthernet0/0']
set_module_args(dict(lines=lines, parents=parents, match='exact')) 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 commands = parents + lines
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)

View file

@ -62,6 +62,7 @@ class TestPluginCLIConfSLXOS(unittest.TestCase):
self._mock_connection = MagicMock() self._mock_connection = MagicMock()
self._mock_connection.send.side_effect = _connection_side_effect self._mock_connection.send.side_effect = _connection_side_effect
self._cliconf = slxos.Cliconf(self._mock_connection) self._cliconf = slxos.Cliconf(self._mock_connection)
self.maxDiff = None
def tearDown(self): def tearDown(self):
pass pass
@ -125,7 +126,9 @@ class TestPluginCLIConfSLXOS(unittest.TestCase):
'get_config', 'get_config',
'edit_config', 'edit_config',
'get_capabilities', 'get_capabilities',
'get' 'get',
'enable_response_logging',
'disable_response_logging'
], ],
'device_info': { 'device_info': {
'network_os_model': 'BR-SLX9140', 'network_os_model': 'BR-SLX9140',