summaryrefslogtreecommitdiff
path: root/plugins/modules
diff options
context:
space:
mode:
authorBradley A. Thornton <bthornto@thethorntons.net>2019-08-08 10:51:33 -0700
committerBradley A. Thornton <bthornto@thethorntons.net>2019-08-08 10:51:33 -0700
commitba99fb9d041f63f5d300956daabefeb0a86edb06 (patch)
treecedee2681cb3b629e4bf3cc56dbd3ec46aeee123 /plugins/modules
parent401cd33cbfa3cb623b5f6deb0f24911527d905c2 (diff)
downloadvyos-ansible-collection-ba99fb9d041f63f5d300956daabefeb0a86edb06.tar.gz
vyos-ansible-collection-ba99fb9d041f63f5d300956daabefeb0a86edb06.zip
reroot
Diffstat (limited to 'plugins/modules')
-rw-r--r--plugins/modules/__init__.py0
-rw-r--r--plugins/modules/vyos_banner.py178
-rw-r--r--plugins/modules/vyos_command.py222
-rw-r--r--plugins/modules/vyos_config.py344
-rw-r--r--plugins/modules/vyos_facts.py333
-rw-r--r--plugins/modules/vyos_interface.py438
-rw-r--r--plugins/modules/vyos_l3_interface.py285
-rw-r--r--plugins/modules/vyos_linkagg.py265
-rw-r--r--plugins/modules/vyos_lldp.py121
-rw-r--r--plugins/modules/vyos_lldp_interface.py228
-rw-r--r--plugins/modules/vyos_logging.py263
-rw-r--r--plugins/modules/vyos_ping.py246
-rw-r--r--plugins/modules/vyos_static_route.py265
-rw-r--r--plugins/modules/vyos_system.py211
-rw-r--r--plugins/modules/vyos_user.py340
-rw-r--r--plugins/modules/vyos_vlan.py332
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()