summaryrefslogtreecommitdiff
path: root/plugins/modules
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/modules')
-rw-r--r--plugins/modules/vyos_bgp_address_family.py631
-rw-r--r--plugins/modules/vyos_bgp_global.py572
-rw-r--r--plugins/modules/vyos_facts.py287
-rw-r--r--plugins/modules/vyos_firewall_global.py402
-rw-r--r--plugins/modules/vyos_firewall_interfaces.py473
-rw-r--r--plugins/modules/vyos_firewall_rules.py546
-rw-r--r--plugins/modules/vyos_user.py302
7 files changed, 3213 insertions, 0 deletions
diff --git a/plugins/modules/vyos_bgp_address_family.py b/plugins/modules/vyos_bgp_address_family.py
new file mode 100644
index 0000000..65f5810
--- /dev/null
+++ b/plugins/modules/vyos_bgp_address_family.py
@@ -0,0 +1,631 @@
+#!/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: Manage BGP address-family configuration on VyOS devices using REST API
+description:
+ - Manages BGP address-family configuration on VyOS devices via the REST API.
+ - Covers global address-family (networks, redistribution) and
+ per-neighbor address-family settings.
+ - BGP must be configured first using M(vyos.rest.vyos_bgp_global).
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: BGP address-family configuration.
+ type: dict
+ suboptions:
+ as_number:
+ description: BGP autonomous system number (required for context).
+ type: int
+ required: true
+ address_family:
+ description: Global BGP address-family settings.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family identifier.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ networks:
+ description: Networks to advertise.
+ type: list
+ elements: dict
+ suboptions:
+ prefix:
+ description: Network prefix.
+ type: str
+ required: true
+ route_map:
+ description: Route map to apply.
+ type: str
+ backdoor:
+ description: Network backdoor.
+ type: bool
+ redistribute:
+ description: Redistribute routes from other protocols.
+ 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
+ neighbors:
+ description: Per-neighbor address-family settings.
+ type: list
+ elements: dict
+ suboptions:
+ neighbor_address:
+ description: Neighbor IP address.
+ type: str
+ required: true
+ address_family:
+ description: Address-family settings for this neighbor.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family identifier.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ allowas_in:
+ description: Accept as-path with my AS present.
+ type: int
+ attribute_unchanged:
+ description: BGP attributes to leave unchanged.
+ type: dict
+ suboptions:
+ as_path:
+ description: Leave as-path unchanged.
+ type: bool
+ med:
+ description: Leave MED unchanged.
+ type: bool
+ next_hop:
+ description: Leave next-hop unchanged.
+ type: bool
+ capability:
+ description: Advertise capability to the peer.
+ type: dict
+ suboptions:
+ orf:
+ description: ORF capability.
+ type: str
+ choices: [receive, send]
+ default_originate:
+ description: Send default route to neighbor.
+ type: bool
+ distribute_list:
+ description: Filter updates using access-list.
+ type: dict
+ suboptions:
+ import:
+ description: Access-list to filter inbound updates.
+ type: int
+ export:
+ description: Access-list to filter outbound updates.
+ type: int
+ maximum_prefix:
+ description: Maximum number of prefixes to accept.
+ type: int
+ nexthop_self:
+ description: Set next-hop to self.
+ type: bool
+ prefix_list:
+ description: Filter updates using prefix-list.
+ type: dict
+ suboptions:
+ import:
+ description: Prefix-list to filter inbound updates.
+ type: str
+ export:
+ description: Prefix-list to filter outbound updates.
+ type: str
+ route_map:
+ description: Route map to apply.
+ type: dict
+ suboptions:
+ import:
+ description: Route map for inbound updates.
+ type: str
+ export:
+ description: Route map for outbound updates.
+ type: str
+ route_reflector_client:
+ description: Configure as route reflector client.
+ type: bool
+ route_server_client:
+ description: Configure as route server client.
+ type: bool
+ soft_reconfiguration:
+ description: Enable soft reconfiguration inbound.
+ type: bool
+ unsuppress_map:
+ description: Route-map to selectively unsuppress suppressed routes.
+ type: str
+ weight:
+ description: Default weight for routes from this neighbor.
+ type: int
+ state:
+ description:
+ - Desired state of the BGP address-family configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces the entire BGP address-family configuration.
+ - C(deleted) removes BGP address-family configuration.
+ - C(gathered) returns current configuration as structured data.
+ type: str
+ choices: [merged, replaced, deleted, gathered]
+ default: merged
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+ - BGP must be configured first using M(vyos.rest.vyos_bgp_global).
+"""
+
+EXAMPLES = r"""
+- name: Merge BGP address-family configuration
+ vyos.rest.vyos_bgp_address_family:
+ config:
+ as_number: 65000
+ address_family:
+ - afi: ipv4
+ networks:
+ - prefix: 192.0.2.0/24
+ redistribute:
+ - protocol: connected
+ metric: 10
+ neighbors:
+ - neighbor_address: 192.0.2.1
+ address_family:
+ - afi: ipv4
+ soft_reconfiguration: true
+ nexthop_self: true
+ - afi: ipv6
+ soft_reconfiguration: true
+ state: merged
+
+- name: Delete all BGP address-family configuration
+ vyos.rest.vyos_bgp_address_family:
+ config:
+ as_number: 65000
+ state: deleted
+
+- name: Gather BGP address-family configuration
+ vyos.rest.vyos_bgp_address_family:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: BGP address-family configuration before this module ran.
+ returned: always
+ type: dict
+after:
+ description: BGP address-family configuration after this module ran.
+ returned: when changed
+ type: dict
+commands:
+ description: List of API command tuples sent to the device.
+ returned: always
+ type: list
+gathered:
+ description: Current BGP address-family configuration as structured data.
+ returned: when state is gathered
+ type: dict
+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 import VyOSModule
+
+
+_BASE = ["protocols", "bgp"]
+_AFI_MAP = {"ipv4": "ipv4-unicast", "ipv6": "ipv6-unicast"}
+_AFI_RMAP = {"ipv4-unicast": "ipv4", "ipv6-unicast": "ipv6"}
+
+
+def _parse_global_af(raw_afs):
+ if not raw_afs or not isinstance(raw_afs, dict):
+ return []
+ result = []
+ for af_key, af_data in sorted(raw_afs.items()):
+ afi = _AFI_RMAP.get(af_key)
+ if not afi:
+ continue
+ af_data = af_data or {}
+ entry = {"afi": afi}
+
+ nets = af_data.get("network", {})
+ if nets and isinstance(nets, dict):
+ entry["networks"] = [{"prefix": p} for p in sorted(nets.keys())]
+
+ redist = af_data.get("redistribute", {})
+ if redist and isinstance(redist, dict):
+ redist_list = []
+ for proto, rdata in sorted(redist.items()):
+ r = {"protocol": proto}
+ rdata = rdata or {}
+ if "metric" in rdata:
+ r["metric"] = int(rdata["metric"])
+ if "route-map" in rdata:
+ r["route_map"] = rdata["route-map"]
+ redist_list.append(r)
+ if redist_list:
+ entry["redistribute"] = redist_list
+
+ result.append(entry)
+ return result
+
+
+def _parse_neighbor_af(raw_afs):
+ if not raw_afs or not isinstance(raw_afs, dict):
+ return []
+ result = []
+ for af_key, af_data in sorted(raw_afs.items()):
+ afi = _AFI_RMAP.get(af_key)
+ if not afi:
+ continue
+ af_data = af_data or {}
+ entry = {"afi": afi}
+
+ if "nexthop-self" in af_data:
+ entry["nexthop_self"] = True
+ if "route-reflector-client" in af_data:
+ entry["route_reflector_client"] = True
+ if "route-server-client" in af_data:
+ entry["route_server_client"] = True
+ if "default-originate" in af_data:
+ entry["default_originate"] = True
+ if "maximum-prefix" in af_data:
+ entry["maximum_prefix"] = int(af_data["maximum-prefix"])
+ if "weight" in af_data:
+ entry["weight"] = int(af_data["weight"])
+ if "unsuppress-map" in af_data:
+ entry["unsuppress_map"] = af_data["unsuppress-map"]
+ if "allowas-in" in af_data:
+ ai = af_data["allowas-in"]
+ if isinstance(ai, dict) and "number" in ai:
+ entry["allowas_in"] = int(ai["number"])
+ else:
+ entry["allowas_in"] = 1
+
+ sc = af_data.get("soft-reconfiguration", {})
+ if sc and "inbound" in sc:
+ entry["soft_reconfiguration"] = True
+
+ rm = af_data.get("route-map", {})
+ if rm:
+ entry["route_map"] = {}
+ if "import" in rm:
+ entry["route_map"]["import"] = rm["import"]
+ if "export" in rm:
+ entry["route_map"]["export"] = rm["export"]
+
+ pl = af_data.get("prefix-list", {})
+ if pl:
+ entry["prefix_list"] = {}
+ if "import" in pl:
+ entry["prefix_list"]["import"] = pl["import"]
+ if "export" in pl:
+ entry["prefix_list"]["export"] = pl["export"]
+
+ result.append(entry)
+ return result
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {}
+
+ if "system-as" in raw:
+ result["as_number"] = int(raw["system-as"])
+
+ global_afs = _parse_global_af(raw.get("address-family"))
+ if global_afs:
+ result["address_family"] = global_afs
+
+ neighbors = []
+ for nb_id, nb_data in sorted((raw.get("neighbor") or {}).items()):
+ nb_data = nb_data or {}
+ nb_afs = _parse_neighbor_af(nb_data.get("address-family"))
+ if nb_afs:
+ neighbors.append({"neighbor_address": nb_id, "address_family": nb_afs})
+ if neighbors:
+ result["neighbors"] = neighbors
+
+ return result
+
+
+def _global_af_cmds(af, have_af):
+ cmds = []
+ afi = af["afi"]
+ af_key = _AFI_MAP[afi]
+ abase = _BASE + ["address-family", af_key]
+ have_af = have_af or {}
+
+ want_nets = {n["prefix"]: n for n in (af.get("networks") or [])}
+ have_nets = {n["prefix"]: n for n in (have_af.get("networks") or [])}
+ for prefix in want_nets:
+ if prefix not in have_nets:
+ cmds.append(("set", abase + ["network", prefix]))
+
+ want_redist = {r["protocol"]: r for r in (af.get("redistribute") or [])}
+ have_redist = {r["protocol"]: r for r in (have_af.get("redistribute") or [])}
+ for proto, entry in want_redist.items():
+ have_entry = have_redist.get(proto, {})
+ rbase = abase + ["redistribute", proto]
+ if proto not in have_redist:
+ cmds.append(("set", rbase))
+ if entry.get("metric") and entry["metric"] != have_entry.get("metric"):
+ cmds.append(("set", rbase + ["metric", str(entry["metric"])]))
+ if entry.get("route_map") and entry["route_map"] != have_entry.get("route_map"):
+ cmds.append(("set", rbase + ["route-map", entry["route_map"]]))
+
+ return cmds
+
+
+def _neighbor_af_cmds(nb_addr, af, have_af):
+ cmds = []
+ afi = af["afi"]
+ af_key = _AFI_MAP[afi]
+ nbase = _BASE + ["neighbor", nb_addr, "address-family", af_key]
+ have_af = have_af or {}
+
+ if af.get("soft_reconfiguration") and not have_af.get("soft_reconfiguration"):
+ cmds.append(("set", nbase + ["soft-reconfiguration", "inbound"]))
+ if af.get("nexthop_self") and not have_af.get("nexthop_self"):
+ cmds.append(("set", nbase + ["nexthop-self"]))
+ if af.get("route_reflector_client") and not have_af.get("route_reflector_client"):
+ cmds.append(("set", nbase + ["route-reflector-client"]))
+ if af.get("route_server_client") and not have_af.get("route_server_client"):
+ cmds.append(("set", nbase + ["route-server-client"]))
+ if af.get("default_originate") and not have_af.get("default_originate"):
+ cmds.append(("set", nbase + ["default-originate"]))
+ if af.get("maximum_prefix") and af["maximum_prefix"] != have_af.get("maximum_prefix"):
+ cmds.append(("set", nbase + ["maximum-prefix", str(af["maximum_prefix"])]))
+ if af.get("weight") and af["weight"] != have_af.get("weight"):
+ cmds.append(("set", nbase + ["weight", str(af["weight"])]))
+ if af.get("allowas_in") and af["allowas_in"] != have_af.get("allowas_in"):
+ cmds.append(("set", nbase + ["allowas-in", "number", str(af["allowas_in"])]))
+ if af.get("unsuppress_map") and af["unsuppress_map"] != have_af.get("unsuppress_map"):
+ cmds.append(("set", nbase + ["unsuppress-map", af["unsuppress_map"]]))
+
+ want_rm = af.get("route_map") or {}
+ have_rm = have_af.get("route_map") or {}
+ if want_rm.get("import") and want_rm["import"] != have_rm.get("import"):
+ cmds.append(("set", nbase + ["route-map", "import", want_rm["import"]]))
+ if want_rm.get("export") and want_rm["export"] != have_rm.get("export"):
+ cmds.append(("set", nbase + ["route-map", "export", want_rm["export"]]))
+
+ want_pl = af.get("prefix_list") or {}
+ have_pl = have_af.get("prefix_list") or {}
+ if want_pl.get("import") and want_pl["import"] != have_pl.get("import"):
+ cmds.append(("set", nbase + ["prefix-list", "import", want_pl["import"]]))
+ if want_pl.get("export") and want_pl["export"] != have_pl.get("export"):
+ cmds.append(("set", nbase + ["prefix-list", "export", want_pl["export"]]))
+
+ return cmds
+
+
+def build_commands(config, have, state):
+ cmds = []
+ config = config or {}
+
+ if state == "deleted":
+ if have.get("address_family"):
+ cmds.append(("delete", _BASE + ["address-family"]))
+ for nb in have.get("neighbors") or []:
+ path = _BASE + ["neighbor", nb["neighbor_address"], "address-family"]
+ cmds.append(("delete", path))
+ return cmds
+
+ if state == "replaced":
+ would_set = build_commands(config, {}, "merged")
+ have_set = build_commands(have, {}, "merged")
+ if would_set == have_set:
+ return []
+ if have.get("address_family"):
+ cmds.append(("delete", _BASE + ["address-family"]))
+ for nb in have.get("neighbors") or []:
+ path = _BASE + ["neighbor", nb["neighbor_address"], "address-family"]
+ cmds.append(("delete", path))
+ have = {}
+
+ # global address-family
+ have_global_af_map = {af["afi"]: af for af in (have.get("address_family") or [])}
+ for af in config.get("address_family") or []:
+ cmds += _global_af_cmds(af, have_global_af_map.get(af["afi"]))
+
+ # per-neighbor address-family
+ have_nb_map = {
+ n["neighbor_address"]: {af["afi"]: af for af in n.get("address_family", [])}
+ for n in (have.get("neighbors") or [])
+ }
+
+ for nb in config.get("neighbors") or []:
+ nb_addr = nb["neighbor_address"]
+ have_nb_afs = have_nb_map.get(nb_addr, {})
+ for af in nb.get("address_family") or []:
+ cmds += _neighbor_af_cmds(nb_addr, af, have_nb_afs.get(af["afi"]))
+
+ return cmds
+
+
+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"], required=True),
+ networks=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ prefix=dict(type="str", required=True),
+ route_map=dict(type="str"),
+ backdoor=dict(type="bool"),
+ ),
+ ),
+ 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"),
+ ),
+ ),
+ ),
+ ),
+ neighbors=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ neighbor_address=dict(type="str", required=True),
+ address_family=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", choices=["ipv4", "ipv6"], required=True),
+ allowas_in=dict(type="int"),
+ default_originate=dict(type="bool"),
+ maximum_prefix=dict(type="int"),
+ nexthop_self=dict(type="bool"),
+ route_reflector_client=dict(type="bool"),
+ route_server_client=dict(type="bool"),
+ soft_reconfiguration=dict(type="bool"),
+ unsuppress_map=dict(type="str"),
+ weight=dict(type="int"),
+ attribute_unchanged=dict(
+ type="dict",
+ options=dict(
+ as_path=dict(type="bool"),
+ med=dict(type="bool"),
+ next_hop=dict(type="bool"),
+ ),
+ ),
+ capability=dict(
+ type="dict",
+ options=dict(
+ orf=dict(type="str", choices=["receive", "send"]),
+ ),
+ ),
+ distribute_list=dict(
+ type="dict",
+ options=dict(
+ **{
+ "import": dict(type="int"),
+ "export": dict(type="int"),
+ },
+ ),
+ ),
+ prefix_list=dict(
+ type="dict",
+ options=dict(
+ **{
+ "import": dict(type="str"),
+ "export": dict(type="str"),
+ },
+ ),
+ ),
+ route_map=dict(
+ type="dict",
+ options=dict(
+ **{
+ "import": dict(type="str"),
+ "export": dict(type="str"),
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ default="merged",
+ choices=["merged", "replaced", "deleted", "gathered"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or {}
+
+ have = get_running_config(vyos)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+
+ commands = build_commands(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()
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=get_running_config(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_bgp_global.py b/plugins/modules/vyos_bgp_global.py
new file mode 100644
index 0000000..a05e3b0
--- /dev/null
+++ b/plugins/modules/vyos_bgp_global.py
@@ -0,0 +1,572 @@
+#!/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_global
+short_description: Manage BGP global configuration on VyOS devices using REST API
+description:
+ - Manages BGP global configuration on VyOS devices via the REST API.
+ - Covers system AS, parameters, neighbors, and peer-groups.
+ - For per-neighbor address-family configuration use M(vyos.rest.vyos_bgp_address_family).
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: BGP global configuration.
+ type: dict
+ suboptions:
+ as_number:
+ description: BGP autonomous system number.
+ type: int
+ required: true
+ parameters:
+ description: BGP global parameters.
+ type: dict
+ suboptions:
+ router_id:
+ description: BGP router ID.
+ type: str
+ confederation:
+ description: AS confederation parameters.
+ type: dict
+ suboptions:
+ identifier:
+ description: Confederation AS identifier.
+ type: int
+ peers:
+ description: Peer ASs in confederation.
+ type: list
+ elements: int
+ bestpath:
+ description: BGP bestpath parameters.
+ type: dict
+ suboptions:
+ as_path:
+ description: AS-path attribute comparison.
+ type: str
+ choices: [confed, ignore, multipath-relax]
+ graceful_restart:
+ description: Enable graceful restart.
+ type: bool
+ log_neighbor_changes:
+ description: Log neighbor up/down changes.
+ type: bool
+ no_ipv4_unicast:
+ description: Disable IPv4 unicast default.
+ type: bool
+ neighbors:
+ description: BGP neighbors.
+ type: list
+ elements: dict
+ suboptions:
+ neighbor_address:
+ description: Neighbor IP address.
+ type: str
+ required: true
+ remote_as:
+ description: Neighbor AS number.
+ type: int
+ description:
+ description: Neighbor description.
+ type: str
+ disable_connected_check:
+ description: Disable connected route check.
+ type: bool
+ ebgp_multihop:
+ description: EBGP multihop TTL.
+ type: int
+ local_as:
+ description: Local AS number.
+ type: int
+ password:
+ description: MD5 password for neighbor.
+ type: str
+ peer_group:
+ description: Peer group name.
+ type: str
+ shutdown:
+ description: Shutdown neighbor.
+ type: bool
+ timers:
+ description: Neighbor timers.
+ type: dict
+ suboptions:
+ holdtime:
+ description: Hold time in seconds.
+ type: int
+ keepalive:
+ description: Keepalive interval in seconds.
+ type: int
+ update_source:
+ description: Source interface/IP for updates.
+ type: str
+ peer_groups:
+ description: BGP peer groups.
+ type: list
+ elements: dict
+ suboptions:
+ peer_group:
+ description: Peer group name.
+ type: str
+ required: true
+ remote_as:
+ description: Peer group AS number.
+ type: int
+ description:
+ description: Peer group description.
+ type: str
+ ebgp_multihop:
+ description: EBGP multihop TTL.
+ type: int
+ password:
+ description: MD5 password.
+ type: str
+ timers:
+ description: Peer group timers.
+ type: dict
+ suboptions:
+ holdtime:
+ description: Hold time in seconds.
+ type: int
+ keepalive:
+ description: Keepalive interval in seconds.
+ type: int
+ update_source:
+ description: Source interface/IP for updates.
+ type: str
+ state:
+ description:
+ - Desired state of the BGP global configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces the entire BGP configuration.
+ - C(deleted) removes BGP configuration.
+ - C(gathered) returns current configuration as structured data.
+ type: str
+ choices: [merged, replaced, deleted, gathered]
+ default: merged
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+ - BGP system-as must be defined before any other BGP configuration.
+"""
+
+EXAMPLES = r"""
+- name: Merge BGP global configuration
+ vyos.rest.vyos_bgp_global:
+ config:
+ as_number: 65000
+ parameters:
+ router_id: 192.0.1.1
+ neighbors:
+ - neighbor_address: 192.0.2.1
+ remote_as: 65001
+ description: peer1
+ timers:
+ holdtime: 30
+ keepalive: 10
+ peer_groups:
+ - peer_group: PG1
+ remote_as: 65002
+ state: merged
+
+- name: Delete BGP configuration
+ vyos.rest.vyos_bgp_global:
+ state: deleted
+
+- name: Gather BGP global configuration
+ vyos.rest.vyos_bgp_global:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: BGP configuration before this module ran.
+ returned: always
+ type: dict
+after:
+ description: BGP configuration after this module ran.
+ returned: when changed
+ type: dict
+commands:
+ description: List of API command tuples sent to the device.
+ returned: always
+ type: list
+gathered:
+ description: Current BGP configuration as structured data.
+ returned: when state is gathered
+ type: dict
+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 import VyOSModule
+
+
+_BASE = ["protocols", "bgp"]
+
+
+def _parse_parameters(raw):
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {}
+ if "router-id" in raw:
+ result["router_id"] = raw["router-id"]
+ if "log-neighbor-changes" in raw:
+ result["log_neighbor_changes"] = True
+ if "no-ipv4-unicast" in raw:
+ result["no_ipv4_unicast"] = True
+ if "graceful-restart" in raw:
+ result["graceful_restart"] = True
+ bp = raw.get("bestpath", {}) or {}
+ if bp:
+ bestpath = {}
+ if "as-path" in bp:
+ bestpath["as_path"] = bp["as-path"]
+ if bestpath:
+ result["bestpath"] = bestpath
+ conf = raw.get("confederation", {}) or {}
+ if conf:
+ confederation = {}
+ if "identifier" in conf:
+ confederation["identifier"] = int(conf["identifier"])
+ if "peers" in conf:
+ peers = conf["peers"]
+ if isinstance(peers, list):
+ confederation["peers"] = [int(p) for p in peers]
+ else:
+ confederation["peers"] = [int(peers)]
+ if confederation:
+ result["confederation"] = confederation
+ return result
+
+
+def _parse_neighbor(nb_id, data):
+ nb = {"neighbor_address": nb_id}
+ data = data or {}
+ if "remote-as" in data:
+ nb["remote_as"] = int(data["remote-as"])
+ if "description" in data:
+ nb["description"] = data["description"]
+ if "ebgp-multihop" in data:
+ nb["ebgp_multihop"] = int(data["ebgp-multihop"])
+ if "local-as" in data:
+ nb["local_as"] = int(data["local-as"])
+ if "password" in data:
+ nb["password"] = data["password"]
+ if "peer-group" in data:
+ nb["peer_group"] = data["peer-group"]
+ if "shutdown" in data:
+ nb["shutdown"] = True
+ if "update-source" in data:
+ nb["update_source"] = data["update-source"]
+ if "disable-connected-check" in data:
+ nb["disable_connected_check"] = True
+ timers = data.get("timers", {}) or {}
+ if timers:
+ t = {}
+ if "holdtime" in timers:
+ t["holdtime"] = int(timers["holdtime"])
+ if "keepalive" in timers:
+ t["keepalive"] = int(timers["keepalive"])
+ if t:
+ nb["timers"] = t
+ return nb
+
+
+def _parse_peer_group(pg_name, data):
+ pg = {"peer_group": pg_name}
+ data = data or {}
+ if "remote-as" in data:
+ pg["remote_as"] = int(data["remote-as"])
+ if "description" in data:
+ pg["description"] = data["description"]
+ if "ebgp-multihop" in data:
+ pg["ebgp_multihop"] = int(data["ebgp-multihop"])
+ if "password" in data:
+ pg["password"] = data["password"]
+ if "update-source" in data:
+ pg["update_source"] = data["update-source"]
+ timers = data.get("timers", {}) or {}
+ if timers:
+ t = {}
+ if "holdtime" in timers:
+ t["holdtime"] = int(timers["holdtime"])
+ if "keepalive" in timers:
+ t["keepalive"] = int(timers["keepalive"])
+ if t:
+ pg["timers"] = t
+ return pg
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {}
+
+ if "system-as" in raw:
+ result["as_number"] = int(raw["system-as"])
+
+ params = _parse_parameters(raw.get("parameters"))
+ if params:
+ result["parameters"] = params
+
+ neighbors = []
+ for nb_id, data in sorted((raw.get("neighbor") or {}).items()):
+ neighbors.append(_parse_neighbor(nb_id, data))
+ if neighbors:
+ result["neighbors"] = neighbors
+
+ peer_groups = []
+ for pg_name, data in sorted((raw.get("peer-group") or {}).items()):
+ peer_groups.append(_parse_peer_group(pg_name, data))
+ if peer_groups:
+ result["peer_groups"] = peer_groups
+
+ return result
+
+
+def _neighbor_cmds(nb, have_nb):
+ cmds = []
+ nb_addr = nb["neighbor_address"]
+ nbase = _BASE + ["neighbor", nb_addr]
+ have_nb = have_nb or {}
+
+ if nb.get("remote_as") and nb["remote_as"] != have_nb.get("remote_as"):
+ cmds.append(("set", nbase + ["remote-as", str(nb["remote_as"])]))
+ if nb.get("description") and nb["description"] != have_nb.get("description"):
+ cmds.append(("set", nbase + ["description", nb["description"]]))
+ if nb.get("ebgp_multihop") and nb["ebgp_multihop"] != have_nb.get("ebgp_multihop"):
+ cmds.append(("set", nbase + ["ebgp-multihop", str(nb["ebgp_multihop"])]))
+ if nb.get("local_as") and nb["local_as"] != have_nb.get("local_as"):
+ cmds.append(("set", nbase + ["local-as", str(nb["local_as"])]))
+ if nb.get("password") and nb["password"] != have_nb.get("password"):
+ cmds.append(("set", nbase + ["password", nb["password"]]))
+ if nb.get("peer_group") and nb["peer_group"] != have_nb.get("peer_group"):
+ cmds.append(("set", nbase + ["peer-group", nb["peer_group"]]))
+ if nb.get("update_source") and nb["update_source"] != have_nb.get("update_source"):
+ cmds.append(("set", nbase + ["update-source", nb["update_source"]]))
+ if nb.get("shutdown") and not have_nb.get("shutdown"):
+ cmds.append(("set", nbase + ["shutdown"]))
+ if nb.get("disable_connected_check") and not have_nb.get("disable_connected_check"):
+ cmds.append(("set", nbase + ["disable-connected-check"]))
+
+ want_t = nb.get("timers") or {}
+ have_t = have_nb.get("timers") or {}
+ if want_t.get("holdtime") and want_t["holdtime"] != have_t.get("holdtime"):
+ cmds.append(("set", nbase + ["timers", "holdtime", str(want_t["holdtime"])]))
+ if want_t.get("keepalive") and want_t["keepalive"] != have_t.get("keepalive"):
+ cmds.append(("set", nbase + ["timers", "keepalive", str(want_t["keepalive"])]))
+
+ return cmds
+
+
+def _peer_group_cmds(pg, have_pg):
+ cmds = []
+ pg_name = pg["peer_group"]
+ pbase = _BASE + ["peer-group", pg_name]
+ have_pg = have_pg or {}
+
+ if pg.get("remote_as") and pg["remote_as"] != have_pg.get("remote_as"):
+ cmds.append(("set", pbase + ["remote-as", str(pg["remote_as"])]))
+ if pg.get("description") and pg["description"] != have_pg.get("description"):
+ cmds.append(("set", pbase + ["description", pg["description"]]))
+ if pg.get("ebgp_multihop") and pg["ebgp_multihop"] != have_pg.get("ebgp_multihop"):
+ cmds.append(("set", pbase + ["ebgp-multihop", str(pg["ebgp_multihop"])]))
+ if pg.get("password") and pg["password"] != have_pg.get("password"):
+ cmds.append(("set", pbase + ["password", pg["password"]]))
+ if pg.get("update_source") and pg["update_source"] != have_pg.get("update_source"):
+ cmds.append(("set", pbase + ["update-source", pg["update_source"]]))
+
+ want_t = pg.get("timers") or {}
+ have_t = have_pg.get("timers") or {}
+ if want_t.get("holdtime") and want_t["holdtime"] != have_t.get("holdtime"):
+ cmds.append(("set", pbase + ["timers", "holdtime", str(want_t["holdtime"])]))
+ if want_t.get("keepalive") and want_t["keepalive"] != have_t.get("keepalive"):
+ cmds.append(("set", pbase + ["timers", "keepalive", str(want_t["keepalive"])]))
+
+ return cmds
+
+
+def build_commands(config, have, state):
+ cmds = []
+
+ if state == "deleted":
+ if have:
+ cmds.append(("delete", _BASE))
+ return cmds
+
+ if state == "replaced":
+ would_set = build_commands(config, {}, "merged")
+ have_set = build_commands(have, {}, "merged")
+ if would_set == have_set:
+ return []
+ if have:
+ cmds.append(("delete", _BASE))
+ have = {}
+
+ config = config or {}
+
+ # system-as — must be first
+ if config.get("as_number") and config["as_number"] != have.get("as_number"):
+ cmds.append(("set", _BASE + ["system-as", str(config["as_number"])]))
+
+ # parameters
+ params = config.get("parameters") or {}
+ have_params = have.get("parameters") or {}
+ if params.get("router_id") and params["router_id"] != have_params.get("router_id"):
+ cmds.append(("set", _BASE + ["parameters", "router-id", params["router_id"]]))
+ if params.get("log_neighbor_changes") and not have_params.get("log_neighbor_changes"):
+ cmds.append(("set", _BASE + ["parameters", "log-neighbor-changes"]))
+ if params.get("no_ipv4_unicast") and not have_params.get("no_ipv4_unicast"):
+ cmds.append(("set", _BASE + ["parameters", "no-ipv4-unicast"]))
+ if params.get("graceful_restart") and not have_params.get("graceful_restart"):
+ cmds.append(("set", _BASE + ["parameters", "graceful-restart"]))
+ bp = params.get("bestpath") or {}
+ have_bp = have_params.get("bestpath") or {}
+ if bp.get("as_path") and bp["as_path"] != have_bp.get("as_path"):
+ cmds.append(("set", _BASE + ["parameters", "bestpath", "as-path", bp["as_path"]]))
+
+ # neighbors
+ have_nb_map = {n["neighbor_address"]: n for n in (have.get("neighbors") or [])}
+ for nb in config.get("neighbors") or []:
+ cmds += _neighbor_cmds(nb, have_nb_map.get(nb["neighbor_address"]))
+
+ # peer_groups
+ have_pg_map = {p["peer_group"]: p for p in (have.get("peer_groups") or [])}
+ for pg in config.get("peer_groups") or []:
+ cmds += _peer_group_cmds(pg, have_pg_map.get(pg["peer_group"]))
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ as_number=dict(type="int", required=True),
+ parameters=dict(
+ type="dict",
+ options=dict(
+ router_id=dict(type="str"),
+ log_neighbor_changes=dict(type="bool"),
+ no_ipv4_unicast=dict(type="bool"),
+ graceful_restart=dict(type="bool"),
+ bestpath=dict(
+ type="dict",
+ options=dict(
+ as_path=dict(
+ type="str",
+ choices=["confed", "ignore", "multipath-relax"],
+ ),
+ ),
+ ),
+ confederation=dict(
+ type="dict",
+ options=dict(
+ identifier=dict(type="int"),
+ peers=dict(type="list", elements="int"),
+ ),
+ ),
+ ),
+ ),
+ neighbors=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ neighbor_address=dict(type="str", required=True),
+ remote_as=dict(type="int"),
+ description=dict(type="str"),
+ disable_connected_check=dict(type="bool"),
+ ebgp_multihop=dict(type="int"),
+ local_as=dict(type="int"),
+ password=dict(type="str", no_log=True),
+ peer_group=dict(type="str"),
+ shutdown=dict(type="bool"),
+ timers=dict(
+ type="dict",
+ options=dict(
+ holdtime=dict(type="int"),
+ keepalive=dict(type="int"),
+ ),
+ ),
+ update_source=dict(type="str"),
+ ),
+ ),
+ peer_groups=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ peer_group=dict(type="str", required=True),
+ remote_as=dict(type="int"),
+ description=dict(type="str"),
+ ebgp_multihop=dict(type="int"),
+ password=dict(type="str", no_log=True),
+ timers=dict(
+ type="dict",
+ options=dict(
+ holdtime=dict(type="int"),
+ keepalive=dict(type="int"),
+ ),
+ ),
+ update_source=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ default="merged",
+ choices=["merged", "replaced", "deleted", "gathered"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or {}
+
+ have = get_running_config(vyos)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+
+ commands = build_commands(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()
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=get_running_config(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_facts.py b/plugins/modules/vyos_facts.py
new file mode 100644
index 0000000..b75522d
--- /dev/null
+++ b/plugins/modules/vyos_facts.py
@@ -0,0 +1,287 @@
+#!/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 using REST API
+description:
+ - Collects facts from VyOS devices via the REST API.
+ - Returns structured facts under the C(ansible_facts) key.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ gather_subset:
+ description:
+ - When supplied, this argument will restrict the facts collected to
+ a given subset. Possible values for this argument include C(all),
+ C(default), C(config), C(interfaces), C(hostname), C(users),
+ C(bgp), C(ospf), C(ntp), C(snmp) and C(logging).
+ - Specify a list of values to include a larger subset. Use the
+ exclamation mark (C(!)) before a value to exclude it. Values
+ C(all) and C(default) cannot be combined with each other or with
+ negation.
+ type: list
+ elements: str
+ default: ['default']
+ gather_network_resources:
+ description:
+ - When supplied, this argument will restrict the facts collected to
+ a given subset. Possible values include the resource module names.
+ - This argument is not currently used.
+ type: list
+ elements: str
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+ - Only configuration facts are available via the REST API.
+ Operational state (interface counters, BGP neighbors) is not supported.
+"""
+
+EXAMPLES = r"""
+- name: Gather all facts
+ vyos.rest.vyos_facts:
+ gather_subset: all
+
+- name: Gather default facts
+ vyos.rest.vyos_facts:
+
+- name: Gather interface and hostname facts only
+ vyos.rest.vyos_facts:
+ gather_subset:
+ - interfaces
+ - hostname
+
+- name: Gather all except config
+ vyos.rest.vyos_facts:
+ gather_subset:
+ - all
+ - '!config'
+"""
+
+RETURN = r"""
+ansible_facts:
+ description: Facts collected from the device.
+ returned: always
+ type: dict
+ contains:
+ vyos_hostname:
+ description: Device hostname.
+ type: str
+ vyos_config:
+ description: Full device configuration as structured data.
+ type: dict
+ vyos_interfaces:
+ description: Interface configuration.
+ type: dict
+ vyos_users:
+ description: User accounts (without passwords).
+ type: list
+ vyos_bgp:
+ description: BGP configuration.
+ type: dict
+ vyos_ospf:
+ description: OSPFv2 configuration.
+ type: dict
+ vyos_ospfv3:
+ description: OSPFv3 configuration.
+ type: dict
+ vyos_ntp:
+ description: NTP configuration.
+ type: dict
+ vyos_snmp:
+ description: SNMP configuration.
+ type: dict
+ vyos_logging:
+ description: Logging configuration.
+ type: dict
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+
+
+VALID_SUBSETS = frozenset(
+ [
+ "all",
+ "default",
+ "config",
+ "interfaces",
+ "hostname",
+ "users",
+ "bgp",
+ "ospf",
+ "ospfv3",
+ "ntp",
+ "snmp",
+ "logging",
+ ],
+)
+
+DEFAULT_SUBSETS = frozenset(["hostname", "interfaces"])
+
+
+def _get_config(vyos, path):
+ try:
+ result = vyos.get_config(path)
+ return result or {}
+ except Exception:
+ return {}
+
+
+def gather_hostname(vyos):
+ raw = _get_config(vyos, ["system"])
+ return raw.get("host-name", "")
+
+
+def gather_config(vyos):
+ return _get_config(vyos, [])
+
+
+def gather_interfaces(vyos):
+ return _get_config(vyos, ["interfaces"])
+
+
+def gather_users(vyos):
+ raw = _get_config(vyos, ["system", "login", "user"])
+ if not raw or not isinstance(raw, dict):
+ return []
+ raw = raw.get("user", raw)
+ users = []
+ for username, data in sorted(raw.items()):
+ user = {"name": username}
+ data = data or {}
+ if data.get("full-name"):
+ user["full_name"] = data["full-name"]
+ auth = data.get("authentication", {}) or {}
+ pub_keys = auth.get("public-keys", {}) or {}
+ if pub_keys:
+ user["public_keys"] = list(pub_keys.keys())
+ users.append(user)
+ return users
+
+
+def gather_bgp(vyos):
+ return _get_config(vyos, ["protocols", "bgp"])
+
+
+def gather_ospf(vyos):
+ return _get_config(vyos, ["protocols", "ospf"])
+
+
+def gather_ospfv3(vyos):
+ return _get_config(vyos, ["protocols", "ospfv3"])
+
+
+def gather_ntp(vyos):
+ return _get_config(vyos, ["service", "ntp"])
+
+
+def gather_snmp(vyos):
+ return _get_config(vyos, ["service", "snmp"])
+
+
+def gather_logging(vyos):
+ return _get_config(vyos, ["system", "syslog"])
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ gather_subset=dict(
+ type="list",
+ elements="str",
+ default=["default"],
+ ),
+ gather_network_resources=dict(
+ type="list",
+ elements="str",
+ ),
+ ),
+ supports_check_mode=True,
+ )
+
+ vyos = VyOSModule(module)
+ gather_subset = module.params["gather_subset"]
+
+ # Normalize subset
+ runable_subsets = set()
+ exclude_subsets = set()
+
+ for subset in gather_subset:
+ if subset.startswith("!"):
+ exclude = subset[1:]
+ if exclude not in VALID_SUBSETS:
+ module.fail_json(msg="Invalid subset: %s" % exclude)
+ exclude_subsets.add(exclude)
+ elif subset == "all":
+ runable_subsets.update(VALID_SUBSETS - {"all", "default"})
+ elif subset == "default":
+ runable_subsets.update(DEFAULT_SUBSETS)
+ elif subset in VALID_SUBSETS:
+ runable_subsets.add(subset)
+ else:
+ module.fail_json(msg="Invalid subset: %s" % subset)
+
+ if not runable_subsets:
+ runable_subsets.update(DEFAULT_SUBSETS)
+
+ runable_subsets -= exclude_subsets
+ runable_subsets -= {"all", "default"}
+
+ facts = {}
+
+ if "hostname" in runable_subsets:
+ facts["vyos_hostname"] = gather_hostname(vyos)
+
+ if "config" in runable_subsets:
+ facts["vyos_config"] = gather_config(vyos)
+
+ if "interfaces" in runable_subsets:
+ facts["vyos_interfaces"] = gather_interfaces(vyos)
+
+ if "users" in runable_subsets:
+ facts["vyos_users"] = gather_users(vyos)
+
+ if "bgp" in runable_subsets:
+ bgp = gather_bgp(vyos)
+ if bgp:
+ facts["vyos_bgp"] = bgp
+
+ if "ospf" in runable_subsets:
+ ospf = gather_ospf(vyos)
+ if ospf:
+ facts["vyos_ospf"] = ospf
+
+ if "ospfv3" in runable_subsets:
+ ospfv3 = gather_ospfv3(vyos)
+ if ospfv3:
+ facts["vyos_ospfv3"] = ospfv3
+
+ if "ntp" in runable_subsets:
+ ntp = gather_ntp(vyos)
+ if ntp:
+ facts["vyos_ntp"] = ntp
+
+ if "snmp" in runable_subsets:
+ snmp = gather_snmp(vyos)
+ if snmp:
+ facts["vyos_snmp"] = snmp
+
+ if "logging" in runable_subsets:
+ logging = gather_logging(vyos)
+ if logging:
+ facts["vyos_logging"] = logging
+
+ module.exit_json(ansible_facts=facts)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_firewall_global.py b/plugins/modules/vyos_firewall_global.py
new file mode 100644
index 0000000..66cdabc
--- /dev/null
+++ b/plugins/modules/vyos_firewall_global.py
@@ -0,0 +1,402 @@
+#!/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: Manage global firewall configuration on VyOS devices using REST API
+description:
+ - Manages global firewall group configuration on VyOS devices via the REST API.
+ - Covers address-groups, network-groups, port-groups, interface-groups,
+ and IPv6 network-groups.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: Global firewall configuration.
+ type: dict
+ suboptions:
+ group:
+ description: Firewall groups.
+ type: dict
+ suboptions:
+ address_group:
+ description: IPv4 address groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ address:
+ description: IP addresses or ranges in the group.
+ type: list
+ elements: str
+ network_group:
+ description: IPv4 network groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ network:
+ description: Network prefixes in the group.
+ type: list
+ elements: str
+ port_group:
+ description: Port groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ port:
+ description: Ports or port ranges in the group.
+ type: list
+ elements: str
+ interface_group:
+ description: Interface groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ interface:
+ description: Interfaces in the group.
+ type: list
+ elements: str
+ ipv6_network_group:
+ description: IPv6 network groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Group name.
+ type: str
+ required: true
+ description:
+ description: Group description.
+ type: str
+ network:
+ description: IPv6 network prefixes in the group.
+ type: list
+ elements: str
+ state:
+ description:
+ - Desired state of the firewall global configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces the entire firewall global configuration.
+ - C(deleted) removes firewall global configuration.
+ - C(gathered) returns current configuration as structured data.
+ type: str
+ choices: [merged, replaced, deleted, gathered]
+ default: merged
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+"""
+
+EXAMPLES = r"""
+- name: Merge firewall global configuration
+ vyos.rest.vyos_firewall_global:
+ config:
+ group:
+ address_group:
+ - name: SERVERS
+ description: Web servers
+ address:
+ - 192.168.1.10
+ - 192.168.1.11
+ network_group:
+ - name: LAN
+ network:
+ - 192.168.0.0/16
+ port_group:
+ - name: WEB-PORTS
+ port:
+ - "80"
+ - "443"
+ interface_group:
+ - name: LAN-IFACES
+ interface:
+ - eth1
+ - eth2
+ ipv6_network_group:
+ - name: IPV6-LAN
+ network:
+ - "2001:db8::/32"
+ state: merged
+
+- name: Delete all firewall global configuration
+ vyos.rest.vyos_firewall_global:
+ state: deleted
+
+- name: Gather firewall global configuration
+ vyos.rest.vyos_firewall_global:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Firewall global configuration before this module ran.
+ returned: always
+ type: dict
+after:
+ description: Firewall global configuration after this module ran.
+ returned: when changed
+ type: dict
+commands:
+ description: List of API command tuples sent to the device.
+ returned: always
+ type: list
+gathered:
+ description: Current firewall global configuration as structured data.
+ returned: when state is gathered
+ type: dict
+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 import VyOSModule
+
+
+_BASE = ["firewall", "group"]
+
+# Map argspec key -> API key, value key
+_GROUP_TYPES = {
+ "address_group": ("address-group", "address"),
+ "network_group": ("network-group", "network"),
+ "port_group": ("port-group", "port"),
+ "interface_group": ("interface-group", "interface"),
+ "ipv6_network_group": ("ipv6-network-group", "network"),
+}
+
+
+def _parse_group_type(raw, val_key):
+ """Parse a group dict from API raw data."""
+ if not raw or not isinstance(raw, dict):
+ return []
+ result = []
+ for name, data in sorted(raw.items()):
+ entry = {"name": name}
+ data = data or {}
+ if data.get("description"):
+ entry["description"] = data["description"]
+ val = data.get(val_key)
+ if val is not None:
+ if isinstance(val, list):
+ entry[val_key.replace("-", "_")] = val
+ elif isinstance(val, str):
+ entry[val_key.replace("-", "_")] = [val]
+ elif isinstance(val, dict):
+ entry[val_key.replace("-", "_")] = list(val.keys())
+ result.append(entry)
+ return result
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {"group": {}}
+
+ for arg_key, (api_key, val_key) in _GROUP_TYPES.items():
+ groups = _parse_group_type(raw.get(api_key), val_key)
+ if groups:
+ result["group"][arg_key] = groups
+
+ if not result["group"]:
+ return {}
+ return result
+
+
+def _group_cmds(arg_key, groups, have_groups, state):
+ cmds = []
+ api_key, val_key = _GROUP_TYPES[arg_key]
+ have_map = {g["name"]: g for g in (have_groups or [])}
+ want_map = {g["name"]: g for g in (groups or [])}
+
+ if state == "replaced":
+ for name in set(have_map) - set(want_map):
+ cmds.append(("delete", _BASE + [api_key, name]))
+
+ for name, group in want_map.items():
+ have_group = have_map.get(name, {})
+ gbase = _BASE + [api_key, name]
+
+ if group.get("description") and group["description"] != have_group.get("description"):
+ cmds.append(("set", gbase + ["description", group["description"]]))
+
+ # normalize val_key for argspec (underscores)
+ arg_val_key = val_key.replace("-", "_")
+ want_vals = set(group.get(arg_val_key) or [])
+ have_vals = set(have_group.get(arg_val_key) or [])
+
+ for val in want_vals - have_vals:
+ cmds.append(("set", gbase + [val_key, val]))
+
+ if state == "replaced":
+ for val in have_vals - want_vals:
+ cmds.append(("delete", gbase + [val_key, val]))
+
+ return cmds
+
+
+def build_commands(config, have, state):
+ cmds = []
+
+ if state == "deleted":
+ if have:
+ cmds.append(("delete", _BASE))
+ return cmds
+
+ if state == "replaced":
+ # Check if anything differs
+ would_set = build_commands(config, {}, "merged")
+ have_set = build_commands(have, {}, "merged")
+ if would_set == have_set:
+ return []
+
+ config = config or {}
+ want_group = config.get("group") or {}
+ have_group = have.get("group") or {}
+
+ for arg_key in _GROUP_TYPES:
+ want_groups = want_group.get(arg_key) or []
+ have_groups = have_group.get(arg_key) or []
+ if want_groups or (state == "replaced" and have_groups):
+ cmds += _group_cmds(arg_key, want_groups, have_groups, state)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ 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"),
+ address=dict(type="list", elements="str"),
+ ),
+ ),
+ network_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ network=dict(type="list", elements="str"),
+ ),
+ ),
+ port_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ port=dict(type="list", elements="str"),
+ ),
+ ),
+ interface_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ interface=dict(type="list", elements="str"),
+ ),
+ ),
+ ipv6_network_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ network=dict(type="list", elements="str"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ default="merged",
+ choices=["merged", "replaced", "deleted", "gathered"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or {}
+
+ have = get_running_config(vyos)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+
+ commands = build_commands(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()
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=get_running_config(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_firewall_interfaces.py b/plugins/modules/vyos_firewall_interfaces.py
new file mode 100644
index 0000000..8769f54
--- /dev/null
+++ b/plugins/modules/vyos_firewall_interfaces.py
@@ -0,0 +1,473 @@
+#!/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: Manage firewall hook filters on VyOS devices using REST API
+description:
+ - Manages firewall hook filter configuration on VyOS devices via the REST API.
+ - In VyOS 1.5+, firewall hook filters (input/output/forward) replace the
+ per-interface firewall assignments used in VyOS 1.4.
+ - Hook filters apply globally to all traffic traversing that hook point.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: Firewall hook filter configuration.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ hooks:
+ description: Hook filter configurations for this address family.
+ type: list
+ elements: dict
+ suboptions:
+ hook:
+ description: Netfilter hook point.
+ type: str
+ choices: [input, output, forward]
+ required: true
+ default_action:
+ description: Default action when no rule matches.
+ type: str
+ choices: [accept, drop, reject]
+ description:
+ description: Filter description.
+ type: str
+ rules:
+ description: Rules in this hook filter.
+ type: list
+ elements: dict
+ suboptions:
+ number:
+ description: Rule number.
+ type: int
+ required: true
+ action:
+ description: Rule action.
+ type: str
+ choices: [accept, drop, reject, return, queue, continue]
+ description:
+ description: Rule description.
+ type: str
+ disable:
+ description: Disable this rule.
+ type: bool
+ protocol:
+ description: Protocol to match.
+ type: str
+ state:
+ description: Connection state to match.
+ type: str
+ choices: [established, invalid, new, related]
+ log:
+ description: Enable logging.
+ type: bool
+ source:
+ description: Source match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Source IP address or prefix.
+ type: str
+ port:
+ description: Source port or range.
+ type: str
+ destination:
+ description: Destination match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Destination IP address or prefix.
+ type: str
+ port:
+ description: Destination port or range.
+ type: str
+ state:
+ description:
+ - Desired state of the firewall hook filter configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces hook filter config for named hooks in config.
+ - C(overridden) replaces all firewall hook filter config.
+ - C(deleted) removes firewall hook filter config.
+ - C(gathered) returns current configuration as structured data.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+ - In VyOS 1.5+, hook filters apply globally rather than per-interface.
+ Use named rule sets (M(vyos.rest.vyos_firewall_rules)) for more granular
+ per-traffic control.
+"""
+
+EXAMPLES = r"""
+- name: Merge firewall hook filter configuration
+ vyos.rest.vyos_firewall_interfaces:
+ config:
+ - afi: ipv4
+ hooks:
+ - hook: input
+ default_action: accept
+ rules:
+ - number: 10
+ action: accept
+ state: established
+ - number: 20
+ action: drop
+ state: invalid
+ - hook: forward
+ default_action: accept
+ - hook: output
+ default_action: accept
+ - afi: ipv6
+ hooks:
+ - hook: input
+ default_action: accept
+ state: merged
+
+- name: Delete all firewall hook filter configuration
+ vyos.rest.vyos_firewall_interfaces:
+ state: deleted
+
+- name: Gather firewall hook filter configuration
+ vyos.rest.vyos_firewall_interfaces:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Firewall hook filter configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: Firewall hook filter 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 firewall hook filter 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 import VyOSModule
+
+
+_BASE = ["firewall"]
+_AFIS = ["ipv4", "ipv6"]
+_HOOKS = ["input", "output", "forward"]
+
+
+def _parse_rule(rule_num, data):
+ rule = {"number": int(rule_num)}
+ data = data or {}
+ if "action" in data:
+ rule["action"] = data["action"]
+ if "description" in data:
+ rule["description"] = data["description"]
+ if "disable" in data:
+ rule["disable"] = True
+ if "protocol" in data:
+ rule["protocol"] = data["protocol"]
+ if "state" in data:
+ rule["state"] = data["state"]
+ if "log" in data:
+ rule["log"] = True
+
+ for endpoint in ["source", "destination"]:
+ ep = data.get(endpoint, {}) or {}
+ if ep:
+ rule[endpoint] = {}
+ if "address" in ep:
+ rule[endpoint]["address"] = ep["address"]
+ if "port" in ep:
+ rule[endpoint]["port"] = ep["port"]
+
+ return rule
+
+
+def _parse_hook_filter(hook, data):
+ entry = {"hook": hook}
+ data = data or {}
+ filter_data = data.get("filter", {}) or {}
+ if "default-action" in filter_data:
+ entry["default_action"] = filter_data["default-action"]
+ if "description" in filter_data:
+ entry["description"] = filter_data["description"]
+ rules_raw = filter_data.get("rule", {}) or {}
+ if rules_raw and isinstance(rules_raw, dict):
+ rules = [
+ _parse_rule(num, rdata)
+ for num, rdata in sorted(
+ rules_raw.items(),
+ key=lambda x: int(x[0]),
+ )
+ ]
+ if rules:
+ entry["rules"] = rules
+ return entry
+
+
+def get_running_config(vyos):
+ result = []
+ for afi in _AFIS:
+ raw = vyos.get_config(_BASE + [afi])
+ if not raw or not isinstance(raw, dict):
+ continue
+ hooks = []
+ for hook in _HOOKS:
+ if hook in raw:
+ parsed = _parse_hook_filter(hook, raw[hook])
+ if len(parsed) > 1: # more than just hook key
+ hooks.append(parsed)
+ if hooks:
+ result.append({"afi": afi, "hooks": hooks})
+ return result
+
+
+def _rule_cmds(afi, hook, rule, have_rule):
+ cmds = []
+ rbase = _BASE + [afi, hook, "filter", "rule", str(rule["number"])]
+ have_rule = have_rule or {}
+
+ 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("disable") and not have_rule.get("disable"):
+ cmds.append(("set", rbase + ["disable"]))
+ if rule.get("protocol") and rule["protocol"] != have_rule.get("protocol"):
+ cmds.append(("set", rbase + ["protocol", rule["protocol"]]))
+ if rule.get("state") and rule["state"] != have_rule.get("state"):
+ cmds.append(("set", rbase + ["state", rule["state"]]))
+ if rule.get("log") and not have_rule.get("log"):
+ cmds.append(("set", rbase + ["log"]))
+
+ for endpoint in ["source", "destination"]:
+ want_ep = rule.get(endpoint) or {}
+ have_ep = have_rule.get(endpoint) or {}
+ if want_ep.get("address") and want_ep["address"] != have_ep.get("address"):
+ cmds.append(("set", rbase + [endpoint, "address", want_ep["address"]]))
+ if want_ep.get("port") and want_ep["port"] != have_ep.get("port"):
+ cmds.append(("set", rbase + [endpoint, "port", str(want_ep["port"])]))
+
+ return cmds
+
+
+def _hook_cmds(afi, hook_entry, have_hook, state):
+ cmds = []
+ hook = hook_entry["hook"]
+ hbase = _BASE + [afi, hook, "filter"]
+ have_hook = have_hook or {}
+
+ if hook_entry.get("default_action") and hook_entry["default_action"] != have_hook.get(
+ "default_action",
+ ):
+ cmds.append(("set", hbase + ["default-action", hook_entry["default_action"]]))
+ if hook_entry.get("description") and hook_entry["description"] != have_hook.get("description"):
+ cmds.append(("set", hbase + ["description", hook_entry["description"]]))
+
+ have_rules = {r["number"]: r for r in (have_hook.get("rules") or [])}
+ want_rules = {r["number"]: r for r in (hook_entry.get("rules") or [])}
+
+ if state == "replaced":
+ for num in set(have_rules) - set(want_rules):
+ cmds.append(("delete", hbase + ["rule", str(num)]))
+
+ for num, rule in want_rules.items():
+ cmds += _rule_cmds(afi, hook, rule, have_rules.get(num))
+
+ return cmds
+
+
+def build_commands(config, have_list, state):
+ cmds = []
+
+ if state == "deleted":
+ if not config:
+ if have_list:
+ for entry in have_list:
+ afi = entry["afi"]
+ for hook_entry in entry.get("hooks", []):
+ cmds.append(("delete", _BASE + [afi, hook_entry["hook"], "filter"]))
+ else:
+ have_map = {(e["afi"], h["hook"]): h for e in have_list for h in e.get("hooks", [])}
+ for entry in config:
+ afi = entry["afi"]
+ for hook_entry in entry.get("hooks") or []:
+ if (afi, hook_entry["hook"]) in have_map:
+ cmds.append(("delete", _BASE + [afi, hook_entry["hook"], "filter"]))
+ return cmds
+
+ have_map = {e["afi"]: {h["hook"]: h for h in e.get("hooks", [])} for e in have_list}
+
+ if state == "overridden":
+ want_keys = {(e["afi"], h["hook"]) for e in (config or []) for h in e.get("hooks", [])}
+ for e in have_list:
+ for h in e.get("hooks", []):
+ if (e["afi"], h["hook"]) not in want_keys:
+ cmds.append(("delete", _BASE + [e["afi"], h["hook"], "filter"]))
+
+ for entry in config or []:
+ afi = entry["afi"]
+ have_afi = have_map.get(afi, {})
+
+ for hook_entry in entry.get("hooks") or []:
+ hook = hook_entry["hook"]
+ have_hook = have_afi.get(hook)
+
+ if state == "replaced" and have_hook:
+ want_cmds = _hook_cmds(afi, hook_entry, {}, "merged")
+ have_hook_entry = {
+ "hook": hook,
+ "default_action": have_hook.get("default_action"),
+ "rules": have_hook.get("rules", []),
+ }
+ have_cmds = _hook_cmds(afi, have_hook_entry, {}, "merged")
+ if want_cmds != have_cmds:
+ cmds.append(("delete", _BASE + [afi, hook, "filter"]))
+ have_hook = None
+
+ effective_state = state if state not in ("replaced", "overridden") else "merged"
+ cmds += _hook_cmds(afi, hook_entry, have_hook, effective_state)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", choices=["ipv4", "ipv6"], required=True),
+ hooks=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ hook=dict(
+ type="str",
+ choices=["input", "output", "forward"],
+ required=True,
+ ),
+ default_action=dict(
+ type="str",
+ choices=["accept", "drop", "reject"],
+ ),
+ description=dict(type="str"),
+ rules=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ number=dict(type="int", required=True),
+ action=dict(
+ type="str",
+ choices=[
+ "accept",
+ "drop",
+ "reject",
+ "return",
+ "queue",
+ "continue",
+ ],
+ ),
+ description=dict(type="str"),
+ disable=dict(type="bool"),
+ protocol=dict(type="str"),
+ state=dict(
+ type="str",
+ choices=[
+ "established",
+ "invalid",
+ "new",
+ "related",
+ ],
+ ),
+ log=dict(type="bool"),
+ source=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ port=dict(type="str"),
+ ),
+ ),
+ destination=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ port=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or []
+
+ have = get_running_config(vyos)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+
+ commands = build_commands(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()
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=get_running_config(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_firewall_rules.py b/plugins/modules/vyos_firewall_rules.py
new file mode 100644
index 0000000..a1c7c01
--- /dev/null
+++ b/plugins/modules/vyos_firewall_rules.py
@@ -0,0 +1,546 @@
+#!/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_rules
+short_description: Manage firewall rule sets on VyOS devices using REST API
+description:
+ - Manages named firewall rule sets on VyOS devices via the REST API.
+ - Supports both IPv4 (C(ipv4)) and IPv6 (C(ipv6)) rule sets.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+ - In VyOS 1.5+, firewall uses named rule sets under C(firewall.ipv4.name)
+ and C(firewall.ipv6.name).
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: Firewall rule set configuration.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ rule_sets:
+ description: Named rule sets for this address family.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Rule set name.
+ type: str
+ required: true
+ default_action:
+ description: Default action when no rule matches.
+ type: str
+ choices: [accept, drop, reject]
+ description:
+ description: Rule set description.
+ type: str
+ rules:
+ description: Firewall rules in this rule set.
+ type: list
+ elements: dict
+ suboptions:
+ number:
+ description: Rule number.
+ type: int
+ required: true
+ action:
+ description: Rule action.
+ type: str
+ choices: [accept, drop, reject, return, queue, continue]
+ description:
+ description: Rule description.
+ type: str
+ disable:
+ description: Disable this rule.
+ type: bool
+ protocol:
+ description: Protocol to match.
+ type: str
+ state:
+ description: Connection state to match.
+ type: str
+ choices: [established, invalid, new, related]
+ source:
+ description: Source match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Source IP address or prefix.
+ type: str
+ group:
+ description: Source group name.
+ type: str
+ port:
+ description: Source port or range.
+ type: str
+ destination:
+ description: Destination match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Destination IP address or prefix.
+ type: str
+ group:
+ description: Destination group name.
+ type: str
+ port:
+ description: Destination port or range.
+ type: str
+ log:
+ description: Enable logging for this rule.
+ type: bool
+ icmp:
+ description: ICMP type/code to match.
+ type: dict
+ suboptions:
+ type:
+ description: ICMP type.
+ type: int
+ code:
+ description: ICMP code.
+ type: int
+ state:
+ description:
+ - Desired state of the firewall rules configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces rule sets for named rule sets in config.
+ - C(overridden) replaces all firewall rule sets.
+ - C(deleted) removes firewall rule sets.
+ - C(gathered) returns current configuration as structured data.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+ - Rule sets are identified by AFI and name. Deleting a rule set removes
+ all its rules.
+"""
+
+EXAMPLES = r"""
+- name: Merge firewall rules
+ vyos.rest.vyos_firewall_rules:
+ config:
+ - afi: ipv4
+ rule_sets:
+ - name: RULE-SET1
+ default_action: drop
+ rules:
+ - number: 10
+ action: accept
+ protocol: tcp
+ source:
+ address: 192.168.1.0/24
+ destination:
+ port: "80"
+ - number: 20
+ action: drop
+ state: invalid
+ - afi: ipv6
+ rule_sets:
+ - name: RULE-SET6
+ default_action: accept
+ rules:
+ - number: 10
+ action: accept
+ state: merged
+
+- name: Delete all firewall rules
+ vyos.rest.vyos_firewall_rules:
+ state: deleted
+
+- name: Gather firewall rules
+ vyos.rest.vyos_firewall_rules:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Firewall rules configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: Firewall rules 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 firewall rules 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 import VyOSModule
+
+
+_BASE = ["firewall"]
+_AFIS = ["ipv4", "ipv6"]
+
+
+def _parse_rule(rule_num, data):
+ rule = {"number": int(rule_num)}
+ data = data or {}
+ if "action" in data:
+ rule["action"] = data["action"]
+ if "description" in data:
+ rule["description"] = data["description"]
+ if "disable" in data:
+ rule["disable"] = True
+ if "protocol" in data:
+ rule["protocol"] = data["protocol"]
+ if "state" in data:
+ rule["state"] = data["state"]
+ if "log" in data:
+ rule["log"] = True
+
+ src = data.get("source", {}) or {}
+ if src:
+ rule["source"] = {}
+ if "address" in src:
+ rule["source"]["address"] = src["address"]
+ if "group" in src:
+ grp = src["group"]
+ if isinstance(grp, dict):
+ rule["source"]["group"] = list(grp.values())[0] if grp else None
+ else:
+ rule["source"]["group"] = grp
+ if "port" in src:
+ rule["source"]["port"] = src["port"]
+
+ dst = data.get("destination", {}) or {}
+ if dst:
+ rule["destination"] = {}
+ if "address" in dst:
+ rule["destination"]["address"] = dst["address"]
+ if "group" in dst:
+ grp = dst["group"]
+ if isinstance(grp, dict):
+ rule["destination"]["group"] = list(grp.values())[0] if grp else None
+ else:
+ rule["destination"]["group"] = grp
+ if "port" in dst:
+ rule["destination"]["port"] = dst["port"]
+
+ icmp = data.get("icmp", {}) or {}
+ if icmp:
+ rule["icmp"] = {}
+ if "type" in icmp:
+ rule["icmp"]["type"] = int(icmp["type"])
+ if "code" in icmp:
+ rule["icmp"]["code"] = int(icmp["code"])
+
+ return rule
+
+
+def _parse_rule_set(rs_name, data):
+ rs = {"name": rs_name}
+ data = data or {}
+ if "default-action" in data:
+ rs["default_action"] = data["default-action"]
+ if "description" in data:
+ rs["description"] = data["description"]
+ rules_raw = data.get("rule", {}) or {}
+ if rules_raw and isinstance(rules_raw, dict):
+ rules = [
+ _parse_rule(num, rdata)
+ for num, rdata in sorted(
+ rules_raw.items(),
+ key=lambda x: int(x[0]),
+ )
+ ]
+ if rules:
+ rs["rules"] = rules
+ return rs
+
+
+def get_running_config(vyos):
+ result = []
+ for afi in _AFIS:
+ raw = vyos.get_config(_BASE + [afi, "name"])
+ if not raw or not isinstance(raw, dict):
+ continue
+ # unwrap "name" key if present
+ raw = raw.get("name", raw)
+ if not raw or not isinstance(raw, dict):
+ continue
+ rule_sets = [_parse_rule_set(name, data) for name, data in sorted(raw.items())]
+ if rule_sets:
+ result.append({"afi": afi, "rule_sets": rule_sets})
+ return result
+
+
+def _rule_cmds(rs_name, afi, rule, have_rule):
+ cmds = []
+ rbase = _BASE + [afi, "name", rs_name, "rule", str(rule["number"])]
+ have_rule = have_rule or {}
+
+ 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("disable") and not have_rule.get("disable"):
+ cmds.append(("set", rbase + ["disable"]))
+ if rule.get("protocol") and rule["protocol"] != have_rule.get("protocol"):
+ cmds.append(("set", rbase + ["protocol", rule["protocol"]]))
+ if rule.get("state") and rule["state"] != have_rule.get("state"):
+ cmds.append(("set", rbase + ["state", rule["state"]]))
+ if rule.get("log") and not have_rule.get("log"):
+ cmds.append(("set", rbase + ["log"]))
+
+ for endpoint in ["source", "destination"]:
+ want_ep = rule.get(endpoint) or {}
+ have_ep = have_rule.get(endpoint) or {}
+ if want_ep.get("address") and want_ep["address"] != have_ep.get("address"):
+ cmds.append(("set", rbase + [endpoint, "address", want_ep["address"]]))
+ if want_ep.get("port") and want_ep["port"] != have_ep.get("port"):
+ cmds.append(("set", rbase + [endpoint, "port", str(want_ep["port"])]))
+ if want_ep.get("group") and want_ep["group"] != have_ep.get("group"):
+ cmds.append(
+ (
+ "set",
+ rbase
+ + [
+ endpoint,
+ "group",
+ "address-group",
+ want_ep["group"],
+ ],
+ ),
+ )
+
+ icmp = rule.get("icmp") or {}
+ have_icmp = have_rule.get("icmp") or {}
+ if icmp.get("type") and icmp["type"] != have_icmp.get("type"):
+ cmds.append(("set", rbase + ["icmp", "type", str(icmp["type"])]))
+ if icmp.get("code") and icmp["code"] != have_icmp.get("code"):
+ cmds.append(("set", rbase + ["icmp", "code", str(icmp["code"])]))
+
+ return cmds
+
+
+def _rule_set_cmds(afi, rs, have_rs, state):
+ cmds = []
+ rs_name = rs["name"]
+ rsbase = _BASE + [afi, "name", rs_name]
+ have_rs = have_rs or {}
+
+ if rs.get("default_action") and rs["default_action"] != have_rs.get("default_action"):
+ cmds.append(("set", rsbase + ["default-action", rs["default_action"]]))
+ if rs.get("description") and rs["description"] != have_rs.get("description"):
+ cmds.append(("set", rsbase + ["description", rs["description"]]))
+
+ have_rules = {r["number"]: r for r in (have_rs.get("rules") or [])}
+ want_rules = {r["number"]: r for r in (rs.get("rules") or [])}
+
+ if state == "replaced":
+ for num in set(have_rules) - set(want_rules):
+ cmds.append(("delete", rsbase + ["rule", str(num)]))
+
+ for num, rule in want_rules.items():
+ cmds += _rule_cmds(rs_name, afi, rule, have_rules.get(num))
+
+ return cmds
+
+
+def build_commands(config, have_list, state):
+ cmds = []
+
+ if state == "deleted":
+ if not config:
+ if have_list:
+ cmds.append(("delete", _BASE))
+ else:
+ have_map = {
+ (e["afi"], rs["name"]): rs for e in have_list for rs in e.get("rule_sets", [])
+ }
+ for entry in config:
+ afi = entry["afi"]
+ for rs in entry.get("rule_sets") or []:
+ if (afi, rs["name"]) in have_map:
+ cmds.append(("delete", _BASE + [afi, "name", rs["name"]]))
+ return cmds
+
+ have_map = {e["afi"]: {rs["name"]: rs for rs in e.get("rule_sets", [])} for e in have_list}
+
+ if state == "overridden":
+ want_keys = {
+ (e["afi"], rs["name"]) for e in (config or []) for rs in e.get("rule_sets", [])
+ }
+ for e in have_list:
+ for rs in e.get("rule_sets", []):
+ if (e["afi"], rs["name"]) not in want_keys:
+ cmds.append(("delete", _BASE + [e["afi"], "name", rs["name"]]))
+
+ for entry in config or []:
+ afi = entry["afi"]
+ have_afi = have_map.get(afi, {})
+
+ for rs in entry.get("rule_sets") or []:
+ have_rs = have_afi.get(rs["name"])
+
+ if state == "replaced" and have_rs:
+ # delete and rebuild if different
+ want_cmds = _rule_set_cmds(afi, rs, {}, "merged")
+ have_cmds = _rule_set_cmds(
+ afi,
+ {
+ "name": rs["name"],
+ "default_action": have_rs.get("default_action"),
+ "rules": have_rs.get("rules", []),
+ },
+ {},
+ "merged",
+ )
+ if want_cmds != have_cmds:
+ cmds.append(("delete", _BASE + [afi, "name", rs["name"]]))
+ have_rs = None
+
+ cmds += _rule_set_cmds(
+ afi,
+ rs,
+ have_rs,
+ state if state not in ("replaced", "overridden") else "merged",
+ )
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", choices=["ipv4", "ipv6"], required=True),
+ rule_sets=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ default_action=dict(
+ type="str",
+ choices=["accept", "drop", "reject"],
+ ),
+ description=dict(type="str"),
+ rules=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ number=dict(type="int", required=True),
+ action=dict(
+ type="str",
+ choices=[
+ "accept",
+ "drop",
+ "reject",
+ "return",
+ "queue",
+ "continue",
+ ],
+ ),
+ description=dict(type="str"),
+ disable=dict(type="bool"),
+ protocol=dict(type="str"),
+ state=dict(
+ type="str",
+ choices=[
+ "established",
+ "invalid",
+ "new",
+ "related",
+ ],
+ ),
+ log=dict(type="bool"),
+ source=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ group=dict(type="str"),
+ port=dict(type="str"),
+ ),
+ ),
+ destination=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ group=dict(type="str"),
+ port=dict(type="str"),
+ ),
+ ),
+ icmp=dict(
+ type="dict",
+ options=dict(
+ type=dict(type="int"),
+ code=dict(type="int"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or []
+
+ have = get_running_config(vyos)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+
+ commands = build_commands(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()
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=get_running_config(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py
new file mode 100644
index 0000000..8ccb148
--- /dev/null
+++ b/plugins/modules/vyos_user.py
@@ -0,0 +1,302 @@
+#!/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 user accounts on VyOS devices using REST API
+description:
+ - Manages local user accounts on VyOS devices via the REST API.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+ - Passwords are write-only. Once set, they cannot be read back in plaintext.
+ - Use C(update_password=on_create) to avoid resetting passwords on every run.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ users:
+ description: List of user definitions.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Username.
+ type: str
+ required: true
+ full_name:
+ description: Full name of the user.
+ type: str
+ password:
+ description: Plaintext password. Write-only — hashed on device immediately.
+ type: str
+ update_password:
+ description:
+ - Control when password is updated.
+ - C(always) updates the password on every run (default).
+ - C(on_create) only sets the password when the user is first created.
+ type: str
+ choices: [always, on_create]
+ default: always
+ public_keys:
+ description: SSH public keys for the user.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Key identifier/name.
+ type: str
+ required: true
+ key:
+ description: Base64-encoded public key.
+ type: str
+ required: true
+ type:
+ description: Key type.
+ type: str
+ choices: [ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384,
+ ecdsa-sha2-nistp521, ssh-ed25519]
+ required: true
+ state:
+ description:
+ - C(present) ensures users exist with the specified configuration.
+ - C(absent) removes specified users.
+ - C(gathered) returns current user configuration as structured data.
+ type: str
+ choices: [present, absent, gathered]
+ default: present
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+ - The C(vyos) user cannot be deleted as it is required for API access.
+ - Passwords are hashed immediately by VyOS and cannot be read back.
+"""
+
+EXAMPLES = r"""
+- name: Create user
+ vyos.rest.vyos_user:
+ users:
+ - name: alice
+ full_name: Alice Smith
+ password: securepassword
+ update_password: on_create
+ state: present
+
+- name: Add SSH public key
+ vyos.rest.vyos_user:
+ users:
+ - name: alice
+ public_keys:
+ - name: alice-laptop
+ type: ssh-rsa
+ key: AAAAB3NzaC1yc2EAAAADAQABAAAB...
+ state: present
+
+- name: Delete user
+ vyos.rest.vyos_user:
+ users:
+ - name: alice
+ state: absent
+
+- name: Gather all users
+ vyos.rest.vyos_user:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: User configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: User 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 user 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 import VyOSModule
+
+
+_BASE = ["system", "login", "user"]
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw or not isinstance(raw, dict):
+ return []
+ raw = raw.get("user", raw)
+ result = []
+ for username, data in sorted(raw.items()):
+ user = {"name": username}
+ data = data or {}
+ if data.get("full-name"):
+ user["full_name"] = data["full-name"]
+ auth = data.get("authentication", {}) or {}
+ if auth.get("encrypted-password"):
+ user["encrypted_password"] = auth["encrypted-password"]
+ pub_keys = auth.get("public-keys", {}) or {}
+ if pub_keys and isinstance(pub_keys, dict):
+ keys = []
+ for key_name, key_data in sorted(pub_keys.items()):
+ key_data = key_data or {}
+ k = {"name": key_name}
+ if key_data.get("key"):
+ k["key"] = key_data["key"]
+ if key_data.get("type"):
+ k["type"] = key_data["type"]
+ keys.append(k)
+ if keys:
+ user["public_keys"] = keys
+ result.append(user)
+ return result
+
+
+def build_commands(users, have_list, state):
+ cmds = []
+ have_map = {u["name"]: u for u in have_list}
+
+ if state == "absent":
+ for user in users:
+ name = user["name"]
+ if name in have_map:
+ cmds.append(("delete", _BASE + [name]))
+ return cmds
+
+ # state == "present"
+ for user in users:
+ name = user["name"]
+ have = have_map.get(name, {})
+ ubase = _BASE + [name]
+ is_new = name not in have_map
+
+ # full_name
+ if user.get("full_name") and user["full_name"] != have.get("full_name"):
+ cmds.append(("set", ubase + ["full-name", user["full_name"]]))
+
+ # password
+ if user.get("password"):
+ update_pw = user.get("update_password", "always")
+ if update_pw == "always" or is_new:
+ cmds.append(
+ (
+ "set",
+ ubase
+ + [
+ "authentication",
+ "plaintext-password",
+ user["password"],
+ ],
+ ),
+ )
+
+ # public_keys
+ want_keys = {k["name"]: k for k in (user.get("public_keys") or [])}
+ have_keys = {k["name"]: k for k in (have.get("public_keys") or [])}
+ for key_name, key_data in want_keys.items():
+ have_key = have_keys.get(key_name, {})
+ kbase = ubase + ["authentication", "public-keys", key_name]
+ if key_data.get("key") and key_data["key"] != have_key.get("key"):
+ cmds.append(("set", kbase + ["key", key_data["key"]]))
+ if key_data.get("type") and key_data["type"] != have_key.get("type"):
+ cmds.append(("set", kbase + ["type", key_data["type"]]))
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ users=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ full_name=dict(type="str"),
+ password=dict(type="str", no_log=True),
+ update_password=dict(
+ type="str",
+ choices=["always", "on_create"],
+ default="always",
+ ),
+ public_keys=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ key=dict(type="str", required=True, no_log=True),
+ type=dict(
+ type="str",
+ required=True,
+ choices=[
+ "ssh-dss",
+ "ssh-rsa",
+ "ecdsa-sha2-nistp256",
+ "ecdsa-sha2-nistp384",
+ "ecdsa-sha2-nistp521",
+ "ssh-ed25519",
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ default="present",
+ choices=["present", "absent", "gathered"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ users = module.params.get("users") or []
+
+ have = get_running_config(vyos)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+
+ commands = build_commands(users, 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()
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=get_running_config(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()