Ansible-Roles/roles/firewall/module_utils/firewall_lsr/get_config.py
2026-05-08 22:12:36 +03:00

1134 lines
46 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright (C) 2016 - 2026 Red Hat, Inc.
# Reusing some firewalld code
# Authors:
# Brennan Paciorek <bpaciore@redhat.com>
# Support for the InMemoryBackend, processing multiple configurations in a
# single module call, reading config from xml, and doing config diffs, was added
# by Claude 4.6 opus using Cursor, and reviewed and tweaked by Richard Megginson
# <rmeggins@redhat.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, division, print_function, unicode_literals
__metaclass__ = type
import copy
import os
import sys
try:
import firewall.config
from firewall.client import FirewallClient
# firewall.core.io modules needed for xml file reading
from firewall.core.io.zone import zone_reader
from firewall.core.io.service import service_reader
from firewall.core.io.icmptype import icmptype_reader
from firewall.core.io.ipset import ipset_reader
from firewall.core.io.helper import helper_reader
from firewall.core.io.firewalld_conf import firewalld_conf
HAS_FIREWALLD = True
FALLBACK_ZONE = firewall.config.FALLBACK_ZONE
except ImportError:
HAS_FIREWALLD = False
FALLBACK_ZONE = "public"
try:
if HAS_FIREWALLD:
firewall.config.FIREWALLD_POLICIES
from firewall.core.io.policy import policy_reader
HAS_POLICIES = True
except AttributeError:
HAS_POLICIES = False
except ImportError:
HAS_POLICIES = False
def offline_cmd(module, args, defaults=False):
# get the defaults by disabling the --system-config dir (ETC_FIREWALLD)
conf_args = ["--system-config=/nonexisting"] if defaults else []
cmd = ["firewall-offline-cmd"] + conf_args + args
rc, out, err = module.run_command(cmd)
if rc != 0:
if (
rc == 1
and "No such file or directory: '/nonexisting/firewalld.conf'" in err
):
pass
else:
module.fail_json(
msg="Failed to execute firewall-offline-cmd: cmd [%s] rc [%s] out [%s] err [%s]"
% (cmd.join(" "), rc, out, err)
)
return out.strip()
# EL7 does not have this method, so make our own
def export_config_dict(io_object):
if HAS_FIREWALLD:
try:
object_dict = io_object.export_config_dict()
except AttributeError:
object_dict = {}
for key, unused_value in io_object.IMPORT_EXPORT_STRUCTURE:
if hasattr(io_object, key) and key != "UNUSED":
object_dict[key] = getattr(io_object, key)
if isinstance(io_object, firewall.core.io.zone.Zone):
if object_dict.get("target") == firewall.core.base.DEFAULT_ZONE_TARGET:
# to correspond with online getTarget()
object_dict["target"] = "default"
if object_dict.get("forward_ports"):
object_dict["forward_ports"] = normalize_forward_ports(
object_dict["forward_ports"]
)
return object_dict
else:
return {}
def normalize_settings(settings):
# normalize the settings to remove empty values and set default values
# this duplicates the logic in core/fw_zone.py:get_config_with_settings_dict()
for kk, vv in list(settings.items()):
if kk == "target" and vv == firewall.core.base.DEFAULT_ZONE_TARGET:
settings[kk] = "default"
elif vv or isinstance(vv, bool) or isinstance(vv, int):
pass
else:
# remove the key if the value is empty
del settings[kk]
return settings
# not used - this method is extremely slow, but kept for reference
def fetch_settings_using_offline_cmd(module, setting_name):
"""
Fetch firewall settings using firewall-offline-cmd.
This function retrieves firewall configuration using the offline command interface,
which can query either default settings or custom permanent settings.
Args:
module: Ansible module object (for running commands)
setting_name: Type of setting to fetch ('zones', 'services', 'icmptypes',
'helpers', 'ipsets', 'policies')
Returns:
Dictionary with object names as keys and their settings as values
"""
# Get list of items for this setting type
setting_options = offline_cmd(
module, ["--get-" + setting_name], defaults=True
).split()
# Get detailed information for each item
settings = {}
for item in setting_options:
element_settings = {}
try:
if setting_name == "zones":
# Get zone details
element_settings["services"] = offline_cmd(
module, ["--zone=" + item, "--list-services"], defaults=True
).split()
element_settings["ports"] = [
tuple(p.split("/"))
for p in offline_cmd(
module, ["--zone=" + item, "--list-ports"], defaults=True
).split()
]
element_settings["protocols"] = offline_cmd(
module, ["--zone=" + item, "--list-protocols"], defaults=True
).split()
element_settings["source_ports"] = [
tuple(p.split("/"))
for p in offline_cmd(
module, ["--zone=" + item, "--list-source-ports"], defaults=True
).split()
]
element_settings["icmp_blocks"] = offline_cmd(
module, ["--zone=" + item, "--list-icmp-blocks"], defaults=True
).split()
element_settings["forward_ports"] = offline_cmd(
module, ["--zone=" + item, "--list-forward-ports"], defaults=True
).split()
element_settings["interfaces"] = offline_cmd(
module, ["--zone=" + item, "--list-interfaces"], defaults=True
).split()
element_settings["sources"] = offline_cmd(
module, ["--zone=" + item, "--list-sources"], defaults=True
).split()
element_settings["rules_str"] = (
offline_cmd(
module, ["--zone=" + item, "--list-rich-rules"], defaults=True
).split("\n")
if offline_cmd(
module, ["--zone=" + item, "--list-rich-rules"], defaults=True
)
else []
)
# Query masquerade (returns yes/no or error)
try:
masq_result = offline_cmd(
module, ["--zone=" + item, "--query-masquerade"], defaults=True
)
element_settings["masquerade"] = masq_result == "yes"
except Exception:
pass # just omit the masquerade setting if it's not available
# Get target
try:
element_settings["target"] = offline_cmd(
module, ["--zone=" + item, "--get-target"], defaults=True
)
except Exception:
pass # just omit the target setting if it's not available
# Get description and short
try:
element_settings["description"] = offline_cmd(
module, ["--zone=" + item, "--get-description"], defaults=True
)
except Exception:
pass # just omit the description setting if it's not available
try:
element_settings["short"] = offline_cmd(
module, ["--zone=" + item, "--get-short"], defaults=True
)
except Exception:
pass # just omit the short setting if it's not available
elif setting_name == "services":
# Get service details
ports_output = offline_cmd(
module, ["--service=" + item, "--get-ports"], defaults=True
)
element_settings["ports"] = (
[tuple(p.split("/")) for p in ports_output.split()]
if ports_output
else []
)
element_settings["protocols"] = offline_cmd(
module, ["--service=" + item, "--get-protocols"], defaults=True
).split()
modules_output = offline_cmd(
module, ["--service=" + item, "--get-modules"], defaults=True
)
element_settings["modules"] = (
modules_output.split() if modules_output else []
)
source_ports_output = offline_cmd(
module, ["--service=" + item, "--get-source-ports"], defaults=True
)
element_settings["source_ports"] = (
[tuple(p.split("/")) for p in source_ports_output.split()]
if source_ports_output
else []
)
try:
element_settings["description"] = offline_cmd(
module,
["--service=" + item, "--get-description"],
defaults=True,
)
except Exception:
pass # just omit the description setting if it's not available
try:
element_settings["short"] = offline_cmd(
module, ["--service=" + item, "--get-short"], defaults=True
)
except Exception:
pass # just omit the short setting if it's not available
elif setting_name == "icmptypes":
# Get icmptype details
dest_output = offline_cmd(
module, ["--icmptype=" + item, "--get-destinations"], defaults=True
)
element_settings["destination"] = (
dest_output.split() if dest_output else []
)
try:
element_settings["description"] = offline_cmd(
module,
["--icmptype=" + item, "--get-description"],
defaults=True,
)
except Exception:
pass # just omit the description setting if it's not available
try:
element_settings["short"] = offline_cmd(
module, ["--icmptype=" + item, "--get-short"], defaults=True
)
except Exception:
pass # just omit the short setting if it's not available
elif setting_name == "helpers":
# Get helper details
element_settings["family"] = offline_cmd(
module, ["--helper=" + item, "--get-family"], defaults=True
)
element_settings["module"] = offline_cmd(
module, ["--helper=" + item, "--get-module"], defaults=True
)
ports_output = offline_cmd(
module, ["--helper=" + item, "--get-ports"], defaults=True
)
element_settings["ports"] = (
[tuple(p.split("/")) for p in ports_output.split()]
if ports_output
else []
)
try:
element_settings["description"] = offline_cmd(
module, ["--helper=" + item, "--get-description"], defaults=True
)
except Exception:
pass # just omit the description setting if it's not available
try:
element_settings["short"] = offline_cmd(
module, ["--helper=" + item, "--get-short"], defaults=True
)
except Exception:
pass # just omit the short setting if it's not available
elif setting_name == "ipsets":
# Get ipset details
element_settings["type"] = offline_cmd(
module, ["--ipset=" + item, "--get-type"], defaults=True
)
entries_output = offline_cmd(
module, ["--ipset=" + item, "--get-entries"], defaults=True
)
element_settings["entries"] = (
entries_output.split("\n") if entries_output else []
)
try:
element_settings["description"] = offline_cmd(
module, ["--ipset=" + item, "--get-description"], defaults=True
)
except Exception:
pass # just omit the description setting if it's not available
try:
element_settings["short"] = offline_cmd(
module, ["--ipset=" + item, "--get-short"], defaults=True
)
except Exception:
pass # just omit the short setting if it's not available
# Get options if available
try:
options_output = offline_cmd(
module, ["--ipset=" + item, "--get-options"], defaults=True
)
element_settings["options"] = (
options_output.split() if options_output else []
)
except Exception:
pass # just omit the options setting if it's not available
elif setting_name == "policies":
# Get policy details (similar to zones)
element_settings["services"] = offline_cmd(
module, ["--policy=" + item, "--list-services"], defaults=True
).split()
element_settings["ports"] = [
tuple(p.split("/"))
for p in offline_cmd(
module, ["--policy=" + item, "--list-ports"], defaults=True
).split()
]
element_settings["protocols"] = offline_cmd(
module, ["--policy=" + item, "--list-protocols"], defaults=True
).split()
element_settings["icmp_blocks"] = offline_cmd(
module, ["--policy=" + item, "--list-icmp-blocks"], defaults=True
).split()
element_settings["forward_ports"] = offline_cmd(
module, ["--policy=" + item, "--list-forward-ports"], defaults=True
).split()
element_settings["rules_str"] = (
offline_cmd(
module, ["--policy=" + item, "--list-rich-rules"], defaults=True
).split("\n")
if offline_cmd(
module, ["--policy=" + item, "--list-rich-rules"], defaults=True
)
else []
)
# Get ingress/egress zones
try:
element_settings["ingress_zones"] = offline_cmd(
module,
["--policy=" + item, "--list-ingress-zones"],
defaults=True,
).split()
except Exception:
pass # just omit the ingress_zones setting if it's not available
try:
element_settings["egress_zones"] = offline_cmd(
module,
["--policy=" + item, "--list-egress-zones"],
defaults=True,
).split()
except Exception:
pass # just omit the egress_zones setting if it's not available
try:
element_settings["target"] = offline_cmd(
module, ["--policy=" + item, "--get-target"], defaults=True
)
except Exception:
pass # just omit the target setting if it's not available
try:
element_settings["description"] = offline_cmd(
module, ["--policy=" + item, "--get-description"], defaults=True
)
except Exception:
pass # just omit the description setting if it's not available
try:
element_settings["short"] = offline_cmd(
module, ["--policy=" + item, "--get-short"], defaults=True
)
except Exception:
pass # just omit the short setting if it's not available
settings[item] = normalize_settings(element_settings)
except Exception as e:
# If we can't get details for an item, log warning and skip
module.warn(
"Failed to get details for "
+ setting_name
+ " '"
+ item
+ "': "
+ str(e)
)
continue
return settings
def config_to_dict(module, detailed=None, online=None):
if detailed is None:
detailed = module.params.get("detailed", False)
if online is None:
online = module.params.get("online", True)
config = {}
defaults = {}
custom_permanent = {}
setting_list = ["zones", "services", "icmptypes", "helpers", "ipsets"]
if HAS_POLICIES:
setting_list.append("policies")
defaults = fetch_settings_from_xml_files(module, setting_list, defaults=True)
config["default"] = defaults
custom_permanent = fetch_settings_from_xml_files(
module, setting_list, defaults=False
)
custom_permanent_with_defaults = merge_with_defaults(custom_permanent, defaults)
config["custom_permanent_with_defaults"] = custom_permanent_with_defaults
if custom_permanent:
config["custom_permanent"] = custom_permanent
config["custom"] = custom_permanent # legacy compatibility
# this is the built-in default zone if there is no firewalld.conf
config["fallback_default_zone"] = FALLBACK_ZONE
# get firewalld.conf settings
fc = firewalld_conf(firewall.config.FIREWALLD_CONF)
fc.read()
config["firewalld_conf"] = {"allow_zone_drifting": fc.get("AllowZoneDrifting")}
if online:
fw = FirewallClient()
current_settings = fetch_online_settings(fw, setting_list, detailed)
# NOTE: In some cases, the current settings may not include the default settings read from the XML files,
# for example, the icmptype beyond-scope cannot be loaded by the firewalld server because it is not
# supported by the kernel, so it will not show up in the online list of icmptypes. Rather than
# trying to duplicate that logic here, and only keep the list of supported icmptypes, we merge the
# the current settings with the defaults to get the full list of settings.
current_settings_with_defaults = merge_with_defaults(current_settings, defaults)
config["current"] = current_settings_with_defaults
config["custom_runtime_with_defaults"] = current_settings_with_defaults
config["default_zone"] = fw.getDefaultZone()
# the current settings include the custom permanent with defaults settings, so we need to diff them to get the custom runtime settings
dict_diff_normalizers = DEFAULT_NORMALIZERS
custom_runtime = recursive_dict_diff(
current_settings_with_defaults,
custom_permanent_with_defaults,
dict_diff_normalizers,
)
if custom_runtime:
config["runtime_only"] = custom_runtime
else:
config["default_zone"] = offline_cmd(module, ["--get-default-zone"])
return config
def normalize_value(value, normalizers=None):
"""
Normalize a value using provided normalization functions.
Args:
value: The value to normalize
normalizers: Optional dict or callable for normalization
- If callable: applied to the value
- If dict: keys are types or paths, values are normalization functions
Returns:
Normalized value
"""
if normalizers is None:
return value
if callable(normalizers):
return normalizers(value)
# For dict-based normalizers, try type-based lookup
if isinstance(normalizers, dict):
value_type = type(value).__name__
if value_type in normalizers:
return normalizers[value_type](value)
return value
def normalize_list(lst, normalizers=None):
"""
Normalize and sort a list for comparison.
Args:
lst: List to normalize
normalizers: Optional normalization functions
Returns:
Sorted list with normalized values
"""
if not isinstance(lst, list):
return lst
# Normalize each item in the list
normalized = [normalize_value(item, normalizers) for item in lst]
# Sort the list for comparison (handle mixed types carefully)
try:
return sorted(normalized)
except TypeError:
# If items aren't directly comparable, convert to strings for sorting
return sorted(normalized, key=str)
DEFAULT_NORMALIZERS = {
"str": lambda s: s.strip().lower(),
"list": lambda ll: sorted([x.strip() for x in ll]),
}
def recursive_dict_diff(dict1, dict2, normalizers, path=""):
"""
Recursively compare two dictionaries and return only the differences.
This function compares dict1 against dict2 and returns a new dictionary
containing only the keys/values from dict1 that are different from dict2
or don't exist in dict2.
Args:
dict1: First dictionary (the one to extract differences from)
dict2: Second dictionary (the reference to compare against)
normalizers: Optional dict of normalization functions for value comparison
Keys can be:
- Type names (e.g., 'str', 'int', 'list')
- Path patterns (e.g., 'zones.*.services')
Values are callables that normalize the value
path: Internal parameter for tracking the current path in nested dicts
Returns:
Dictionary containing only the differences from dict1
"""
if dict1 is None and dict2 is None:
return None
if dict1 is None:
return None
if dict2 is None:
return dict1
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
# For non-dict values, normalize and compare
norm1 = normalize_value(dict1, normalizers)
norm2 = normalize_value(dict2, normalizers)
if isinstance(dict1, list) and isinstance(dict2, list):
# Compare lists (normalized for comparison)
normalized1 = normalize_list(dict1, normalizers)
normalized2 = normalize_list(dict2, normalizers)
if normalized1 != normalized2:
return list(set(normalized1) - set(normalized2))
return None
elif norm1 != norm2:
return dict1
return None
diff = {}
# Check all keys in dict1
for key in dict1:
current_path = "%s.%s" % (path, key) if path else key
if key not in dict2:
# Key exists in dict1 but not in dict2
diff[key] = dict1[key]
else:
# Key exists in both, compare values
value1 = dict1[key]
value2 = dict2[key]
if isinstance(value1, dict) and isinstance(value2, dict):
# Recursively compare nested dictionaries
nested_diff = recursive_dict_diff(
value1, value2, normalizers, current_path
)
if nested_diff:
diff[key] = nested_diff
elif isinstance(value1, list) and isinstance(value2, list):
# Compare lists (normalized for comparison)
normalized1 = normalize_list(value1, normalizers)
normalized2 = normalize_list(value2, normalizers)
if normalized1 != normalized2:
diff[key] = list(set(normalized1) - set(normalized2))
else:
# Compare scalar values (with normalization)
norm1 = normalize_value(value1, normalizers)
norm2 = normalize_value(value2, normalizers)
if norm1 != norm2:
diff[key] = value1
return diff if diff else None
def _sequence_element_diff(seq1, seq2, normalizers):
"""
For two list or tuple values, return elements present only in seq1 (removed)
and only in seq2 (added), using multiset semantics on normalized items.
Returns:
(removed_items, added_items) using list or tuple to match inputs, or
None if the values are not both non-string sequences or if unhashable.
"""
if not isinstance(seq1, (list, tuple)) or not isinstance(seq2, (list, tuple)):
return None
# We want to compare normalized values, but return the original values.
norm_seq1 = normalize_list(seq1, normalizers)
norm_seq2 = normalize_list(seq2, normalizers)
# The removed list are items in the first sequence that are not in the second sequence.
rem_list = [ii for ii in seq1 if normalize_value(ii, normalizers) not in norm_seq2]
# The added list are items in the second sequence that are not in the first sequence.
add_list = [ii for ii in seq2 if normalize_value(ii, normalizers) not in norm_seq1]
if isinstance(seq1, tuple):
rem_list = tuple(rem_list)
if isinstance(seq2, tuple):
add_list = tuple(add_list)
return (rem_list, add_list)
def recursive_show_diffs(dict1, dict2, normalizers, ignore_interface=False, path=""):
"""
Recursively compare two dictionaries and return removed/added differences.
For dictionary keys, 'removed' contains subtrees or values that exist in
dict1 but not dict2 (missing keys or list/tuple entries only in dict1),
and 'added' contains those that exist in dict2 but not dict1. For the same
key with different scalar values, the old value appears under 'removed' and
the new under 'added'.
Args:
dict1: First dictionary (the original state)
dict2: Second dictionary (the new state)
normalizers: Optional dict of normalization functions for value comparison
Keys can be:
- Type names (e.g., 'str', 'int', 'list')
- Path patterns (e.g., 'zones.*.services')
Values are callables that normalize the value
ignore_interface: If True, ignore changes to the interface list
path: Internal parameter for tracking the current path in nested dicts
Returns:
Dictionary with 'removed' and 'added' keys containing the differences,
or None if no differences found
"""
# If ignore_interface is True, we need to ignore changes to the interface list.
# This is because the interface list is not part of the firewall configuration,
# it is managed by the NetworkManager, and the InMemoryBackend has no way to
# know if a change to the interface list is actually a change to the firewall configuration.
# we are lucky because the only key named "interface" is in the zone configuration. If
# in the future there is another key named "interface" in some other object or at some
# other level, we will need to modify this code to use path e.g. "zones.*.interface".
if dict1 is None and dict2 is None:
return None
if dict1 is None and dict2 is not None:
return {"removed": None, "added": dict2}
if dict2 is None and dict1 is not None:
return {"removed": dict1, "added": None}
if not isinstance(dict1, dict) or not isinstance(dict2, dict):
seq_diff = _sequence_element_diff(dict1, dict2, normalizers)
if seq_diff is not None:
rem, add = seq_diff
return {"removed": rem, "added": add}
norm1 = normalize_value(dict1, normalizers)
norm2 = normalize_value(dict2, normalizers)
if norm1 != norm2:
return {"removed": dict1, "added": dict2}
return None
removed = {}
added = {}
# Check all keys in dict1
for key in dict1:
if ignore_interface and key == "interfaces":
continue
current_path = "%s.%s" % (path, key) if path else key
if key not in dict2:
if dict1[key] in [None, [], {}, ""]:
pass # missing key is same as empty value
else:
# Key exists in dict1 but not in dict2
removed[key] = dict1[key]
elif key == "forward":
pass # we don't allow setting forward in zones, so we don't compare it
else:
# Key exists in both, compare values
value1 = dict1[key]
value2 = dict2[key]
if isinstance(value1, dict) and isinstance(value2, dict):
# Recursively compare nested dictionaries
nested_result = recursive_show_diffs(
value1,
value2,
normalizers,
ignore_interface=ignore_interface,
path=current_path,
)
if nested_result:
if nested_result["removed"]:
removed[key] = nested_result["removed"]
if nested_result["added"]:
added[key] = nested_result["added"]
else:
seq_diff = _sequence_element_diff(value1, value2, normalizers)
if seq_diff is not None:
rem, add = seq_diff
if rem:
removed[key] = rem
if add:
added[key] = add
else:
# Compare scalar values (with normalization)
norm1 = normalize_value(value1, normalizers)
norm2 = normalize_value(value2, normalizers)
if norm1 != norm2:
removed[key] = value1
added[key] = value2
# Check for keys only in dict2
for key in dict2:
if ignore_interface and key == "interfaces":
continue
if key not in dict1:
if dict2[key] in [None, [], {}, ""]:
pass # missing key is same as empty value
else:
# Key exists in dict2 but not in dict1
added[key] = dict2[key]
if removed or added:
return {"removed": removed, "added": added}
return None
def _recursive_merge_with_defaults(custom, defaults):
if isinstance(custom, dict) and isinstance(defaults, dict):
return_value = {}
for key in defaults:
if key in custom:
return_value[key] = _recursive_merge_with_defaults(
custom[key], defaults[key]
)
else:
return_value[key] = copy.deepcopy(defaults[key])
for key in custom:
if key not in defaults:
return_value[key] = copy.deepcopy(custom[key])
else:
return_value = copy.deepcopy(custom)
return return_value
def merge_with_defaults(custom, defaults):
"""
Merge default settings into custom settings.
This function merges default firewall configuration settings into custom
settings. For each top-level configuration type (zones, services, icmptypes,
helpers, ipsets, policies), if an item exists in defaults but not in custom,
it will be copied from defaults to custom.
Args:
custom: Dictionary with custom settings (will be modified in place)
defaults: Dictionary with default settings
Returns:
The modified custom dictionary with defaults merged in
"""
# Top-level keys that contain named items to merge
if not custom:
return defaults
if not defaults:
return custom
if not isinstance(defaults, dict) or not isinstance(custom, dict):
return copy.deepcopy(custom)
return _recursive_merge_with_defaults(custom, defaults)
def normalize_forward_ports(forward_ports):
fwd_ports_list_of_tuples = []
for fwd_port in forward_ports:
fwd_port_tuple = []
for ii in fwd_port:
if ii == "":
fwd_port_tuple.append(None)
else:
fwd_port_tuple.append(ii)
fwd_ports_list_of_tuples.append(tuple(fwd_port_tuple))
return fwd_ports_list_of_tuples
def fetch_online_settings(fw, setting_list, detailed=False):
"""
Fetch firewall runtime settings using the online FirewallClient API.
Args:
fw: FirewallClient object
setting_list: List of setting types to fetch (e.g., ['zones', 'services', 'icmptypes', 'helpers', 'ipsets', 'policies'])
detailed: If True, fetch detailed information for each object; if False, return only names
Returns:
Dictionary with setting types as keys and their data as values
"""
all_settings = {}
for setting_name in setting_list:
# Get list of items for this setting type using the runtime API
if setting_name == "zones":
setting_options = fw.getZones()
elif setting_name == "services":
setting_options = fw.listServices()
elif setting_name == "icmptypes":
setting_options = fw.listIcmpTypes()
elif setting_name == "helpers":
setting_options = fw.getHelpers()
elif setting_name == "ipsets":
setting_options = fw.getIPSets()
elif setting_name == "policies":
setting_options = fw.getPolicies()
else:
continue
if not detailed:
all_settings[setting_name] = setting_options
else:
settings = {}
for _item in setting_options:
element_settings = {}
if setting_name == "zones":
element = fw.getZoneSettings(_item)
try:
element_settings = element.getSettingsDict()
except AttributeError:
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["target"] = element.getTarget()
element_settings["services"] = element.getServices()
element_settings["ports"] = element.getPorts()
element_settings["icmp_blocks"] = element.getIcmpBlocks()
element_settings["masquerade"] = element.getMasquerade()
element_settings["forward_ports"] = element.getForwardPorts()
element_settings["interfaces"] = element.getInterfaces()
element_settings["sources"] = element.getSources()
element_settings["rules_str"] = element.getRichRules()
element_settings["protocols"] = element.getProtocols()
element_settings["source_ports"] = element.getSourcePorts()
element_settings["icmp_block_inversion"] = (
element.getIcmpBlockInversion()
)
# normalize the elements
if "forward_ports" in element_settings:
element_settings["forward_ports"] = normalize_forward_ports(
element_settings["forward_ports"]
)
elif setting_name == "services":
element = fw.getServiceSettings(_item)
try:
element_settings = element.getSettingsDict()
except AttributeError:
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["ports"] = element.getPorts()
element_settings["protocols"] = element.getProtocols()
element_settings["source_ports"] = element.getSourcePorts()
element_settings["destination"] = element.getDestinations()
element_settings["modules"] = element.getModules()
if hasattr(element, "getHelpers"):
element_settings["helpers"] = element.getHelpers()
elif setting_name == "icmptypes":
element = fw.getIcmpTypeSettings(_item)
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["destination"] = element.getDestinations()
elif setting_name == "helpers":
element = fw.getHelperSettings(_item)
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["family"] = element.getFamily()
element_settings["module"] = element.getModule()
element_settings["ports"] = element.getPorts()
elif setting_name == "ipsets":
element = fw.getIPSetSettings(_item)
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["options"] = element.getOptions()
element_settings["entries"] = element.getEntries()
element_settings["type"] = element.getType()
elif setting_name == "policies":
element = fw.getPolicySettings(_item)
element_settings = element.getSettingsDict()
settings[_item] = normalize_settings(element_settings)
all_settings[setting_name] = settings
return all_settings
def cvt_str(string_value):
if sys.version_info >= (3, 0):
return string_value
else:
return str(string_value)
def fetch_settings_from_xml_files(module, setting_list, defaults=False):
"""
Fetch firewall settings using FirewallConfig reader API to read XML files.
This function reads firewall configuration files directly using the
firewall.core.io reader functions (zone_reader, service_reader, etc.)
to parse XML configuration files.
Args:
module: Ansible module object (for error handling)
setting_list: List of setting types to fetch ('zones', 'services', etc.)
defaults: If True, read from USR_LIB_FIREWALLD (default configs);
if False, read from ETC_FIREWALLD (custom configs)
Returns:
Dictionary with setting types as keys and their data as values
"""
# Determine root directory based on defaults parameter
root_dir = cvt_str(
firewall.config.USR_LIB_FIREWALLD if defaults else firewall.config.ETC_FIREWALLD
)
all_settings = {}
for setting_name in setting_list:
# Construct the directory path for this setting type
setting_dir = cvt_str(os.path.join(root_dir, setting_name))
# Check if directory exists
if not os.path.exists(setting_dir) or not os.path.isdir(setting_dir):
continue
# Get list of XML files in this directory
try:
xml_files = [
cvt_str(f) for f in os.listdir(setting_dir) if f.endswith(".xml")
]
except OSError:
continue
if not xml_files:
continue
# Get detailed information for each item
settings = {}
for xml_file in xml_files:
item_name = xml_file[:-4] # Remove .xml extension
file_path = cvt_str(os.path.join(setting_dir, xml_file))
try:
# Read the configuration file using appropriate reader
if setting_name == "zones":
obj = zone_reader(xml_file, setting_dir)
elif setting_name == "services":
obj = service_reader(xml_file, setting_dir)
elif setting_name == "icmptypes":
obj = icmptype_reader(xml_file, setting_dir)
elif setting_name == "ipsets":
obj = ipset_reader(xml_file, setting_dir)
elif setting_name == "helpers":
obj = helper_reader(xml_file, setting_dir)
elif setting_name == "policies":
obj = policy_reader(xml_file, setting_dir)
else:
continue
# Extract and normalize settings from the object
settings[item_name] = normalize_settings(export_config_dict(obj))
except Exception as e:
# If we can't read a file, log warning and continue
module.warn(
"Failed to read "
+ setting_name
+ " configuration from "
+ file_path
+ ": "
+ str(e)
)
continue
if settings:
all_settings[setting_name] = settings
return all_settings
def fetch_settings_from_dir(directory, detailed=False, fw=None):
setting_options = [
_file[:-4] for _file in os.listdir(directory) if _file.endswith(".xml")
]
if not detailed:
return setting_options
else:
setting_name = os.path.basename(directory)
settings = {}
for _item in setting_options:
element_settings = {}
if setting_name == "zones":
element = fw.config().getZoneByName(_item).getSettings()
try:
element_settings = element.getSettingsDict()
except AttributeError:
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["target"] = element.getTarget()
element_settings["services"] = element.getServices()
element_settings["ports"] = element.getPorts()
element_settings["icmp_blocks"] = element.getIcmpBlocks()
element_settings["masquerade"] = element.getMasquerade()
element_settings["forward_ports"] = element.getForwardPorts()
element_settings["interfaces"] = element.getInterfaces()
element_settings["sources"] = element.getSources()
element_settings["rules_str"] = element.getRichRules()
element_settings["protocols"] = element.getProtocols()
element_settings["source_ports"] = element.getSourcePorts()
element_settings["icmp_block_inversion"] = (
element.getIcmpBlockInversion()
)
elif setting_name == "services":
element = fw.config().getServiceByName(_item).getSettings()
try:
element_settings = element.getSettingsDict()
except AttributeError:
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["protocols"] = element.getProtocols()
element_settings["source_ports"] = element.getSourcePorts()
element_settings["modules"] = element.getModules()
if hasattr(element, "getHelpers"):
element_settings["helpers"] = element.getHelpers()
elif setting_name == "icmptypes":
element = fw.config().getIcmpTypeByName(_item).getSettings()
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["destination"] = element.getDestinations()
elif setting_name == "helpers":
element = fw.config().getHelperByName(_item).getSettings()
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["family"] = element.getFamily()
element_settings["module"] = element.getModule()
element_settings["ports"] = element.getPorts()
elif setting_name == "ipsets":
element = fw.config().getIPSetByName(_item).getSettings()
element_settings["version"] = element.getVersion()
element_settings["short"] = element.getShort()
element_settings["description"] = element.getDescription()
element_settings["options"] = element.getOptions()
element_settings["entries"] = element.getEntries()
element_settings["type"] = element.getType()
elif setting_name == "policies":
element = fw.config().getPolicyByName(_item).getSettings()
element_settings = element.getSettingsDict()
settings[_item] = normalize_settings(element_settings)
return settings