adds more intelligent save logic and diff to network config modules (#26565)

* adds more intelligent save logic and diff to network config modules

* adds sha1 property to NetworkConfig
* adds new argument save_when to argument_spec
* adds new argument diff_against to argument_spec
* adds new argument intended_config to argument_spec
* renames config argument to running_config with alias to config
* deprecates the use of the save argument
* before and after now work with src argument
* misc module clean

Modules updated
* nxos_config
* ios_config
* eos_config

Most notably this makes the save mechanism more intelligent for config
modules for devices that need to copy the ephemeral config to
non-volatile storage.

The diff_against argument allows the playbook task to control what the
device's running-config is diff'ed against. By default it will return
the diff of the startup-config.

* removes ios_config from pep8/legacy_files.txt

* extends the ignore lines argument to the module

* clean up CI errors

* add missing list brackets

* fixes typo

* fixes unit test cases

* remove last line break when returning config contents

* encode config string to bytes before hashing

* fix typo

* addresses feedback in PR

* update unit test cases
pull/26665/head
Peter Sprygada 7 years ago committed by GitHub
parent dc4037e5a7
commit 0b6f0e6c0d

@ -26,12 +26,20 @@
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# #
import re import re
import hashlib
from ansible.module_utils.six.moves import zip from ansible.module_utils.six.moves import zip
from ansible.module_utils._text import to_bytes
from ansible.module_utils.network_common import to_list from ansible.module_utils.network_common import to_list
DEFAULT_COMMENT_TOKENS = ['#', '!', '/*', '*/', 'echo'] DEFAULT_COMMENT_TOKENS = ['#', '!', '/*', '*/', 'echo']
DEFAULT_IGNORE_LINES_RE = set([
re.compile("Using \d+ out of \d+ bytes"),
re.compile("Building configuration"),
re.compile("Current configuration : \d+ bytes")
])
class ConfigLine(object): class ConfigLine(object):
@ -97,6 +105,9 @@ def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS): for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item): if text.startswith(item):
return True return True
for regex in DEFAULT_IGNORE_LINES_RE:
if regex.match(text):
return True
def _obj_to_text(x): def _obj_to_text(x):
@ -141,9 +152,16 @@ def dumps(objects, output='block', comments=False):
class NetworkConfig(object): class NetworkConfig(object):
def __init__(self, indent=1, contents=None): def __init__(self, indent=1, contents=None, ignore_lines=None):
self._indent = indent self._indent = indent
self._items = list() self._items = list()
self._config_text = None
if ignore_lines:
for item in ignore_lines:
if not isinstance(item, re._pattern_type):
item = re.compile(item)
DEFAULT_IGNORE_LINES_RE.add(item)
if contents: if contents:
self.load(contents) self.load(contents)
@ -152,6 +170,16 @@ class NetworkConfig(object):
def items(self): def items(self):
return self._items return self._items
@property
def config_text(self):
return self._config_text
@property
def sha1(self):
sha1 = hashlib.sha1()
sha1.update(to_bytes(str(self), errors='surrogate_or_strict'))
return sha1.digest()
def __getitem__(self, key): def __getitem__(self, key):
for line in self: for line in self:
if line.text == key: if line.text == key:
@ -168,6 +196,7 @@ class NetworkConfig(object):
return len(self._items) return len(self._items)
def load(self, s): def load(self, s):
self._config_text = s
self._items = self.parse(s) self._items = self.parse(s)
def loadfp(self, fp): def loadfp(self, fp):

@ -30,7 +30,7 @@ short_description: Manage Arista EOS configuration sections
description: description:
- Arista EOS configurations use a simple block indent file syntax - Arista EOS configurations use a simple block indent file syntax
for segmenting configuration into sections. This module provides for segmenting configuration into sections. This module provides
an implementation for working with eos configuration sections in an implementation for working with EOS configuration sections in
a deterministic way. This module works with either CLI or eAPI a deterministic way. This module works with either CLI or eAPI
transports. transports.
extends_documentation_fragment: eos extends_documentation_fragment: eos
@ -115,7 +115,7 @@ options:
will be removed in a future release. will be removed in a future release.
required: false required: false
default: false default: false
choices: ['yes', 'no'] type: bool
backup: backup:
description: description:
- This argument will cause the module to create a full backup of - This argument will cause the module to create a full backup of
@ -125,19 +125,21 @@ options:
exist, it is created. exist, it is created.
required: false required: false
default: no default: no
choices: ['yes', 'no'] type: bool
version_added: "2.2" version_added: "2.2"
config: running_config:
description: description:
- The module, by default, will connect to the remote device and - The module, by default, will connect to the remote device and
retrieve the current running-config to use as a base for comparing retrieve the current running-config to use as a base for comparing
against the contents of source. There are times when it is not against the contents of source. There are times when it is not
desirable to have the task get the current running-config for desirable to have the task get the current running-config for
every task in a playbook. The I(config) argument allows the every task in a playbook. The I(running_config) argument allows the
implementer to pass in the configuration to use as the base implementer to pass in the configuration to use as the base
config for comparison. config for this module.
required: false required: false
default: null default: null
aliases: ['config']
version_added: "2.4"
defaults: defaults:
description: description:
- The I(defaults) argument will influence how the running-config - The I(defaults) argument will influence how the running-config
@ -147,6 +149,7 @@ options:
is issued without the all keyword is issued without the all keyword
required: false required: false
default: false default: false
type: bool
version_added: "2.2" version_added: "2.2"
save: save:
description: description:
@ -156,27 +159,78 @@ options:
no changes are made, the configuration is still saved to the no changes are made, the configuration is still saved to the
startup config. This option will always cause the module to startup config. This option will always cause the module to
return changed. return changed.
- This option is deprecated as of Ansible 2.4, use C(save_when)
required: false required: false
default: false default: false
type: bool
version_added: "2.2" version_added: "2.2"
save_when:
description:
- When changes are made to the device running-configuration, the
changes are not copied to non-volatile storage by default. Using
this argument will change that before. If the argument is set to
I(always), then the running-config will always be copied to the
startup-config and the I(changed) flag will always be set to
True. If the argument is set to I(changed), then the running-config
will only be copied to the startup-config if it has changed since
the last save to startup-config. If the argument is set to
I(never), the running-config will never be copied to the the
startup-config
required: false
default: never
choices: ['always', 'never', 'changed']
version_added: "2.4"
diff_against:
description:
- When using the C(ansible-playbook --diff) command line argument the i
module can generate diffs against different sources.
- When this option is configure as I(startup), the module will return
the diff of the running-config against the startup-config.
- When this option is configured as I(intended), the module will
return the diff of the running-config against the configuration
provided in the C(intended_config) argument.
- When this option is configured as I(running), the module will
return the before and after diff of the running-config with respect
to any changes made to the device configuration.
- When this option is configured as C(session), the diff returned will
be based on the configuration session.
required: false
default: session
choices: ['startup', 'running', 'intended', 'session']
version_added: "2.4"
diff_ignore_lines:
description:
- 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.
required: false
version_added: "2.4"
intended_config:
description:
- The C(intended_config) provides the master configuration that
the node should conform to and is used to check the final
running-config against. This argument will not modify any settings
on the remote device and is strictly used to check the compliance
of the current device's configuration against. When specifying this
argument, the task should also modify the C(diff_against) value and
set it to I(intended).
required: false
version_added: "2.4"
""" """
EXAMPLES = """ EXAMPLES = """
- eos_config: - name: configure top level settings
eos_config:
lines: hostname {{ inventory_hostname }} lines: hostname {{ inventory_hostname }}
- eos_config: - name: diff against a provided master config
lines: eos_config:
- 10 permit ip 1.1.1.1/32 any log diff_against: config
- 20 permit ip 2.2.2.2/32 any log config: "{{ lookup('file', 'master.cfg') }}"
- 30 permit ip 3.3.3.3/32 any log
- 40 permit ip 4.4.4.4/32 any log
- 50 permit ip 5.5.5.5/32 any log
parents: ip access-list test
before: no ip access-list test
match: exact
- eos_config: - name: load an acl into the device
eos_config:
lines: lines:
- 10 permit ip 1.1.1.1/32 any log - 10 permit ip 1.1.1.1/32 any log
- 20 permit ip 2.2.2.2/32 any log - 20 permit ip 2.2.2.2/32 any log
@ -189,12 +243,22 @@ EXAMPLES = """
- name: load configuration from file - name: load configuration from file
eos_config: eos_config:
src: eos.cfg src: eos.cfg
- name: diff the running config against a master config
eos_config:
diff_against: intended
intended_config: "{{ lookup('file', 'master.cfg') }}"
""" """
RETURN = """ RETURN = """
commands: commands:
description: The set of commands that will be pushed to the remote device description: The set of commands that will be pushed to the remote device
returned: Only when lines is specified. returned: always
type: list
sample: ['hostname switch01', 'interface Ethernet1', 'no shutdown']
updates:
description: The set of commands that will be pushed to the remote device
returned: always
type: list type: list
sample: ['hostname switch01', 'interface Ethernet1', 'no shutdown'] sample: ['hostname switch01', 'interface Ethernet1', 'no shutdown']
backup_path: backup_path:
@ -208,14 +272,8 @@ from ansible.module_utils.netcfg import NetworkConfig, dumps
from ansible.module_utils.eos import get_config, load_config from ansible.module_utils.eos import get_config, load_config
from ansible.module_utils.eos import run_commands from ansible.module_utils.eos import run_commands
from ansible.module_utils.eos import eos_argument_spec from ansible.module_utils.eos import eos_argument_spec
from ansible.module_utils.eos import check_args as eos_check_args from ansible.module_utils.eos import check_args
def check_args(module, warnings):
eos_check_args(module, warnings)
if module.params['force']:
warnings.append('The force argument is deprecated, please use '
'match=none instead. This argument will be '
'removed in the future')
def get_candidate(module): def get_candidate(module):
candidate = NetworkConfig(indent=3) candidate = NetworkConfig(indent=3)
@ -226,51 +284,17 @@ def get_candidate(module):
candidate.add(module.params['lines'], parents=parents) candidate.add(module.params['lines'], parents=parents)
return candidate return candidate
def get_running_config(module):
flags = []
if module.params['defaults'] is True:
flags.append('all')
return get_config(module, flags)
def run(module, result):
match = module.params['match']
replace = module.params['replace']
candidate = get_candidate(module)
if match != 'none' and replace != 'config':
config_text = get_running_config(module)
config = NetworkConfig(indent=3, contents=config_text)
path = module.params['parents']
configobjs = candidate.difference(config, match=match, replace=replace, path=path)
else:
configobjs = candidate.items
if configobjs:
commands = dumps(configobjs, 'commands').split('\n')
if module.params['lines']:
if module.params['before']:
commands[:0] = module.params['before']
if module.params['after']:
commands.extend(module.params['after'])
result['commands'] = commands
result['updates'] = commands
replace = module.params['replace'] == 'config'
commit = not module.check_mode
response = load_config(module, commands, replace=replace, commit=commit)
if 'diff' in response: def get_running_config(module, config=None):
result['diff'] = {'prepared': response['diff']} contents = module.params['running_config']
if not contents:
if not module.params['defaults'] and config:
contents = config
else:
flags = ['all']
contents = get_config(module, flags=flags)
return NetworkConfig(indent=3, contents=contents)
if 'session' in response:
result['session'] = response['session']
result['changed'] = True
def main(): def main():
""" main entry point for module execution """ main entry point for module execution
@ -288,34 +312,39 @@ def main():
replace=dict(default='line', choices=['line', 'block', 'config']), replace=dict(default='line', choices=['line', 'block', 'config']),
defaults=dict(type='bool', default=False), defaults=dict(type='bool', default=False),
backup=dict(type='bool', default=False), backup=dict(type='bool', default=False),
save=dict(default=False, type='bool'),
# deprecated arguments (Ansible 2.3) save_when=dict(choices=['always', 'never', 'changed'], default='never'),
config=dict(),
# this argument is deprecated in favor of setting match: none diff_against=dict(choices=['startup', 'session', 'intended', 'running'], default='session'),
# it will be removed in a future version diff_ignore_lines=dict(type='list'),
force=dict(default=False, type='bool'),
running_config=dict(aliases=['config']),
intended_config=dict(),
# save is deprecated as of ans2.4, use save_when instead
save=dict(default=False, type='bool', removed_in_version='2.4'),
# force argument deprecated in ans2.2
force=dict(default=False, type='bool', removed_in_version='2.2')
) )
argument_spec.update(eos_argument_spec) argument_spec.update(eos_argument_spec)
mutually_exclusive = [('lines', 'src')] mutually_exclusive = [('lines', 'src'),
('save', 'save_when')]
required_if = [('match', 'strict', ['lines']), required_if = [('match', 'strict', ['lines']),
('match', 'exact', ['lines']), ('match', 'exact', ['lines']),
('replace', 'block', ['lines']), ('replace', 'block', ['lines']),
('replace', 'config', ['src'])] ('replace', 'config', ['src']),
('diff_against', 'intended', ['intended_config'])]
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive, mutually_exclusive=mutually_exclusive,
required_if=required_if, required_if=required_if,
supports_check_mode=True) supports_check_mode=True)
if module.params['force'] is True:
module.params['match'] = 'none'
warnings = list() warnings = list()
check_args(module, warnings) check_args(module, warnings)
@ -323,18 +352,110 @@ def main():
if warnings: if warnings:
result['warnings'] = warnings result['warnings'] = warnings
if module.params['backup']: config = None
result['__backup__'] = get_config(module)
if module.params['backup'] or (module._diff and module.params['diff_against'] == 'running'):
contents = get_config(module)
config = NetworkConfig(indent=2, contents=contents)
if module.params['backup']:
result['__backup__'] = contents
if any((module.params['src'], module.params['lines'])): if any((module.params['src'], module.params['lines'])):
run(module, result) match = module.params['match']
replace = module.params['replace']
if module.params['save']:
if not module.check_mode: candidate = get_candidate(module)
response = run_commands(module, ['show running-config diffs'])
if len(response[0]): if match != 'none' and replace != 'config':
run_commands(module, ['copy running-config startup-config']) config_text = get_running_config(module)
result['changed'] = True config = NetworkConfig(indent=3, contents=config_text)
path = module.params['parents']
configobjs = candidate.difference(config, match=match, replace=replace, path=path)
else:
configobjs = candidate.items
if configobjs:
commands = dumps(configobjs, 'commands').split('\n')
if module.params['before']:
commands[:0] = module.params['before']
if module.params['after']:
commands.extend(module.params['after'])
result['commands'] = commands
result['updates'] = commands
replace = module.params['replace'] == 'config'
commit = not module.check_mode
response = load_config(module, commands, replace=replace, commit=commit)
if 'diff' in response and module.params['diff_against'] == 'session':
result['diff'] = {'prepared': response['diff']}
if 'session' in response:
result['session'] = response['session']
result['changed'] = True
running_config = None
startup_config = None
diff_ignore_lines = module.params['diff_ignore_lines']
if module.params['save_when'] != 'never':
output = run_commands(module, ['show running-config', 'show startup-config'])
running_config = NetworkConfig(indent=1, contents=output[0], ignore_lines=diff_ignore_lines)
startup_config = NetworkConfig(indent=1, contents=output[1], ignore_lines=diff_ignore_lines)
if running_config.sha1 != startup_config.sha1 or module.params['save_when'] == 'always':
result['changed'] = True
if not module.check_mode:
cmd = {'command': 'copy running-config startup-config', 'output': 'text'}
run_commands(module, [cmd])
else:
module.warn('Skipping command `copy running-config startup-config` '
'due to check_mode. Configuration not copied to '
'non-volatile storage')
if module._diff:
if not running_config:
output = run_commands(module, 'show running-config')
contents = output[0]
else:
contents = running_config.config_text
# recreate the object in order to process diff_ignore_lines
running_config = NetworkConfig(indent=1, contents=config_text, ignore_lines=diff_ignore_lines)
if module.params['diff_against'] == 'running':
if module.check_mode:
module.warn("unable to perform diff against running-config due to check mode")
contents = None
else:
contents = config.config_text
elif module.params['diff_against'] == 'startup':
if not startup_config:
output = run_commands(module, 'show startup-config')
contents = output[0]
else:
contents = startup_config.config_text
elif module.params['diff_against'] == 'intended':
contents = module.params['intended_config']
if contents is not None:
base_config = NetworkConfig(indent=1, contents=contents, ignore_lines=diff_ignore_lines)
if running_config.sha1 != base_config.sha1:
result.update({
'changed': True,
'diff': {'before': str(base_config), 'after': str(running_config)}
})
module.exit_json(**result) module.exit_json(**result)

@ -123,7 +123,7 @@ options:
will be removed in a future release. will be removed in a future release.
required: false required: false
default: false default: false
choices: ["true", "false"] type: bool
backup: backup:
description: description:
- This argument will cause the module to create a full backup of - This argument will cause the module to create a full backup of
@ -133,17 +133,21 @@ options:
exist, it is created. exist, it is created.
required: false required: false
default: no default: no
choices: ['yes', 'no'] type: bool
version_added: "2.2" version_added: "2.2"
config: running_config:
description: description:
- The C(config) argument allows the playbook designer to supply - The module, by default, will connect to the remote device and
the base configuration to be used to validate configuration retrieve the current running-config to use as a base for comparing
changes necessary. If this argument is provided, the module against the contents of source. There are times when it is not
will not download the running-config from the remote node. desirable to have the task get the current running-config for
every task in a playbook. The I(running_config) argument allows the
implementer to pass in the configuration to use as the base
config for comparison.
required: false required: false
default: null default: null
version_added: "2.2" aliases: ['config']
version_added: "2.4"
defaults: defaults:
description: description:
- This argument specifies whether or not to collect all defaults - This argument specifies whether or not to collect all defaults
@ -152,17 +156,68 @@ options:
C(show running-config all). C(show running-config all).
required: false required: false
default: no default: no
choices: ['yes', 'no'] type: bool
version_added: "2.2" version_added: "2.2"
save: save:
description: description:
- The C(save) argument instructs the module to save the running- - The C(save) argument instructs the module to save the running-
config to the startup-config at the conclusion of the module config to the startup-config at the conclusion of the module
running. If check mode is specified, this argument is ignored. running. If check mode is specified, this argument is ignored.
- This option is deprecated as of Ansible 2.4, use C(save_when)
required: false required: false
default: no default: false
choices: ['yes', 'no'] type: bool
version_added: "2.2" version_added: "2.2"
save_when:
description:
- When changes are made to the device running-configuration, the
changes are not copied to non-volatile storage by default. Using
this argument will change that before. If the argument is set to
I(always), then the running-config will always be copied to the
startup-config and the I(changed) flag will always be set to
True. If the argument is set to I(changed), then the running-config
will only be copied to the startup-config if it has changed since
the last save to startup-config. If the argument is set to
I(never), the running-config will never be copied to the the
startup-config
required: false
default: never
choices: ['always', 'never', 'changed']
version_added: "2.4"
diff_against:
description:
- When using the C(ansible-playbook --diff) command line argument
the module can generate diffs against different sources.
- When this option is configure as I(startup), the module will return
the diff of the running-config against the startup-config.
- When this option is configured as I(intended), the module will
return the diff of the running-config against the configuration
provided in the C(intended_config) argument.
- When this option is configured as I(running), the module will
return the before and after diff of the running-config with respect
to any changes made to the device configuration.
required: false
choices: ['running', 'startup', 'intended']
version_added: "2.4"
diff_ignore_lines:
description:
- 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.
required: false
version_added: "2.4"
intended_config:
description:
- The C(intended_config) provides the master configuration that
the node should conform to and is used to check the final
running-config against. This argument will not modify any settings
on the remote device and is strictly used to check the compliance
of the current device's configuration against. When specifying this
argument, the task should also modify the C(diff_against) value and
set it to I(intended).
required: false
version_added: "2.4"
""" """
EXAMPLES = """ EXAMPLES = """
@ -188,14 +243,34 @@ EXAMPLES = """
parents: ip access-list extended test parents: ip access-list extended test
before: no ip access-list extended test before: no ip access-list extended test
match: exact match: exact
- name: check the running-config against master config
ios_config:
diff_config: intended
intended_config: "{{ lookup('file', 'master.cfg') }}"
- name: check the startup-config against the running-config
ios_config:
diff_against: startup
diff_ignore_lines:
- ntp clock .*
- name: save running to startup when changed
ios_config:
save_when: changed
""" """
RETURN = """ RETURN = """
updates: updates:
description: The set of commands that will be pushed to the remote device description: The set of commands that will be pushed to the remote device
returned: Only when lines is specified. returned: always
type: list type: list
sample: ['...', '...'] sample: ['hostname foo', 'router ospf 1', 'router-id 1.1.1.1']
commands:
description: The set of commands that will be pushed to the remote device
returned: always
type: list
sample: ['hostname foo', 'router ospf 1', 'router-id 1.1.1.1']
backup_path: backup_path:
description: The full path to the backup file description: The full path to the backup file
returned: when backup is yes returned: when backup is yes
@ -221,10 +296,7 @@ def check_args(module, warnings):
if len(module.params['multiline_delimiter']) != 1: if len(module.params['multiline_delimiter']) != 1:
module.fail_json(msg='multiline_delimiter value can only be a ' module.fail_json(msg='multiline_delimiter value can only be a '
'single character') 'single character')
if module.params['force']:
warnings.append('The force argument is deprecated as of Ansible 2.2, '
'please use match=none instead. This argument will '
'be removed in the future')
def extract_banners(config): def extract_banners(config):
banners = {} banners = {}
@ -245,6 +317,7 @@ def extract_banners(config):
config = re.sub(r'banner \w+ \^C\^C', '!! banner removed', config) config = re.sub(r'banner \w+ \^C\^C', '!! banner removed', config)
return (config, banners) return (config, banners)
def diff_banners(want, have): def diff_banners(want, have):
candidate = {} candidate = {}
for key, value in iteritems(want): for key, value in iteritems(want):
@ -252,6 +325,7 @@ def diff_banners(want, have):
candidate[key] = value candidate[key] = value
return candidate return candidate
def load_banners(module, banners): def load_banners(module, banners):
delimiter = module.params['multiline_delimiter'] delimiter = module.params['multiline_delimiter']
for key, value in iteritems(banners): for key, value in iteritems(banners):
@ -262,16 +336,19 @@ def load_banners(module, banners):
time.sleep(0.1) time.sleep(0.1)
run_commands(module, ['\n']) run_commands(module, ['\n'])
def get_running_config(module):
contents = module.params['config'] def get_running_config(module, current_config=None):
contents = module.params['running_config']
if not contents: if not contents:
flags = [] if not module.params['defaults'] and current_config:
if module.params['defaults']: contents, banners = extract_banners(current_config.config_text)
flags.append(get_defaults_flag(module)) else:
contents = get_config(module, flags=flags) flags = get_defaults_flag(module) if module.params['defaults'] else None
contents = get_config(module, flags=flags)
contents, banners = extract_banners(contents) contents, banners = extract_banners(contents)
return NetworkConfig(indent=1, contents=contents), banners return NetworkConfig(indent=1, contents=contents), banners
def get_candidate(module): def get_candidate(module):
candidate = NetworkConfig(indent=1) candidate = NetworkConfig(indent=1)
banners = {} banners = {}
@ -286,6 +363,7 @@ def get_candidate(module):
return candidate, banners return candidate, banners
def main(): def main():
""" main entry point for module execution """ main entry point for module execution
""" """
@ -302,39 +380,53 @@ def main():
replace=dict(default='line', choices=['line', 'block']), replace=dict(default='line', choices=['line', 'block']),
multiline_delimiter=dict(default='@'), multiline_delimiter=dict(default='@'),
# this argument is deprecated (2.2) in favor of setting match: none running_config=dict(aliases=['config']),
# it will be removed in a future version intended_config=dict(),
force=dict(default=False, type='bool'),
config=dict(),
defaults=dict(type='bool', default=False), defaults=dict(type='bool', default=False),
backup=dict(type='bool', default=False), backup=dict(type='bool', default=False),
save=dict(type='bool', default=False),
save_when=dict(choices=['always', 'never', 'changed'], default='never'),
diff_against=dict(choices=['startup', 'intended', 'running']),
diff_ignore_lines=dict(type='list'),
# save is deprecated as of ans2.4, use save_when instead
save=dict(default=False, type='bool', removed_in_version='2.4'),
# force argument deprecated in ans2.2
force=dict(default=False, type='bool', removed_in_version='2.2')
) )
argument_spec.update(ios_argument_spec) argument_spec.update(ios_argument_spec)
mutually_exclusive = [('lines', 'src')] mutually_exclusive = [('lines', 'src'),
('save', 'save_when')]
required_if = [('match', 'strict', ['lines']), required_if = [('match', 'strict', ['lines']),
('match', 'exact', ['lines']), ('match', 'exact', ['lines']),
('replace', 'block', ['lines'])] ('replace', 'block', ['lines']),
('diff_against', 'intended', ['intended_config'])]
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive, mutually_exclusive=mutually_exclusive,
required_if=required_if, required_if=required_if,
supports_check_mode=True) supports_check_mode=True)
if module.params['force'] is True:
module.params['match'] = 'none'
result = {'changed': False} result = {'changed': False}
warnings = list() warnings = list()
check_args(module, warnings) check_args(module, warnings)
result['warnings'] = warnings result['warnings'] = warnings
config = None
if module.params['backup'] or (module._diff and module.params['diff_against'] == 'running'):
contents = get_config(module)
config = NetworkConfig(indent=1, contents=contents)
if module.params['backup']:
result['__backup__'] = contents
if any((module.params['lines'], module.params['src'])): if any((module.params['lines'], module.params['src'])):
match = module.params['match'] match = module.params['match']
replace = module.params['replace'] replace = module.params['replace']
@ -343,10 +435,9 @@ def main():
candidate, want_banners = get_candidate(module) candidate, want_banners = get_candidate(module)
if match != 'none': if match != 'none':
config, have_banners = get_running_config(module) config, have_banners = get_running_config(module, config)
path = module.params['parents'] path = module.params['parents']
configobjs = candidate.difference(config, path=path, match=match, configobjs = candidate.difference(config, path=path, match=match, replace=replace)
replace=replace)
else: else:
configobjs = candidate.items configobjs = candidate.items
have_banners = {} have_banners = {}
@ -356,12 +447,11 @@ def main():
if configobjs or banners: if configobjs or banners:
commands = dumps(configobjs, 'commands').split('\n') commands = dumps(configobjs, 'commands').split('\n')
if module.params['lines']: if module.params['before']:
if module.params['before']: commands[:0] = module.params['before']
commands[:0] = module.params['before']
if module.params['after']: if module.params['after']:
commands.extend(module.params['after']) commands.extend(module.params['after'])
result['commands'] = commands result['commands'] = commands
result['updates'] = commands result['updates'] = commands
@ -377,15 +467,61 @@ def main():
result['changed'] = True result['changed'] = True
if module.params['backup']: running_config = None
result['__backup__'] = get_config(module=module) startup_config = None
diff_ignore_lines = module.params['diff_ignore_lines']
if module.params['save']: if module.params['save_when'] != 'never':
if not module.check_mode: output = run_commands(module, ['show running-config', 'show startup-config'])
response = run_commands(module, ['show archive config differences'])
if response[0].find('!No changes were found') < 0: running_config = NetworkConfig(indent=1, contents=output[0], ignore_lines=diff_ignore_lines)
run_commands(module, ['copy running-config startup-config\r']) startup_config = NetworkConfig(indent=1, contents=output[1], ignore_lines=diff_ignore_lines)
result['changed'] = True
if running_config.sha1 != startup_config.sha1 or module.params['save_when'] == 'always':
result['changed'] = True
if not module.check_mode:
run_commands(module, 'copy running-config startup-config')
else:
module.warn('Skipping command `copy running-config startup-config` '
'due to check_mode. Configuration not copied to '
'non-volatile storage')
if module._diff:
if not running_config:
output = run_commands(module, 'show running-config')
contents = output[0]
else:
contents = running_config.config_text
# recreate the object in order to process diff_ignore_lines
running_config = NetworkConfig(indent=1, contents=config_text, ignore_lines=diff_ignore_lines)
if module.params['diff_against'] == 'running':
if module.check_mode:
module.warn("unable to perform diff against running-config due to check mode")
contents = None
else:
contents = config.config_text
elif module.params['diff_against'] == 'startup':
if not startup_config:
output = run_commands(module, 'show startup-config')
contents = output[0]
else:
contents = startup_config.config_text
elif module.params['diff_against'] == 'intended':
contents = module.params['intended_config']
if contents is not None:
base_config = NetworkConfig(indent=1, contents=contents, ignore_lines=diff_ignore_lines)
if running_config.sha1 != base_config.sha1:
result.update({
'changed': True,
'diff': {'before': str(base_config), 'after': str(running_config)}
})
module.exit_json(**result) module.exit_json(**result)

@ -102,7 +102,7 @@ options:
command block is pushed to the device in configuration mode if any command block is pushed to the device in configuration mode if any
line is not correct. line is not correct.
required: false required: false
default: line default: lineo
choices: ['line', 'block'] choices: ['line', 'block']
force: force:
description: description:
@ -115,7 +115,7 @@ options:
will be removed in a future release. will be removed in a future release.
required: false required: false
default: false default: false
choices: [ "true", "false" ] type: bool
backup: backup:
description: description:
- This argument will cause the module to create a full backup of - This argument will cause the module to create a full backup of
@ -124,20 +124,22 @@ options:
folder in the playbook root directory. If the directory does not folder in the playbook root directory. If the directory does not
exist, it is created. exist, it is created.
required: false required: false
default: no default: false
choices: ['yes', 'no'] type: bool
version_added: "2.2" version_added: "2.2"
config: running_config:
description: description:
- The module, by default, will connect to the remote device and - The module, by default, will connect to the remote device and
retrieve the current running-config to use as a base for comparing retrieve the current running-config to use as a base for comparing
against the contents of source. There are times when it is not against the contents of source. There are times when it is not
desirable to have the task get the current running-config for desirable to have the task get the current running-config for
every task in a playbook. The I(config) argument allows the every task in a playbook. The I(running_config) argument allows the
implementer to pass in the configuration to use as the base implementer to pass in the configuration to use as the base
config for comparison. config for comparison.
required: false required: false
default: null default: null
aliases: ['config']
version_added: "2.4"
defaults: defaults:
description: description:
- The I(defaults) argument will influence how the running-config - The I(defaults) argument will influence how the running-config
@ -147,6 +149,7 @@ options:
is issued without the all keyword is issued without the all keyword
required: false required: false
default: false default: false
type: bool
version_added: "2.2" version_added: "2.2"
save: save:
description: description:
@ -156,28 +159,75 @@ options:
no changes are made, the configuration is still saved to the no changes are made, the configuration is still saved to the
startup config. This option will always cause the module to startup config. This option will always cause the module to
return changed. return changed.
- This option is deprecated as of Ansible 2.4, use C(save_when)
required: false required: false
default: false default: false
type: bool
version_added: "2.2" version_added: "2.2"
save_when:
description:
- When changes are made to the device running-configuration, the
changes are not copied to non-volatile storage by default. Using
this argument will change that before. If the argument is set to
I(always), then the running-config will always be copied to the
startup-config and the I(changed) flag will always be set to
True. If the argument is set to I(changed), then the running-config
will only be copied to the startup-config if it has changed since
the last save to startup-config. If the argument is set to
I(never), the running-config will never be copied to the the
startup-config
required: false
default: never
choices: ['always', 'never', 'changed']
version_added: "2.4"
diff_against:
description:
- When using the C(ansible-playbook --diff) command line argument
the module can generate diffs against different sources.
- When this option is configure as I(startup), the module will return
the diff of the running-config against the startup-config.
- When this option is configured as I(intended), the module will
return the diff of the running-config against the configuration
provided in the C(intended_config) argument.
- When this option is configured as I(running), the module will
return the before and after diff of the running-config with respect
to any changes made to the device configuration.
required: false
default: startup
choices: ['startup', 'intended', 'running']
version_added: "2.4"
diff_ignore_lines:
description:
- 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.
required: false
version_added: "2.4"
intended_config:
description:
- The C(intended_config) provides the master configuration that
the node should conform to and is used to check the final
running-config against. This argument will not modify any settings
on the remote device and is strictly used to check the compliance
of the current device's configuration against. When specifying this
argument, the task should also modify the C(diff_against) value and
set it to I(intended).
required: false
version_added: "2.4"
""" """
EXAMPLES = """ EXAMPLES = """
# Note: examples below use the following provider dict to handle
# transport and authentication to the node.
---
vars:
cli:
host: "{{ inventory_hostname }}"
username: admin
password: admin
transport: cli
--- ---
- name: configure top level configuration and save it - name: configure top level configuration and save it
nxos_config: nxos_config:
lines: hostname {{ inventory_hostname }} lines: hostname {{ inventory_hostname }}
save: yes save_when: changed
provider: "{{ cli }}"
- name: diff the running-config against a provided config
nxos_config:
diff_against: intended
intended: "{{ lookup('file', 'master.cfg') }}"
- nxos_config: - nxos_config:
lines: lines:
@ -189,7 +239,6 @@ vars:
parents: ip access-list test parents: ip access-list test
before: no ip access-list test before: no ip access-list test
match: exact match: exact
provider: "{{ cli }}"
- nxos_config: - nxos_config:
lines: lines:
@ -200,15 +249,19 @@ vars:
parents: ip access-list test parents: ip access-list test
before: no ip access-list test before: no ip access-list test
replace: block replace: block
provider: "{{ cli }}"
""" """
RETURN = """ RETURN = """
commands:
description: The set of commands that will be pushed to the remote device
returned: always
type: list
sample: ['hostname foo', 'vlan 1', 'name default']
updates: updates:
description: The set of commands that will be pushed to the remote device description: The set of commands that will be pushed to the remote device
returned: Only when lines is specified. returned: always
type: list type: list
sample: ['...', '...'] sample: ['hostname foo', 'vlan 1', 'name default']
backup_path: backup_path:
description: The full path to the backup file description: The full path to the backup file
returned: when backup is yes returned: when backup is yes
@ -221,21 +274,17 @@ from ansible.module_utils.nxos import get_config, load_config, run_commands
from ansible.module_utils.nxos import nxos_argument_spec from ansible.module_utils.nxos import nxos_argument_spec
from ansible.module_utils.nxos import check_args as nxos_check_args from ansible.module_utils.nxos import check_args as nxos_check_args
def check_args(module, warnings):
nxos_check_args(module, warnings)
if module.params['force']:
warnings.append('The force argument is deprecated, please use '
'match=none instead. This argument will be '
'removed in the future')
def get_running_config(module): def get_running_config(module, config=None):
contents = module.params['config'] contents = module.params['running_config']
if not contents: if not contents:
flags = [] if not module.params['defaults'] and config:
if module.params['defaults']: contents = config
flags.append('all') else:
contents = get_config(module, flags=flags) flags = ['all']
return NetworkConfig(indent=2, contents=contents) contents = get_config(module, flags=flags)
return NetworkConfig(indent=3, contents=contents)
def get_candidate(module): def get_candidate(module):
candidate = NetworkConfig(indent=2) candidate = NetworkConfig(indent=2)
@ -246,36 +295,6 @@ def get_candidate(module):
candidate.add(module.params['lines'], parents=parents) candidate.add(module.params['lines'], parents=parents)
return candidate return candidate
def run(module, result):
match = module.params['match']
replace = module.params['replace']
candidate = get_candidate(module)
if match != 'none':
config = get_running_config(module)
path = module.params['parents']
configobjs = candidate.difference(config, match=match, replace=replace, path=path)
else:
configobjs = candidate.items
if configobjs:
commands = dumps(configobjs, 'commands').split('\n')
if module.params['lines']:
if module.params['before']:
commands[:0] = module.params['before']
if module.params['after']:
commands.extend(module.params['after'])
result['commands'] = commands
result['updates'] = commands
if not module.check_mode:
load_config(module, commands)
result['changed'] = True
def main(): def main():
""" main entry point for module execution """ main entry point for module execution
@ -292,49 +311,140 @@ def main():
match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), match=dict(default='line', choices=['line', 'strict', 'exact', 'none']),
replace=dict(default='line', choices=['line', 'block']), replace=dict(default='line', choices=['line', 'block']),
# this argument is deprecated in favor of setting match: none running_config=dict(aliases=['config']),
# it will be removed in a future version intended_config=dict(),
force=dict(default=False, type='bool'),
config=dict(),
defaults=dict(type='bool', default=False), defaults=dict(type='bool', default=False),
backup=dict(type='bool', default=False), backup=dict(type='bool', default=False),
save=dict(type='bool', default=False),
save_when=dict(choices=['always', 'never', 'changed'], default='never'),
diff_against=dict(choices=['running', 'startup', 'intended']),
diff_ignore_lines=dict(type='list'),
# save is deprecated as of ans2.4, use save_when instead
save=dict(default=False, type='bool', removed_in_version='2.4'),
# force argument deprecated in ans2.2
force=dict(default=False, type='bool', removed_in_version='2.2')
) )
argument_spec.update(nxos_argument_spec) argument_spec.update(nxos_argument_spec)
mutually_exclusive = [('lines', 'src')] mutually_exclusive = [('lines', 'src'),
('save', 'save_when')]
required_if = [('match', 'strict', ['lines']), required_if = [('match', 'strict', ['lines']),
('match', 'exact', ['lines']), ('match', 'exact', ['lines']),
('replace', 'block', ['lines'])] ('replace', 'block', ['lines']),
('diff_against', 'intended', ['intended_config'])]
module = AnsibleModule(argument_spec=argument_spec, module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive, mutually_exclusive=mutually_exclusive,
required_if=required_if, required_if=required_if,
supports_check_mode=True) supports_check_mode=True)
if module.params['force'] is True:
module.params['match'] = 'none'
warnings = list() warnings = list()
check_args(module, warnings) nxos_check_args(module, warnings)
result = dict(changed=False, warnings=warnings) result = {'changed': False, 'warnings': warnings}
if module.params['backup']: config = None
result['__backup__'] = get_config(module)
if module.params['backup'] or (module._diff and module.params['diff_against'] == 'running'):
contents = get_config(module)
config = NetworkConfig(indent=2, contents=contents)
if module.params['backup']:
result['__backup__'] = contents
if any((module.params['src'], module.params['lines'])): if any((module.params['src'], module.params['lines'])):
run(module, result) match = module.params['match']
replace = module.params['replace']
candidate = get_candidate(module)
if match != 'none':
config = get_running_config(module, config)
path = module.params['parents']
configobjs = candidate.difference(config, match=match, replace=replace, path=path)
else:
configobjs = candidate.items
if configobjs:
commands = dumps(configobjs, 'commands').split('\n')
if module.params['before']:
commands[:0] = module.params['before']
if module.params['after']:
commands.extend(module.params['after'])
result['commands'] = commands
result['updates'] = commands
if not module.check_mode:
load_config(module, commands)
result['changed'] = True
running_config = None
startup_config = None
diff_ignore_lines = module.params['diff_ignore_lines']
if module.params['save_when'] != 'never':
output = run_commands(module, ['show running-config', 'startup-config'])
running_config = NetworkConfig(indent=1, contents=output[0], ignore_lines=diff_ignore_lines)
startup_config = NetworkConfig(indent=1, contents=output[1], ignore_lines=diff_ignore_lines)
if running_config.sha1 != startup_config.sha1 or module.params['save_when'] == 'always':
result['changed'] = True
if not module.check_mode:
cmd = {'command': 'copy running-config startup-config', 'output': 'text'}
run_commands(module, [cmd])
else:
module.warn('Skipping command `copy running-config startup-config` '
'due to check_mode. Configuration not copied to '
'non-volatile storage')
if module._diff:
if not running_config:
output = run_commands(module, 'show running-config')
contents = output[0]
else:
contents = running_config.config_text
# recreate the object in order to process diff_ignore_lines
running_config = NetworkConfig(indent=1, contents=config_text, ignore_lines=diff_ignore_lines)
if module.params['diff_against'] == 'running':
if module.check_mode:
module.warn("unable to perform diff against running-config due to check mode")
contents = None
else:
contents = config.config_text
elif module.params['diff_against'] == 'startup':
if not startup_config:
output = run_commands(module, 'show startup-config')
contents = output[0]
else:
contents = output[0]
contents = startup_config.config_text
elif module.params['diff_against'] == 'intended':
contents = module.params['intended_config']
if contents is not None:
base_config = NetworkConfig(indent=1, contents=contents, ignore_lines=diff_ignore_lines)
if running_config.sha1 != base_config.sha1:
result.update({
'changed': True,
'diff': {'before': str(base_config), 'after': str(running_config)}
})
if module.params['save']:
if not module.check_mode:
cmd = {'command': 'copy running-config startup-config', 'output': 'text'}
run_commands(module, [cmd])
result['changed'] = True
module.exit_json(**result) module.exit_json(**result)

@ -321,7 +321,6 @@ lib/ansible/modules/network/illumos/dladm_linkprop.py
lib/ansible/modules/network/ios/_ios_template.py lib/ansible/modules/network/ios/_ios_template.py
lib/ansible/modules/network/ios/ios_banner.py lib/ansible/modules/network/ios/ios_banner.py
lib/ansible/modules/network/ios/ios_command.py lib/ansible/modules/network/ios/ios_command.py
lib/ansible/modules/network/ios/ios_config.py
lib/ansible/modules/network/ios/ios_facts.py lib/ansible/modules/network/ios/ios_facts.py
lib/ansible/modules/network/ios/ios_system.py lib/ansible/modules/network/ios/ios_system.py
lib/ansible/modules/network/ios/ios_vrf.py lib/ansible/modules/network/ios/ios_vrf.py

@ -68,15 +68,15 @@ class TestIosConfigModule(TestIosModule):
result = self.execute_module() result = self.execute_module()
self.assertIn('__backup__', result) self.assertIn('__backup__', result)
def test_ios_config_save(self): def test_ios_config_save_always(self):
self.run_commands.return_value = "Hostname foo" self.run_commands.return_value = "Hostname foo"
set_module_args(dict(save=True)) set_module_args(dict(save_when='always'))
self.execute_module(changed=True) self.execute_module(changed=True)
self.assertEqual(self.run_commands.call_count, 2) self.assertEqual(self.run_commands.call_count, 2)
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.load_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', args)
def test_ios_config_lines_wo_parents(self): def test_ios_config_lines_wo_parents(self):
set_module_args(dict(lines=['hostname foo'])) set_module_args(dict(lines=['hostname foo']))
@ -117,9 +117,9 @@ class TestIosConfigModule(TestIosModule):
commands = parents + lines commands = parents + lines
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
def test_ios_config_force(self): def test_ios_config_match_none(self):
lines = ['hostname router'] lines = ['hostname router']
set_module_args(dict(lines=lines, force=True)) set_module_args(dict(lines=lines, 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):

Loading…
Cancel
Save