diff options
author | Bradley A. Thornton <bthornto@thethorntons.net> | 2019-08-08 10:51:33 -0700 |
---|---|---|
committer | Bradley A. Thornton <bthornto@thethorntons.net> | 2019-08-08 10:51:33 -0700 |
commit | ba99fb9d041f63f5d300956daabefeb0a86edb06 (patch) | |
tree | cedee2681cb3b629e4bf3cc56dbd3ec46aeee123 /plugins/modules/vyos_config.py | |
parent | 401cd33cbfa3cb623b5f6deb0f24911527d905c2 (diff) | |
download | vyos.vyos-ba99fb9d041f63f5d300956daabefeb0a86edb06.tar.gz vyos.vyos-ba99fb9d041f63f5d300956daabefeb0a86edb06.zip |
reroot
Diffstat (limited to 'plugins/modules/vyos_config.py')
-rw-r--r-- | plugins/modules/vyos_config.py | 344 |
1 files changed, 344 insertions, 0 deletions
diff --git a/plugins/modules/vyos_config.py b/plugins/modules/vyos_config.py new file mode 100644 index 0000000..6ed07fc --- /dev/null +++ b/plugins/modules/vyos_config.py @@ -0,0 +1,344 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: vyos_config +version_added: "2.2" +author: "Nathaniel Case (@qalthos)" +short_description: Manage VyOS configuration on remote device +description: + - This module provides configuration file management of VyOS + devices. It provides arguments for managing both the + configuration file and state of the active configuration. All + configuration statements are based on `set` and `delete` commands + in the device configuration. +extends_documentation_fragment: vyos +notes: + - Tested against VYOS 1.1.7 + - Abbreviated commands are NOT idempotent, see + L(Network FAQ,../network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands). +options: + lines: + description: + - The ordered set of configuration lines to be managed and + compared with the existing configuration on the remote + device. + src: + description: + - The C(src) argument specifies the path to the source config + file to load. The source config file can either be in + bracket format or set format. The source file can include + Jinja2 template variables. + match: + description: + - The C(match) argument controls the method used to match + against the current active configuration. By default, the + desired config is matched against the active config and the + deltas are loaded. If the C(match) argument is set to C(none) + the active configuration is ignored and the configuration is + always loaded. + default: line + choices: ['line', 'none'] + backup: + description: + - The C(backup) argument will backup the current devices active + configuration to the Ansible control host prior to making any + changes. If the C(backup_options) value is not given, the + backup file will be located in the 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. + type: bool + default: 'no' + comment: + description: + - Allows a commit description to be specified to be included + when the configuration is committed. If the configuration is + not changed or committed, this argument is ignored. + default: 'configured by vyos_config' + config: + description: + - The C(config) argument specifies the base configuration to use + to compare against the desired configuration. If this value + is not specified, the module will automatically retrieve the + current active configuration from the remote device. + save: + description: + - The C(save) argument controls whether or not changes made + to the active configuration are saved to disk. This is + independent of committing the config. When set to True, the + active configuration is saved. + type: bool + default: 'no' + 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" +""" + +EXAMPLES = """ +- name: configure the remote device + vyos_config: + lines: + - set system host-name {{ inventory_hostname }} + - set service lldp + - delete service dhcp-server + +- name: backup and load from file + vyos_config: + src: vyos.cfg + backup: yes + +- name: render a Jinja2 template onto the VyOS router + vyos_config: + src: vyos_template.j2 + +- name: for idempotency, use full-form commands + vyos_config: + lines: + # - set int eth eth2 description 'OUTSIDE' + - set interface ethernet eth2 description 'OUTSIDE' + +- name: configurable backup path + vyos_config: + backup: yes + backup_options: + filename: backup.cfg + dir_path: /home/user +""" + +RETURN = """ +commands: + description: The list of configuration commands sent to the device + returned: always + type: list + sample: ['...', '...'] +filtered: + description: The list of configuration commands removed to avoid a load failure + returned: always + type: list + sample: ['...', '...'] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: str + sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34 +filename: + description: The name of the backup file + returned: when backup is yes and filename is not specified in backup options + type: str + sample: vyos_config.2016-07-16@22:28:34 +shortname: + description: The full path to the backup file excluding the timestamp + returned: when backup is yes and filename is not specified in backup options + type: str + sample: /playbooks/ansible/backup/vyos_config +date: + description: The date extracted from the backup file name + returned: when backup is yes + type: str + sample: "2016-07-16" +time: + description: The time extracted from the backup file name + returned: when backup is yes + type: str + sample: "22:28:34" +""" +import re + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import ConnectionError +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, get_config, run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec, get_connection + + +DEFAULT_COMMENT = 'configured by vyos_config' + +CONFIG_FILTERS = [ + re.compile(r'set system login user \S+ authentication encrypted-password') +] + + +def get_candidate(module): + contents = module.params['src'] or module.params['lines'] + + if module.params['src']: + contents = format_commands(contents.splitlines()) + + contents = '\n'.join(contents) + return contents + + +def format_commands(commands): + return [line for line in commands if len(line.strip()) > 0] + + +def diff_config(commands, config): + config = [str(c).replace("'", '') for c in config.splitlines()] + + updates = list() + visited = set() + + for line in commands: + item = str(line).replace("'", '') + + if not item.startswith('set') and not item.startswith('delete'): + raise ValueError('line must start with either `set` or `delete`') + + elif item.startswith('set') and item not in config: + updates.append(line) + + elif item.startswith('delete'): + if not config: + updates.append(line) + else: + item = re.sub(r'delete', 'set', item) + for entry in config: + if entry.startswith(item) and line not in visited: + updates.append(line) + visited.add(line) + + return list(updates) + + +def sanitize_config(config, result): + result['filtered'] = list() + index_to_filter = list() + for regex in CONFIG_FILTERS: + for index, line in enumerate(list(config)): + if regex.search(line): + result['filtered'].append(line) + index_to_filter.append(index) + # Delete all filtered configs + for filter_index in sorted(index_to_filter, reverse=True): + del config[filter_index] + + +def run(module, result): + # get the current active config from the node or passed in via + # the config param + config = module.params['config'] or get_config(module) + + # create the candidate config object from the arguments + candidate = get_candidate(module) + + # create loadable config that includes only the configuration updates + connection = get_connection(module) + try: + response = connection.get_diff(candidate=candidate, running=config, diff_match=module.params['match']) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors='surrogate_then_replace')) + + commands = response.get('config_diff') + sanitize_config(commands, result) + + result['commands'] = commands + + commit = not module.check_mode + comment = module.params['comment'] + + diff = None + if commands: + diff = load_config(module, commands, commit=commit, comment=comment) + + if result.get('filtered'): + result['warnings'].append('Some configuration commands were ' + 'removed, please see the filtered key') + + result['changed'] = True + + if module._diff: + result['diff'] = {'prepared': diff} + + +def main(): + backup_spec = dict( + filename=dict(), + dir_path=dict(type='path') + ) + argument_spec = dict( + src=dict(type='path'), + lines=dict(type='list'), + + match=dict(default='line', choices=['line', 'none']), + + comment=dict(default=DEFAULT_COMMENT), + + config=dict(), + + backup=dict(type='bool', default=False), + backup_options=dict(type='dict', options=backup_spec), + save=dict(type='bool', default=False), + ) + + argument_spec.update(vyos_argument_spec) + + mutually_exclusive = [('lines', 'src')] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True + ) + + warnings = list() + + result = dict(changed=False, warnings=warnings) + + if module.params['backup']: + result['__backup__'] = get_config(module=module) + + if any((module.params['src'], module.params['lines'])): + run(module, result) + + if module.params['save']: + diff = run_commands(module, commands=['configure', 'compare saved'])[1] + if diff != '[edit]': + run_commands(module, commands=['save']) + result['changed'] = True + run_commands(module, commands=['exit']) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() |