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
This commit is contained in:
Peter Sprygada 2017-07-11 20:34:20 -04:00 committed by GitHub
parent dc4037e5a7
commit 0b6f0e6c0d
6 changed files with 628 additions and 233 deletions

View file

@ -26,12 +26,20 @@
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
import re
import hashlib
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
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):
@ -97,6 +105,9 @@ def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
for regex in DEFAULT_IGNORE_LINES_RE:
if regex.match(text):
return True
def _obj_to_text(x):
@ -141,9 +152,16 @@ def dumps(objects, output='block', comments=False):
class NetworkConfig(object):
def __init__(self, indent=1, contents=None):
def __init__(self, indent=1, contents=None, ignore_lines=None):
self._indent = indent
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:
self.load(contents)
@ -152,6 +170,16 @@ class NetworkConfig(object):
def items(self):
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):
for line in self:
if line.text == key:
@ -168,6 +196,7 @@ class NetworkConfig(object):
return len(self._items)
def load(self, s):
self._config_text = s
self._items = self.parse(s)
def loadfp(self, fp):

View file

@ -30,7 +30,7 @@ short_description: Manage Arista EOS configuration sections
description:
- Arista EOS configurations use a simple block indent file syntax
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
transports.
extends_documentation_fragment: eos
@ -115,7 +115,7 @@ options:
will be removed in a future release.
required: false
default: false
choices: ['yes', 'no']
type: bool
backup:
description:
- This argument will cause the module to create a full backup of
@ -125,19 +125,21 @@ options:
exist, it is created.
required: false
default: no
choices: ['yes', 'no']
type: bool
version_added: "2.2"
config:
running_config:
description:
- The module, by default, will connect to the remote device and
retrieve the current running-config to use as a base for comparing
against the contents of source. There are times when it is not
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
config for comparison.
config for this module.
required: false
default: null
aliases: ['config']
version_added: "2.4"
defaults:
description:
- The I(defaults) argument will influence how the running-config
@ -147,6 +149,7 @@ options:
is issued without the all keyword
required: false
default: false
type: bool
version_added: "2.2"
save:
description:
@ -156,27 +159,78 @@ options:
no changes are made, the configuration is still saved to the
startup config. This option will always cause the module to
return changed.
- This option is deprecated as of Ansible 2.4, use C(save_when)
required: false
default: false
type: bool
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 = """
- eos_config:
- name: configure top level settings
eos_config:
lines: hostname {{ inventory_hostname }}
- eos_config:
lines:
- 10 permit ip 1.1.1.1/32 any log
- 20 permit ip 2.2.2.2/32 any log
- 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
- name: diff against a provided master config
eos_config:
diff_against: config
config: "{{ lookup('file', 'master.cfg') }}"
- eos_config:
- name: load an acl into the device
eos_config:
lines:
- 10 permit ip 1.1.1.1/32 any log
- 20 permit ip 2.2.2.2/32 any log
@ -189,12 +243,22 @@ EXAMPLES = """
- name: load configuration from file
eos_config:
src: eos.cfg
- name: diff the running config against a master config
eos_config:
diff_against: intended
intended_config: "{{ lookup('file', 'master.cfg') }}"
"""
RETURN = """
commands:
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
sample: ['hostname switch01', 'interface Ethernet1', 'no shutdown']
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 run_commands
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):
candidate = NetworkConfig(indent=3)
@ -226,51 +284,17 @@ def get_candidate(module):
candidate.add(module.params['lines'], parents=parents)
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)
def get_running_config(module, config=None):
contents = module.params['running_config']
if not contents:
if not module.params['defaults'] and config:
contents = config
else:
configobjs = candidate.items
flags = ['all']
contents = get_config(module, flags=flags)
return NetworkConfig(indent=3, contents=contents)
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:
result['diff'] = {'prepared': response['diff']}
if 'session' in response:
result['session'] = response['session']
result['changed'] = True
def main():
""" main entry point for module execution
@ -288,34 +312,39 @@ def main():
replace=dict(default='line', choices=['line', 'block', 'config']),
defaults=dict(type='bool', default=False),
backup=dict(type='bool', default=False),
save=dict(default=False, type='bool'),
# deprecated arguments (Ansible 2.3)
config=dict(),
# this argument is deprecated in favor of setting match: none
# it will be removed in a future version
force=dict(default=False, type='bool'),
save_when=dict(choices=['always', 'never', 'changed'], default='never'),
diff_against=dict(choices=['startup', 'session', 'intended', 'running'], default='session'),
diff_ignore_lines=dict(type='list'),
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)
mutually_exclusive = [('lines', 'src')]
mutually_exclusive = [('lines', 'src'),
('save', 'save_when')]
required_if = [('match', 'strict', ['lines']),
('match', 'exact', ['lines']),
('replace', 'block', ['lines']),
('replace', 'config', ['src'])]
('replace', 'config', ['src']),
('diff_against', 'intended', ['intended_config'])]
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
required_if=required_if,
supports_check_mode=True)
if module.params['force'] is True:
module.params['match'] = 'none'
warnings = list()
check_args(module, warnings)
@ -323,19 +352,111 @@ def main():
if 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=2, contents=contents)
if module.params['backup']:
result['__backup__'] = get_config(module)
result['__backup__'] = contents
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' 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['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']
if module.params['save']:
if not module.check_mode:
response = run_commands(module, ['show running-config diffs'])
if len(response[0]):
run_commands(module, ['copy running-config startup-config'])
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)

View file

@ -123,7 +123,7 @@ options:
will be removed in a future release.
required: false
default: false
choices: ["true", "false"]
type: bool
backup:
description:
- This argument will cause the module to create a full backup of
@ -133,17 +133,21 @@ options:
exist, it is created.
required: false
default: no
choices: ['yes', 'no']
type: bool
version_added: "2.2"
config:
running_config:
description:
- The C(config) argument allows the playbook designer to supply
the base configuration to be used to validate configuration
changes necessary. If this argument is provided, the module
will not download the running-config from the remote node.
- The module, by default, will connect to the remote device and
retrieve the current running-config to use as a base for comparing
against the contents of source. There are times when it is not
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
default: null
version_added: "2.2"
aliases: ['config']
version_added: "2.4"
defaults:
description:
- This argument specifies whether or not to collect all defaults
@ -152,17 +156,68 @@ options:
C(show running-config all).
required: false
default: no
choices: ['yes', 'no']
type: bool
version_added: "2.2"
save:
description:
- The C(save) argument instructs the module to save the running-
config to the startup-config at the conclusion of the module
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
default: no
choices: ['yes', 'no']
default: false
type: bool
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 = """
@ -188,14 +243,34 @@ EXAMPLES = """
parents: ip access-list extended test
before: no ip access-list extended test
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 = """
updates:
description: The set of commands that will be pushed to the remote device
returned: Only when lines is specified.
returned: always
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:
description: The full path to the backup file
returned: when backup is yes
@ -221,10 +296,7 @@ def check_args(module, warnings):
if len(module.params['multiline_delimiter']) != 1:
module.fail_json(msg='multiline_delimiter value can only be a '
'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):
banners = {}
@ -245,6 +317,7 @@ def extract_banners(config):
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):
@ -252,6 +325,7 @@ def diff_banners(want, have):
candidate[key] = value
return candidate
def load_banners(module, banners):
delimiter = module.params['multiline_delimiter']
for key, value in iteritems(banners):
@ -262,16 +336,19 @@ def load_banners(module, banners):
time.sleep(0.1)
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:
flags = []
if module.params['defaults']:
flags.append(get_defaults_flag(module))
if not module.params['defaults'] and current_config:
contents, banners = extract_banners(current_config.config_text)
else:
flags = get_defaults_flag(module) if module.params['defaults'] else None
contents = get_config(module, flags=flags)
contents, banners = extract_banners(contents)
return NetworkConfig(indent=1, contents=contents), banners
def get_candidate(module):
candidate = NetworkConfig(indent=1)
banners = {}
@ -286,6 +363,7 @@ def get_candidate(module):
return candidate, banners
def main():
""" main entry point for module execution
"""
@ -302,39 +380,53 @@ def main():
replace=dict(default='line', choices=['line', 'block']),
multiline_delimiter=dict(default='@'),
# this argument is deprecated (2.2) in favor of setting match: none
# it will be removed in a future version
force=dict(default=False, type='bool'),
running_config=dict(aliases=['config']),
intended_config=dict(),
config=dict(),
defaults=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)
mutually_exclusive = [('lines', 'src')]
mutually_exclusive = [('lines', 'src'),
('save', 'save_when')]
required_if = [('match', 'strict', ['lines']),
('match', 'exact', ['lines']),
('replace', 'block', ['lines'])]
('replace', 'block', ['lines']),
('diff_against', 'intended', ['intended_config'])]
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
required_if=required_if,
supports_check_mode=True)
if module.params['force'] is True:
module.params['match'] = 'none'
result = {'changed': False}
warnings = list()
check_args(module, 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'])):
match = module.params['match']
replace = module.params['replace']
@ -343,10 +435,9 @@ def main():
candidate, want_banners = get_candidate(module)
if match != 'none':
config, have_banners = get_running_config(module)
config, have_banners = get_running_config(module, config)
path = module.params['parents']
configobjs = candidate.difference(config, path=path, match=match,
replace=replace)
configobjs = candidate.difference(config, path=path, match=match, replace=replace)
else:
configobjs = candidate.items
have_banners = {}
@ -356,7 +447,6 @@ def main():
if configobjs or banners:
commands = dumps(configobjs, 'commands').split('\n')
if module.params['lines']:
if module.params['before']:
commands[:0] = module.params['before']
@ -377,15 +467,61 @@ def main():
result['changed'] = True
if module.params['backup']:
result['__backup__'] = get_config(module=module)
running_config = None
startup_config = None
if module.params['save']:
if not module.check_mode:
response = run_commands(module, ['show archive config differences'])
if response[0].find('!No changes were found') < 0:
run_commands(module, ['copy running-config startup-config\r'])
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:
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)

View file

@ -102,7 +102,7 @@ options:
command block is pushed to the device in configuration mode if any
line is not correct.
required: false
default: line
default: lineo
choices: ['line', 'block']
force:
description:
@ -115,7 +115,7 @@ options:
will be removed in a future release.
required: false
default: false
choices: [ "true", "false" ]
type: bool
backup:
description:
- 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
exist, it is created.
required: false
default: no
choices: ['yes', 'no']
default: false
type: bool
version_added: "2.2"
config:
running_config:
description:
- The module, by default, will connect to the remote device and
retrieve the current running-config to use as a base for comparing
against the contents of source. There are times when it is not
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
config for comparison.
required: false
default: null
aliases: ['config']
version_added: "2.4"
defaults:
description:
- The I(defaults) argument will influence how the running-config
@ -147,6 +149,7 @@ options:
is issued without the all keyword
required: false
default: false
type: bool
version_added: "2.2"
save:
description:
@ -156,28 +159,75 @@ options:
no changes are made, the configuration is still saved to the
startup config. This option will always cause the module to
return changed.
- This option is deprecated as of Ansible 2.4, use C(save_when)
required: false
default: false
type: bool
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 = """
# 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
nxos_config:
lines: hostname {{ inventory_hostname }}
save: yes
provider: "{{ cli }}"
save_when: changed
- name: diff the running-config against a provided config
nxos_config:
diff_against: intended
intended: "{{ lookup('file', 'master.cfg') }}"
- nxos_config:
lines:
@ -189,7 +239,6 @@ vars:
parents: ip access-list test
before: no ip access-list test
match: exact
provider: "{{ cli }}"
- nxos_config:
lines:
@ -200,15 +249,19 @@ vars:
parents: ip access-list test
before: no ip access-list test
replace: block
provider: "{{ cli }}"
"""
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:
description: The set of commands that will be pushed to the remote device
returned: Only when lines is specified.
returned: always
type: list
sample: ['...', '...']
sample: ['hostname foo', 'vlan 1', 'name default']
backup_path:
description: The full path to the backup file
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 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):
contents = module.params['config']
def get_running_config(module, config=None):
contents = module.params['running_config']
if not contents:
flags = []
if module.params['defaults']:
flags.append('all')
if not module.params['defaults'] and config:
contents = config
else:
flags = ['all']
contents = get_config(module, flags=flags)
return NetworkConfig(indent=2, contents=contents)
return NetworkConfig(indent=3, contents=contents)
def get_candidate(module):
candidate = NetworkConfig(indent=2)
@ -246,36 +295,6 @@ def get_candidate(module):
candidate.add(module.params['lines'], parents=parents)
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():
""" main entry point for module execution
@ -292,49 +311,140 @@ def main():
match=dict(default='line', choices=['line', 'strict', 'exact', 'none']),
replace=dict(default='line', choices=['line', 'block']),
# this argument is deprecated in favor of setting match: none
# it will be removed in a future version
force=dict(default=False, type='bool'),
running_config=dict(aliases=['config']),
intended_config=dict(),
config=dict(),
defaults=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)
mutually_exclusive = [('lines', 'src')]
mutually_exclusive = [('lines', 'src'),
('save', 'save_when')]
required_if = [('match', 'strict', ['lines']),
('match', 'exact', ['lines']),
('replace', 'block', ['lines'])]
('replace', 'block', ['lines']),
('diff_against', 'intended', ['intended_config'])]
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
required_if=required_if,
supports_check_mode=True)
if module.params['force'] is True:
module.params['match'] = 'none'
warnings = list()
check_args(module, warnings)
nxos_check_args(module, warnings)
result = dict(changed=False, warnings=warnings)
result = {'changed': False, 'warnings': warnings}
config = None
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__'] = get_config(module)
result['__backup__'] = contents
if any((module.params['src'], module.params['lines'])):
run(module, result)
match = module.params['match']
replace = module.params['replace']
if module.params['save']:
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])
result['changed'] = True
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)}
})
module.exit_json(**result)

View file

@ -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_banner.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_system.py
lib/ansible/modules/network/ios/ios_vrf.py

View file

@ -68,15 +68,15 @@ class TestIosConfigModule(TestIosModule):
result = self.execute_module()
self.assertIn('__backup__', result)
def test_ios_config_save(self):
def test_ios_config_save_always(self):
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.assertEqual(self.run_commands.call_count, 2)
self.assertEqual(self.get_config.call_count, 0)
self.assertEqual(self.load_config.call_count, 0)
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):
set_module_args(dict(lines=['hostname foo']))
@ -117,9 +117,9 @@ class TestIosConfigModule(TestIosModule):
commands = parents + lines
self.execute_module(changed=True, commands=commands)
def test_ios_config_force(self):
def test_ios_config_match_none(self):
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)
def test_ios_config_match_none(self):