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 | |
parent | 401cd33cbfa3cb623b5f6deb0f24911527d905c2 (diff) | |
download | vyos-ansible-collection-ba99fb9d041f63f5d300956daabefeb0a86edb06.tar.gz vyos-ansible-collection-ba99fb9d041f63f5d300956daabefeb0a86edb06.zip |
reroot
Diffstat (limited to 'plugins/modules')
-rw-r--r-- | plugins/modules/__init__.py | 0 | ||||
-rw-r--r-- | plugins/modules/vyos_banner.py | 178 | ||||
-rw-r--r-- | plugins/modules/vyos_command.py | 222 | ||||
-rw-r--r-- | plugins/modules/vyos_config.py | 344 | ||||
-rw-r--r-- | plugins/modules/vyos_facts.py | 333 | ||||
-rw-r--r-- | plugins/modules/vyos_interface.py | 438 | ||||
-rw-r--r-- | plugins/modules/vyos_l3_interface.py | 285 | ||||
-rw-r--r-- | plugins/modules/vyos_linkagg.py | 265 | ||||
-rw-r--r-- | plugins/modules/vyos_lldp.py | 121 | ||||
-rw-r--r-- | plugins/modules/vyos_lldp_interface.py | 228 | ||||
-rw-r--r-- | plugins/modules/vyos_logging.py | 263 | ||||
-rw-r--r-- | plugins/modules/vyos_ping.py | 246 | ||||
-rw-r--r-- | plugins/modules/vyos_static_route.py | 265 | ||||
-rw-r--r-- | plugins/modules/vyos_system.py | 211 | ||||
-rw-r--r-- | plugins/modules/vyos_user.py | 340 | ||||
-rw-r--r-- | plugins/modules/vyos_vlan.py | 332 |
16 files changed, 4071 insertions, 0 deletions
diff --git a/plugins/modules/__init__.py b/plugins/modules/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/plugins/modules/__init__.py diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py new file mode 100644 index 0000000..06d5a30 --- /dev/null +++ b/plugins/modules/vyos_banner.py @@ -0,0 +1,178 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_banner +version_added: "2.4" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage multiline banners on VyOS devices +description: + - This will configure both pre-login and post-login banners on remote + devices running VyOS. It allows playbooks to add or remote + banner text from the active running configuration. +notes: + - Tested against VYOS 1.1.7 +options: + banner: + description: + - Specifies which banner that should be + configured on the remote device. + required: true + choices: ['pre-login', 'post-login'] + text: + description: + - The banner text that should be + present in the remote device running configuration. This argument + accepts a multiline string, with no empty lines. Requires I(state=present). + state: + description: + - Specifies whether or not the configuration is present in the current + devices active running configuration. + default: present + choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure the pre-login banner + vyos_banner: + banner: pre-login + text: | + this is my pre-login banner + that contains a multiline + string + state: present +- name: remove the post-login banner + vyos_banner: + banner: post-login + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - banner pre-login + - this is my pre-login banner + - that contains a multiline + - string +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def spec_to_commands(updates, module): + commands = list() + want, have = updates + state = module.params['state'] + + if state == 'absent': + if have.get('state') != 'absent' or (have.get('state') != 'absent' and + 'text' in have.keys() and have['text']): + commands.append('delete system login banner %s' % module.params['banner']) + + elif state == 'present': + if want['text'] and want['text'].encode().decode('unicode_escape') != have.get('text'): + banner_cmd = 'set system login banner %s ' % module.params['banner'] + banner_cmd += want['text'].strip() + commands.append(banner_cmd) + + return commands + + +def config_to_dict(module): + data = get_config(module) + output = None + obj = {'banner': module.params['banner'], 'state': 'absent'} + + for line in data.split('\n'): + if line.startswith('set system login banner %s' % obj['banner']): + match = re.findall(r'%s (.*)' % obj['banner'], line, re.M) + output = match + if output: + obj['text'] = output[0].encode().decode('unicode_escape') + obj['state'] = 'present' + + return obj + + +def map_params_to_obj(module): + text = module.params['text'] + if text: + text = "%r" % (str(text).strip()) + + return { + 'banner': module.params['banner'], + 'text': text, + 'state': module.params['state'] + } + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + banner=dict(required=True, choices=['pre-login', 'post-login']), + text=dict(), + state=dict(default='present', choices=['present', 'absent']) + ) + + argument_spec.update(vyos_argument_spec) + + required_if = [('state', 'present', ('text',))] + + module = AnsibleModule(argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = config_to_dict(module) + + commands = spec_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_command.py b/plugins/modules/vyos_command.py new file mode 100644 index 0000000..16487e3 --- /dev/null +++ b/plugins/modules/vyos_command.py @@ -0,0 +1,222 @@ +#!/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_command +version_added: "2.2" +author: "Nathaniel Case (@qalthos)" +short_description: Run one or more commands on VyOS devices +description: + - The command module allows running one or more commands on remote + devices running VyOS. This module can also be introspected + to validate key parameters before returning successfully. If the + conditional statements are not met in the wait period, the task + fails. + - Certain C(show) commands in VyOS produce many lines of output and + use a custom pager that can cause this module to hang. If the + value of the environment variable C(ANSIBLE_VYOS_TERMINAL_LENGTH) + is not set, the default number of 10000 is used. +extends_documentation_fragment: vyos +options: + commands: + description: + - The ordered set of commands to execute on the remote device + running VyOS. The output from the command execution is + returned to the playbook. If the I(wait_for) argument is + provided, the module is not returned until the condition is + satisfied or the number of retries has been exceeded. + required: true + wait_for: + description: + - Specifies what to evaluate from the output of the command + and what conditionals to apply. This argument will cause + the task to wait for a particular conditional to be true + before moving forward. If the conditional is not true + by the configured I(retries), the task fails. See examples. + aliases: ['waitfor'] + match: + description: + - The I(match) argument is used in conjunction with the + I(wait_for) argument to specify the match policy. Valid + values are C(all) or C(any). If the value is set to C(all) + then all conditionals in the wait_for must be satisfied. If + the value is set to C(any) then only one of the values must be + satisfied. + default: all + choices: ['any', 'all'] + retries: + description: + - Specifies the number of retries a command should be tried + before it is considered failed. The command is run on the + target device every retry and evaluated against the I(wait_for) + conditionals. + default: 10 + interval: + description: + - Configures the interval in seconds to wait between I(retries) + of the command. If the command does not pass the specified + conditions, the interval indicates how long to wait before + trying the command again. + default: 1 + +notes: + - Tested against VYOS 1.1.7 + - Running C(show system boot-messages all) will cause the module to hang since + VyOS is using a custom pager setting to display the output of that command. + - If a command sent to the device requires answering a prompt, it is possible + to pass a dict containing I(command), I(answer) and I(prompt). See examples. +""" + +EXAMPLES = """ +tasks: + - name: show configuration on ethernet devices eth0 and eth1 + vyos_command: + commands: + - show interfaces ethernet {{ item }} + with_items: + - eth0 + - eth1 + + - name: run multiple commands and check if version output contains specific version string + vyos_command: + commands: + - show version + - show hardware cpu + wait_for: + - "result[0] contains 'VyOS 1.1.7'" + + - name: run command that requires answering a prompt + vyos_command: + commands: + - command: 'rollback 1' + prompt: 'Proceed with reboot? [confirm][y]' + answer: y +""" + +RETURN = """ +stdout: + description: The set of responses from the commands + returned: always apart from low level errors (such as action plugin) + type: list + sample: ['...', '...'] +stdout_lines: + description: The value of stdout split into a list + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] +failed_conditions: + description: The list of conditionals that have failed + returned: failed + type: list + sample: ['...', '...'] +warnings: + description: The list of warnings (if any) generated by module based on arguments + returned: always + type: list + sample: ['...', '...'] +""" +import time + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.parsing import Conditional +from ansible.module_utils.network.common.utils import transform_commands, to_lines +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def parse_commands(module, warnings): + commands = transform_commands(module) + + if module.check_mode: + for item in list(commands): + if not item['command'].startswith('show'): + warnings.append( + 'Only show commands are supported when using check mode, not ' + 'executing %s' % item['command'] + ) + commands.remove(item) + + return commands + + +def main(): + spec = dict( + commands=dict(type='list', required=True), + + wait_for=dict(type='list', aliases=['waitfor']), + match=dict(default='all', choices=['all', 'any']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + spec.update(vyos_argument_spec) + + module = AnsibleModule(argument_spec=spec, supports_check_mode=True) + + warnings = list() + result = {'changed': False, 'warnings': warnings} + commands = parse_commands(module, warnings) + wait_for = module.params['wait_for'] or list() + + try: + conditionals = [Conditional(c) for c in wait_for] + except AttributeError as exc: + module.fail_json(msg=to_text(exc)) + + retries = module.params['retries'] + interval = module.params['interval'] + match = module.params['match'] + + for _ in range(retries): + responses = run_commands(module, commands) + + for item in list(conditionals): + if item(responses): + if match == 'any': + conditionals = list() + break + conditionals.remove(item) + + if not conditionals: + break + + time.sleep(interval) + + if conditionals: + failed_conditions = [item.raw for item in conditionals] + msg = 'One or more conditional statements have not been satisfied' + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + result.update({ + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)), + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() 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() diff --git a/plugins/modules/vyos_facts.py b/plugins/modules/vyos_facts.py new file mode 100644 index 0000000..c480969 --- /dev/null +++ b/plugins/modules/vyos_facts.py @@ -0,0 +1,333 @@ +#!/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_facts +version_added: "2.2" +author: "Nathaniel Case (@qalthos)" +short_description: Collect facts from remote devices running VyOS +description: + - Collects a base set of device facts from a remote device that + is running VyOS. This module prepends all of the + base network fact keys with U(ansible_net_<fact>). The facts + module will always collect a base set of facts from the device + and can enable or disable collection of additional facts. +extends_documentation_fragment: vyos +notes: + - Tested against VYOS 1.1.7 +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all, default, config, and neighbors. Can specify a list of + values to include a larger subset. Values can also be used + with an initial C(M(!)) to specify that a specific subset should + not be collected. + required: false + default: "!config" +""" + +EXAMPLES = """ +- name: collect all facts from the device + vyos_facts: + gather_subset: all + +- name: collect only the config and default facts + vyos_facts: + gather_subset: config + +- name: collect everything exception the config + vyos_facts: + gather_subset: "!config" +""" + +RETURN = """ +ansible_net_config: + description: The running-config from the device + returned: when config is configured + type: str +ansible_net_commits: + description: The set of available configuration revisions + returned: when present + type: list +ansible_net_hostname: + description: The configured system hostname + returned: always + type: str +ansible_net_model: + description: The device model string + returned: always + type: str +ansible_net_serialnum: + description: The serial number of the device + returned: always + type: str +ansible_net_version: + description: The version of the software running + returned: always + type: str +ansible_net_neighbors: + description: The set of LLDP neighbors + returned: when interface is configured + type: list +ansible_net_gather_subset: + description: The list of subsets gathered by the module + returned: always + type: list +ansible_net_api: + description: The name of the transport + returned: always + type: str +ansible_net_python_version: + description: The Python version Ansible controller is using + returned: always + type: str +""" + +import platform +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import run_commands, get_capabilities +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +class FactsBase(object): + + COMMANDS = frozenset() + + def __init__(self, module): + self.module = module + self.facts = dict() + self.responses = None + + def populate(self): + self.responses = run_commands(self.module, list(self.COMMANDS)) + + +class Default(FactsBase): + + COMMANDS = [ + 'show version', + ] + + def populate(self): + super(Default, self).populate() + data = self.responses[0] + self.facts['serialnum'] = self.parse_serialnum(data) + self.facts.update(self.platform_facts()) + + def parse_serialnum(self, data): + match = re.search(r'HW S/N:\s+(\S+)', data) + if match: + return match.group(1) + + def platform_facts(self): + platform_facts = {} + + resp = get_capabilities(self.module) + device_info = resp['device_info'] + + platform_facts['system'] = device_info['network_os'] + + for item in ('model', 'image', 'version', 'platform', 'hostname'): + val = device_info.get('network_os_%s' % item) + if val: + platform_facts[item] = val + + platform_facts['api'] = resp['network_api'] + platform_facts['python_version'] = platform.python_version() + + return platform_facts + + +class Config(FactsBase): + + COMMANDS = [ + 'show configuration commands', + 'show system commit', + ] + + def populate(self): + super(Config, self).populate() + + self.facts['config'] = self.responses + + commits = self.responses[1] + entries = list() + entry = None + + for line in commits.split('\n'): + match = re.match(r'(\d+)\s+(.+)by(.+)via(.+)', line) + if match: + if entry: + entries.append(entry) + + entry = dict(revision=match.group(1), + datetime=match.group(2), + by=str(match.group(3)).strip(), + via=str(match.group(4)).strip(), + comment=None) + else: + entry['comment'] = line.strip() + + self.facts['commits'] = entries + + +class Neighbors(FactsBase): + + COMMANDS = [ + 'show lldp neighbors', + 'show lldp neighbors detail', + ] + + def populate(self): + super(Neighbors, self).populate() + + all_neighbors = self.responses[0] + if 'LLDP not configured' not in all_neighbors: + neighbors = self.parse( + self.responses[1] + ) + self.facts['neighbors'] = self.parse_neighbors(neighbors) + + def parse(self, data): + parsed = list() + values = None + for line in data.split('\n'): + if not line: + continue + elif line[0] == ' ': + values += '\n%s' % line + elif line.startswith('Interface'): + if values: + parsed.append(values) + values = line + if values: + parsed.append(values) + return parsed + + def parse_neighbors(self, data): + facts = dict() + for item in data: + interface = self.parse_interface(item) + host = self.parse_host(item) + port = self.parse_port(item) + if interface not in facts: + facts[interface] = list() + facts[interface].append(dict(host=host, port=port)) + return facts + + def parse_interface(self, data): + match = re.search(r'^Interface:\s+(\S+),', data) + return match.group(1) + + def parse_host(self, data): + match = re.search(r'SysName:\s+(.+)$', data, re.M) + if match: + return match.group(1) + + def parse_port(self, data): + match = re.search(r'PortDescr:\s+(.+)$', data, re.M) + if match: + return match.group(1) + + +FACT_SUBSETS = dict( + default=Default, + neighbors=Neighbors, + config=Config +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + + +def main(): + argument_spec = dict( + gather_subset=dict(default=['!config'], type='list') + ) + + argument_spec.update(vyos_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + warnings = list() + + gather_subset = module.params['gather_subset'] + + runable_subsets = set() + exclude_subsets = set() + + for subset in gather_subset: + if subset == 'all': + runable_subsets.update(VALID_SUBSETS) + continue + + if subset.startswith('!'): + subset = subset[1:] + if subset == 'all': + exclude_subsets.update(VALID_SUBSETS) + continue + exclude = True + else: + exclude = False + + if subset not in VALID_SUBSETS: + module.fail_json(msg='Subset must be one of [%s], got %s' % + (', '.join(VALID_SUBSETS), subset)) + + if exclude: + exclude_subsets.add(subset) + else: + runable_subsets.add(subset) + + if not runable_subsets: + runable_subsets.update(VALID_SUBSETS) + + runable_subsets.difference_update(exclude_subsets) + runable_subsets.add('default') + + facts = dict() + facts['gather_subset'] = list(runable_subsets) + + instances = list() + for key in runable_subsets: + instances.append(FACT_SUBSETS[key](module)) + + for inst in instances: + inst.populate() + facts.update(inst.facts) + + ansible_facts = dict() + for key, value in iteritems(facts): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_interface.py b/plugins/modules/vyos_interface.py new file mode 100644 index 0000000..e7af0c1 --- /dev/null +++ b/plugins/modules/vyos_interface.py @@ -0,0 +1,438 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_interface +version_added: "2.4" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Manage Interface on VyOS network devices +description: + - This module provides declarative management of Interfaces + on VyOS network devices. +notes: + - Tested against VYOS 1.1.7 +options: + name: + description: + - Name of the Interface. + required: true + description: + description: + - Description of Interface. + enabled: + description: + - Interface link status. + type: bool + speed: + description: + - Interface link speed. + mtu: + description: + - Maximum size of transmit packet. + duplex: + description: + - Interface link status. + default: auto + choices: ['full', 'half', 'auto'] + delay: + description: + - Time in seconds to wait before checking for the operational state on remote + device. This wait is applicable for operational state argument which are + I(state) with values C(up)/C(down) and I(neighbors). + default: 10 + neighbors: + description: + - Check the operational state of given interface C(name) for LLDP neighbor. + - The following suboptions are available. + suboptions: + host: + description: + - "LLDP neighbor host for given interface C(name)." + port: + description: + - "LLDP neighbor port to which given interface C(name) is connected." + version_added: 2.5 + aggregate: + description: List of Interfaces definitions. + state: + description: + - State of the Interface configuration, C(up) means present and + operationally up and C(down) means present and operationally C(down) + default: present + choices: ['present', 'absent', 'up', 'down'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure interface + vyos_interface: + name: eth0 + description: test-interface + +- name: remove interface + vyos_interface: + name: eth0 + state: absent + +- name: make interface down + vyos_interface: + name: eth0 + enabled: False + +- name: make interface up + vyos_interface: + name: eth0 + enabled: True + +- name: Configure interface speed, mtu, duplex + vyos_interface: + name: eth5 + state: present + speed: 100 + mtu: 256 + duplex: full + +- name: Set interface using aggregate + vyos_interface: + aggregate: + - { name: eth1, description: test-interface-1, speed: 100, duplex: half, mtu: 512} + - { name: eth2, description: test-interface-2, speed: 1000, duplex: full, mtu: 256} + +- name: Disable interface on aggregate + net_interface: + aggregate: + - name: eth1 + - name: eth2 + enabled: False + +- name: Delete interface using aggregate + net_interface: + aggregate: + - name: eth1 + - name: eth2 + state: absent + +- name: Check lldp neighbors intent arguments + vyos_interface: + name: eth0 + neighbors: + - port: eth0 + host: netdev + +- name: Config + intent + vyos_interface: + name: eth1 + enabled: False + state: down +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - set interfaces ethernet eth0 description "test-interface" + - set interfaces ethernet eth0 speed 100 + - set interfaces ethernet eth0 mtu 256 + - set interfaces ethernet eth0 duplex full +""" +import re + +from copy import deepcopy +from time import sleep + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import exec_command +from ansible.module_utils.network.common.utils import conditional, remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, get_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def search_obj_in_list(name, lst): + for o in lst: + if o['name'] == name: + return o + + return None + + +def map_obj_to_commands(updates): + commands = list() + want, have = updates + + params = ('speed', 'description', 'duplex', 'mtu') + for w in want: + name = w['name'] + disable = w['disable'] + state = w['state'] + + obj_in_have = search_obj_in_list(name, have) + set_interface = 'set interfaces ethernet ' + name + delete_interface = 'delete interfaces ethernet ' + name + + if state == 'absent' and obj_in_have: + commands.append(delete_interface) + elif state in ('present', 'up', 'down'): + if obj_in_have: + for item in params: + value = w.get(item) + + if value and value != obj_in_have.get(item): + if item == 'description': + value = "\'" + str(value) + "\'" + commands.append(set_interface + ' ' + item + ' ' + str(value)) + + if disable and not obj_in_have.get('disable', False): + commands.append(set_interface + ' disable') + elif not disable and obj_in_have.get('disable', False): + commands.append(delete_interface + ' disable') + else: + commands.append(set_interface) + for item in params: + value = w.get(item) + if value: + if item == 'description': + value = "\'" + str(value) + "\'" + commands.append(set_interface + ' ' + item + ' ' + str(value)) + + if disable: + commands.append(set_interface + ' disable') + return commands + + +def map_config_to_obj(module): + data = get_config(module) + obj = [] + for line in data.split('\n'): + if line.startswith('set interfaces ethernet'): + match = re.search(r'set interfaces ethernet (\S+)', line, re.M) + name = match.group(1) + if name: + interface = {} + for item in obj: + if item['name'] == name: + interface = item + break + + if not interface: + interface = {'name': name} + obj.append(interface) + + match = re.search(r'%s (\S+)' % name, line, re.M) + if match: + param = match.group(1) + if param == 'description': + match = re.search(r'description (.+)', line, re.M) + description = match.group(1).strip("'") + interface['description'] = description + elif param == 'speed': + match = re.search(r'speed (\S+)', line, re.M) + speed = match.group(1).strip("'") + interface['speed'] = speed + elif param == 'mtu': + match = re.search(r'mtu (\S+)', line, re.M) + mtu = match.group(1).strip("'") + interface['mtu'] = int(mtu) + elif param == 'duplex': + match = re.search(r'duplex (\S+)', line, re.M) + duplex = match.group(1).strip("'") + interface['duplex'] = duplex + elif param.strip("'") == 'disable': + interface['disable'] = True + + return obj + + +def map_params_to_obj(module): + obj = [] + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + d = item.copy() + if d['enabled']: + d['disable'] = False + else: + d['disable'] = True + + obj.append(d) + else: + params = { + 'name': module.params['name'], + 'description': module.params['description'], + 'speed': module.params['speed'], + 'mtu': module.params['mtu'], + 'duplex': module.params['duplex'], + 'delay': module.params['delay'], + 'state': module.params['state'], + 'neighbors': module.params['neighbors'] + } + + if module.params['enabled']: + params.update({'disable': False}) + else: + params.update({'disable': True}) + + obj.append(params) + return obj + + +def check_declarative_intent_params(module, want, result): + failed_conditions = [] + have_neighbors = None + for w in want: + want_state = w.get('state') + want_neighbors = w.get('neighbors') + + if want_state not in ('up', 'down') and not want_neighbors: + continue + + if result['changed']: + sleep(w['delay']) + + command = 'show interfaces ethernet %s' % w['name'] + rc, out, err = exec_command(module, command) + if rc != 0: + module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc) + + if want_state in ('up', 'down'): + match = re.search(r'%s (\w+)' % 'state', out, re.M) + have_state = None + if match: + have_state = match.group(1) + if have_state is None or not conditional(want_state, have_state.strip().lower()): + failed_conditions.append('state ' + 'eq(%s)' % want_state) + + if want_neighbors: + have_host = [] + have_port = [] + if have_neighbors is None: + rc, have_neighbors, err = exec_command(module, 'show lldp neighbors detail') + if rc != 0: + module.fail_json(msg=to_text(err, errors='surrogate_then_replace'), command=command, rc=rc) + + if have_neighbors: + lines = have_neighbors.strip().split('Interface: ') + for line in lines: + field = line.split('\n') + if field[0].split(',')[0].strip() == w['name']: + for item in field: + if item.strip().startswith('SysName:'): + have_host.append(item.split(':')[1].strip()) + if item.strip().startswith('PortDescr:'): + have_port.append(item.split(':')[1].strip()) + for item in want_neighbors: + host = item.get('host') + port = item.get('port') + if host and host not in have_host: + failed_conditions.append('host ' + host) + if port and port not in have_port: + failed_conditions.append('port ' + port) + + return failed_conditions + + +def main(): + """ main entry point for module execution + """ + neighbors_spec = dict( + host=dict(), + port=dict() + ) + + element_spec = dict( + name=dict(), + description=dict(), + speed=dict(), + mtu=dict(type='int'), + duplex=dict(choices=['full', 'half', 'auto']), + enabled=dict(default=True, type='bool'), + neighbors=dict(type='list', elements='dict', options=neighbors_spec), + delay=dict(default=10, type='int'), + state=dict(default='present', + choices=['present', 'absent', 'up', 'down']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + argument_spec.update(vyos_argument_spec) + + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate']] + + required_together = [['speed', 'duplex']] + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + required_together=required_together, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have)) + result['commands'] = commands + + if commands: + commit = not module.check_mode + diff = load_config(module, commands, commit=commit) + if diff: + if module._diff: + result['diff'] = {'prepared': diff} + result['changed'] = True + + failed_conditions = check_declarative_intent_params(module, want, result) + + if failed_conditions: + msg = 'One or more conditional statements have not been satisfied' + module.fail_json(msg=msg, failed_conditions=failed_conditions) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_l3_interface.py b/plugins/modules/vyos_l3_interface.py new file mode 100644 index 0000000..98fbb4b --- /dev/null +++ b/plugins/modules/vyos_l3_interface.py @@ -0,0 +1,285 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_l3_interface +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage L3 interfaces on VyOS network devices +description: + - This module provides declarative management of L3 interfaces + on VyOS network devices. +notes: + - Tested against VYOS 1.1.7 +options: + name: + description: + - Name of the L3 interface. + ipv4: + description: + - IPv4 of the L3 interface. + ipv6: + description: + - IPv6 of the L3 interface. + aggregate: + description: List of L3 interfaces definitions + state: + description: + - State of the L3 interface configuration. + default: present + choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Set eth0 IPv4 address + vyos_l3_interface: + name: eth0 + ipv4: 192.168.0.1/24 + +- name: Remove eth0 IPv4 address + vyos_l3_interface: + name: eth0 + state: absent + +- name: Set IP addresses on aggregate + vyos_l3_interface: + aggregate: + - { name: eth1, ipv4: 192.168.2.10/24 } + - { name: eth2, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" } + +- name: Remove IP addresses on aggregate + vyos_l3_interface: + aggregate: + - { name: eth1, ipv4: 192.168.2.10/24 } + - { name: eth2, ipv4: 192.168.3.10/24, ipv6: "fd5d:12c9:2201:1::1/64" } + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - set interfaces ethernet eth0 address '192.168.0.1/24' +""" + +import socket +import re + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import is_masklen, validate_ip_address +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def is_ipv4(value): + if value: + address = value.split('/') + if is_masklen(address[1]) and validate_ip_address(address[0]): + return True + return False + + +def is_ipv6(value): + if value: + address = value.split('/') + if 0 <= int(address[1]) <= 128: + try: + socket.inet_pton(socket.AF_INET6, address[0]) + except socket.error: + return False + return True + return False + + +def search_obj_in_list(name, lst): + for o in lst: + if o['name'] == name: + return o + + return None + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + + for w in want: + name = w['name'] + ipv4 = w['ipv4'] + ipv6 = w['ipv6'] + state = w['state'] + + obj_in_have = search_obj_in_list(name, have) + + if state == 'absent' and obj_in_have: + if not ipv4 and not ipv6 and (obj_in_have['ipv4'] or obj_in_have['ipv6']): + if name == "lo": + commands.append('delete interfaces loopback lo address') + else: + commands.append('delete interfaces ethernet ' + name + ' address') + else: + if ipv4 and ipv4 in obj_in_have['ipv4']: + if name == "lo": + commands.append('delete interfaces loopback lo address ' + ipv4) + else: + commands.append('delete interfaces ethernet ' + name + ' address ' + ipv4) + if ipv6 and ipv6 in obj_in_have['ipv6']: + if name == "lo": + commands.append('delete interfaces loopback lo address ' + ipv6) + else: + commands.append('delete interfaces ethernet ' + name + ' address ' + ipv6) + elif (state == 'present' and obj_in_have): + if ipv4 and ipv4 not in obj_in_have['ipv4']: + if name == "lo": + commands.append('set interfaces loopback lo address ' + ipv4) + else: + commands.append('set interfaces ethernet ' + name + ' address ' + ipv4) + + if ipv6 and ipv6 not in obj_in_have['ipv6']: + if name == "lo": + commands.append('set interfaces loopback lo address ' + ipv6) + else: + commands.append('set interfaces ethernet ' + name + ' address ' + ipv6) + + return commands + + +def map_config_to_obj(module): + obj = [] + output = run_commands(module, ['show interfaces']) + lines = re.split(r'\n[e|l]', output[0])[1:] + + if len(lines) > 0: + for line in lines: + splitted_line = line.split() + + if len(splitted_line) > 0: + ipv4 = [] + ipv6 = [] + + if splitted_line[0].lower().startswith('th'): + name = 'e' + splitted_line[0].lower() + elif splitted_line[0].lower().startswith('o'): + name = 'l' + splitted_line[0].lower() + + for i in splitted_line[1:]: + if (('.' in i or ':' in i) and '/' in i): + value = i.split(r'\n')[0] + if is_ipv4(value): + ipv4.append(value) + elif is_ipv6(value): + ipv6.append(value) + + obj.append({'name': name, + 'ipv4': ipv4, + 'ipv6': ipv6}) + + return obj + + +def map_params_to_obj(module): + obj = [] + + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + obj.append(item.copy()) + else: + obj.append({ + 'name': module.params['name'], + 'ipv4': module.params['ipv4'], + 'ipv6': module.params['ipv6'], + 'state': module.params['state'] + }) + + return obj + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + name=dict(), + ipv4=dict(), + ipv6=dict(), + state=dict(default='present', + choices=['present', 'absent']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + argument_spec.update(vyos_argument_spec) + + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate']] + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_linkagg.py b/plugins/modules/vyos_linkagg.py new file mode 100644 index 0000000..75ffa77 --- /dev/null +++ b/plugins/modules/vyos_linkagg.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_linkagg +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage link aggregation groups on VyOS network devices +description: + - This module provides declarative management of link aggregation groups + on VyOS network devices. +notes: + - Tested against VYOS 1.1.7 +options: + name: + description: + - Name of the link aggregation group. + required: true + mode: + description: + - Mode of the link aggregation group. + choices: ['802.3ad', 'active-backup', 'broadcast', + 'round-robin', 'transmit-load-balance', + 'adaptive-load-balance', 'xor-hash', 'on'] + members: + description: + - List of members of the link aggregation group. + aggregate: + description: List of link aggregation definitions. + state: + description: + - State of the link aggregation group. + default: present + choices: ['present', 'absent', 'up', 'down'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure link aggregation group + vyos_linkagg: + name: bond0 + members: + - eth0 + - eth1 + +- name: remove configuration + vyos_linkagg: + name: bond0 + state: absent + +- name: Create aggregate of linkagg definitions + vyos_linkagg: + aggregate: + - { name: bond0, members: [eth1] } + - { name: bond1, members: [eth2] } + +- name: Remove aggregate of linkagg definitions + vyos_linkagg: + aggregate: + - name: bond0 + - name: bond1 + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - set interfaces bonding bond0 + - set interfaces ethernet eth0 bond-group 'bond0' + - set interfaces ethernet eth1 bond-group 'bond0' +""" +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def search_obj_in_list(name, lst): + for o in lst: + if o['name'] == name: + return o + + return None + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + + for w in want: + name = w['name'] + members = w.get('members') or [] + mode = w['mode'] + + if mode == 'on': + mode = '802.3ad' + + state = w['state'] + + obj_in_have = search_obj_in_list(name, have) + + if state == 'absent': + if obj_in_have: + for m in obj_in_have['members']: + commands.append('delete interfaces ethernet ' + m + ' bond-group') + + commands.append('delete interfaces bonding ' + name) + else: + if not obj_in_have: + commands.append('set interfaces bonding ' + name + ' mode ' + mode) + + for m in members: + commands.append('set interfaces ethernet ' + m + ' bond-group ' + name) + + if state == 'down': + commands.append('set interfaces bonding ' + name + ' disable') + else: + if mode != obj_in_have['mode']: + commands.append('set interfaces bonding ' + name + ' mode ' + mode) + + missing_members = list(set(members) - set(obj_in_have['members'])) + for m in missing_members: + commands.append('set interfaces ethernet ' + m + ' bond-group ' + name) + + if state == 'down' and obj_in_have['state'] == 'up': + commands.append('set interfaces bonding ' + name + ' disable') + elif state == 'up' and obj_in_have['state'] == 'down': + commands.append('delete interfaces bonding ' + name + ' disable') + + return commands + + +def map_config_to_obj(module): + obj = [] + output = run_commands(module, ['show interfaces bonding slaves']) + lines = output[0].splitlines() + + if len(lines) > 1: + for line in lines[1:]: + splitted_line = line.split() + + name = splitted_line[0] + mode = splitted_line[1] + state = splitted_line[2] + + if len(splitted_line) > 4: + members = splitted_line[4:] + else: + members = [] + + obj.append({'name': name, + 'mode': mode, + 'members': members, + 'state': state}) + + return obj + + +def map_params_to_obj(module): + obj = [] + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + obj.append(item.copy()) + else: + obj.append({ + 'name': module.params['name'], + 'mode': module.params['mode'], + 'members': module.params['members'], + 'state': module.params['state'] + }) + + return obj + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + name=dict(), + mode=dict(choices=['802.3ad', 'active-backup', 'broadcast', + 'round-robin', 'transmit-load-balance', + 'adaptive-load-balance', 'xor-hash', 'on'], + default='802.3ad'), + members=dict(type='list'), + state=dict(default='present', + choices=['present', 'absent', 'up', 'down']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + argument_spec.update(vyos_argument_spec) + + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate']] + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_lldp.py b/plugins/modules/vyos_lldp.py new file mode 100644 index 0000000..213e2ac --- /dev/null +++ b/plugins/modules/vyos_lldp.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_lldp +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage LLDP configuration on VyOS network devices +description: + - This module provides declarative management of LLDP service + on VyOS network devices. +notes: + - Tested against VYOS 1.1.7 +options: + state: + description: + - State of the LLDP configuration. + default: present + choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Enable LLDP service + vyos_lldp: + state: present + +- name: Disable LLDP service + vyos_lldp: + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - set service lldp +""" +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def has_lldp(module): + config = get_config(module).splitlines() + + if "set service 'lldp'" in config or 'set service lldp' in config: + return True + else: + return False + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + interfaces=dict(type='list'), + state=dict(default='present', + choices=['present', 'absent', + 'enabled', 'disabled']) + ) + + argument_spec.update(vyos_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + HAS_LLDP = has_lldp(module) + + commands = [] + + if module.params['state'] == 'absent' and HAS_LLDP: + commands.append('delete service lldp') + elif module.params['state'] == 'present' and not HAS_LLDP: + commands.append('set service lldp') + + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_lldp_interface.py b/plugins/modules/vyos_lldp_interface.py new file mode 100644 index 0000000..f0cad83 --- /dev/null +++ b/plugins/modules/vyos_lldp_interface.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_lldp_interface +version_added: "2.4" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" +short_description: Manage LLDP interfaces configuration on VyOS network devices +description: + - This module provides declarative management of LLDP interfaces + configuration on VyOS network devices. +notes: + - Tested against VYOS 1.1.7 +options: + name: + description: + - Name of the interface LLDP should be configured on. + aggregate: + description: List of interfaces LLDP should be configured on. + state: + description: + - State of the LLDP configuration. + default: present + choices: ['present', 'absent', 'enabled', 'disabled'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Enable LLDP on eth1 + net_lldp_interface: + state: present + +- name: Enable LLDP on specific interfaces + net_lldp_interface: + interfaces: + - eth1 + - eth2 + state: present + +- name: Disable LLDP globally + net_lldp_interface: + state: disabled + +- name: Create aggregate of LLDP interface configurations + vyos_lldp_interface: + aggregate: + - name: eth1 + - name: eth2 + state: present + +- name: Delete aggregate of LLDP interface configurations + vyos_lldp_interface: + aggregate: + - name: eth1 + - name: eth2 + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - set service lldp eth1 + - set service lldp eth2 disable +""" +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def search_obj_in_list(name, lst): + for o in lst: + if o['name'] == name: + return o + + return None + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + + for w in want: + name = w['name'] + state = w['state'] + + obj_in_have = search_obj_in_list(name, have) + + if state == 'absent' and obj_in_have: + commands.append('delete service lldp interface ' + name) + elif state in ('present', 'enabled'): + if not obj_in_have: + commands.append('set service lldp interface ' + name) + elif obj_in_have and obj_in_have['state'] == 'disabled' and state == 'enabled': + commands.append('delete service lldp interface ' + name + ' disable') + elif state == 'disabled': + if not obj_in_have: + commands.append('set service lldp interface ' + name) + commands.append('set service lldp interface ' + name + ' disable') + elif obj_in_have and obj_in_have['state'] != 'disabled': + commands.append('set service lldp interface ' + name + ' disable') + + return commands + + +def map_config_to_obj(module): + obj = [] + config = get_config(module).splitlines() + + output = [c for c in config if c.startswith("set service lldp interface")] + + for i in output: + splitted_line = i.split() + + if len(splitted_line) > 5: + new_obj = {'name': splitted_line[4]} + + if splitted_line[5] == "'disable'": + new_obj['state'] = 'disabled' + else: + new_obj = {'name': splitted_line[4][1:-1]} + new_obj['state'] = 'present' + + obj.append(new_obj) + + return obj + + +def map_params_to_obj(module): + obj = [] + + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + obj.append(item.copy()) + else: + obj.append({'name': module.params['name'], 'state': module.params['state']}) + + return obj + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + name=dict(), + state=dict(default='present', + choices=['present', 'absent', + 'enabled', 'disabled']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + argument_spec.update(vyos_argument_spec) + + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate']] + + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_logging.py b/plugins/modules/vyos_logging.py new file mode 100644 index 0000000..e7be395 --- /dev/null +++ b/plugins/modules/vyos_logging.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_logging +version_added: "2.4" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage logging on network devices +description: + - This module provides declarative management of logging + on Vyatta Vyos devices. +notes: + - Tested against VYOS 1.1.7 +options: + dest: + description: + - Destination of the logs. + choices: ['console', 'file', 'global', 'host', 'user'] + name: + description: + - If value of C(dest) is I(file) it indicates file-name, + for I(user) it indicates username and for I(host) indicates + the host name to be notified. + facility: + description: + - Set logging facility. + level: + description: + - Set logging severity levels. + aggregate: + description: List of logging definitions. + state: + description: + - State of the logging configuration. + default: present + choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure console logging + vyos_logging: + dest: console + facility: all + level: crit + +- name: remove console logging configuration + vyos_logging: + dest: console + state: absent + +- name: configure file logging + vyos_logging: + dest: file + name: test + facility: local3 + level: err + +- name: Add logging aggregate + vyos_logging: + aggregate: + - { dest: file, name: test1, facility: all, level: info } + - { dest: file, name: test2, facility: news, level: debug } + state: present + +- name: Remove logging aggregate + vyos_logging: + aggregate: + - { dest: console, facility: all, level: info } + - { dest: console, facility: daemon, level: warning } + - { dest: file, name: test2, facility: news, level: debug } + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - set system syslog global facility all level notice +""" + +import re + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def spec_to_commands(updates, module): + commands = list() + want, have = updates + + for w in want: + dest = w['dest'] + name = w['name'] + facility = w['facility'] + level = w['level'] + state = w['state'] + del w['state'] + + if state == 'absent' and w in have: + if w['name']: + commands.append('delete system syslog {0} {1} facility {2} level {3}'.format( + dest, name, facility, level)) + else: + commands.append('delete system syslog {0} facility {1} level {2}'.format( + dest, facility, level)) + elif state == 'present' and w not in have: + if w['name']: + commands.append('set system syslog {0} {1} facility {2} level {3}'.format( + dest, name, facility, level)) + else: + commands.append('set system syslog {0} facility {1} level {2}'.format( + dest, facility, level)) + + return commands + + +def config_to_dict(module): + data = get_config(module) + obj = [] + + for line in data.split('\n'): + if line.startswith('set system syslog'): + match = re.search(r'set system syslog (\S+)', line, re.M) + dest = match.group(1) + if dest == 'host': + match = re.search(r'host (\S+)', line, re.M) + name = match.group(1) + elif dest == 'file': + match = re.search(r'file (\S+)', line, re.M) + name = match.group(1) + elif dest == 'user': + match = re.search(r'user (\S+)', line, re.M) + name = match.group(1) + else: + name = None + + if 'facility' in line: + match = re.search(r'facility (\S+)', line, re.M) + facility = match.group(1) + if 'level' in line: + match = re.search(r'level (\S+)', line, re.M) + level = match.group(1).strip("'") + + obj.append({'dest': dest, + 'name': name, + 'facility': facility, + 'level': level}) + + return obj + + +def map_params_to_obj(module, required_if=None): + obj = [] + + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + module._check_required_if(required_if, item) + obj.append(item.copy()) + + else: + if module.params['dest'] not in ('host', 'file', 'user'): + module.params['name'] = None + + obj.append({ + 'dest': module.params['dest'], + 'name': module.params['name'], + 'facility': module.params['facility'], + 'level': module.params['level'], + 'state': module.params['state'] + }) + + return obj + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + dest=dict(type='str', choices=['console', 'file', 'global', 'host', 'user']), + name=dict(type='str'), + facility=dict(type='str'), + level=dict(type='str'), + state=dict(default='present', choices=['present', 'absent']), + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + + argument_spec.update(vyos_argument_spec) + required_if = [('dest', 'host', ['name', 'facility', 'level']), + ('dest', 'file', ['name', 'facility', 'level']), + ('dest', 'user', ['name', 'facility', 'level']), + ('dest', 'console', ['facility', 'level']), + ('dest', 'global', ['facility', 'level'])] + + module = AnsibleModule(argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + if warnings: + result['warnings'] = warnings + want = map_params_to_obj(module, required_if=required_if) + have = config_to_dict(module) + + commands = spec_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_ping.py b/plugins/modules/vyos_ping.py new file mode 100644 index 0000000..fe7bd9d --- /dev/null +++ b/plugins/modules/vyos_ping.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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/>. +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: vyos_ping +short_description: Tests reachability using ping from VyOS network devices +description: + - Tests reachability using ping from a VyOS device to a remote destination. + - Tested against VyOS 1.1.8 (helium) + - For a general purpose network module, see the M(net_ping) module. + - For Windows targets, use the M(win_ping) module instead. + - For targets running Python, use the M(ping) module instead. +author: + - Nilashish Chakraborty (@nilashishc) +version_added: '2.8' +options: + dest: + description: + - The IP Address or hostname (resolvable by the device) of the remote node. + required: true + count: + description: + - Number of packets to send to check reachability. + type: int + default: 5 + source: + description: + - The source interface or IP Address to use while sending the ping packet(s). + ttl: + description: + - The time-to-live value for the ICMP packet(s). + type: int + size: + description: + - Determines the size (in bytes) of the ping packet(s). + type: int + interval: + description: + - Determines the interval (in seconds) between consecutive pings. + type: int + state: + description: + - Determines if the expected result is success or fail. + choices: [ absent, present ] + default: present +notes: + - For a general purpose network module, see the M(net_ping) module. + - For Windows targets, use the M(win_ping) module instead. + - For targets running Python, use the M(ping) module instead. +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Test reachability to 10.10.10.10 + vyos_ping: + dest: 10.10.10.10 + +- name: Test reachability to 10.20.20.20 using source and ttl set + vyos_ping: + dest: 10.20.20.20 + source: eth0 + ttl: 128 + +- name: Test unreachability to 10.30.30.30 using interval + vyos_ping: + dest: 10.30.30.30 + interval: 3 + state: absent + +- name: Test reachability to 10.40.40.40 setting count and source + vyos_ping: + dest: 10.40.40.40 + source: eth1 + count: 20 + size: 512 +""" + +RETURN = """ +commands: + description: List of commands sent. + returned: always + type: list + sample: ["ping 10.8.38.44 count 10 interface eth0 ttl 128"] +packet_loss: + description: Percentage of packets lost. + returned: always + type: str + sample: "0%" +packets_rx: + description: Packets successfully received. + returned: always + type: int + sample: 20 +packets_tx: + description: Packets successfully transmitted. + returned: always + type: int + sample: 20 +rtt: + description: The round trip time (RTT) stats. + returned: when ping succeeds + type: dict + sample: {"avg": 2, "max": 8, "min": 1, "mdev": 24} +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec +import re + + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + count=dict(type="int", default=5), + dest=dict(type="str", required=True), + source=dict(type="str"), + ttl=dict(type='int'), + size=dict(type='int'), + interval=dict(type='int'), + state=dict(type="str", choices=["absent", "present"], default="present"), + ) + + argument_spec.update(vyos_argument_spec) + + module = AnsibleModule(argument_spec=argument_spec) + + count = module.params["count"] + dest = module.params["dest"] + source = module.params["source"] + size = module.params["size"] + ttl = module.params["ttl"] + interval = module.params["interval"] + + warnings = list() + + results = {} + if warnings: + results["warnings"] = warnings + + results["commands"] = [build_ping(dest, count, size, interval, source, ttl)] + + ping_results = run_commands(module, commands=results["commands"]) + ping_results_list = ping_results[0].split("\n") + + rtt_info, rate_info = None, None + for line in ping_results_list: + if line.startswith('rtt'): + rtt_info = line + if line.startswith('%s packets transmitted' % count): + rate_info = line + + if rtt_info: + rtt = parse_rtt(rtt_info) + for k, v in rtt.items(): + if rtt[k] is not None: + rtt[k] = int(v) + results["rtt"] = rtt + + pkt_loss, rx, tx = parse_rate(rate_info) + results["packet_loss"] = str(pkt_loss) + "%" + results["packets_rx"] = int(rx) + results["packets_tx"] = int(tx) + + validate_results(module, pkt_loss, results) + + module.exit_json(**results) + + +def build_ping(dest, count, size=None, interval=None, source=None, ttl=None): + cmd = "ping {0} count {1}".format(dest, str(count)) + + if source: + cmd += " interface {0}".format(source) + + if ttl: + cmd += " ttl {0}".format(str(ttl)) + + if size: + cmd += " size {0}".format(str(size)) + + if interval: + cmd += " interval {0}".format(str(interval)) + + return cmd + + +def parse_rate(rate_info): + rate_re = re.compile( + r"(?P<tx>\d+) (?:\w+) (?:\w+), (?P<rx>\d+) (?:\w+), (?P<pkt_loss>\d+)% (?:\w+) (?:\w+), (?:\w+) (?P<time>\d+)") + rate_err_re = re.compile( + r"(?P<tx>\d+) (?:\w+) (?:\w+), (?P<rx>\d+) (?:\w+), (?:[+-])(?P<err>\d+) (?:\w+), (?P<pkt_loss>\d+)% (?:\w+) (?:\w+), (?:\w+) (?P<time>\d+)") + + if rate_re.match(rate_info): + rate = rate_re.match(rate_info) + elif rate_err_re.match(rate_info): + rate = rate_err_re.match(rate_info) + + return rate.group("pkt_loss"), rate.group("rx"), rate.group("tx") + + +def parse_rtt(rtt_info): + rtt_re = re.compile( + r"rtt (?:.*)=(?:\s*)(?P<min>\d*).(?:\d*)/(?P<avg>\d*).(?:\d*)/(?P<max>\d+).(?:\d*)/(?P<mdev>\d*)") + rtt = rtt_re.match(rtt_info) + + return rtt.groupdict() + + +def validate_results(module, loss, results): + state = module.params["state"] + if state == "present" and int(loss) == 100: + module.fail_json(msg="Ping failed unexpectedly", **results) + elif state == "absent" and int(loss) < 100: + module.fail_json(msg="Ping succeeded unexpectedly", **results) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_static_route.py b/plugins/modules/vyos_static_route.py new file mode 100644 index 0000000..ec1c6c9 --- /dev/null +++ b/plugins/modules/vyos_static_route.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_static_route +version_added: "2.4" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage static IP routes on Vyatta VyOS network devices +description: + - This module provides declarative management of static + IP routes on Vyatta VyOS network devices. +notes: + - Tested against VYOS 1.1.7 +options: + prefix: + description: + - Network prefix of the static route. + C(mask) param should be ignored if C(prefix) is provided + with C(mask) value C(prefix/mask). + mask: + description: + - Network prefix mask of the static route. + next_hop: + description: + - Next hop IP of the static route. + admin_distance: + description: + - Admin distance of the static route. + aggregate: + description: List of static route definitions + state: + description: + - State of the static route configuration. + default: present + choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: configure static route + vyos_static_route: + prefix: 192.168.2.0 + mask: 24 + next_hop: 10.0.0.1 + +- name: configure static route prefix/mask + vyos_static_route: + prefix: 192.168.2.0/16 + next_hop: 10.0.0.1 + +- name: remove configuration + vyos_static_route: + prefix: 192.168.2.0 + mask: 16 + next_hop: 10.0.0.1 + state: absent + +- name: configure aggregates of static routes + vyos_static_route: + aggregate: + - { prefix: 192.168.2.0, mask: 24, next_hop: 10.0.0.1 } + - { prefix: 192.168.3.0, mask: 16, next_hop: 10.0.2.1 } + - { prefix: 192.168.3.0/16, next_hop: 10.0.2.1 } + +- name: Remove static route collections + vyos_static_route: + aggregate: + - { prefix: 172.24.1.0/24, next_hop: 192.168.42.64 } + - { prefix: 172.24.3.0/24, next_hop: 192.168.42.64 } + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - set protocols static route 192.168.2.0/16 next-hop 10.0.0.1 +""" +import re + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def spec_to_commands(updates, module): + commands = list() + want, have = updates + for w in want: + prefix = w['prefix'] + mask = w['mask'] + next_hop = w['next_hop'] + admin_distance = w['admin_distance'] + state = w['state'] + del w['state'] + + if state == 'absent' and w in have: + commands.append('delete protocols static route %s/%s' % (prefix, mask)) + elif state == 'present' and w not in have: + cmd = 'set protocols static route %s/%s next-hop %s' % (prefix, mask, next_hop) + if admin_distance != 'None': + cmd += ' distance %s' % (admin_distance) + commands.append(cmd) + + return commands + + +def config_to_dict(module): + data = get_config(module) + obj = [] + + for line in data.split('\n'): + if line.startswith('set protocols static route'): + match = re.search(r'static route (\S+)', line, re.M) + prefix = match.group(1).split('/')[0] + mask = match.group(1).split('/')[1] + if 'next-hop' in line: + match_hop = re.search(r'next-hop (\S+)', line, re.M) + next_hop = match_hop.group(1).strip("'") + + match_distance = re.search(r'distance (\S+)', line, re.M) + if match_distance is not None: + admin_distance = match_distance.group(1)[1:-1] + else: + admin_distance = None + + if admin_distance is not None: + obj.append({'prefix': prefix, + 'mask': mask, + 'next_hop': next_hop, + 'admin_distance': admin_distance}) + else: + obj.append({'prefix': prefix, + 'mask': mask, + 'next_hop': next_hop, + 'admin_distance': 'None'}) + + return obj + + +def map_params_to_obj(module, required_together=None): + obj = [] + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + module._check_required_together(required_together, item) + d = item.copy() + if '/' in d['prefix']: + d['mask'] = d['prefix'].split('/')[1] + d['prefix'] = d['prefix'].split('/')[0] + + if 'admin_distance' in d: + d['admin_distance'] = str(d['admin_distance']) + + obj.append(d) + else: + prefix = module.params['prefix'].strip() + if '/' in prefix: + mask = prefix.split('/')[1] + prefix = prefix.split('/')[0] + else: + mask = module.params['mask'].strip() + next_hop = module.params['next_hop'].strip() + admin_distance = str(module.params['admin_distance']) + state = module.params['state'] + + obj.append({ + 'prefix': prefix, + 'mask': mask, + 'next_hop': next_hop, + 'admin_distance': admin_distance, + 'state': state + }) + + return obj + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + prefix=dict(type='str'), + mask=dict(type='str'), + next_hop=dict(type='str'), + admin_distance=dict(type='int'), + state=dict(default='present', choices=['present', 'absent']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['prefix'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + argument_spec.update(vyos_argument_spec) + + required_one_of = [['aggregate', 'prefix']] + required_together = [['prefix', 'next_hop']] + mutually_exclusive = [['aggregate', 'prefix']] + + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + required_together=required_together, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + warnings = list() + + result = {'changed': False} + if warnings: + result['warnings'] = warnings + want = map_params_to_obj(module, required_together=required_together) + have = config_to_dict(module) + + commands = spec_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_system.py b/plugins/modules/vyos_system.py new file mode 100644 index 0000000..f1f093d --- /dev/null +++ b/plugins/modules/vyos_system.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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_system" +version_added: "2.3" +author: "Nathaniel Case (@qalthos)" +short_description: Run `set system` commands on VyOS devices +description: + - Runs one or more commands on remote devices running VyOS. + This module can also be introspected to validate key parameters before + returning successfully. +extends_documentation_fragment: vyos +notes: + - Tested against VYOS 1.1.7 +options: + host_name: + description: + - Configure the device hostname parameter. This option takes an ASCII string value. + domain_name: + description: + - The new domain name to apply to the device. + name_servers: + description: + - A list of name servers to use with the device. Mutually exclusive with + I(domain_search) + aliases: ['name_server'] + domain_search: + description: + - A list of domain names to search. Mutually exclusive with + I(name_server) + state: + description: + - Whether to apply (C(present)) or remove (C(absent)) the settings. + default: present + choices: ['present', 'absent'] +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - set system hostname vyos01 + - set system domain-name foo.example.com +""" + +EXAMPLES = """ +- name: configure hostname and domain-name + vyos_system: + host_name: vyos01 + domain_name: test.example.com + +- name: remove all configuration + vyos_system: + state: absent + +- name: configure name servers + vyos_system: + name_servers + - 8.8.8.8 + - 8.8.4.4 + +- name: configure domain search suffixes + vyos_system: + domain_search: + - sub1.example.com + - sub2.example.com +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def spec_key_to_device_key(key): + device_key = key.replace('_', '-') + + # domain-search is longer than just it's key + if device_key == 'domain-search': + device_key += ' domain' + + return device_key + + +def config_to_dict(module): + data = get_config(module) + + config = {'domain_search': [], 'name_server': []} + + for line in data.split('\n'): + if line.startswith('set system host-name'): + config['host_name'] = line[22:-1] + elif line.startswith('set system domain-name'): + config['domain_name'] = line[24:-1] + elif line.startswith('set system domain-search domain'): + config['domain_search'].append(line[33:-1]) + elif line.startswith('set system name-server'): + config['name_server'].append(line[24:-1]) + + return config + + +def spec_to_commands(want, have): + commands = [] + + state = want.pop('state') + + # state='absent' by itself has special meaning + if state == 'absent' and all(v is None for v in want.values()): + # Clear everything + for key in have: + commands.append('delete system %s' % spec_key_to_device_key(key)) + + for key in want: + if want[key] is None: + continue + + current = have.get(key) + proposed = want[key] + device_key = spec_key_to_device_key(key) + + # These keys are lists which may need to be reconciled with the device + if key in ['domain_search', 'name_server']: + if not proposed: + # Empty list was passed, delete all values + commands.append("delete system %s" % device_key) + for config in proposed: + if state == 'absent' and config in current: + commands.append("delete system %s '%s'" % (device_key, config)) + elif state == 'present' and config not in current: + commands.append("set system %s '%s'" % (device_key, config)) + else: + if state == 'absent' and current and proposed: + commands.append('delete system %s' % device_key) + elif state == 'present' and proposed and proposed != current: + commands.append("set system %s '%s'" % (device_key, proposed)) + + return commands + + +def map_param_to_obj(module): + return { + 'host_name': module.params['host_name'], + 'domain_name': module.params['domain_name'], + 'domain_search': module.params['domain_search'], + 'name_server': module.params['name_server'], + 'state': module.params['state'] + } + + +def main(): + argument_spec = dict( + host_name=dict(type='str'), + domain_name=dict(type='str'), + domain_search=dict(type='list'), + name_server=dict(type='list', aliases=['name_servers']), + state=dict(type='str', default='present', choices=['present', 'absent']), + ) + + argument_spec.update(vyos_argument_spec) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[('domain_name', 'domain_search')], + ) + + warnings = list() + + result = {'changed': False, 'warnings': warnings} + + want = map_param_to_obj(module) + have = config_to_dict(module) + + commands = spec_to_commands(want, have) + result['commands'] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py new file mode 100644 index 0000000..2981cef --- /dev/null +++ b/plugins/modules/vyos_user.py @@ -0,0 +1,340 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# 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_user +version_added: "2.4" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage the collection of local users on VyOS device +description: + - This module provides declarative management of the local usernames + configured on network devices. It allows playbooks to manage + either individual usernames or the collection of usernames in the + current running config. It also supports purging usernames from the + configuration that are not explicitly defined. +notes: + - Tested against VYOS 1.1.7 +options: + aggregate: + description: + - The set of username objects to be configured on the remote + VyOS device. The list entries can either be the username or + a hash of username and properties. This argument is mutually + exclusive with the C(name) argument. + aliases: ['users', 'collection'] + name: + description: + - The username to be configured on the VyOS device. + This argument accepts a string value and is mutually exclusive + with the C(aggregate) argument. + Please note that this option is not same as C(provider username). + full_name: + description: + - The C(full_name) argument provides the full name of the user + account to be created on the remote device. This argument accepts + any text string value. + configured_password: + description: + - The password to be configured on the VyOS device. The + password needs to be provided in clear and it will be encrypted + on the device. + Please note that this option is not same as C(provider password). + update_password: + description: + - Since passwords are encrypted in the device running config, this + argument will instruct the module when to change the password. When + set to C(always), the password will always be updated in the device + and when set to C(on_create) the password will be updated only if + the username is created. + default: always + choices: ['on_create', 'always'] + level: + description: + - The C(level) argument configures the level of the user when logged + into the system. This argument accepts string values admin or operator. + aliases: ['role'] + purge: + description: + - Instructs the module to consider the + resource definition absolute. It will remove any previously + configured usernames on the device with the exception of the + `admin` user (the current defined set of users). + type: bool + default: false + state: + description: + - Configures the state of the username definition + as it relates to the device operational configuration. When set + to I(present), the username(s) should be configured in the device active + configuration and when set to I(absent) the username(s) should not be + in the device active configuration + default: present + choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: create a new user + vyos_user: + name: ansible + configured_password: password + state: present +- name: remove all users except admin + vyos_user: + purge: yes +- name: set multiple users to level operator + vyos_user: + aggregate: + - name: netop + - name: netend + level: operator + state: present +- name: Change Password for User netop + vyos_user: + name: netop + configured_password: "{{ new_password }}" + update_password: always + state: present +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - set system login user test level operator + - set system login user authentication plaintext-password password +""" + +import re + +from copy import deepcopy +from functools import partial + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_config, load_config +from ansible.module_utils.six import iteritems +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def validate_level(value, module): + if value not in ('admin', 'operator'): + module.fail_json(msg='level must be either admin or operator, got %s' % value) + + +def spec_to_commands(updates, module): + commands = list() + state = module.params['state'] + update_password = module.params['update_password'] + + def needs_update(want, have, x): + return want.get(x) and (want.get(x) != have.get(x)) + + def add(command, want, x): + command.append('set system login user %s %s' % (want['name'], x)) + + for update in updates: + want, have = update + + if want['state'] == 'absent': + commands.append('delete system login user %s' % want['name']) + continue + + if needs_update(want, have, 'level'): + add(commands, want, "level %s" % want['level']) + + if needs_update(want, have, 'full_name'): + add(commands, want, "full-name %s" % want['full_name']) + + if needs_update(want, have, 'configured_password'): + if update_password == 'always' or not have: + add(commands, want, 'authentication plaintext-password %s' % want['configured_password']) + + return commands + + +def parse_level(data): + match = re.search(r'level (\S+)', data, re.M) + if match: + level = match.group(1)[1:-1] + return level + + +def parse_full_name(data): + match = re.search(r'full-name (\S+)', data, re.M) + if match: + full_name = match.group(1)[1:-1] + return full_name + + +def config_to_dict(module): + data = get_config(module) + + match = re.findall(r'^set system login user (\S+)', data, re.M) + if not match: + return list() + + instances = list() + + for user in set(match): + regex = r' %s .+$' % user + cfg = re.findall(regex, data, re.M) + cfg = '\n'.join(cfg) + obj = { + 'name': user, + 'state': 'present', + 'configured_password': None, + 'level': parse_level(cfg), + 'full_name': parse_full_name(cfg) + } + instances.append(obj) + + return instances + + +def get_param_value(key, item, module): + # if key doesn't exist in the item, get it from module.params + if not item.get(key): + value = module.params[key] + + # validate the param value (if validator func exists) + validator = globals().get('validate_%s' % key) + if all((value, validator)): + validator(value, module) + + return value + + +def map_params_to_obj(module): + aggregate = module.params['aggregate'] + if not aggregate: + if not module.params['name'] and module.params['purge']: + return list() + else: + users = [{'name': module.params['name']}] + else: + users = list() + for item in aggregate: + if not isinstance(item, dict): + users.append({'name': item}) + else: + users.append(item) + + objects = list() + + for item in users: + get_value = partial(get_param_value, item=item, module=module) + item['configured_password'] = get_value('configured_password') + item['full_name'] = get_value('full_name') + item['level'] = get_value('level') + item['state'] = get_value('state') + objects.append(item) + + return objects + + +def update_objects(want, have): + updates = list() + for entry in want: + item = next((i for i in have if i['name'] == entry['name']), None) + if item is None: + updates.append((entry, {})) + elif item: + for key, value in iteritems(entry): + if value and value != item[key]: + updates.append((entry, item)) + return updates + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + name=dict(), + + full_name=dict(), + level=dict(aliases=['role']), + + configured_password=dict(no_log=True), + update_password=dict(default='always', choices=['on_create', 'always']), + + state=dict(default='present', choices=['present', 'absent']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec, aliases=['users', 'collection']), + purge=dict(type='bool', default=False) + ) + + argument_spec.update(element_spec) + argument_spec.update(vyos_argument_spec) + + mutually_exclusive = [('name', 'aggregate')] + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + warnings = list() + if module.params['password'] and not module.params['configured_password']: + warnings.append( + 'The "password" argument is used to authenticate the current connection. ' + + 'To set a user password use "configured_password" instead.' + ) + + result = {'changed': False} + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = config_to_dict(module) + commands = spec_to_commands(update_objects(want, have), module) + + if module.params['purge']: + want_users = [x['name'] for x in want] + have_users = [x['name'] for x in have] + for item in set(have_users).difference(want_users): + commands.append('delete system login user %s' % item) + + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/vyos_vlan.py b/plugins/modules/vyos_vlan.py new file mode 100644 index 0000000..ca7bafa --- /dev/null +++ b/plugins/modules/vyos_vlan.py @@ -0,0 +1,332 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, 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: vyos_vlan +version_added: "2.5" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage VLANs on VyOS network devices +description: + - This module provides declarative management of VLANs + on VyOS network devices. +notes: + - Tested against VYOS 1.1.7 +options: + name: + description: + - Name of the VLAN. + address: + description: + - Configure Virtual interface address. + vlan_id: + description: + - ID of the VLAN. Range 0-4094. + required: true + interfaces: + description: + - List of interfaces that should be associated to the VLAN. + required: true + associated_interfaces: + description: + - This is a intent option and checks the operational state of the for given vlan C(name) + for associated interfaces. If the value in the C(associated_interfaces) does not match with + the operational state of vlan on device it will result in failure. + version_added: "2.5" + delay: + description: + - Delay the play should wait to check for declarative intent params values. + default: 10 + aggregate: + description: List of VLANs definitions. + purge: + description: + - Purge VLANs not defined in the I(aggregate) parameter. + default: no + type: bool + state: + description: + - State of the VLAN configuration. + default: present + choices: ['present', 'absent'] +extends_documentation_fragment: vyos +""" + +EXAMPLES = """ +- name: Create vlan + vyos_vlan: + vlan_id: 100 + name: vlan-100 + interfaces: eth1 + state: present + +- name: Add interfaces to VLAN + vyos_vlan: + vlan_id: 100 + interfaces: + - eth1 + - eth2 + +- name: Configure virtual interface address + vyos_vlan: + vlan_id: 100 + interfaces: eth1 + address: 172.26.100.37/24 + +- name: vlan interface config + intent + vyos_vlan: + vlan_id: 100 + interfaces: eth0 + associated_interfaces: + - eth0 + +- name: vlan intent check + vyos_vlan: + vlan_id: 100 + associated_interfaces: + - eth3 + - eth4 + +- name: Delete vlan + vyos_vlan: + vlan_id: 100 + interfaces: eth1 + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - set interfaces ethernet eth1 vif 100 description VLAN 100 + - set interfaces ethernet eth1 vif 100 address 172.26.100.37/24 + - delete interfaces ethernet eth1 vif 100 +""" +import re +import time + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import load_config, run_commands +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import vyos_argument_spec + + +def search_obj_in_list(vlan_id, lst): + obj = list() + for o in lst: + if o['vlan_id'] == vlan_id: + obj.append(o) + return obj + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + purge = module.params['purge'] + + for w in want: + vlan_id = w['vlan_id'] + name = w['name'] + address = w['address'] + state = w['state'] + interfaces = w['interfaces'] + + obj_in_have = search_obj_in_list(vlan_id, have) + + if state == 'absent': + if obj_in_have: + for obj in obj_in_have: + for i in obj['interfaces']: + commands.append('delete interfaces ethernet {0} vif {1}'.format(i, vlan_id)) + + elif state == 'present': + if not obj_in_have: + if w['interfaces'] and w['vlan_id']: + for i in w['interfaces']: + cmd = 'set interfaces ethernet {0} vif {1}'.format(i, vlan_id) + if w['name']: + commands.append(cmd + ' description {0}'.format(name)) + elif w['address']: + commands.append(cmd + ' address {0}'.format(address)) + else: + commands.append(cmd) + + if purge: + for h in have: + obj_in_want = search_obj_in_list(h['vlan_id'], want) + if not obj_in_want: + for i in h['interfaces']: + commands.append('delete interfaces ethernet {0} vif {1}'.format(i, h['vlan_id'])) + + return commands + + +def map_params_to_obj(module): + obj = [] + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + d = item.copy() + + if not d['vlan_id']: + module.fail_json(msg='vlan_id is required') + + d['vlan_id'] = str(d['vlan_id']) + module._check_required_one_of(module.required_one_of, item) + + obj.append(d) + else: + obj.append({ + 'vlan_id': str(module.params['vlan_id']), + 'name': module.params['name'], + 'address': module.params['address'], + 'state': module.params['state'], + 'interfaces': module.params['interfaces'], + 'associated_interfaces': module.params['associated_interfaces'] + }) + + return obj + + +def map_config_to_obj(module): + objs = [] + interfaces = list() + + output = run_commands(module, 'show interfaces') + lines = output[0].strip().splitlines()[3:] + + for l in lines: + splitted_line = re.split(r'\s{2,}', l.strip()) + obj = {} + + eth = splitted_line[0].strip("'") + if eth.startswith('eth'): + obj['interfaces'] = [] + if '.' in eth: + interface = eth.split('.')[0] + obj['interfaces'].append(interface) + obj['vlan_id'] = eth.split('.')[-1] + else: + obj['interfaces'].append(eth) + obj['vlan_id'] = None + + if splitted_line[1].strip("'") != '-': + obj['address'] = splitted_line[1].strip("'") + + if len(splitted_line) > 3: + obj['name'] = splitted_line[3].strip("'") + obj['state'] = 'present' + objs.append(obj) + + return objs + + +def check_declarative_intent_params(want, module, result): + + have = None + obj_interface = list() + is_delay = False + + for w in want: + if w.get('associated_interfaces') is None: + continue + + if result['changed'] and not is_delay: + time.sleep(module.params['delay']) + is_delay = True + + if have is None: + have = map_config_to_obj(module) + + obj_in_have = search_obj_in_list(w['vlan_id'], have) + if obj_in_have: + for obj in obj_in_have: + obj_interface.extend(obj['interfaces']) + + for w in want: + if w.get('associated_interfaces') is None: + continue + for i in w['associated_interfaces']: + if (set(obj_interface) - set(w['associated_interfaces'])) != set([]): + module.fail_json(msg='Interface {0} not configured on vlan {1}'.format(i, w['vlan_id'])) + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + vlan_id=dict(type='int'), + name=dict(), + address=dict(), + interfaces=dict(type='list'), + associated_interfaces=dict(type='list'), + delay=dict(default=10, type='int'), + state=dict(default='present', + choices=['present', 'absent']) + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + purge=dict(default=False, type='bool') + ) + + argument_spec.update(element_spec) + argument_spec.update(vyos_argument_spec) + + required_one_of = [['vlan_id', 'aggregate'], + ['aggregate', 'interfaces', 'associated_interfaces']] + + mutually_exclusive = [['vlan_id', 'aggregate']] + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive) + + warnings = list() + result = {'changed': False} + + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + load_config(module, commands, commit=commit) + result['changed'] = True + + check_declarative_intent_params(want, module, result) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() |