# -*- coding: utf-8 -*- # # Copyright (C) 2016 - 2026 Red Hat, Inc. # Reusing some firewalld code # Authors: # Brennan Paciorek # 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 # # # 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 . 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