2018-08-14 08:22:15 +00:00
|
|
|
#!/usr/bin/python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# (c) 2018, Ansible by Red Hat, inc
|
|
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
|
|
|
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
|
|
'status': ['preview'],
|
|
|
|
'supported_by': 'network'}
|
|
|
|
|
|
|
|
|
|
|
|
DOCUMENTATION = """
|
|
|
|
---
|
|
|
|
module: cli_config
|
|
|
|
version_added: "2.7"
|
|
|
|
author: "Trishna Guha (@trishnaguha)"
|
|
|
|
short_description: Push text based configuration to network devices over network_cli
|
|
|
|
description:
|
|
|
|
- This module provides platform agnostic way of pushing text based
|
|
|
|
configuration to network devices over network_cli connection plugin.
|
|
|
|
options:
|
|
|
|
config:
|
|
|
|
description:
|
2018-08-29 15:30:11 +00:00
|
|
|
- The config to be pushed to the network device. This argument
|
|
|
|
is mutually exclusive with C(rollback) and either one of the
|
2018-09-07 11:39:08 +00:00
|
|
|
option should be given as input. The config should have
|
|
|
|
indentation that the device uses.
|
2018-08-14 08:22:15 +00:00
|
|
|
type: 'str'
|
|
|
|
commit:
|
|
|
|
description:
|
|
|
|
- The C(commit) argument instructs the module to push the
|
|
|
|
configuration to the device. This is mapped to module check mode.
|
|
|
|
type: 'bool'
|
|
|
|
replace:
|
|
|
|
description:
|
|
|
|
- If the C(replace) argument is set to C(yes), it will replace
|
|
|
|
the entire running-config of the device with the C(config)
|
|
|
|
argument value. For NXOS devices, C(replace) argument takes
|
|
|
|
path to the file on the device that will be used for replacing
|
|
|
|
the entire running-config. Nexus 9K devices only support replace.
|
|
|
|
Use I(net_put) or I(nxos_file_copy) module to copy the flat file
|
|
|
|
to remote device and then use set the fullpath to this argument.
|
|
|
|
type: 'str'
|
2018-12-21 14:55:14 +00:00
|
|
|
backup:
|
|
|
|
description:
|
|
|
|
- This argument will cause the module to create a full backup of
|
|
|
|
the current running config from the remote device before any
|
2019-01-24 04:06:16 +00:00
|
|
|
changes are made. If the C(backup_options) value is not given,
|
|
|
|
the backup file is written to the C(backup) folder in the playbook
|
|
|
|
root directory or role root directory, if playbook is part of an
|
|
|
|
ansible role. If the directory does not exist, it is created.
|
2018-12-21 14:55:14 +00:00
|
|
|
type: bool
|
|
|
|
default: 'no'
|
|
|
|
version_added: "2.8"
|
2018-08-14 08:22:15 +00:00
|
|
|
rollback:
|
|
|
|
description:
|
|
|
|
- The C(rollback) argument instructs the module to rollback the
|
|
|
|
current configuration to the identifier specified in the
|
|
|
|
argument. If the specified rollback identifier does not
|
|
|
|
exist on the remote device, the module will fail. To rollback
|
|
|
|
to the most recent commit, set the C(rollback) argument to 0.
|
2018-08-29 15:30:11 +00:00
|
|
|
This option is mutually exclusive with C(config).
|
2018-08-14 08:22:15 +00:00
|
|
|
commit_comment:
|
|
|
|
description:
|
|
|
|
- The C(commit_comment) argument specifies a text string to be used
|
|
|
|
when committing the configuration. If the C(commit) argument
|
|
|
|
is set to False, this argument is silently ignored. This argument
|
|
|
|
is only valid for the platforms that support commit operation
|
|
|
|
with comment.
|
|
|
|
type: 'str'
|
|
|
|
defaults:
|
|
|
|
description:
|
|
|
|
- The I(defaults) argument will influence how the running-config
|
|
|
|
is collected from the device. When the value is set to true,
|
|
|
|
the command used to collect the running-config is append with
|
|
|
|
the all keyword. When the value is set to false, the command
|
|
|
|
is issued without the all keyword.
|
|
|
|
default: 'no'
|
|
|
|
type: 'bool'
|
|
|
|
multiline_delimiter:
|
|
|
|
description:
|
|
|
|
- This argument is used when pushing a multiline configuration
|
|
|
|
element to the device. It specifies the character to use as
|
|
|
|
the delimiting character. This only applies to the configuration
|
|
|
|
action.
|
|
|
|
type: 'str'
|
|
|
|
diff_replace:
|
|
|
|
description:
|
|
|
|
- Instructs the module on the way to perform the configuration
|
|
|
|
on the device. If the C(diff_replace) argument is set to I(line)
|
|
|
|
then the modified lines are pushed to the device in configuration
|
|
|
|
mode. If the argument is set to I(block) then the entire command
|
|
|
|
block is pushed to the device in configuration mode if any
|
|
|
|
line is not correct. Note that this parameter will be ignored if
|
|
|
|
the platform has onbox diff support.
|
|
|
|
choices: ['line', 'block', 'config']
|
|
|
|
diff_match:
|
|
|
|
description:
|
|
|
|
- Instructs the module on the way to perform the matching of
|
|
|
|
the set of commands against the current device config. If C(diff_match)
|
|
|
|
is set to I(line), commands are matched line by line. If C(diff_match)
|
|
|
|
is set to I(strict), command lines are matched with respect to position.
|
|
|
|
If C(diff_match) is set to I(exact), command lines must be an equal match.
|
|
|
|
Finally, if C(diff_match) is set to I(none), the module will not attempt
|
|
|
|
to compare the source configuration with the running configuration on the
|
|
|
|
remote device. Note that this parameter will be ignored if the platform
|
|
|
|
has onbox diff support.
|
|
|
|
choices: ['line', 'strict', 'exact', 'none']
|
|
|
|
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.
|
|
|
|
Note that this parameter will be ignored if the platform has onbox
|
|
|
|
diff support.
|
2019-01-24 04:06:16 +00:00
|
|
|
backup_options:
|
|
|
|
description:
|
|
|
|
- This is a dict object containing configurable options related to backup file path.
|
|
|
|
The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set
|
|
|
|
to I(no) this option will be silently ignored.
|
|
|
|
suboptions:
|
|
|
|
filename:
|
|
|
|
description:
|
|
|
|
- The filename to be used to store the backup configuration. If the the filename
|
|
|
|
is not given it will be generated based on the hostname, current time and date
|
|
|
|
in format defined by <hostname>_config.<current-date>@<current-time>
|
|
|
|
dir_path:
|
|
|
|
description:
|
|
|
|
- This option provides the path ending with directory name in which the backup
|
|
|
|
configuration file will be stored. If the directory does not exist it will be first
|
|
|
|
created and the filename is either the value of C(filename) or default filename
|
|
|
|
as described in C(filename) options description. If the path value is not given
|
|
|
|
in that case a I(backup) directory will be created in the current working directory
|
|
|
|
and backup configuration will be copied in C(filename) within I(backup) directory.
|
|
|
|
type: path
|
|
|
|
type: dict
|
|
|
|
version_added: "2.8"
|
2018-08-14 08:22:15 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
EXAMPLES = """
|
|
|
|
- name: configure device with config
|
|
|
|
cli_config:
|
|
|
|
config: "{{ lookup('template', 'basic/config.j2') }}"
|
|
|
|
|
|
|
|
- name: configure device with config with defaults enabled
|
|
|
|
cli_config:
|
|
|
|
config: "{{ lookup('template', 'basic/config.j2') }}"
|
|
|
|
defaults: yes
|
|
|
|
|
|
|
|
- name: Use diff_match
|
|
|
|
cli_config:
|
2018-09-07 11:39:08 +00:00
|
|
|
config: "{{ lookup('file', 'interface_config') }}"
|
2018-08-14 08:22:15 +00:00
|
|
|
diff_match: none
|
|
|
|
|
|
|
|
- name: nxos replace config
|
|
|
|
cli_config:
|
|
|
|
replace: 'bootflash:nxoscfg'
|
|
|
|
|
|
|
|
- name: commit with comment
|
|
|
|
cli_config:
|
|
|
|
config: set system host-name foo
|
|
|
|
commit_comment: this is a test
|
2019-01-24 04:06:16 +00:00
|
|
|
|
|
|
|
- name: configurable backup path
|
|
|
|
cli_config:
|
|
|
|
config: "{{ lookup('template', 'basic/config.j2') }}"
|
|
|
|
backup: yes
|
|
|
|
backup_options:
|
|
|
|
filename: backup.cfg
|
|
|
|
dir_path: /home/user
|
2018-08-14 08:22:15 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
RETURN = """
|
|
|
|
commands:
|
|
|
|
description: The set of commands that will be pushed to the remote device
|
|
|
|
returned: always
|
|
|
|
type: list
|
|
|
|
sample: ['interface Loopback999', 'no shutdown']
|
2018-12-21 14:55:14 +00:00
|
|
|
backup_path:
|
|
|
|
description: The full path to the backup file
|
|
|
|
returned: when backup is yes
|
|
|
|
type: str
|
|
|
|
sample: /playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34
|
2018-08-14 08:22:15 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
|
|
from ansible.module_utils.connection import Connection
|
|
|
|
from ansible.module_utils._text import to_text
|
|
|
|
|
|
|
|
|
|
|
|
def validate_args(module, capabilities):
|
|
|
|
"""validate param if it is supported on the platform
|
|
|
|
"""
|
|
|
|
if (module.params['replace'] and
|
|
|
|
not capabilities['device_operations']['supports_replace']):
|
|
|
|
module.fail_json(msg='replace is not supported on this platform')
|
|
|
|
|
2018-08-29 15:30:11 +00:00
|
|
|
if (module.params['rollback'] is not None and
|
2018-08-14 08:22:15 +00:00
|
|
|
not capabilities['device_operations']['supports_rollback']):
|
|
|
|
module.fail_json(msg='rollback is not supported on this platform')
|
|
|
|
|
|
|
|
if (module.params['commit_comment'] and
|
|
|
|
not capabilities['device_operations']['supports_commit_comment']):
|
|
|
|
module.fail_json(msg='commit_comment is not supported on this platform')
|
|
|
|
|
|
|
|
if (module.params['defaults'] and
|
|
|
|
not capabilities['device_operations']['supports_defaults']):
|
|
|
|
module.fail_json(msg='defaults is not supported on this platform')
|
|
|
|
|
|
|
|
if (module.params['multiline_delimiter'] and
|
|
|
|
not capabilities['device_operations']['supports_multiline_delimiter']):
|
|
|
|
module.fail_json(msg='multiline_delimiter is not supported on this platform')
|
|
|
|
|
|
|
|
if (module.params['diff_replace'] and
|
|
|
|
not capabilities['device_operations']['supports_diff_replace']):
|
|
|
|
module.fail_json(msg='diff_replace is not supported on this platform')
|
|
|
|
|
|
|
|
if (module.params['diff_match'] and
|
|
|
|
not capabilities['device_operations']['supports_diff_match']):
|
|
|
|
module.fail_json(msg='diff_match is not supported on this platform')
|
|
|
|
|
|
|
|
if (module.params['diff_ignore_lines'] and
|
|
|
|
not capabilities['device_operations']['supports_diff_ignore_lines']):
|
|
|
|
module.fail_json(msg='diff_ignore_lines is not supported on this platform')
|
|
|
|
|
|
|
|
|
2019-01-15 11:18:42 +00:00
|
|
|
def run(module, capabilities, connection, candidate, running, rollback_id):
|
2018-08-14 08:22:15 +00:00
|
|
|
result = {}
|
|
|
|
resp = {}
|
|
|
|
config_diff = []
|
|
|
|
banner_diff = {}
|
|
|
|
|
|
|
|
replace = module.params['replace']
|
|
|
|
commit_comment = module.params['commit_comment']
|
|
|
|
multiline_delimiter = module.params['multiline_delimiter']
|
|
|
|
diff_replace = module.params['diff_replace']
|
|
|
|
diff_match = module.params['diff_match']
|
|
|
|
diff_ignore_lines = module.params['diff_ignore_lines']
|
|
|
|
|
|
|
|
commit = not module.check_mode
|
|
|
|
|
|
|
|
if replace in ('yes', 'true', 'True'):
|
|
|
|
replace = True
|
|
|
|
elif replace in ('no', 'false', 'False'):
|
|
|
|
replace = False
|
|
|
|
|
2018-08-29 15:30:11 +00:00
|
|
|
if rollback_id is not None:
|
|
|
|
resp = connection.rollback(rollback_id, commit)
|
|
|
|
if 'diff' in resp:
|
|
|
|
result['changed'] = True
|
|
|
|
|
|
|
|
elif capabilities['device_operations']['supports_onbox_diff']:
|
2018-08-17 07:29:24 +00:00
|
|
|
if diff_replace:
|
|
|
|
module.warn('diff_replace is ignored as the device supports onbox diff')
|
|
|
|
if diff_match:
|
|
|
|
module.warn('diff_mattch is ignored as the device supports onbox diff')
|
|
|
|
if diff_ignore_lines:
|
|
|
|
module.warn('diff_ignore_lines is ignored as the device supports onbox diff')
|
|
|
|
|
|
|
|
if not isinstance(candidate, list):
|
|
|
|
candidate = candidate.strip('\n').splitlines()
|
|
|
|
|
|
|
|
kwargs = {'candidate': candidate, 'commit': commit, 'replace': replace,
|
|
|
|
'comment': commit_comment}
|
|
|
|
resp = connection.edit_config(**kwargs)
|
|
|
|
|
|
|
|
if 'diff' in resp:
|
|
|
|
result['changed'] = True
|
|
|
|
|
|
|
|
elif capabilities['device_operations']['supports_generate_diff']:
|
2018-08-14 08:22:15 +00:00
|
|
|
kwargs = {'candidate': candidate, 'running': running}
|
|
|
|
if diff_match:
|
|
|
|
kwargs.update({'diff_match': diff_match})
|
|
|
|
if diff_replace:
|
|
|
|
kwargs.update({'diff_replace': diff_replace})
|
|
|
|
if diff_ignore_lines:
|
|
|
|
kwargs.update({'diff_ignore_lines': diff_ignore_lines})
|
|
|
|
|
|
|
|
diff_response = connection.get_diff(**kwargs)
|
|
|
|
|
|
|
|
config_diff = diff_response.get('config_diff')
|
|
|
|
banner_diff = diff_response.get('banner_diff')
|
|
|
|
|
|
|
|
if config_diff:
|
|
|
|
if isinstance(config_diff, list):
|
|
|
|
candidate = config_diff
|
|
|
|
else:
|
|
|
|
candidate = config_diff.splitlines()
|
|
|
|
|
|
|
|
kwargs = {'candidate': candidate, 'commit': commit, 'replace': replace,
|
|
|
|
'comment': commit_comment}
|
|
|
|
connection.edit_config(**kwargs)
|
|
|
|
result['changed'] = True
|
|
|
|
|
|
|
|
if banner_diff:
|
|
|
|
candidate = json.dumps(banner_diff)
|
|
|
|
|
|
|
|
kwargs = {'candidate': candidate, 'commit': commit}
|
|
|
|
if multiline_delimiter:
|
|
|
|
kwargs.update({'multiline_delimiter': multiline_delimiter})
|
|
|
|
connection.edit_banner(**kwargs)
|
|
|
|
result['changed'] = True
|
|
|
|
|
|
|
|
if module._diff:
|
|
|
|
if 'diff' in resp:
|
|
|
|
result['diff'] = {'prepared': resp['diff']}
|
|
|
|
else:
|
|
|
|
diff = ''
|
|
|
|
if config_diff:
|
|
|
|
if isinstance(config_diff, list):
|
|
|
|
diff += '\n'.join(config_diff)
|
|
|
|
else:
|
|
|
|
diff += config_diff
|
|
|
|
if banner_diff:
|
|
|
|
diff += json.dumps(banner_diff)
|
|
|
|
result['diff'] = {'prepared': diff}
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
"""main entry point for execution
|
|
|
|
"""
|
2019-01-24 04:06:16 +00:00
|
|
|
backup_spec = dict(
|
|
|
|
filename=dict(),
|
|
|
|
dir_path=dict(type='path')
|
|
|
|
)
|
2018-08-14 08:22:15 +00:00
|
|
|
argument_spec = dict(
|
2018-12-21 14:55:14 +00:00
|
|
|
backup=dict(default=False, type='bool'),
|
2019-01-24 04:06:16 +00:00
|
|
|
backup_options=dict(type='dict', options=backup_spec),
|
2018-08-29 15:30:11 +00:00
|
|
|
config=dict(type='str'),
|
2018-08-14 08:22:15 +00:00
|
|
|
commit=dict(type='bool'),
|
|
|
|
replace=dict(type='str'),
|
|
|
|
rollback=dict(type='int'),
|
|
|
|
commit_comment=dict(type='str'),
|
|
|
|
defaults=dict(default=False, type='bool'),
|
|
|
|
multiline_delimiter=dict(type='str'),
|
|
|
|
diff_replace=dict(choices=['line', 'block', 'config']),
|
|
|
|
diff_match=dict(choices=['line', 'strict', 'exact', 'none']),
|
|
|
|
diff_ignore_lines=dict(type='list')
|
|
|
|
)
|
|
|
|
|
2018-08-29 15:30:11 +00:00
|
|
|
mutually_exclusive = [('config', 'rollback')]
|
2018-12-21 14:55:14 +00:00
|
|
|
required_one_of = [['backup', 'config', 'rollback']]
|
2018-08-29 15:30:11 +00:00
|
|
|
|
2018-08-14 08:22:15 +00:00
|
|
|
module = AnsibleModule(argument_spec=argument_spec,
|
2018-08-29 15:30:11 +00:00
|
|
|
mutually_exclusive=mutually_exclusive,
|
|
|
|
required_one_of=required_one_of,
|
2018-08-14 08:22:15 +00:00
|
|
|
supports_check_mode=True)
|
|
|
|
|
|
|
|
result = {'changed': False}
|
|
|
|
|
|
|
|
connection = Connection(module._socket_path)
|
|
|
|
capabilities = module.from_json(connection.get_capabilities())
|
|
|
|
|
|
|
|
if capabilities:
|
|
|
|
validate_args(module, capabilities)
|
|
|
|
|
|
|
|
if module.params['defaults']:
|
|
|
|
if 'get_default_flag' in capabilities.get('rpc'):
|
|
|
|
flags = connection.get_default_flag()
|
|
|
|
else:
|
|
|
|
flags = 'all'
|
|
|
|
else:
|
|
|
|
flags = []
|
|
|
|
|
2019-01-15 11:18:42 +00:00
|
|
|
candidate = module.params['config']
|
|
|
|
candidate = to_text(candidate, errors='surrogate_then_replace') if candidate else None
|
2018-08-14 08:22:15 +00:00
|
|
|
running = connection.get_config(flags=flags)
|
2019-01-15 11:18:42 +00:00
|
|
|
rollback_id = module.params['rollback']
|
2018-08-14 08:22:15 +00:00
|
|
|
|
2018-12-21 14:55:14 +00:00
|
|
|
if module.params['backup']:
|
|
|
|
result['__backup__'] = running
|
|
|
|
|
2019-01-15 11:18:42 +00:00
|
|
|
if candidate or rollback_id:
|
|
|
|
try:
|
|
|
|
result.update(run(module, capabilities, connection, candidate, running, rollback_id))
|
|
|
|
except Exception as exc:
|
|
|
|
module.fail_json(msg=to_text(exc))
|
2018-08-14 08:22:15 +00:00
|
|
|
|
|
|
|
module.exit_json(**result)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|