summaryrefslogtreecommitdiff
path: root/plugins/module_utils
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/module_utils')
-rw-r--r--plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py75
-rw-r--r--plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py325
-rw-r--r--plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py126
3 files changed, 506 insertions, 20 deletions
diff --git a/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py
index aeef42bc..dd17ef85 100644
--- a/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py
+++ b/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py
@@ -4,23 +4,6 @@
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-#############################################
-# WARNING #
-#############################################
-#
-# This file is auto generated by the resource
-# module builder playbook.
-#
-# Do not edit this file manually.
-#
-# Changes to this file will be over written
-# by the resource module builder.
-#
-# Changes should be made in the model used to
-# generate this file or in the resource module
-# builder template.
-#
-#############################################
"""
The arg spec for the vyos_firewall_global module
"""
@@ -185,6 +168,64 @@ class Firewall_globalArgs(object): # pylint: disable=R0903
],
"type": "str",
},
+ "zone": {
+ "elements": "dict",
+ "type": "list",
+ "options": {
+ "description": {"type": "str"},
+ "default_action": {
+ "choices": [
+ "drop",
+ "reject",
+ ],
+ "default": "drop",
+ "type": "str",
+ },
+ "default_log": {"type": "bool"},
+ "interfaces": {
+ "elements": "str",
+ "type": "list",
+ },
+ "local_zone": {"type": "bool"},
+ "name": {
+ "required": True,
+ "type": "str",
+ },
+ "intra_zone_filtering": {
+ "type": "dict",
+ "options": {
+ "action": {
+ "choices": ["accept", "drop"],
+ "type": "str",
+ },
+ "firewall": {
+ "type": "dict",
+ "options": {
+ "name": {"type": "str"},
+ "ipv6_name": {"type": "str"},
+ },
+ },
+ },
+ },
+ "sources": {
+ "elements": "dict",
+ "type": "list",
+ "options": {
+ "zone": {
+ "required": True,
+ "type": "str",
+ },
+ "firewall": {
+ "type": "dict",
+ "options": {
+ "name": {"type": "str"},
+ "ipv6_name": {"type": "str"},
+ },
+ },
+ },
+ },
+ },
+ },
},
"type": "dict",
},
diff --git a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py
index fcacb913..289037ef 100644
--- a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py
+++ b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py
@@ -241,6 +241,8 @@ class Firewall_global(ConfigBase):
commands.extend(self._render_state_policy(key, w, h, opr=opr))
elif key == "route_redirects":
commands.extend(self._render_route_redirects(key, w, h, opr=opr))
+ elif key == "zone":
+ commands.extend(self._render_zone(key, w, h, opr=opr))
return commands
def _add_global_attr(self, w, h, opr=True):
@@ -692,7 +694,7 @@ class Firewall_global(ConfigBase):
cmd = "set firewall "
if (
attr
- and key != "group"
+ and key not in ["group", "zone"]
and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4")
):
cmd += "global-options "
@@ -810,3 +812,324 @@ class Firewall_global(ConfigBase):
elif attrib == "validation":
regex = "source-validation"
return regex
+
+ def _render_zone(self, attr, w, h, opr):
+ """
+ This function forms the commands for group list/members attributes based on the 'opr'.
+ :param attr: attribute name.
+ :param w: the desired config.
+ :param h: the target config.
+ :param opr: True/False.
+ :return: generated list of commands.
+ """
+ commands = []
+ h_grp = []
+ w_grp = []
+ l_set = ("name", "description", "default_action", "default_log", "local_zone")
+ if w:
+ w_grp = w.get(attr) or []
+ if h:
+ h_grp = h.get(attr) or []
+
+ if w_grp:
+ for want in w_grp:
+ h = self.search_attrib_in_have(h_grp, want, "name")
+
+ cmd = self._compute_command(key="zone", attr="", opr=opr)
+
+ if not opr and self._is_grp_del(h, want, "name"):
+ commands.append(cmd + " " + want["name"])
+ continue
+
+ for key, val in want.items():
+ if val:
+ if opr and key in l_set and not (h and self._is_w_same(want, h, key)):
+ if key == "name":
+ pass
+ elif isinstance(val, bool):
+ commands.append(
+ cmd + " " + want["name"] + " " + key.replace("_", "-"),
+ )
+ else:
+ commands.append(
+ cmd
+ + " "
+ + want["name"]
+ + " "
+ + key.replace("_", "-")
+ + " '"
+ + str(want[key])
+ + "'",
+ )
+ elif not opr and key in l_set:
+ if not (h and in_target_not_none(h, key)) and not self._is_grp_del(
+ h,
+ want,
+ "name",
+ ):
+ commands.append(
+ cmd + " " + want["name"] + " " + key.replace("_", "-"),
+ )
+ elif key == "interfaces":
+ commands.extend(
+ self._render_interfaces(
+ key,
+ want,
+ h,
+ opr,
+ cmd,
+ want["name"],
+ attr,
+ ),
+ )
+ elif key == "intra_zone_filtering":
+ commands.extend(
+ self._render_izf(
+ key,
+ want,
+ h,
+ opr,
+ cmd,
+ want["name"],
+ attr,
+ ),
+ )
+ elif key == "sources":
+ commands.extend(
+ self._render_sources(
+ key,
+ want,
+ h,
+ opr,
+ cmd,
+ want["name"],
+ attr,
+ ),
+ )
+ return commands
+
+ def _render_interfaces(self, attr, w, h, opr, cmd, name, type):
+ """
+ This function forms the commands for interfaces
+ based on the 'opr'.
+ :param attr: attribute name.
+ :param w: the desired config.
+ :param h: the target config.
+ :param cmd: commands to be prepend.
+ :param name: name of group.
+ :param type: group type.
+ :return: generated list of commands.
+ """
+ commands = []
+ have = []
+ if w:
+ want = w.get(attr) or []
+ if h:
+ have = h.get(attr) or []
+
+ # VyOS 1.5.0 GA moved 'interface' under a new 'member' node
+ # ("set firewall zone <name> member interface <ifname>"). 1.4.x and
+ # 1.5-rolling snapshots predating this change still use the bare
+ # 'interface' node. Known limitation: a 1.5-rolling build reporting
+ # "1.5" that predates this change will incorrectly get the new
+ # syntax -- accepted trade-off, see PR notes.
+ if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.5"):
+ iface_kw = "member interface"
+ else:
+ iface_kw = "interface"
+
+ if want:
+ if opr:
+ interfaces = list_diff_want_only(want, have)
+
+ for interface in interfaces:
+ commands.append(
+ cmd + " " + name + " " + iface_kw + " " + interface,
+ )
+ elif not opr and have:
+ interfaces = list_diff_want_only(want, have)
+ for interface in interfaces:
+ commands.append(
+ cmd + " " + name + " " + iface_kw + " " + interface,
+ )
+ elif not opr and not have:
+ for interface in want:
+ commands.append(
+ cmd + " " + name + " " + iface_kw + " " + interface,
+ )
+ else:
+ self._module.fail_json(msg={"want": want, "have": have, "opr": opr})
+
+ return commands
+
+ def _render_izf(self, attr, w, h, opr, cmd, name, type):
+ """
+ This function forms the commands for intra zone filtering
+ based on the 'opr'.
+ :param attr: attribute name.
+ :param w: the desired config.
+ :param h: the target config.
+ :param cmd: commands to be prepend.
+ :param name: name of group.
+ :param type: group type.
+ :return: generated list of commands.
+ """
+ commands = []
+ have = []
+ if w:
+ want = w.get(attr) or []
+ if h:
+ have = h.get(attr) or []
+
+ if want:
+ if opr:
+ izfs = self._dict_diff(want, have)
+ for izf in izfs:
+ commands.append(
+ cmd
+ + " "
+ + name
+ + " intra-zone-filtering "
+ + izf[0].replace(".", " ")
+ + " "
+ + izf[1],
+ )
+ elif not opr and have:
+ izfs = self._dict_diff(want, have)
+
+ for izf in izfs:
+ commands.append(
+ cmd + " " + name + " intra-zone-filtering " + izf[0].replace(".", " "),
+ )
+ elif not opr and not have:
+ commands.append(
+ cmd + " " + name + " intra-zone-filtering",
+ )
+ return commands
+
+ def _dict_diff(self, want, have, path=""):
+ """
+ Recursively find keys/values in `want` that differ or are missing in `have`.
+ Returns list of tuples: (full_path, value_in_want)
+ """
+ diffs = []
+
+ have = have or {}
+
+ for key, want_val in want.items():
+ current_path = f"{path}.{key.replace('_', '-')}" if path else key.replace("_", "-")
+
+ if key not in have:
+ if isinstance(want_val, dict):
+ diffs.extend(self._dict_diff(want_val, {}, current_path))
+ elif isinstance(want_val, list):
+ for i, item in enumerate(want_val):
+ if isinstance(item, dict):
+ diffs.extend(self._dict_diff(item, {}, f"{current_path}[{i}]"))
+ else:
+ diffs.append((f"{current_path}[{i}]", item))
+ else:
+ diffs.append((current_path, want_val))
+
+ else:
+ have_val = have[key]
+
+ if isinstance(want_val, dict) and isinstance(have_val, dict):
+ diffs.extend(self._dict_diff(want_val, have_val, current_path))
+
+ elif isinstance(want_val, list) and isinstance(have_val, list):
+ for i, item in enumerate(want_val):
+ if i >= len(have_val):
+ diffs.append((f"{current_path}[{i}]", item))
+ elif isinstance(item, dict) and isinstance(have_val[i], dict):
+ diffs.extend(
+ self._dict_diff(item, have_val[i], f"{current_path}[{i}]"),
+ )
+ elif item != have_val[i]:
+ diffs.append((f"{current_path}[{i}]", item))
+
+ elif want_val != have_val:
+ diffs.append((current_path, want_val))
+
+ return diffs
+
+ def _render_sources(self, attr, w, h, opr, cmd, name, type):
+ """
+ This function forms the commands for sources (from)
+ based on the 'opr'.
+ :param attr: attribute name.
+ :param w: the desired config.
+ :param h: the target config.
+ :param cmd: commands to be prepend.
+ :param name: name of group.
+ :param type: group type.
+ :return: generated list of commands.
+ """
+ commands = []
+ have = []
+ if w:
+ want = w.get(attr) or []
+ if h:
+ have = h.get(attr) or []
+
+ have_index = {item["zone"]: item for item in have}
+
+ for item1 in want:
+ zone = item1["zone"]
+
+ if zone in have_index:
+ item2 = have_index[zone]
+
+ wfw = item1.get("firewall", {})
+ hfw = item2.get("firewall", {})
+ if wfw:
+ if opr:
+ sources = self._dict_diff(wfw, hfw)
+ for source in sources:
+ commands.append(
+ cmd
+ + " "
+ + name
+ + " from "
+ + zone
+ + " firewall "
+ + source[0].replace("_", "-")
+ + " "
+ + source[1],
+ )
+ elif not opr and hfw:
+ sources = self._dict_diff(wfw, hfw)
+ for source in sources:
+ commands.append(
+ cmd
+ + " "
+ + name
+ + " from "
+ + zone
+ + " firewall "
+ + source[0].replace("_", "-"),
+ )
+ elif not opr and not hfw:
+ commands.append(
+ cmd + " " + name + " from " + zone,
+ )
+ elif opr:
+ wfw = item1.get("firewall", {})
+ for key, val in wfw.items():
+ if val:
+ commands.append(
+ cmd
+ + " "
+ + name
+ + " from "
+ + zone
+ + " firewall "
+ + key.replace("_", "-")
+ + " "
+ + val,
+ )
+ elif not opr:
+ commands.append(
+ cmd + " " + name + " from " + zone,
+ )
+ return commands
diff --git a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py
index c74e63d3..e13c1939 100644
--- a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py
+++ b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py
@@ -77,13 +77,17 @@ class Firewall_globalFacts(object):
:rtype: dictionary
:returns: The generated config
"""
+
conf = "\n".join(
filter(
- lambda x: ("firewall ipv6-name" and "firewall name" not in x),
+ lambda x: not (
+ x.startswith("set firewall name")
+ or x.startswith("set firewall ipv6-name")
+ or x.startswith("set firewall ipv6 name")
+ ),
conf,
),
)
-
a_lst = [
"config_trap",
"validation",
@@ -97,6 +101,7 @@ class Firewall_globalFacts(object):
"group": self.parse_group(conf),
"route_redirects": self.route_redirects(conf),
"state_policy": self.parse_state_policy(conf),
+ "zone": self.parse_zone(conf),
}
firewall.update(f_sub)
return firewall
@@ -400,3 +405,120 @@ class Firewall_globalFacts(object):
"twa_hazards_protection",
)
return True if attrib in bool_set else False
+
+ def parse_zone(self, conf):
+ """
+ This function triggers the parsing of 'zone' attributes.
+ :param conf: configuration.
+ :return: generated config dictionary.
+ """
+ cfg_dict = {}
+
+ KEY_MAP = {
+ "interface": "interfaces",
+ "intra-zone-filtering": "intra-zone-filtering",
+ "from": "sources",
+ }
+
+ LIST_ATTRS = {
+ "interfaces",
+ "intra_zone_filtering",
+ "sources",
+ }
+
+ for line in conf.splitlines():
+
+ m = search(
+ r"^set firewall zone (?P<zone>\S+)\s+(?P<attr>[a-z-]+)(?:\s+(?P<value>'[^']+'|[^\n]+))?$",
+ line,
+ )
+ if not m:
+ continue
+
+ zone_name = m.group("zone")
+ raw_attr = m.group("attr").replace("-", "_")
+ value = m.group("value")
+
+ if value is None:
+ value = True
+ else:
+ value = value.strip("'")
+
+ # VyOS 1.5.0 GA wraps 'interface' under a new 'member' node:
+ # "set firewall zone <name> member interface <ifname>". Unwrap
+ # it here so it lands in the same 'interfaces' list as the
+ # pre-1.5.0 bare "interface <ifname>" form. No version check
+ # needed -- 1.4.x/1.5-rolling configs never emit 'member'.
+ if raw_attr == "member" and isinstance(value, str) and value.startswith("interface "):
+ raw_attr = "interface"
+ value = value.split(None, 1)[1].strip("'")
+
+ zone = cfg_dict.setdefault(zone_name, {"name": zone_name})
+
+ attr = KEY_MAP.get(raw_attr, raw_attr)
+
+ if attr in LIST_ATTRS:
+ if attr == "intra_zone_filtering":
+ izf = zone.setdefault(attr, {})
+ izf_attr = self._parse_izf(value)
+ for k, v in izf_attr.items():
+ if isinstance(v, dict):
+ izf.setdefault(k, {}).update(v)
+ else:
+ izf[k] = v
+ elif attr == "sources":
+ self._parse_sources(zone, value)
+ else:
+ zone.setdefault(attr, []).append(value)
+ else:
+ zone[attr] = value
+
+ return list(cfg_dict.values())
+
+ def _parse_izf(self, value):
+
+ tokens = value.replace("'", "").split()
+
+ result = {}
+
+ key = tokens[0].replace("-", "_")
+
+ if len(tokens) == 2:
+ result[key] = tokens[1]
+
+ elif len(tokens) >= 3:
+ subkey = tokens[1].replace("-", "_")
+ result[key] = {subkey: tokens[2]}
+
+ return result
+
+ def _parse_sources(self, zone, value):
+
+ tokens = value.split()
+
+ if len(tokens) < 1:
+ return
+
+ src_zone = tokens[0]
+
+ sources = zone.setdefault("sources", [])
+
+ entry = None
+ for s in sources:
+ if s.get("zone") == src_zone:
+ entry = s
+ break
+
+ if entry is None:
+ entry = {"zone": src_zone}
+ sources.append(entry)
+
+ if len(tokens) == 1:
+ return
+
+ if tokens[1] == "firewall" and len(tokens) >= 4:
+ key = tokens[2].replace("-", "_")
+ val = tokens[3].strip("'")
+
+ firewall = entry.setdefault("firewall", {})
+ firewall[key] = val