diff --git a/plugins/doc_fragments/vyos.py b/plugins/doc_fragments/vyos.py index 094963f..952c81d 100644 --- a/plugins/doc_fragments/vyos.py +++ b/plugins/doc_fragments/vyos.py @@ -1,63 +1,66 @@ # -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + +__metaclass__ = type # Copyright: (c) 2015, Peter Sprygada # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) class ModuleDocFragment(object): # Standard files documentation fragment DOCUMENTATION = r"""options: provider: description: - B(Deprecated) - 'Starting with Ansible 2.5 we recommend using C(connection: network_cli).' - For more information please see the L(Network Guide, ../network/getting_started/network_differences.html#multiple-communication-protocols). - HORIZONTALLINE - A dict object containing connection details. type: dict suboptions: host: description: - Specifies the DNS host name or address for connecting to the remote device over the specified transport. The value of host is used as the destination address for the transport. type: str required: true port: description: - Specifies the port to use when building the connection to the remote device. type: int default: 22 username: description: - Configures the username to use to authenticate the connection to the remote device. This value is used to authenticate the SSH session. If the value is not specified in the task, the value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead. type: str password: description: - Specifies the password to use to authenticate the connection to the remote device. This value is used to authenticate the SSH session. If the value is not specified in the task, the value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead. type: str timeout: description: - Specifies the timeout in seconds for communicating with the network device for either connecting or sending commands. If the timeout is exceeded before the operation is completed, the module will error. type: int default: 10 ssh_keyfile: description: - Specifies the SSH key to use to authenticate the connection to the remote device. This value is the path to the key used to authenticate the SSH session. If the value is not specified in the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE) will be used instead. type: path notes: - For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide ` """ diff --git a/plugins/module_utils/network/vyos/vyos.py b/plugins/module_utils/network/vyos/vyos.py index 908395a..7257b85 100644 --- a/plugins/module_utils/network/vyos/vyos.py +++ b/plugins/module_utils/network/vyos/vyos.py @@ -1,124 +1,127 @@ # This code is part of Ansible, but is an independent component. # This particular file snippet, and this file snippet only, is BSD licensed. # Modules you write using this snippet, which is embedded dynamically by Ansible # still belong to the author of the module, and may assign their own license # to the complete work. # # (c) 2016 Red Hat Inc. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # +from __future__ import absolute_import, division, print_function + +__metaclass__ = type import json from ansible.module_utils._text import to_text from ansible.module_utils.basic import env_fallback from ansible.module_utils.connection import Connection, ConnectionError _DEVICE_CONFIGS = {} vyos_provider_spec = { "host": dict(), "port": dict(type="int"), "username": dict(fallback=(env_fallback, ["ANSIBLE_NET_USERNAME"])), "password": dict( fallback=(env_fallback, ["ANSIBLE_NET_PASSWORD"]), no_log=True ), "ssh_keyfile": dict( fallback=(env_fallback, ["ANSIBLE_NET_SSH_KEYFILE"]), type="path" ), "timeout": dict(type="int"), } vyos_argument_spec = { "provider": dict( type="dict", options=vyos_provider_spec, removed_in_version=2.14 ), } def get_provider_argspec(): return vyos_provider_spec def get_connection(module): if hasattr(module, "_vyos_connection"): return module._vyos_connection capabilities = get_capabilities(module) network_api = capabilities.get("network_api") if network_api == "cliconf": module._vyos_connection = Connection(module._socket_path) else: module.fail_json(msg="Invalid connection type %s" % network_api) return module._vyos_connection def get_capabilities(module): if hasattr(module, "_vyos_capabilities"): return module._vyos_capabilities try: capabilities = Connection(module._socket_path).get_capabilities() except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) module._vyos_capabilities = json.loads(capabilities) return module._vyos_capabilities def get_config(module, flags=None, format=None): flags = [] if flags is None else flags global _DEVICE_CONFIGS if _DEVICE_CONFIGS != {}: return _DEVICE_CONFIGS else: connection = get_connection(module) try: out = connection.get_config(flags=flags, format=format) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) cfg = to_text(out, errors="surrogate_then_replace").strip() _DEVICE_CONFIGS = cfg return cfg def run_commands(module, commands, check_rc=True): connection = get_connection(module) try: response = connection.run_commands( commands=commands, check_rc=check_rc ) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) return response def load_config(module, commands, commit=False, comment=None): connection = get_connection(module) try: response = connection.edit_config( candidate=commands, commit=commit, comment=comment ) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) return response.get("diff") diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py index aa0bd5c..50cc6ee 100644 --- a/plugins/modules/vyos_banner.py +++ b/plugins/modules/vyos_banner.py @@ -1,193 +1,196 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + +__metaclass__ = type # (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 . # DOCUMENTATION = """ module: vyos_banner 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. version_added: 1.0.0 notes: - Tested against VyOS 1.1.8 (helium). - This module works with connection C(network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). 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.vyos.vyos """ EXAMPLES = """ - name: configure the pre-login banner vyos.vyos.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.vyos.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 index eec4344..03f476c 100644 --- a/plugins/modules/vyos_command.py +++ b/plugins/modules/vyos_command.py @@ -1,219 +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 . # +from __future__ import absolute_import, division, print_function + +__metaclass__ = type DOCUMENTATION = """ module: vyos_command 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. version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.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.8 (helium). - 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. - This module works with connection C(network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). """ EXAMPLES = """ - name: show configuration on ethernet devices eth0 and eth1 vyos.vyos.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.vyos.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.vyos.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_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import ( Conditional, ) from ansible_collections.ansible.netcommon.plugins.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 item 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 index baa63f3..a945d6a 100644 --- a/plugins/modules/vyos_config.py +++ b/plugins/modules/vyos_config.py @@ -1,350 +1,353 @@ #!/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 . # +from __future__ import absolute_import, division, print_function + +__metaclass__ = type DOCUMENTATION = """ module: vyos_config 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. version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.vyos notes: - Tested against VyOS 1.1.8 (helium). - This module works with connection C(network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). 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 filename is not given it will be generated based on the hostname, current time and date in format defined by _config.@ 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 """ EXAMPLES = """ - name: configure the remote device vyos.vyos.vyos_config: lines: - set system host-name {{ inventory_hostname }} - set service lldp - delete service dhcp-server - name: backup and load from file vyos.vyos.vyos_config: src: vyos.cfg backup: yes - name: render a Jinja2 template onto the VyOS router vyos.vyos.vyos_config: src: vyos_template.j2 - name: for idempotency, use full-form commands vyos.vyos.vyos_config: lines: # - set int eth eth2 description 'OUTSIDE' - set interface ethernet eth2 description 'OUTSIDE' - name: configurable backup path vyos.vyos.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): """ This function format the input commands and removes the prepend white spaces for command lines having 'set' or 'delete' and it skips empty lines. :param commands: :return: list of commands """ return [ line.strip() if line.split()[0] in ("set", "delete") else 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 index 7521a3b..b878c23 100644 --- a/plugins/modules/vyos_facts.py +++ b/plugins/modules/vyos_facts.py @@ -1,170 +1,173 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2019 Red Hat # 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 """ The module file for vyos_facts """ DOCUMENTATION = """ module: vyos_facts short_description: Get facts about vyos devices. description: - Collects facts from network devices running the vyos operating system. This module places the facts gathered in the fact tree keyed by the respective resource name. The facts module will always collect a base set of facts from the device and can enable or disable collection of additional facts. version_added: 1.0.0 author: - Nathaniel Case (@qalthos) - Nilashish Chakraborty (@Nilashishc) - Rohit Thakur (@rohitthakur2590) extends_documentation_fragment: - vyos.vyos.vyos notes: - Tested against VyOS 1.1.8 (helium). - This module works with connection C(network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). 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' gather_network_resources: description: - When supplied, this argument will restrict the facts collected to a given subset. Possible values for this argument include all and the resources like interfaces. 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. Valid subsets are 'all', 'interfaces', 'l3_interfaces', 'lag_interfaces', 'lldp_global', 'lldp_interfaces', 'static_routes', 'firewall_rules', 'firewall_global', 'firewall_interfaces', 'ospfv3', 'ospfv2'. required: false """ EXAMPLES = """ # Gather all facts - vyos.vyos.vyos_facts: gather_subset: all gather_network_resources: all # collect only the config and default facts - vyos.vyos.vyos_facts: gather_subset: config # collect everything exception the config - vyos.vyos.vyos_facts: gather_subset: '!config' # Collect only the interfaces facts - vyos.vyos.vyos_facts: gather_subset: - '!all' - '!min' gather_network_resources: - interfaces # Do not collect interfaces facts - vyos.vyos.vyos_facts: gather_network_resources: - '!interfaces' # Collect interfaces and minimal default facts - vyos.vyos.vyos_facts: gather_subset: min gather_network_resources: interfaces """ 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 ansible_net_gather_network_resources: description: The list of fact resource subsets collected from the device returned: always type: list """ from ansible.module_utils.basic import AnsibleModule from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.facts.facts import ( FactsArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( Facts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( vyos_argument_spec, ) def main(): """ Main entry point for module execution :returns: ansible_facts """ argument_spec = FactsArgs.argument_spec argument_spec.update(vyos_argument_spec) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True ) warnings = [] if module.params["gather_subset"] == "!config": warnings.append( "default value for `gather_subset` will be changed to `min` from `!config` v2.11 onwards" ) result = Facts(module).get_facts() ansible_facts, additional_warnings = result warnings.extend(additional_warnings) module.exit_json(ansible_facts=ansible_facts, warnings=warnings) if __name__ == "__main__": main() diff --git a/plugins/modules/vyos_logging.py b/plugins/modules/vyos_logging.py index 8732647..7b5d214 100644 --- a/plugins/modules/vyos_logging.py +++ b/plugins/modules/vyos_logging.py @@ -1,297 +1,300 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + +__metaclass__ = type # (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 . # DOCUMENTATION = """ module: vyos_logging author: Trishna Guha (@trishnaguha) short_description: Manage logging on network devices description: - This module provides declarative management of logging on Vyatta Vyos devices. version_added: 1.0.0 notes: - Tested against VyOS 1.1.8 (helium). - This module works with connection C(network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). 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.vyos.vyos """ EXAMPLES = """ - name: configure console logging vyos.vyos.vyos_logging: dest: console facility: all level: crit - name: remove console logging configuration vyos.vyos.vyos_logging: dest: console state: absent - name: configure file logging vyos.vyos.vyos_logging: dest: file name: test facility: local3 level: err - name: Add logging aggregate vyos.vyos.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.vyos.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_collections.ansible.netcommon.plugins.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_system.py b/plugins/modules/vyos_system.py index 53b04c8..220d1ff 100644 --- a/plugins/modules/vyos_system.py +++ b/plugins/modules/vyos_system.py @@ -1,215 +1,218 @@ #!/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 . # +from __future__ import absolute_import, division, print_function + +__metaclass__ = type DOCUMENTATION = """ module: vyos_system 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. version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.vyos notes: - Tested against VyOS 1.1.8 (helium). - This module works with connection C(network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). 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.vyos.vyos_system: host_name: vyos01 domain_name: test.example.com - name: remove all configuration vyos.vyos.vyos_system: state: absent - name: configure name servers vyos.vyos.vyos_system: name_servers - 8.8.8.8 - 8.8.4.4 - name: configure domain search suffixes vyos.vyos.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 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 index d788a99..58532eb 100644 --- a/plugins/modules/vyos_user.py +++ b/plugins/modules/vyos_user.py @@ -1,348 +1,351 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + +__metaclass__ = type # (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 . # DOCUMENTATION = """ module: vyos_user 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. version_added: 1.0.0 notes: - Tested against VyOS 1.1.8 (helium). - This module works with connection C(network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). 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.vyos.vyos """ EXAMPLES = """ - name: create a new user vyos.vyos.vyos_user: name: ansible configured_password: password state: present - name: remove all users except admin vyos.vyos.vyos_user: purge: yes - name: set multiple users to level operator vyos.vyos.vyos_user: aggregate: - name: netop - name: netend level: operator state: present - name: Change Password for User netop vyos.vyos.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_collections.ansible.netcommon.plugins.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() 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() result = {"changed": False, "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/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 86c7f86..527fc32 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -1,102 +1,76 @@ plugins/action/vyos.py action-plugin-docs # base class for deprecated network platform modules using `connection: local` -plugins/doc_fragments/vyos.py future-import-boilerplate -plugins/doc_fragments/vyos.py metaclass-boilerplate -plugins/modules/vyos_banner.py future-import-boilerplate -plugins/modules/vyos_banner.py metaclass-boilerplate plugins/modules/vyos_banner.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_banner.py validate-modules:doc-missing-type -plugins/modules/vyos_command.py future-import-boilerplate -plugins/modules/vyos_command.py metaclass-boilerplate plugins/modules/vyos_command.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_command.py validate-modules:doc-missing-type plugins/modules/vyos_command.py validate-modules:parameter-type-not-in-doc -plugins/modules/vyos_config.py future-import-boilerplate -plugins/modules/vyos_config.py metaclass-boilerplate plugins/modules/vyos_config.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_config.py validate-modules:doc-missing-type plugins/modules/vyos_config.py validate-modules:parameter-type-not-in-doc -plugins/modules/vyos_facts.py future-import-boilerplate -plugins/modules/vyos_facts.py metaclass-boilerplate plugins/modules/vyos_facts.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_facts.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_firewall_global.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_interface.py validate-modules:deprecation-mismatch plugins/modules/vyos_interface.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_interface.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_interface.py validate-modules:doc-missing-type plugins/modules/vyos_interface.py validate-modules:invalid-documentation plugins/modules/vyos_interface.py validate-modules:missing-suboption-docs plugins/modules/vyos_interface.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_interface.py validate-modules:undocumented-parameter plugins/modules/vyos_l3_interface.py validate-modules:deprecation-mismatch plugins/modules/vyos_l3_interface.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_l3_interface.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_l3_interface.py validate-modules:doc-missing-type plugins/modules/vyos_l3_interface.py validate-modules:invalid-documentation plugins/modules/vyos_l3_interface.py validate-modules:missing-suboption-docs plugins/modules/vyos_l3_interface.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_l3_interface.py validate-modules:undocumented-parameter plugins/modules/vyos_linkagg.py validate-modules:deprecation-mismatch plugins/modules/vyos_linkagg.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_linkagg.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_linkagg.py validate-modules:doc-missing-type plugins/modules/vyos_linkagg.py validate-modules:invalid-documentation plugins/modules/vyos_linkagg.py validate-modules:missing-suboption-docs plugins/modules/vyos_linkagg.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_linkagg.py validate-modules:undocumented-parameter plugins/modules/vyos_lldp_interface.py validate-modules:deprecation-mismatch plugins/modules/vyos_lldp_interface.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_lldp_interface.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_lldp_interface.py validate-modules:doc-missing-type plugins/modules/vyos_lldp_interface.py validate-modules:invalid-documentation plugins/modules/vyos_lldp_interface.py validate-modules:missing-suboption-docs plugins/modules/vyos_lldp_interface.py validate-modules:undocumented-parameter plugins/modules/vyos_lldp.py validate-modules:deprecation-mismatch plugins/modules/vyos_lldp.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_lldp.py validate-modules:invalid-documentation -plugins/modules/vyos_logging.py future-import-boilerplate -plugins/modules/vyos_logging.py metaclass-boilerplate plugins/modules/vyos_logging.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_logging.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_logging.py validate-modules:doc-missing-type plugins/modules/vyos_logging.py validate-modules:missing-suboption-docs plugins/modules/vyos_logging.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_logging.py validate-modules:undocumented-parameter plugins/modules/vyos_ping.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_ping.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_static_route.py validate-modules:deprecation-mismatch plugins/modules/vyos_static_route.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_static_route.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_static_route.py validate-modules:doc-missing-type plugins/modules/vyos_static_route.py validate-modules:invalid-documentation plugins/modules/vyos_static_route.py validate-modules:missing-suboption-docs plugins/modules/vyos_static_route.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_static_route.py validate-modules:undocumented-parameter -plugins/modules/vyos_system.py future-import-boilerplate -plugins/modules/vyos_system.py metaclass-boilerplate plugins/modules/vyos_system.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_system.py validate-modules:parameter-type-not-in-doc -plugins/modules/vyos_user.py future-import-boilerplate -plugins/modules/vyos_user.py metaclass-boilerplate plugins/modules/vyos_user.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_user.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_user.py validate-modules:doc-missing-type plugins/modules/vyos_user.py validate-modules:missing-suboption-docs plugins/modules/vyos_user.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_user.py validate-modules:undocumented-parameter plugins/modules/vyos_vlan.py validate-modules:doc-choices-do-not-match-spec plugins/modules/vyos_vlan.py validate-modules:doc-default-does-not-match-spec plugins/modules/vyos_vlan.py validate-modules:doc-missing-type plugins/modules/vyos_vlan.py validate-modules:missing-suboption-docs plugins/modules/vyos_vlan.py validate-modules:parameter-type-not-in-doc plugins/modules/vyos_vlan.py validate-modules:undocumented-parameter -plugins/module_utils/network/vyos/vyos.py future-import-boilerplate -plugins/module_utils/network/vyos/vyos.py metaclass-boilerplate -tests/unit/mock/path.py future-import-boilerplate -tests/unit/mock/path.py metaclass-boilerplate -tests/unit/mock/yaml_helper.py future-import-boilerplate -tests/unit/mock/yaml_helper.py metaclass-boilerplate -tests/unit/modules/conftest.py future-import-boilerplate -tests/unit/modules/conftest.py metaclass-boilerplate -tests/unit/modules/utils.py future-import-boilerplate -tests/unit/modules/utils.py metaclass-boilerplate diff --git a/tests/unit/mock/path.py b/tests/unit/mock/path.py index aea8ba1..74b02be 100644 --- a/tests/unit/mock/path.py +++ b/tests/unit/mock/path.py @@ -1,7 +1,10 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type from ansible_collections.vyos.vyos.tests.unit.compat.mock import MagicMock from ansible.utils.path import unfrackpath mock_unfrackpath_noop = MagicMock( spec_set=unfrackpath, side_effect=lambda x, *args, **kwargs: x ) diff --git a/tests/unit/mock/yaml_helper.py b/tests/unit/mock/yaml_helper.py index 1a945f1..5df30aa 100644 --- a/tests/unit/mock/yaml_helper.py +++ b/tests/unit/mock/yaml_helper.py @@ -1,164 +1,167 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type import io import yaml from ansible.module_utils.six import PY3 from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.dumper import AnsibleDumper class YamlTestUtils(object): """Mixin class to combine with a unittest.TestCase subclass.""" def _loader(self, stream): """Vault related tests will want to override this. Vault cases should setup a AnsibleLoader that has the vault password.""" return AnsibleLoader(stream) def _dump_stream(self, obj, stream, dumper=None): """Dump to a py2-unicode or py3-string stream.""" if PY3: return yaml.dump(obj, stream, Dumper=dumper) else: return yaml.dump(obj, stream, Dumper=dumper, encoding=None) def _dump_string(self, obj, dumper=None): """Dump to a py2-unicode or py3-string""" if PY3: return yaml.dump(obj, Dumper=dumper) else: return yaml.dump(obj, Dumper=dumper, encoding=None) def _dump_load_cycle(self, obj): # Each pass though a dump or load revs the 'generation' # obj to yaml string string_from_object_dump = self._dump_string(obj, dumper=AnsibleDumper) # wrap a stream/file like StringIO around that yaml stream_from_object_dump = io.StringIO(string_from_object_dump) loader = self._loader(stream_from_object_dump) # load the yaml stream to create a new instance of the object (gen 2) obj_2 = loader.get_data() # dump the gen 2 objects directory to strings string_from_object_dump_2 = self._dump_string( obj_2, dumper=AnsibleDumper ) # The gen 1 and gen 2 yaml strings self.assertEqual(string_from_object_dump, string_from_object_dump_2) # the gen 1 (orig) and gen 2 py object self.assertEqual(obj, obj_2) # again! gen 3... load strings into py objects stream_3 = io.StringIO(string_from_object_dump_2) loader_3 = self._loader(stream_3) obj_3 = loader_3.get_data() string_from_object_dump_3 = self._dump_string( obj_3, dumper=AnsibleDumper ) self.assertEqual(obj, obj_3) # should be transitive, but... self.assertEqual(obj_2, obj_3) self.assertEqual(string_from_object_dump, string_from_object_dump_3) def _old_dump_load_cycle(self, obj): """Dump the passed in object to yaml, load it back up, dump again, compare.""" stream = io.StringIO() yaml_string = self._dump_string(obj, dumper=AnsibleDumper) self._dump_stream(obj, stream, dumper=AnsibleDumper) yaml_string_from_stream = stream.getvalue() # reset stream stream.seek(0) loader = self._loader(stream) # loader = AnsibleLoader(stream, vault_password=self.vault_password) obj_from_stream = loader.get_data() stream_from_string = io.StringIO(yaml_string) loader2 = self._loader(stream_from_string) # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password) obj_from_string = loader2.get_data() stream_obj_from_stream = io.StringIO() stream_obj_from_string = io.StringIO() if PY3: yaml.dump( obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper ) yaml.dump( obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper ) else: yaml.dump( obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None, ) yaml.dump( obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None, ) yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue() yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue() stream_obj_from_stream.seek(0) stream_obj_from_string.seek(0) if PY3: yaml_string_obj_from_stream = yaml.dump( obj_from_stream, Dumper=AnsibleDumper ) yaml_string_obj_from_string = yaml.dump( obj_from_string, Dumper=AnsibleDumper ) else: yaml_string_obj_from_stream = yaml.dump( obj_from_stream, Dumper=AnsibleDumper, encoding=None ) yaml_string_obj_from_string = yaml.dump( obj_from_string, Dumper=AnsibleDumper, encoding=None ) assert yaml_string == yaml_string_obj_from_stream assert ( yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string ) assert ( yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream == yaml_string_stream_obj_from_string ) assert obj == obj_from_stream assert obj == obj_from_string assert obj == yaml_string_obj_from_stream assert obj == yaml_string_obj_from_string assert ( obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string ) return { "obj": obj, "yaml_string": yaml_string, "yaml_string_from_stream": yaml_string_from_stream, "obj_from_stream": obj_from_stream, "obj_from_string": obj_from_string, "yaml_string_obj_from_string": yaml_string_obj_from_string, } diff --git a/tests/unit/modules/conftest.py b/tests/unit/modules/conftest.py index ac56c9c..e19a1e0 100644 --- a/tests/unit/modules/conftest.py +++ b/tests/unit/modules/conftest.py @@ -1,37 +1,40 @@ # Copyright (c) 2017 Ansible Project # 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 import json import pytest from ansible.module_utils.six import string_types from ansible.module_utils._text import to_bytes from ansible.module_utils.common._collections_compat import MutableMapping @pytest.fixture def patch_ansible_module(request, mocker): if isinstance(request.param, string_types): args = request.param elif isinstance(request.param, MutableMapping): if "ANSIBLE_MODULE_ARGS" not in request.param: request.param = {"ANSIBLE_MODULE_ARGS": request.param} if "_ansible_remote_tmp" not in request.param["ANSIBLE_MODULE_ARGS"]: request.param["ANSIBLE_MODULE_ARGS"][ "_ansible_remote_tmp" ] = "/tmp" if ( "_ansible_keep_remote_files" not in request.param["ANSIBLE_MODULE_ARGS"] ): request.param["ANSIBLE_MODULE_ARGS"][ "_ansible_keep_remote_files" ] = False args = json.dumps(request.param) else: raise Exception( "Malformed data to the patch_ansible_module pytest fixture" ) mocker.patch("ansible.module_utils.basic._ANSIBLE_ARGS", to_bytes(args)) diff --git a/tests/unit/modules/utils.py b/tests/unit/modules/utils.py index 2c9c602..90023ce 100644 --- a/tests/unit/modules/utils.py +++ b/tests/unit/modules/utils.py @@ -1,48 +1,51 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type import json from ansible_collections.vyos.vyos.tests.unit.compat import unittest from ansible_collections.vyos.vyos.tests.unit.compat.mock import patch from ansible.module_utils import basic from ansible.module_utils._text import to_bytes def set_module_args(args): if "_ansible_remote_tmp" not in args: args["_ansible_remote_tmp"] = "/tmp" if "_ansible_keep_remote_files" not in args: args["_ansible_keep_remote_files"] = False args = json.dumps({"ANSIBLE_MODULE_ARGS": args}) basic._ANSIBLE_ARGS = to_bytes(args) class AnsibleExitJson(Exception): pass class AnsibleFailJson(Exception): pass def exit_json(*args, **kwargs): if "changed" not in kwargs: kwargs["changed"] = False raise AnsibleExitJson(kwargs) def fail_json(*args, **kwargs): kwargs["failed"] = True raise AnsibleFailJson(kwargs) class ModuleTestCase(unittest.TestCase): def setUp(self): self.mock_module = patch.multiple( basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json ) self.mock_module.start() self.mock_sleep = patch("time.sleep") self.mock_sleep.start() set_module_args({}) self.addCleanup(self.mock_module.stop) self.addCleanup(self.mock_sleep.stop)