diff --git a/plugins/cliconf/vyos.py b/plugins/cliconf/vyos.py index 3033603..de9e93d 100644 --- a/plugins/cliconf/vyos.py +++ b/plugins/cliconf/vyos.py @@ -1,342 +1,341 @@ # # (c) 2017 Red Hat Inc. # # 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 = """ ---- author: Ansible Networking Team cliconf: vyos short_description: Use vyos cliconf to run command on VyOS platform description: - - This vyos plugin provides low level abstraction apis for - sending and receiving CLI commands from VyOS network devices. -version_added: "2.4" +- This vyos plugin provides low level abstraction apis for sending and receiving CLI + commands from VyOS network devices. +version_added: 1.0.0 """ import re import json from ansible.errors import AnsibleConnectionFailure from ansible.module_utils._text import to_text from ansible.module_utils.common._collections_compat import Mapping from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( NetworkConfig, ) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( to_list, ) from ansible.plugins.cliconf import CliconfBase class Cliconf(CliconfBase): def get_device_info(self): device_info = {} device_info["network_os"] = "vyos" reply = self.get("show version") data = to_text(reply, errors="surrogate_or_strict").strip() match = re.search(r"Version:\s*(.*)", data) if match: device_info["network_os_version"] = match.group(1) match = re.search(r"HW model:\s*(\S+)", data) if match: device_info["network_os_model"] = match.group(1) reply = self.get("show host name") device_info["network_os_hostname"] = to_text( reply, errors="surrogate_or_strict" ).strip() return device_info def get_config(self, flags=None, format=None): if format: option_values = self.get_option_values() if format not in option_values["format"]: raise ValueError( "'format' value %s is invalid. Valid values of format are %s" % (format, ", ".join(option_values["format"])) ) if not flags: flags = [] if format == "text": command = "show configuration" else: command = "show configuration commands" command += " ".join(to_list(flags)) command = command.strip() out = self.send_command(command) return out def edit_config( self, candidate=None, commit=True, replace=None, comment=None ): resp = {} operations = self.get_device_operations() self.check_edit_config_capability( operations, candidate, commit, replace, comment ) results = [] requests = [] self.send_command("configure") for cmd in to_list(candidate): if not isinstance(cmd, Mapping): cmd = {"command": cmd} results.append(self.send_command(**cmd)) requests.append(cmd["command"]) out = self.get("compare") out = to_text(out, errors="surrogate_or_strict") diff_config = out if not out.startswith("No changes") else None if diff_config: if commit: try: self.commit(comment) except AnsibleConnectionFailure as e: msg = "commit failed: %s" % e.message self.discard_changes() raise AnsibleConnectionFailure(msg) else: self.send_command("exit") else: self.discard_changes() else: self.send_command("exit") if ( to_text( self._connection.get_prompt(), errors="surrogate_or_strict" ) .strip() .endswith("#") ): self.discard_changes() if diff_config: resp["diff"] = diff_config resp["response"] = results resp["request"] = requests return resp def get( self, command=None, prompt=None, answer=None, sendonly=False, output=None, newline=True, check_all=False, ): if not command: raise ValueError("must provide value of command to execute") if output: raise ValueError( "'output' value %s is not supported for get" % output ) return self.send_command( command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all, ) def commit(self, comment=None): if comment: command = 'commit comment "{0}"'.format(comment) else: command = "commit" self.send_command(command) def discard_changes(self): self.send_command("exit discard") def get_diff( self, candidate=None, running=None, diff_match="line", diff_ignore_lines=None, path=None, diff_replace=None, ): diff = {} device_operations = self.get_device_operations() option_values = self.get_option_values() if candidate is None and device_operations["supports_generate_diff"]: raise ValueError( "candidate configuration is required to generate diff" ) if diff_match not in option_values["diff_match"]: raise ValueError( "'match' value %s in invalid, valid values are %s" % (diff_match, ", ".join(option_values["diff_match"])) ) if diff_replace: raise ValueError("'replace' in diff is not supported") if diff_ignore_lines: raise ValueError("'diff_ignore_lines' in diff is not supported") if path: raise ValueError("'path' in diff is not supported") set_format = candidate.startswith("set") or candidate.startswith( "delete" ) candidate_obj = NetworkConfig(indent=4, contents=candidate) if not set_format: config = [c.line for c in candidate_obj.items] commands = list() # this filters out less specific lines for item in config: for index, entry in enumerate(commands): if item.startswith(entry): del commands[index] break commands.append(item) candidate_commands = [ "set %s" % cmd.replace(" {", "") for cmd in commands ] else: candidate_commands = str(candidate).strip().split("\n") if diff_match == "none": diff["config_diff"] = list(candidate_commands) return diff running_commands = [ str(c).replace("'", "") for c in running.splitlines() ] updates = list() visited = set() for line in candidate_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 running_commands: updates.append(line) elif item.startswith("delete"): if not running_commands: updates.append(line) else: item = re.sub(r"delete", "set", item) for entry in running_commands: if entry.startswith(item) and line not in visited: updates.append(line) visited.add(line) diff["config_diff"] = list(updates) return diff def run_commands(self, commands=None, check_rc=True): if commands is None: raise ValueError("'commands' value is required") responses = list() for cmd in to_list(commands): if not isinstance(cmd, Mapping): cmd = {"command": cmd} output = cmd.pop("output", None) if output: raise ValueError( "'output' value %s is not supported for run_commands" % output ) try: out = self.send_command(**cmd) except AnsibleConnectionFailure as e: if check_rc: raise out = getattr(e, "err", e) responses.append(out) return responses def get_device_operations(self): return { "supports_diff_replace": False, "supports_commit": True, "supports_rollback": False, "supports_defaults": False, "supports_onbox_diff": True, "supports_commit_comment": True, "supports_multiline_delimiter": False, "supports_diff_match": True, "supports_diff_ignore_lines": False, "supports_generate_diff": False, "supports_replace": False, } def get_option_values(self): return { "format": ["text", "set"], "diff_match": ["line", "none"], "diff_replace": [], "output": [], } def get_capabilities(self): result = super(Cliconf, self).get_capabilities() result["rpc"] += [ "commit", "discard_changes", "get_diff", "run_commands", ] result["device_operations"] = self.get_device_operations() result.update(self.get_option_values()) return json.dumps(result) def set_cli_prompt_context(self): """ Make sure we are in the operational cli mode :return: None """ if self._connection.connected: self._update_cli_prompt_context( config_context="#", exit_command="exit discard" ) diff --git a/plugins/modules/vyos_interface.py b/plugins/modules/vyos_interface.py index fe4fce3..be844c5 100644 --- a/plugins/modules/vyos_interface.py +++ b/plugins/modules/vyos_interface.py @@ -1,471 +1,473 @@ #!/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 . # from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_interface author: Ganesh Nalawade (@ganeshrn) -short_description: (deprecated) Manage Interface on VyOS network devices +short_description: (deprecated, removed after 2022-06-01) Manage Interface on VyOS + network devices description: - This module provides declarative management of Interfaces on VyOS network devices. version_added: 1.0.0 deprecated: - removed_in: '2.13' alternative: vyos_interfaces why: Updated modules released with more functionality. + removed_at_date: '2022-06-01' 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. 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.vyos.vyos + """ EXAMPLES = """ - name: configure interface vyos.vyos.vyos_interface: name: eth0 description: test-interface - name: remove interface vyos.vyos.vyos_interface: name: eth0 state: absent - name: make interface down vyos.vyos.vyos_interface: name: eth0 enabled: false - name: make interface up vyos.vyos.vyos_interface: name: eth0 enabled: true - name: Configure interface speed, mtu, duplex vyos.vyos.vyos_interface: name: eth5 state: present speed: 100 mtu: 256 duplex: full - name: Set interface using aggregate vyos.vyos.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.vyos.vyos_interface: name: eth0 neighbors: - port: eth0 host: netdev - name: Config + intent vyos.vyos.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_collections.ansible.netcommon.plugins.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, flags=["| grep interface"]) 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 index 2feb824..676d6ec 100644 --- a/plugins/modules/vyos_l3_interface.py +++ b/plugins/modules/vyos_l3_interface.py @@ -1,329 +1,331 @@ #!/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 . # from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_l3_interface author: Ricardo Carrillo Cruz (@rcarrillocruz) -short_description: (deprecated) Manage L3 interfaces on VyOS network devices +short_description: (deprecated, removed after 2022-06-01) Manage L3 interfaces on + VyOS network devices description: - This module provides declarative management of L3 interfaces on VyOS network devices. version_added: 1.0.0 deprecated: - removed_in: '2.13' alternative: vyos_l3_interfaces why: Updated modules released with more functionality. + removed_at_date: '2022-06-01' 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.vyos.vyos + """ EXAMPLES = """ - name: Set eth0 IPv4 address vyos.vyos.vyos_l3_interface: name: eth0 ipv4: 192.168.0.1/24 - name: Remove eth0 IPv4 address vyos.vyos.vyos_l3_interface: name: eth0 state: absent - name: Set IP addresses on aggregate vyos.vyos.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.vyos.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_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( is_masklen, validate_ip_address, ) 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 ( 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 index a68197b..4e63e2f 100644 --- a/plugins/modules/vyos_linkagg.py +++ b/plugins/modules/vyos_linkagg.py @@ -1,327 +1,329 @@ #!/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 . # from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_linkagg author: Ricardo Carrillo Cruz (@rcarrillocruz) -short_description: (deprecated) Manage link aggregation groups on VyOS network devices +short_description: (deprecated, removed after 2022-06-01) Manage link aggregation + groups on VyOS network devices description: - This module provides declarative management of link aggregation groups on VyOS network devices. version_added: 1.0.0 deprecated: - removed_in: '2.13' alternative: vyos_lag_interfaces why: Updated modules released with more functionality. + removed_at_date: '2022-06-01' notes: - Tested against VYOS 1.1.7 options: name: description: - Name of the link aggregation group. required: true type: str 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 type: str members: description: - List of members of the link aggregation group. type: list aggregate: description: List of link aggregation definitions. type: list state: description: - State of the link aggregation group. default: present choices: - present - absent - up - down type: str extends_documentation_fragment: - vyos.vyos.vyos + """ EXAMPLES = """ - name: configure link aggregation group vyos.vyos.vyos_linkagg: name: bond0 members: - eth0 - eth1 - name: remove configuration vyos.vyos.vyos_linkagg: name: bond0 state: absent - name: Create aggregate of linkagg definitions vyos.vyos.vyos_linkagg: aggregate: - {name: bond0, members: [eth1]} - {name: bond1, members: [eth2]} - name: Remove aggregate of linkagg definitions vyos.vyos.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_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 ( 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 index 5b697c2..9b138c1 100644 --- a/plugins/modules/vyos_lldp.py +++ b/plugins/modules/vyos_lldp.py @@ -1,141 +1,143 @@ #!/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 . # from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_lldp author: Ricardo Carrillo Cruz (@rcarrillocruz) -short_description: (deprecated) Manage LLDP configuration on VyOS network devices +short_description: (deprecated, removed after 2022-06-01) Manage LLDP configuration + on VyOS network devices description: - This module provides declarative management of LLDP service on VyOS network devices. version_added: 1.0.0 deprecated: - removed_in: '2.13' alternative: vyos_lldp_global why: Updated modules released with more functionality. + removed_at_date: '2022-06-01' notes: - Tested against VYOS 1.1.7 options: interfaces: description: - Name of the interfaces. type: list state: description: - State of the link aggregation group. default: present choices: - present - absent - enabled - disabled type: str extends_documentation_fragment: - vyos.vyos.vyos + """ EXAMPLES = """ - name: Enable LLDP service vyos.vyos.vyos_lldp: state: present - name: Disable LLDP service vyos.vyos.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 index 90e123d..2fd1bc5 100644 --- a/plugins/modules/vyos_lldp_interface.py +++ b/plugins/modules/vyos_lldp_interface.py @@ -1,264 +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 . # from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_lldp_interface author: Ricardo Carrillo Cruz (@rcarrillocruz) -short_description: (deprecated) Manage LLDP interfaces configuration on VyOS network - devices +short_description: (deprecated, removed after 2022-06-01) Manage LLDP interfaces configuration + on VyOS network devices description: - This module provides declarative management of LLDP interfaces configuration on VyOS network devices. version_added: 1.0.0 deprecated: - removed_in: '2.13' alternative: vyos_lldp_interfaces why: Updated modules released with more functionality. + removed_at_date: '2022-06-01' notes: - Tested against VYOS 1.1.7 options: name: description: - Name of the interface LLDP should be configured on. type: str aggregate: description: List of interfaces LLDP should be configured on. type: list state: description: - State of the LLDP configuration. default: present choices: - present - absent - enabled - disabled type: str extends_documentation_fragment: - vyos.vyos.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.vyos.vyos_lldp_interface: aggregate: - name: eth1 - name: eth2 state: present - name: Delete aggregate of LLDP interface configurations vyos.vyos.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_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 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_static_route.py b/plugins/modules/vyos_static_route.py index 4724d5f..67f9954 100644 --- a/plugins/modules/vyos_static_route.py +++ b/plugins/modules/vyos_static_route.py @@ -1,302 +1,304 @@ #!/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 . # from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_static_route author: Trishna Guha (@trishnaguha) -short_description: (deprecated) Manage static IP routes on Vyatta VyOS network devices +short_description: (deprecated, removed after 2022-06-01) Manage static IP routes + on Vyatta VyOS network devices description: - This module provides declarative management of static IP routes on Vyatta VyOS network devices. version_added: 1.0.0 deprecated: - removed_in: '2.13' alternative: vyos_static_routes why: Updated modules released with more functionality. + removed_at_date: '2022-06-01' 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: 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). type: str mask: description: - Network prefix mask of the static route. type: str next_hop: description: - Next hop IP of the static route. type: str admin_distance: description: - Admin distance of the static route. type: int aggregate: description: List of static route definitions type: list state: description: - State of the static route configuration. default: present choices: - present - absent type: str extends_documentation_fragment: - vyos.vyos.vyos + """ EXAMPLES = """ - name: configure static route vyos.vyos.vyos_static_route: prefix: 192.168.2.0 mask: 24 next_hop: 10.0.0.1 - name: configure static route prefix/mask vyos.vyos.vyos_static_route: prefix: 192.168.2.0/16 next_hop: 10.0.0.1 - name: remove configuration vyos.vyos.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.vyos.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.vyos.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_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: 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()