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

@ -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))
contents = get_config(module, flags=flags)
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,12 +447,11 @@ 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']
if module.params['before']:
commands[:0] = module.params['before']
if module.params['after']:
commands.extend(module.params['after'])
if module.params['after']:
commands.extend(module.params['after'])
result['commands'] = commands
result['updates'] = commands
@ -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'])
result['changed'] = True
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)