diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-06-04 06:48:22 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-04 06:48:22 +1000 |
| commit | 4d03dd58cd5d14b8f8094e2490034b31187aea2b (patch) | |
| tree | b42be5a26b1881fd81542d510a88aef8d4247d3b /plugins | |
| parent | 6a660fbbb1a855ca88b4926eda118f28f61518ca (diff) | |
| parent | cc8b7230098feb057ce5d903ded00ac0a7e648c0 (diff) | |
| download | rest.vyos-4d03dd58cd5d14b8f8094e2490034b31187aea2b.tar.gz rest.vyos-4d03dd58cd5d14b8f8094e2490034b31187aea2b.zip | |
Merge pull request #3 from vyos/init
Init Collection
Diffstat (limited to 'plugins')
36 files changed, 891 insertions, 7074 deletions
diff --git a/plugins/httpapi/vyos.py b/plugins/httpapi/vyos.py index 1719254..07a60fe 100644 --- a/plugins/httpapi/vyos.py +++ b/plugins/httpapi/vyos.py @@ -1,6 +1,5 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- -# GNU General Public License v3.0+ +# 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 @@ -90,13 +89,14 @@ class HttpApi(HttpApiBase): ) return key - def send_request(self, endpoint, **payload): + def send_request(self, data, **payload): # pylint: disable=arguments-renamed """POST to a VyOS REST endpoint. Args: - endpoint (str): API path, e.g. '/configure' or '/retrieve'. - Named 'endpoint' not 'path' to avoid collision - with the VyOS payload field also called 'path'. + data (str): API path, e.g. '/configure' or '/retrieve'. + Named 'data' to match the HttpApiBase signature. + Internally referred to as endpoint to avoid collision + with the VyOS payload field also called 'data'. **payload: VyOS API fields: op, path, value, url, file, etc. Returns: @@ -105,11 +105,16 @@ class HttpApi(HttpApiBase): Raises: ConnectionError: on HTTP error or VyOS success=false response. """ + endpoint = data + try: api_key = self._get_api_key() - body = json.dumps(payload) - form_data = urlencode({"data": body, "key": api_key}) + if "_raw_list" in payload: + body = json.dumps(payload["_raw_list"]) + else: + body = json.dumps(payload) + form_data = urlencode({"data": body, "key": api_key}) response, response_data = self.connection.send( endpoint, data=form_data, diff --git a/plugins/module_utils/vyos.py b/plugins/module_utils/vyos.py index 87fc37b..82670e7 100644 --- a/plugins/module_utils/vyos.py +++ b/plugins/module_utils/vyos.py @@ -62,58 +62,75 @@ class VyOSModule: # Write # ------------------------------------------------------------------ - def apply_commands(self, commands): - """Execute a list of ``(op, path)`` command tuples against the device. - - Args: - commands (list): Each item is a 2-tuple ``(op, path)`` where: - - *op* is ``"set"`` or ``"delete"`` - - *path* is a list of config path tokens - - Optionally a 3-tuple ``(op, path, value)`` for leaf nodes - that carry a value. - - Returns: - list: Results from each command (dicts with ``op`` and ``path``). - - Raises: - VyOSRestError: If a command fails and cannot be recovered. - - Notes: - **Version-adaptive set:** if a ``set`` is rejected by the device, - the method retries once with the last path segment removed. - This handles schema simplifications between VyOS releases - (e.g. ``allow-client address <prefix>`` -> ``allow-client <prefix>`` - in VyOS 1.5+). - - **Idempotent delete:** ``delete`` failures are silently ignored - (the path is already absent). - """ - results = [] + # def apply_commands(self, commands): + # """Execute a list of ``(op, path)`` command tuples against the device. + + # Args: + # commands (list): Each item is a 2-tuple ``(op, path)`` where: + # - *op* is ``"set"`` or ``"delete"`` + # - *path* is a list of config path tokens + + # Optionally a 3-tuple ``(op, path, value)`` for leaf nodes + # that carry a value. + + # Returns: + # list: Results from each command (dicts with ``op`` and ``path``). + + # Raises: + # VyOSRestError: If a command fails and cannot be recovered. + + # Notes: + # **Version-adaptive set:** if a ``set`` is rejected by the device, + # the method retries once with the last path segment removed. + # This handles schema simplifications between VyOS releases + # (e.g. ``allow-client address <prefix>`` -> ``allow-client <prefix>`` + # in VyOS 1.5+). + + # **Idempotent delete:** ``delete`` failures are silently ignored + # (the path is already absent). + # """ + # results = [] + + # for cmd in commands: + # # Support two command formats: + # # Tuple: ("set", ["path", "tokens"]) or ("set", ["path"], "value") + # # Dict: {"op": "set", "path": ["path", "tokens"]} + # if isinstance(cmd, dict): + # op = cmd["op"] + # path = list(cmd["path"]) + # value = cmd.get("value") + # else: + # op = cmd[0] + # path = list(cmd[1]) + # value = cmd[2] if len(cmd) > 2 else None + + # if op == "set": + # results.append(self._apply_set(path, value)) + # elif op == "delete": + # results.append(self._apply_delete(path)) + # else: + # self._module.fail_json( + # msg="Unknown command op '{op}' in apply_commands".format(op=op), + # ) + + # return results + def apply_commands(self, commands): + if not commands: + return [] + payload = [] for cmd in commands: - # Support two command formats: - # Tuple: ("set", ["path", "tokens"]) or ("set", ["path"], "value") - # Dict: {"op": "set", "path": ["path", "tokens"]} if isinstance(cmd, dict): - op = cmd["op"] - path = list(cmd["path"]) - value = cmd.get("value") - else: - op = cmd[0] - path = list(cmd[1]) - value = cmd[2] if len(cmd) > 2 else None - - if op == "set": - results.append(self._apply_set(path, value)) - elif op == "delete": - results.append(self._apply_delete(path)) + op, path = cmd["op"], list(cmd["path"]) else: - self._module.fail_json( - msg="Unknown command op '{op}' in apply_commands".format(op=op), - ) - - return results + op, path = cmd[0], list(cmd[1]) + payload.append({"op": op, "path": path}) + try: + return self._client.configure_batch(payload) + except VyOSRestError as exc: + self._module.fail_json( + msg="apply_commands failed: {e}".format(e=str(exc)), + ) def _apply_set(self, path, value=None): """Set a config path, retrying with a shortened path on failure.""" diff --git a/plugins/module_utils/vyos_rest.py b/plugins/module_utils/vyos_rest.py index 95b3aac..f4aa0dc 100644 --- a/plugins/module_utils/vyos_rest.py +++ b/plugins/module_utils/vyos_rest.py @@ -1,36 +1,5 @@ """ VyOS REST API client utility — supports both connection modes. - -**httpapi mode (recommended)**:: - - # inventory - vyos15x ansible_host=192.168.122.194 - ansible_network_os=vyos.rest.vyos - ansible_connection=ansible.netcommon.httpapi - ansible_httpapi_use_ssl=true - ansible_httpapi_port=443 - ansible_httpapi_validate_certs=false - ansible_httpapi_api_key=vyos123 - - # playbook — no connection params needed on any task - - vyos.rest.vyos_banner: - banner: pre-login - text: "Authorised use only." - state: present - -**local mode (alternative, no inventory network_os required)**:: - - - vyos.rest.vyos_banner: - hostname: 192.168.122.194 - api_key: vyos123 - verify_ssl: false - banner: pre-login - text: "Authorised use only." - state: present - -Both modes call the same VyOS endpoints. The httpapi mode routes calls -through the persistent connection daemon (better for large inventories); -the local mode opens a new HTTPS connection per task. """ from __future__ import absolute_import, division, print_function @@ -45,7 +14,7 @@ import os try: from urllib.parse import urlencode except ImportError: - from urllib import urlencode # Python 2 + from urllib import urlencode try: import urllib3 @@ -111,7 +80,7 @@ class VyOSRestClient: def _post_httpapi(self, endpoint, payload): try: - result = self._conn.send_request(endpoint=endpoint, **payload) + result = self._conn.send_request(endpoint, **payload) return result except ConnectionError as exc: raise VyOSRestError(str(exc)) @@ -218,6 +187,21 @@ class VyOSRestClient: def image_delete(self, name): return self._post("/image", {"op": "delete", "name": name}) + def configure_batch(self, commands): + """Send all commands as a single atomic commit.""" + if self._mode == "httpapi": + try: + return self._conn.send_request( + "/configure", + _raw_list=commands, + ) + except ConnectionError as exc: + raise VyOSRestError(str(exc)) + except Exception as exc: + raise VyOSRestError( + "configure_batch failed: {e}".format(e=str(exc)), + ) + def config_file_save(self, path=None): payload = {"op": "save"} if path: diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py index b9df27d..304fa20 100644 --- a/plugins/modules/vyos_banner.py +++ b/plugins/modules/vyos_banner.py @@ -31,7 +31,6 @@ options: description: - Which banner to configure. type: str - required: true choices: - pre-login - post-login @@ -47,9 +46,11 @@ options: - Desired state of the banner configuration. - C(merged) - set the banner if it differs from the current value. - C(replaced) - replace the banner text unconditionally. - - C(deleted) - remove the banner. + - C(deleted) - remove the banner. If C(config.banner) is omitted, + both pre-login and post-login banners are removed. - C(gathered) - return the current banner in I(gathered) without - making any changes. + making any changes. If C(config.banner) is omitted, all banners + are returned. type: str default: merged choices: @@ -67,7 +68,6 @@ options: api_key: description: REST API key (not needed when ansible_httpapi_api_key is set). type: str - no_log: true timeout: description: Request timeout in seconds. type: int @@ -97,12 +97,16 @@ EXAMPLES = r""" text: "Welcome. Authorised access only." state: replaced -- name: Remove pre-login banner +- name: Remove pre-login banner only vyos.rest.vyos_banner: config: banner: pre-login state: deleted +- name: Remove all banners + vyos.rest.vyos_banner: + state: deleted + - name: Read current pre-login banner without changing it vyos.rest.vyos_banner: config: @@ -110,6 +114,11 @@ EXAMPLES = r""" state: gathered register: result +- name: Read all banners + vyos.rest.vyos_banner: + state: gathered + register: result + - name: Print gathered banner ansible.builtin.debug: msg: "Current banner: {{ result.gathered.text }}" @@ -184,7 +193,7 @@ def main(): options=dict( banner=dict( type="str", - required=True, + required=False, choices=["pre-login", "post-login"], ), text=dict(type="str"), @@ -203,17 +212,47 @@ def main(): required_if=[ ("state", "merged", ["config"]), ("state", "replaced", ["config"]), - ("state", "deleted", ["config"]), - ("state", "gathered", ["config"]), ], supports_check_mode=True, ) client = VyOSRestClient(module) state = module.params["state"] - config = module.params["config"] - banner_type = config["banner"] + config = module.params["config"] or {} + banner_type = config.get("banner") desired_text = config.get("text") or "" + + # deleted without banner_type — delete all banners + if state == "deleted" and not banner_type: + commands = [] + changed = False + for bt in ["pre-login", "post-login"]: + current = _get_current(client, bt) + if current.get("text"): + if not module.check_mode: + try: + client.configure_delete(_BANNER_PATH[bt]) + except VyOSRestError as exc: + module.fail_json(msg=str(exc)) + commands.append("delete {p}".format(p=" ".join(_BANNER_PATH[bt]))) + changed = True + module.exit_json(changed=changed, commands=commands) + + # gathered without banner_type — return all banners + if state == "gathered" and not banner_type: + gathered = {} + for bt in ["pre-login", "post-login"]: + current = _get_current(client, bt) + if current.get("text"): + gathered[bt] = current + module.exit_json(changed=False, gathered=gathered, commands=[]) + + # all other states require banner_type + if not banner_type: + module.fail_json( + msg="config.banner is required for state={0}".format(state), + ) + path = _BANNER_PATH[banner_type] commands = [] changed = False diff --git a/plugins/modules/vyos_bgp_address_family.py b/plugins/modules/vyos_bgp_address_family.py deleted file mode 100644 index ff80053..0000000 --- a/plugins/modules/vyos_bgp_address_family.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_bgp_address_family -short_description: BGP Address Family resource module via REST API. -description: - - Manages BGP address-family configuration (networks, redistribute, - aggregate-address, per-neighbor AFI settings) via the VyOS HTTPS REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: BGP address-family configuration. - type: dict - suboptions: - as_number: - description: Local AS number. - type: int - required: true - address_family: - description: Global address-family parameters. - type: list - elements: dict - suboptions: - afi: - description: Address family (ipv4 or ipv6). - type: str - choices: [ipv4, ipv6] - aggregate_address: - description: List of BGP aggregate networks. - type: list - elements: dict - suboptions: - prefix: - type: str - as_set: - type: bool - summary_only: - type: bool - networks: - description: Networks to originate. - type: list - elements: dict - suboptions: - prefix: - type: str - path_limit: - type: int - backdoor: - type: bool - route_map: - type: str - redistribute: - description: Protocols to redistribute. - type: list - elements: dict - suboptions: - protocol: - type: str - choices: [connected, kernel, ospf, ospfv3, rip, ripng, static] - table: - type: str - route_map: - type: str - metric: - type: int - neighbors: - description: Per-neighbor address-family settings. - type: list - elements: dict - suboptions: - neighbor_address: - description: Neighbor IP. - type: str - address_family: - type: list - elements: dict - suboptions: - afi: - type: str - choices: [ipv4, ipv6] - allowas_in: - type: int - as_override: - type: bool - route_map: - description: Route-map settings. - type: dict - suboptions: - import_map: - type: str - export_map: - type: str - soft_reconfiguration: - type: bool - nexthop_self: - type: bool - remove_private_as: - type: bool - state: - description: Desired state. - type: str - choices: [merged, replaced, deleted, gathered] - default: merged - hostname: - description: Device IP or FQDN. - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - description: Config before module ran. - returned: always - type: dict -after: - description: Config after module ran. - returned: when changed - type: dict -commands: - description: Commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def _get_bgp(client): - try: - result = client.retrieve_show_config(["protocols", "bgp"]) - return result.get("data") or {} - except VyOSRestError: - return {} - - -def _afi_unicast_key(afi): - return "ipv4-unicast" if afi == "ipv4" else "ipv6-unicast" - - -def _apply(client, config, commands): - asn = str(config["as_number"]) - base = ["protocols", "bgp", asn] - - for af in config.get("address_family") or []: - afi = af["afi"] - af_base = base + ["address-family", _afi_unicast_key(afi)] - - for agg in af.get("aggregate_address") or []: - p = agg["prefix"] - agg_base = af_base + ["aggregate-address", p] - client.configure_set(agg_base) - commands.append("set {b}".format(b=" ".join(agg_base))) - if agg.get("as_set"): - client.configure_set(agg_base + ["as-set"]) - if agg.get("summary_only"): - client.configure_set(agg_base + ["summary-only"]) - - for net in af.get("networks") or []: - net_base = af_base + ["network", net["prefix"]] - client.configure_set(net_base) - commands.append("set {b}".format(b=" ".join(net_base))) - if net.get("route_map"): - client.configure_set(net_base + ["route-map"], net["route_map"]) - if net.get("backdoor"): - client.configure_set(net_base + ["backdoor"]) - - for redist in af.get("redistribute") or []: - r_base = af_base + ["redistribute", redist["protocol"]] - client.configure_set(r_base) - commands.append("set {b}".format(b=" ".join(r_base))) - if redist.get("metric"): - client.configure_set(r_base + ["metric"], str(redist["metric"])) - if redist.get("route_map"): - client.configure_set(r_base + ["route-map"], redist["route_map"]) - - for nbr in config.get("neighbors") or []: - n_base = base + ["neighbor", nbr["neighbor_address"]] - for af in nbr.get("address_family") or []: - afi = af["afi"] - n_af_base = n_base + ["address-family", _afi_unicast_key(afi)] - client.configure_set(n_af_base) - commands.append("set {b}".format(b=" ".join(n_af_base))) - if af.get("allowas_in"): - client.configure_set(n_af_base + ["allowas-in", "number"], str(af["allowas_in"])) - if af.get("as_override"): - client.configure_set(n_af_base + ["as-override"]) - if af.get("nexthop_self"): - client.configure_set(n_af_base + ["nexthop-self"]) - if af.get("remove_private_as"): - client.configure_set(n_af_base + ["remove-private-as"]) - if af.get("soft_reconfiguration"): - client.configure_set(n_af_base + ["soft-reconfiguration", "inbound"]) - rm = af.get("route_map") or {} - if rm.get("import_map"): - client.configure_set(n_af_base + ["route-map", "import"], rm["import_map"]) - if rm.get("export_map"): - client.configure_set(n_af_base + ["route-map", "export"], rm["export_map"]) - - -def main(): - argument_spec = dict( - config=dict( - type="dict", - options=dict( - as_number=dict(type="int", required=True), - address_family=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", choices=["ipv4", "ipv6"]), - aggregate_address=dict( - type="list", - elements="dict", - options=dict( - prefix=dict(type="str"), - as_set=dict(type="bool"), - summary_only=dict(type="bool"), - ), - ), - networks=dict( - type="list", - elements="dict", - options=dict( - prefix=dict(type="str"), - path_limit=dict(type="int"), - backdoor=dict(type="bool"), - route_map=dict(type="str"), - ), - ), - redistribute=dict( - type="list", - elements="dict", - options=dict( - protocol=dict( - type="str", - choices=[ - "connected", - "kernel", - "ospf", - "ospfv3", - "rip", - "ripng", - "static", - ], - ), - table=dict(type="str"), - route_map=dict(type="str"), - metric=dict(type="int"), - ), - ), - ), - ), - neighbors=dict( - type="list", - elements="dict", - options=dict( - neighbor_address=dict(type="str"), - address_family=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", choices=["ipv4", "ipv6"]), - allowas_in=dict(type="int"), - as_override=dict(type="bool"), - nexthop_self=dict(type="bool"), - remove_private_as=dict(type="bool"), - soft_reconfiguration=dict(type="bool"), - route_map=dict( - type="dict", - options=dict( - import_map=dict(type="str"), - export_map=dict(type="str"), - ), - ), - ), - ), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule( - argument_spec=argument_spec, - required_if=[("state", "merged", ["config"]), ("state", "replaced", ["config"])], - supports_check_mode=True, - ) - - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") - commands = [] - changed = False - before = _get_bgp(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - if before: - client.configure_delete(["protocols", "bgp"]) - commands.append("delete protocols bgp") - changed = True - elif state in ("merged", "replaced"): - if state == "replaced" and before: - client.configure_delete(["protocols", "bgp"]) - commands.append("delete protocols bgp") - _apply(client, config, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get_bgp(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_bgp_global.py b/plugins/modules/vyos_bgp_global.py deleted file mode 100644 index 6a08f4f..0000000 --- a/plugins/modules/vyos_bgp_global.py +++ /dev/null @@ -1,365 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_bgp_global -short_description: Manage BGP global configuration on VyOS via the REST API. -description: - - Manages BGP global parameters on VyOS devices using the HTTPS REST API. - - Mirrors C(vyos.vyos.vyos_bgp_global) but uses the HTTP API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: BGP global configuration. - type: dict - suboptions: - as_number: - description: Local BGP AS number. - type: int - required: true - router_id: - description: BGP router ID (IPv4 address). - type: str - neighbors: - description: List of BGP neighbor configurations. - type: list - elements: dict - suboptions: - neighbor: - description: Neighbor IP address. - type: str - required: true - remote_as: - description: Neighbor's AS number. - type: int - description: - description: Neighbor description. - type: str - password: - description: MD5 authentication password. - type: str - no_log: true - update_source: - description: Local interface or address for BGP session. - type: str - ebgp_multihop: - description: Maximum hops for eBGP sessions. - type: int - shutdown: - description: Whether to administratively shut down the neighbor. - type: bool - networks: - description: List of networks to originate. - type: list - elements: dict - suboptions: - prefix: - description: Network prefix to advertise. - type: str - required: true - route_map: - description: Route map to apply. - type: str - redistribute: - description: List of protocols to redistribute into BGP. - type: list - elements: dict - suboptions: - protocol: - description: Protocol to redistribute. - type: str - choices: [connected, kernel, ospf, ospfv3, rip, ripng, static] - required: true - metric: - description: Metric for redistributed routes. - type: int - route_map: - description: Route map to apply. - type: str - state: - description: - - C(merged): Merge BGP config with existing. - - C(replaced): Replace entire BGP config. - - C(deleted): Remove BGP configuration. - - C(gathered): Read BGP config from device. - type: str - choices: [merged, replaced, deleted, gathered] - default: merged - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -seealso: - - module: vyos.vyos.vyos_bgp_global -examples: | - - name: Configure BGP - vyos.rest.vyos_bgp_global: - hostname: 192.168.1.1 - api_key: MY-KEY - config: - as_number: 65001 - router_id: 192.168.1.1 - neighbors: - - neighbor: 192.168.2.1 - remote_as: 65002 - description: "Peer AS65002" - networks: - - prefix: 10.0.0.0/8 - redistribute: - - protocol: connected - state: merged - - - name: Remove BGP configuration - vyos.rest.vyos_bgp_global: - hostname: 192.168.1.1 - api_key: MY-KEY - state: deleted -""" - -RETURN = r""" -before: - description: BGP config before the module ran. - returned: always - type: dict -after: - description: BGP config after the module ran. - returned: when changed - type: dict -gathered: - description: BGP config read from device (state=gathered). - returned: when state is gathered - type: dict -commands: - description: set/delete commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_BGP_BASE = ["protocols", "bgp"] - - -def _get_bgp(client): - try: - result = client.retrieve_show_config(_BGP_BASE) - return result.get("data") or {} - except VyOSRestError: - return {} - - -def _apply_bgp(client, config, commands): - asn = str(config["as_number"]) - base = _BGP_BASE + [asn] - client.configure_set(base) - commands.append("set protocols bgp {a}".format(a=asn)) - - if config.get("router_id"): - client.configure_set(base + ["parameters", "router-id"], config["router_id"]) - commands.append( - "set protocols bgp {a} parameters router-id {r}".format( - a=asn, - r=config["router_id"], - ), - ) - - for nbr in config.get("neighbors") or []: - nbase = base + ["neighbor", nbr["neighbor"]] - client.configure_set(nbase) - commands.append("set protocols bgp {a} neighbor {n}".format(a=asn, n=nbr["neighbor"])) - if nbr.get("remote_as"): - client.configure_set(nbase + ["remote-as"], str(nbr["remote_as"])) - commands.append( - "set protocols bgp {a} neighbor {n} remote-as {r}".format( - a=asn, - n=nbr["neighbor"], - r=nbr["remote_as"], - ), - ) - if nbr.get("description"): - client.configure_set(nbase + ["description"], nbr["description"]) - if nbr.get("password"): - client.configure_set(nbase + ["password"], nbr["password"]) - if nbr.get("update_source"): - client.configure_set(nbase + ["update-source"], nbr["update_source"]) - if nbr.get("ebgp_multihop"): - client.configure_set(nbase + ["ebgp-multihop"], str(nbr["ebgp_multihop"])) - if nbr.get("shutdown"): - client.configure_set(nbase + ["shutdown"]) - commands.append( - "set protocols bgp {a} neighbor {n} shutdown".format( - a=asn, - n=nbr["neighbor"], - ), - ) - - for net in config.get("networks") or []: - nbase = base + ["address-family", "ipv4-unicast", "network", net["prefix"]] - client.configure_set(nbase) - commands.append( - "set protocols bgp {a} address-family ipv4-unicast network {p}".format( - a=asn, - p=net["prefix"], - ), - ) - if net.get("route_map"): - client.configure_set(nbase + ["route-map"], net["route_map"]) - - for redist in config.get("redistribute") or []: - rbase = base + [ - "address-family", - "ipv4-unicast", - "redistribute", - redist["protocol"], - ] - client.configure_set(rbase) - commands.append( - "set protocols bgp {a} address-family ipv4-unicast redistribute {p}".format( - a=asn, - p=redist["protocol"], - ), - ) - if redist.get("metric"): - client.configure_set(rbase + ["metric"], str(redist["metric"])) - if redist.get("route_map"): - client.configure_set(rbase + ["route-map"], redist["route_map"]) - - -def main(): - argument_spec = dict( - config=dict( - type="dict", - options=dict( - as_number=dict(type="int", required=True), - router_id=dict(type="str"), - neighbors=dict( - type="list", - elements="dict", - options=dict( - neighbor=dict(type="str", required=True), - remote_as=dict(type="int"), - description=dict(type="str"), - password=dict(type="str", no_log=True), - update_source=dict(type="str"), - ebgp_multihop=dict(type="int"), - shutdown=dict(type="bool"), - ), - ), - networks=dict( - type="list", - elements="dict", - options=dict( - prefix=dict(type="str", required=True), - route_map=dict(type="str"), - ), - ), - redistribute=dict( - type="list", - elements="dict", - options=dict( - protocol=dict( - type="str", - required=True, - choices=[ - "connected", - "kernel", - "ospf", - "ospfv3", - "rip", - "ripng", - "static", - ], - ), - metric=dict(type="int"), - route_map=dict(type="str"), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - required_if=[ - ("state", "merged", ["config"]), - ("state", "replaced", ["config"]), - ], - supports_check_mode=True, - ) - - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") - commands = [] - changed = False - - before = _get_bgp(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - if before: - client.configure_delete(_BGP_BASE) - commands.append("delete protocols bgp") - changed = True - - elif state in ("merged", "replaced"): - if state == "replaced" and before: - client.configure_delete(_BGP_BASE) - commands.append("delete protocols bgp") - _apply_bgp(client, config, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get_bgp(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_config_file.py b/plugins/modules/vyos_config_file.py deleted file mode 100644 index 9d5e48a..0000000 --- a/plugins/modules/vyos_config_file.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_config_file -short_description: Save or load VyOS configuration files via the REST API. -description: - - Manages VyOS configuration persistence via the HTTPS REST API - (C(/config-file) endpoint). - - Use I(op=save) to write the running configuration to a file. - - Use I(op=load) to load a configuration from a file and apply it. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - op: - description: - - C(save) - Save the running configuration to I(file) or the default C(/config/config.boot). - - C(load) - Load and commit the configuration from I(file). - type: str - choices: [save, load] - default: save - file: - description: - - Path on the VyOS device for save/load target. - - If omitted for C(save), the device uses its default boot config path. - type: str - hostname: - description: - - IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: - - HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: - - API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: - - Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: - - Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+""" - -RETURN = r""" -output: - description: Text output from the config-file operation. - returned: success - type: str -""" - -EXAMPLES = r""" -- name: Save running config (default location) - vyos.rest.vyos_config_file: - hostname: 192.168.1.1 - api_key: MY-KEY - op: save - -- name: Save running config to a custom file - vyos.rest.vyos_config_file: - hostname: 192.168.1.1 - api_key: MY-KEY - op: save - file: /config/backups/config.boot.bak - -- name: Load a configuration from file - vyos.rest.vyos_config_file: - hostname: 192.168.1.1 - api_key: MY-KEY - op: load - file: /config/backups/config.boot.bak -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def main(): - argument_spec = dict( - op=dict(type="str", default="save", choices=["save", "load"]), - file=dict(type="str"), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - required_if=[("op", "load", ["file"])], - supports_check_mode=False, - ) - - client = VyOSRestClient(module) - op = module.params["op"] - file_path = module.params.get("file") - - try: - if op == "save": - result = client.config_file_save(file_path) - else: - result = client.config_file_load(file_path) - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - module.exit_json(changed=True, output=result.get("data", "")) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_configure.py b/plugins/modules/vyos_configure.py index 355a78c..02fc7ad 100644 --- a/plugins/modules/vyos_configure.py +++ b/plugins/modules/vyos_configure.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# GNU General Public License v3.0+ from __future__ import absolute_import, division, print_function @@ -10,249 +10,127 @@ __metaclass__ = type DOCUMENTATION = r""" --- module: vyos_configure -short_description: Manage VyOS device configuration via the REST API. +short_description: Send raw set/delete commands to a VyOS device via REST API. description: - - Sends one or more C(set) or C(delete) configuration commands to a VyOS - device via its HTTPS REST API (C(/configure) endpoint). - - Each command in I(commands) is executed and committed atomically. - - Supports Ansible check-mode (C(check_mode=yes)) - no changes are applied. + - Sends one or more set/delete configuration commands to a VyOS device + via the HTTPS REST API as a single atomic batch commit. + - Useful for configuration not covered by dedicated resource modules, + or for test setup and teardown tasks. + - Commands are parsed from CLI-style strings (C(set ...) / C(delete ...)). version_added: "1.0.0" author: - VyOS Community (@vyos) options: commands: description: - - List of VyOS configuration commands. - - Each item may be either a plain string in VyOS CLI syntax - (e.g. C(set interfaces ethernet eth0 description 'WAN')) - or a dict with keys C(op), C(path), and optionally C(value). + - List of CLI-style configuration commands. + - Each command must start with C(set) or C(delete). type: list - elements: raw + elements: str required: true save: description: - - Whether to save the configuration to disk after a successful commit. + - Whether to save the configuration after applying commands. type: bool default: false - hostname: - description: - - IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: - - HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: - - API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: - - Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: - - Validate the device's TLS certificate. - type: bool - default: false -notes: - - The VyOS HTTP API must be enabled on the device before using this module. - - Enable with C(set service https api keys id <ID> key <KEY>) and - C(set service https api rest), then C(commit && save). - - Tested against VyOS 1.3 and 1.4. -requirements: - - VyOS 1.3+ seealso: - - module: vyos.rest.vyos_retrieve - - module: vyos.rest.vyos_show""" - -RETURN = r""" -commands_sent: - description: The list of commands dispatched to the API. - returned: always - type: list - elements: dict - sample: - - op: set - path: ["system", "host-name"] - value: vyos-router -changed: - description: Whether the device configuration was modified. - returned: always - type: bool -saved: - description: Whether the configuration was saved to disk. - returned: always - type: bool + - module: vyos.vyos.vyos_config """ -import re - - EXAMPLES = r""" -- name: Set hostname via REST API +- name: Add loopback address for testing vyos.rest.vyos_configure: - hostname: 192.168.1.1 - api_key: MY-KEY commands: - - "set system host-name vyos-router" + - set interfaces loopback lo address 20.1.1.1/32 + save: false -- name: Configure an interface description and address +- name: Remove loopback address after testing vyos.rest.vyos_configure: - hostname: 192.168.1.1 - api_key: MY-KEY commands: - - "set interfaces ethernet eth1 description 'UPLINK'" - - "set interfaces ethernet eth1 address 10.0.0.1/30" - save: true + - delete interfaces loopback lo address 20.1.1.1/32 + save: false -- name: Delete a static route using dict syntax +- name: Multiple commands in one atomic commit vyos.rest.vyos_configure: - hostname: 192.168.1.1 - api_key: MY-KEY commands: - - op: delete - path: ["protocols", "static", "route", "192.0.2.0/24"] + - set system host-name vyos-test + - set system domain-name example.com + save: true """ -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - +RETURN = r""" +commands: + description: Parsed command payloads sent to the device. + returned: always + type: list +response: + description: Raw API response. + returned: when commands are applied + type: dict +""" -def _parse_cli_command(cmd_str): - """Parse a VyOS CLI-syntax string into an API payload dict. +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule - Supports: - set <path> [<value>] - delete <path> - comment <path> <comment> - Args: - cmd_str (str): e.g. "set interfaces ethernet eth0 description 'WAN'" +def _parse_command(line): + """Parse a CLI-style command string into (op, path) tuple. - Returns: - dict: e.g. {"op": "set", "path": [...], "value": "WAN"} + Examples: + "set interfaces loopback lo address 20.1.1.1/32" + -> ("set", ["interfaces", "loopback", "lo", "address", "20.1.1.1/32"]) + "delete service snmp" + -> ("delete", ["service", "snmp"]) """ - cmd_str = cmd_str.strip() - op_match = re.match(r"^(set|delete|comment)\s+(.+)$", cmd_str, re.IGNORECASE) - if not op_match: - raise ValueError( - "Cannot parse command '{cmd}'. Must start with set/delete/comment.".format( - cmd=cmd_str, - ), - ) - op = op_match.group(1).lower() - rest = op_match.group(2).strip() - - # Extract a quoted or bare trailing value for 'set' - value = None - if op == "set": - # Look for a trailing quoted value: '...' or "..." - quoted = re.search(r"""(['"])(.*?)\1\s*$""", rest) - if quoted: - value = quoted.group(2) - rest = rest[: quoted.start()].strip() - else: - # Bare final token may be the value if the second-to-last token - # is a known leaf keyword – use a simple heuristic: if the - # last segment has no sub-tokens we treat it as value. - tokens = rest.split() - # We cannot know the schema here, so we pass the full path and - # no value; the device will decide. - path = tokens - return {"op": op, "path": path} - - path = rest.split() - payload = {"op": op, "path": path} - if value is not None: - payload["value"] = value - return payload - - -def _normalise_commands(raw_commands): - """Normalise the mixed list of str/dict commands into API dicts.""" - result = [] - for item in raw_commands: - if isinstance(item, dict): - if "op" not in item or "path" not in item: - raise ValueError( - "Command dict must have 'op' and 'path' keys: {item}".format( - item=item, - ), - ) - result.append(item) - elif isinstance(item, str): - result.append(_parse_cli_command(item)) - else: - raise ValueError( - "Command must be a string or dict, got: {t}".format(t=type(item)), - ) - return result + line = line.strip() + if line.startswith("set "): + parts = line[4:].split() + return ("set", parts) + elif line.startswith("delete "): + parts = line[7:].split() + return ("delete", parts) + else: + return None def main(): - argument_spec = dict( - commands=dict(type="list", elements="raw", required=True), - save=dict(type="bool", default=False), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule( - argument_spec=argument_spec, + argument_spec=dict( + commands=dict(type="list", elements="str", required=True), + save=dict(type="bool", default=False), + ), supports_check_mode=True, ) - try: - commands = _normalise_commands(module.params["commands"]) - except ValueError as exc: - module.fail_json(msg=str(exc)) + vyos = VyOSModule(module) + commands_raw = module.params["commands"] + do_save = module.params["save"] - if module.check_mode: - module.exit_json( - changed=True, - commands_sent=commands, - saved=False, - msg="Check mode: no changes applied.", - ) + commands = [] + for line in commands_raw: + parsed = _parse_command(line) + if parsed is None: + module.fail_json( + msg="Invalid command '{c}' — must start with 'set' or 'delete'".format(c=line), + ) + commands.append(parsed) - client = VyOSRestClient(module) - changed = False - saved = False + if not commands: + module.exit_json(changed=False, commands=[]) - try: - for cmd in commands: - op = cmd["op"].lower() - path = cmd["path"] - value = cmd.get("value") - if op == "set": - client.configure_set(path, value) - elif op == "delete": - client.configure_delete(path) - elif op == "comment": - client.configure_comment(path, value or "") - else: - module.fail_json( - msg="Unsupported op '{op}'".format(op=op), - ) - changed = True + if module.check_mode: + module.exit_json(changed=True, commands=commands) - if module.params["save"]: - client.config_file_save() - saved = True + response = vyos.apply_commands(commands) - except VyOSRestError as exc: - module.fail_json(msg=str(exc), commands_sent=commands) + if do_save: + vyos.save_config() - module.exit_json(changed=changed, commands_sent=commands, saved=saved) + module.exit_json( + changed=True, + commands=commands, + response=response, + ) if __name__ == "__main__": diff --git a/plugins/modules/vyos_facts.py b/plugins/modules/vyos_facts.py deleted file mode 100644 index 66e684b..0000000 --- a/plugins/modules/vyos_facts.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_facts -short_description: Get facts about VyOS devices via REST API. -description: - - Collects facts from VyOS devices using the HTTPS REST API. - - Returns system info and optionally per-resource configuration facts. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - gather_subset: - description: - - Restrict facts to a subset. Use C(all), C(default), C(config), or C(min). - type: list - elements: str - default: ["min"] - gather_network_resources: - description: - - Which network resource facts to gather. - type: list - elements: str - available_network_resources: - description: - - When true, return a list of available resource names. - type: bool - default: false - hostname: - description: - - Device IP or FQDN. - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -ansible_facts: - description: Dictionary of facts keyed by resource name. - returned: always - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_NETWORK_RESOURCES = [ - "interfaces", - "l3_interfaces", - "lag_interfaces", - "lldp_global", - "lldp_interfaces", - "static_routes", - "firewall_rules", - "firewall_global", - "firewall_interfaces", - "ospfv2", - "ospfv3", -] - -_RESOURCE_PATHS = { - "interfaces": ["interfaces"], - "l3_interfaces": ["interfaces"], - "lag_interfaces": ["interfaces", "bonding"], - "lldp_global": ["service", "lldp"], - "lldp_interfaces": ["service", "lldp"], - "static_routes": ["protocols", "static"], - "firewall_rules": ["firewall"], - "firewall_global": ["firewall"], - "firewall_interfaces": ["firewall"], - "ospfv2": ["protocols", "ospf"], - "ospfv3": ["protocols", "ospfv3"], -} - - -def _get_subset(client, resource): - path = _RESOURCE_PATHS.get(resource, []) - try: - result = client.retrieve_show_config(path) - return result.get("data") or {} - except VyOSRestError: - return {} - - -def main(): - argument_spec = dict( - gather_subset=dict(type="list", elements="str", default=["min"]), - gather_network_resources=dict(type="list", elements="str"), - available_network_resources=dict(type="bool", default=False), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - - client = VyOSRestClient(module) - facts = {} - - if module.params["available_network_resources"]: - facts["available_network_resources"] = _NETWORK_RESOURCES - - gather_subset = module.params["gather_subset"] - get_all = "all" in gather_subset - - # Always gather system info for "min" or "all" - if get_all or "min" in gather_subset or "default" in gather_subset: - try: - info = client.info() - facts["version"] = info.get("data", {}).get("version", "") - facts["hostname"] = info.get("data", {}).get("hostname", "") - except VyOSRestError: - pass - - # Config subset - if get_all or "config" in gather_subset: - try: - result = client.retrieve_show_config([]) - facts["config"] = result.get("data") or {} - except VyOSRestError: - facts["config"] = {} - - # Network resources - requested = module.params.get("gather_network_resources") or [] - if "all" in requested: - requested = _NETWORK_RESOURCES - - for resource in requested: - if resource.startswith("!"): - continue - if resource in _NETWORK_RESOURCES: - facts["network_resources"] = facts.get("network_resources", {}) - facts["network_resources"][resource] = _get_subset(client, resource) - - module.exit_json(changed=False, ansible_facts=facts) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_firewall_global.py b/plugins/modules/vyos_firewall_global.py deleted file mode 100644 index a3974fd..0000000 --- a/plugins/modules/vyos_firewall_global.py +++ /dev/null @@ -1,360 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_firewall_global -short_description: Firewall global resource module via REST API. -description: - - Manages global VyOS firewall parameters (groups, ICMP redirect policy, - source validation, ping policy) via the HTTPS REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: Global firewall configuration. - type: dict - suboptions: - validation: - description: Source-validation policy (strict/loose/disable). - type: str - choices: [strict, loose, disable] - config_trap: - description: SNMP trap on firewall config changes. - type: bool - ping: - description: ICMP echo policy. - type: dict - suboptions: - all: - type: bool - broadcast: - type: bool - route_redirects: - description: ICMP redirect / source-route options per AFI. - type: list - elements: dict - suboptions: - afi: - type: str - choices: [ipv4, ipv6] - required: true - icmp_redirects: - type: dict - suboptions: - send: - type: bool - receive: - type: bool - ip_src_route: - type: bool - group: - description: Firewall object groups. - type: dict - suboptions: - address_group: - type: list - elements: dict - suboptions: - name: - type: str - required: true - description: - type: str - members: - description: List of IP addresses or ranges. - type: list - elements: dict - suboptions: - address: - type: str - afi: - type: str - default: ipv4 - choices: [ipv4, ipv6] - network_group: - type: list - elements: dict - suboptions: - name: - type: str - required: true - description: - type: str - members: - description: List of network prefixes. - type: list - elements: dict - suboptions: - address: - type: str - port_group: - type: list - elements: dict - suboptions: - name: - type: str - required: true - description: - type: str - members: - description: List of port numbers or ranges. - type: list - elements: dict - suboptions: - port: - type: str - state: - description: Desired state. - type: str - choices: [merged, replaced, deleted, gathered] - default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - returned: always - type: dict -after: - returned: when changed - type: dict -commands: - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_FW_BASE = ["firewall"] - - -def _get(client): - try: - r = client.retrieve_show_config(_FW_BASE) - return r.get("data") or {} - except VyOSRestError: - return {} - - -def _apply(client, config, commands): - if config.get("validation"): - path = ( - _FW_BASE + ["ip", "src-route"] - if config["validation"] == "disable" - else _FW_BASE + ["ip", "enable-default-log"] - ) - # Use the proper path - p = ( - ["firewall", "global-options", "source-validation"] - if client.retrieve_exists(["firewall", "global-options"]) - else ["firewall", "ip", "src-route"] - ) - client.configure_set(["firewall", "ip", "source-validation"], config["validation"]) - commands.append("set firewall ip source-validation {v}".format(v=config["validation"])) - - if config.get("config_trap") is not None: - val = "enable" if config["config_trap"] else "disable" - client.configure_set(["firewall", "config-trap"], val) - commands.append("set firewall config-trap {v}".format(v=val)) - - ping = config.get("ping") or {} - if ping.get("all") is not None: - action = "enable" if ping["all"] else "disable" - client.configure_set(["firewall", "ip", "ping", "all"], action) - commands.append("set firewall ip ping all {a}".format(a=action)) - if ping.get("broadcast") is not None: - action = "enable" if ping["broadcast"] else "disable" - client.configure_set(["firewall", "ip", "ping", "broadcast"], action) - commands.append("set firewall ip ping broadcast {a}".format(a=action)) - - for rr in config.get("route_redirects") or []: - afi = rr["afi"] - ip_key = "ip" if afi == "ipv4" else "ipv6" - icmp = rr.get("icmp_redirects") or {} - if icmp.get("send") is not None: - p = ["firewall", ip_key, "send-redirects"] - client.configure_set(p, "enable" if icmp["send"] else "disable") - commands.append( - "set {p} {v}".format(p=" ".join(p), v="enable" if icmp["send"] else "disable"), - ) - if icmp.get("receive") is not None: - p = ["firewall", ip_key, "disable-forwarding"] - client.configure_set(p, "enable" if not icmp["receive"] else "disable") - if rr.get("ip_src_route") is not None: - p = ["firewall", ip_key, "source-route"] - client.configure_set(p, "enable" if rr["ip_src_route"] else "disable") - - grp = config.get("group") or {} - for ag in grp.get("address_group") or []: - gpath = ["firewall", "group", "address-group", ag["name"]] - client.configure_set(gpath) - commands.append("set {p}".format(p=" ".join(gpath))) - if ag.get("description"): - client.configure_set(gpath + ["description"], ag["description"]) - for m in ag.get("members") or []: - client.configure_set(gpath + ["address"], m["address"]) - commands.append("set {p} address {a}".format(p=" ".join(gpath), a=m["address"])) - - for ng in grp.get("network_group") or []: - gpath = ["firewall", "group", "network-group", ng["name"]] - client.configure_set(gpath) - commands.append("set {p}".format(p=" ".join(gpath))) - if ng.get("description"): - client.configure_set(gpath + ["description"], ng["description"]) - for m in ng.get("members") or []: - client.configure_set(gpath + ["network"], m["address"]) - - for pg in grp.get("port_group") or []: - gpath = ["firewall", "group", "port-group", pg["name"]] - client.configure_set(gpath) - commands.append("set {p}".format(p=" ".join(gpath))) - if pg.get("description"): - client.configure_set(gpath + ["description"], pg["description"]) - for m in pg.get("members") or []: - client.configure_set(gpath + ["port"], str(m["port"])) - - -def main(): - argument_spec = dict( - config=dict( - type="dict", - options=dict( - validation=dict(type="str", choices=["strict", "loose", "disable"]), - config_trap=dict(type="bool"), - ping=dict( - type="dict", - options=dict(all=dict(type="bool"), broadcast=dict(type="bool")), - ), - route_redirects=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]), - icmp_redirects=dict( - type="dict", - options=dict(send=dict(type="bool"), receive=dict(type="bool")), - ), - ip_src_route=dict(type="bool"), - ), - ), - group=dict( - type="dict", - options=dict( - address_group=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - description=dict(type="str"), - afi=dict(type="str", default="ipv4"), - members=dict( - type="list", - elements="dict", - options=dict(address=dict(type="str")), - ), - ), - ), - network_group=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - description=dict(type="str"), - members=dict( - type="list", - elements="dict", - options=dict(address=dict(type="str")), - ), - ), - ), - port_group=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - description=dict(type="str"), - members=dict( - type="list", - elements="dict", - options=dict(port=dict(type="str")), - ), - ), - ), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule( - argument_spec=argument_spec, - required_if=[("state", "merged", ["config"]), ("state", "replaced", ["config"])], - supports_check_mode=True, - ) - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") - commands = [] - changed = False - before = _get(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - if before: - client.configure_delete(_FW_BASE) - commands.append("delete firewall") - changed = True - elif state in ("merged", "replaced"): - if state == "replaced" and before: - client.configure_delete(_FW_BASE) - commands.append("delete firewall") - _apply(client, config, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_firewall_interfaces.py b/plugins/modules/vyos_firewall_interfaces.py deleted file mode 100644 index d9085a6..0000000 --- a/plugins/modules/vyos_firewall_interfaces.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_firewall_interfaces -short_description: Firewall interfaces resource module via REST API. -description: - - Attaches or detaches named firewall rule sets to/from interface - directions (in/out/local) using the VyOS HTTPS REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of interface firewall attachment configurations. - type: list - elements: dict - suboptions: - name: - description: Interface name. - type: str - required: true - access_rules: - description: Firewall rule sets per AFI. - type: list - elements: dict - suboptions: - afi: - description: Address family. - type: str - choices: [ipv4, ipv6] - required: true - rules: - description: Rule sets and directions. - type: list - elements: dict - suboptions: - name: - description: Rule set name. - type: str - direction: - description: Traffic direction. - type: str - choices: [in, local, out] - required: true - state: - description: Desired state. - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - returned: always - type: list -after: - returned: when changed - type: list -commands: - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_IFACE_TYPES = { - "eth": "ethernet", - "bond": "bonding", - "vti": "vti", - "vxlan": "vxlan", - "lo": "loopback", - "dummy": "dummy", - "br": "bridge", - "wg": "wireguard", - "tun": "tunnel", -} - - -def _itype(name): - for p, t in _IFACE_TYPES.items(): - if name.startswith(p): - return t - return "ethernet" - - -def _fw_direction_key(afi, direction): - if afi == "ipv4": - return {"in": "in", "out": "out", "local": "local"}[direction] - return {"in": "in", "out": "out", "local": "local"}[direction] - - -def _apply(client, iface_cfg, commands): - name = iface_cfg["name"] - itype = _itype(name) - ibase = ["interfaces", itype, name, "firewall"] - - for ar in iface_cfg.get("access_rules") or []: - afi = ar["afi"] - fw_key = "firewall" if afi == "ipv4" else "ipv6" - for rule in ar.get("rules") or []: - direction = rule["direction"] - rs_name = rule.get("name") - if rs_name: - path = ibase + [direction, fw_key if afi == "ipv6" else "name"] - client.configure_set(path, rs_name) - commands.append( - "set interfaces {t} {n} firewall {d} {k} '{rs}'".format( - t=itype, - n=name, - d=direction, - k="ipv6-name" if afi == "ipv6" else "name", - rs=rs_name, - ), - ) - - -def _delete_iface_fw(client, name, commands): - itype = _itype(name) - path = ["interfaces", itype, name, "firewall"] - try: - client.configure_delete(path) - commands.append("delete interfaces {t} {n} firewall".format(t=itype, n=name)) - except VyOSRestError: - pass - - -def _get(client): - try: - result = client.retrieve_show_config(["interfaces"]) - raw = result.get("data") or {} - out = [] - for itype, itype_data in raw.items(): - if not isinstance(itype_data, dict): - continue - for iname, idata in itype_data.items(): - fw = idata.get("firewall") if isinstance(idata, dict) else None - if fw: - out.append({"name": iname, "firewall": fw}) - return out - except VyOSRestError: - return [] - - -def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - access_rules=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]), - rules=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str"), - direction=dict( - type="str", - required=True, - choices=["in", "local", "out"], - ), - ), - ), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - before = _get(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - targets = {i["name"] for i in config} if config else {i["name"] for i in before} - for iface in before: - if iface["name"] in targets: - _delete_iface_fw(client, iface["name"], commands) - changed = True - elif state in ("merged", "replaced", "overridden"): - if state in ("replaced", "overridden"): - for cfg in config: - _delete_iface_fw(client, cfg["name"], commands) - for cfg in config: - _apply(client, cfg, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_firewall_rules.py b/plugins/modules/vyos_firewall_rules.py deleted file mode 100644 index f535f3a..0000000 --- a/plugins/modules/vyos_firewall_rules.py +++ /dev/null @@ -1,492 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_firewall_rules -short_description: Manage firewall rules on VyOS via the REST API. -description: - - Manages VyOS firewall rule sets and individual rules using the HTTPS REST API. - - Mirrors C(vyos.vyos.vyos_firewall_rules) but uses the HTTP API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of firewall rule set configurations. - type: list - elements: dict - suboptions: - afi: - description: Address family. - type: str - choices: [ipv4, ipv6] - required: true - rule_sets: - description: List of named rule sets. - type: list - elements: dict - suboptions: - name: - description: Rule set name. - type: str - required: true - default_action: - description: Default action for unmatched traffic. - type: str - choices: [drop, reject, accept] - default: drop - description: - description: Rule set description. - type: str - rules: - description: List of firewall rules. - type: list - elements: dict - suboptions: - number: - description: Rule number (1-999999). - type: int - required: true - action: - description: Rule action. - type: str - choices: [drop, reject, accept, inspect] - description: - description: Rule description. - type: str - protocol: - description: Protocol (e.g. C(tcp), C(udp), C(icmp), C(all)). - type: str - source: - description: Source match criteria. - type: dict - suboptions: - address: - description: Source IP address or network. - type: str - port: - description: Source port or port range. - type: str - group: - description: Address or network group. - type: dict - suboptions: - address_group: - type: str - network_group: - type: str - destination: - description: Destination match criteria. - type: dict - suboptions: - address: - description: Destination IP address or network. - type: str - port: - description: Destination port or port range. - type: str - group: - description: Address or network group. - type: dict - suboptions: - address_group: - type: str - network_group: - type: str - state: - description: Connection state matching. - type: dict - suboptions: - established: - type: bool - new: - type: bool - related: - type: bool - invalid: - type: bool - enabled: - description: Whether the rule is active. - type: bool - default: true - state: - description: - - C(merged): Add/update listed rule sets and rules. - - C(replaced): Replace listed rule sets entirely. - - C(overridden): Replace the entire firewall rule config. - - C(deleted): Remove listed (or all) rule sets. - - C(gathered): Read firewall rule config from device. - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -seealso: - - module: vyos.vyos.vyos_firewall_rules -examples: | - - name: Create an IPv4 firewall rule set - vyos.rest.vyos_firewall_rules: - hostname: 192.168.1.1 - api_key: MY-KEY - config: - - afi: ipv4 - rule_sets: - - name: OUTSIDE-IN - default_action: drop - rules: - - number: 10 - action: accept - protocol: tcp - destination: - port: "80,443" - state: - established: true - related: true - state: merged - - - name: Delete all firewall rules - vyos.rest.vyos_firewall_rules: - hostname: 192.168.1.1 - api_key: MY-KEY - state: deleted -""" - -RETURN = r""" -before: - description: Firewall rule config before the module ran. - returned: always - type: list -after: - description: Firewall rule config after the module ran. - returned: when changed - type: list -gathered: - description: Firewall config read from device (state=gathered). - returned: when state is gathered - type: list -commands: - description: set/delete commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_FW_PATH = { - "ipv4": ["firewall", "name"], - "ipv6": ["firewall", "ipv6-name"], -} - - -def _get_fw_rules(client): - try: - result = client.retrieve_show_config(["firewall"]) - data = result.get("data") or {} - out = [] - for afi, key in [("ipv4", "name"), ("ipv6", "ipv6-name")]: - sets_data = data.get(key, {}) - if not isinstance(sets_data, dict): - continue - rule_sets = [] - for rs_name, rs_data in sets_data.items(): - if not isinstance(rs_data, dict): - continue - rs = { - "name": rs_name, - "default_action": rs_data.get("default-action", "drop"), - } - if "description" in rs_data: - rs["description"] = rs_data["description"] - rules = [] - for rnum, rdata in (rs_data.get("rule") or {}).items(): - if not isinstance(rdata, dict): - continue - rule = {"number": int(rnum)} - if "action" in rdata: - rule["action"] = rdata["action"] - if "description" in rdata: - rule["description"] = rdata["description"] - if "protocol" in rdata: - rule["protocol"] = rdata["protocol"] - rule["enabled"] = "disable" not in rdata - rules.append(rule) - if rules: - rs["rules"] = sorted(rules, key=lambda r: r["number"]) - rule_sets.append(rs) - if rule_sets: - out.append({"afi": afi, "rule_sets": rule_sets}) - return out - except VyOSRestError: - return [] - - -def _apply_rule_set(client, afi, rs, commands): - base = _FW_PATH[afi] + [rs["name"]] - client.configure_set(base) - commands.append("set {p}".format(p=" ".join(base))) - - if rs.get("default_action"): - client.configure_set(base + ["default-action"], rs["default_action"]) - commands.append( - "set {p} default-action {a}".format( - p=" ".join(base), - a=rs["default_action"], - ), - ) - if rs.get("description"): - client.configure_set(base + ["description"], rs["description"]) - commands.append( - "set {p} description '{d}'".format( - p=" ".join(base), - d=rs["description"], - ), - ) - - for rule in rs.get("rules") or []: - rbase = base + ["rule", str(rule["number"])] - client.configure_set(rbase) - commands.append("set {p}".format(p=" ".join(rbase))) - - if rule.get("action"): - client.configure_set(rbase + ["action"], rule["action"]) - commands.append( - "set {p} action {a}".format(p=" ".join(rbase), a=rule["action"]), - ) - if rule.get("description"): - client.configure_set(rbase + ["description"], rule["description"]) - if rule.get("protocol"): - client.configure_set(rbase + ["protocol"], rule["protocol"]) - commands.append( - "set {p} protocol {pr}".format( - p=" ".join(rbase), - pr=rule["protocol"], - ), - ) - for direction in ("source", "destination"): - match = rule.get(direction) - if not match: - continue - mbase = rbase + [direction] - if match.get("address"): - client.configure_set(mbase + ["address"], match["address"]) - commands.append( - "set {p} address '{a}'".format( - p=" ".join(mbase), - a=match["address"], - ), - ) - if match.get("port"): - client.configure_set(mbase + ["port"], match["port"]) - commands.append( - "set {p} port {pt}".format( - p=" ".join(mbase), - pt=match["port"], - ), - ) - - state_match = rule.get("state") - if state_match: - sbase = rbase + ["state"] - for st_name in ("established", "new", "related", "invalid"): - if state_match.get(st_name): - client.configure_set(sbase + [st_name], "enable") - commands.append( - "set {p} {s} enable".format( - p=" ".join(sbase), - s=st_name, - ), - ) - - if "enabled" in rule and not rule["enabled"]: - client.configure_set(rbase + ["disable"]) - commands.append("set {p} disable".format(p=" ".join(rbase))) - - -def main(): - rule_spec = dict( - number=dict(type="int", required=True), - action=dict(type="str", choices=["drop", "reject", "accept", "inspect"]), - description=dict(type="str"), - protocol=dict(type="str"), - source=dict( - type="dict", - options=dict( - address=dict(type="str"), - port=dict(type="str"), - group=dict( - type="dict", - options=dict( - address_group=dict(type="str"), - network_group=dict(type="str"), - ), - ), - ), - ), - destination=dict( - type="dict", - options=dict( - address=dict(type="str"), - port=dict(type="str"), - group=dict( - type="dict", - options=dict( - address_group=dict(type="str"), - network_group=dict(type="str"), - ), - ), - ), - ), - state=dict( - type="dict", - options=dict( - established=dict(type="bool"), - new=dict(type="bool"), - related=dict(type="bool"), - invalid=dict(type="bool"), - ), - ), - enabled=dict(type="bool", default=True), - ) - - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]), - rule_sets=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - default_action=dict( - type="str", - choices=["drop", "reject", "accept"], - default="drop", - ), - description=dict(type="str"), - rules=dict(type="list", elements="dict", options=rule_spec), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - - before = _get_fw_rules(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state in ("deleted", "overridden") and not config: - for afi_key in ("name", "ipv6-name"): - try: - client.configure_delete(["firewall", afi_key]) - commands.append( - "delete firewall {k}".format(k=afi_key), - ) - except VyOSRestError: - pass - changed = True - - elif state == "deleted" and config: - for entry in config: - afi = entry["afi"] - base_path = _FW_PATH[afi] - for rs in entry.get("rule_sets") or []: - try: - client.configure_delete(base_path + [rs["name"]]) - commands.append( - "delete {p} {n}".format( - p=" ".join(base_path), - n=rs["name"], - ), - ) - changed = True - except VyOSRestError: - pass - - elif state in ("merged", "replaced", "overridden"): - if state == "replaced": - for entry in config: - afi = entry["afi"] - base_path = _FW_PATH[afi] - for rs in entry.get("rule_sets") or []: - try: - client.configure_delete(base_path + [rs["name"]]) - commands.append( - "delete {p} {n}".format( - p=" ".join(base_path), - n=rs["name"], - ), - ) - except VyOSRestError: - pass - for entry in config: - afi = entry["afi"] - for rs in entry.get("rule_sets") or []: - _apply_rule_set(client, afi, rs, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get_fw_rules(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_generate.py b/plugins/modules/vyos_generate.py deleted file mode 100644 index 448575a..0000000 --- a/plugins/modules/vyos_generate.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_generate -short_description: Execute generate commands on a VyOS device via REST API. -description: - - Sends a C(generate) operational command to a VyOS device via the HTTPS - REST API (C(/generate) endpoint). - - Commonly used to generate WireGuard keypairs, TLS certificates, etc. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - path: - description: - - Generate command tokens. - - E.g. C(["wireguard", "default-keypair"]). - type: list - elements: str - required: true - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -examples: | - - name: Generate WireGuard default keypair - vyos.rest.vyos_generate: - hostname: 192.168.1.1 - api_key: MY-KEY - path: ["wireguard", "default-keypair"] - - - name: Generate a new SSH host key - vyos.rest.vyos_generate: - hostname: 192.168.1.1 - api_key: MY-KEY - path: ["ssh-server", "host-key", "rsa"] -""" - -RETURN = r""" -output: - description: Text output produced by the generate command (may be empty). - returned: success - type: str -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def main(): - argument_spec = dict( - path=dict(type="list", elements="str", required=True), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=False, - ) - - client = VyOSRestClient(module) - try: - result = client.generate(module.params["path"]) - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - module.exit_json(changed=True, output=result.get("data", "")) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_hostname.py b/plugins/modules/vyos_hostname.py index 10255b5..86444ad 100644 --- a/plugins/modules/vyos_hostname.py +++ b/plugins/modules/vyos_hostname.py @@ -16,6 +16,8 @@ description: using the HTTPS REST API. - Mirrors the behaviour of C(vyos.vyos.vyos_hostname) but uses the HTTP API instead of SSH/network_cli. + - The states C(replaced), C(overridden) behave identically to C(merged) + for this single-value resource. version_added: "1.0.0" author: - VyOS Community (@vyos) @@ -27,42 +29,58 @@ options: suboptions: hostname: description: - - System hostname (max 63 characters, no underscores). + - System hostname (max 63 characters, no underscores). type: str required: true + running_config: + description: + - Used only with state C(parsed). + - The value should be the output of + B(show configuration commands | grep host-name) from the device. + type: str state: description: - C(merged) - Ensure the hostname is set to the value in I(config). + - C(replaced) - Identical to C(merged) for this single-value resource. + - C(overridden) - Identical to C(merged) for this single-value resource. - C(deleted) - Remove the configured hostname (resets to default). - C(gathered) - Read the current hostname from the device and return it in I(gathered) without making changes. + - C(rendered) - Return the CLI commands for the given config without + connecting to the device. + - C(parsed) - Parse the C(running_config) string and return structured + data without connecting to the device. type: str - choices: [merged, deleted, gathered] + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed default: merged hostname: description: - - IP address or FQDN of the VyOS device. + - IP address or FQDN of the VyOS device (not needed with httpapi inventory). type: str - required: true port: description: - - HTTPS port for the REST API. + - HTTPS port for the REST API. type: int default: 443 api_key: description: - - API key configured on the device. + - API key configured on the device. type: str - required: true - no_log: true timeout: description: - - Request timeout in seconds. + - Request timeout in seconds. type: int default: 30 verify_ssl: description: - - Validate the device's TLS certificate. + - Validate the device's TLS certificate. type: bool default: false requirements: @@ -84,13 +102,20 @@ gathered: description: Hostname read from the device (state=gathered only). returned: when state is gathered type: dict +rendered: + description: CLI commands for the provided config (state=rendered only). + returned: when state is rendered + type: list +parsed: + description: Structured data parsed from running_config (state=parsed only). + returned: when state is parsed + type: dict commands: description: REST API commands dispatched. returned: always type: list """ - EXAMPLES = r""" - name: Set hostname vyos.rest.vyos_hostname: @@ -98,6 +123,12 @@ EXAMPLES = r""" hostname: vyos-core-01 state: merged +- name: Replace hostname + vyos.rest.vyos_hostname: + config: + hostname: vyos-core-02 + state: replaced + - name: Gather current hostname vyos.rest.vyos_hostname: state: gathered @@ -106,8 +137,21 @@ EXAMPLES = r""" - name: Delete hostname configuration vyos.rest.vyos_hostname: state: deleted + +- name: Render commands without connecting + vyos.rest.vyos_hostname: + config: + hostname: vyos-core-01 + state: rendered + +- name: Parse running config + vyos.rest.vyos_hostname: + running_config: "set system host-name 'vyos'" + state: parsed """ +import re + from ansible.module_utils.basic import AnsibleModule from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( VYOS_REST_CONNECTION_ARGSPEC, @@ -127,6 +171,12 @@ def _get_hostname(client): return "" +def _parse_hostname(running_config): + """Parse hostname from 'show configuration commands | grep host-name' output.""" + match = re.search(r"host-name\s+['\"]?(\S+?)['\"]?\s*$", running_config, re.M) + return match.group(1) if match else "" + + def main(): argument_spec = dict( config=dict( @@ -135,22 +185,59 @@ def main(): hostname=dict(type="str", required=True), ), ), + running_config=dict(type="str"), state=dict( type="str", default="merged", - choices=["merged", "deleted", "gathered"], + choices=[ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], ), ) argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) module = AnsibleModule( argument_spec=argument_spec, - required_if=[("state", "merged", ["config"])], + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ("state", "merged", ["config"]), + ("state", "replaced", ["config"]), + ("state", "overridden", ["config"]), + ("state", "rendered", ["config"]), + ("state", "parsed", ["running_config"]), + ], supports_check_mode=True, ) - client = VyOSRestClient(module) state = module.params["state"] + + # rendered — offline, no device connection needed + if state == "rendered": + hostname = module.params["config"]["hostname"] + module.exit_json( + rendered=["set system host-name '{h}'".format(h=hostname)], + commands=[], + ) + + # parsed — offline, no device connection needed + if state == "parsed": + hostname = _parse_hostname(module.params["running_config"] or "") + module.exit_json( + parsed={"hostname": hostname}, + commands=[], + ) + + # collapsed states — replaced and overridden are identical to merged + if state in ("replaced", "overridden"): + state = "merged" + + client = VyOSRestClient(module) commands = [] changed = False @@ -168,15 +255,15 @@ def main(): desired = module.params["config"]["hostname"] if current != desired: client.configure_set(_PATH, desired) - commands.append( - "set system host-name '{h}'".format(h=desired), - ) + commands.append("set system host-name '{h}'".format(h=desired)) changed = True + elif state == "deleted": if current: client.configure_delete(_PATH) commands.append("delete system host-name") changed = True + except VyOSRestError as exc: module.fail_json(msg=str(exc)) diff --git a/plugins/modules/vyos_image.py b/plugins/modules/vyos_image.py deleted file mode 100644 index be12705..0000000 --- a/plugins/modules/vyos_image.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_image -short_description: Manage VyOS system images via the REST API. -description: - - Adds or removes VyOS system images using the HTTPS REST API - (C(/image) endpoint). - - Use I(state=present) to download and install an image from a URL. - - Use I(state=absent) to delete an installed image by name. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - state: - description: - - C(present): Download and install the image from I(url). - - C(absent): Delete the image specified by I(name). - type: str - choices: [present, absent] - required: true - url: - description: - - HTTP(S) URL of the VyOS C(.iso) file to install. - - Required when I(state=present). - type: str - name: - description: - - Version name of the image to remove (e.g. C(1.4-rolling-202401010000)). - - Required when I(state=absent). - type: str - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: > - Request timeout in seconds. Image downloads can take several minutes; - increase this value accordingly. - type: int - default: 300 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -examples: | - - name: Install the latest rolling release - vyos.rest.vyos_image: - hostname: 192.168.1.1 - api_key: MY-KEY - state: present - url: "https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso" - timeout: 600 - - - name: Remove an old image - vyos.rest.vyos_image: - hostname: 192.168.1.1 - api_key: MY-KEY - state: absent - name: "1.4-rolling-202312010000" -""" - -RETURN = r""" -output: - description: Text output from the image add/delete operation. - returned: success - type: str -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def main(): - argument_spec = dict( - state=dict(type="str", required=True, choices=["present", "absent"]), - url=dict(type="str"), - name=dict(type="str"), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - # Override default timeout to 300s for image downloads - argument_spec["timeout"]["default"] = 300 - - module = AnsibleModule( - argument_spec=argument_spec, - required_if=[ - ("state", "present", ["url"]), - ("state", "absent", ["name"]), - ], - supports_check_mode=False, - ) - - client = VyOSRestClient(module) - state = module.params["state"] - - try: - if state == "present": - result = client.image_add(module.params["url"]) - else: - result = client.image_delete(module.params["name"]) - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - module.exit_json(changed=True, output=result.get("data", "")) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_info.py b/plugins/modules/vyos_info.py deleted file mode 100644 index 8946868..0000000 --- a/plugins/modules/vyos_info.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_info -short_description: Retrieve system information from a VyOS device via the REST API. -description: - - Queries the public C(/info) endpoint (HTTP GET) which requires no - authentication and returns the VyOS version, hostname, and a welcome banner. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: > - API key. Not required for this module (the /info endpoint is public) - but kept in the spec for consistency with other modules in this collection. - type: str - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 10 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -examples: | - - name: Get VyOS system info - vyos.rest.vyos_info: - hostname: 192.168.1.1 - register: sys_info - - - name: Print VyOS version - ansible.builtin.debug: - msg: "Running VyOS {{ sys_info.version }}" -""" - -RETURN = r""" -version: - description: VyOS version string. - returned: success - type: str - sample: "1.4-rolling-202401010000" -hostname: - description: System hostname. - returned: success - type: str - sample: vyos-router -banner: - description: Welcome banner text. - returned: success - type: str - sample: "Welcome to VyOS" -info: - description: Full info dict as returned by the device. - returned: success - type: dict -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def main(): - argument_spec = dict() - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - # api_key not strictly required for /info - argument_spec["api_key"]["required"] = False - argument_spec["api_key"]["default"] = "" - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - client = VyOSRestClient(module) - try: - result = client.info() - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - data = result.get("data", {}) - module.exit_json( - changed=False, - version=data.get("version", ""), - hostname=data.get("hostname", ""), - banner=data.get("banner", ""), - info=data, - ) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_interfaces.py b/plugins/modules/vyos_interfaces.py deleted file mode 100644 index 9edc1da..0000000 --- a/plugins/modules/vyos_interfaces.py +++ /dev/null @@ -1,401 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_interfaces -short_description: Manage interface attributes on a VyOS device via the REST API. -description: - - Manages interface base attributes (description, MTU, enabled state, VIFs, - duplex, speed) on VyOS devices using the HTTPS REST API. - - Mirrors C(vyos.vyos.vyos_interfaces) but uses HTTP API. - - Supports Ethernet, Bonding, VXLAN, Loopback, and Virtual Tunnel interfaces. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of interface configurations. - type: list - elements: dict - suboptions: - name: - description: > - Full interface name (e.g. C(eth0), C(bond0), C(vti1), C(vxlan2)). - type: str - required: true - description: - description: Interface description. - type: str - enabled: - description: > - Administrative state. C(true) = up, C(false) = admin-down. - type: bool - default: true - mtu: - description: MTU in bytes. Applicable for Ethernet, Bonding, VXLAN, VTI. - type: int - duplex: - description: Duplex mode. Only for Ethernet interfaces. - type: str - choices: [auto, full, half] - speed: - description: Link speed. Only for Ethernet interfaces. - type: str - vifs: - description: List of VLAN sub-interface configurations. - type: list - elements: dict - suboptions: - vlan_id: - description: 802.1Q VLAN ID. - type: int - required: true - description: - description: VIF description. - type: str - enabled: - description: Administrative state of the VIF. - type: bool - default: true - state: - description: - - C(merged): Merge the provided config with existing interface config. - - C(replaced): Replace per-interface config with provided values. - - C(overridden): Override all interface config with provided values. - - C(deleted): Delete interface config for listed interfaces (or all). - - C(gathered): Read current interface config from device. - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -seealso: - - module: vyos.vyos.vyos_interfaces - - module: vyos.rest.vyos_l3_interfaces -examples: | - - name: Set description and MTU on eth1 - vyos.rest.vyos_interfaces: - hostname: 192.168.1.1 - api_key: MY-KEY - config: - - name: eth1 - description: "WAN uplink" - mtu: 1500 - enabled: true - state: merged - - - name: Disable eth2 - vyos.rest.vyos_interfaces: - hostname: 192.168.1.1 - api_key: MY-KEY - config: - - name: eth2 - enabled: false - state: merged - - - name: Create a VLAN sub-interface - vyos.rest.vyos_interfaces: - hostname: 192.168.1.1 - api_key: MY-KEY - config: - - name: eth1 - vifs: - - vlan_id: 100 - description: "VLAN 100" - state: merged -""" - -RETURN = r""" -before: - description: Interface configuration before the module ran. - returned: always - type: list -after: - description: Interface configuration after the module ran. - returned: when changed - type: list -gathered: - description: Interface config read from device (state=gathered). - returned: when state is gathered - type: list -commands: - description: REST API set/delete commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_IFACE_TYPES = { - "eth": "ethernet", - "bond": "bonding", - "vti": "vti", - "vxlan": "vxlan", - "lo": "loopback", - "dummy": "dummy", - "peth": "pseudo-ethernet", - "vtun": "openvpn", - "wg": "wireguard", - "br": "bridge", - "macsec": "macsec", - "tun": "tunnel", -} - - -def _iface_type(name): - """Infer the VyOS interface type from its name prefix.""" - for prefix, itype in _IFACE_TYPES.items(): - if name.startswith(prefix): - return itype - return "ethernet" - - -def _iface_base_path(name): - return ["interfaces", _iface_type(name), name] - - -def _get_interfaces(client): - try: - result = client.retrieve_show_config(["interfaces"]) - ifaces_raw = result.get("data") or {} - interfaces = [] - for itype, itype_data in ifaces_raw.items(): - if not isinstance(itype_data, dict): - continue - for iname, idata in itype_data.items(): - if not isinstance(idata, dict): - continue - entry = {"name": iname} - if "description" in idata: - entry["description"] = idata["description"] - if "mtu" in idata: - entry["mtu"] = int(idata["mtu"]) - entry["enabled"] = "disable" not in idata - if "duplex" in idata: - entry["duplex"] = idata["duplex"] - if "speed" in idata: - entry["speed"] = idata["speed"] - if "vif" in idata: - vifs = [] - for vid, vdata in idata["vif"].items(): - vif_entry = {"vlan_id": int(vid)} - if isinstance(vdata, dict): - if "description" in vdata: - vif_entry["description"] = vdata["description"] - vif_entry["enabled"] = "disable" not in vdata - vifs.append(vif_entry) - entry["vifs"] = vifs - interfaces.append(entry) - return interfaces - except VyOSRestError: - return [] - - -def _apply_interface(client, iface_cfg, commands): - name = iface_cfg["name"] - base = _iface_base_path(name) - - if iface_cfg.get("description") is not None: - client.configure_set(base + ["description"], iface_cfg["description"]) - commands.append( - "set interfaces {t} {n} description '{d}'".format( - t=_iface_type(name), - n=name, - d=iface_cfg["description"], - ), - ) - if iface_cfg.get("mtu") is not None: - client.configure_set(base + ["mtu"], str(iface_cfg["mtu"])) - commands.append( - "set interfaces {t} {n} mtu {m}".format( - t=_iface_type(name), - n=name, - m=iface_cfg["mtu"], - ), - ) - if "enabled" in iface_cfg: - if not iface_cfg["enabled"]: - client.configure_set(base + ["disable"]) - commands.append( - "set interfaces {t} {n} disable".format(t=_iface_type(name), n=name), - ) - else: - try: - client.configure_delete(base + ["disable"]) - commands.append( - "delete interfaces {t} {n} disable".format( - t=_iface_type(name), - n=name, - ), - ) - except VyOSRestError: - pass - if iface_cfg.get("duplex"): - client.configure_set(base + ["duplex"], iface_cfg["duplex"]) - commands.append( - "set interfaces {t} {n} duplex {d}".format( - t=_iface_type(name), - n=name, - d=iface_cfg["duplex"], - ), - ) - if iface_cfg.get("speed"): - client.configure_set(base + ["speed"], iface_cfg["speed"]) - commands.append( - "set interfaces {t} {n} speed {s}".format( - t=_iface_type(name), - n=name, - s=iface_cfg["speed"], - ), - ) - for vif in iface_cfg.get("vifs") or []: - vid = str(vif["vlan_id"]) - vif_base = base + ["vif", vid] - client.configure_set(vif_base) - commands.append( - "set interfaces {t} {n} vif {v}".format( - t=_iface_type(name), - n=name, - v=vid, - ), - ) - if vif.get("description"): - client.configure_set(vif_base + ["description"], vif["description"]) - commands.append( - "set interfaces {t} {n} vif {v} description '{d}'".format( - t=_iface_type(name), - n=name, - v=vid, - d=vif["description"], - ), - ) - if "enabled" in vif and not vif["enabled"]: - client.configure_set(vif_base + ["disable"]) - commands.append( - "set interfaces {t} {n} vif {v} disable".format( - t=_iface_type(name), - n=name, - v=vid, - ), - ) - - -def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - description=dict(type="str"), - enabled=dict(type="bool", default=True), - mtu=dict(type="int"), - duplex=dict(type="str", choices=["auto", "full", "half"]), - speed=dict(type="str"), - vifs=dict( - type="list", - elements="dict", - options=dict( - vlan_id=dict(type="int", required=True), - description=dict(type="str"), - enabled=dict(type="bool", default=True), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - - before = _get_interfaces(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - targets = {i["name"] for i in config} if config else {i["name"] for i in before} - for iface in before: - if iface["name"] in targets: - base = _iface_base_path(iface["name"]) - # Only delete mutable attributes, not the interface itself - for attr in ["description", "mtu", "duplex", "speed"]: - if attr in iface: - try: - client.configure_delete(base + [attr]) - commands.append( - "delete interfaces {t} {n} {a}".format( - t=_iface_type(iface["name"]), - n=iface["name"], - a=attr, - ), - ) - except VyOSRestError: - pass - changed = True - - elif state in ("merged", "replaced", "overridden"): - for iface_cfg in config: - _apply_interface(client, iface_cfg, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get_interfaces(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_l3_interfaces.py b/plugins/modules/vyos_l3_interfaces.py deleted file mode 100644 index f6d4e6c..0000000 --- a/plugins/modules/vyos_l3_interfaces.py +++ /dev/null @@ -1,381 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_l3_interfaces -short_description: Manage L3 interface attributes on VyOS via the REST API. -description: - - Manages IPv4 and IPv6 addresses on VyOS interfaces via the HTTPS REST API. - - Mirrors C(vyos.vyos.vyos_l3_interfaces) but uses the HTTP API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of L3 interface configurations. - type: list - elements: dict - suboptions: - name: - description: Full interface name (e.g. C(eth0), C(eth1)). - type: str - required: true - ipv4: - description: List of IPv4 address assignments. - type: list - elements: dict - suboptions: - address: - description: IPv4 address in CIDR notation or C(dhcp). - type: str - required: true - ipv6: - description: List of IPv6 address assignments. - type: list - elements: dict - suboptions: - address: - description: IPv6 address in CIDR notation or C(dhcpv6) or C(autoconf). - type: str - required: true - vifs: - description: VLAN sub-interface L3 settings. - type: list - elements: dict - suboptions: - vlan_id: - description: 802.1Q VLAN ID. - type: int - required: true - ipv4: - description: IPv4 addresses for this VIF. - type: list - elements: dict - suboptions: - address: - type: str - required: true - ipv6: - description: IPv6 addresses for this VIF. - type: list - elements: dict - suboptions: - address: - type: str - required: true - state: - description: - - C(merged): Add addresses to the interface (preserve existing). - - C(replaced): Replace all addresses on each listed interface. - - C(overridden): Replace all addresses on all interfaces. - - C(deleted): Remove all addresses from listed (or all) interfaces. - - C(gathered): Read L3 config from device without changes. - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -seealso: - - module: vyos.vyos.vyos_l3_interfaces - - module: vyos.rest.vyos_interfaces -examples: | - - name: Assign addresses to eth1 and eth2 - vyos.rest.vyos_l3_interfaces: - hostname: 192.168.1.1 - api_key: MY-KEY - config: - - name: eth1 - ipv4: - - address: 10.0.1.1/24 - ipv6: - - address: "2001:db8::1/64" - - name: eth2 - ipv4: - - address: dhcp - state: merged - - - name: Remove all addresses from eth1 - vyos.rest.vyos_l3_interfaces: - hostname: 192.168.1.1 - api_key: MY-KEY - config: - - name: eth1 - state: deleted -""" - -RETURN = r""" -before: - description: L3 interface config before the module ran. - returned: always - type: list -after: - description: L3 interface config after the module ran. - returned: when changed - type: list -gathered: - description: L3 config read from device (state=gathered). - returned: when state is gathered - type: list -commands: - description: set/delete commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_IFACE_TYPES = { - "eth": "ethernet", - "bond": "bonding", - "vti": "vti", - "vxlan": "vxlan", - "lo": "loopback", - "dummy": "dummy", - "br": "bridge", - "wg": "wireguard", - "tun": "tunnel", -} - - -def _iface_type(name): - for prefix, t in _IFACE_TYPES.items(): - if name.startswith(prefix): - return t - return "ethernet" - - -def _base(name): - return ["interfaces", _iface_type(name), name] - - -def _get_l3_interfaces(client): - try: - result = client.retrieve_show_config(["interfaces"]) - raw = result.get("data") or {} - out = [] - for itype, itype_data in raw.items(): - if not isinstance(itype_data, dict): - continue - for iname, idata in itype_data.items(): - if not isinstance(idata, dict): - continue - entry = {"name": iname, "ipv4": [], "ipv6": []} - for addr in _listify(idata.get("address")): - if ":" in addr: - entry["ipv6"].append({"address": addr}) - elif addr == "dhcp": - entry["ipv4"].append({"address": addr}) - else: - entry["ipv4"].append({"address": addr}) - # VIFs - if "vif" in idata: - vifs = [] - for vid, vdata in idata["vif"].items(): - vif_e = {"vlan_id": int(vid), "ipv4": [], "ipv6": []} - if isinstance(vdata, dict): - for addr in _listify(vdata.get("address")): - if ":" in addr: - vif_e["ipv6"].append({"address": addr}) - else: - vif_e["ipv4"].append({"address": addr}) - vifs.append(vif_e) - entry["vifs"] = vifs - if entry["ipv4"] or entry["ipv6"] or entry.get("vifs"): - out.append(entry) - return out - except VyOSRestError: - return [] - - -def _listify(val): - """Return val as a list regardless of whether it's a str or list.""" - if val is None: - return [] - if isinstance(val, list): - return val - return [val] - - -def _set_addresses(client, name, ipv4, ipv6, commands): - base = _base(name) - itype = _iface_type(name) - for a in ipv4 or []: - client.configure_set(base + ["address"], a["address"]) - commands.append( - "set interfaces {t} {n} address '{a}'".format( - t=itype, - n=name, - a=a["address"], - ), - ) - for a in ipv6 or []: - client.configure_set(base + ["address"], a["address"]) - commands.append( - "set interfaces {t} {n} address '{a}'".format( - t=itype, - n=name, - a=a["address"], - ), - ) - - -def _delete_addresses(client, name, commands): - base = _base(name) - itype = _iface_type(name) - try: - client.configure_delete(base + ["address"]) - commands.append( - "delete interfaces {t} {n} address".format(t=itype, n=name), - ) - except VyOSRestError: - pass - - -def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - ipv4=dict( - type="list", - elements="dict", - options=dict(address=dict(type="str", required=True)), - ), - ipv6=dict( - type="list", - elements="dict", - options=dict(address=dict(type="str", required=True)), - ), - vifs=dict( - type="list", - elements="dict", - options=dict( - vlan_id=dict(type="int", required=True), - ipv4=dict( - type="list", - elements="dict", - options=dict(address=dict(type="str", required=True)), - ), - ipv6=dict( - type="list", - elements="dict", - options=dict(address=dict(type="str", required=True)), - ), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - - before = _get_l3_interfaces(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - targets = {i["name"] for i in config} if config else {i["name"] for i in before} - for iface in before: - if iface["name"] in targets: - _delete_addresses(client, iface["name"], commands) - changed = True - - elif state in ("merged", "replaced", "overridden"): - if state in ("replaced", "overridden"): - # First remove existing addresses on targeted interfaces - targets = {i["name"] for i in config} - if state == "overridden": - targets = {i["name"] for i in before} - for iface in before: - if iface["name"] in targets: - _delete_addresses(client, iface["name"], commands) - - for iface_cfg in config: - name = iface_cfg["name"] - _set_addresses( - client, - name, - iface_cfg.get("ipv4"), - iface_cfg.get("ipv6"), - commands, - ) - for vif in iface_cfg.get("vifs") or []: - vid = str(vif["vlan_id"]) - base = _base(name) + ["vif", vid] - itype = _iface_type(name) - for a in (vif.get("ipv4") or []) + (vif.get("ipv6") or []): - client.configure_set(base + ["address"], a["address"]) - commands.append( - "set interfaces {t} {n} vif {v} address '{a}'".format( - t=itype, - n=name, - v=vid, - a=a["address"], - ), - ) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get_l3_interfaces(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_lag_interfaces.py b/plugins/modules/vyos_lag_interfaces.py deleted file mode 100644 index 15fe1f3..0000000 --- a/plugins/modules/vyos_lag_interfaces.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_lag_interfaces -short_description: LAG/bonding interfaces resource module via REST API. -description: - - Manages Link Aggregation Group (LAG/bond) interfaces on VyOS via REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of LAG interface configurations. - type: list - elements: dict - suboptions: - name: - description: Bond interface name (e.g. bond0). - type: str - required: true - mode: - description: Bonding mode. - type: str - choices: [802.3ad, active-backup, broadcast, round-robin, - transmit-load-balance, adaptive-load-balance, xor-hash] - members: - description: Member interfaces. - type: list - elements: dict - suboptions: - member: - description: Interface name. - type: str - primary: - description: Primary interface name. - type: str - hash_policy: - description: Transmit hash policy. - type: str - choices: [layer2, layer2+3, layer3+4] - arp_monitor: - description: ARP monitoring settings. - type: dict - suboptions: - interval: - description: Monitoring interval in ms. - type: int - target: - description: Target IP addresses. - type: list - elements: str - state: - description: Desired state. - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - returned: always - type: list -after: - returned: when changed - type: list -commands: - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_MODE_MAP = { - "802.3ad": "802.3ad", - "active-backup": "active-backup", - "broadcast": "broadcast", - "round-robin": "round-robin", - "transmit-load-balance": "transmit-load-balance", - "adaptive-load-balance": "adaptive-load-balance", - "xor-hash": "xor-hash", -} - - -def _get(client): - try: - r = client.retrieve_show_config(["interfaces", "bonding"]) - data = r.get("data") or {} - out = [] - for bname, bdata in data.items(): - entry = {"name": bname} - if isinstance(bdata, dict): - if "mode" in bdata: - entry["mode"] = bdata["mode"] - if "primary" in bdata: - entry["primary"] = bdata["primary"] - if "hash-policy" in bdata: - entry["hash_policy"] = bdata["hash-policy"] - members = [] - for m in ( - bdata.get("member", {}).keys() if isinstance(bdata.get("member"), dict) else [] - ): - members.append({"member": m}) - if members: - entry["members"] = members - out.append(entry) - return out - except VyOSRestError: - return [] - - -def _apply(client, cfg, commands): - name = cfg["name"] - base = ["interfaces", "bonding", name] - client.configure_set(base) - commands.append("set interfaces bonding {n}".format(n=name)) - - if cfg.get("mode"): - client.configure_set(base + ["mode"], cfg["mode"]) - commands.append("set interfaces bonding {n} mode {m}".format(n=name, m=cfg["mode"])) - if cfg.get("primary"): - client.configure_set(base + ["primary"], cfg["primary"]) - if cfg.get("hash_policy"): - client.configure_set(base + ["hash-policy"], cfg["hash_policy"]) - for m in cfg.get("members") or []: - client.configure_set(["interfaces", "ethernet", m["member"], "bond-group"], name) - commands.append("set interfaces ethernet {m} bond-group {n}".format(m=m["member"], n=name)) - arp = cfg.get("arp_monitor") or {} - if arp.get("interval"): - client.configure_set(base + ["arp-monitor", "interval"], str(arp["interval"])) - for t in arp.get("target") or []: - client.configure_set(base + ["arp-monitor", "target"], t) - - -def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - mode=dict(type="str", choices=list(_MODE_MAP.keys())), - members=dict( - type="list", - elements="dict", - options=dict(member=dict(type="str")), - ), - primary=dict(type="str"), - hash_policy=dict(type="str", choices=["layer2", "layer2+3", "layer3+4"]), - arp_monitor=dict( - type="dict", - options=dict( - interval=dict(type="int"), - target=dict(type="list", elements="str"), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - before = _get(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - targets = {i["name"] for i in config} if config else {i["name"] for i in before} - for b in before: - if b["name"] in targets: - client.configure_delete(["interfaces", "bonding", b["name"]]) - commands.append("delete interfaces bonding {n}".format(n=b["name"])) - changed = True - elif state in ("merged", "replaced", "overridden"): - for cfg in config: - _apply(client, cfg, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_lldp_global.py b/plugins/modules/vyos_lldp_global.py index 0a9618d..9ebbac0 100644 --- a/plugins/modules/vyos_lldp_global.py +++ b/plugins/modules/vyos_lldp_global.py @@ -1,62 +1,103 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# GNU General Public License v3.0+ from __future__ import absolute_import, division, print_function __metaclass__ = type + DOCUMENTATION = r""" --- module: vyos_lldp_global short_description: Manage LLDP global configuration on VyOS via REST API. description: - Manage LLDP global configuration on VyOS via REST API. + - Uses C(ansible_connection=ansible.netcommon.httpapi). version_added: "1.0.0" author: VyOS Community (@vyos) options: config: + description: LLDP global configuration. type: dict suboptions: enable: + description: Enable LLDP service. type: bool addresses: + description: List of management addresses to advertise. type: list elements: str snmp: description: - - C(enable) or C(disable). + - C(enable) to enable LLDP SNMP subagent. + - C(disable) to disable it. + - Requires SNMP service to be configured on the device. type: str legacy_protocols: + description: Legacy protocols to support. type: list elements: str choices: [cdp, edp, fdp, sonmp] state: + description: + - C(merged) - merge config with existing. + - C(replaced) - replace existing config with provided. + - C(deleted) - delete LLDP configuration. + - C(gathered) - return current config without changes. type: str default: merged choices: [merged, replaced, deleted, gathered] """ + EXAMPLES = r""" -- vyos.rest.vyos_lldp_global: +- name: Enable LLDP with management address and legacy protocols + vyos.rest.vyos_lldp_global: config: enable: true - addresses: [192.0.2.17] - snmp: enable - legacy_protocols: [cdp, fdp] + addresses: + - 192.0.2.17 + legacy_protocols: + - cdp + - fdp state: merged + +- name: Replace LLDP configuration + vyos.rest.vyos_lldp_global: + config: + enable: true + addresses: + - 192.0.2.99 + state: replaced + +- name: Delete all LLDP configuration + vyos.rest.vyos_lldp_global: + state: deleted + +- name: Gather current LLDP configuration + vyos.rest.vyos_lldp_global: + state: gathered + register: result """ + RETURN = r""" before: + description: LLDP configuration before the module ran. returned: always type: dict after: + description: LLDP configuration after the module ran. returned: when changed type: dict commands: + description: List of commands sent to the device. returned: always type: list gathered: + description: Current LLDP configuration from the device. returned: when state is gathered type: dict """ + from ansible.module_utils.basic import AnsibleModule from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule @@ -74,41 +115,82 @@ def _get(vyos): if "management-address" in raw: v = raw["management-address"] result["addresses"] = ( - list(v.keys()) if isinstance(v, dict) else ([v] if isinstance(v, str) else list(v)) + sorted(list(v.keys())) + if isinstance(v, dict) + else ([v] if isinstance(v, str) else sorted(list(v))) ) if "snmp" in raw: result["snmp"] = "enable" if "legacy-protocols" in raw: v = raw["legacy-protocols"] result["legacy_protocols"] = ( - list(v.keys()) if isinstance(v, dict) else ([v] if isinstance(v, str) else list(v)) + sorted(list(v.keys())) + if isinstance(v, dict) + else ([v] if isinstance(v, str) else sorted(list(v))) ) return result def _build(want, have, state): cmds = [] - if state in ("replaced", "deleted") and have: - cmds.append(("delete", _BASE)) - if state == "deleted": - return cmds - if want.get("enable") is not False: + + if state == "deleted": + if have: + cmds.append(("delete", _BASE)) + return cmds + + if state == "replaced": + # only wipe and rebuild if have differs from want + want_addrs = sorted(want.get("addresses") or []) + have_addrs = sorted(have.get("addresses") or []) + want_protos = sorted(want.get("legacy_protocols") or []) + have_protos = sorted(have.get("legacy_protocols") or []) + want_snmp = want.get("snmp") + have_snmp = have.get("snmp") + want_enable = bool(want.get("enable")) + have_enable = bool(have.get("enable")) + + if ( + want_addrs != have_addrs + or want_protos != have_protos + or want_snmp != have_snmp + or want_enable != have_enable + ): + if have: + cmds.append(("delete", _BASE)) + # fall through to set everything from want + else: + return cmds # already matches — idempotent + + # enable + if want.get("enable") and not have.get("enable"): cmds.append(("set", _BASE)) + + # addresses + have_addrs_set = set(have.get("addresses") or []) if state != "replaced" else set() for addr in want.get("addresses") or []: - cmds.append(("set", _BASE + ["management-address", addr])) + if addr not in have_addrs_set: + cmds.append(("set", _BASE + ["management-address", addr])) + + # snmp if want.get("snmp"): - if want["snmp"] == "disable": + if want["snmp"] == "disable" and have.get("snmp"): cmds.append(("delete", _BASE + ["snmp"])) - else: + elif want["snmp"] != "disable" and not have.get("snmp"): cmds.append(("set", _BASE + ["snmp"])) + + # legacy_protocols + have_protos_set = set(have.get("legacy_protocols") or []) if state != "replaced" else set() for p in want.get("legacy_protocols") or []: - cmds.append(("set", _BASE + ["legacy-protocols", p])) + if p not in have_protos_set: + cmds.append(("set", _BASE + ["legacy-protocols", p])) + return cmds def main(): module = AnsibleModule( - dict( + argument_spec=dict( config=dict( type="dict", options=dict( @@ -122,19 +204,27 @@ def main(): ), ), ), - state=dict(default="merged", choices=["merged", "replaced", "deleted", "gathered"]), + state=dict( + default="merged", + choices=["merged", "replaced", "deleted", "gathered"], + ), ), supports_check_mode=True, ) + vyos = VyOSModule(module) state = module.params["state"] config = module.params.get("config") or {} have = _get(vyos) + if state == "gathered": module.exit_json(changed=False, gathered=have) + commands = _build(config, have, state) + if module.check_mode: module.exit_json(changed=bool(commands), commands=commands, before=have) + if commands: response = vyos.apply_commands(commands) saved = vyos.save_config() @@ -146,6 +236,7 @@ def main(): saved=saved, response=response, ) + module.exit_json(changed=False, before=have, after=have, commands=[]) diff --git a/plugins/modules/vyos_lldp_interfaces.py b/plugins/modules/vyos_lldp_interfaces.py deleted file mode 100644 index 4f012aa..0000000 --- a/plugins/modules/vyos_lldp_interfaces.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_lldp_interfaces -short_description: LLDP interfaces resource module via REST API. -description: - - Manages per-interface LLDP settings (enable/disable, location) on VyOS - via the HTTPS REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of LLDP interface configurations. - type: list - elements: dict - suboptions: - name: - description: Interface name. - type: str - required: true - enable: - description: Enable LLDP on this interface (default true). - type: bool - default: true - location: - description: LLDP-MED location data. - type: dict - suboptions: - coordinate_based: - type: dict - suboptions: - altitude: - type: int - datum: - type: str - choices: [WGS84, NAD83, MLLW] - latitude: - type: str - required: true - longitude: - type: str - required: true - elin: - description: Emergency ELIN number (10-25 digits). - type: str - state: - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - returned: always - type: list -after: - returned: when changed - type: list -commands: - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_IFACE_TYPES = { - "eth": "ethernet", - "bond": "bonding", - "lo": "loopback", - "dummy": "dummy", - "br": "bridge", -} - - -def _itype(name): - for p, t in _IFACE_TYPES.items(): - if name.startswith(p): - return t - return "ethernet" - - -def _get(client): - try: - r = client.retrieve_show_config(["service", "lldp", "interface"]) - data = r.get("data") or {} - out = [] - for iname, idata in data.items(): - entry = {"name": iname} - if isinstance(idata, dict): - entry["enable"] = "disable" not in idata - out.append(entry) - return out - except VyOSRestError: - return [] - - -def _apply(client, cfg, commands): - name = cfg["name"] - base = ["service", "lldp", "interface", name] - client.configure_set(base) - commands.append("set service lldp interface {n}".format(n=name)) - - if "enable" in cfg and not cfg["enable"]: - client.configure_set(base + ["disable"]) - commands.append("set service lldp interface {n} disable".format(n=name)) - - loc = cfg.get("location") or {} - coord = loc.get("coordinate_based") or {} - if coord: - lbase = base + ["location", "coordinate-based"] - if coord.get("latitude"): - client.configure_set(lbase + ["latitude"], coord["latitude"]) - if coord.get("longitude"): - client.configure_set(lbase + ["longitude"], coord["longitude"]) - if coord.get("altitude"): - client.configure_set(lbase + ["altitude"], str(coord["altitude"])) - if coord.get("datum"): - client.configure_set(lbase + ["datum"], coord["datum"]) - commands.append( - "set service lldp interface {n} location coordinate-based ...".format(n=name), - ) - if loc.get("elin"): - client.configure_set(base + ["location", "elin"], loc["elin"]) - commands.append( - "set service lldp interface {n} location elin {e}".format(n=name, e=loc["elin"]), - ) - - -def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - enable=dict(type="bool", default=True), - location=dict( - type="dict", - options=dict( - coordinate_based=dict( - type="dict", - options=dict( - altitude=dict(type="int"), - datum=dict(type="str", choices=["WGS84", "NAD83", "MLLW"]), - latitude=dict(type="str", required=True), - longitude=dict(type="str", required=True), - ), - ), - elin=dict(type="str"), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - before = _get(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - for cfg in config if config else before: - name = cfg["name"] if isinstance(cfg, dict) else cfg - try: - client.configure_delete(["service", "lldp", "interface", name]) - commands.append("delete service lldp interface {n}".format(n=name)) - changed = True - except VyOSRestError: - pass - elif state in ("merged", "replaced", "overridden"): - for cfg in config: - _apply(client, cfg, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_logging_global.py b/plugins/modules/vyos_logging_global.py index be240ff..09fdd6b 100644 --- a/plugins/modules/vyos_logging_global.py +++ b/plugins/modules/vyos_logging_global.py @@ -1,15 +1,12 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# 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 -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule - - DOCUMENTATION = r""" --- module: vyos_logging_global @@ -33,12 +30,15 @@ options: type: dict suboptions: facilities: + description: List of syslog facilities to log to the console. type: list elements: dict suboptions: facility: + description: Syslog facility name (e.g. local7, all, kern). type: str severity: + description: Minimum severity level to log (e.g. err, debug, all). type: str files: description: Logging to local files. @@ -46,44 +46,59 @@ options: elements: dict suboptions: path: + description: Path to the log file on the device. type: str archive: + description: Log file archive/rotation settings. type: dict suboptions: file_num: + description: Number of archived log files to keep. type: int size: + description: Maximum size of log file in kilobytes before rotation. type: int facilities: + description: List of syslog facilities to log to this file. type: list elements: dict suboptions: facility: + description: Syslog facility name. type: str severity: + description: Minimum severity level to log. type: str global_params: description: Global syslog parameters (maps to C(system syslog global)). type: dict suboptions: archive: + description: Global log archive/rotation settings. type: dict suboptions: file_num: + description: Number of archived log files to keep. type: int size: + description: Maximum size of log file in kilobytes before rotation. type: int facilities: + description: List of syslog facilities for global logging. type: list elements: dict suboptions: facility: + description: Syslog facility name. type: str severity: + description: Minimum severity level to log. type: str marker_interval: + description: Interval in seconds between marker log entries. type: int preserve_fqdn: + description: Use the fully qualified domain name in syslog messages. type: bool hosts: description: Logging to remote syslog hosts. @@ -91,21 +106,27 @@ options: elements: dict suboptions: hostname: + description: IP address or hostname of the remote syslog server. type: str port: + description: UDP/TCP port on the remote syslog server (default 514). type: int protocol: + description: Transport protocol (udp or tcp). type: str facilities: + description: List of syslog facilities to forward to this host. type: list elements: dict suboptions: facility: + description: Syslog facility name. type: str severity: + description: Minimum severity level to forward. type: str protocol: - description: Per-facility protocol override. + description: Per-facility protocol override (udp or tcp). type: str users: description: Logging to local user terminals. @@ -113,14 +134,18 @@ options: elements: dict suboptions: username: + description: Local username whose terminal receives log messages. type: str facilities: + description: List of syslog facilities to send to this user. type: list elements: dict suboptions: facility: + description: Syslog facility name. type: str severity: + description: Minimum severity level to send. type: str running_config: @@ -213,6 +238,9 @@ saved: type: dict """ +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + # ------------------------------------------------------------ # Normalization diff --git a/plugins/modules/vyos_ntp_global.py b/plugins/modules/vyos_ntp_global.py index 185dc02..dd5fe4e 100644 --- a/plugins/modules/vyos_ntp_global.py +++ b/plugins/modules/vyos_ntp_global.py @@ -1,5 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- +# 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 @@ -218,13 +219,12 @@ def build_commands(desired, existing, state): cmds = [] if state == "overridden": - if existing["allow_clients"]: - cmds.append(("delete", ["service", "ntp", "allow-client"])) - if existing["listen_addresses"]: - cmds.append(("delete", ["service", "ntp", "listen-address"])) - if existing["servers"]: - cmds.append(("delete", ["service", "ntp", "server"])) - state = "merged" + state = "replaced" + + if state == "deleted": + if existing["servers"] or existing["allow_clients"] or existing["listen_addresses"]: + cmds.append(("delete", ["service", "ntp"])) + return cmds cmds += diff_list( "allow-client", @@ -339,7 +339,20 @@ def main(): elements="dict", options=dict( server=dict(type="str", required=True), - options=dict(type="list", elements="str"), + options=dict( + type="list", + elements="str", + choices=[ + "dynamic", + "noselect", + "pool", + "preempt", + "prefer", + "nts", + "ptp", + "interleave", + ], + ), ), ), ), diff --git a/plugins/modules/vyos_ospf_interfaces.py b/plugins/modules/vyos_ospf_interfaces.py deleted file mode 100644 index 37876d4..0000000 --- a/plugins/modules/vyos_ospf_interfaces.py +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_ospf_interfaces -short_description: OSPF Interfaces Resource Module via REST API. -description: - - Manages per-interface OSPF parameters (cost, hello/dead timers, authentication, - passive mode, etc.) for both OSPFv2 and OSPFv3 via the VyOS HTTPS REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of OSPF interface configurations. - type: list - elements: dict - suboptions: - name: - description: Interface name. - type: str - address_family: - description: OSPF settings per address family. - type: list - elements: dict - suboptions: - afi: - description: Address family (ipv4=OSPFv2, ipv6=OSPFv3). - type: str - choices: [ipv4, ipv6] - required: true - authentication: - type: dict - suboptions: - plaintext_password: - type: str - md5_key: - type: dict - suboptions: - key_id: - type: int - key: - type: str - bandwidth: - type: int - cost: - type: int - dead_interval: - type: int - hello_interval: - type: int - mtu_ignore: - type: bool - network: - type: str - priority: - type: int - retransmit_interval: - type: int - transmit_delay: - type: int - ifmtu: - type: int - instance: - type: str - passive: - type: bool - state: - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - returned: always - type: list -after: - returned: when changed - type: list -commands: - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_IFACE_TYPES = { - "eth": "ethernet", - "bond": "bonding", - "lo": "loopback", - "dummy": "dummy", - "br": "bridge", - "wg": "wireguard", - "tun": "tunnel", - "vxlan": "vxlan", -} - - -def _itype(name): - for p, t in _IFACE_TYPES.items(): - if name.startswith(p): - return t - return "ethernet" - - -def _ospf_iface_base(name, afi): - itype = _itype(name) - if afi == "ipv4": - return ["interfaces", itype, name, "ip", "ospf"] - return ["interfaces", itype, name, "ipv6", "ospfv3"] - - -def _apply(client, cfg, commands): - name = cfg["name"] - for af in cfg.get("address_family") or []: - afi = af["afi"] - base = _ospf_iface_base(name, afi) - - int_map = { - "cost": "cost", - "bandwidth": "bandwidth", - "dead_interval": "dead-interval", - "hello_interval": "hello-interval", - "priority": "priority", - "retransmit_interval": "retransmit-interval", - "transmit_delay": "transmit-delay", - "ifmtu": "ifmtu", - } - for param, key in int_map.items(): - if af.get(param) is not None: - client.configure_set(base + [key], str(af[param])) - commands.append("set {b} {k} {v}".format(b=" ".join(base), k=key, v=af[param])) - - if af.get("mtu_ignore"): - client.configure_set(base + ["mtu-ignore"]) - commands.append("set {b} mtu-ignore".format(b=" ".join(base))) - if af.get("passive"): - client.configure_set(base + ["passive"]) - commands.append("set {b} passive".format(b=" ".join(base))) - if af.get("network"): - client.configure_set(base + ["network"], af["network"]) - if af.get("instance"): - client.configure_set(base + ["instance-id"], af["instance"]) - - auth = af.get("authentication") or {} - if auth.get("plaintext_password"): - client.configure_set( - base + ["authentication", "plaintext-password"], - auth["plaintext_password"], - ) - commands.append( - "set {b} authentication plaintext-password ...".format(b=" ".join(base)), - ) - md5 = auth.get("md5_key") or {} - if md5.get("key_id") and md5.get("key"): - client.configure_set( - base + ["authentication", "md5", "key-id", str(md5["key_id"]), "md5-key"], - md5["key"], - ) - - -def _get(client): - try: - r = client.retrieve_show_config(["interfaces"]) - raw = r.get("data") or {} - out = [] - for itype, itype_data in raw.items(): - if not isinstance(itype_data, dict): - continue - for iname, idata in itype_data.items(): - if not isinstance(idata, dict): - continue - afs = [] - ip_ospf = (idata.get("ip") or {}).get("ospf") - if ip_ospf: - afs.append({"afi": "ipv4", "ospf_data": ip_ospf}) - ip6_ospfv3 = (idata.get("ipv6") or {}).get("ospfv3") - if ip6_ospfv3: - afs.append({"afi": "ipv6", "ospfv3_data": ip6_ospfv3}) - if afs: - out.append({"name": iname, "address_family": afs}) - return out - except VyOSRestError: - return [] - - -def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str"), - address_family=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]), - authentication=dict( - type="dict", - options=dict( - plaintext_password=dict(type="str", no_log=True), - md5_key=dict( - type="dict", - options=dict( - key_id=dict(type="int"), - key=dict(type="str", no_log=True), - ), - ), - ), - ), - bandwidth=dict(type="int"), - cost=dict(type="int"), - dead_interval=dict(type="int"), - hello_interval=dict(type="int"), - mtu_ignore=dict(type="bool"), - network=dict(type="str"), - priority=dict(type="int"), - retransmit_interval=dict(type="int"), - transmit_delay=dict(type="int"), - ifmtu=dict(type="int"), - instance=dict(type="str"), - passive=dict(type="bool"), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - before = _get(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - for cfg in config: - _apply(client, cfg, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_ospfv2.py b/plugins/modules/vyos_ospfv2.py deleted file mode 100644 index 063d0dc..0000000 --- a/plugins/modules/vyos_ospfv2.py +++ /dev/null @@ -1,487 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_ospfv2 -short_description: OSPFv2 resource module via REST API. -description: - - Manages OSPFv2 (OSPF for IPv4) configuration on VyOS via HTTPS REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: OSPFv2 configuration. - type: dict - suboptions: - areas: - type: list - elements: dict - suboptions: - area_id: - type: str - area_type: - type: dict - suboptions: - normal: - type: bool - nssa: - type: dict - suboptions: - set: - type: bool - default_cost: - type: int - no_summary: - type: bool - translate: - type: str - choices: [always, candidate, never] - stub: - type: dict - suboptions: - set: - type: bool - default_cost: - type: int - no_summary: - type: bool - authentication: - type: str - choices: [plaintext-password, md5] - network: - type: list - elements: dict - suboptions: - address: - type: str - range: - type: list - elements: dict - suboptions: - address: - type: str - cost: - type: int - not_advertise: - type: bool - substitute: - type: str - shortcut: - type: str - choices: [default, disable, enable] - auto_cost: - type: dict - suboptions: - reference_bandwidth: - type: int - default_information: - type: dict - suboptions: - originate: - type: dict - suboptions: - always: - type: bool - metric: - type: int - metric_type: - type: int - route_map: - type: str - log_adjacency_changes: - type: str - choices: [detail] - max_metric: - type: dict - suboptions: - router_lsa: - type: dict - suboptions: - administrative: - type: bool - on_shutdown: - type: int - on_startup: - type: int - mpls_te: - type: dict - suboptions: - enabled: - type: bool - router_address: - type: str - neighbor: - type: list - elements: dict - suboptions: - neighbor_id: - type: str - poll_interval: - type: int - priority: - type: int - parameters: - type: dict - suboptions: - router_id: - type: str - opaque_lsa: - type: bool - rfc1583_compatibility: - type: bool - abr_type: - type: str - choices: [cisco, ibm, shortcut, standard] - passive_interface: - type: list - elements: str - redistribute: - type: list - elements: dict - suboptions: - route_type: - type: str - choices: [bgp, connected, kernel, rip, static] - metric: - type: int - metric_type: - type: int - route_map: - type: str - state: - type: str - choices: [merged, replaced, deleted, gathered] - default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - returned: always - type: dict -after: - returned: when changed - type: dict -commands: - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_BASE = ["protocols", "ospf"] - - -def _get(client): - try: - r = client.retrieve_show_config(_BASE) - return r.get("data") or {} - except VyOSRestError: - return {} - - -def _apply(client, config, commands): - params = config.get("parameters") or {} - if params.get("router_id"): - client.configure_set(_BASE + ["parameters", "router-id"], params["router_id"]) - commands.append("set protocols ospf parameters router-id {r}".format(r=params["router_id"])) - if params.get("opaque_lsa"): - client.configure_set(_BASE + ["parameters", "opaque-lsa"]) - if params.get("rfc1583_compatibility"): - client.configure_set(_BASE + ["parameters", "rfc1583-compatibility"]) - if params.get("abr_type"): - client.configure_set(_BASE + ["parameters", "abr-type"], params["abr_type"]) - - ac = config.get("auto_cost") or {} - if ac.get("reference_bandwidth"): - client.configure_set( - _BASE + ["auto-cost", "reference-bandwidth"], - str(ac["reference_bandwidth"]), - ) - commands.append( - "set protocols ospf auto-cost reference-bandwidth {b}".format( - b=ac["reference_bandwidth"], - ), - ) - - if config.get("log_adjacency_changes"): - client.configure_set(_BASE + ["log-adjacency-changes", config["log_adjacency_changes"]]) - - for nbr in config.get("neighbor") or []: - nb = _BASE + ["neighbor", nbr["neighbor_id"]] - client.configure_set(nb) - if nbr.get("poll_interval"): - client.configure_set(nb + ["poll-interval"], str(nbr["poll_interval"])) - if nbr.get("priority"): - client.configure_set(nb + ["priority"], str(nbr["priority"])) - - for pi in config.get("passive_interface") or []: - client.configure_set(_BASE + ["passive-interface"], pi) - commands.append("set protocols ospf passive-interface {i}".format(i=pi)) - - for redist in config.get("redistribute") or []: - rb = _BASE + ["redistribute", redist["route_type"]] - client.configure_set(rb) - commands.append("set protocols ospf redistribute {r}".format(r=redist["route_type"])) - if redist.get("metric"): - client.configure_set(rb + ["metric"], str(redist["metric"])) - if redist.get("metric_type"): - client.configure_set(rb + ["metric-type"], str(redist["metric_type"])) - if redist.get("route_map"): - client.configure_set(rb + ["route-map"], redist["route_map"]) - - di = (config.get("default_information") or {}).get("originate") or {} - if di: - dib = _BASE + ["default-information", "originate"] - client.configure_set(dib) - commands.append("set protocols ospf default-information originate") - if di.get("always"): - client.configure_set(dib + ["always"]) - if di.get("metric"): - client.configure_set(dib + ["metric"], str(di["metric"])) - if di.get("metric_type"): - client.configure_set(dib + ["metric-type"], str(di["metric_type"])) - if di.get("route_map"): - client.configure_set(dib + ["route-map"], di["route_map"]) - - for area in config.get("areas") or []: - aid = area["area_id"] - ab = _BASE + ["area", aid] - client.configure_set(ab) - commands.append("set protocols ospf area {a}".format(a=aid)) - - at = area.get("area_type") or {} - if at.get("normal"): - pass # default - nssa = at.get("nssa") or {} - if nssa.get("set"): - client.configure_set(ab + ["area-type", "nssa"]) - if nssa.get("default_cost"): - client.configure_set( - ab + ["area-type", "nssa", "default-cost"], - str(nssa["default_cost"]), - ) - if nssa.get("no_summary"): - client.configure_set(ab + ["area-type", "nssa", "no-summary"]) - stub = at.get("stub") or {} - if stub.get("set"): - client.configure_set(ab + ["area-type", "stub"]) - if stub.get("default_cost"): - client.configure_set( - ab + ["area-type", "stub", "default-cost"], - str(stub["default_cost"]), - ) - - if area.get("authentication"): - client.configure_set(ab + ["authentication"], area["authentication"]) - - for net in area.get("network") or []: - client.configure_set(ab + ["network"], net["address"]) - commands.append( - "set protocols ospf area {a} network {n}".format( - a=aid, - n=net["address"], - ), - ) - - for rng in area.get("range") or []: - rb2 = ab + ["range", rng["address"]] - client.configure_set(rb2) - if rng.get("cost"): - client.configure_set(rb2 + ["cost"], str(rng["cost"])) - if rng.get("not_advertise"): - client.configure_set(rb2 + ["not-advertise"]) - if rng.get("substitute"): - client.configure_set(rb2 + ["substitute"], rng["substitute"]) - - if area.get("shortcut"): - client.configure_set(ab + ["shortcut"], area["shortcut"]) - - -def main(): - argument_spec = dict( - config=dict( - type="dict", - options=dict( - areas=dict( - type="list", - elements="dict", - options=dict( - area_id=dict(type="str"), - area_type=dict( - type="dict", - options=dict( - normal=dict(type="bool"), - nssa=dict( - type="dict", - options=dict( - set=dict(type="bool"), - default_cost=dict(type="int"), - no_summary=dict(type="bool"), - translate=dict( - type="str", - choices=["always", "candidate", "never"], - ), - ), - ), - stub=dict( - type="dict", - options=dict( - set=dict(type="bool"), - default_cost=dict(type="int"), - no_summary=dict(type="bool"), - ), - ), - ), - ), - authentication=dict(type="str", choices=["plaintext-password", "md5"]), - network=dict( - type="list", - elements="dict", - options=dict(address=dict(type="str")), - ), - range=dict( - type="list", - elements="dict", - options=dict( - address=dict(type="str"), - cost=dict(type="int"), - not_advertise=dict(type="bool"), - substitute=dict(type="str"), - ), - ), - shortcut=dict(type="str", choices=["default", "disable", "enable"]), - ), - ), - auto_cost=dict(type="dict", options=dict(reference_bandwidth=dict(type="int"))), - default_information=dict( - type="dict", - options=dict( - originate=dict( - type="dict", - options=dict( - always=dict(type="bool"), - metric=dict(type="int"), - metric_type=dict(type="int"), - route_map=dict(type="str"), - ), - ), - ), - ), - log_adjacency_changes=dict(type="str", choices=["detail"]), - max_metric=dict(type="dict"), - mpls_te=dict( - type="dict", - options=dict( - enabled=dict(type="bool"), - router_address=dict(type="str"), - ), - ), - neighbor=dict( - type="list", - elements="dict", - options=dict( - neighbor_id=dict(type="str"), - poll_interval=dict(type="int"), - priority=dict(type="int"), - ), - ), - parameters=dict( - type="dict", - options=dict( - router_id=dict(type="str"), - opaque_lsa=dict(type="bool"), - rfc1583_compatibility=dict(type="bool"), - abr_type=dict(type="str", choices=["cisco", "ibm", "shortcut", "standard"]), - ), - ), - passive_interface=dict(type="list", elements="str"), - redistribute=dict( - type="list", - elements="dict", - options=dict( - route_type=dict( - type="str", - choices=["bgp", "connected", "kernel", "rip", "static"], - ), - metric=dict(type="int"), - metric_type=dict(type="int"), - route_map=dict(type="str"), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule( - argument_spec=argument_spec, - required_if=[("state", "merged", ["config"]), ("state", "replaced", ["config"])], - supports_check_mode=True, - ) - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") - commands = [] - changed = False - before = _get(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - if before: - client.configure_delete(_BASE) - commands.append("delete protocols ospf") - changed = True - elif state in ("merged", "replaced"): - if state == "replaced" and before: - client.configure_delete(_BASE) - commands.append("delete protocols ospf") - _apply(client, config, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_ospfv3.py b/plugins/modules/vyos_ospfv3.py deleted file mode 100644 index 0de49d7..0000000 --- a/plugins/modules/vyos_ospfv3.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_ospfv3 -short_description: OSPFv3 resource module via REST API. -description: - - Manages OSPFv3 (OSPF for IPv6) configuration on VyOS via HTTPS REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: OSPFv3 configuration. - type: dict - suboptions: - areas: - type: list - elements: dict - suboptions: - area_id: - type: str - export_list: - type: str - import_list: - type: str - interface: - description: Interfaces in this area. - type: list - elements: dict - suboptions: - name: - type: str - range: - type: list - elements: dict - suboptions: - address: - type: str - advertise: - type: bool - not_advertise: - type: bool - parameters: - type: dict - suboptions: - router_id: - type: str - redistribute: - type: list - elements: dict - suboptions: - route_type: - type: str - choices: [bgp, connected, kernel, ripng, static] - route_map: - type: str - state: - type: str - choices: [merged, replaced, deleted, gathered] - default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - returned: always - type: dict -after: - returned: when changed - type: dict -commands: - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_BASE = ["protocols", "ospfv3"] - - -def _get(client): - try: - r = client.retrieve_show_config(_BASE) - return r.get("data") or {} - except VyOSRestError: - return {} - - -def _apply(client, config, commands): - params = config.get("parameters") or {} - if params.get("router_id"): - client.configure_set(_BASE + ["parameters", "router-id"], params["router_id"]) - commands.append( - "set protocols ospfv3 parameters router-id {r}".format(r=params["router_id"]), - ) - - for redist in config.get("redistribute") or []: - rb = _BASE + ["redistribute", redist["route_type"]] - client.configure_set(rb) - commands.append("set protocols ospfv3 redistribute {r}".format(r=redist["route_type"])) - if redist.get("route_map"): - client.configure_set(rb + ["route-map"], redist["route_map"]) - - for area in config.get("areas") or []: - aid = area["area_id"] - ab = _BASE + ["area", aid] - client.configure_set(ab) - commands.append("set protocols ospfv3 area '{a}'".format(a=aid)) - if area.get("export_list"): - client.configure_set(ab + ["export-list"], area["export_list"]) - commands.append( - "set protocols ospfv3 area {a} export-list {e}".format( - a=aid, - e=area["export_list"], - ), - ) - if area.get("import_list"): - client.configure_set(ab + ["import-list"], area["import_list"]) - for iface in area.get("interface") or []: - client.configure_set(ab + ["interface"], iface["name"]) - commands.append( - "set protocols ospfv3 area {a} interface {i}".format( - a=aid, - i=iface["name"], - ), - ) - for rng in area.get("range") or []: - rb2 = ab + ["range", rng["address"]] - client.configure_set(rb2) - commands.append( - "set protocols ospfv3 area {a} range {r}".format( - a=aid, - r=rng["address"], - ), - ) - if rng.get("advertise"): - client.configure_set(rb2 + ["advertise"]) - if rng.get("not_advertise"): - client.configure_set(rb2 + ["not-advertise"]) - - -def main(): - argument_spec = dict( - config=dict( - type="dict", - options=dict( - areas=dict( - type="list", - elements="dict", - options=dict( - area_id=dict(type="str"), - export_list=dict(type="str"), - import_list=dict(type="str"), - interface=dict( - type="list", - elements="dict", - options=dict(name=dict(type="str")), - ), - range=dict( - type="list", - elements="dict", - options=dict( - address=dict(type="str"), - advertise=dict(type="bool"), - not_advertise=dict(type="bool"), - ), - ), - ), - ), - parameters=dict(type="dict", options=dict(router_id=dict(type="str"))), - redistribute=dict( - type="list", - elements="dict", - options=dict( - route_type=dict( - type="str", - choices=["bgp", "connected", "kernel", "ripng", "static"], - ), - route_map=dict(type="str"), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule( - argument_spec=argument_spec, - required_if=[("state", "merged", ["config"]), ("state", "replaced", ["config"])], - supports_check_mode=True, - ) - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") - commands = [] - changed = False - before = _get(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted": - if before: - client.configure_delete(_BASE) - commands.append("delete protocols ospfv3") - changed = True - elif state in ("merged", "replaced"): - if state == "replaced" and before: - client.configure_delete(_BASE) - commands.append("delete protocols ospfv3") - _apply(client, config, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_ping.py b/plugins/modules/vyos_ping.py deleted file mode 100644 index 45368d1..0000000 --- a/plugins/modules/vyos_ping.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_ping -short_description: Test reachability using ping via VyOS REST API. -description: - - Sends an ICMP ping via the VyOS HTTPS REST API (/show endpoint) and - checks reachability. Mirrors C(vyos.vyos.vyos_ping). -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - dest: - description: Destination IP address or hostname. - type: str - required: true - count: - description: Number of packets to send. - type: int - default: 5 - source: - description: Source interface or IP address. - type: str - ttl: - description: Time-to-live value. - type: int - size: - description: Packet size in bytes. - type: int - interval: - description: Interval between pings in seconds. - type: int - state: - description: C(present) = expect success; C(absent) = expect failure. - type: str - choices: [present, absent] - default: present - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -commands: - description: Ping command tokens sent to the API. - returned: always - type: list -packet_loss: - description: Packet loss percentage string. - returned: always - type: str -packets_rx: - description: Packets received. - returned: always - type: int -packets_tx: - description: Packets transmitted. - returned: always - type: int -rtt: - description: RTT statistics dict (min/avg/max/mdev). - returned: when available - type: dict -""" - -import re - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def _build_ping_path(params): - path = ["ping", params["dest"]] - if params.get("count"): - path += ["count", str(params["count"])] - if params.get("source"): - path += ["interface", params["source"]] - if params.get("ttl"): - path += ["ttl", str(params["ttl"])] - if params.get("size"): - path += ["size", str(params["size"])] - return path - - -def _parse_ping_output(output): - stats = {"packet_loss": "100%", "packets_rx": 0, "packets_tx": 0} - tx_match = re.search(r"(\d+) packets transmitted", output) - rx_match = re.search(r"(\d+) received", output) - loss_match = re.search(r"(\d+(?:\.\d+)?%)\s+packet loss", output) - if tx_match: - stats["packets_tx"] = int(tx_match.group(1)) - if rx_match: - stats["packets_rx"] = int(rx_match.group(1)) - if loss_match: - stats["packet_loss"] = loss_match.group(1) - rtt_match = re.search( - r"rtt min/avg/max/mdev = ([\d.]+)/([\d.]+)/([\d.]+)/([\d.]+)", - output, - ) - if rtt_match: - stats["rtt"] = { - "min": rtt_match.group(1), - "avg": rtt_match.group(2), - "max": rtt_match.group(3), - "mdev": rtt_match.group(4), - } - return stats - - -def main(): - argument_spec = dict( - dest=dict(type="str", required=True), - count=dict(type="int", default=5), - source=dict(type="str"), - ttl=dict(type="int"), - size=dict(type="int"), - interval=dict(type="int"), - state=dict(type="str", default="present", choices=["present", "absent"]), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - - if module.check_mode: - module.exit_json(changed=False, commands=[], packet_loss="0%", packets_rx=0, packets_tx=0) - - client = VyOSRestClient(module) - path = _build_ping_path(module.params) - - try: - result = client.show(path) - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - output = result.get("data", "") - stats = _parse_ping_output(output) - reachable = stats["packets_rx"] > 0 - - if module.params["state"] == "present" and not reachable: - module.fail_json( - msg="Ping to {dest} failed — no packets received.".format(dest=module.params["dest"]), - **stats, - ) - elif module.params["state"] == "absent" and reachable: - module.fail_json( - msg="Expected {dest} to be unreachable but ping succeeded.".format( - dest=module.params["dest"], - ), - **stats, - ) - - module.exit_json(changed=False, commands=path, **stats) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_prefix_lists.py b/plugins/modules/vyos_prefix_lists.py deleted file mode 100644 index 63fa584..0000000 --- a/plugins/modules/vyos_prefix_lists.py +++ /dev/null @@ -1,267 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_prefix_lists -short_description: Prefix-Lists resource module via REST API. -description: - - Manages IPv4 and IPv6 prefix lists on VyOS via the HTTPS REST API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of prefix-list configurations. - type: list - elements: dict - suboptions: - afi: - description: Address family. - type: str - choices: [ipv4, ipv6] - required: true - prefix_lists: - description: Named prefix lists. - type: list - elements: dict - suboptions: - name: - description: Prefix list name. - type: str - required: true - description: - description: Description. - type: str - entries: - description: Prefix list rules. - type: list - elements: dict - suboptions: - sequence: - description: Rule sequence number. - type: int - required: true - description: - type: str - action: - description: permit or deny. - type: str - choices: [permit, deny] - ge: - description: Minimum prefix length. - type: int - le: - description: Maximum prefix length. - type: int - prefix: - description: Network prefix to match. - type: str - state: - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -before: - returned: always - type: list -after: - returned: when changed - type: list -commands: - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_PL_KEY = {"ipv4": "prefix-list", "ipv6": "prefix-list6"} - - -def _get(client): - try: - r = client.retrieve_show_config(["policy"]) - data = r.get("data") or {} - out = [] - for afi, key in [("ipv4", "prefix-list"), ("ipv6", "prefix-list6")]: - pl_data = data.get(key) or {} - if not isinstance(pl_data, dict): - continue - pls = [] - for pl_name, pl_info in pl_data.items(): - entry = {"name": pl_name} - if isinstance(pl_info, dict): - if "description" in pl_info: - entry["description"] = pl_info["description"] - rules = [] - for seq, rdata in (pl_info.get("rule") or {}).items(): - if isinstance(rdata, dict): - rules.append({"sequence": int(seq), **rdata}) - if rules: - entry["entries"] = rules - pls.append(entry) - if pls: - out.append({"afi": afi, "prefix_lists": pls}) - return out - except VyOSRestError: - return [] - - -def _apply(client, entry, commands): - afi = entry["afi"] - key = _PL_KEY[afi] - for pl in entry.get("prefix_lists") or []: - base = ["policy", key, pl["name"]] - client.configure_set(base) - commands.append("set policy {k} {n}".format(k=key, n=pl["name"])) - if pl.get("description"): - client.configure_set(base + ["description"], pl["description"]) - for rule in pl.get("entries") or []: - rb = base + ["rule", str(rule["sequence"])] - client.configure_set(rb) - commands.append( - "set policy {k} {n} rule {s}".format( - k=key, - n=pl["name"], - s=rule["sequence"], - ), - ) - if rule.get("action"): - client.configure_set(rb + ["action"], rule["action"]) - commands.append( - "set policy {k} {n} rule {s} action {a}".format( - k=key, - n=pl["name"], - s=rule["sequence"], - a=rule["action"], - ), - ) - if rule.get("prefix"): - client.configure_set(rb + ["prefix"], rule["prefix"]) - if rule.get("ge") is not None: - client.configure_set(rb + ["ge"], str(rule["ge"])) - if rule.get("le") is not None: - client.configure_set(rb + ["le"], str(rule["le"])) - if rule.get("description"): - client.configure_set(rb + ["description"], rule["description"]) - - -def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]), - prefix_lists=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - description=dict(type="str"), - entries=dict( - type="list", - elements="dict", - options=dict( - sequence=dict(type="int", required=True), - description=dict(type="str"), - action=dict(type="str", choices=["permit", "deny"]), - ge=dict(type="int"), - le=dict(type="int"), - prefix=dict(type="str"), - ), - ), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - before = _get(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted" and not config: - for afi, key in [("ipv4", "prefix-list"), ("ipv6", "prefix-list6")]: - try: - client.configure_delete(["policy", key]) - commands.append("delete policy {k}".format(k=key)) - except VyOSRestError: - pass - changed = True - elif state == "deleted" and config: - for entry in config: - key = _PL_KEY[entry["afi"]] - for pl in entry.get("prefix_lists") or []: - try: - client.configure_delete(["policy", key, pl["name"]]) - commands.append("delete policy {k} {n}".format(k=key, n=pl["name"])) - changed = True - except VyOSRestError: - pass - elif state in ("merged", "replaced", "overridden"): - if state in ("replaced", "overridden"): - for entry in config: - key = _PL_KEY[entry["afi"]] - for pl in entry.get("prefix_lists") or []: - try: - client.configure_delete(["policy", key, pl["name"]]) - except VyOSRestError: - pass - for entry in config: - _apply(client, entry, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_reset.py b/plugins/modules/vyos_reset.py deleted file mode 100644 index ca3e343..0000000 --- a/plugins/modules/vyos_reset.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_reset -short_description: Execute reset commands on a VyOS device via REST API. -description: - - Sends a C(reset) operational command to a VyOS device via the HTTPS REST - API (C(/reset) endpoint). - - Useful for resetting BGP sessions, VPN tunnels, ARP caches, etc. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - path: - description: - - Reset command path tokens. - - E.g. C(["ip", "bgp", "192.0.2.1"]) to reset a BGP peer. - type: list - elements: str - required: true - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -examples: | - - name: Reset a BGP peer - vyos.rest.vyos_reset: - hostname: 192.168.1.1 - api_key: MY-KEY - path: ["ip", "bgp", "192.0.2.11"] - - - name: Reset an IPsec VPN peer - vyos.rest.vyos_reset: - hostname: 192.168.1.1 - api_key: MY-KEY - path: ["vpn", "ipsec-peer", "203.0.113.5"] - - - name: Clear ARP cache - vyos.rest.vyos_reset: - hostname: 192.168.1.1 - api_key: MY-KEY - path: ["arp", "cache"] -""" - -RETURN = r""" -output: - description: Text output from the reset command (usually empty on success). - returned: success - type: str -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def main(): - argument_spec = dict( - path=dict(type="list", elements="str", required=True), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=False, - ) - - client = VyOSRestClient(module) - try: - result = client.reset(module.params["path"]) - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - module.exit_json(changed=True, output=result.get("data", "")) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_route_maps.py b/plugins/modules/vyos_route_maps.py index d13a88d..55b1706 100644 --- a/plugins/modules/vyos_route_maps.py +++ b/plugins/modules/vyos_route_maps.py @@ -1,7 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # GNU General Public License v3.0+ - from __future__ import absolute_import, division, print_function @@ -10,12 +9,14 @@ __metaclass__ = type DOCUMENTATION = r""" --- module: vyos_route_maps -short_description: Route Map resource module via REST API. +short_description: Manage route-map configuration on VyOS devices using REST API description: - - Manages route maps on VyOS via the HTTPS REST API. + - Manages route maps on VyOS via the REST API. + - Uses REST API (C(connection=httpapi)) instead of CLI. version_added: "1.0.0" author: - VyOS Community (@vyos) + options: config: description: List of route-map configurations. @@ -25,351 +26,416 @@ options: route_map: description: Route map name. type: str + required: true entries: description: Route map rules. type: list elements: dict suboptions: sequence: - description: Rule number (1-65535). + description: Rule sequence number (1-65535). type: int + required: true action: - description: permit or deny. + description: Permit or deny. type: str - choices: [permit, deny] description: + description: Rule description. type: str call: description: Call another route map. type: str continue_sequence: - description: Continue at a different sequence. + description: Continue at a different sequence number. type: int match: - description: Match conditions. + description: Match conditions (passed through to VyOS API). type: dict - suboptions: - prefix_list: - type: str - prefix_list6: - type: str - interface: - type: str - metric: - type: int - origin: - type: str - ip: - type: dict - suboptions: - nexthop_address: - type: str - nexthop_prefix_list: - type: str - ipv6: - type: dict - suboptions: - nexthop_address: - type: str set: - description: Route parameters to set. + description: Route parameters to set (passed through to VyOS API). type: dict - suboptions: - aggregator: - type: dict - suboptions: - as: - type: str - ip: - type: str - as_path_prepend: - type: str - atomic_aggregate: - type: bool - bgp_extcommunity_rt: - type: str - community: - type: dict - suboptions: - value: - type: str - ip_next_hop: - type: str - ipv6_next_hop: - type: dict - suboptions: - ip_type: - type: str - choices: [global, local] - value: - type: str - large_community: - type: str - local_preference: - type: int - metric: - type: str - metric_type: - type: str - origin: - type: str - originator_id: - type: str - src: - type: str - tag: - type: str - weight: - type: int + state: + description: + - Desired state of the route-map configuration. + - C(merged) adds or updates entries without removing existing ones. + - C(replaced) replaces each named route map mentioned in config. + - C(overridden) replaces all route maps. + - C(deleted) removes route maps. Without config removes all. + - C(gathered) returns current configuration as structured data. type: str choices: [merged, replaced, overridden, deleted, gathered] default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false + +notes: + - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin. + - C(ansible_network_os) must be set to C(vyos.rest.vyos). + - Input validation is delegated to the VyOS API. +""" + +EXAMPLES = r""" +- name: Merge route map configuration + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + entries: + - sequence: 10 + action: permit + match: + peer: 192.0.2.32 + set: + metric: "5" + as_path_exclude: "111" + aggregator: + as: 100 + state: merged + +- name: Delete all route maps + vyos.rest.vyos_route_maps: + state: deleted + +- name: Delete a specific route map + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + state: deleted + +- name: Gather current route map configuration + vyos.rest.vyos_route_maps: + state: gathered """ RETURN = r""" before: + description: Route map configuration before this module ran. returned: always type: list + after: + description: Route map configuration after this module ran. returned: when changed type: list + commands: + description: List of API command tuples sent to the device. returned: always type: list + +gathered: + description: Current route map configuration as structured data. + returned: when state is gathered + type: list + +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool + +response: + description: Raw API response. + returned: when changes are applied + type: dict """ from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule -def _get(client): - try: - r = client.retrieve_show_config(["policy", "route-map"]) - data = r.get("data") or {} - return [{"route_map": name, "raw": rdata} for name, rdata in data.items()] - except VyOSRestError: +_BASE = ["policy", "route-map"] + +_SET_MAP = { + "as_path_prepend": ["as-path", "prepend"], + "as_path_exclude": ["as-path", "exclude"], + "as_path_prepend_last_as": ["as-path", "prepend-last-as"], + "ip_next_hop": ["ip-next-hop"], + "local_preference": ["local-preference"], + "metric": ["metric"], + "metric_type": ["metric-type"], + "origin": ["origin"], + "originator_id": ["originator-id"], + "src": ["src"], + "tag": ["tag"], + "weight": ["weight"], + "distance": ["distance"], + "table": ["table"], +} + +_MATCH_MAP = { + "interface": ["interface"], + "metric": ["metric"], + "origin": ["origin"], + "peer": ["peer"], + "protocol": ["protocol"], +} + + +def get_running_config(vyos): + raw = vyos.get_config(["policy", "route-map"]) + if not raw or not isinstance(raw, dict): + return [] + + rm_data = raw.get("route-map", raw) + if not isinstance(rm_data, dict): return [] + result = [] + for rm_name, rm_info in sorted(rm_data.items()): + entry = {"route_map": rm_name, "entries": []} + rm_info = rm_info or {} + + for seq, rule_data in sorted( + (rm_info.get("rule") or {}).items(), + key=lambda x: int(x[0]), + ): + rule_data = rule_data or {} + rule = {"sequence": int(seq)} + if rule_data.get("action"): + rule["action"] = rule_data["action"] + if rule_data.get("description"): + rule["description"] = rule_data["description"] + if rule_data.get("call"): + rule["call"] = rule_data["call"] + if rule_data.get("continue"): + rule["continue_sequence"] = int(rule_data["continue"]) + if rule_data.get("match"): + rule["match"] = rule_data["match"] + if rule_data.get("set"): + rule["set"] = rule_data["set"] + entry["entries"].append(rule) + + result.append(entry) + + return result + + +def _match_cmds(rbase, match): + cmds = [] + if not match: + return cmds + + mbase = rbase + ["match"] + + for key, path_suffix in _MATCH_MAP.items(): + if match.get(key) is not None: + cmds.append(("set", mbase + path_suffix + [str(match[key])])) + + if match.get("prefix_list"): + cmds.append(("set", mbase + ["ip", "address", "prefix-list", match["prefix_list"]])) + if match.get("prefix_list6"): + cmds.append(("set", mbase + ["ipv6", "address", "prefix-list", match["prefix_list6"]])) + + ip = match.get("ip") or {} + if ip.get("nexthop_address"): + cmds.append(("set", mbase + ["ip", "nexthop", "address", ip["nexthop_address"]])) + if ip.get("nexthop_prefix_list"): + cmds.append(("set", mbase + ["ip", "nexthop", "prefix-list", ip["nexthop_prefix_list"]])) + + ipv6 = match.get("ipv6") or {} + if ipv6.get("nexthop_address"): + cmds.append(("set", mbase + ["ipv6", "nexthop", "address", ipv6["nexthop_address"]])) + + return cmds + + +def _set_cmds(rbase, setv): + cmds = [] + if not setv: + return cmds -def _apply(client, rm_cfg, commands): - rm_name = rm_cfg["route_map"] - for rule in rm_cfg.get("entries") or []: - seq = str(rule["sequence"]) - base = ["policy", "route-map", rm_name, "rule", seq] - client.configure_set(base) - commands.append("set policy route-map {n} rule {s}".format(n=rm_name, s=seq)) - - if rule.get("action"): - client.configure_set(base + ["action"], rule["action"]) - commands.append( - "set policy route-map {n} rule {s} action {a}".format( - n=rm_name, - s=seq, - a=rule["action"], - ), - ) - if rule.get("description"): - client.configure_set(base + ["description"], rule["description"]) - if rule.get("call"): - client.configure_set(base + ["call"], rule["call"]) - if rule.get("continue_sequence"): - client.configure_set(base + ["continue"], str(rule["continue_sequence"])) - - # Match conditions + sbase = rbase + ["set"] + + for key, path_suffix in _SET_MAP.items(): + if setv.get(key) is not None: + cmds.append(("set", sbase + path_suffix + [str(setv[key])])) + + if setv.get("atomic_aggregate"): + cmds.append(("set", sbase + ["atomic-aggregate"])) + + comm = setv.get("community") or {} + if comm.get("value"): + cmds.append(("set", sbase + ["community", comm["value"]])) + + large_comm = setv.get("large_community") + if large_comm is not None: + cmds.append(("set", sbase + ["large-community", str(large_comm)])) + + agg = setv.get("aggregator") or {} + agg_as = agg.get("as") or agg.get("as_") + if agg_as and agg.get("ip"): + cmds.append(("set", sbase + ["aggregator", "as", str(agg_as), "address", agg["ip"]])) + elif agg_as: + cmds.append(("set", sbase + ["aggregator", "as", str(agg_as)])) + + nh6 = setv.get("ipv6_next_hop") or {} + if nh6.get("value"): + ip_type = nh6.get("ip_type") or "global" + cmds.append(("set", sbase + ["ipv6-next-hop", ip_type, nh6["value"]])) + + return cmds + + +def _want_to_api_set(setv): + if not setv: + return {} + api = {} + for key, path in _SET_MAP.items(): + if setv.get(key) is not None: + d = api + for p in path[:-1]: + d = d.setdefault(p, {}) + d[path[-1]] = str(setv[key]) + agg = setv.get("aggregator") or {} + agg_as = agg.get("as") or agg.get("as_") + if agg_as: + api.setdefault("aggregator", {})["as"] = str(agg_as) + large_comm = setv.get("large_community") + if large_comm is not None: + api["large-community"] = {str(large_comm): {}} + return api + + +def _want_to_api_match(match): + if not match: + return {} + api = {} + for key in _MATCH_MAP: + if match.get(key) is not None: + api[key] = str(match[key]) + if match.get("prefix_list"): + api.setdefault("ip", {}).setdefault("address", {})["prefix-list"] = match["prefix_list"] + if match.get("prefix_list6"): + api.setdefault("ipv6", {}).setdefault("address", {})["prefix-list"] = match["prefix_list6"] + return api + + +def _rule_cmds(rm_name, rule, have_rule): + cmds = [] + seq = str(rule["sequence"]) + rbase = _BASE + [rm_name, "rule", seq] + + if rule.get("action") and rule["action"] != have_rule.get("action"): + cmds.append(("set", rbase + ["action", rule["action"]])) + if rule.get("description") and rule["description"] != have_rule.get("description"): + cmds.append(("set", rbase + ["description", rule["description"]])) + if rule.get("call") and rule["call"] != have_rule.get("call"): + cmds.append(("set", rbase + ["call", rule["call"]])) + if rule.get("continue_sequence") is not None and rule["continue_sequence"] != have_rule.get( + "continue_sequence", + ): + cmds.append(("set", rbase + ["continue", str(rule["continue_sequence"])])) + + want_match_api = _want_to_api_match(rule.get("match")) + have_match = have_rule.get("match") or {} + changed_match = {k: v for k, v in want_match_api.items() if have_match.get(k) != v} + if changed_match: match = rule.get("match") or {} - if match.get("prefix_list"): - client.configure_set( - base + ["match", "ip", "address", "prefix-list"], - match["prefix_list"], - ) - if match.get("interface"): - client.configure_set(base + ["match", "interface"], match["interface"]) - if match.get("metric") is not None: - client.configure_set(base + ["match", "metric"], str(match["metric"])) - ip_match = match.get("ip") or {} - if ip_match.get("nexthop_address"): - client.configure_set( - base + ["match", "ip", "nexthop", "address"], - ip_match["nexthop_address"], - ) - - # Set actions - setv = rule.get("set") or {} - if setv.get("ip_next_hop"): - client.configure_set(base + ["set", "ip-next-hop"], setv["ip_next_hop"]) - commands.append( - "set policy route-map {n} rule {s} set ip-next-hop {nh}".format( - n=rm_name, - s=seq, - nh=setv["ip_next_hop"], - ), - ) - if setv.get("local_preference") is not None: - client.configure_set( - base + ["set", "local-preference"], - str(setv["local_preference"]), - ) - if setv.get("metric"): - client.configure_set(base + ["set", "metric"], str(setv["metric"])) - if setv.get("origin"): - client.configure_set(base + ["set", "origin"], setv["origin"]) - if setv.get("weight") is not None: - client.configure_set(base + ["set", "weight"], str(setv["weight"])) - if setv.get("as_path_prepend"): - client.configure_set(base + ["set", "as-path-prepend"], setv["as_path_prepend"]) - comm = setv.get("community") or {} - if comm.get("value"): - client.configure_set(base + ["set", "community", comm["value"]]) - if setv.get("tag"): - client.configure_set(base + ["set", "tag"], setv["tag"]) - if setv.get("src"): - client.configure_set(base + ["set", "src"], setv["src"]) - nh6 = setv.get("ipv6_next_hop") or {} - if nh6.get("value"): - ip_type = nh6.get("ip_type", "global") - client.configure_set(base + ["set", "ipv6-next-hop", ip_type], nh6["value"]) + changed_keys = set(changed_match.keys()) + partial_match = { + k: v + for k, v in match.items() + if k in changed_keys + or (k == "prefix_list" and "ip" in changed_keys) + or (k == "prefix_list6" and "ipv6" in changed_keys) + } + if not partial_match: + partial_match = match + cmds += _match_cmds(rbase, partial_match) + + want_set_api = _want_to_api_set(rule.get("set")) + have_set = have_rule.get("set") or {} + if want_set_api != have_set: + cmds += _set_cmds(rbase, rule.get("set")) + + return cmds + + +def build_commands(config, have_raw, state): + cmds = [] + + if state == "deleted": + if not config: + if have_raw: + cmds.append(("delete", _BASE)) + else: + for rm in config: + cmds.append(("delete", _BASE + [rm["route_map"]])) + return cmds + + have_map = {e["route_map"]: e for e in have_raw} + + if state == "overridden": + want_names = {rm["route_map"] for rm in config} + for name in set(have_map) - want_names: + cmds.append(("delete", _BASE + [name])) + + for rm in config: + rm_name = rm["route_map"] + have_rm = have_map.get(rm_name, {}) + + if state == "replaced" and rm_name in have_map: + # Only delete and rebuild if something actually differs + have_entries = {str(r["sequence"]): r for r in (have_rm.get("entries") or [])} + want_seqs = {str(r["sequence"]) for r in (rm.get("entries") or [])} + extra_seqs = set(have_entries) - want_seqs + test_cmds = [] + for rule in rm.get("entries") or []: + have_rule = have_entries.get(str(rule["sequence"]), {}) + test_cmds += _rule_cmds(rm_name, rule, have_rule) + if test_cmds or extra_seqs: + cmds.append(("delete", _BASE + [rm_name])) + have_rm = {} + else: + continue # already matches — idempotent + + have_entries = {str(r["sequence"]): r for r in (have_rm.get("entries") or [])} + + for rule in rm.get("entries") or []: + have_rule = have_entries.get(str(rule["sequence"]), {}) + cmds += _rule_cmds(rm_name, rule, have_rule) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict(type="list", elements="dict"), + state=dict( + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered"], + ), +) def main(): - set_spec = dict( - type="dict", - options=dict( - aggregator=dict(type="dict", options=dict(as_=dict(type="str"), ip=dict(type="str"))), - as_path_prepend=dict(type="str"), - atomic_aggregate=dict(type="bool"), - bgp_extcommunity_rt=dict(type="str"), - community=dict(type="dict", options=dict(value=dict(type="str"))), - ip_next_hop=dict(type="str"), - ipv6_next_hop=dict( - type="dict", - options=dict( - ip_type=dict(type="str", choices=["global", "local"]), - value=dict(type="str"), - ), - ), - large_community=dict(type="str"), - local_preference=dict(type="int"), - metric=dict(type="str"), - metric_type=dict(type="str"), - origin=dict(type="str"), - originator_id=dict(type="str"), - src=dict(type="str"), - tag=dict(type="str"), - weight=dict(type="int"), - ), - ) - match_spec = dict( - type="dict", - options=dict( - prefix_list=dict(type="str"), - prefix_list6=dict(type="str"), - interface=dict(type="str"), - metric=dict(type="int"), - origin=dict(type="str"), - ip=dict( - type="dict", - options=dict( - nexthop_address=dict(type="str"), - nexthop_prefix_list=dict(type="str"), - ), - ), - ipv6=dict(type="dict", options=dict(nexthop_address=dict(type="str"))), - ), - ) - - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - route_map=dict(type="str"), - entries=dict( - type="list", - elements="dict", - options=dict( - sequence=dict(type="int"), - action=dict(type="str", choices=["permit", "deny"]), - description=dict(type="str"), - call=dict(type="str"), - continue_sequence=dict(type="int"), - match=match_spec, - set=set_spec, - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - client = VyOSRestClient(module) + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + + vyos = VyOSModule(module) + state = module.params["state"] config = module.params.get("config") or [] - commands = [] - changed = False - before = _get(client) + + have = get_running_config(vyos) if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted" and not config: - try: - client.configure_delete(["policy", "route-map"]) - commands.append("delete policy route-map") - changed = True - except VyOSRestError: - pass - elif state == "deleted" and config: - for rm in config: - try: - client.configure_delete(["policy", "route-map", rm["route_map"]]) - commands.append("delete policy route-map {n}".format(n=rm["route_map"])) - changed = True - except VyOSRestError: - pass - elif state in ("merged", "replaced", "overridden"): - if state in ("replaced", "overridden"): - for rm in config: - try: - client.configure_delete(["policy", "route-map", rm["route_map"]]) - except VyOSRestError: - pass - for rm in config: - _apply(client, rm, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) + module.exit_json(changed=False, before=have, after=have, commands=[]) if __name__ == "__main__": diff --git a/plugins/modules/vyos_show.py b/plugins/modules/vyos_show.py deleted file mode 100644 index fbe6c1b..0000000 --- a/plugins/modules/vyos_show.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_show -short_description: Execute op-mode show commands on a VyOS device via REST API. -description: - - Sends a C(show) operational-mode command to a VyOS device via the - HTTPS REST API (C(/show) endpoint) and returns the text output. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - path: - description: - - Operational-mode command tokens following C(show). - - E.g. C(["interfaces"]) maps to C(show interfaces). - type: list - elements: str - required: true - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -notes: - - This module never modifies device state. -requirements: - - VyOS 1.3+ -seealso: - - module: vyos.rest.vyos_configure - - module: vyos.rest.vyos_retrieve -examples: | - - name: Show interfaces - vyos.rest.vyos_show: - hostname: 192.168.1.1 - api_key: MY-KEY - path: ["interfaces"] - register: iface_output - - - name: Show version - vyos.rest.vyos_show: - hostname: 192.168.1.1 - api_key: MY-KEY - path: ["version"] - register: ver_output - - - name: Show installed images - vyos.rest.vyos_show: - hostname: 192.168.1.1 - api_key: MY-KEY - path: ["system", "image"] - register: images -""" - -RETURN = r""" -output: - description: Raw text output from the show command. - returned: success - type: str - sample: | - Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down - Interface IP Address S/L Description - eth0 10.0.0.1/24 u/u WAN -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def main(): - argument_spec = dict( - path=dict(type="list", elements="str", required=True), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - client = VyOSRestClient(module) - try: - result = client.show(module.params["path"]) - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - module.exit_json(changed=False, output=result.get("data", "")) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_snmp_server.py b/plugins/modules/vyos_snmp_server.py index 9bdf451..3b40344 100644 --- a/plugins/modules/vyos_snmp_server.py +++ b/plugins/modules/vyos_snmp_server.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# GNU General Public License v3.0+ +# 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 @@ -41,7 +41,7 @@ options: type: list elements: str authorization_type: - description: Authorization type. + description: Authorization type (ro=read-only, rw=read-write). type: str choices: ['ro', 'rw'] contact: @@ -65,7 +65,7 @@ options: elements: dict suboptions: address: - description: IP address. + description: IP address to listen on. type: str required: true port: @@ -76,10 +76,13 @@ options: type: dict suboptions: address: + description: IP address of the trap target host. type: str community: + description: Community name to use for traps. type: str port: + description: UDP port on the trap target host. type: int snmp_v3: description: SNMPv3 configuration. @@ -89,104 +92,132 @@ options: description: EngineID as a hex string. type: str groups: + description: SNMPv3 group configuration. type: list elements: dict suboptions: group: + description: Group name. type: str required: true mode: + description: Access mode (ro=read-only, rw=read-write). type: str choices: ['ro', 'rw'] seclevel: + description: Minimum security level required for group members. type: str choices: ['auth', 'priv'] view: + description: View name the group has access to. type: str users: + description: SNMPv3 user configuration. type: list elements: dict suboptions: user: + description: Username. type: str required: true authentication: + description: Authentication parameters for this user. type: dict suboptions: type: + description: Authentication algorithm. type: str - choices: ['md5', 'sha'] encrypted_key: - description: Encrypted key (stored as encrypted-password on device). + description: Encrypted authentication key (stored as encrypted-password on device). type: str plaintext_key: - description: Plaintext key (device encrypts it; recommended for 1.5+). + description: Plaintext authentication key (device encrypts it). type: str - no_log: true privacy: + description: Privacy (encryption) parameters for this user. type: dict suboptions: type: + description: Privacy algorithm. type: str - choices: ['des', 'aes'] encrypted_key: + description: Encrypted privacy key. type: str plaintext_key: + description: Plaintext privacy key (device encrypts it). type: str - no_log: true group: + description: Group this user belongs to. type: str mode: + description: Access mode for this user. type: str choices: ['ro', 'rw'] tsm_key: + description: TSM fingerprint of the certificate mapped to this user. type: str trap_targets: + description: SNMPv3 trap target configuration. type: list elements: dict suboptions: address: + description: IP address of the SNMPv3 trap target. type: str port: + description: UDP port on the trap target host. type: int protocol: + description: Transport protocol for traps. type: str choices: ['tcp', 'udp'] type: + description: Trap type. type: str choices: ['inform', 'trap'] authentication: + description: Authentication parameters for trap target. type: dict suboptions: type: + description: Authentication algorithm. type: str encrypted_key: + description: Encrypted authentication key. type: str plaintext_key: + description: Plaintext authentication key. type: str - no_log: true privacy: + description: Privacy parameters for trap target. type: dict suboptions: type: + description: Privacy algorithm. type: str encrypted_key: + description: Encrypted privacy key. type: str plaintext_key: + description: Plaintext privacy key. type: str - no_log: true views: + description: SNMPv3 view configuration. type: list elements: dict suboptions: view: + description: View name. type: str required: true oid: + description: OID subtree included in this view. type: str exclude: + description: OID subtree excluded from this view. type: str mask: + description: OID mask for the view. type: str state: description: @@ -781,7 +812,7 @@ ARGUMENT_SPEC = dict( privacy=dict(type="dict", options=_auth_privacy_spec()), group=dict(type="str"), mode=dict(type="str", choices=["ro", "rw"]), - tsm_key=dict(type="str"), + tsm_key=dict(type="str", no_log=True), ), ), trap_targets=dict( diff --git a/plugins/modules/vyos_static_routes.py b/plugins/modules/vyos_static_routes.py deleted file mode 100644 index f203603..0000000 --- a/plugins/modules/vyos_static_routes.py +++ /dev/null @@ -1,397 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - -DOCUMENTATION = r""" ---- -module: vyos_static_routes -short_description: Manage static routes on VyOS via the REST API. -description: - - Manages IPv4 and IPv6 static routes on VyOS devices using the HTTPS REST API. - - Mirrors C(vyos.vyos.vyos_static_routes) but uses the HTTP API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - config: - description: List of static route address-family configurations. - type: list - elements: dict - suboptions: - address_families: - description: List of address family route groups. - type: list - elements: dict - suboptions: - afi: - description: Address family indicator. - type: str - choices: [ipv4, ipv6] - required: true - routes: - description: List of static route entries. - type: list - elements: dict - suboptions: - dest: - description: Destination prefix in CIDR notation. - type: str - required: true - blackhole_config: - description: Blackhole route configuration. - type: dict - suboptions: - distance: - description: Administrative distance (1-255). - type: int - type: - description: Blackhole type. - type: str - next_hops: - description: List of next-hop addresses. - type: list - elements: dict - suboptions: - forward_router_address: - description: Next-hop IP address. - type: str - required: true - admin_distance: - description: Administrative distance for this next-hop. - type: int - enabled: - description: Whether this next-hop is enabled. - type: bool - default: true - interface: - description: Outgoing interface name. - type: str - state: - description: - - C(merged): Add routes (preserve existing). - - C(replaced): Replace routes for listed destinations. - - C(overridden): Replace the entire static route table. - - C(deleted): Remove listed (or all) static routes. - - C(gathered): Read static routes from device. - type: str - choices: [merged, replaced, overridden, deleted, gathered] - default: merged - hostname: - description: IP address or FQDN of the VyOS device. - type: str - required: true - port: - description: HTTPS port for the REST API. - type: int - default: 443 - api_key: - description: API key configured on the device. - type: str - required: true - no_log: true - timeout: - description: Request timeout in seconds. - type: int - default: 30 - verify_ssl: - description: Validate the device's TLS certificate. - type: bool - default: false -requirements: - - VyOS 1.3+ -seealso: - - module: vyos.vyos.vyos_static_routes -examples: | - - name: Add IPv4 static routes - vyos.rest.vyos_static_routes: - hostname: 192.168.1.1 - api_key: MY-KEY - config: - - address_families: - - afi: ipv4 - routes: - - dest: 192.0.2.0/24 - next_hops: - - forward_router_address: 10.0.0.1 - - dest: 203.0.113.0/24 - blackhole_config: - distance: 200 - state: merged - - - name: Delete all static routes - vyos.rest.vyos_static_routes: - hostname: 192.168.1.1 - api_key: MY-KEY - state: deleted -""" - -RETURN = r""" -before: - description: Static route config before the module ran. - returned: always - type: list -after: - description: Static route config after the module ran. - returned: when changed - type: list -gathered: - description: Static routes read from device (state=gathered). - returned: when state is gathered - type: list -commands: - description: set/delete commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_ROUTE_BASE = { - "ipv4": ["protocols", "static", "route"], - "ipv6": ["protocols", "static", "route6"], -} - - -def _get_static_routes(client): - try: - result = client.retrieve_show_config(["protocols", "static"]) - data = result.get("data") or {} - out = [] - for afi, route_key in [("ipv4", "route"), ("ipv6", "route6")]: - routes_data = data.get(route_key, {}) - if not isinstance(routes_data, dict): - continue - routes = [] - for dest, rdata in routes_data.items(): - route_entry = {"dest": dest} - if isinstance(rdata, dict): - if "blackhole" in rdata: - bh = rdata["blackhole"] - bc = {} - if isinstance(bh, dict) and "distance" in bh: - bc["distance"] = int(bh["distance"]) - route_entry["blackhole_config"] = bc - next_hops = [] - for nh_key in ("next-hop", "next_hop"): - nh_data = rdata.get(nh_key, {}) - if isinstance(nh_data, dict): - for nh_addr, nh_opts in nh_data.items(): - nh = {"forward_router_address": nh_addr} - if isinstance(nh_opts, dict): - if "distance" in nh_opts: - nh["admin_distance"] = int(nh_opts["distance"]) - nh["enabled"] = "disable" not in nh_opts - next_hops.append(nh) - if next_hops: - route_entry["next_hops"] = next_hops - routes.append(route_entry) - if routes: - out.append({"address_families": [{"afi": afi, "routes": routes}]}) - return out - except VyOSRestError: - return [] - - -def _apply_route(client, afi, route, commands): - base = _ROUTE_BASE[afi] - dest = route["dest"] - - if route.get("blackhole_config") is not None: - bh_path = base + [dest, "blackhole"] - client.configure_set(bh_path) - commands.append("set {p}".format(p=" ".join(bh_path))) - dist = route["blackhole_config"].get("distance") - if dist: - client.configure_set(bh_path + ["distance"], str(dist)) - commands.append( - "set {p} distance {d}".format(p=" ".join(bh_path), d=dist), - ) - - for nh in route.get("next_hops") or []: - nh_addr = nh["forward_router_address"] - nh_path = base + [dest, "next-hop", nh_addr] - client.configure_set(nh_path) - commands.append("set {p}".format(p=" ".join(nh_path))) - if nh.get("admin_distance"): - client.configure_set(nh_path + ["distance"], str(nh["admin_distance"])) - commands.append( - "set {p} distance {d}".format( - p=" ".join(nh_path), - d=nh["admin_distance"], - ), - ) - if "enabled" in nh and not nh["enabled"]: - client.configure_set(nh_path + ["disable"]) - commands.append("set {p} disable".format(p=" ".join(nh_path))) - if nh.get("interface"): - client.configure_set(nh_path + ["interface"], nh["interface"]) - commands.append( - "set {p} interface {i}".format( - p=" ".join(nh_path), - i=nh["interface"], - ), - ) - - -def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - address_families=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]), - routes=dict( - type="list", - elements="dict", - options=dict( - dest=dict(type="str", required=True), - blackhole_config=dict( - type="dict", - options=dict( - distance=dict(type="int"), - type=dict(type="str"), - ), - ), - next_hops=dict( - type="list", - elements="dict", - options=dict( - forward_router_address=dict( - type="str", - required=True, - ), - admin_distance=dict(type="int"), - enabled=dict(type="bool", default=True), - interface=dict(type="str"), - ), - ), - ), - ), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - ) - - client = VyOSRestClient(module) - state = module.params["state"] - config = module.params.get("config") or [] - commands = [] - changed = False - - before = _get_static_routes(client) - - if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) - - if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state in ("overridden", "deleted") and not config: - # Delete all static routes - for afi_key in ("route", "route6"): - try: - client.configure_delete(["protocols", "static", afi_key]) - commands.append( - "delete protocols static {k}".format(k=afi_key), - ) - except VyOSRestError: - pass - changed = True - - elif state == "deleted" and config: - for entry in config: - for af in entry.get("address_families") or []: - afi = af["afi"] - base = _ROUTE_BASE[afi] - for route in af.get("routes") or []: - try: - client.configure_delete(base + [route["dest"]]) - commands.append( - "delete {p} {d}".format( - p=" ".join(base), - d=route["dest"], - ), - ) - except VyOSRestError: - pass - changed = True - - elif state in ("merged", "replaced", "overridden"): - if state in ("replaced", "overridden"): - # Remove existing routes for affected destinations - dests_by_afi = {} - for entry in config: - for af in entry.get("address_families") or []: - afi = af["afi"] - dests_by_afi.setdefault(afi, set()) - for route in af.get("routes") or []: - dests_by_afi[afi].add(route["dest"]) - if state == "overridden": - for afi_key in ("route", "route6"): - try: - client.configure_delete( - ["protocols", "static", afi_key], - ) - commands.append( - "delete protocols static {k}".format(k=afi_key), - ) - except VyOSRestError: - pass - else: - for afi, dests in dests_by_afi.items(): - base = _ROUTE_BASE[afi] - for dest in dests: - try: - client.configure_delete(base + [dest]) - commands.append( - "delete {p} {d}".format( - p=" ".join(base), - d=dest, - ), - ) - except VyOSRestError: - pass - - for entry in config: - for af in entry.get("address_families") or []: - afi = af["afi"] - for route in af.get("routes") or []: - _apply_route(client, afi, route, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - after = _get_static_routes(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_system.py b/plugins/modules/vyos_system.py deleted file mode 100644 index d1d696e..0000000 --- a/plugins/modules/vyos_system.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_system -short_description: Manage system settings on VyOS via the REST API. -description: - - Manages system-level settings — hostname, domain name, name servers, - domain search list — using the VyOS HTTPS REST API. - - Mirrors C(vyos.vyos.vyos_system) but uses the HTTP API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - host_name: - description: System hostname. - type: str - domain_name: - description: System domain name. - type: str - name_server: - description: List of DNS name servers. Mutually exclusive with domain_search. - type: list - elements: str - aliases: [name_servers] - domain_search: - description: List of domain search suffixes. Mutually exclusive with name_server. - type: list - elements: str - state: - description: C(present) to apply, C(absent) to remove. - type: str - choices: [present, absent] - default: present - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -commands: - description: set/delete commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def main(): - argument_spec = dict( - host_name=dict(type="str"), - domain_name=dict(type="str"), - name_server=dict(type="list", elements="str", aliases=["name_servers"]), - domain_search=dict(type="list", elements="str"), - state=dict(type="str", default="present", choices=["present", "absent"]), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[["name_server", "domain_search"]], - supports_check_mode=True, - ) - - if module.check_mode: - module.exit_json(changed=True, commands=["(check mode)"]) - - client = VyOSRestClient(module) - state = module.params["state"] - commands = [] - changed = False - - try: - if state == "present": - if module.params.get("host_name"): - client.configure_set(["system", "host-name"], module.params["host_name"]) - commands.append("set system host-name '{h}'".format(h=module.params["host_name"])) - changed = True - if module.params.get("domain_name"): - client.configure_set(["system", "domain-name"], module.params["domain_name"]) - commands.append( - "set system domain-name '{d}'".format(d=module.params["domain_name"]), - ) - changed = True - for ns in module.params.get("name_server") or []: - client.configure_set(["system", "name-server"], ns) - commands.append("set system name-server {ns}".format(ns=ns)) - changed = True - for ds in module.params.get("domain_search") or []: - client.configure_set(["system", "domain-search", "domain"], ds) - commands.append("set system domain-search domain {ds}".format(ds=ds)) - changed = True - else: - if module.params.get("host_name"): - try: - client.configure_delete(["system", "host-name"]) - commands.append("delete system host-name") - changed = True - except VyOSRestError: - pass - if module.params.get("domain_name"): - try: - client.configure_delete(["system", "domain-name"]) - commands.append("delete system domain-name") - changed = True - except VyOSRestError: - pass - if module.params.get("name_server"): - try: - client.configure_delete(["system", "name-server"]) - commands.append("delete system name-server") - changed = True - except VyOSRestError: - pass - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - module.exit_json(changed=changed, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py deleted file mode 100644 index 41e014d..0000000 --- a/plugins/modules/vyos_user.py +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_user -short_description: Manage local users on VyOS via the REST API. -description: - - Manages local user accounts on VyOS devices using the HTTPS REST API. - - Mirrors C(vyos.vyos.vyos_user) but uses the HTTP API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - aggregate: - description: List of user definitions. Mutually exclusive with I(name). - type: list - elements: dict - aliases: [users, collection] - suboptions: - name: - description: Username. - type: str - required: true - full_name: - description: Full name. - type: str - configured_password: - description: Plaintext password (will be hashed on device). - type: str - no_log: true - encrypted_password: - description: Pre-hashed password string. - type: str - no_log: true - update_password: - description: When to update the password. - type: str - choices: [always, on_create] - level: - description: User privilege level. - type: str - choices: [admin, operator] - public_keys: - description: SSH public keys for authentication. - type: list - elements: dict - suboptions: - name: - description: Key identifier (e.g. user@host). - type: str - required: true - key: - description: Base64-encoded public key. - type: str - required: true - type: - description: Key type. - type: str - required: true - choices: [ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, - ecdsa-sha2-nistp384, ssh-ed25519, ecdsa-sha2-nistp521] - state: - description: Present or absent. - type: str - choices: [present, absent] - name: - description: Single username. Mutually exclusive with I(aggregate). - type: str - full_name: - description: Full name for the single user. - type: str - configured_password: - description: Plaintext password. - type: str - no_log: true - encrypted_password: - description: Pre-hashed password. - type: str - no_log: true - update_password: - type: str - choices: [always, on_create] - level: - type: str - choices: [admin, operator] - public_keys: - type: list - elements: dict - suboptions: - name: - type: str - required: true - key: - type: str - required: true - type: - type: str - required: true - state: - description: Whether to create (present) or remove (absent) the user. - type: str - choices: [present, absent] - default: present - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -commands: - description: set/delete commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -_KEY_TYPES = [ - "ssh-dss", - "ssh-rsa", - "ecdsa-sha2-nistp256", - "ecdsa-sha2-nistp384", - "ssh-ed25519", - "ecdsa-sha2-nistp521", -] - - -def _apply_user(client, user_def, commands, update_password="always"): - name = user_def["name"] - base = ["system", "login", "user", name] - client.configure_set(base) - commands.append("set system login user {n}".format(n=name)) - - if user_def.get("full_name"): - client.configure_set(base + ["full-name"], user_def["full_name"]) - if user_def.get("level"): - client.configure_set(base + ["level"], user_def["level"]) - if user_def.get("configured_password") and update_password in ("always",): - client.configure_set( - base + ["authentication", "plaintext-password"], - user_def["configured_password"], - ) - commands.append( - "set system login user {n} authentication plaintext-password".format(n=name), - ) - if user_def.get("encrypted_password"): - client.configure_set( - base + ["authentication", "encrypted-password"], - user_def["encrypted_password"], - ) - for pk in user_def.get("public_keys") or []: - kb = base + ["authentication", "public-keys", pk["name"]] - client.configure_set(kb + ["key"], pk["key"]) - client.configure_set(kb + ["type"], pk["type"]) - commands.append( - "set system login user {n} authentication public-keys {k}".format( - n=name, - k=pk["name"], - ), - ) - - -def _delete_user(client, name, commands): - try: - client.configure_delete(["system", "login", "user", name]) - commands.append("delete system login user {n}".format(n=name)) - except VyOSRestError: - pass - - -def main(): - pk_spec = dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - key=dict(type="str", required=True), - type=dict(type="str", required=True, choices=_KEY_TYPES), - ), - ) - user_spec = dict( - name=dict(type="str", required=True), - full_name=dict(type="str"), - configured_password=dict(type="str", no_log=True), - encrypted_password=dict(type="str", no_log=True), - update_password=dict(type="str", choices=["always", "on_create"]), - level=dict(type="str", choices=["admin", "operator"]), - public_keys=pk_spec, - state=dict(type="str", choices=["present", "absent"]), - ) - argument_spec = dict( - aggregate=dict( - type="list", - elements="dict", - aliases=["users", "collection"], - options=user_spec, - ), - name=dict(type="str"), - full_name=dict(type="str"), - configured_password=dict(type="str", no_log=True), - encrypted_password=dict(type="str", no_log=True), - update_password=dict(type="str", choices=["always", "on_create"]), - level=dict(type="str", choices=["admin", "operator"]), - public_keys=pk_spec, - state=dict(type="str", default="present", choices=["present", "absent"]), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[["aggregate", "name"]], - supports_check_mode=True, - ) - if module.check_mode: - module.exit_json(changed=True, commands=["(check mode)"]) - - client = VyOSRestClient(module) - commands = [] - changed = False - - # Build unified user list - users = module.params.get("aggregate") or [] - if module.params.get("name"): - users = [ - { - "name": module.params["name"], - "full_name": module.params.get("full_name"), - "configured_password": module.params.get("configured_password"), - "encrypted_password": module.params.get("encrypted_password"), - "update_password": module.params.get("update_password", "always"), - "level": module.params.get("level"), - "public_keys": module.params.get("public_keys"), - "state": module.params.get("state", "present"), - }, - ] - - try: - for u in users: - u_state = u.get("state") or module.params.get("state", "present") - if u_state == "absent": - _delete_user(client, u["name"], commands) - else: - _apply_user( - client, - u, - commands, - update_password=u.get("update_password") or "always", - ) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - module.exit_json(changed=changed, commands=commands) - - -if __name__ == "__main__": - main() diff --git a/plugins/modules/vyos_vlan.py b/plugins/modules/vyos_vlan.py deleted file mode 100644 index 18eca92..0000000 --- a/plugins/modules/vyos_vlan.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# GNU General Public License v3.0+ - -from __future__ import absolute_import, division, print_function - - -__metaclass__ = type - -DOCUMENTATION = r""" ---- -module: vyos_vlan -short_description: Manage VLANs on VyOS network devices via REST API. -description: - - Manages 802.1Q VLAN sub-interfaces on VyOS Ethernet interfaces using - the HTTPS REST API. - - Mirrors C(vyos.vyos.vyos_vlan) but uses the HTTP API. -version_added: "1.0.0" -author: - - VyOS Community (@vyos) -options: - vlan_id: - description: VLAN ID (0-4094). - type: int - name: - description: VLAN name (used as description on the VIF). - type: str - address: - description: IP address for the VLAN interface. - type: str - interfaces: - description: List of physical interfaces to configure the VLAN on. - type: list - elements: str - aggregate: - description: List of VLAN definitions. - type: list - elements: dict - suboptions: - vlan_id: - type: int - required: true - name: - type: str - address: - type: str - interfaces: - type: list - elements: str - required: true - state: - type: str - choices: [present, absent] - state: - description: C(present) to configure, C(absent) to remove. - type: str - choices: [present, absent] - default: present - purge: - description: Remove VLANs not defined in aggregate. - type: bool - default: false - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false -""" - -RETURN = r""" -commands: - description: set/delete commands issued. - returned: always - type: list -""" - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) - - -def _apply_vlan(client, vlan, commands): - vid = str(vlan["vlan_id"]) - for iface in vlan.get("interfaces") or []: - # Detect interface type - if iface.startswith("bond"): - itype = "bonding" - else: - itype = "ethernet" - vif_base = ["interfaces", itype, iface, "vif", vid] - client.configure_set(vif_base) - commands.append("set interfaces {t} {i} vif {v}".format(t=itype, i=iface, v=vid)) - if vlan.get("name"): - client.configure_set(vif_base + ["description"], vlan["name"]) - commands.append( - "set interfaces {t} {i} vif {v} description '{n}'".format( - t=itype, - i=iface, - v=vid, - n=vlan["name"], - ), - ) - if vlan.get("address"): - client.configure_set(vif_base + ["address"], vlan["address"]) - commands.append( - "set interfaces {t} {i} vif {v} address {a}".format( - t=itype, - i=iface, - v=vid, - a=vlan["address"], - ), - ) - - -def _delete_vlan(client, vlan, commands): - vid = str(vlan["vlan_id"]) - for iface in vlan.get("interfaces") or []: - itype = "bonding" if iface.startswith("bond") else "ethernet" - try: - client.configure_delete(["interfaces", itype, iface, "vif", vid]) - commands.append("delete interfaces {t} {i} vif {v}".format(t=itype, i=iface, v=vid)) - except VyOSRestError: - pass - - -def main(): - vlan_spec = dict( - vlan_id=dict(type="int", required=True), - name=dict(type="str"), - address=dict(type="str"), - interfaces=dict(type="list", elements="str"), - state=dict(type="str", choices=["present", "absent"]), - ) - argument_spec = dict( - vlan_id=dict(type="int"), - name=dict(type="str"), - address=dict(type="str"), - interfaces=dict(type="list", elements="str"), - aggregate=dict(type="list", elements="dict", options=vlan_spec), - state=dict(type="str", default="present", choices=["present", "absent"]), - purge=dict(type="bool", default=False), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[["aggregate", "vlan_id"]], - supports_check_mode=True, - ) - if module.check_mode: - module.exit_json(changed=True, commands=["(check mode)"]) - - client = VyOSRestClient(module) - commands = [] - changed = False - - vlans = module.params.get("aggregate") or [] - if module.params.get("vlan_id"): - vlans = [ - { - "vlan_id": module.params["vlan_id"], - "name": module.params.get("name"), - "address": module.params.get("address"), - "interfaces": module.params.get("interfaces") or [], - "state": module.params.get("state", "present"), - }, - ] - - try: - for vlan in vlans: - v_state = vlan.get("state") or module.params.get("state", "present") - if v_state == "absent": - _delete_vlan(client, vlan, commands) - else: - _apply_vlan(client, vlan, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) - - module.exit_json(changed=changed, commands=commands) - - -if __name__ == "__main__": - main() |
